nx-kv 0.18.0 → 0.19.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 (111) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/assets/kv-get/references/wait-workflow.md +4 -0
  3. package/package.json +1 -1
  4. package/src/core/workflow/edges.js +31 -0
  5. package/src/core/workflow/event-names.js +10 -0
  6. package/src/core/workflow/layout.js +119 -0
  7. package/src/core/workflow/lifecycle.js +58 -0
  8. package/src/core/workflow/plugin-registry.js +47 -0
  9. package/src/core/workflow/project.js +108 -0
  10. package/src/core/workflow/readiness.js +98 -0
  11. package/src/core/workflow/snapshot.example.json +46 -0
  12. package/src/core/workflow/types.js +169 -0
  13. package/src/modules/localtask/bus/index.js +41 -0
  14. package/src/modules/localtask/dag.js +6 -0
  15. package/src/modules/localtask/engine.js +49 -0
  16. package/src/modules/localtask/index.js +43 -2
  17. package/src/modules/localtask/mutator/index.js +153 -0
  18. package/src/modules/localtask/mutator/validate.js +62 -0
  19. package/src/modules/localtask/scheduler/index.js +55 -0
  20. package/src/modules/localtask/scheduler/state-drive.js +30 -0
  21. package/src/modules/localtask/service.js +140 -15
  22. package/src/modules/localtask/store/file-adapter.js +51 -0
  23. package/src/modules/localtask/store/index.js +100 -0
  24. package/src/modules/localtask/store/memory-adapter.js +26 -0
  25. package/src/modules/localtask/store/topic-adapter.js +43 -0
  26. package/src/modules/localtask/writes.js +120 -0
  27. package/src/modules/tasktool/add.js +11 -1
  28. package/src/modules/tasktool/facade/get.js +46 -0
  29. package/src/modules/tasktool/facade/index.js +4 -0
  30. package/src/modules/tasktool/facade/submit.js +49 -0
  31. package/src/modules/tasktool/facade/wait.js +3 -0
  32. package/src/modules/tasktool/getloop.js +2 -84
  33. package/src/modules/tasktool/plugins/base.js +92 -0
  34. package/src/modules/tasktool/plugins/blockgoal.js +23 -0
  35. package/src/modules/tasktool/plugins/container.js +47 -0
  36. package/src/modules/tasktool/plugins/discover.js +45 -0
  37. package/src/modules/tasktool/plugins/index.js +20 -0
  38. package/src/modules/tasktool/plugins/runtime.js +69 -0
  39. package/src/modules/tasktool/plugins/taskgoal-store.js +100 -0
  40. package/src/modules/tasktool/plugins/taskgoal.js +17 -0
  41. package/src/modules/tasktool/plugins/timegoal-store.js +748 -0
  42. package/src/modules/tasktool/plugins/timegoal.js +51 -0
  43. package/src/modules/tasktool/plugins/waitgoal-config.js +84 -0
  44. package/src/modules/tasktool/plugins/waitgoal.js +75 -0
  45. package/src/modules/tasktool/taskgoal.js +2 -100
  46. package/src/modules/tasktool/timegoal.js +2 -748
  47. package/src/modules/tasktool/wait.js +25 -0
  48. package/src/modules/templates/index.js +21 -0
  49. package/src/modules/templates/service.js +62 -9
  50. package/src/shared/frontmatter.js +28 -1
  51. package/src/web/frontend/App.jsx +15 -46
  52. package/src/web/frontend/components/layout/breadcrumb.jsx +26 -0
  53. package/src/web/frontend/components/layout/crumbs.js +34 -0
  54. package/src/web/frontend/components/md/md-panes.jsx +10 -6
  55. package/src/web/frontend/components/md/shortcut-sheet.jsx +1 -0
  56. package/src/web/frontend/components/md/tool-bar.jsx +11 -0
  57. package/src/web/frontend/components/sidebar/index.jsx +4 -3
  58. package/src/web/frontend/modules/md/md-parse.js +122 -3
  59. package/src/web/frontend/modules/md/style.css +54 -5
  60. package/src/web/frontend/modules/md/use-templates.js +7 -3
  61. package/src/web/frontend/modules/prompts/placeholders.js +98 -0
  62. package/src/web/frontend/modules/prompts/prompt-editor.jsx +169 -0
  63. package/src/web/frontend/modules/prompts/style.css +63 -0
  64. package/src/web/frontend/modules/prompts/template-slots.jsx +87 -0
  65. package/src/web/frontend/page/dir/landing.jsx +11 -0
  66. package/src/web/frontend/page/dir/task.jsx +203 -38
  67. package/src/web/frontend/page/group/index.jsx +8 -2
  68. package/src/web/frontend/page/progress/index.jsx +19 -3
  69. package/src/web/frontend/page/settings/index.jsx +14 -133
  70. package/src/web/frontend/page/settings/prompts-section.jsx +249 -0
  71. package/src/web/frontend/page/settings/style.css +39 -0
  72. package/src/web/frontend/page/templates/index.jsx +27 -0
  73. package/src/web/frontend/panels.jsx +19 -10
  74. package/src/web/frontend/style-v2.css +20 -1
  75. package/src/web/frontend/workspace/areas.js +55 -0
  76. package/src/web/frontend/workspace.js +272 -7
  77. package/src/web/public/assets/Introduction-C4qC8N7g.js +1 -0
  78. package/src/web/public/assets/attach-BZrdOH46.css +1 -0
  79. package/src/web/public/assets/attach-DcyGhCUW.js +1 -0
  80. package/src/web/public/assets/cloud-D0SRl665.js +3 -0
  81. package/src/web/public/assets/dir-B4egV6kV.js +3 -0
  82. package/src/web/public/assets/group-Cbd1PVuv.js +1 -0
  83. package/src/web/public/assets/index-DidFyMnw.js +50 -0
  84. package/src/web/public/assets/index-fhYYxphP.css +1 -0
  85. package/src/web/public/assets/md-history-C29-dF29.js +5 -0
  86. package/src/web/public/assets/member-picker-B7dnMq25.js +1 -0
  87. package/src/web/public/assets/overview-CX9XZ5EO.js +1 -0
  88. package/src/web/public/assets/progress-CnHK_jKP.js +3 -0
  89. package/src/web/public/assets/repos-CyxHTr1L.js +10 -0
  90. package/src/web/public/assets/{settings-DM779jcF.css → settings-C5xCFk_y.css} +1 -1
  91. package/src/web/public/assets/settings-DIvkm7lx.js +1 -0
  92. package/src/web/public/assets/task-CsEvkBW7.js +2 -0
  93. package/src/web/public/assets/template-slots-BybxMN3v.js +1 -0
  94. package/src/web/public/assets/template-slots-CNdlIB1I.css +1 -0
  95. package/src/web/public/assets/templates-BtdK84si.js +2 -0
  96. package/src/web/public/assets/{upload-BupH8dMK.js → upload-jOng3KOU.js} +1 -1
  97. package/src/web/public/index.html +4 -2
  98. package/src/web/public/assets/Introduction-CLTMLBZ5.js +0 -1
  99. package/src/web/public/assets/cloud-B9_h1Ym7.js +0 -3
  100. package/src/web/public/assets/dir-BLUQranN.js +0 -3
  101. package/src/web/public/assets/group-DIeOm80x.js +0 -1
  102. package/src/web/public/assets/index-DA98QEzx.css +0 -1
  103. package/src/web/public/assets/index-t7PgSf_t.js +0 -50
  104. package/src/web/public/assets/md-history-CDVA0KIl.js +0 -5
  105. package/src/web/public/assets/member-picker-9MAmKm9d.js +0 -1
  106. package/src/web/public/assets/overview-DNLRHNzP.js +0 -1
  107. package/src/web/public/assets/progress-C0MiaOW0.js +0 -3
  108. package/src/web/public/assets/repos--i4W5fzj.js +0 -10
  109. package/src/web/public/assets/settings-DSx2Wsto.js +0 -1
  110. package/src/web/public/assets/task-D2-CWZXv.js +0 -2
  111. package/src/web/public/assets/templates-DD6m8DT4.js +0 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,68 @@
2
2
 
3
3
  本文件记录对外可见的变更。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。
4
4
 
5
+ ## [0.19.0] - 2026-11-04 提示词/模板收敛到设置面板 + 面板无页签化(面包屑 + 区域策略)+ DAG 引擎与 goal 插件族
6
+
7
+ ### Added
8
+
9
+ - **提示词 / 模板统一收敛到设置面板一栏(任务 #32)**:此前提示词与模板散在
10
+ 「模板与任务工具」区、模板页、项目级设置三处,各抄一份编辑器、风格也不一。
11
+ 现在设置面板新增「**提示词 / 模板**」一栏,三样收在一起:
12
+ - **任务模板**:正文里的 `{{槽位}}` 是槽位,**骨架固定**(内置模板不可改结构)
13
+ 只填槽位内容——设置面板与模板页都能填(`template update --values`);
14
+ 新建任务时正文按槽位值渲染。
15
+ - **回调提示词**(全局,多条):与收尾指令同一份存储与语义。
16
+ - **ack 完成总结提示词**(全局,多条,**新增**):agent 写完成总结(ack)时的
17
+ 格式要求,随领取交付的 `instruction` 一起给到 agent;占位符语法与收尾指令
18
+ 同款(`{{task.id}}` / `{{goal.*}}` / 段落 `{{#goal}}…{{/goal}}`)。
19
+ **默认不配置 = 交付里不追加任何东西**(默认交付逐字不变)。
20
+ CLI 同口径:`localtask config --ack "<提示词>"`。
21
+ - **共享的提示词编辑器**(`modules/prompts/`):文本框 + 「占位符」插入菜单 +
22
+ 已用占位符标记,回调提示词 / ack 提示词 / 持续 get 模板 / 模板槽位共用一份实现
23
+ ——此前三套长得不一样的编辑器,现在只有一套。
24
+ - **面板模型收敛(任务 #33)**:工作区统一为四个区域——sidebar / maincontent /
25
+ rightSidebar / float;面板落点由一张**策略表**声明(单例与否一并声明),
26
+ 打开新面板即替换主内容区(主区是**单面板位**)。页签与页签条彻底删除:
27
+ 面板头部不再有 float / popup 标识,「清空本组全部页签」垃圾桶按钮一并移除;
28
+ 顶部新增**面包屑**显示当前位置(如 `[ nx / nx-kv / #28 ]`)。老布局读盘
29
+ 自动愈合(关页签条、补右侧栏)。
30
+ - **DAG 依赖与闸门(任务 #34)**:`localtask add --block-by 12,13`(依赖:这些
31
+ 任务全部完成后本条才可领取)、`--gate`(闸门:自身未完成前挡住队列里排在
32
+ 后面的 pending);`localtask update` 可改依赖 / 设清闸门。领取按 D-first 取
33
+ 第一条**可领**的 pending,目录任务列表带「闸门 / 锁定」徽标。
34
+ - **workflow 契约与引擎骨架(任务 #34 阶段 0)**:`core/workflow/` 冻结类型契约
35
+ (workflowId → 主题 key;对外稳定门面只有 submit / get;goal 是插件族:
36
+ block=调度侧、wait/time=消费侧);`localtask` 拆出 store / bus / mutator /
37
+ scheduler 分层(唯一写快照入口 Mutator,原子覆盖);`tasktool/plugins/`
38
+ 目录扫描自注册(base / blockgoal / container / taskgoal / timegoal /
39
+ waitgoal),`tasktool/facade/` 收敛 submit / get / wait 门面。
40
+ - **评论任务旁路原文**:工具栏「原文」或 Ctrl/⌘⇧O 在主区右侧并排打开锚定
41
+ 原文(`frontmatter.parent`,旧数据从「锚定自…」回推),可单独关掉、不改路由。
42
+ - **验收条 ack 按 md 渲染**:完成总结走正文同一套渲染;过长默认夹紧、
43
+ 可展开全文(任务 #20)。
44
+
45
+ ### Changed
46
+
47
+ - **面板贴图嵌入修复(任务 #15 再开)**:实时预览的块内光标坐标与源码全文偏移
48
+ 分开(`insertAttachmentSnippet` 双坐标),落点禁止进 frontmatter;已落库任务
49
+ 贴图后立刻 PATCH 同步服务端,不再等「保存并同步」——修掉「贴图上传成功但
50
+ 正文未嵌入、agent 领取 attachments count: 0」。
51
+ - **frontmatter 键自愈**:贴图误插撕出的残留键(`board:` 被撕成 `oard:` / `d:`)
52
+ 解析时升回规范键、保存时写回(#15 / #23 案例),组面板不再丢任务。
53
+ - **模板槽位不露 {{标记}}**:新建任务正文一律按槽位值渲染(缺省用内置默认值
54
+ 兜底),`{{背景}}` 之类标记不再出现在任务正文里;设置面板「提示词 / 模板」
55
+ 栏改左 tab 布局,槽位显示名字不给标记。
56
+ - **右栏可关 + 进程面板跟随**:进程面板可关闭(右栏随之收起);主区切到别的
57
+ 任务时已开的进程面板同步跟随(#33 验收反馈);侧栏收起入口挪到品牌行。
58
+ - **面包屑带组名**:目录页面包屑显示 `[ 组 / 目录 / 任务 ]`(层级抽成纯函数
59
+ `crumbs.js`,面板与单测共用)。
60
+
61
+ ### Fixed
62
+
63
+ - **float 面板(设置)自调用栈溢出**:`openPanel` 的 float 分支原先递归调
64
+ `openFloatingPanel`,表现是「设置按钮点了没反应」;改为直接走
65
+ 「加进主区 → floatTab 浮出」,`openFloatingPanel` 只做委托。
66
+
5
67
  ## [0.18.0] - 2026-11-04 描述绑定文档 + 描述弹窗
6
68
 
7
69
  ### Changed
@@ -97,6 +97,10 @@ wait 必带一句当前现状总结(≤2000 字,连续空白会折叠为单
97
97
  `{{goal.name}} {{goal.remainingHuman}} {{goal.prompt}} {{task.text}} {{source}} {{name}}…`,
98
98
  段落 `{{#goal}}…{{/goal}}` 上下文缺失整段消失;`--no-suffix` 关闭、
99
99
  `--reset-suffix` 恢复默认)。字段缺席 = 已关闭或本轮没有可渲染内容。
100
+ - **ack 提示词(完成总结的格式要求)**:`nx-kv localtask config --ack "<提示词>"`
101
+ (占位符同上,多条)。配置后,它会随 `instruction` 一起交给你——按它要求的格式
102
+ 写 `localtask done --summary "<完成总结>"` 即可;没配置时 `instruction` 里没有
103
+ 这段,格式自便。设置面板「提示词 / 模板」一栏也能配。
100
104
 
101
105
  超时(exit 0,业务结果而非错误):
102
106
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nx-kv",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "nx-kv — KV 清单管理中枢(远程)+ 本机本地任务队列(localtask):serve 驱动 CLI 与 Web 面板,同源同构,附带可安装 skill 供 agent 提交 / 领取 / 阻塞接收任务",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,31 @@
1
+ import { edgesFromTasks } from './types.js';
2
+
3
+ export { edgesFromTasks };
4
+
5
+ /**
6
+ * 从快照边表收集某节点的前驱 id。
7
+ * @param {import('./types.js').WorkflowSnapshot} snap
8
+ * @param {string|number} nodeId
9
+ */
10
+ export function predecessorsOf(snap, nodeId) {
11
+ const id = String(nodeId);
12
+ const out = [];
13
+ for (const e of snap?.edges || []) {
14
+ if (String(e.to) === id) out.push(e.from);
15
+ }
16
+ return out;
17
+ }
18
+
19
+ /**
20
+ * 某节点的后继 id。
21
+ * @param {import('./types.js').WorkflowSnapshot} snap
22
+ * @param {string|number} nodeId
23
+ */
24
+ export function successorsOf(snap, nodeId) {
25
+ const id = String(nodeId);
26
+ const out = [];
27
+ for (const e of snap?.edges || []) {
28
+ if (String(e.from) === id) out.push(e.to);
29
+ }
30
+ return out;
31
+ }
@@ -0,0 +1,10 @@
1
+ /** workflow EventBus 事件名(冻结) */
2
+ export const EVENTS = Object.freeze({
3
+ WORKFLOW_CREATED: 'workflow:created',
4
+ DAG_MUTATED: 'dag:mutated',
5
+ NODE_STATE_CHANGED: 'node:stateChanged',
6
+ GOAL_BOUND: 'goal:bound',
7
+ GOAL_UNBOUND: 'goal:unbound',
8
+ GOAL_CONFIG_UPDATED: 'goal:configUpdated',
9
+ WORKFLOW_COMPLETED: 'workflow:completed',
10
+ });
@@ -0,0 +1,119 @@
1
+ // workflow 目标目录规范(按 nx-kv 分层 + 语义子目录)。
2
+ //
3
+ // 原则
4
+ // 1. 禁止平行顶层 src/{facade,store,mutator,scheduler,plugins}/。
5
+ // 同名语义**必须**落在 modules 或 core/workflow 的子目录里。
6
+ // 2. **全部 goal 同一目录、每种一份**——block / wait / time / task 都在
7
+ // tasktool/plugins/;禁止再拆「core 一份 + modules 一份」。
8
+ // 3. localtask = 写侧(store/mutator/scheduler/bus);
9
+ // tasktool = 消费门面 + goal 族;二者成对,允许互引
10
+ // (scheduler 调 blockgoal → import tasktool/plugins 即可)。
11
+ // 4. 面板只吃 /api;两端同构投影才进 shared/。
12
+ // 5. JS ESM only;子目录均带 index.js 桶文件对外导出。
13
+ //
14
+ // 依赖方向(单向)
15
+ // web → modules → core/workflow → Node 内置
16
+ // tasktool/facade ──▶ localtask/{store,mutator}
17
+ // localtask/scheduler ──▶ tasktool/plugins/blockgoal + core readiness + bus
18
+ // localtask/mutator ──▶ localtask/store
19
+ // tasktool/plugins/runtime ──▶ 同目录各 goal
20
+ //
21
+ // ---------------------------------------------------------------------------
22
+ // 目标树(✅ 已有 / ○ 待落地 / → 自现状迁入)
23
+ // ---------------------------------------------------------------------------
24
+ //
25
+ // src/core/workflow/
26
+ // ├── types.js ✅
27
+ // ├── snapshot.example.json ✅
28
+ // ├── layout.js ✅ 本规范
29
+ // ├── readiness.js ✅ 自 dag 下沉
30
+ // ├── edges.js ✅
31
+ // ├── event-names.js ✅
32
+ // └── plugin-registry.js ✅ 按 name / scope 校验
33
+ //
34
+ // src/modules/localtask/
35
+ // ├── index.js / service.js ✅ 模块壳;complete → refreshTopicAfterComplete
36
+ // ├── dag.js ✅ 再导出 readiness
37
+ // ├── engine.js / writes.js ✅ 主题引擎;DAG 日常写经 Mutator
38
+ // ├── store/ ✅ SnapshotStore + memory/file/topic adapter
39
+ // ├── bus/ mutator/ scheduler/ ✅
40
+ //
41
+ // src/modules/tasktool/
42
+ // ├── index.js ✅
43
+ // ├── facade/ ✅ submit/get/wait 再导出
44
+ // ├── plugins/ ✅ 目录扫描 IoC + 实现同目录
45
+ // │ ├── discover.js 扫描注册(跳过 *-store/*-config)
46
+ // │ ├── *-store.js / waitgoal-config 原 timegoal/taskgoal/getloop 迁入
47
+ // │ └── block|wait|time|task goal.js definePlugin
48
+ // ├── facade/ ✅ submit→store+入队;get→AsyncIterable
49
+ // ├── groupctx.js / attach.js ✅
50
+ // └── getloop/timegoal/taskgoal.js ✅ 薄再导出 → plugins/*
51
+ //
52
+ // 为何 blockgoal 也进 tasktool/plugins(不留 core)
53
+ // goal 是插件族,视觉与注册平级;拆到 core 会造成「调度 goal 一套路径、
54
+ // 消费 goal 另一套路径」。localtask↔tasktool 本就可配对互引,scheduler
55
+ // 直接 import blockgoal 即可,无需为避依赖把家族拆开。
56
+ //
57
+ // ---------------------------------------------------------------------------
58
+
59
+ /** @type {Readonly<Record<string, string>>} */
60
+ export const TARGET_PATHS = Object.freeze({
61
+ 'core types': 'src/core/workflow/types.js',
62
+ 'snapshot example': 'src/core/workflow/snapshot.example.json',
63
+ 'this layout norm': 'src/core/workflow/layout.js',
64
+ readiness: 'src/core/workflow/readiness.js',
65
+ 'plugin registry': 'src/core/workflow/plugin-registry.js',
66
+ 'store/': 'src/modules/localtask/store/',
67
+ 'store barrel': 'src/modules/localtask/store/index.js',
68
+ 'store file-adapter': 'src/modules/localtask/store/file-adapter.js',
69
+ 'store memory-adapter': 'src/modules/localtask/store/memory-adapter.js',
70
+ 'bus/': 'src/modules/localtask/bus/',
71
+ 'bus barrel': 'src/modules/localtask/bus/index.js',
72
+ 'mutator/': 'src/modules/localtask/mutator/',
73
+ 'mutator barrel': 'src/modules/localtask/mutator/index.js',
74
+ 'mutator validate': 'src/modules/localtask/mutator/validate.js',
75
+ 'scheduler/': 'src/modules/localtask/scheduler/',
76
+ 'scheduler barrel': 'src/modules/localtask/scheduler/index.js',
77
+ 'scheduler state-drive': 'src/modules/localtask/scheduler/state-drive.js',
78
+ 'facade/': 'src/modules/tasktool/facade/',
79
+ 'facade barrel': 'src/modules/tasktool/facade/index.js',
80
+ 'facade submit': 'src/modules/tasktool/facade/submit.js',
81
+ 'facade get': 'src/modules/tasktool/facade/get.js',
82
+ 'facade wait': 'src/modules/tasktool/facade/wait.js',
83
+ 'plugins/': 'src/modules/tasktool/plugins/',
84
+ 'plugins barrel': 'src/modules/tasktool/plugins/index.js',
85
+ 'plugins runtime': 'src/modules/tasktool/plugins/runtime.js',
86
+ blockgoal: 'src/modules/tasktool/plugins/blockgoal.js',
87
+ waitgoal: 'src/modules/tasktool/plugins/waitgoal.js',
88
+ timegoal: 'src/modules/tasktool/plugins/timegoal.js',
89
+ taskgoal: 'src/modules/tasktool/plugins/taskgoal.js',
90
+ });
91
+
92
+ /** 全部 goal 唯一落点(同一目录;禁止第二份) */
93
+ export const GOAL_HOMES = Object.freeze({
94
+ blockgoal: 'src/modules/tasktool/plugins/blockgoal.js',
95
+ waitgoal: 'src/modules/tasktool/plugins/waitgoal.js',
96
+ timegoal: 'src/modules/tasktool/plugins/timegoal.js',
97
+ taskgoal: 'src/modules/tasktool/plugins/taskgoal.js',
98
+ });
99
+
100
+ /** 必须出现的语义子目录(相对仓库根) */
101
+ export const REQUIRED_SEMANTIC_DIRS = Object.freeze([
102
+ 'src/modules/localtask/store',
103
+ 'src/modules/localtask/bus',
104
+ 'src/modules/localtask/mutator',
105
+ 'src/modules/localtask/scheduler',
106
+ 'src/modules/tasktool/facade',
107
+ 'src/modules/tasktool/plugins',
108
+ ]);
109
+
110
+ /** 禁止的平行顶层 */
111
+ export const FORBIDDEN_TOP_LEVEL = Object.freeze([
112
+ 'src/facade',
113
+ 'src/store',
114
+ 'src/mutator',
115
+ 'src/scheduler',
116
+ 'src/plugins',
117
+ 'src/bus',
118
+ 'src/core/types',
119
+ ]);
@@ -0,0 +1,58 @@
1
+ // Workflow / Node / Plugin 生命周期(冻结)。插件只挂钩子,不拥有可变状态。
2
+ //
3
+ // ┌─ Workflow ──────────────────────────────────────────────┐
4
+ // │ created ──▶ active ──▶ completed | failed │
5
+ // │ │ │ │
6
+ // │ │ ├─ Mutator 改拓扑/goal → dag:mutated │
7
+ // │ │ ├─ Scheduler onSchedule(永不 onGet) │
8
+ // │ │ └─ Facade get → onGet 消费流 │
9
+ // └─────────────────────────────────────────────────────────┘
10
+ //
11
+ // ┌─ Node ──────────────────────────────────────────────────┐
12
+ // │ pending ──▶ ready ──▶ running ──▶ success | failed │
13
+ // │ ▲ │ │ │
14
+ // │ └─ 屏障加回 │ └─ done / worker │
15
+ // │ └─ Scheduler driveReadyStates │
16
+ // └─────────────────────────────────────────────────────────┘
17
+ //
18
+ // ┌─ Plugin 实例钩子(按绑定点注入)──────────────────────────┐
19
+ // │ onBind Mutator.bindPlugin 时:校验并规范化 config │
20
+ // │ onSchedule Scheduler 轮:仅 scope=control(blockgoal)│
21
+ // │ onGet Facade get:scope=workflow|node 消费流 │
22
+ // │ onUnbind Mutator.unbindPlugin 时:可选清理(无状态) │
23
+ // │ │
24
+ // │ 叠加:workflow.timegoal 包裹 node.waitgoal │
25
+ // │ 约束:调度链禁止调 onGet;消费链禁止写快照(只读 ctx) │
26
+ // └─────────────────────────────────────────────────────────┘
27
+
28
+ /** @typedef {'created'|'active'|'completed'|'failed'} WorkflowPhase */
29
+ /** @typedef {'pending'|'ready'|'running'|'success'|'failed'} NodePhase */
30
+ /** @typedef {'onBind'|'onSchedule'|'onGet'|'onUnbind'} PluginHook */
31
+
32
+ export const WORKFLOW_PHASES = Object.freeze([
33
+ 'created',
34
+ 'active',
35
+ 'completed',
36
+ 'failed',
37
+ ]);
38
+
39
+ export const NODE_PHASES = Object.freeze([
40
+ 'pending',
41
+ 'ready',
42
+ 'running',
43
+ 'success',
44
+ 'failed',
45
+ ]);
46
+
47
+ /** 插件可实现的钩子;未实现视为空操作 */
48
+ export const PLUGIN_HOOKS = Object.freeze(['onBind', 'onSchedule', 'onGet', 'onUnbind']);
49
+
50
+ /**
51
+ * 哪条执行链会调用哪个钩子。
52
+ * @type {Readonly<Record<'schedule'|'get'|'mutate', PluginHook[]>>}
53
+ */
54
+ export const HOOK_CHAINS = Object.freeze({
55
+ mutate: Object.freeze(['onBind', 'onUnbind']),
56
+ schedule: Object.freeze(['onSchedule']),
57
+ get: Object.freeze(['onGet']),
58
+ });
@@ -0,0 +1,47 @@
1
+ import { BUILTIN_GOALS, GOAL_SCOPES } from './types.js';
2
+
3
+ /** @typedef {import('./types.js').GoalScope} GoalScope */
4
+ /** @typedef {import('./types.js').PluginSpec} PluginSpec */
5
+
6
+ /** @type {Map<string, { name: string, scope: GoalScope }>} */
7
+ const registry = new Map();
8
+
9
+ export function registerPluginMeta(name, scope) {
10
+ if (!name || typeof name !== 'string') throw new Error('plugin name required');
11
+ if (!GOAL_SCOPES.includes(scope)) throw new Error(`invalid goal scope: ${scope}`);
12
+ registry.set(name, { name, scope });
13
+ return registry.get(name);
14
+ }
15
+
16
+ export function getPluginMeta(name) {
17
+ return registry.get(name) || null;
18
+ }
19
+
20
+ export function listPluginMetas() {
21
+ return [...registry.values()];
22
+ }
23
+
24
+ /** 校验 PluginSpec:已注册且 scope 匹配(未注册则只认内置名) */
25
+ export function assertPluginSpec(spec) {
26
+ if (!spec || typeof spec !== 'object') throw new Error('PluginSpec required');
27
+ const name = String(spec.name || '');
28
+ if (!name) throw new Error('PluginSpec.name required');
29
+ const meta = registry.get(name);
30
+ const scope = spec.scope || meta?.scope;
31
+ if (!scope || !GOAL_SCOPES.includes(scope)) {
32
+ throw new Error(`PluginSpec.scope invalid for ${name}`);
33
+ }
34
+ if (meta && meta.scope !== scope) {
35
+ throw new Error(`plugin ${name} scope must be ${meta.scope}, got ${scope}`);
36
+ }
37
+ if (!meta && !Object.values(BUILTIN_GOALS).includes(name) && name !== 'taskgoal') {
38
+ throw new Error(`unknown plugin: ${name}`);
39
+ }
40
+ return { name, scope, config: spec.config && typeof spec.config === 'object' ? spec.config : {} };
41
+ }
42
+
43
+ // 内置登记(实现落在 tasktool/plugins;此处只登记元数据)
44
+ registerPluginMeta(BUILTIN_GOALS.blockgoal, 'control');
45
+ registerPluginMeta(BUILTIN_GOALS.waitgoal, 'node');
46
+ registerPluginMeta(BUILTIN_GOALS.timegoal, 'workflow');
47
+ registerPluginMeta('taskgoal', 'node');
@@ -0,0 +1,108 @@
1
+ // 主题队列(localtask topic.tasks)↔ WorkflowSnapshot 双向投影。
2
+ // 存盘形状仍是任务行;快照是引擎视图。正文不进快照(payload.textRef)。
3
+
4
+ import { BUILTIN_GOALS, edgesFromTasks, taskToNode } from './types.js';
5
+
6
+ /** @typedef {import('./types.js').WorkflowSnapshot} WorkflowSnapshot */
7
+ /** @typedef {import('./types.js').WfNode} WfNode */
8
+
9
+ /**
10
+ * @param {object} topic localtask 主题桶
11
+ * @returns {WorkflowSnapshot}
12
+ */
13
+ export function topicToSnapshot(topic) {
14
+ const workflowId = String(topic?.key || topic?.workflowId || '');
15
+ const tasks = Array.isArray(topic?.tasks) ? topic.tasks : [];
16
+ const nodes = tasks.filter((t) => t && !t.deletedAt && !t.migratedTo).map(taskToNode);
17
+ const edges = edgesFromTasks(tasks);
18
+ const revision = Number(topic?.workflowRevision) || 0;
19
+ /** @type {WorkflowSnapshot} */
20
+ return {
21
+ workflowId,
22
+ state: topic?.workflowState || 'active',
23
+ nodes,
24
+ edges,
25
+ workflowPlugins: Array.isArray(topic?.workflowPlugins) ? topic.workflowPlugins : [],
26
+ revision,
27
+ updatedAt: topic?.updatedAt || new Date().toISOString(),
28
+ meta: {
29
+ cwd: topic?.cwd || '',
30
+ name: topic?.name || '',
31
+ },
32
+ };
33
+ }
34
+
35
+ /**
36
+ * 把快照写回主题任务行(保守:不改 text;按 id 对齐)。
37
+ * @param {object} topic
38
+ * @param {WorkflowSnapshot} snap
39
+ */
40
+ export function applySnapshotToTopic(topic, snap) {
41
+ if (!topic || !snap) return topic;
42
+ const tasks = Array.isArray(topic.tasks) ? topic.tasks : (topic.tasks = []);
43
+ const byId = new Map(tasks.map((t) => [String(t.id), t]));
44
+
45
+ for (const node of snap.nodes || []) {
46
+ let task = byId.get(String(node.id));
47
+ if (!task) {
48
+ // 引擎新建的节点 → 补一条占位任务(正文外置占位)
49
+ task = {
50
+ id: Number(node.id) || node.id,
51
+ text: '',
52
+ status: 'pending',
53
+ createdAt: new Date().toISOString().replace('T', ' ').slice(0, 19),
54
+ completedAt: '',
55
+ deletedAt: '',
56
+ blockBy: [],
57
+ blocksFollowing: false,
58
+ };
59
+ tasks.push(task);
60
+ byId.set(String(task.id), task);
61
+ }
62
+ syncTaskFromNode(task, node);
63
+ }
64
+
65
+ // 边 → blockBy(显式依赖);闸门边由 blocksFollowing 表达,不写进每条 blockBy
66
+ const gateIds = new Set(
67
+ (snap.nodes || [])
68
+ .filter(
69
+ (n) =>
70
+ n.kind === 'control' ||
71
+ (n.plugins || []).some((p) => p.name === BUILTIN_GOALS.blockgoal),
72
+ )
73
+ .map((n) => String(n.id)),
74
+ );
75
+
76
+ for (const task of tasks) {
77
+ if (task.deletedAt || task.migratedTo) continue;
78
+ const incoming = (snap.edges || []).filter((e) => String(e.to) === String(task.id));
79
+ const deps = [];
80
+ for (const e of incoming) {
81
+ // 跳过「闸门挡后续」推出来的边:from 是闸门且闸门在数组序上早于本任务时由 blocksFollowing 覆盖
82
+ if (gateIds.has(String(e.from))) continue;
83
+ deps.push(e.from);
84
+ }
85
+ task.blockBy = deps.map((d) => Number(d)).filter((n) => Number.isFinite(n) && n > 0);
86
+ }
87
+
88
+ topic.workflowRevision = Number(snap.revision) || 0;
89
+ topic.workflowState = snap.state || 'active';
90
+ topic.workflowPlugins = Array.isArray(snap.workflowPlugins) ? snap.workflowPlugins : [];
91
+ if (snap.updatedAt) topic.updatedAt = snap.updatedAt;
92
+ return topic;
93
+ }
94
+
95
+ /** @param {object} task @param {WfNode} node */
96
+ function syncTaskFromNode(task, node) {
97
+ const hasBlock = (node.plugins || []).some((p) => p.name === BUILTIN_GOALS.blockgoal);
98
+ task.blocksFollowing = node.kind === 'control' || hasBlock;
99
+
100
+ if (node.state === 'success') {
101
+ if (!task.completedAt) {
102
+ task.completedAt = new Date().toISOString().replace('T', ' ').slice(0, 19);
103
+ }
104
+ } else if (node.state === 'running' && task.status !== 'consumed') {
105
+ task.status = 'consumed';
106
+ }
107
+ // pending/ready:不改 status(领取态由 wait/get 管;ready 由 isReadyToClaim 现算)
108
+ }
@@ -0,0 +1,98 @@
1
+ // 拓扑就绪(自 localtask/dag.js 下沉)。任务行与快照节点两套入口。
2
+
3
+ /** 规范化 blockBy:正整数 id、去重、丢掉自身 */
4
+ export function normalizeBlockBy(raw, selfId) {
5
+ const self = Number(selfId);
6
+ const seen = new Set();
7
+ const out = [];
8
+ for (const x of Array.isArray(raw) ? raw : []) {
9
+ const n = Number(x);
10
+ if (!Number.isFinite(n) || n <= 0) continue;
11
+ if (Number.isFinite(self) && n === self) continue;
12
+ if (seen.has(n)) continue;
13
+ seen.add(n);
14
+ out.push(n);
15
+ }
16
+ return out;
17
+ }
18
+
19
+ function isAlive(t) {
20
+ return t && !t.deletedAt && !t.migratedTo;
21
+ }
22
+
23
+ function isCompleted(t) {
24
+ return isAlive(t) && !!t.completedAt;
25
+ }
26
+
27
+ function isPendingLike(t) {
28
+ return isAlive(t) && t.status !== 'consumed';
29
+ }
30
+
31
+ /**
32
+ * 某条 pending 现在能不能领(任务行模型)。
33
+ * @param {object} task
34
+ * @param {object[]} all
35
+ */
36
+ export function isReadyToClaim(task, all = []) {
37
+ if (!isPendingLike(task)) return false;
38
+ const byId = new Map((all || []).filter(isAlive).map((t) => [Number(t.id), t]));
39
+
40
+ const deps = normalizeBlockBy(task.blockBy, task.id);
41
+ for (const id of deps) {
42
+ const dep = byId.get(id);
43
+ if (!dep) continue;
44
+ if (!isCompleted(dep)) return false;
45
+ }
46
+
47
+ const idx = (all || []).indexOf(task);
48
+ const order = all || [];
49
+ const myPos = idx >= 0 ? idx : order.findIndex((t) => Number(t.id) === Number(task.id));
50
+ if (myPos < 0) return true;
51
+ for (let i = 0; i < myPos; i += 1) {
52
+ const prev = order[i];
53
+ if (!isAlive(prev) || !prev.blocksFollowing) continue;
54
+ if (!isCompleted(prev)) return false;
55
+ }
56
+ return true;
57
+ }
58
+
59
+ /** 按数组序找第一条可领取的 pending;没有 → null */
60
+ export function findFirstReadyPending(tasks = []) {
61
+ for (const t of tasks || []) {
62
+ if (isReadyToClaim(t, tasks)) return t;
63
+ }
64
+ return null;
65
+ }
66
+
67
+ /**
68
+ * 快照节点是否就绪(边 + control/blockgoal 前驱须 success)。
69
+ * @param {import('./types.js').WorkflowSnapshot} snap
70
+ * @param {import('./types.js').WfNode} node
71
+ * @param {(snap: import('./types.js').WorkflowSnapshot, nodeId: string|number) => boolean} [isBarrierOpen]
72
+ */
73
+ export function isNodeReady(snap, node, isBarrierOpen) {
74
+ if (!node || node.state === 'success' || node.state === 'failed' || node.state === 'running') {
75
+ return false;
76
+ }
77
+ if (node.state !== 'pending' && node.state !== 'ready') return false;
78
+
79
+ const preds = (snap.edges || []).filter((e) => String(e.to) === String(node.id));
80
+ const byId = new Map((snap.nodes || []).map((n) => [String(n.id), n]));
81
+
82
+ for (const e of preds) {
83
+ const pred = byId.get(String(e.from));
84
+ if (!pred) continue;
85
+ const isControl =
86
+ pred.kind === 'control' ||
87
+ (pred.plugins || []).some((p) => p.name === 'blockgoal' || p.scope === 'control');
88
+ if (isControl) {
89
+ const open = isBarrierOpen
90
+ ? isBarrierOpen(snap, pred.id)
91
+ : pred.state === 'success';
92
+ if (!open) return false;
93
+ continue;
94
+ }
95
+ if (pred.state !== 'success') return false;
96
+ }
97
+ return true;
98
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "workflowId": "local/nx-kv",
3
+ "state": "active",
4
+ "revision": 1,
5
+ "updatedAt": "2026-11-04T08:00:00.000Z",
6
+ "workflowPlugins": [
7
+ {
8
+ "name": "timegoal",
9
+ "scope": "workflow",
10
+ "config": { "duration": "45m", "prompt": "目录级静默超时示例" }
11
+ }
12
+ ],
13
+ "nodes": [
14
+ {
15
+ "id": 10,
16
+ "kind": "task",
17
+ "state": "success",
18
+ "plugins": [],
19
+ "payload": { "textRef": true }
20
+ },
21
+ {
22
+ "id": 11,
23
+ "kind": "control",
24
+ "state": "running",
25
+ "plugins": [{ "name": "blockgoal", "scope": "control", "config": {} }],
26
+ "payload": { "textRef": true }
27
+ },
28
+ {
29
+ "id": 12,
30
+ "kind": "task",
31
+ "state": "pending",
32
+ "plugins": [
33
+ {
34
+ "name": "waitgoal",
35
+ "scope": "node",
36
+ "config": { "waitMinutes": 3 }
37
+ }
38
+ ],
39
+ "payload": { "textRef": true }
40
+ }
41
+ ],
42
+ "edges": [
43
+ { "from": 10, "to": 11 },
44
+ { "from": 11, "to": 12 }
45
+ ]
46
+ }