openfeel 1.3.5 → 1.5.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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/index.d.ts.map +1 -1
  3. package/dist/cli/index.js +4 -0
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/commands/chore.d.ts +12 -0
  6. package/dist/commands/chore.d.ts.map +1 -0
  7. package/dist/commands/chore.js +126 -0
  8. package/dist/commands/chore.js.map +1 -0
  9. package/dist/commands/dev.d.ts +11 -0
  10. package/dist/commands/dev.d.ts.map +1 -0
  11. package/dist/commands/dev.js +47 -0
  12. package/dist/commands/dev.js.map +1 -0
  13. package/dist/commands/flow.d.ts.map +1 -1
  14. package/dist/commands/flow.js +12 -0
  15. package/dist/commands/flow.js.map +1 -1
  16. package/dist/core/config.js +2 -2
  17. package/dist/core/dev-status.d.ts +37 -0
  18. package/dist/core/dev-status.d.ts.map +1 -0
  19. package/dist/core/dev-status.js +176 -0
  20. package/dist/core/dev-status.js.map +1 -0
  21. package/dist/core/flow-manager.d.ts +3 -1
  22. package/dist/core/flow-manager.d.ts.map +1 -1
  23. package/dist/core/flow-manager.js +6 -1
  24. package/dist/core/flow-manager.js.map +1 -1
  25. package/dist/core/i18n-data/en.d.ts.map +1 -1
  26. package/dist/core/i18n-data/en.js +23 -0
  27. package/dist/core/i18n-data/en.js.map +1 -1
  28. package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
  29. package/dist/core/i18n-data/zh-CN.js +23 -0
  30. package/dist/core/i18n-data/zh-CN.js.map +1 -1
  31. package/dist/core/template-loader.d.ts.map +1 -1
  32. package/dist/core/template-loader.js +131 -183
  33. package/dist/core/template-loader.js.map +1 -1
  34. package/dist/core/update.d.ts.map +1 -1
  35. package/dist/core/update.js +68 -140
  36. package/dist/core/update.js.map +1 -1
  37. package/dsh/ADAPTER.md +5 -2
  38. package/dsh/README.md +9 -4
  39. package/dsh/lib/adapters/dashboard.js +6 -0
  40. package/dsh/lib/adapters/dev-status.js +40 -0
  41. package/dsh/lib/guard.js +112 -6
  42. package/dsh/lib/surface/prompt.js +10 -0
  43. package/dsh/lib/tools/index.js +3 -0
  44. package/dsh/package.json +2 -2
  45. package/package.json +1 -1
@@ -349,6 +349,7 @@ export function planNextStep(snapshot) {
349
349
  legalNextPhases: [],
350
350
  suggestedCommand: focus.suggestedCommand ?? null,
351
351
  guards,
352
+ statusSync: snapshot.statusSync ?? null,
352
353
  adapterHints: [],
353
354
  cli: focus.suggestedCommand ?? (allDone ? 'openfeel flow status' : 'openfeel plan stage list'),
354
355
  toolCall: null,
@@ -365,6 +366,10 @@ export function planNextStep(snapshot) {
365
366
  if (phase === 'review_failed' || phase === 'test_failed') {
366
367
  adapterHints.push('失败回环:决定「重试 / 回退重定方案 / 请求人工介入」,并把结论记入 REV 条目。');
367
368
  }
369
+ // v1.5.0:状态记录到期只提示,不阻塞;CLI advance 会在节点自动同步。
370
+ if (snapshot.statusSync?.due === true) {
371
+ adapterHints.push('状态记录已超过 30 分钟:更新交接摘要(仅 Feel)后运行 `openfeel dev sync`;CLI 在节点/到期时会自动同步。');
372
+ }
368
373
 
369
374
  const cliCommand = typeof focus.suggestedCommand === 'string' && focus.suggestedCommand !== ''
370
375
  ? focus.suggestedCommand
@@ -382,6 +387,7 @@ export function planNextStep(snapshot) {
382
387
  legalNextPhases: nextPhases,
383
388
  suggestedCommand: focus.suggestedCommand ?? null,
384
389
  guards,
390
+ statusSync: snapshot.statusSync ?? null,
385
391
  adapterHints,
386
392
  cli: cliCommand,
387
393
  toolCall: nextPhases.length === 0
@@ -0,0 +1,40 @@
1
+ /**
2
+ * dev 状态同步(v1.5.0,适配器侧只读)
3
+ *
4
+ * 与 CLI `openfeel dev status` 同源:读 `.openfeel/dev/.sync.json` 判断是否到期;
5
+ * 只读,不写盘。用于 feel_next / feel_status 暴露 statusSync.due。
6
+ */
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+
10
+ /** 默认同步间隔(分钟) */
11
+ export const DEFAULT_SYNC_INTERVAL_MINUTES = 30;
12
+
13
+ /**
14
+ * 读取状态同步快照(缺失/损坏 → due=true 且 lastSync=null)。
15
+ * @param {string} projectRoot
16
+ * @param {number} [now]
17
+ * @returns {{ due: boolean, lastSync: string|null, intervalMinutes: number }}
18
+ */
19
+ export function readStatusSync(projectRoot, now = Date.now()) {
20
+ const file = join(projectRoot, '.openfeel', 'dev', '.sync.json');
21
+ let lastSync = null;
22
+ let intervalMinutes = DEFAULT_SYNC_INTERVAL_MINUTES;
23
+ try {
24
+ if (existsSync(file)) {
25
+ const raw = JSON.parse(readFileSync(file, 'utf-8'));
26
+ if (typeof raw.last_sync === 'string') lastSync = raw.last_sync;
27
+ if (typeof raw.interval_minutes === 'number' && raw.interval_minutes > 0) {
28
+ intervalMinutes = raw.interval_minutes;
29
+ }
30
+ }
31
+ } catch {
32
+ /* 损坏 → 视为到期 */
33
+ }
34
+ let due = true;
35
+ if (lastSync !== null) {
36
+ const last = Date.parse(lastSync);
37
+ due = !Number.isFinite(last) || now - last >= intervalMinutes * 60_000;
38
+ }
39
+ return { due, lastSync, intervalMinutes };
40
+ }
package/dsh/lib/guard.js CHANGED
@@ -14,7 +14,7 @@
14
14
  * @module openfeel-dsh/guard
15
15
  */
16
16
 
17
- import { existsSync, readFileSync, statSync } from 'node:fs';
17
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
18
18
  import { basename, extname, join, resolve } from 'node:path';
19
19
 
20
20
  import { isProjectWorkspace, OPENFEEL_DIR } from './adapters/workspace.js';
@@ -43,6 +43,25 @@ export function normalizeForComparison(value, platform = process.platform) {
43
43
  /** 文档/笔记类扩展名(写门禁按产物类型分层:doc/note 不绑相位)。 */
44
44
  export const DOC_EXTENSIONS = Object.freeze(['.md', '.mdx', '.txt', '.rst', '.adoc']);
45
45
 
46
+ /**
47
+ * 静态展示资源扩展名(v1.4.0;与 doc 同层:任意相位可写,不要求活跃阶段)。
48
+ * 依据:反馈 `openfeel-write-gate-friction-2026-10-30.md` —— UI 微调无执行语义。
49
+ */
50
+ export const PRESENTATIONAL_EXTENSIONS = Object.freeze([
51
+ '.css', '.scss', '.html', '.svg', '.png', '.jpg', '.jpeg', '.webp',
52
+ ]);
53
+
54
+ /**
55
+ * 源码扩展名(v1.4.0):`assets/**` / `public/**` 的目录豁免**不得**覆盖这些扩展名,
56
+ * 保证「源码仍严格要求 exec_running」这条底线(`assets/gen.ts` 仍是源码)。
57
+ */
58
+ export const SOURCE_EXTENSIONS = Object.freeze([
59
+ '.rs', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py',
60
+ '.go', '.java', '.rb', '.c', '.cc', '.cpp', '.h', '.hpp', '.cs',
61
+ '.php', '.swift', '.kt', '.kts', '.scala', '.sh', '.bash', '.ps1',
62
+ '.lua', '.dart', '.ex', '.exs',
63
+ ]);
64
+
46
65
  /**
47
66
  * 是否为文档/笔记类目标(P0-4):扩展名命中,或位于**工作区根级 `docs/`** 下。
48
67
  * v1.3.3 复核:`docs` 必须直接位于会话工作区下(`src/docs/x.ts` 不算文档,防源码借名绕过相位门)。
@@ -69,6 +88,77 @@ function isUnderWorkspace(absolute, cwd) {
69
88
  return absolute === cwd || absolute.startsWith(`${cwd}\\`) || absolute.startsWith(`${cwd}/`);
70
89
  }
71
90
 
91
+ /**
92
+ * 是否为**静态展示资源**(v1.4.0;与 doc 同层豁免)。
93
+ *
94
+ * 命中条件(任一):
95
+ * - 扩展名 ∈ `PRESENTATIONAL_EXTENSIONS`(css/scss/html/svg/png/jpg/jpeg/webp);
96
+ * - 路径任一段为 `assets` 或 `public`(支持 `crates/cli/assets/index.html` 这类嵌套资产目录)。
97
+ *
98
+ * 源码扩展名(`.rs/.ts/.js/.py` 等)不在此列;混合目标仍按源码规则从严。
99
+ * @param {string} absolute 已归一化绝对路径
100
+ * @returns {boolean}
101
+ */
102
+ export function isPresentationalAssetPath(absolute) {
103
+ const ext = extname(absolute).toLowerCase();
104
+ if (PRESENTATIONAL_EXTENSIONS.includes(ext)) return true;
105
+ // 目录豁免不覆盖源码扩展名(`assets/gen.ts` 仍按源码门禁)。
106
+ if (SOURCE_EXTENSIONS.includes(ext)) return false;
107
+ const segments = absolute.split(/[\\/]/);
108
+ return segments.includes('assets') || segments.includes('public');
109
+ }
110
+
111
+ /** doc + 静态展示资源合并豁免判据(v1.4.0)。 */
112
+ function isExemptPath(absolute, cwd) {
113
+ return isDocPath(absolute, cwd) || isPresentationalAssetPath(absolute);
114
+ }
115
+
116
+ /**
117
+ * 写入门禁审计(v1.4.0):豁免放行时追加 JSONL 到 `.openfeel/log/guard-audit.jsonl`。
118
+ * best-effort:任何写失败都吞掉,不影响放行结果。
119
+ * @param {string} cwd 已归一化工作区
120
+ * @param {{ tool: string, reason: 'doc-asset'|'chore', phase: string, paths: string[] }} event
121
+ */
122
+ function recordGuardAudit(cwd, event) {
123
+ try {
124
+ const logDir = join(cwd, '.openfeel', 'log');
125
+ if (!existsSync(logDir)) {
126
+ mkdirSync(logDir, { recursive: true });
127
+ }
128
+ const entry = {
129
+ time: new Date().toISOString(),
130
+ event: 'guard_allow',
131
+ tool: event.tool,
132
+ reason: event.reason,
133
+ phase: event.phase,
134
+ paths: event.paths,
135
+ };
136
+ appendFileSync(join(logDir, 'guard-audit.jsonl'), `${JSON.stringify(entry)}\n`, 'utf-8');
137
+ } catch {
138
+ /* 审计写失败不阻塞放行 */
139
+ }
140
+ }
141
+
142
+ /**
143
+ * 读取 chore 写窗口(v1.4.0;`.openfeel/chore.json`)。
144
+ * 仅当文件存在、JSON 合法且 `expires_at` 未过期时返回对象,否则 null。
145
+ * @param {string} cwd 已归一化工作区
146
+ * @returns {{ id?: string, note?: string, expires_at?: string } | null}
147
+ */
148
+ function readActiveChore(cwd) {
149
+ try {
150
+ const chorePath = join(cwd, '.openfeel', 'chore.json');
151
+ if (!existsSync(chorePath)) return null;
152
+ const raw = JSON.parse(readFileSync(chorePath, 'utf-8'));
153
+ if (typeof raw !== 'object' || raw === null) return null;
154
+ const expiresAt = typeof raw.expires_at === 'string' ? Date.parse(raw.expires_at) : NaN;
155
+ if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) return null;
156
+ return raw;
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
161
+
72
162
  /** flow.json 读取缓存(path → { mtimeMs, size, value })。 */
73
163
  const flowCache = new Map();
74
164
 
@@ -340,7 +430,8 @@ export function createOpenFeelGuard(options = {}) {
340
430
  }
341
431
 
342
432
  const targets = rawTargets.map((target) => normalizeTarget(resolvedCwd, target));
343
- const allDoc = targets.every((target) => isDocPath(target, normalizedCwd));
433
+ // doc + 静态展示资源合并豁免(v1.4.0);混合目标须**全部**命中豁免才放行
434
+ const allExempt = targets.every((target) => isExemptPath(target, normalizedCwd));
344
435
 
345
436
  // 受保护路径:guard 启用时始终拒绝(enforce=off 已提前返回)。
346
437
  if (targets.some((target) => isProtectedPath(target, normalizedCwd))) {
@@ -394,8 +485,17 @@ export function createOpenFeelGuard(options = {}) {
394
485
  const active = activeStages(flow.value);
395
486
  if (active.length === 0) {
396
487
  if (allowGeneral && hasGeneralRequirement(flow.value)) return undefined;
397
- // P0-4:doc/note 类产物不要求活跃阶段(收口后仍可写文档);混合目标从严。
398
- if (allDoc) return undefined;
488
+ // v1.4.0 chore 通道:仅在**无活跃阶段**时生效;受保护路径已在上方拒绝。
489
+ const chore = readActiveChore(normalizedCwd);
490
+ if (chore !== null) {
491
+ recordGuardAudit(normalizedCwd, { tool: name, reason: 'chore', phase: 'done', paths: targets });
492
+ return undefined;
493
+ }
494
+ // P0-4/v1.4.0:doc/note 与静态展示资源不要求活跃阶段(收口后仍可写);混合目标从严。
495
+ if (allExempt) {
496
+ recordGuardAudit(normalizedCwd, { tool: name, reason: 'doc-asset', phase: 'done', paths: targets });
497
+ return undefined;
498
+ }
399
499
  return t(
400
500
  'OpenFeel 写入门禁:当前无活跃阶段,不能修改源码/文档。请先 feel_req add → claim → feel_stage add 建立阶段。',
401
501
  'OpenFeel write guard: no active stage, so source/docs are read-only. Create a stage via feel_req add → claim → feel_stage add.',
@@ -403,8 +503,14 @@ export function createOpenFeelGuard(options = {}) {
403
503
  }
404
504
 
405
505
  if (active.some((stage) => stage.phase === 'exec_running')) return undefined;
406
- // P0-4:非 exec 相位仍允许 doc/note(研究记录、设计文档等);代码仍严格。
407
- if (allDoc) return undefined;
506
+ // P0-4/v1.4.0:非 exec 相位仍允许 doc/note 与静态展示资源(研究记录、设计文档、UI 资源等);源码仍严格。
507
+ if (allExempt) {
508
+ const phases = active
509
+ .map((stage) => `${stage.id ?? '(unknown)'}=${stage.phase ?? '(unknown)'}`)
510
+ .join(', ');
511
+ recordGuardAudit(normalizedCwd, { tool: name, reason: 'doc-asset', phase: phases, paths: targets });
512
+ return undefined;
513
+ }
408
514
 
409
515
  const summary = active
410
516
  .map((stage) => `${stage.id ?? '(unknown)'}=${stage.phase ?? '(unknown)'}`)
@@ -141,6 +141,11 @@ Feel(你)只做编排、决策与门禁判断。
141
141
  - **归档判据不可协商**:版本归档要求该版本**全部阶段 done** 且**无 blocking open REV**,没有 force 覆盖。归档由 CLI 执行,调度侧只提示不代劳。
142
142
  - **不主动读归档**:常规调度不要读 \`.openfeel/flow-archive/**\`(防上下文污染);需要历史时用 \`feel_archive action=show\`。
143
143
 
144
+ ## 状态与记录(v1.5.0)
145
+ - **只有 Feel 写状态**:\`dev_last*\`、\`dev/current.md\`、\`status.md\`、\`flow.json\` 只由 Feel 更新;**子 agent 一律不读不写**(上下文由派发 prompt / 交接文档提供)。
146
+ - **日志不强制**:不写流水账;\`flow.json.log\` 由 CLI 自动维护(仅保留最近 200 条)。跨会话信息用一条 ≤5 行交接摘要。
147
+ - **更新节点**:开始 / 转折 / 结束,或 \`statusSync.due=true\`(默认 30 分钟)时更新一次交接摘要,随后运行 \`openfeel dev sync\`(CLI 在节点/到期时也会自动同步)。
148
+
144
149
  ## 三种工作流模式
145
150
  - \`full\`:plan → plan_review → scheme → exec → review → test → archive,六道关全走。
146
151
  - \`fast\`:跳过 \`scheme_*\` 与 \`test_*\`;**计划审查与代码审查各保留一轮**(fast 不等于免审)。
@@ -196,6 +201,11 @@ planner / schemer / executor / reviewer / feel-tester / archiver. **The authorit
196
201
  - **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.
197
202
  - **Do not read archives during normal scheduling**: avoid \`.openfeel/flow-archive/**\`; use \`feel_archive action=show\` on demand.
198
203
 
204
+ ## State & records (v1.5.0)
205
+ - **Only Feel writes state**: \`dev_last*\`, \`dev/current.md\`, \`status.md\`, \`flow.json\` are updated by Feel only; **subagents never read or write them** (context comes from the dispatch prompt / handoff docs).
206
+ - **No mandatory logs**: do not keep a per-step diary; \`flow.json.log\` is maintained by the CLI (last 200 entries only). For cross-session info, write a <=5-line handoff summary.
207
+ - **Update nodes**: start / turning point / end, or when \`statusSync.due=true\` (default 30 min) — update the handoff summary once, then run \`openfeel dev sync\` (the CLI also auto-syncs on nodes/expiry).
208
+
199
209
  ## The three workflow modes
200
210
  - \`full\`: plan → plan_review → scheme → exec → review → test → archive (all six gates).
201
211
  - \`fast\`: skips \`scheme_*\` and \`test_*\`; **plan review and code review each keep one round** (fast is not review-free).
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { FeelError } from '../adapters/cli.js';
15
15
  import { legalNextPhases, planNextStep, readSnapshot } from '../adapters/dashboard.js';
16
+ import { readStatusSync } from '../adapters/dev-status.js';
16
17
  import { resolveWorkspace } from '../adapters/workspace.js';
17
18
  import {
18
19
  boolean,
@@ -152,6 +153,7 @@ export function createTools(context) {
152
153
  withRequirements: args.with_requirements === true,
153
154
  });
154
155
  snapshot.project.root = resolved.cwd;
156
+ snapshot.statusSync = readStatusSync(resolved.cwd);
155
157
  const focus = snapshot.focus;
156
158
  return {
157
159
  data: snapshot,
@@ -182,6 +184,7 @@ export function createTools(context) {
182
184
  return envelope('next', async () => {
183
185
  const snapshot = await readSnapshot(cli, { cwd: resolved.cwd, signal: exec.signal });
184
186
  snapshot.project.root = resolved.cwd;
187
+ snapshot.statusSync = readStatusSync(resolved.cwd);
185
188
  const plan = planNextStep(snapshot);
186
189
  return {
187
190
  data: plan,
package/dsh/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openfeel-dsh",
3
- "version": "1.3.5",
3
+ "version": "1.5.1",
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.5"
35
+ "requiresCli": ">=1.5.1"
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.5",
3
+ "version": "1.5.1",
4
4
  "description": "AI Agent 开发流程治理 CLI 工具",
5
5
  "type": "module",
6
6
  "private": false,