openfeel 1.2.4 → 1.3.1
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 -74
- package/README.md +39 -62
- package/README.zh-CN.md +40 -74
- 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/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 +400 -0
- package/dist/core/dsh-profile.js.map +1 -0
- package/dist/core/i18n-data/en.d.ts.map +1 -1
- package/dist/core/i18n-data/en.js +21 -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 +21 -2
- package/dist/core/i18n-data/zh-CN.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 +18 -9
- 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 +25 -7
- 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
package/dsh/lib/index.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenFeel × DSH — Feel 模式适配器入口
|
|
3
|
+
*
|
|
4
|
+
* 本插件是 **OpenFeel 的 DSH 适配版本**,不是独立实现:
|
|
5
|
+
* 它把 OpenFeel 的 CLI 与 `.openfeel/` 工作区投影到 DeepSeek Harness,
|
|
6
|
+
* 让 agent 与用户都能在 DSH 内使用 Feel 流程。
|
|
7
|
+
*
|
|
8
|
+
* 插件结构(与 OpenFeel 的适配器层约定一致):
|
|
9
|
+
* lib/index.js 插件入口(name / inject / apply)
|
|
10
|
+
* lib/adapters/cli.js CLI 执行器(进程、编码、退出码语义)
|
|
11
|
+
* lib/adapters/workspace.js 会话工作区 → OpenFeel 项目根
|
|
12
|
+
* lib/adapters/dashboard.js 状态快照与「下一步」决策
|
|
13
|
+
* lib/tools/index.js 13 个模型工具
|
|
14
|
+
* lib/surface/command.js `/feel` 人类命令
|
|
15
|
+
* lib/surface/prompt.js Feel 纪律系统提示段
|
|
16
|
+
*
|
|
17
|
+
* 设计约束:
|
|
18
|
+
* - **零运行时依赖**:只用 Node 内置模块,因此不依赖任何 DSH 内部包即可加载。
|
|
19
|
+
* - **单一权威**:所有状态变更与门禁判定都在 OpenFeel CLI 侧;本插件只做投影。
|
|
20
|
+
* - **拒绝即信息**:CLI 的门禁拒绝原样透传给模型,附可操作的纠正建议。
|
|
21
|
+
*
|
|
22
|
+
* @module openfeel-dsh
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { FeelCli } from './adapters/cli.js';
|
|
26
|
+
import { resolveWorkspace } from './adapters/workspace.js';
|
|
27
|
+
import { createFeelCommandHandler } from './surface/command.js';
|
|
28
|
+
import { FEEL_PROMPT_NAME, FEEL_PROMPT_ORDER, renderFeelPrompt } from './surface/prompt.js';
|
|
29
|
+
import { createTools } from './tools/index.js';
|
|
30
|
+
|
|
31
|
+
/** Loader 入口 id。 */
|
|
32
|
+
export const name = 'openfeel-dsh';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 硬依赖的宿主服务。
|
|
36
|
+
*
|
|
37
|
+
* 刻意保持最小:`tools` / `commands` / `systemPrompt` 是插件的三个落点。
|
|
38
|
+
* `skills` 等可选服务不在此列——缺失时插件仍然工作。
|
|
39
|
+
*/
|
|
40
|
+
export const inject = ['tools', 'commands', 'systemPrompt'];
|
|
41
|
+
|
|
42
|
+
/** 命令定义 id(与包名一致)。 */
|
|
43
|
+
const COMMAND_ID = '@openfeel/dsh/feel';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 归一化插件配置。
|
|
47
|
+
*
|
|
48
|
+
* 配置缺失时给出**安全默认值**:
|
|
49
|
+
* - `command: 'openfeel'`(走 PATH,安装后最常见)。
|
|
50
|
+
* - `autoPrompt: true`(启用即注入纪律段落)。
|
|
51
|
+
* - `log: false`(不写 `~/.openfeel/cli/logs/`,避免只读沙箱下失败)。
|
|
52
|
+
*
|
|
53
|
+
* @param {Record<string, any> | undefined} raw Loader 传入的配置。
|
|
54
|
+
* @returns {{ command: string, commandPrefix: string[], timeoutMs: number, log: boolean, autoPrompt: boolean, language: string, cwd: string | undefined }}
|
|
55
|
+
*/
|
|
56
|
+
export function normalizeConfig(raw) {
|
|
57
|
+
const source = typeof raw === 'object' && raw !== null ? raw : {};
|
|
58
|
+
const commandPrefix = Array.isArray(source.commandPrefix)
|
|
59
|
+
? source.commandPrefix.filter((item) => typeof item === 'string')
|
|
60
|
+
: [];
|
|
61
|
+
const timeoutMs = Number.isFinite(source.timeoutMs) && source.timeoutMs > 0 ? Number(source.timeoutMs) : 120_000;
|
|
62
|
+
return {
|
|
63
|
+
command: typeof source.command === 'string' && source.command !== '' ? source.command : 'openfeel',
|
|
64
|
+
commandPrefix,
|
|
65
|
+
timeoutMs,
|
|
66
|
+
log: source.log === true,
|
|
67
|
+
autoPrompt: source.autoPrompt !== false,
|
|
68
|
+
language: source.language === 'en' ? 'en' : 'zh-CN',
|
|
69
|
+
cwd: typeof source.cwd === 'string' && source.cwd !== '' ? source.cwd : undefined,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 取当前正在装配提示的会话 Agent。
|
|
75
|
+
*
|
|
76
|
+
* 提示装配期没有直接的 agent/cwd 入参,只能经 `agents.currentInitiator()`
|
|
77
|
+
* 沿进程内的驱动链反查——`dsh-tool-goal` 用的是同一手法。
|
|
78
|
+
* `agents` 未挂载或调用失败时返回 `undefined`(提示段据此放弃注入)。
|
|
79
|
+
*
|
|
80
|
+
* @param {object} ctx 插件上下文。
|
|
81
|
+
* @returns {object | undefined} 发起当前装配的 Agent。
|
|
82
|
+
*/
|
|
83
|
+
function currentAgent(ctx) {
|
|
84
|
+
try {
|
|
85
|
+
const agents = typeof ctx.get === 'function' ? ctx.get('agents') : undefined;
|
|
86
|
+
if (agents === undefined || typeof agents.currentInitiator !== 'function') return undefined;
|
|
87
|
+
return agents.currentInitiator();
|
|
88
|
+
} catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 插件挂载。
|
|
95
|
+
*
|
|
96
|
+
* @param {object} ctx Cordis 上下文(插件私域)。
|
|
97
|
+
* @param {Record<string, any>} [rawConfig] Loader 传入的配置。
|
|
98
|
+
*/
|
|
99
|
+
export function apply(ctx, rawConfig) {
|
|
100
|
+
const config = normalizeConfig(rawConfig);
|
|
101
|
+
const log = typeof ctx.logger === 'function'
|
|
102
|
+
? ctx.logger('openfeel-dsh')
|
|
103
|
+
: {
|
|
104
|
+
info: (...args) => console.log('[openfeel-dsh]', ...args),
|
|
105
|
+
warn: (...args) => console.warn('[openfeel-dsh]', ...args),
|
|
106
|
+
error: (...args) => console.error('[openfeel-dsh]', ...args),
|
|
107
|
+
debug: () => {},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const cli = new FeelCli({
|
|
111
|
+
command: config.command,
|
|
112
|
+
commandPrefix: config.commandPrefix,
|
|
113
|
+
timeoutMs: config.timeoutMs,
|
|
114
|
+
log: config.log,
|
|
115
|
+
cwd: config.cwd ?? process.cwd(),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/* ── 工具面 ─────────────────────────────────────────────── */
|
|
119
|
+
|
|
120
|
+
const tools = createTools({ cli });
|
|
121
|
+
for (const tool of tools) {
|
|
122
|
+
ctx.tools.register(tool);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* ── 人类命令面 ─────────────────────────────────────────── */
|
|
126
|
+
|
|
127
|
+
ctx.commands.register({
|
|
128
|
+
definitionId: COMMAND_ID,
|
|
129
|
+
name: 'feel',
|
|
130
|
+
description: 'Feel 模式(OpenFeel 流水线治理):状态 / 需求看板 / 相位推进 / 健康检查',
|
|
131
|
+
input: {
|
|
132
|
+
hint: '[status|next|overview|board|recover|health|stage|req|claim|advance|archive|kb|config]',
|
|
133
|
+
attachments: false,
|
|
134
|
+
},
|
|
135
|
+
handler: createFeelCommandHandler({ cli }),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
/* ── 系统提示面 ─────────────────────────────────────────── */
|
|
139
|
+
|
|
140
|
+
if (config.autoPrompt) {
|
|
141
|
+
ctx.systemPrompt.section({
|
|
142
|
+
name: FEEL_PROMPT_NAME,
|
|
143
|
+
order: FEEL_PROMPT_ORDER,
|
|
144
|
+
// 注意:PromptSection.text 的签名是 `(context: AssembleContext) => string`,
|
|
145
|
+
// 而 `AssembleContext = { scope?, signal? }` —— **不含 cwd**。
|
|
146
|
+
// 因此必须经 `agents.currentInitiator()` 反查会话,否则只能用宿主进程 cwd,
|
|
147
|
+
// 会把「宿主启动目录」误当会话工作区。
|
|
148
|
+
text: () => {
|
|
149
|
+
const agent = currentAgent(ctx);
|
|
150
|
+
const sessionCwd = agent?.session?.header?.cwd;
|
|
151
|
+
// 拿不到会话且没有显式 cwd 配置 → 不注入(宁可不打扰,也不给错的项目)。
|
|
152
|
+
if ((typeof sessionCwd !== 'string' || sessionCwd === '') && config.cwd === undefined) return '';
|
|
153
|
+
|
|
154
|
+
let resolved;
|
|
155
|
+
try {
|
|
156
|
+
resolved = resolveWorkspace(agent === undefined ? {} : { agent }, { fallback: config.cwd });
|
|
157
|
+
} catch {
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
160
|
+
// 不回溯:只有会话工作区**本身**是 OpenFeel 项目时才注入 Feel 纪律。
|
|
161
|
+
// 否则在任意目录开会话都会被灌流程提示(这正是曾经的误报来源)。
|
|
162
|
+
if (!resolved.initialized) return '';
|
|
163
|
+
return renderFeelPrompt({ projectRoot: resolved.projectRoot, detected: true, language: config.language });
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* ── 启动自检(非阻断) ─────────────────────────────────── */
|
|
169
|
+
|
|
170
|
+
void cli
|
|
171
|
+
.probeVersion()
|
|
172
|
+
.then((version) => {
|
|
173
|
+
if (version === undefined) {
|
|
174
|
+
log.warn(
|
|
175
|
+
`未检测到 OpenFeel CLI(命令 "${config.command}")。Feel 工具会返回可操作的安装指引;` +
|
|
176
|
+
'安装后执行 npm install -g openfeel 即可,无需重启宿主。',
|
|
177
|
+
);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
log.info(`已就绪:OpenFeel CLI ${version};注册 ${tools.length} 个工具、1 个 /feel 命令。`);
|
|
181
|
+
})
|
|
182
|
+
.catch(() => {
|
|
183
|
+
/* 自检失败不影响挂载 */
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
log.debug?.(`Feel 适配器已挂载:command=${config.command} timeoutMs=${config.timeoutMs} autoPrompt=${config.autoPrompt}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export { FeelCli, resolveWorkspace };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenFeel × DSH 适配器 — `/feel` 人类命令
|
|
3
|
+
*
|
|
4
|
+
* 面向**人**的命令面:直接转发到 OpenFeel CLI 并把输出回显到会话。
|
|
5
|
+
* 与工具面的区别:工具面给 agent 用结构化 JSON;命令面给人用 OpenFeel
|
|
6
|
+
* 原生的可读输出(看板、健康报告、阶段状态)。
|
|
7
|
+
*
|
|
8
|
+
* 门禁不在适配器里判:写操作一律交给 CLI,拒绝原因原样回显。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { FeelError } from '../adapters/cli.js';
|
|
12
|
+
import { resolveWorkspace } from '../adapters/workspace.js';
|
|
13
|
+
|
|
14
|
+
/** 命令用法(帮助文本)。 */
|
|
15
|
+
export const FEEL_USAGE = [
|
|
16
|
+
'Feel 模式(OpenFeel × DSH)— 用法:',
|
|
17
|
+
'',
|
|
18
|
+
' /feel 查看帮助',
|
|
19
|
+
' /feel status 流水线状态摘要',
|
|
20
|
+
' /feel next 下一步该谁做',
|
|
21
|
+
' /feel overview 全状态可视化',
|
|
22
|
+
' /feel board 需求看板(7 状态)',
|
|
23
|
+
' /feel recover 跨会话上下文恢复',
|
|
24
|
+
' /feel health [--fix] 健康检查(可回写 status.md 状态字段)',
|
|
25
|
+
' /feel stage [<id>] 阶段状态(省略 id 列出全部)',
|
|
26
|
+
' /feel req <id> 需求详情',
|
|
27
|
+
' /feel advance <stage> <to> 推进相位(受转移表 / done 门 / REV 阻塞约束)',
|
|
28
|
+
' /feel claim <REQ-NNN> 认领需求',
|
|
29
|
+
' /feel archive list 已归档版本',
|
|
30
|
+
' /feel kb <query> 检索知识库',
|
|
31
|
+
' /feel config 查看有效配置',
|
|
32
|
+
'',
|
|
33
|
+
'所有能力同样以工具形式提供给 agent(feel_status / feel_flow / feel_req / ...)。',
|
|
34
|
+
].join('\n');
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 简单分词(支持双引号包裹的参数)。
|
|
38
|
+
* @param {string} input
|
|
39
|
+
* @returns {string[]}
|
|
40
|
+
*/
|
|
41
|
+
export function tokenize(input) {
|
|
42
|
+
/** @type {string[]} */
|
|
43
|
+
const tokens = [];
|
|
44
|
+
let current = '';
|
|
45
|
+
let quote = null;
|
|
46
|
+
for (const ch of input) {
|
|
47
|
+
if (quote !== null) {
|
|
48
|
+
if (ch === quote) {
|
|
49
|
+
quote = null;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
current += ch;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (ch === '"' || ch === "'") {
|
|
56
|
+
quote = ch;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (/\s/.test(ch)) {
|
|
60
|
+
if (current !== '') {
|
|
61
|
+
tokens.push(current);
|
|
62
|
+
current = '';
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
current += ch;
|
|
67
|
+
}
|
|
68
|
+
if (current !== '') tokens.push(current);
|
|
69
|
+
return tokens;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 创建 `/feel` 命令的处理器。
|
|
74
|
+
*
|
|
75
|
+
* @param {object} context
|
|
76
|
+
* @param {import('../adapters/cli.js').FeelCli} context.cli
|
|
77
|
+
* @returns {(invocation: { agent: object, rawInput: string }) => Promise<{ kind: 'success'|'error', text: string }>}
|
|
78
|
+
*/
|
|
79
|
+
export function createFeelCommandHandler(context) {
|
|
80
|
+
const { cli } = context;
|
|
81
|
+
|
|
82
|
+
return async function handleFeelCommand(invocation) {
|
|
83
|
+
const tokens = tokenize(invocation.rawInput ?? '');
|
|
84
|
+
const sub = (tokens[0] ?? '').toLowerCase();
|
|
85
|
+
const rest = tokens.slice(1);
|
|
86
|
+
|
|
87
|
+
if (sub === '' || sub === 'help') {
|
|
88
|
+
return { kind: 'success', text: FEEL_USAGE };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @param {object} execLike */
|
|
92
|
+
const workspaceOf = (execLike) => {
|
|
93
|
+
const resolved = resolveWorkspace(execLike, { fallback: cli.cwd });
|
|
94
|
+
if (!resolved.initialized) {
|
|
95
|
+
throw new FeelError(
|
|
96
|
+
`当前工作区 ${resolved.projectRoot} 不是 OpenFeel 项目(缺少 .openfeel/)。请先执行 \`openfeel init\`。`,
|
|
97
|
+
{ kind: 'refusal', code: 'FEEL_NOT_INITIALIZED' },
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return resolved.projectRoot;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const execLike = { agent: invocation.agent };
|
|
104
|
+
let cwd;
|
|
105
|
+
try {
|
|
106
|
+
cwd = workspaceOf(execLike);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return { kind: 'error', text: messageOf(error) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** @param {string[]} args */
|
|
112
|
+
const runText = async (args) => {
|
|
113
|
+
const result = await cli.run(args, { cwd });
|
|
114
|
+
if (result.code !== 0) throw cli.errorFromResult(result, args);
|
|
115
|
+
return result.stdout;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
switch (sub) {
|
|
120
|
+
case 'status':
|
|
121
|
+
return { kind: 'success', text: await runText(['flow', 'status']) };
|
|
122
|
+
case 'overview':
|
|
123
|
+
return { kind: 'success', text: await runText(['flow', 'overview']) };
|
|
124
|
+
case 'board':
|
|
125
|
+
return { kind: 'success', text: await runText(['req', 'board']) };
|
|
126
|
+
case 'recover':
|
|
127
|
+
return { kind: 'success', text: await runText(['flow', 'recover']) };
|
|
128
|
+
case 'next': {
|
|
129
|
+
const next = await runText(['req', 'next']);
|
|
130
|
+
const flow = await cli.run(['flow', 'current'], { cwd });
|
|
131
|
+
const parts = [flow.stdout, '', '可接取需求:', next];
|
|
132
|
+
return { kind: 'success', text: parts.filter((part) => part.trim() !== '').join('\n') };
|
|
133
|
+
}
|
|
134
|
+
case 'health': {
|
|
135
|
+
const argv = ['flow', 'health', ...rest];
|
|
136
|
+
return { kind: 'success', text: await runText(argv) };
|
|
137
|
+
}
|
|
138
|
+
case 'stage': {
|
|
139
|
+
const argv = rest.length === 0 ? ['stage', 'status'] : ['stage', 'status', rest[0]];
|
|
140
|
+
return { kind: 'success', text: await runText(argv) };
|
|
141
|
+
}
|
|
142
|
+
case 'req': {
|
|
143
|
+
const id = rest[0];
|
|
144
|
+
if (id === undefined) return { kind: 'error', text: '用法:/feel req <REQ-NNN>\n\n' + FEEL_USAGE };
|
|
145
|
+
return { kind: 'success', text: await runText(['req', 'show', id]) };
|
|
146
|
+
}
|
|
147
|
+
case 'claim': {
|
|
148
|
+
const id = rest[0];
|
|
149
|
+
if (id === undefined) return { kind: 'error', text: '用法:/feel claim <REQ-NNN>' };
|
|
150
|
+
return { kind: 'success', text: await runText(['req', 'claim', id]) };
|
|
151
|
+
}
|
|
152
|
+
case 'advance': {
|
|
153
|
+
const [stage, to] = rest;
|
|
154
|
+
if (stage === undefined || to === undefined) {
|
|
155
|
+
return { kind: 'error', text: '用法:/feel advance <stage> <phase>\n\n合法相位见 /feel status 或 openfeel flow phases。' };
|
|
156
|
+
}
|
|
157
|
+
return { kind: 'success', text: await runText(['flow', 'advance', '--stage', stage, '--to', to]) };
|
|
158
|
+
}
|
|
159
|
+
case 'archive': {
|
|
160
|
+
const argv = rest[0] === 'list' ? ['flow', 'archive', 'list'] : ['flow', 'archive', 'list'];
|
|
161
|
+
return { kind: 'success', text: await runText(argv) };
|
|
162
|
+
}
|
|
163
|
+
case 'kb': {
|
|
164
|
+
const query = rest.join(' ').trim();
|
|
165
|
+
if (query === '') return { kind: 'error', text: '用法:/feel kb <关键词>' };
|
|
166
|
+
return { kind: 'success', text: await runText(['knowledge', 'search', query]) };
|
|
167
|
+
}
|
|
168
|
+
case 'config':
|
|
169
|
+
return { kind: 'success', text: await runText(['config', 'effective']) };
|
|
170
|
+
default:
|
|
171
|
+
return { kind: 'error', text: `未知子命令「${sub}」。\n\n${FEEL_USAGE}` };
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return { kind: 'error', text: messageOf(error) };
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 取错误消息。
|
|
181
|
+
* @param {unknown} error
|
|
182
|
+
* @returns {string}
|
|
183
|
+
*/
|
|
184
|
+
function messageOf(error) {
|
|
185
|
+
return error instanceof Error ? error.message : String(error);
|
|
186
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenFeel × DSH 适配器 — 系统提示段落
|
|
3
|
+
*
|
|
4
|
+
* 本段落只在 Feel 模式启用时注入,用来教会 agent **OpenFeel 的纪律**,
|
|
5
|
+
* 而不是重复工具说明。原则来自 OpenFeel 的 `feel` 角色定义与 AGENTS.md:
|
|
6
|
+
*
|
|
7
|
+
* - 调度者不是执行者:先判断「该谁做」。
|
|
8
|
+
* - `pipeline.phase` 只是宏观态;调度目标是 `phase != done` 的**活跃阶段**。
|
|
9
|
+
* - 禁止手改 `flow.json` / `status.md`;一切写入经 CLI,门禁才有权威。
|
|
10
|
+
* - 门禁拒绝是**预期结果**,据其纠正,不得绕过。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** 段落放置顺序:紧跟工具说明,位于长上下文之前。 */
|
|
14
|
+
export const FEEL_PROMPT_ORDER = 40;
|
|
15
|
+
|
|
16
|
+
/** 段落名(稳定标识;同层重名会抛错)。 */
|
|
17
|
+
export const FEEL_PROMPT_NAME = 'feel-mode';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 生成系统提示段落正文。
|
|
21
|
+
*
|
|
22
|
+
* @param {object} [options]
|
|
23
|
+
* @param {string} [options.projectRoot] 当前项目根(用于指路)。
|
|
24
|
+
* @param {boolean} [options.detected] 是否检测到 .openfeel/。
|
|
25
|
+
* @param {string} [options.language] 'zh-CN' | 'en'。
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
export function renderFeelPrompt(options = {}) {
|
|
29
|
+
const language = options.language ?? 'zh-CN';
|
|
30
|
+
if (language === 'en') return renderEnglish(options);
|
|
31
|
+
return renderChinese(options);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 中文段落。
|
|
36
|
+
* @param {{ projectRoot?: string, detected?: boolean }} options
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
function renderChinese(options) {
|
|
40
|
+
const where = options.detected === true && options.projectRoot !== undefined
|
|
41
|
+
? `当前项目:\`${options.projectRoot}\`(已初始化 \`.openfeel/\`)。`
|
|
42
|
+
: '当前工作区尚未检测到 `.openfeel/`:若用户要求使用 Feel 流程,先提示运行 `openfeel init`。';
|
|
43
|
+
|
|
44
|
+
return `<feel_mode>
|
|
45
|
+
Feel 模式已启用:本会话在 OpenFeel 流程治理下工作。${where}
|
|
46
|
+
|
|
47
|
+
## 你的角色:调度者,不是执行者
|
|
48
|
+
你的价值是判断「该谁做」,而不是自己做。OpenFeel 的 15 个相位各有负责 Agent:
|
|
49
|
+
planner(计划)/ schemer(方案)/ executor(执行)/ reviewer(审查)/ feel-tester(测试)/ archiver(归档)。
|
|
50
|
+
**具体映射由 \`flow next --json\` 权威给出**,本适配器不维护本地映射。
|
|
51
|
+
Feel(你)只做编排、决策与门禁判断。
|
|
52
|
+
|
|
53
|
+
## 每次动手前
|
|
54
|
+
1. 调 \`feel_status\` 读快照:宏观相位、**每个阶段的相位**、当前模式与来源、该阶段在此模式下的**合法下一相位**、负责 Agent。
|
|
55
|
+
2. 调 \`feel_next\` 拿「下一步该谁做」的明确建议(含合法下一相位、推荐工具调用、OpenFeel 交接标记)。
|
|
56
|
+
3. 按建议派发(DSH 用 \`subagent\` / \`workflow\` 工具),或经 \`feel_flow action=advance\` 推进。
|
|
57
|
+
|
|
58
|
+
## 不可违背的纪律
|
|
59
|
+
- **调度目标是活跃阶段**:\`pipeline.phase\` 只是宏观态(active/paused/done)。多阶段并行时,取 \`phase != done\` 的阶段为调度目标,按依赖与优先级择一推进。
|
|
60
|
+
- **禁止手改 \`flow.json\`**:一切经 \`feel_flow\`(底层 \`openfeel flow\`)。手改会破坏 revision 乐观并发、审计日志与跨文件一致性。
|
|
61
|
+
- **禁止手改 \`status.md\`**:经 \`feel_stage action=set\` / \`action=task\`(底层 \`openfeel stage\`)。
|
|
62
|
+
- **门禁拒绝是预期结果**:相位不合法、done 门未过、blocking REV 未关,都是流程在保护你。读拒绝原因 → 纠正 → 重试;**不要**用 \`force\` 绕过(\`--force\` 也只能跳过相位合法性,**不能**绕过 blocking REV 门与版本归档判据)。
|
|
63
|
+
- **退出码 2 = 并发冲突,可重试**:先重读状态(\`feel_status\` 或 \`feel_verify action=recover\`)再重试。
|
|
64
|
+
- **错误分类看 JSON envelope**:\`--json\` 失败的 \`error.kind\` / \`error.code\` / \`error.detail\` 是权威;退出码 3 = 内部故障。
|
|
65
|
+
- **manual 模式先问**:当 \`auto_advance=disabled\` 时,推进到下一相位前必须询问用户,禁止静默推进。
|
|
66
|
+
- **归档判据不可协商**:版本归档要求该版本**全部阶段 done** 且**无 blocking open REV**,没有 force 覆盖。归档由 CLI 执行,调度侧只提示不代劳。
|
|
67
|
+
- **不主动读归档**:常规调度不要读 \`.openfeel/flow-archive/**\`(防上下文污染);需要历史时用 \`feel_archive action=show\`。
|
|
68
|
+
|
|
69
|
+
## 三种工作流模式
|
|
70
|
+
- \`full\`:plan → plan_review → scheme → exec → review → test → archive,六道关全走。
|
|
71
|
+
- \`fast\`:跳过 \`scheme_*\` 与 \`test_*\`;**计划审查与代码审查各保留一轮**(fast 不等于免审)。
|
|
72
|
+
- \`general\`:无流水线(空转移表),按需直接调度通用工作者;**仅需求级合法**,阶段级/部署级会拒绝。
|
|
73
|
+
|
|
74
|
+
## 需求分发
|
|
75
|
+
需求是排期层(\`REQ-NNN\`,7 状态:draft/scheduled/in_progress/done/blocked/cancelled/rejected)。
|
|
76
|
+
接取流程:\`feel_req action=next\` → \`claim\` → \`link\` 到阶段 → 阶段推进驱动聚合 → 全部关联阶段 done 时需求自动收口。
|
|
77
|
+
\`done\` 门:非 general 需求必须**全部关联阶段 done**;general 需求在零关联阶段时走双完成路径。
|
|
78
|
+
|
|
79
|
+
## 收尾
|
|
80
|
+
多步骤任务(≥3 步)开始即建 todo 列表并持续更新。阶段推进与需求流转后,向用户汇报「做了什么 / 卡在哪 / 建议下一步」。
|
|
81
|
+
</feel_mode>`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 英文段落。
|
|
86
|
+
* @param {{ projectRoot?: string, detected?: boolean }} options
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
89
|
+
function renderEnglish(options) {
|
|
90
|
+
const where = options.detected === true && options.projectRoot !== undefined
|
|
91
|
+
? `Project: \`${options.projectRoot}\` (initialized \`.openfeel/\`).`
|
|
92
|
+
: 'No `.openfeel/` detected in this workspace: if the user asks for the Feel process, suggest `openfeel init` first.';
|
|
93
|
+
|
|
94
|
+
return `<feel_mode>
|
|
95
|
+
Feel mode is active: this session works under OpenFeel process governance. ${where}
|
|
96
|
+
|
|
97
|
+
## Your role: orchestrator, not implementer
|
|
98
|
+
Your value is deciding WHO acts, not acting yourself. The 15 pipeline phases each have an owning agent:
|
|
99
|
+
planner / schemer / executor / reviewer / feel-tester / archiver. **The authoritative mapping comes from
|
|
100
|
+
\`flow next --json\`**; this adapter keeps no local copy. You (Feel) only orchestrate, decide, and read gates.
|
|
101
|
+
|
|
102
|
+
## Before acting
|
|
103
|
+
1. Call \`feel_status\` for the snapshot: macro phase, **each stage's phase**, current mode and its source, the **legal next phases** for that stage under its mode, and the responsible agent.
|
|
104
|
+
2. Call \`feel_next\` for an explicit "who does what next" recommendation (legal next phases, suggested tool call, OpenFeel handoff marker).
|
|
105
|
+
3. Dispatch accordingly (DSH \`subagent\` / \`workflow\`), or advance with \`feel_flow action=advance\`.
|
|
106
|
+
|
|
107
|
+
## Non-negotiable rules
|
|
108
|
+
- **The schedule target is an ACTIVE stage**: \`pipeline.phase\` is only macro state (active/paused/done). With parallel stages, pick a stage whose \`phase != done\` and advance one at a time following deps and priority.
|
|
109
|
+
- **Never hand-edit \`flow.json\`**: go through \`feel_flow\` (the \`openfeel flow\` CLI). Hand edits break revision CAS, the audit log, and cross-file consistency.
|
|
110
|
+
- **Never hand-edit \`status.md\`**: use \`feel_stage action=set\` / \`action=task\`.
|
|
111
|
+
- **Gate refusals are expected**: an illegal phase, an unmet done gate, or an open blocking REV is the process protecting you. Read the reason, correct, retry. Do **not** reach for \`force\`: \`--force\` only skips phase legality; it cannot bypass the blocking-REV gate or the version-archive criteria.
|
|
112
|
+
- **Exit code 2 = concurrent revision conflict, retryable**: re-read state (\`feel_status\` or \`feel_verify action=recover\`), then retry.
|
|
113
|
+
- **Classify failures from the JSON envelope**: \`--json\` failures carry \`error.kind\` / \`error.code\` / \`error.detail\`; exit code 3 = internal error.
|
|
114
|
+
- **Ask before advancing under manual mode**: when \`auto_advance=disabled\`, ask the user before advancing; never advance silently.
|
|
115
|
+
- **Archive criteria are not negotiable**: a version archive requires every stage of that version to be \`done\` AND no blocking open REV; there is no force override. The CLI performs the archive; you only prompt.
|
|
116
|
+
- **Do not read archives during normal scheduling**: avoid \`.openfeel/flow-archive/**\`; use \`feel_archive action=show\` on demand.
|
|
117
|
+
|
|
118
|
+
## The three workflow modes
|
|
119
|
+
- \`full\`: plan → plan_review → scheme → exec → review → test → archive (all six gates).
|
|
120
|
+
- \`fast\`: skips \`scheme_*\` and \`test_*\`; **plan review and code review each keep one round** (fast is not review-free).
|
|
121
|
+
- \`general\`: no pipeline (empty transition table), dispatch general workers on demand; requirement-level only.
|
|
122
|
+
|
|
123
|
+
## Requirement distribution
|
|
124
|
+
Requirements are the scheduling layer (\`REQ-NNN\`, 7 statuses: draft/scheduled/in_progress/done/blocked/cancelled/rejected).
|
|
125
|
+
Flow: \`feel_req action=next\` → \`claim\` → \`link\` to a stage → stage advancement drives aggregation → auto-close when every linked stage is done.
|
|
126
|
+
The \`done\` gate: a non-general requirement needs ALL linked stages done; a general requirement with zero linked stages uses the dual completion path.
|
|
127
|
+
|
|
128
|
+
## Wrap-up
|
|
129
|
+
Create a todo list at the start of any multi-step task (≥3 steps) and keep it current. After advancing a phase or requirement, report what changed, what blocked, and the suggested next step.
|
|
130
|
+
</feel_mode>`;
|
|
131
|
+
}
|