agentic-flow-kit 2.0.0 → 2.1.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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +92 -23
  3. package/bin/flow-kit.mjs +3 -1
  4. package/locales/en/installer.json +87 -2
  5. package/locales/en/overrides/.codebuddy/skills/tfs-pr/SKILL.md +1 -1
  6. package/locales/en/overrides/workflow/README.md +2 -0
  7. package/locales/zh-CN/installer.json +87 -2
  8. package/package.json +2 -2
  9. package/schemas/install-manifest.schema.json +3 -1
  10. package/schemas/project-profile.schema.json +1 -1
  11. package/src/cli/args.mjs +3 -1
  12. package/src/cli/main.mjs +9 -2
  13. package/src/commands/doctor.mjs +50 -4
  14. package/src/commands/install.mjs +160 -13
  15. package/src/hosts/registry.mjs +154 -10
  16. package/src/i18n.mjs +3 -0
  17. package/src/install/apply.mjs +17 -3
  18. package/src/install/frontmatter.mjs +84 -0
  19. package/src/install/plan.mjs +174 -16
  20. package/src/models/known.mjs +13 -0
  21. package/src/models/probe.mjs +163 -0
  22. package/src/models/resolve.mjs +312 -0
  23. package/src/render/tokens.mjs +56 -9
  24. package/src/tui/ansi.mjs +55 -0
  25. package/src/tui/flows.mjs +306 -0
  26. package/src/tui/prompts.mjs +139 -0
  27. package/src/tui/screen.mjs +217 -0
  28. package/src/tui/widgets.mjs +111 -0
  29. package/src/tui/wizard.mjs +68 -0
  30. package/templates/engine/.agents/commands/new-task.md +2 -2
  31. package/templates/engine/.agents/commands/pipeline-run.md +5 -1
  32. package/templates/engine/.agents/commands/stages.md +2 -1
  33. package/templates/engine/.agents/config/agent-eval.json +17 -0
  34. package/templates/engine/.agents/config/mechanism-registry.md +2 -1
  35. package/templates/engine/.agents/config/memory-tiers.json +13 -0
  36. package/templates/engine/.agents/config/pipeline.json +36 -0
  37. package/templates/engine/.agents/config/steering.json +28 -0
  38. package/templates/engine/.agents/evals/README.md +49 -0
  39. package/templates/engine/.agents/evals/tasks.tsv +6 -0
  40. package/templates/engine/.agents/hooks/pre-write-check.cjs +27 -0
  41. package/templates/engine/.agents/notes/injected-memory.md +14 -0
  42. package/templates/engine/.agents/notes/steering-tiers.md +28 -0
  43. package/templates/engine/.agents/scripts/agent-eval.mjs +268 -0
  44. package/templates/engine/.agents/scripts/check-pipeline-state.mjs +85 -0
  45. package/templates/engine/.agents/scripts/claims.mjs +166 -0
  46. package/templates/engine/.agents/scripts/dream-run.mjs +78 -3
  47. package/templates/engine/.agents/scripts/fix-task.mjs +77 -0
  48. package/templates/engine/.agents/scripts/memory-tier.mjs +196 -0
  49. package/templates/engine/.agents/scripts/memory-wiki.mjs +296 -0
  50. package/templates/engine/.agents/scripts/outcome-metrics.mjs +195 -0
  51. package/templates/engine/.agents/scripts/path-lease.mjs +177 -0
  52. package/templates/engine/.agents/scripts/session-log.mjs +157 -0
  53. package/templates/engine/.agents/scripts/steering.mjs +75 -0
  54. package/templates/engine/.agents/scripts/verify.mjs +16 -5
  55. package/templates/engine/.agents/skills/arena/SKILL.md +4 -0
  56. package/templates/engine/.agents/skills/dream/SKILL.md +21 -2
  57. package/templates/engine/.agents/skills/eval/SKILL.md +16 -0
  58. package/templates/engine/.agents/skills/model-upgrade/SKILL.md +41 -0
  59. package/templates/engine/.agents/skills/session-resume/SKILL.md +7 -0
  60. package/templates/engine/.agents/skills/show-me-your-work/SKILL.md +14 -0
  61. package/templates/engine/.agents/skills/wiki/SKILL.md +4 -1
  62. package/templates/git/.gitattributes +1 -0
  63. package/templates/git/.githooks/package.json +3 -0
  64. package/templates/git/.github/workflows/ci.yml +32 -0
  65. package/templates/git/azure-pipelines.yml +46 -0
  66. package/templates/host-meta/SKILLS.md +3 -0
  67. package/templates/host-meta/claude/skill-pointer.md +6 -0
  68. package/templates/host-meta/cline/skill-index.md +6 -0
  69. package/templates/host-meta/codebuddy/skill-pointer.md +6 -0
  70. package/templates/host-meta/commandcode/skill-index.md +5 -0
  71. package/templates/host-meta/opencode/skill-pointer.md +6 -0
  72. package/templates/host-meta/qoder/skill-index.md +5 -0
  73. package/templates/host-meta/qwen/skill-index.md +5 -0
  74. package/templates/host-meta/trae/skill-index.md +6 -0
  75. package/templates/hosts/claude/settings.hardened.json +94 -0
  76. package/templates/hosts/claude/settings.json +1 -3
  77. package/templates/hosts/commandcode/settings.json +1 -3
  78. package/templates/hosts/qoder/settings.json +1 -3
  79. package/templates/hosts/qwen/settings.json +1 -3
  80. package/templates/profiles/go/AGENTS.md +52 -0
  81. package/templates/profiles/java/AGENTS.md +52 -0
  82. package/templates/profiles/rust/AGENTS.md +52 -0
  83. package/templates/profiles/typescript/AGENTS.md +52 -0
  84. package/templates/scaffold/workflow/INDEX.md +16 -0
  85. package/templates/scaffold/workflow/README.md +2 -0
  86. package/templates/scaffold/workflow/artifact-sources.md +25 -0
  87. package/templates/scaffold/workflow/non-engineer-intent.md +51 -0
@@ -3,8 +3,9 @@ import path from 'node:path';
3
3
  import { spawnSync } from 'node:child_process';
4
4
  import { readManifest } from '../install/manifest.mjs';
5
5
  import { doctorGit } from '../git/config.mjs';
6
- import { HOSTS } from '../hosts/registry.mjs';
6
+ import { HOSTS, hostTargetRel } from '../hosts/registry.mjs';
7
7
  import { listFiles, TEMPLATES } from '../install/plan.mjs';
8
+ import { ROLE_NAMES, readInjectedModels, readLocalModelRoles } from '../models/resolve.mjs';
8
9
  import { resolveTarget } from './install.mjs';
9
10
 
10
11
  const HOOK_FILES = ['pre-commit', 'commit-msg', 'post-commit', 'pre-merge-commit', 'pre-push'];
@@ -12,7 +13,8 @@ const HOOK_FILES = ['pre-commit', 'commit-msg', 'post-commit', 'pre-merge-commit
12
13
  export function doctorCommand({ args, t }) {
13
14
  const target = resolveTarget(args);
14
15
  const checks = [];
15
- const add = (ok, name, detail, fix) => checks.push({ ok, name, detail, fix });
16
+ const add = (ok, name, detail, fix, extra = {}) => checks.push({ ok, name, detail, fix, ...extra });
17
+ const roleVal = (entry) => (typeof entry === 'string' ? entry : entry?.model || null);
16
18
 
17
19
  const manifest = readManifest(target);
18
20
  if (!manifest) {
@@ -47,9 +49,52 @@ export function doctorCommand({ args, t }) {
47
49
  continue;
48
50
  }
49
51
  for (const rel of listFiles(path.join(TEMPLATES, 'hosts', host))) {
50
- const targetRel = host === 'claude' && rel === 'CLAUDE.md' ? 'CLAUDE.md' : `${h.dir}/${rel}`;
52
+ if (rel === 'settings.hardened.json') continue; // 替换源,不独立安装
53
+ const targetRel = hostTargetRel(host, rel);
51
54
  if (!fs.existsSync(path.join(target, targetRel))) add(false, `host ${host}: ${targetRel}`, t('doctor.fileMissing'));
52
55
  }
56
+ // 能力缺口:没有 hooks / modelField 的宿主,相关保证只能是 advisory。
57
+ if (!h.hooks) add(true, `guarantee ${host}`, t('doctor.guaranteeGap', { host, capability: 'hooks' }), '', { warn: true });
58
+ if (h.agents && !h.agents.modelField) add(true, `guarantee ${host}`, t('doctor.guaranteeGap', { host, capability: 'model injection' }), '', { warn: true });
59
+ }
60
+
61
+ // 安装档位与交付面:lean 允许没有 CI;standard/enterprise 应有可运行 CI。
62
+ const tier = manifest?.tier || 'standard';
63
+ add(true, 'tier', t('doctor.tier', { tier }));
64
+ if (tier !== 'lean') {
65
+ const ci = ['azure-pipelines.yml', '.github/workflows/ci.yml'].filter((rel) => fs.existsSync(path.join(target, rel)));
66
+ if (ci.length) add(true, 'ci', ci.join(', '));
67
+ else add(true, 'ci', t('doctor.ciMissing', { paths: 'azure-pipelines.yml / .github/workflows/ci.yml' }), '', { warn: true });
68
+ }
69
+ if (manifest?.hardened) add(true, 'hardened', t('doctor.hardenedHint'), '', { warn: true });
70
+
71
+ // 模型配置是用户本地状态:存在则校验,缺失不失败(旧安装保持 doctor 全过)。
72
+ const localModels = readLocalModelRoles(target);
73
+ if (localModels.error) {
74
+ add(false, 'model-roles.local', t('doctor.modelConfigInvalid'));
75
+ } else if (localModels.exists) {
76
+ add(true, 'model-roles.local', path.relative(target, localModels.file) || localModels.file);
77
+ const value = (entry) => (typeof entry === 'string' ? entry : entry?.model);
78
+ const code = value(localModels.roles.code);
79
+ const review = value(localModels.roles.review);
80
+ if (code && review && code === review) {
81
+ add(true, 'models heterogeneous', t('doctor.reviewSameAsCode'));
82
+ }
83
+ for (const role of ROLE_NAMES) {
84
+ if (value(localModels.roles[role]) == null) continue;
85
+ add(true, `model ${role}`, String(value(localModels.roles[role])));
86
+ }
87
+ // 模型漂移:本地配置 vs 已安装子代理的 model 字段。升级前必须先评测(model-upgrade 技能)。
88
+ const injected = readInjectedModels({ target, hosts: manifest?.hosts || [] });
89
+ for (const host of manifest?.hosts || []) {
90
+ for (const role of ROLE_NAMES) {
91
+ const local = localModels.hosts?.[host]?.[role] ?? roleVal(localModels.roles[role]);
92
+ const installed = injected[host]?.[role];
93
+ if (local != null && installed != null && String(local) !== String(installed)) {
94
+ add(true, `drift ${host}/${role}`, t('doctor.modelDrift', { host, role, installed, local }), '', { warn: true });
95
+ }
96
+ }
97
+ }
53
98
  }
54
99
 
55
100
  const nodeMajor = Number(process.versions.node.split('.')[0]);
@@ -58,7 +103,8 @@ export function doctorCommand({ args, t }) {
58
103
  let failed = 0;
59
104
  for (const c of checks) {
60
105
  if (c.ok) {
61
- console.log(`✓ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`);
106
+ const mark = c.warn ? '!' : '✓';
107
+ console.log(`${mark} ${c.name}${c.detail ? ` — ${c.detail}` : ''}`);
62
108
  } else {
63
109
  failed += 1;
64
110
  console.log(`✗ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`);
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { spawnSync } from 'node:child_process';
4
5
  import { flag } from '../cli/args.mjs';
5
6
  import { buildPlan } from '../install/plan.mjs';
6
7
  import { applyPlan, summaryLine } from '../install/apply.mjs';
@@ -8,6 +9,9 @@ import { emptyManifest, readManifest, writeManifest } from '../install/manifest.
8
9
  import { buildVars } from '../render/tokens.mjs';
9
10
  import { HOST_NAMES, resolveHost } from '../hosts/registry.mjs';
10
11
  import { configureGit, isGitRepo } from '../git/config.mjs';
12
+ import {
13
+ parseModelFlags, readInjectedModels, resolveModels, summarizeModels, writeLocalModelRoles,
14
+ } from '../models/resolve.mjs';
11
15
 
12
16
  export function readKitVersion() {
13
17
  try {
@@ -52,30 +56,117 @@ export function resolveHosts(args, t, fallback = []) {
52
56
  return out;
53
57
  }
54
58
 
55
- export function detectStack(target) {
59
+ // 根目录标记,先匹配者胜出。package.json 放在最后,避免吞掉 Cargo.toml / go.mod / pom.xml / tsconfig.json。
60
+ // label 是菜单上的典型标记;match 返回实际命中的文件名。
61
+ const STACK_MARKERS = [
62
+ {
63
+ stack: 'dotnet',
64
+ label: '.sln / .csproj',
65
+ match: (names) => names.find((n) => n.endsWith('.sln')) || names.find((n) => n.endsWith('.csproj')) || null,
66
+ },
67
+ { stack: 'rust', label: 'Cargo.toml', match: (names) => (names.includes('Cargo.toml') ? 'Cargo.toml' : null) },
68
+ { stack: 'go', label: 'go.mod', match: (names) => (names.includes('go.mod') ? 'go.mod' : null) },
69
+ {
70
+ stack: 'java',
71
+ label: 'pom.xml / build.gradle',
72
+ match: (names) => ['pom.xml', 'build.gradle', 'build.gradle.kts'].find((n) => names.includes(n)) || null,
73
+ },
74
+ { stack: 'typescript', label: 'tsconfig.json', match: (names) => (names.includes('tsconfig.json') ? 'tsconfig.json' : null) },
75
+ { stack: 'node', label: 'package.json', match: (names) => (names.includes('package.json') ? 'package.json' : null) },
76
+ ];
77
+
78
+ export function stackMarkerLabel(stack) {
79
+ return STACK_MARKERS.find((marker) => marker.stack === stack)?.label || '';
80
+ }
81
+
82
+ export function matchStack(names) {
83
+ const list = Array.isArray(names) ? names : [];
84
+ for (const marker of STACK_MARKERS) {
85
+ const file = marker.match(list);
86
+ if (file) return { stack: marker.stack, file };
87
+ }
88
+ return { stack: 'empty', file: null };
89
+ }
90
+
91
+ export function detectStackDetail(target) {
56
92
  let top = [];
57
93
  try {
58
94
  top = fs.readdirSync(target);
59
95
  } catch {
60
- return 'empty';
96
+ return { stack: 'empty', file: null };
61
97
  }
62
- if (top.some((n) => n.endsWith('.sln') || n.endsWith('.csproj'))) return 'dotnet';
63
- if (top.includes('package.json')) return 'node';
64
- return 'empty';
98
+ return matchStack(top);
99
+ }
100
+
101
+ export function detectStack(target) {
102
+ return detectStackDetail(target).stack;
65
103
  }
66
104
 
105
+ // 可选技术栈单一来源:TUI 向导栈菜单由它生成
106
+ export const STACKS = ['dotnet', 'node', 'typescript', 'rust', 'go', 'java', 'empty'];
107
+
67
108
  export function resolveStack(args, target) {
68
109
  const stack = String(args.stack || args.profile || detectStack(target));
69
110
  const abs = fileURLToPath(new URL(`../../templates/profiles/${stack}/`, import.meta.url));
70
111
  return fs.existsSync(abs) ? stack : 'empty';
71
112
  }
72
113
 
73
- export function installCommand({ args, t, lang }) {
114
+ export function installCommand({ args, t, lang, onLog }) {
74
115
  const hosts = resolveHosts(args, t);
75
- return runInstall({ args, t, lang, hosts, commandLabel: 'install' });
116
+ return runInstall({ args, t, lang, hosts, commandLabel: 'install', onLog });
117
+ }
118
+
119
+ const ACTION_MARK = { create: '+', update: '~', skip: '-', conflict: '!', merge: '~' };
120
+
121
+ export function formatInstallLog(action, msg) {
122
+ return `${ACTION_MARK[action] || ' '} ${msg}`;
123
+ }
124
+
125
+ // 「已是最新」在升级时会占绝大多数,进度条已能表达。冲突和其余跳过必须留下原文。
126
+ export function installLogNotable(action, msg) {
127
+ if (action === 'conflict') return true;
128
+ if (action === 'skip') return !String(msg).includes('已是最新');
129
+ return false;
130
+ }
131
+
132
+ // CLI 缺省进度行:action 前缀 + 消息(TUI 经 onLog 接管渲染)
133
+ function logLine(action, msg) {
134
+ console.log(formatInstallLog(action, msg));
135
+ }
136
+
137
+ function normalizeModelMode(value) {
138
+ const v = String(value || 'recommend').toLowerCase();
139
+ return ['recommend', 'detect', 'inherit'].includes(v) ? v : 'recommend';
140
+ }
141
+
142
+ // 派生工件(索引/引擎地图)从单源现场取数生成,不能静态随模板漂移。
143
+ // 它们不进 manifest:属于生成区,升级时重新生成即可。
144
+ function generateDerivedArtifacts(target) {
145
+ for (const script of ['gen-workflow-index.mjs', 'gen-engine-map.mjs']) {
146
+ const abs = path.join(target, '.agents', 'scripts', script);
147
+ if (!fs.existsSync(abs)) continue;
148
+ try {
149
+ spawnSync(process.execPath, [abs], {
150
+ cwd: target, stdio: 'ignore', windowsHide: true, timeout: 30000,
151
+ });
152
+ } catch { /* 派生工件失败不阻断安装;verify/dream 会报漂移 */ }
153
+ }
76
154
  }
77
155
 
78
- export function runInstall({ args, t, lang, hosts, planHosts = hosts, categories, commandLabel, stackOverride }) {
156
+ const TIERS = ['lean', 'standard', 'enterprise'];
157
+ function normalizeTier(value, t) {
158
+ const v = String(value || 'standard').toLowerCase();
159
+ if (!TIERS.includes(v)) {
160
+ const err = new Error(t('error.badTier', { value: value || '' }));
161
+ err.code = 'BAD_TIER';
162
+ throw err;
163
+ }
164
+ return v;
165
+ }
166
+
167
+ // onLog 回调契约:(action, msg, done, total) —— action ∈ create/update/skip/conflict/merge,
168
+ // done/total 为已完成/总文件数,供 TUI 渲染 spinner 进度;缺省走 CLI 前缀行。
169
+ export function runInstall({ args, t, lang, hosts, planHosts = hosts, categories, commandLabel, stackOverride, onLog = logLine }) {
79
170
  const target = resolveTarget(args);
80
171
  const dryRun = flag(args, 'dry-run');
81
172
  const force = flag(args, 'force');
@@ -84,8 +175,49 @@ export function runInstall({ args, t, lang, hosts, planHosts = hosts, categories
84
175
  const previous = readManifest(target);
85
176
  const hostSet = [...new Set([...(previous?.hosts || []), ...hosts])];
86
177
  const trackHosts = flag(args, 'track-hosts');
178
+ // 档位:CLI > --hardened(=enterprise)> 既有 manifest > standard。
179
+ // 档位只影响交付面(CI / 加固),不削弱引擎门禁。
180
+ const tier = normalizeTier(
181
+ args.tier || (flag(args, 'hardened') ? 'enterprise' : null) || previous?.tier || 'standard',
182
+ t,
183
+ );
184
+ const hardened = flag(args, 'hardened') || tier === 'enterprise' || Boolean(previous?.hardened && !args.tier);
87
185
 
88
- const plan = buildPlan({ target, hosts: planHosts, lang, stack, projectName, categories });
186
+ let modelFlags;
187
+ try {
188
+ modelFlags = parseModelFlags(args.models || []);
189
+ } catch (err) {
190
+ if (err.code !== 'BAD_MODEL') throw err;
191
+ const e = new Error(t('error.badModel', { value: err.value || '' }));
192
+ e.code = 'BAD_MODEL';
193
+ throw e;
194
+ }
195
+ const tuiModels = args.tuiModels || null;
196
+ const explicitModels = Object.keys(modelFlags.global).length > 0
197
+ || Object.keys(modelFlags.hosts).length > 0
198
+ || Boolean(tuiModels && Object.keys(tuiModels).length);
199
+ const modelMode = normalizeModelMode(args['model-mode']);
200
+ // 升级默认不重新探测:以本地 model-roles.local.json 为准,保证 agent 文件哈希稳定。
201
+ const shouldProbe = !previous || commandLabel === 'add-host' || explicitModels || modelMode === 'detect' || flag(args, 'probe-models');
202
+ const previousModels = commandLabel === 'upgrade'
203
+ ? readInjectedModels({ target, hosts: planHosts })
204
+ : {};
205
+ const models = flag(args, 'no-model-config') ? null : resolveModels({
206
+ hosts: planHosts,
207
+ target,
208
+ cli: modelFlags,
209
+ tui: tuiModels,
210
+ previous: previousModels,
211
+ mode: modelMode,
212
+ probe: shouldProbe,
213
+ allowCli: flag(args, 'probe-models'),
214
+ });
215
+
216
+ const plan = buildPlan({
217
+ target, hosts: planHosts, lang, stack, projectName, categories, models: models?.hosts, tier, hardened,
218
+ });
219
+ let done = 0;
220
+ const total = plan.entries.length;
89
221
  const vars = buildVars({
90
222
  target,
91
223
  projectName,
@@ -99,6 +231,13 @@ export function runInstall({ args, t, lang, hosts, planHosts = hosts, categories
99
231
 
100
232
  console.log(t('install.start', { command: commandLabel, target }));
101
233
  console.log(t('install.plan', { hosts: hostSet.join(', '), stack, lang, count: plan.entries.length }));
234
+ console.log(t('install.tier', { tier, hardened: t(hardened ? 'tui.yes' : 'tui.no') }));
235
+ if (models) {
236
+ console.log(t('install.models', { models: summarizeModels(models) }));
237
+ for (const warning of models.warnings || []) {
238
+ console.log(t('install.modelWarning', { host: warning.host, role: warning.role }));
239
+ }
240
+ }
102
241
  if (dryRun) console.log(t('install.dryRun'));
103
242
 
104
243
  const manifest = previous || emptyManifest({
@@ -114,15 +253,21 @@ export function runInstall({ args, t, lang, hosts, planHosts = hosts, categories
114
253
  manifest,
115
254
  dryRun,
116
255
  force,
117
- log: (action, msg) => {
118
- const prefix = { create: '+', update: '~', skip: '-', conflict: '!', merge: '~' }[action] || ' ';
119
- console.log(`${prefix} ${msg}`);
120
- },
256
+ log: (action, msg) => onLog(action, msg, (done += 1), total),
121
257
  });
122
258
 
123
259
  console.log(summaryLine(result.stats, t));
124
260
 
125
261
  if (!dryRun) {
262
+ generateDerivedArtifacts(target);
263
+ // 模型 ID 是机器本地状态:只写 gitignore 的 local 覆盖,不进 manifest。
264
+ if (models && (commandLabel !== 'upgrade' || explicitModels)) {
265
+ const saved = writeLocalModelRoles(target, models, {
266
+ overwrite: explicitModels || commandLabel === 'add-host',
267
+ });
268
+ if (saved.written) console.log(t('install.localModels', { path: path.join(target, '.agents/config/model-roles.local.json') }));
269
+ }
270
+
126
271
  const next = {
127
272
  ...manifest,
128
273
  version: KIT_VERSION,
@@ -130,6 +275,8 @@ export function runInstall({ args, t, lang, hosts, planHosts = hosts, categories
130
275
  language: lang,
131
276
  hosts: hostSet,
132
277
  stack,
278
+ tier,
279
+ hardened,
133
280
  files: result.files,
134
281
  };
135
282
  writeManifest(target, next);
@@ -1,16 +1,124 @@
1
1
  // 宿主注册表:v1 十宿主。dir 为宿主点目录;rootMemory 为项目根记忆文件(仅 Claude 有)。
2
+ // 能力字段描述安装器可以写什么、怎么写;探测不到的字段保持 null,不编造目录。
2
3
  // tracked 仅描述当前本仓的人库政策,不影响安装能力。
4
+
3
5
  export const HOSTS = {
4
- trae: { dir: '.trae', label: 'Trae', tracked: true },
5
- codebuddy: { dir: '.codebuddy', label: 'CodeBuddy', tracked: true },
6
- claude: { dir: '.claude', label: 'Claude Code', tracked: false, rootMemory: 'CLAUDE.md' },
7
- opencode: { dir: '.opencode', label: 'OpenCode', tracked: false },
8
- qwen: { dir: '.qwen', label: 'Qwen Code', tracked: false },
9
- pi: { dir: '.pi', label: 'PI', tracked: false },
10
- cline: { dir: '.cline', label: 'Cline', tracked: false },
11
- commandcode: { dir: '.commandcode', label: 'Command Code', tracked: false },
12
- qoder: { dir: '.qoder', label: 'Qoder', tracked: false },
13
- zcode: { dir: '.zcode', label: 'ZCode', tracked: false },
6
+ trae: {
7
+ dir: '.trae',
8
+ label: 'Trae',
9
+ tracked: true,
10
+ commands: { dir: 'commands', ext: '.md' },
11
+ agents: { dir: 'agents', ext: '.md', modelField: null, inheritValue: null },
12
+ skills: { mode: 'index' },
13
+ hooks: { file: 'hooks.json', format: 'json' },
14
+ modelProbe: { configs: [], env: [], cli: [] },
15
+ },
16
+ codebuddy: {
17
+ dir: '.codebuddy',
18
+ label: 'CodeBuddy',
19
+ tracked: true,
20
+ commands: { dir: 'commands', ext: '.md' },
21
+ agents: { dir: 'agents', ext: '.md', modelField: null, inheritValue: null },
22
+ // CodeBuddy 官方文档不可得;本地既有 .codebuddy/skills/<name>/SKILL.md 约定
23
+ skills: { mode: 'native', dir: 'skills', file: 'SKILL.md' },
24
+ hooks: { file: 'settings.json', format: 'json' },
25
+ modelProbe: { configs: ['~/.codebuddy/settings.json'], env: ['CODEBUDDY_MODEL'], cli: [] },
26
+ },
27
+ claude: {
28
+ dir: '.claude',
29
+ label: 'Claude Code',
30
+ tracked: false,
31
+ rootMemory: 'CLAUDE.md',
32
+ commands: { dir: 'commands', ext: '.md' },
33
+ agents: { dir: 'agents', ext: '.md', modelField: 'model', inheritValue: 'inherit' },
34
+ skills: { mode: 'native', dir: 'skills', file: 'SKILL.md' },
35
+ hooks: { file: 'settings.json', format: 'json' },
36
+ modelProbe: {
37
+ configs: ['~/.claude/settings.json'],
38
+ env: ['ANTHROPIC_MODEL', 'CLAUDE_CODE_SUBAGENT_MODEL'],
39
+ cli: [],
40
+ },
41
+ },
42
+ opencode: {
43
+ dir: '.opencode',
44
+ label: 'OpenCode',
45
+ tracked: false,
46
+ commands: { dir: 'commands', ext: '.md' },
47
+ // 省略 model = 继承父级;不写 inherit 值
48
+ agents: { dir: 'agents', ext: '.md', modelField: 'model', inheritValue: null },
49
+ skills: { mode: 'native', dir: 'skills', file: 'SKILL.md' },
50
+ hooks: null,
51
+ modelProbe: { configs: ['~/.config/opencode/opencode.json'], env: ['OPENCODE_MODEL'], cli: [] },
52
+ },
53
+ qwen: {
54
+ dir: '.qwen',
55
+ label: 'Qwen Code',
56
+ tracked: false,
57
+ commands: { dir: 'commands', ext: '.md' },
58
+ agents: { dir: 'agents', ext: '.md', modelField: 'model', inheritValue: 'inherit' },
59
+ // Qwen 原生发现 .agents/skills;不复制宿主技能副本
60
+ skills: { mode: 'shared' },
61
+ hooks: { file: 'settings.json', format: 'json' },
62
+ modelProbe: { configs: ['~/.qwen/settings.json'], env: ['QWEN_MODEL'], cli: [] },
63
+ },
64
+ pi: {
65
+ dir: '.pi',
66
+ label: 'PI',
67
+ tracked: false,
68
+ commands: null,
69
+ // PI 的 model 是网关限定 ID,没有 inherit 语义;解析不到时保留模板原值
70
+ agents: { dir: 'agents', ext: '.md', modelField: 'model', inheritValue: null },
71
+ skills: { mode: 'index' },
72
+ hooks: null,
73
+ modelProbe: { configs: ['~/.pi/settings.json'], env: ['PI_MODEL'], cli: [] },
74
+ },
75
+ cline: {
76
+ dir: '.cline',
77
+ label: 'Cline',
78
+ tracked: false,
79
+ // Cline 的 workflows 是 Markdown 工作流,用作技能索引入口
80
+ commands: { dir: 'workflows', ext: '.md' },
81
+ agents: { dir: 'agents', ext: '.yml', modelField: null, inheritValue: null },
82
+ skills: { mode: 'index' },
83
+ hooks: null,
84
+ modelProbe: { configs: [], env: [], cli: [] },
85
+ },
86
+ commandcode: {
87
+ dir: '.commandcode',
88
+ label: 'Command Code',
89
+ tracked: false,
90
+ commands: { dir: 'commands', ext: '.md' },
91
+ agents: { dir: 'agents', ext: '.md', modelField: 'model', inheritValue: 'inherit' },
92
+ // Command Code 原生发现 .agents/skills;不复制宿主技能副本
93
+ skills: { mode: 'shared' },
94
+ hooks: { file: 'settings.json', format: 'json' },
95
+ modelProbe: {
96
+ configs: ['~/.commandcode/settings.json'],
97
+ env: ['COMMANDCODE_MODEL'],
98
+ // cmd --list-models 是唯一已核实的非交互列表命令;Windows 上跳过 cmd(可能是系统 cmd.exe)
99
+ cli: [{ command: 'cmd', args: ['--list-models'] }, { command: 'commandcode', args: ['--list-models'] }],
100
+ },
101
+ },
102
+ qoder: {
103
+ dir: '.qoder',
104
+ label: 'Qoder',
105
+ tracked: false,
106
+ commands: { dir: 'commands', ext: '.md' },
107
+ agents: { dir: 'agents', ext: '.md', modelField: null, inheritValue: null },
108
+ skills: { mode: 'index' },
109
+ hooks: { file: 'settings.json', format: 'json' },
110
+ modelProbe: { configs: ['~/.qoder/settings.json'], env: [], cli: [] },
111
+ },
112
+ zcode: {
113
+ dir: '.zcode',
114
+ label: 'ZCode',
115
+ tracked: false,
116
+ commands: null,
117
+ agents: { dir: 'agents', ext: '.md', modelField: null, inheritValue: null },
118
+ skills: { mode: 'index' },
119
+ hooks: { file: 'config.json', format: 'json' },
120
+ modelProbe: { configs: ['~/.zcode/config.json'], env: [], cli: [] },
121
+ },
14
122
  };
15
123
 
16
124
  export const HOST_NAMES = Object.keys(HOSTS);
@@ -24,3 +132,39 @@ export function resolveHost(name) {
24
132
  }
25
133
  return { name: key, ...HOSTS[key] };
26
134
  }
135
+
136
+ // 宿主模板相对路径 -> 目标相对路径。rootMemory 文件放项目根,其余放宿主点目录。
137
+ export function hostTargetRel(name, rel) {
138
+ const h = HOSTS[name];
139
+ if (!h) {
140
+ const err = new Error(`unknown host: ${name}`);
141
+ err.code = 'UNKNOWN_HOST';
142
+ throw err;
143
+ }
144
+ const normalized = String(rel).replace(/\\/g, '/');
145
+ if (h.rootMemory && normalized === h.rootMemory) return h.rootMemory;
146
+ return `${h.dir}/${normalized}`;
147
+ }
148
+
149
+ // 子代理文件名 -> 引擎模型角色。未知角色按 judgment 处理(强推理默认更稳)。
150
+ const MODEL_ROLE_BY_AGENT = {
151
+ implementer: 'code',
152
+ 'independent-reviewer': 'review',
153
+ 'pr-verifier': 'review',
154
+ 'pr-adjudicator': 'judgment',
155
+ 'pr-finder': 'judgment',
156
+ 'ui-verifier': 'judgment',
157
+ 'wiki-compiler': 'judgment',
158
+ };
159
+
160
+ export function modelRoleForAgent(rel) {
161
+ const base = String(rel).split('/').pop().replace(/\.[^.]+$/, '');
162
+ return MODEL_ROLE_BY_AGENT[base] || 'judgment';
163
+ }
164
+
165
+ export function isAgentTemplate(hostName, rel) {
166
+ const h = HOSTS[hostName];
167
+ if (!h?.agents) return false;
168
+ const normalized = String(rel).replace(/\\/g, '/');
169
+ return normalized.startsWith(`${h.agents.dir}/`) && normalized.endsWith(h.agents.ext);
170
+ }
package/src/i18n.mjs CHANGED
@@ -7,6 +7,9 @@ const KIT = path.resolve(HERE, '..');
7
7
  const LOCALES = path.join(KIT, 'locales');
8
8
  const FALLBACK = 'zh-CN';
9
9
 
10
+ // 支持语言单一来源:cli/main.mjs 的 SUPPORTED_LANGS 与 TUI 向导语言菜单都由它派生
11
+ export const LANGUAGES = ['zh-CN', 'en'];
12
+
10
13
  const cache = new Map();
11
14
  export function loadCatalog(lang) {
12
15
  if (cache.has(lang)) return cache.get(lang);
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { render } from '../render/tokens.mjs';
4
+ import { setField } from './frontmatter.mjs';
4
5
  import { sha256Text } from './manifest.mjs';
5
6
 
6
7
  const GITATTR_BEGIN = '# >>> agentic-flow-kit gitattributes';
@@ -37,9 +38,22 @@ function mergeBlock(existing, block, begin, end) {
37
38
  }
38
39
 
39
40
  export function readPlanEntry(entry, vars) {
40
- if (isBinary(entry.srcAbs)) return null;
41
- const raw = readText(entry.srcAbs);
42
- return render(raw, vars);
41
+ let text;
42
+ if (typeof entry.getText === 'function') {
43
+ text = entry.getText(vars);
44
+ } else {
45
+ if (!entry.srcAbs || isBinary(entry.srcAbs)) return null;
46
+ text = render(readText(entry.srcAbs), vars);
47
+ }
48
+
49
+ // 模型注入必须在语言覆盖/渲染之后:manifest 记录的是最终文本哈希。
50
+ if (entry.modelField && entry.modelRole && typeof entry.modelValue === 'string') {
51
+ const value = entry.modelValue.trim();
52
+ if (value && (value !== 'inherit' || entry.modelInherit)) {
53
+ text = setField(text, entry.modelField, value);
54
+ }
55
+ }
56
+ return text;
43
57
  }
44
58
 
45
59
  export function applyPlan({ plan, vars, manifest, dryRun, force, log }) {
@@ -0,0 +1,84 @@
1
+ // 无依赖 frontmatter 读写:只处理单行 key: value,够用于宿主 agent / skill 头部。
2
+ // 不引入 YAML 解析器;description 等含空格/冒号的值在写入时用 JSON 双引号转义(YAML 合法)。
3
+
4
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
5
+
6
+ function eolOf(text) {
7
+ return String(text).includes('\r\n') ? '\r\n' : '\n';
8
+ }
9
+
10
+ function unquote(value) {
11
+ const v = String(value).trim();
12
+ if (v.startsWith('"') && v.endsWith('"')) {
13
+ try {
14
+ return JSON.parse(v);
15
+ } catch {
16
+ return v.slice(1, -1);
17
+ }
18
+ }
19
+ if (v.startsWith("'") && v.endsWith("'")) return v.slice(1, -1).replace(/''/g, "'");
20
+ return v;
21
+ }
22
+
23
+ export function splitFrontmatter(text) {
24
+ const s = String(text ?? '');
25
+ const m = FRONTMATTER_RE.exec(s);
26
+ if (!m) return { has: false, frontmatter: '', body: s, eol: eolOf(s) };
27
+ const eol = eolOf(s);
28
+ return { has: true, frontmatter: m[1], body: s.slice(m[0].length), eol };
29
+ }
30
+
31
+ export function parseFrontmatter(text) {
32
+ const { frontmatter } = splitFrontmatter(text);
33
+ const out = {};
34
+ for (const line of frontmatter.split(/\r?\n/)) {
35
+ const t = line.trim();
36
+ if (!t || t.startsWith('#')) continue;
37
+ const i = t.indexOf(':');
38
+ if (i <= 0) continue;
39
+ const key = t.slice(0, i).trim();
40
+ if (!key) continue;
41
+ out[key] = unquote(t.slice(i + 1).trim());
42
+ }
43
+ return out;
44
+ }
45
+
46
+ export function getField(text, key) {
47
+ return parseFrontmatter(text)[key];
48
+ }
49
+
50
+ // 写/替换/删除单行字段;无 frontmatter 时在文首新建。value 为 null/undefined 表示删除。
51
+ export function setField(text, key, value) {
52
+ const s = String(text ?? '');
53
+ const { has, frontmatter, body, eol } = splitFrontmatter(s);
54
+ const lines = has ? frontmatter.split(/\r?\n/) : [];
55
+ const idx = lines.findIndex((line) => new RegExp(`^\\s*${escapeRe(key)}\\s*:`).test(line));
56
+
57
+ if (value == null) {
58
+ if (!has || idx < 0) return s;
59
+ lines.splice(idx, 1);
60
+ } else {
61
+ const line = `${key}: ${yamlScalar(value)}`;
62
+ if (idx >= 0) lines[idx] = line;
63
+ else lines.push(line);
64
+ }
65
+
66
+ if (!has) return `---${eol}${lines.join(eol)}${eol}---${eol}${eol}${s}`;
67
+ return `---${eol}${lines.join(eol)}${eol}---${eol}${body}`;
68
+ }
69
+
70
+ export function yamlScalar(value) {
71
+ const s = String(value);
72
+ if (s === '') return '""';
73
+ // 安全裸值:字母数字 + 常见模型 ID 标点;排除 YAML 布尔/空值/数字
74
+ if (/^[A-Za-z0-9_./:@+~=-]+$/.test(s)
75
+ && !/^(true|false|null|yes|no|on|off|~)$/i.test(s)
76
+ && !/^-?\d+(\.\d+)?$/.test(s)) {
77
+ return s.includes('#') ? JSON.stringify(s) : s;
78
+ }
79
+ return JSON.stringify(s);
80
+ }
81
+
82
+ function escapeRe(s) {
83
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
84
+ }