abelien-harness 0.8.0 → 0.9.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 (33) hide show
  1. package/.env.example +19 -20
  2. package/README.md +12 -12
  3. package/package.json +1 -1
  4. package/skills/abelien-hardware/SKILL.md +4 -4
  5. package/skills/abelien-hardware/domain.json +4 -4
  6. package/skills/abelien-hardware/references/knowledge-recall-service-contract.md +4 -4
  7. package/skills/abelien-hardware/references/search-index-format.md +5 -5
  8. package/skills/abelien-hardware/references/search-index-release.md +7 -7
  9. package/skills/abelien-hardware/references/selection-service-contract.md +3 -3
  10. package/skills/abelien-hardware/scripts/build-search-index.mjs +1 -1
  11. package/skills/abelien-hardware/scripts/oss-object-store.mjs +247 -0
  12. package/skills/abelien-hardware/scripts/{tos-wiki-search.mjs → oss-wiki-search.mjs} +19 -19
  13. package/skills/abelien-hardware/scripts/search-index-release.mjs +25 -25
  14. package/skills/abelien-hardware-selection/SKILL.md +2 -2
  15. package/skills/abelien-hardware-selection/domain.json +2 -2
  16. package/skills/abelien-hardware-selection/scripts/load-guide.mjs +1 -1
  17. package/skills/abelien-harness/references/adapter-contract.md +1 -1
  18. package/skills/abelien-harness/references/m1-integration.md +2 -2
  19. package/skills/abelien-harness/scripts/ab.mjs +8 -1
  20. package/skills/abelien-harness/scripts/studio.mjs +15 -14
  21. package/skills/abelien-reference/domain.json +1 -1
  22. package/skills/abelien-reference/references/research-contract.md +1 -0
  23. package/skills/abelien-reference/schemas/reference-evidence.v1.json +1 -1
  24. package/skills/abelien-reference/scripts/finalize.mjs +3 -1
  25. package/skills/abelien-workspace/SKILL.md +1 -1
  26. package/skills/abelien-workspace/assets/journey-stage-plan.json +46 -13
  27. package/skills/abelien-workspace/domain.json +3 -2
  28. package/skills/abelien-workspace/references/hardware-journey-orchestration.md +2 -2
  29. package/skills/abelien-workspace/references/harness-binding.md +3 -3
  30. package/skills/abelien-workspace/references/user-journey.md +5 -5
  31. package/skills/abelien-workspace/scripts/journey-next.mjs +10 -2
  32. package/skills/abelien-workspace/workflows/user-journey.json +1 -1
  33. package/skills/abelien-hardware/scripts/tos-object-store.mjs +0 -202
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema": "abelien.journey-stage-plan.v1",
3
- "plan_version": "2026-10-12.6",
4
- "description": "按意图的作品旅程阶段计划。orders 是自动推进的主线;side 阶段由宿主守卫按真实结果触发。每个阶段的 directives 是领域规则(宿主中立),宿主把它们附在自己的执行说明(工具名、字段)之后,不改写、不删减。旅程规则的唯一正文是 references/user-journey.md,本计划是其机读派生:orders 对应 A1 通过之后到交付首版(§2.4、§5),side 对应首版前后由真实结果触发的旁路(§5.3、§6、§7);两处不一致时以正文为准,改正文后同步本计划并升 plan_version。",
3
+ "plan_version": "2026-10-14.1",
4
+ "description": "按意图的作品旅程阶段计划。orders 是自动推进的主线;side 阶段由宿主守卫按真实结果触发。每个阶段的 directives 是领域规则(宿主中立),宿主把它们附在自己的执行说明(工具名、字段)之后,不改写、不删减。每个阶段的 actions 是该阶段允许的动作,取自 action_catalog(宿主中立的动作目录),按意图不同时写在 action_variants;宿主把动作映射为自己的工具,自动阶段只放行这些工具和宿主自己的只读工具,不另维护工具白名单。旅程规则的唯一正文是 references/user-journey.md,本计划是其机读派生:orders 对应 A1 通过之后到交付首版(§2.4、§5),side 对应首版前后由真实结果触发的旁路(§5.3、§6、§7);两处不一致时以正文为准,改正文后同步本计划并升 plan_version。",
5
5
  "artifacts": ["reference_final", "concept_image", "hardware", "appearance_baseline", "cad_candidate", "review"],
6
6
  "orders": {
7
7
  "create": ["reference", "concept", "hardware", "real_image", "cad", "review"],
@@ -19,6 +19,24 @@
19
19
  "review": "review"
20
20
  },
21
21
  "side": ["hardware_reselect", "hardware_decision", "fallback", "repair", "questions"],
22
+ "action_catalog": {
23
+ "reference.research": "收集识别、拆解资料或相似产品的检索证据",
24
+ "reference.page": "读取已收集页面的正文,挑选逐字引用",
25
+ "reference.finalize": "提交识别或借用判断,定稿参考证据",
26
+ "studio.prepare": "保存外观目标,冻结本次创建的来源",
27
+ "image.generate": "生成概念图",
28
+ "image.import": "把识别出的真实产品图(没有时用用户上传图)结算为外观基准图",
29
+ "component.search": "检索组件库",
30
+ "hardware.guide": "按分类读取硬件选型指引",
31
+ "hardware.wiki": "读取候选型号的 Wiki 资料",
32
+ "hardware.resource": "读取已保存的硬件选型资源",
33
+ "hardware.workflow": "启动、查询、推进或恢复硬件选型工作流 user-journey-hardware",
34
+ "hardware.web-extract": "在网页证据暂停点,从已保存的网页中逐字抽取候选",
35
+ "hardware.prepare": "把通过校验的硬件选型登记为创建来源",
36
+ "cad.build": "构建 CAD 候选,含几何检查与实际渲染",
37
+ "visual.review": "按契约审查这一版的实际渲染",
38
+ "question.card": "发布一张问题卡,等待用户回答"
39
+ },
22
40
  "stages": {
23
41
  "identify": {
24
42
  "goal": "识别用户要的现成产品,并取得规格与拆解资料",
@@ -27,7 +45,8 @@
27
45
  "识别不确定时按最可能的候选继续,保留其他候选供用户切换;首版之前默认不向用户提问。",
28
46
  "例外:用户的硬要求缺少取值、无法合理默认,且按默认继续会做出错误结果(例如要求放进某个卡槽却没给尺寸)时,可以在首版之前发一张问题卡;整个首版之前至多一张,最多三题、每题最多三个选项;其余未定项一律按默认处理。",
29
47
  "完成后展示识别出的原产品图并注明来源(页面与站点),再用一两句话说明识别为哪款产品、依据是什么,以及比例核对结果。"
30
- ]
48
+ ],
49
+ "actions": ["reference.research", "reference.page", "reference.finalize"]
31
50
  },
32
51
  "reference": {
33
52
  "goal": "检索相似产品,作为尺寸、结构、器件与实现方式的参考",
@@ -36,7 +55,8 @@
36
55
  "找不到完整的相似产品时,按声学、供电、交互等子系统分别参考,不硬凑数量;首版之前默认不向用户提问。",
37
56
  "相似产品只作参考,不成为可编辑模板,也不自动采用其 BOM。",
38
57
  "例外:用户的硬要求缺少取值、无法合理默认,且按默认继续会做出错误结果(例如要求放进某个卡槽却没给尺寸)时,可以在首版之前发一张问题卡;整个首版之前至多一张,最多三题、每题最多三个选项;其余未定项一律按默认处理。"
39
- ]
58
+ ],
59
+ "actions": ["reference.research", "reference.page", "reference.finalize"]
40
60
  },
41
61
  "concept": {
42
62
  "goal": "生成第一张概念图",
@@ -50,7 +70,8 @@
50
70
  "以识别出的原产品图为输入生成修改后的图,只改用户要求修改的部分,保留清单中的特征。",
51
71
  "出图后用一两句话说明改了什么、保留了什么,并结束本轮;硬件选型在下一阶段进行。"
52
72
  ]
53
- }
73
+ },
74
+ "actions": ["studio.prepare", "image.generate"]
54
75
  },
55
76
  "hardware": {
56
77
  "goal": "为当前需求选型并登记",
@@ -67,7 +88,8 @@
67
88
  "登记后用一两句话说明替代关系与默认值,然后结束本轮;待确认项在首版之后合并成一张问题卡统一询问。",
68
89
  "例外:用户的硬要求缺少取值、无法合理默认,且按默认继续会做出错误结果(例如要求放进某个卡槽却没给尺寸)时,可以在首版之前发一张问题卡;整个首版之前至多一张,最多三题、每题最多三个选项;其余未定项一律按默认处理。"
69
90
  ]
70
- }
91
+ },
92
+ "actions": ["component.search", "hardware.guide", "hardware.wiki", "hardware.resource", "hardware.workflow", "hardware.web-extract", "hardware.prepare"]
71
93
  },
72
94
  "hardware_reselect": {
73
95
  "goal": "当前硬件放不下用户的硬尺寸上限或锁定分区时重新选型",
@@ -80,13 +102,15 @@
80
102
  "shape": {
81
103
  "true": "外形已锁定:槽位的 locked 分区(fit_envelope)同样不得放宽。没有能装下的候选时,先由结构在锁定外形内调整骨架(分区、层高、朝向)并给出新的 fit_envelope 后再选,仍装不下才发问题卡;此时“放宽尺寸”就是改外形,记为外观偏差。"
82
104
  }
83
- }
105
+ },
106
+ "actions": ["question.card", "component.search", "hardware.guide", "hardware.wiki", "hardware.resource", "hardware.workflow", "hardware.web-extract", "hardware.prepare"]
84
107
  },
85
108
  "hardware_decision": {
86
109
  "goal": "硬尺寸上限下没有能装下的候选,交用户取舍",
87
110
  "directives": [
88
111
  "本轮不重新选型、不修改需求、不出图;只发一张问题卡,让用户在“换件”和“放宽尺寸”之间选择,然后结束本轮。"
89
- ]
112
+ ],
113
+ "actions": ["question.card"]
90
114
  },
91
115
  "real_image": {
92
116
  "goal": "把外观基准图与当前硬件绑定",
@@ -101,6 +125,10 @@
101
125
  "用户明确要求重画时,在用户回合准备新的图片版本,保留原产物。",
102
126
  "不要修改需求;待答的问题卡保持原样,不代用户回答。"
103
127
  ]
128
+ },
129
+ "actions": ["studio.prepare"],
130
+ "action_variants": {
131
+ "restore": ["studio.prepare", "image.import"]
104
132
  }
105
133
  },
106
134
  "cad": {
@@ -118,13 +146,15 @@
118
146
  "false": "外形未锁定:硬件放得下优先,但外壳只能在 appearance 认可的外观容差内调整尺寸与比例,并记录调整量及依据;超出容差时换件(交回硬件)或把取舍交给用户,不得自行放大外壳。",
119
147
  "true": "外形尺寸与比例已锁定:不得放大或缩小外壳;装不下时停止建模,先交回硬件换件,仍不行再调整结构骨架,改外形必须由用户决定。"
120
148
  }
121
- }
149
+ },
150
+ "actions": ["cad.build"]
122
151
  },
123
152
  "fallback": {
124
153
  "goal": "建模没有产出可展示的 CAD 时,由宿主生成标注清楚的简化草模",
125
154
  "directives": [
126
155
  "宿主按冻结的硬件尺寸生成一份已明确标注的简化草模;本轮不调用模型,不进行视觉审查。"
127
- ]
156
+ ],
157
+ "actions": ["cad.build"]
128
158
  },
129
159
  "review": {
130
160
  "goal": "审查这一版的实际渲染",
@@ -137,7 +167,8 @@
137
167
  "以识别出的真实产品图为基准,按契约审查这一版,审查对象全部取自 CAD 回执,不要猜;然后用一两句话说明与原产品的差异。",
138
168
  "不要重新建模、不要重新出图、不要修改需求;待答的问题卡保持原样,不代用户回答。"
139
169
  ]
140
- }
170
+ },
171
+ "actions": ["visual.review"]
141
172
  },
142
173
  "repair": {
143
174
  "goal": "只修正检查发现的问题",
@@ -145,7 +176,8 @@
145
176
  "只修改检查报告或几何分诊指出的冲突部位与实例,逐项保留其余实例;需要整体重建时按缺失的硬件与冲突证据完整重建。",
146
177
  "尽量保留外观基准图的轮廓、比例、特征位置与材质;不缩放原生元器件。",
147
178
  "不要修改需求、重新选型或重新出图;先完成轻量预览,视觉复检在下一阶段进行;待答的问题卡保持原样,不代用户回答。"
148
- ]
179
+ ],
180
+ "actions": ["cad.build"]
149
181
  },
150
182
  "questions": {
151
183
  "goal": "首版之后合并询问按默认处理的事项",
@@ -155,7 +187,8 @@
155
187
  "意图、目标深度或型号识别仍有疑问时,可以作为其中一题,第一个选项写首版采用的判断。",
156
188
  "没有待确认项就不发卡,一两句话说明后结束本轮。",
157
189
  "不要修改需求、重新选型、建模或出图;用户的答复作为对首版的修订处理。"
158
- ]
190
+ ],
191
+ "actions": ["question.card"]
159
192
  }
160
193
  }
161
194
  }
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "abelien-workspace",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "description": "作品旅程、PRD 与业务契约;user-journey 由 Agent 驱动,draft 供真实硬件阶段使用;不声明假 PRD、确认、报价或支付能力",
5
5
  "resources": [
6
6
  "draft"
7
7
  ],
8
8
  "capabilities": {
9
9
  "workspace.journey-next": {
10
- "description": "按意图(create / restore / modify / remix)的阶段计划给出下一阶段与该阶段的领域规则;纯计算,不联网、不写资源。宿主先跑完自己的守卫,再把 directives 附在自己的执行说明之后",
10
+ "description": "按意图(create / restore / modify / remix)的阶段计划给出下一阶段与该阶段的领域规则;纯计算,不联网、不写资源。宿主先跑完自己的守卫,再把 directives 附在自己的执行说明之后;actions 是该阶段允许的宿主中立动作,宿主据此放行自己的工具",
11
11
  "adapter": {
12
12
  "type": "module",
13
13
  "path": "scripts/journey-next.mjs",
@@ -23,6 +23,7 @@
23
23
  ],
24
24
  "arrays": [
25
25
  "directives",
26
+ "actions",
26
27
  "order",
27
28
  "completed"
28
29
  ]
@@ -5,14 +5,14 @@
5
5
  ## 新建与修订分流
6
6
 
7
7
  - 没有已验证硬件方案、要形成正式初选:运行 `user-journey-hardware`。Agent 先做需求到三级槽位分类,再调用同级 `abelien-hardware-selection` 的 `hardware.select`;只有返回的 `hardware-selection` 通过门禁后才算完成正式硬件阶段。
8
- - 只需要研究 TOS 候选、暂不形成正式初选:运行 `user-journey-hardware-candidates`。Agent 先调用 `hardware.search` 从 TOS 分类索引读取 Top-K 最新候选 Wiki,再比较 `adapt_system` 或 `assemble_new` 并由 `hardware.finalize` 保存 `hardware-proposal`;这个提案不能通过正式硬件门,也不能直接开始算法阶段。
8
+ - 只需要研究 OSS 候选、暂不形成正式初选:运行 `user-journey-hardware-candidates`。Agent 先调用 `hardware.search` 从 OSS 分类索引读取 Top-K 最新候选 Wiki,再比较 `adapt_system` 或 `assemble_new` 并由 `hardware.finalize` 保存 `hardware-proposal`;这个提案不能通过正式硬件门,也不能直接开始算法阶段。
9
9
  - 同一次任务还要完成算法选型:运行 `user-journey-hardware-algorithm`。硬件阶段门通过后,当前 run 自动进入算法研究和 `algorithm.finalize`,无需由前端另起第二个 workflow。
10
10
  - 已有已验证方案,用户明确修改其中一项:运行 `user-journey-hardware-revise`。输入只包含当前 draft ID、基线选型 ID/版本和最新变更请求;不要把整段对话塞入硬件服务。
11
11
  - 用户改变了产品目标或大量功能,而不是局部部件:形成新的需求版本,再运行新建流程,不把它伪装成局部修订。
12
12
 
13
13
  修订 workflow 先保存提案,再执行状态门,最后才更新 draft 指针。`needs_confirmation`、版本冲突、兼容未知或未授权连带变化都会在激活前停止。用户补充信息或批准连带项后,以同一有效基线和新的变更请求启动下一次修订 run;每次尝试都有独立资源和审计记录,这就是 v1 下的显式迭代,不是隐藏循环。
14
14
 
15
- TOS Wiki 读取由 `abelien-hardware` 的本地 module adapter 实现,通用内核只提供受目录约束的 module adapter 装载机制和 `search_hardware` 快捷命令。TOS 候选流程使用本地 `hardware.finalize` 保存提案;正式入口使用本地 hardware.select,回读同需求的召回并校验逐项证据;候选提案不能跳过该门。TOS 和服务凭据只能来自运行时环境,不得写入 Skill、workflow、结果资源或前端包。
15
+ OSS Wiki 读取由 `abelien-hardware` 的本地 module adapter 实现,通用内核只提供受目录约束的 module adapter 装载机制和 `search_hardware` 快捷命令。OSS 候选流程使用本地 `hardware.finalize` 保存提案;正式入口使用本地 hardware.select,回读同需求的召回并校验逐项证据;候选提案不能跳过该门。OSS 和服务凭据只能来自运行时环境,不得写入 Skill、workflow、结果资源或前端包。
16
16
 
17
17
  ## 完整智能体
18
18
 
@@ -55,9 +55,9 @@ stdout 的 `ok:true` 表示命令取得了回执。workflow 调用还必须检
55
55
 
56
56
  | 用户旅程所需能力 | 当前 Harness 对应与限制 |
57
57
  | --- | --- |
58
- | 按意图的下一阶段与阶段规则 | `workspace.journey-next`(本包 `domain.json` 唯一的 capability,`ab capability run workspace.journey-next --input '{"intent":"restore","artifacts":{"reference_final":true}}'`):读取[阶段计划](../assets/journey-stage-plan.json),返回下一阶段、`goal`、`directives`、`order`、`completed`、`locks` 与 `plan_version`。纯计算,不联网、不写资源、不读宿主状态;不判断 A1–C,也不代替宿主的结算、计费、预算、保底与幂等守卫。`stage` 只接受本意图的主线阶段与 `side` 旁路阶段,`artifacts`/`locks` 不用时省略、不传 null。阶段计划派生自[完整旅程](user-journey.md),规则以旅程正文为准 |
58
+ | 按意图的下一阶段与阶段规则 | `workspace.journey-next`(本包 `domain.json` 唯一的 capability,`ab capability run workspace.journey-next --input '{"intent":"restore","artifacts":{"reference_final":true}}'`):读取[阶段计划](../assets/journey-stage-plan.json),返回下一阶段、`goal`、`directives`、`actions`(该阶段允许的宿主中立动作,宿主据此放行自己的工具)、`order`、`completed`、`locks` 与 `plan_version`。纯计算,不联网、不写资源、不读宿主状态;不判断 A1–C,也不代替宿主的结算、计费、预算、保底与幂等守卫。`stage` 只接受本意图的主线阶段与 `side` 旁路阶段,`artifacts`/`locks` 不用时省略、不传 null。阶段计划派生自[完整旅程](user-journey.md),规则以旅程正文为准 |
59
59
  | clarify 需求整理 | 同级 `abelien-clarify`:以当前操作和同版上下文换取 `result / questions / note`,不是 CLI capability,不写资源;结构可用其 `scripts/check_result.py` 校验。共用表达规则在同级 guide 的 references,由 guide 读一次注入。已移除旧 workspace.brief,不提供假澄清 |
60
- | 硬件候选检索与提案 | `user-journey-hardware-candidates` → `abelien-hardware`:`hardware.search`(TOS 检索)→ agent 比较整机与零部件候选 → `hardware.finalize`(保存 `hardware-proposal`)。提案不创建正式 `hardware-selection` 门,也不能直接开始算法阶段 |
60
+ | 硬件候选检索与提案 | `user-journey-hardware-candidates` → `abelien-hardware`:`hardware.search`(OSS 检索)→ agent 比较整机与零部件候选 → `hardware.finalize`(保存 `hardware-proposal`)。提案不创建正式 `hardware-selection` 门,也不能直接开始算法阶段 |
61
61
  | 正式硬件选型 | `user-journey-hardware` / `user-journey-hardware-algorithm` → `abelien-hardware-selection` → `hardware.select`;本地模块回读真实召回并校验 Agent 判断,返回 hardware-selection;证据不足不可用候选提案代替 |
62
62
  | 算法与行为实现 | `user-journey-hardware-algorithm`(正式硬件过门后同 run 继续)或独立 `user-journey-algorithm` → `abelien-algorithm`:agent 按官方资料优先联网研究,`algorithm.finalize` 本地保存。两者的 `reactions` 输入取 clarify Requirements 的 `reaction[]` 原样传入 |
63
63
  | 外观/ID editing 与视觉审查 | 同级 `abelien-appearance`:维护轮廓、比例、CMF、部位与参考匹配目标,实际查看 structure 的本版渲染。没有注册 `appearance.*` 产品 capability;宿主须具备真实图片读取/编辑能力,不能把旧外观 mock 当成现行实现 |
@@ -126,7 +126,7 @@ node "$AB" --workdir "$WD" receipt get --wait <waitId> --digest <resultDigest>
126
126
 
127
127
  ## 硬件阶段实际驱动
128
128
 
129
- 需求满足 A1 后,先读取同级 `abelien-hardware-selection/SKILL.md` 和工作流定义 `abelien-workspace/workflows/user-journey-hardware.json`。当前使用本地 hardware.select,无需 HTTP URL;先 TOS 搜索、再 Agent 逐约束引用、最后同源校验,详见选型域 selection-service-contract。下面的尖括号与 JSON 占位须替换为本次真实值。
129
+ 需求满足 A1 后,先读取同级 `abelien-hardware-selection/SKILL.md` 和工作流定义 `abelien-workspace/workflows/user-journey-hardware.json`。当前使用本地 hardware.select,无需 HTTP URL;先 OSS 搜索、再 Agent 逐约束引用、最后同源校验,详见选型域 selection-service-contract。下面的尖括号与 JSON 占位须替换为本次真实值。
130
130
 
131
131
  ```bash
132
132
  node "$AB" --workdir "$WD" workflow run user-journey-hardware --input '{"requirements": <clarify 交回的完整 Requirements>}'
@@ -216,15 +216,15 @@ AI 补充的是展示编排,须与用户已定义的行为一致并保留系
216
216
 
217
217
  ## 5. A2:尽早交给用户一个可以改的起点
218
218
 
219
- **机读阶段计划**:A1 通过之后到交付首版这一段,按意图(create/restore/modify/remix)的阶段顺序与各阶段的领域规则,另有机读形式[阶段计划](../assets/journey-stage-plan.json),由 `workspace.journey-next` 解释(调用见 [能力映射](harness-binding.md#当前能力映射))。§2.4 的识别与参考、本节的硬件初选和首版,对应计划的主线 `orders`;由真实结果触发的硬件重选与取舍、保底草模、修复和首版后合并提问,对应计划的旁路 `side`(§5.3、§6、§7)。本文是旅程规则的唯一正文,阶段计划是它的派生:两者不一致时以本文为准,先改本文,再同步阶段计划并升 `plan_version`。
219
+ **机读阶段计划**:A1 通过之后到交付首版这一段,按意图(create/restore/modify/remix)的阶段顺序与各阶段的领域规则,另有机读形式[阶段计划](../assets/journey-stage-plan.json),由 `workspace.journey-next` 解释(调用见 [能力映射](harness-binding.md#当前能力映射))。§2.4 的识别与参考、本节的硬件初选和首版,对应计划的主线 `orders`;由真实结果触发的硬件重选与取舍、保底草模、修复和首版后合并提问,对应计划的旁路 `side`(§5.3、§6、§7)。本文是旅程规则的唯一正文,阶段计划是它的派生:两者不一致时以本文为准,先改本文,再同步阶段计划并升 `plan_version`。各阶段允许的动作(`actions`,宿主据此放行工具)只在阶段计划中声明,须与本文对该阶段的要求一致。
220
220
 
221
221
  ### 5.1 得到有来源的硬件初选
222
222
 
223
- 首张概念图不等待硬件初选;硬件阶段接收 clarify 的完整 Requirements(context / reaction / limits,保留 source 与 strength),按功能分类为各槽位组合组件;同类整机的配置只作参考,不先套整机(还原现成产品或基于现成产品修改时,才评估整机改造)。数量、硬约束与完整行为一起参与选择,不能只拿摘要或品类标签选型,分类槽位也不等于选定型号。TOS `hardware.search` 可作为候选召回,`hardware.finalize` 只保存 `hardware-proposal`;正式初选唯一走 `abelien-hardware-selection` 的 `hardware.select`,返回正式 `hardware-selection` 后才进入硬件门和算法阶段。
223
+ 首张概念图不等待硬件初选;硬件阶段接收 clarify 的完整 Requirements(context / reaction / limits,保留 source 与 strength),按功能分类为各槽位组合组件;同类整机的配置只作参考,不先套整机(还原现成产品或基于现成产品修改时,才评估整机改造)。数量、硬约束与完整行为一起参与选择,不能只拿摘要或品类标签选型,分类槽位也不等于选定型号。OSS `hardware.search` 可作为候选召回,`hardware.finalize` 只保存 `hardware-proposal`;正式初选唯一走 `abelien-hardware-selection` 的 `hardware.select`,返回正式 `hardware-selection` 后才进入硬件门和算法阶段。
224
224
 
225
225
  返回结果要能说明:选了什么、来自哪里、覆盖哪些行为、还缺什么、哪些只是候选。整机已覆盖的能力不重复采购;只采用某个外壳或结构资产,也不自动采用来源整机的整份 BOM。
226
226
 
227
- 没有真实候选、来源不可读、正式选型服务失败或必要覆盖不足时,保留候选/提案并说明缺口,不能编出一份正式初版硬件,也不能把 TOS 提案标成 verified。工程证据由领域能力补充;确需改变用户要求时才请用户取舍。
227
+ 没有真实候选、来源不可读、正式选型服务失败或必要覆盖不足时,保留候选/提案并说明缺口,不能编出一份正式初版硬件,也不能把 OSS 提案标成 verified。工程证据由领域能力补充;确需改变用户要求时才请用户取舍。
228
228
 
229
229
  ### 5.2 硬件初版出来,PRD 和 BOM 就可以先出
230
230
 
@@ -547,11 +547,11 @@ BOM 记录实际采用的组合,已有整机覆盖的功能不重复计件。
547
547
 
548
548
  ## 附录 B:当前 Harness 与旧 MVP 的适用边界
549
549
 
550
- 以下为 2026-09-30 合并后的执行边界:正式硬件选型、TOS 候选提案、算法、外观与结构能力并存,但真实产品端到端验收和生产部署状态仍需单独核对。
550
+ 以下为 2026-09-30 合并后的执行边界:正式硬件选型、OSS 候选提案、算法、外观与结构能力并存,但真实产品端到端验收和生产部署状态仍需单独核对。
551
551
 
552
552
  | 部分 | 本地可核对的事实 | 不能由此推导的结论 |
553
553
  | --- | --- | --- |
554
- | 硬件初选 | `user-journey-hardware` → `abelien-hardware-selection` → `hardware.search` → Agent 证据判断 → 本地 `hardware.select` 是正式入口;`user-journey-hardware-candidates` → TOS `hardware.search` / `hardware.finalize` 仅保存候选提案 | 当前环境一定连通;全组合检查通过;已成为产品确认版本 |
554
+ | 硬件初选 | `user-journey-hardware` → `abelien-hardware-selection` → `hardware.search` → Agent 证据判断 → 本地 `hardware.select` 是正式入口;`user-journey-hardware-candidates` → OSS `hardware.search` / `hardware.finalize` 仅保存候选提案 | 当前环境一定连通;全组合检查通过;已成为产品确认版本 |
555
555
  | 硬件局部修订 | 本 MR 不包含修订能力和 workflow;以后须核对实际安装声明 | 修订入口已存在或服务已可用 |
556
556
  | 算法选型 | 本 MR 不包含算法域与组合 workflow;产品旅程保留其目标职责 | 算法入口已接入、实现已部署或在设备实测 |
557
557
  | 3D 生成 | 有独立生成入口与 provider 适配;旧 workspace 生成 mock 已移除 | 已接通产品结构编辑、版本采用和装配验证闭环 |
@@ -1,6 +1,7 @@
1
1
  // workspace.journey-next(P2-13.05):按意图的阶段计划给出下一阶段及其领域规则。
2
2
  // 纯函数:不联网、不写资源、不读宿主状态。宿主把自己保存的产物映射为 artifacts,先跑完自己的守卫
3
- // (结算、计费、保底、预算、工具集、幂等),再调用本能力;拿到的 directives 原样附在宿主自己的执行说明之后。
3
+ // (结算、计费、保底、预算、幂等),再调用本能力;拿到的 directives 原样附在宿主自己的执行说明之后,
4
+ // actions(action_catalog 中的宿主中立动作)由宿主映射为自己的工具,是该阶段唯一的放行依据。
4
5
  import { readFileSync } from "node:fs";
5
6
 
6
7
  const deepFreeze = value => {
@@ -22,6 +23,12 @@ export function stageDirectives(stage, intent, locks) {
22
23
  return base;
23
24
  }
24
25
 
26
+ /** Actions one stage may take (ids from action_catalog): the intent's action variant if present, else the stage's actions. */
27
+ export function stageActions(stage, intent) {
28
+ const entry = PLAN.stages[stage];
29
+ return [...(entry.action_variants?.[intent] ?? entry.actions)];
30
+ }
31
+
25
32
  export function journeyNext({ input }) {
26
33
  if (!record(input) || Object.keys(input).some(key => !["intent", "artifacts", "locks", "stage"].includes(key))) invalid("input accepts intent, artifacts, locks and stage only");
27
34
  if (!INTENTS.includes(input.intent)) invalid(`intent must be one of ${INTENTS.join(", ")}`);
@@ -45,6 +52,7 @@ export function journeyNext({ input }) {
45
52
  const stage = input.stage ?? (firstOpen === -1 ? null : order[firstOpen]);
46
53
  return {
47
54
  plan_version: PLAN.plan_version, intent: input.intent, stage, goal: stage ? PLAN.stages[stage].goal : null,
48
- directives: stage ? stageDirectives(stage, input.intent, locks) : [], order: [...order], completed, locks,
55
+ directives: stage ? stageDirectives(stage, input.intent, locks) : [], actions: stage ? stageActions(stage, input.intent) : [],
56
+ order: [...order], completed, locks,
49
57
  };
50
58
  }
@@ -316,7 +316,7 @@
316
316
  ],
317
317
  "capabilityRoutes": {
318
318
  "clarify": "读取 ../../abelien-clarify/SKILL.md,整理需求;不是 CLI capability。",
319
- "hardwareInitial": "user-journey-hardware → abelien-hardware-selection → hardware.search TOS → Agent 证据判断 → 本地 hardware.select;唯一正式初选入口;TOS hardware.search 只提供候选,不能静默替代正式服务",
319
+ "hardwareInitial": "user-journey-hardware → abelien-hardware-selection → hardware.search OSS → Agent 证据判断 → 本地 hardware.select;唯一正式初选入口;OSS hardware.search 只提供候选,不能静默替代正式服务",
320
320
  "hardwareAndAlgorithm": "user-journey-hardware-algorithm;只在完整 reactions 及必要输入具备时使用",
321
321
  "hardwareRevision": "user-journey-hardware-revise;服务需真实可用",
322
322
  "algorithm": "user-journey-algorithm;输入已验证硬件与完整 reactions",
@@ -1,202 +0,0 @@
1
- import crypto from "node:crypto";
2
-
3
- const EMPTY = Buffer.alloc(0);
4
-
5
- function tosError(code, message, details) {
6
- const error = new Error(message);
7
- error.code = code;
8
- if (details !== undefined) error.details = details;
9
- return error;
10
- }
11
-
12
- export function sha256(value) {
13
- return crypto.createHash("sha256").update(value).digest("hex");
14
- }
15
-
16
- function hmac(key, value, encoding) {
17
- return crypto.createHmac("sha256", key).update(value).digest(encoding);
18
- }
19
-
20
- function first(env, primary, alias) {
21
- return env[primary] || env[alias] || "";
22
- }
23
-
24
- export function normalizeTosEnv(env = {}) {
25
- return {
26
- ...env,
27
- TOS_REGION: first(env, "TOS_REGION", "CAD_STORAGE_REGION"),
28
- TOS_ENDPOINT: first(env, "TOS_ENDPOINT", "CAD_STORAGE_ENDPOINT"),
29
- TOS_BUCKET: first(env, "TOS_BUCKET", "CAD_STORAGE_BUCKET"),
30
- TOS_ACCESS_KEY_ID: first(env, "TOS_ACCESS_KEY_ID", "CAD_STORAGE_ACCESS_KEY"),
31
- TOS_SECRET_ACCESS_KEY: first(env, "TOS_SECRET_ACCESS_KEY", "CAD_STORAGE_SECRET_KEY"),
32
- TOS_ADDRESSING_STYLE: first(env, "TOS_ADDRESSING_STYLE", "CAD_STORAGE_ADDRESSING_STYLE") || "virtual",
33
- };
34
- }
35
-
36
- function normalizePrefix(value) {
37
- const cleaned = String(value || "hardware_profile_wiki/").replace(/^\/+|\/+$/g, "");
38
- if (!cleaned || cleaned.split("/").some(part => part === "." || part === "..")) {
39
- throw tosError("VALIDATION", "invalid TOS Wiki prefix");
40
- }
41
- return `${cleaned}/`;
42
- }
43
-
44
- function positiveIntegerEnv(env, name, fallback) {
45
- if (env[name] === undefined || env[name] === "") return fallback;
46
- const value = Number(env[name]);
47
- if (!Number.isInteger(value) || value <= 0) throw tosError("VALIDATION", `${name} must be a positive integer`, { name });
48
- return value;
49
- }
50
-
51
- export function createTosConfig(inputEnv = {}) {
52
- const env = normalizeTosEnv(inputEnv);
53
- const required = ["TOS_REGION", "TOS_ENDPOINT", "TOS_BUCKET", "TOS_ACCESS_KEY_ID", "TOS_SECRET_ACCESS_KEY"];
54
- const missing = required.filter(name => !env[name]);
55
- if (missing.length) throw tosError("ENV_MISSING", `env var not set: ${missing[0]}`, { name: missing[0], missing });
56
- let endpoint;
57
- try { endpoint = new URL(env.TOS_ENDPOINT); }
58
- catch { throw tosError("VALIDATION", "TOS_ENDPOINT must be an absolute URL"); }
59
- if (!['http:', 'https:'].includes(endpoint.protocol)) throw tosError("VALIDATION", "TOS_ENDPOINT must use http or https");
60
- if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
61
- throw tosError("VALIDATION", "TOS_ENDPOINT must not contain credentials, query or fragment");
62
- }
63
- if (!/^[A-Za-z0-9][A-Za-z0-9._-]{1,62}$/.test(env.TOS_BUCKET)) throw tosError("VALIDATION", "invalid TOS_BUCKET");
64
- if (!['virtual', 'path'].includes(env.TOS_ADDRESSING_STYLE)) throw tosError("VALIDATION", "TOS_ADDRESSING_STYLE must be virtual or path");
65
- return {
66
- region: env.TOS_REGION,
67
- endpoint,
68
- bucket: env.TOS_BUCKET,
69
- accessKeyId: env.TOS_ACCESS_KEY_ID,
70
- secretAccessKey: env.TOS_SECRET_ACCESS_KEY,
71
- sessionToken: env.TOS_SESSION_TOKEN || "",
72
- addressingStyle: env.TOS_ADDRESSING_STYLE,
73
- prefix: normalizePrefix(env.TOS_WIKI_PREFIX),
74
- // 网络读取的两道边界。此前 module adapter 路径既无超时也无体积上限:一个不响应的
75
- // 对象存储会把召回挂到进程结束,一个超大对象会被整段读进内存(召回本身还并发 8 路)。
76
- // 默认值取"够宽但有限"——Wiki 与分类分片都是小对象,写读都要走这两道闸。
77
- timeoutMs: positiveIntegerEnv(env, "TOS_REQUEST_TIMEOUT_MS", 30000),
78
- maxObjectBytes: positiveIntegerEnv(env, "TOS_MAX_OBJECT_BYTES", 16 * 1024 * 1024),
79
- };
80
- }
81
-
82
- // 先按声明的 content-length 拦一道(能在读之前拒),再在读的过程中边读边数——
83
- // arrayBuffer() 是先整段进内存再回看长度,对"超大对象"这个失败形态等于没拦。
84
- async function readObjectBytes(response, maxBytes, key, method) {
85
- const declared = Number(response.headers?.get?.("content-length"));
86
- if (Number.isFinite(declared) && declared > maxBytes) {
87
- throw tosError("TOS_OBJECT_TOO_LARGE", `TOS object exceeds TOS_MAX_OBJECT_BYTES (${maxBytes})`, { key, method, maxBytes, contentLength: declared });
88
- }
89
- if (response.body && typeof response.body.getReader === "function") {
90
- const reader = response.body.getReader();
91
- const chunks = [];
92
- let total = 0;
93
- for (;;) {
94
- const { done, value } = await reader.read();
95
- if (done) break;
96
- total += value.byteLength;
97
- if (total > maxBytes) {
98
- await reader.cancel().catch(() => {});
99
- throw tosError("TOS_OBJECT_TOO_LARGE", `TOS object exceeds TOS_MAX_OBJECT_BYTES (${maxBytes})`, { key, method, maxBytes });
100
- }
101
- chunks.push(Buffer.from(value));
102
- }
103
- return Buffer.concat(chunks);
104
- }
105
- const bytes = typeof response.arrayBuffer === "function"
106
- ? Buffer.from(await response.arrayBuffer())
107
- : Buffer.from(await response.text(), "utf8");
108
- if (bytes.length > maxBytes) {
109
- throw tosError("TOS_OBJECT_TOO_LARGE", `TOS object exceeds TOS_MAX_OBJECT_BYTES (${maxBytes})`, { key, method, maxBytes, contentLength: bytes.length });
110
- }
111
- return bytes;
112
- }
113
-
114
- function encodePath(value) {
115
- return String(value).split("/").map(part => encodeURIComponent(part).replace(/[!'()*]/g, char => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)).join("/");
116
- }
117
-
118
- function objectLocation(config, key) {
119
- const cleanKey = String(key).replace(/^\/+/, "");
120
- if (!cleanKey || cleanKey.split("/").some(part => part === "." || part === "..")) throw tosError("VALIDATION", "invalid TOS object key", { key });
121
- const parts = [config.endpoint.pathname.replace(/^\/+|\/+$/g, "")].filter(Boolean);
122
- let host = config.endpoint.host;
123
- if (config.addressingStyle === "path") parts.push(config.bucket);
124
- else host = `${config.bucket}.${host}`;
125
- parts.push(cleanKey);
126
- const uri = `/${encodePath(parts.join("/"))}`;
127
- return { host, uri, url: `${config.endpoint.protocol}//${host}${uri}` };
128
- }
129
-
130
- export function signTosObjectRequest(config, key, { method = "GET", body = EMPTY, headers = {}, now = new Date() } = {}) {
131
- const payload = Buffer.isBuffer(body) ? body : Buffer.from(body ?? "");
132
- const location = objectLocation(config, key);
133
- const timestamp = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
134
- const date = timestamp.slice(0, 8);
135
- const signed = {
136
- host: location.host,
137
- "x-amz-content-sha256": sha256(payload),
138
- "x-amz-date": timestamp,
139
- };
140
- for (const [name, value] of Object.entries(headers)) signed[name.toLowerCase()] = String(value).trim();
141
- if (config.sessionToken) signed["x-amz-security-token"] = config.sessionToken;
142
- const names = Object.keys(signed).sort();
143
- const canonicalHeaders = names.map(name => `${name}:${signed[name]}\n`).join("");
144
- const signedHeaders = names.join(";");
145
- const canonicalRequest = [method.toUpperCase(), location.uri, "", canonicalHeaders, signedHeaders, sha256(payload)].join("\n");
146
- const scope = `${date}/${config.region}/s3/aws4_request`;
147
- const stringToSign = ["AWS4-HMAC-SHA256", timestamp, scope, sha256(canonicalRequest)].join("\n");
148
- const dateKey = hmac(`AWS4${config.secretAccessKey}`, date);
149
- const regionKey = hmac(dateKey, config.region);
150
- const serviceKey = hmac(regionKey, "s3");
151
- const signingKey = hmac(serviceKey, "aws4_request");
152
- const signature = hmac(signingKey, stringToSign, "hex");
153
- signed.authorization = `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
154
- return { url: location.url, method: method.toUpperCase(), headers: signed, body: payload };
155
- }
156
-
157
- export async function requestTosObject(config, key, { method = "GET", body = EMPTY, headers = {}, optional = false, fetchImpl = fetch } = {}) {
158
- const request = signTosObjectRequest(config, key, { method, body, headers });
159
- let response;
160
- try {
161
- response = await fetchImpl(request.url, {
162
- method: request.method,
163
- headers: request.headers,
164
- // 超时覆盖到响应体读完(undici 把 signal 一路带到 body 流):否则一个连上就不发完的
165
- // 对端同样能挂死召回。既有桩 fetch 忽略 signal,行为不变。
166
- signal: AbortSignal.timeout(config.timeoutMs),
167
- ...(request.method === "GET" || request.method === "HEAD" ? {} : { body: request.body }),
168
- });
169
- } catch (cause) {
170
- if (cause?.name === "TimeoutError" || cause?.code === "ABORT_ERR") {
171
- throw tosError("TOS_REQUEST_TIMEOUT", `TOS request timed out after ${config.timeoutMs}ms`, { key, method: request.method, timeout_ms: config.timeoutMs });
172
- }
173
- throw tosError("TOS_REQUEST_FAILED", `TOS request failed: ${cause.message}`, { key, method: request.method });
174
- }
175
- if (!response.ok) {
176
- if (optional && response.status === 404) return null;
177
- throw tosError("TOS_HTTP_STATUS", `TOS returned HTTP ${response.status}`, { key, method: request.method, status: response.status });
178
- }
179
- const bytes = request.method === "HEAD"
180
- ? EMPTY
181
- : await readObjectBytes(response, config.maxObjectBytes, key, request.method);
182
- return {
183
- key,
184
- bytes,
185
- etag: response.headers?.get?.("etag")?.replace(/^"|"$/g, "") || sha256(bytes),
186
- lastModified: response.headers?.get?.("last-modified") || null,
187
- contentLength: Number(response.headers?.get?.("content-length")) || null,
188
- metadataSha256: response.headers?.get?.("x-amz-meta-sha256") || response.headers?.get?.("x-tos-meta-sha256") || null,
189
- };
190
- }
191
-
192
- export function getTosObject(config, key, options = {}) {
193
- return requestTosObject(config, key, { ...options, method: "GET" });
194
- }
195
-
196
- export function putTosObject(config, key, body, options = {}) {
197
- return requestTosObject(config, key, { ...options, method: "PUT", body });
198
- }
199
-
200
- export function headTosObject(config, key, options = {}) {
201
- return requestTosObject(config, key, { ...options, method: "HEAD" });
202
- }