openfeel 1.3.2 → 1.3.4

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 (82) hide show
  1. package/README.en.md +1 -0
  2. package/README.md +2 -1
  3. package/README.zh-CN.md +1 -0
  4. package/dist/cli/deploy-check-output.d.ts.map +1 -1
  5. package/dist/cli/deploy-check-output.js +7 -1
  6. package/dist/cli/deploy-check-output.js.map +1 -1
  7. package/dist/commands/flow.d.ts.map +1 -1
  8. package/dist/commands/flow.js +98 -14
  9. package/dist/commands/flow.js.map +1 -1
  10. package/dist/commands/instructions.js +1 -1
  11. package/dist/commands/instructions.js.map +1 -1
  12. package/dist/commands/plan.d.ts.map +1 -1
  13. package/dist/commands/plan.js +9 -1
  14. package/dist/commands/plan.js.map +1 -1
  15. package/dist/commands/setup.d.ts.map +1 -1
  16. package/dist/commands/setup.js +7 -0
  17. package/dist/commands/setup.js.map +1 -1
  18. package/dist/core/adapter-hash.d.ts +7 -0
  19. package/dist/core/adapter-hash.d.ts.map +1 -0
  20. package/dist/core/adapter-hash.js +64 -0
  21. package/dist/core/adapter-hash.js.map +1 -0
  22. package/dist/core/archive/merge.js +8 -8
  23. package/dist/core/archive/merge.js.map +1 -1
  24. package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
  25. package/dist/core/artifact-graph/resolver.js +8 -0
  26. package/dist/core/artifact-graph/resolver.js.map +1 -1
  27. package/dist/core/config.d.ts +12 -0
  28. package/dist/core/config.d.ts.map +1 -1
  29. package/dist/core/config.js +25 -3
  30. package/dist/core/config.js.map +1 -1
  31. package/dist/core/deployment-check.d.ts +17 -4
  32. package/dist/core/deployment-check.d.ts.map +1 -1
  33. package/dist/core/deployment-check.js +59 -7
  34. package/dist/core/deployment-check.js.map +1 -1
  35. package/dist/core/flow-manager.d.ts +26 -1
  36. package/dist/core/flow-manager.d.ts.map +1 -1
  37. package/dist/core/flow-manager.js +112 -9
  38. package/dist/core/flow-manager.js.map +1 -1
  39. package/dist/core/global-paths.d.ts +5 -0
  40. package/dist/core/global-paths.d.ts.map +1 -1
  41. package/dist/core/global-paths.js +7 -0
  42. package/dist/core/global-paths.js.map +1 -1
  43. package/dist/core/i18n-data/en.d.ts.map +1 -1
  44. package/dist/core/i18n-data/en.js +14 -0
  45. package/dist/core/i18n-data/en.js.map +1 -1
  46. package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
  47. package/dist/core/i18n-data/zh-CN.js +14 -0
  48. package/dist/core/i18n-data/zh-CN.js.map +1 -1
  49. package/dist/core/plan/stage.d.ts.map +1 -1
  50. package/dist/core/plan/stage.js +3 -1
  51. package/dist/core/plan/stage.js.map +1 -1
  52. package/dist/core/setup.d.ts.map +1 -1
  53. package/dist/core/setup.js +8 -3
  54. package/dist/core/setup.js.map +1 -1
  55. package/dist/core/template-loader.d.ts.map +1 -1
  56. package/dist/core/template-loader.js +38 -18
  57. package/dist/core/template-loader.js.map +1 -1
  58. package/dist/core/update-state.d.ts +20 -0
  59. package/dist/core/update-state.d.ts.map +1 -1
  60. package/dist/core/update-state.js +26 -0
  61. package/dist/core/update-state.js.map +1 -1
  62. package/dist/core/update.d.ts.map +1 -1
  63. package/dist/core/update.js +25 -5
  64. package/dist/core/update.js.map +1 -1
  65. package/dist/core/view/entry.d.ts +13 -4
  66. package/dist/core/view/entry.d.ts.map +1 -1
  67. package/dist/core/view/entry.js +40 -16
  68. package/dist/core/view/entry.js.map +1 -1
  69. package/dist/core/workspace/structure.d.ts +21 -1
  70. package/dist/core/workspace/structure.d.ts.map +1 -1
  71. package/dist/core/workspace/structure.js +30 -11
  72. package/dist/core/workspace/structure.js.map +1 -1
  73. package/dsh/ADAPTER.md +20 -8
  74. package/dsh/README.md +10 -5
  75. package/dsh/lib/adapters/dashboard.js +2 -2
  76. package/dsh/lib/adapters/host-state.js +111 -0
  77. package/dsh/lib/guard.js +78 -11
  78. package/dsh/lib/index.js +21 -0
  79. package/dsh/lib/tools/index.js +8 -3
  80. package/dsh/lib/tools/shared.js +28 -14
  81. package/dsh/package.json +2 -2
  82. package/package.json +1 -1
package/dsh/lib/guard.js CHANGED
@@ -14,8 +14,8 @@
14
14
  * @module openfeel-dsh/guard
15
15
  */
16
16
 
17
- import { readFileSync, statSync } from 'node:fs';
18
- import { basename, join, resolve } from 'node:path';
17
+ import { existsSync, readFileSync, statSync } from 'node:fs';
18
+ import { basename, extname, join, resolve } from 'node:path';
19
19
 
20
20
  import { isProjectWorkspace, OPENFEEL_DIR } from './adapters/workspace.js';
21
21
 
@@ -40,6 +40,35 @@ export function normalizeForComparison(value, platform = process.platform) {
40
40
  return platform === 'win32' || platform === 'darwin' ? value.toLowerCase() : value;
41
41
  }
42
42
 
43
+ /** 文档/笔记类扩展名(写门禁按产物类型分层:doc/note 不绑相位)。 */
44
+ export const DOC_EXTENSIONS = Object.freeze(['.md', '.mdx', '.txt', '.rst', '.adoc']);
45
+
46
+ /**
47
+ * 是否为文档/笔记类目标(P0-4):扩展名命中,或位于**工作区根级 `docs/`** 下。
48
+ * v1.3.3 复核:`docs` 必须直接位于会话工作区下(`src/docs/x.ts` 不算文档,防源码借名绕过相位门)。
49
+ * 仅用于「放行 doc/note」;源代码仍按 `exec_running` 严格判定。
50
+ * @param {string} absolute 已归一化绝对路径
51
+ * @param {string} cwd 已归一化工作区
52
+ * @returns {boolean}
53
+ */
54
+ function isDocPath(absolute, cwd) {
55
+ if (DOC_EXTENSIONS.includes(extname(absolute).toLowerCase())) return true;
56
+ const docsRoot = normalizeTarget(cwd, 'docs');
57
+ return absolute === docsRoot
58
+ || absolute.startsWith(`${docsRoot}\\`)
59
+ || absolute.startsWith(`${docsRoot}/`);
60
+ }
61
+
62
+ /**
63
+ * 是否位于工作区内(bootstrap 例外边界;防 `..`/绝对路径写出工作区)。
64
+ * @param {string} absolute 已归一化绝对路径
65
+ * @param {string} cwd 已归一化工作区
66
+ * @returns {boolean}
67
+ */
68
+ function isUnderWorkspace(absolute, cwd) {
69
+ return absolute === cwd || absolute.startsWith(`${cwd}\\`) || absolute.startsWith(`${cwd}/`);
70
+ }
71
+
43
72
  /** flow.json 读取缓存(path → { mtimeMs, size, value })。 */
44
73
  const flowCache = new Map();
45
74
 
@@ -120,22 +149,32 @@ function extractPatchText(args) {
120
149
  }
121
150
 
122
151
  /**
123
- * 解析 apply_patch 的全部目标路径。
152
+ * 解析 apply_patch 的全部目标路径与变更类型。
124
153
  *
125
154
  * 只认 patch 头,避免正文里出现 `.openfeel/` 被误判。支持:
126
155
  * - `*** Update File:`, `*** Add File:`, `*** Delete File:`
127
156
  * - `*** Move to:`(要求同块 `*** Update File:` 源,否则拒绝)
128
157
  * - `--- a/path`, `+++ b/path`;忽略 `/dev/null`
129
158
  *
159
+ * 返回 `entries`(路径 + kind)供 bootstrap 例外判定「是否纯新增」:
160
+ * kind ∈ `add | update | delete | move`;同一路径出现多种 kind 时从严记 `update`。
161
+ *
130
162
  * @param {string} patch
131
- * @returns {{ ok: true, paths: string[] } | { ok: false, reason: string }}
163
+ * @returns {{ ok: true, paths: string[], entries: Array<{ path: string, kind: string }> } | { ok: false, reason: string }}
132
164
  */
133
165
  export function parsePatchTargets(patch) {
134
166
  const paths = [];
167
+ const kinds = new Map();
135
168
  const seen = new Set();
136
- const add = (raw) => {
169
+ const add = (raw, kind) => {
137
170
  const value = cleanPatchPath(raw);
138
171
  if (value === '' || value === '/dev/null') return;
172
+ const prev = kinds.get(value);
173
+ if (prev === undefined) {
174
+ kinds.set(value, kind);
175
+ } else if (prev !== kind) {
176
+ kinds.set(value, 'update'); // 同路径混合 kind → 从严(bootstrap 例外不成立)
177
+ }
139
178
  if (seen.has(value)) return;
140
179
  seen.add(value);
141
180
  paths.push(value);
@@ -147,19 +186,19 @@ export function parsePatchTargets(patch) {
147
186
  const update = /^\*\*\* Update File:\s*(.+?)\s*$/.exec(line);
148
187
  if (update !== null) {
149
188
  currentBlock = 'update';
150
- add(update[1]);
189
+ add(update[1], 'update');
151
190
  continue;
152
191
  }
153
192
  const addFile = /^\*\*\* Add File:\s*(.+?)\s*$/.exec(line);
154
193
  if (addFile !== null) {
155
194
  currentBlock = 'add';
156
- add(addFile[1]);
195
+ add(addFile[1], 'add');
157
196
  continue;
158
197
  }
159
198
  const deleteFile = /^\*\*\* Delete File:\s*(.+?)\s*$/.exec(line);
160
199
  if (deleteFile !== null) {
161
200
  currentBlock = 'delete';
162
- add(deleteFile[1]);
201
+ add(deleteFile[1], 'delete');
163
202
  continue;
164
203
  }
165
204
  const move = /^\*\*\* Move to:\s*(.+?)\s*$/.exec(line);
@@ -167,7 +206,7 @@ export function parsePatchTargets(patch) {
167
206
  if (currentBlock !== 'update') {
168
207
  return { ok: false, reason: '*** Move to: 缺少同块 *** Update File: 源路径' };
169
208
  }
170
- add(move[1]);
209
+ add(move[1], 'move');
171
210
  continue;
172
211
  }
173
212
  if (/^\*\*\* (?:Begin|End) Patch/.test(line)) {
@@ -175,13 +214,13 @@ export function parsePatchTargets(patch) {
175
214
  continue;
176
215
  }
177
216
  const diff = /^(?:---|\+\+\+) [ab]\/(.+?)\s*$/.exec(line);
178
- if (diff !== null) add(diff[1]);
217
+ if (diff !== null) add(diff[1], 'update');
179
218
  }
180
219
 
181
220
  if (paths.length === 0) {
182
221
  return { ok: false, reason: '无法从 patch 头确定目标路径' };
183
222
  }
184
- return { ok: true, paths };
223
+ return { ok: true, paths, entries: paths.map((path) => ({ path, kind: kinds.get(path) })) };
185
224
  }
186
225
 
187
226
  /**
@@ -253,6 +292,8 @@ export function createOpenFeelGuard(options = {}) {
253
292
  const fallbackCwd = typeof options.cwd === 'string' && options.cwd !== '' ? options.cwd : undefined;
254
293
 
255
294
  const t = (zh, en) => (english ? en : zh);
295
+ /** bootstrap 例外提示去重(按工作区路径)。 */
296
+ const bootstrapHinted = new Set();
256
297
 
257
298
  if (enforce === 'off') {
258
299
  // 完全关闭:连 flow.json/status.md 也不拦。
@@ -270,6 +311,7 @@ export function createOpenFeelGuard(options = {}) {
270
311
  const normalizedCwd = normalizeForComparison(resolvedCwd);
271
312
 
272
313
  let rawTargets;
314
+ let patchEntries = null;
273
315
  if (name === 'apply_patch') {
274
316
  const patch = extractPatchText(execution?.arguments);
275
317
  if (patch === undefined) {
@@ -286,6 +328,7 @@ export function createOpenFeelGuard(options = {}) {
286
328
  );
287
329
  }
288
330
  rawTargets = parsed.paths;
331
+ patchEntries = parsed.entries;
289
332
  } else {
290
333
  rawTargets = extractArgumentPaths(execution?.arguments);
291
334
  if (rawTargets.length === 0) {
@@ -297,6 +340,7 @@ export function createOpenFeelGuard(options = {}) {
297
340
  }
298
341
 
299
342
  const targets = rawTargets.map((target) => normalizeTarget(resolvedCwd, target));
343
+ const allDoc = targets.every((target) => isDocPath(target, normalizedCwd));
300
344
 
301
345
  // 受保护路径:guard 启用时始终拒绝(enforce=off 已提前返回)。
302
346
  if (targets.some((target) => isProtectedPath(target, normalizedCwd))) {
@@ -309,6 +353,25 @@ export function createOpenFeelGuard(options = {}) {
309
353
  const initialized = isProjectWorkspace(resolvedCwd);
310
354
  if (!initialized) {
311
355
  if (enforce === 'always') {
356
+ // P0-1 bootstrap 例外:空工作区 + 全部为「新增文件」→ 放行(最高风险等级不成立)。
357
+ // apply_patch 额外要求全部 block 为 `*** Add File:`(Update/Delete/Move/混合一律从严)。
358
+ // `.openfeel/**` 不适用:流程产物仍要求先 init(B8)。
359
+ const allNew = targets.every((target) => !existsSync(target));
360
+ const anyInternal = targets.some((target) => isUnderOpenFeel(target, normalizedCwd));
361
+ const allInside = targets.every((target) => isUnderWorkspace(target, normalizedCwd));
362
+ const addOnly = patchEntries === null || patchEntries.every((entry) => entry.kind === 'add');
363
+ if (allNew && addOnly && !anyInternal && allInside) {
364
+ if (!bootstrapHinted.has(normalizedCwd)) {
365
+ bootstrapHinted.add(normalizedCwd);
366
+ process.stderr.write(
367
+ `${t(
368
+ 'OpenFeel 写入门禁:[bootstrap] 工作区尚未初始化,已放行本次新增文件;建议稍后运行 `openfeel init` 补账本。',
369
+ 'OpenFeel write guard: [bootstrap] workspace not initialized; this new-file write was allowed. Run `openfeel init` later to record it.',
370
+ )}\n`,
371
+ );
372
+ }
373
+ return undefined;
374
+ }
312
375
  return t(
313
376
  'OpenFeel 写入门禁:当前工作区尚未初始化 OpenFeel(无 `.openfeel/`)。请先运行 `openfeel init` 并建立需求/阶段;或让用户把 profile 的 `enforce` 改为 `project` / `off`。',
314
377
  'OpenFeel write guard: this workspace is not OpenFeel-initialized (no `.openfeel/`). Run `openfeel init` and create a requirement/stage first; or ask the user to set `enforce` to `project` / `off`.',
@@ -331,6 +394,8 @@ export function createOpenFeelGuard(options = {}) {
331
394
  const active = activeStages(flow.value);
332
395
  if (active.length === 0) {
333
396
  if (allowGeneral && hasGeneralRequirement(flow.value)) return undefined;
397
+ // P0-4:doc/note 类产物不要求活跃阶段(收口后仍可写文档);混合目标从严。
398
+ if (allDoc) return undefined;
334
399
  return t(
335
400
  'OpenFeel 写入门禁:当前无活跃阶段,不能修改源码/文档。请先 feel_req add → claim → feel_stage add 建立阶段。',
336
401
  'OpenFeel write guard: no active stage, so source/docs are read-only. Create a stage via feel_req add → claim → feel_stage add.',
@@ -338,6 +403,8 @@ export function createOpenFeelGuard(options = {}) {
338
403
  }
339
404
 
340
405
  if (active.some((stage) => stage.phase === 'exec_running')) return undefined;
406
+ // P0-4:非 exec 相位仍允许 doc/note(研究记录、设计文档等);代码仍严格。
407
+ if (allDoc) return undefined;
341
408
 
342
409
  const summary = active
343
410
  .map((stage) => `${stage.id ?? '(unknown)'}=${stage.phase ?? '(unknown)'}`)
package/dsh/lib/index.js CHANGED
@@ -22,8 +22,13 @@
22
22
  * @module openfeel-dsh
23
23
  */
24
24
 
25
+ import { readFileSync } from 'node:fs';
26
+ import { dirname } from 'node:path';
27
+ import { fileURLToPath } from 'node:url';
28
+
25
29
  import { FeelCli } from './adapters/cli.js';
26
30
  import { resolveWorkspace } from './adapters/workspace.js';
31
+ import { writeHostMarker } from './adapters/host-state.js';
27
32
  import { createOpenFeelGuard } from './guard.js';
28
33
  import { createFeelCommandHandler } from './surface/command.js';
29
34
  import { FEEL_PROMPT_NAME, FEEL_PROMPT_ORDER, renderBootstrapPrompt, renderFeelPrompt } from './surface/prompt.js';
@@ -110,6 +115,22 @@ function currentAgent(ctx) {
110
115
  */
111
116
  export function apply(ctx, rawConfig) {
112
117
  const config = normalizeConfig(rawConfig);
118
+
119
+ // v1.3.4:记录「本宿主实际加载的适配器指纹」,供 `openfeel setup --check` 区分
120
+ // 「文件旧」与「文件已新但宿主未重启」。best-effort:失败不影响挂载。
121
+ try {
122
+ const adapterRoot = dirname(dirname(fileURLToPath(import.meta.url)));
123
+ let adapterVersion = '0.0.0';
124
+ try {
125
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
126
+ if (typeof pkg.version === 'string' && pkg.version !== '') adapterVersion = pkg.version;
127
+ } catch {
128
+ /* package.json 不可读时保持占位版本 */
129
+ }
130
+ writeHostMarker({ adapterRoot, version: adapterVersion });
131
+ } catch {
132
+ /* 标记写入失败 → CLI check 会提示 restart-required,可操作,不阻塞 */
133
+ }
113
134
  const log = typeof ctx.logger === 'function'
114
135
  ? ctx.logger('openfeel-dsh')
115
136
  : {
@@ -207,9 +207,9 @@ export function createTools(context) {
207
207
  '操作。',
208
208
  { required: true },
209
209
  ),
210
- stage: string('阶段 ID(advance/retry 必需;phases/mode 可选用于按阶段派生模式)。'),
210
+ stage: string('阶段 ID(advance/retry 必需;review_add 与 op 二选一;phases/mode 可选用于按阶段派生模式)。'),
211
211
  to: stringEnum(PHASES, 'advance 的目标相位。'),
212
- op: string('操作 ID(如 v1.2.1-stage-93.op-001)。'),
212
+ op: string('操作 ID(如 v1.2.1-stage-93.op-001);review_add 与 stage 二选一。'),
213
213
  force: boolean('advance 时跳过相位合法性与跳跃检查(不能绕过 blocking REV 门)。'),
214
214
  dry_run: boolean('仅校验/预览,不写盘。'),
215
215
  review_tier: stringEnum(TIERS, '审查档位:full=零裁剪 / lean=裁 scheme_review / skip=再裁 plan_review。缺省不裁剪(=full)。'),
@@ -294,7 +294,12 @@ export function createTools(context) {
294
294
  }
295
295
  case 'review_add': {
296
296
  const argv = ['flow', 'review', 'add'];
297
- pushOption(argv, '--op', args.op);
297
+ // v1.3.4:op / stage 二选一透传(fast 无 op 场景必须能挂阶段级 REV)
298
+ if (!args.op && !args.stage) {
299
+ throw new FeelError('review_add 需要 op 或 stage 之一', { kind: 'usage', code: 'FEEL_BAD_USAGE' });
300
+ }
301
+ if (args.op) pushOption(argv, '--op', args.op);
302
+ else pushOption(argv, '--stage', args.stage);
298
303
  pushOption(argv, '--title', requireArg(args, 'title', action));
299
304
  pushOption(argv, '--auto-fix', args.auto_fix);
300
305
  if (args.blocking !== undefined) argv.push('--blocking', args.blocking === true ? 'true' : 'false');
@@ -188,26 +188,40 @@ export function toWireEnvelope(action, payload) {
188
188
  }
189
189
 
190
190
  /**
191
- * 安全序列化:处理循环引用与不可序列化值,永不抛错。
191
+ * 安全序列化:只对**真环**(祖先路径上出现过的对象)写 `[循环引用]`;
192
+ * 同一对象的**重复引用**(DAG,如 focus.deps 与 stages[i].deps 指向同一数组)正常复制(P1-2)。
193
+ * 兼处理 BigInt / 不可序列化值,永不抛错。
192
194
  * @param {unknown} value
193
195
  * @returns {string}
194
196
  */
195
197
  export function stringifySafe(value) {
196
198
  try {
197
- /** @type {WeakSet<object>} */
198
- const seen = new WeakSet();
199
- return JSON.stringify(
200
- value,
201
- (_key, item) => {
202
- if (typeof item === 'object' && item !== null) {
203
- if (seen.has(item)) return '[循环引用]';
204
- seen.add(item);
199
+ /**
200
+ * @param {unknown} item
201
+ * @param {WeakSet<object>} ancestors
202
+ * @returns {unknown}
203
+ */
204
+ const clone = (item, ancestors) => {
205
+ if (typeof item === 'bigint') return item.toString();
206
+ if (typeof item === 'function' || typeof item === 'symbol') return undefined;
207
+ if (typeof item !== 'object' || item === null) return item;
208
+ // 保留 Date / URL 等自带 toJSON 的语义(与 JSON.stringify 一致)
209
+ if (typeof item.toJSON === 'function') return item.toJSON();
210
+ if (ancestors.has(item)) return '[循环引用]';
211
+ ancestors.add(item);
212
+ let out;
213
+ if (Array.isArray(item)) {
214
+ out = item.map((entry) => clone(entry, ancestors));
215
+ } else {
216
+ out = {};
217
+ for (const [key, entry] of Object.entries(item)) {
218
+ out[key] = clone(entry, ancestors);
205
219
  }
206
- if (typeof item === 'bigint') return item.toString();
207
- return item;
208
- },
209
- 2,
210
- );
220
+ }
221
+ ancestors.delete(item);
222
+ return out;
223
+ };
224
+ return JSON.stringify(clone(value, new WeakSet()), null, 2);
211
225
  } catch {
212
226
  return String(value);
213
227
  }
package/dsh/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openfeel-dsh",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "OpenFeel × DSH — Feel 模式适配器:把 OpenFeel 的流水线、需求分发、版本归档与知识库接入 DeepSeek Harness",
5
5
  "type": "module",
6
6
  "private": true,
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "openfeel": {
34
34
  "adapter": "dsh",
35
- "requiresCli": ">=1.3.2"
35
+ "requiresCli": ">=1.3.4"
36
36
  },
37
37
  "keywords": [
38
38
  "openfeel",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openfeel",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "AI Agent 开发流程治理 CLI 工具",
5
5
  "type": "module",
6
6
  "private": false,