abelien-harness 0.3.0 → 0.5.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 (62) hide show
  1. package/.env.example +3 -0
  2. package/README.md +9 -3
  3. package/package.json +1 -1
  4. package/skills/abelien-3dgen/SKILL.md +4 -2
  5. package/skills/abelien-3dgen/providers/tripo.json +64 -1
  6. package/skills/abelien-3dgen/references/engineering-lowpoly-qualification.md +20 -0
  7. package/skills/abelien-3dgen/references/host-api.md +128 -0
  8. package/skills/abelien-3dgen/references/provider-profile.md +4 -0
  9. package/skills/abelien-appearance/SKILL.md +2 -0
  10. package/skills/abelien-appearance/references/local-component-edit.md +31 -0
  11. package/skills/abelien-clarify/SKILL.md +8 -3
  12. package/skills/abelien-clarify/references/examples.md +9 -0
  13. package/skills/abelien-clarify/scripts/check_result.py +53 -1
  14. package/skills/abelien-clarify/scripts/test_check_result.py +40 -0
  15. package/skills/abelien-guide/SKILL.md +2 -0
  16. package/skills/abelien-guide/references/shared-response-rules.md +2 -2
  17. package/skills/abelien-hardware/SKILL.md +12 -2
  18. package/skills/abelien-hardware/domain.json +4 -4
  19. package/skills/abelien-hardware/references/search-index-format.md +41 -2
  20. package/skills/abelien-hardware/references/search-index-release.md +12 -1
  21. package/skills/abelien-hardware/references/selection-service-contract.md +3 -0
  22. package/skills/abelien-hardware/references/web-evidence-contract.md +2 -2
  23. package/skills/abelien-hardware/scripts/build-search-index.mjs +50 -6
  24. package/skills/abelien-hardware/scripts/dimensions.mjs +184 -0
  25. package/skills/abelien-hardware/scripts/electrical.mjs +125 -0
  26. package/skills/abelien-hardware/scripts/finalize-selection.mjs +15 -0
  27. package/skills/abelien-hardware/scripts/provider-chain-contract.mjs +10 -6
  28. package/skills/abelien-hardware/scripts/publish-search-index.mjs +2 -1
  29. package/skills/abelien-hardware/scripts/search-index-release.mjs +45 -19
  30. package/skills/abelien-hardware/scripts/tos-wiki-search.mjs +40 -20
  31. package/skills/abelien-hardware/scripts/volc-web-search.mjs +6 -1
  32. package/skills/abelien-hardware/scripts/web-common.mjs +4 -1
  33. package/skills/abelien-hardware/scripts/web-provider-chain.mjs +8 -2
  34. package/skills/abelien-hardware/scripts/web-search-plan.mjs +16 -1
  35. package/skills/abelien-hardware/scripts/web-search.mjs +4 -2
  36. package/skills/abelien-hardware-selection/SKILL.md +1 -1
  37. package/skills/abelien-hardware-selection/domain.json +2 -2
  38. package/skills/abelien-hardware-selection/references/selection-service-contract.md +32 -2
  39. package/skills/abelien-hardware-selection/scripts/select.mjs +35 -7
  40. package/skills/abelien-harness/references/workflow-spec.md +15 -2
  41. package/skills/abelien-harness/scripts/generate3d-host.mjs +398 -0
  42. package/skills/abelien-harness/scripts/generate3d.mjs +54 -15
  43. package/skills/abelien-harness/scripts/receipt.mjs +8 -2
  44. package/skills/abelien-harness/scripts/studio.mjs +18 -10
  45. package/skills/abelien-structure/SKILL.md +3 -1
  46. package/skills/abelien-structure/references/build123d-execution.md +1 -1
  47. package/skills/abelien-structure/scripts/run_stage.py +8 -1
  48. package/skills/abelien-workspace/SKILL.md +2 -0
  49. package/skills/abelien-workspace/domain.json +1 -1
  50. package/skills/abelien-workspace/references/hardware-journey-orchestration.md +1 -1
  51. package/skills/abelien-workspace/workflows/definitions/0852e4949f5a6246880c1a26bf5ccaa42f8cda06b960e7c7ce234a0a176878f9.json +229 -0
  52. package/skills/abelien-workspace/workflows/definitions/7c69ea0d51099b1ae947f08b056ea67f5cad6b40ae8bcb00efb496fa5b9d5ed3.json +149 -0
  53. package/skills/abelien-workspace/workflows/definitions/9b5c65ad03fdecc0148931a7677cfa684a3fb04ab2738d6f7a1086a2c79ac86c.json +149 -0
  54. package/skills/abelien-workspace/workflows/definitions/a6e8254e1839d94530ae243cba779e423953b7169cbce338f0503d748d44800b.json +175 -0
  55. package/skills/abelien-workspace/workflows/definitions/b37f3b47e1e8bd1208859de57456152b149416e21949297b22bba937c51a5670.json +149 -0
  56. package/skills/abelien-workspace/workflows/definitions/b780bf280d61596b43eb6dbc6a417cf8df1ec9a7ae79a6de10f048da9ed58729.json +229 -0
  57. package/skills/abelien-workspace/workflows/definitions/c1b5c6c653f0c4761d51efee923aafbbeea05512e7f35c60f7f7d36ec49fc1d1.json +229 -0
  58. package/skills/abelien-workspace/workflows/definitions/e391e14c14cb39408a0a9e226ed54ffab7a36badea305ae4a3eafbac8124fc02.json +175 -0
  59. package/skills/abelien-workspace/workflows/definitions/fd8ce7074e2044985778446e93153864940bb9465471c1c71c41328cefff70bf.json +175 -0
  60. package/skills/abelien-workspace/workflows/user-journey-hardware-algorithm.json +3 -3
  61. package/skills/abelien-workspace/workflows/user-journey-hardware-candidates.json +3 -3
  62. package/skills/abelien-workspace/workflows/user-journey-hardware.json +3 -3
@@ -126,6 +126,10 @@ export function validateProfile(p) {
126
126
  // (mock baseUrl 等),CLI 不暴露;校验先于合并(override 可放松 https 钉子指向 http mock),
127
127
  // 指向未知名显式 PROVIDER_NOT_FOUND(拒绝静默吞掉测试笔误)。
128
128
  export function loadProfiles(deps) {
129
+ // Trusted host-only port: the exact verified configuration snapshot is used for
130
+ // preflight, upload, paid submit and subsequent reads within this operation.
131
+ // No CLI flag or environment setting exposes this port.
132
+ if (deps?.profileSnapshot) return deps.profileSnapshot;
129
133
  const domain = discoverDomains({ workdir: deps.workdir }).find(d => d.name === DOMAIN_NAME);
130
134
  if (!domain) throw fail("PROVIDER_NOT_FOUND", `domain pack ${DOMAIN_NAME} not discovered`, { domain: DOMAIN_NAME });
131
135
  const dir = path.join(domain.dir, "providers");
@@ -514,27 +518,62 @@ export function unzipPick(zipBuf, pickRe, destPath, options = {}) {
514
518
  }
515
519
 
516
520
  // downloadArtifact——产物下载落盘(Task 8):GET 签名 URL(fetchImpl 可注入,测试零网络),
517
- // 60s 超时(AbortController);非 2xx / 网络失败 / 超时一律 DOWNLOAD_FAILED(非 2xx 附
518
- // status)。落盘恒 destDir/model.glb:binary 直接原子写;zip/auto 先嗅探(Content-Type 含
521
+ // 60s 超时(AbortController);非 2xx / 网络失败 / 超时 / 3xx 一律 DOWNLOAD_FAILED(非 2xx 附
522
+ // status)。**签名 URL 一律不跟随重定向**(`redirect:'error'`,与 restJson/uploadImages 同款):
523
+ // 产物地址是 provider 给的带签名链接,跟随 302 就等于把凭据送到未受信主机;这条对 CLI 的
524
+ // `ab generate 3d fetch` 与宿主 API 一并生效(宿主面本就在 hostDeps 强制,此处补齐内核面)。
525
+ // 落盘恒 destDir/model.glb:binary 直接原子写;zip/auto 先嗅探(Content-Type 含
519
526
  // zip 或字节头 PK\x03\x04)走 unzipPick 挑 glb 条目。返回 {file, path, sha256, bytes}——
520
527
  // spec 红线:签名 URL 永不进入返回值或落盘内容,只存在于调用栈;错误消息同样 redact
521
528
  // (T8 遗留收口:URL 不进 message/details,诊断靠 details.status / details.reason)。
529
+ // Keep the host cancellation signal attached until the entire response body is read.
530
+ function requestAbort(deps, timeoutMs) {
531
+ const ac = new AbortController();
532
+ const abort = () => ac.abort();
533
+ if (deps.signal?.aborted) ac.abort();
534
+ deps.signal?.addEventListener('abort', abort, { once: true });
535
+ const timer = setTimeout(abort, timeoutMs);
536
+ return {
537
+ signal: ac.signal,
538
+ close() { clearTimeout(timer); deps.signal?.removeEventListener('abort', abort); },
539
+ };
540
+ }
541
+
542
+ async function artifactBytes(deps, res) {
543
+ if (deps.credentialPolicy !== 'env-only') return Buffer.from(await res.arrayBuffer());
544
+ const limit = deps.maxArtifactBytes;
545
+ const advertised = Number(res.headers?.get?.('content-length'));
546
+ if (Number.isFinite(advertised) && advertised > limit) throw fail('DOWNLOAD_FAILED', 'Artifact exceeds host budget');
547
+ const reader = res.body?.getReader();
548
+ if (!reader) throw fail('DOWNLOAD_FAILED', 'Hosted artifact response must be bounded');
549
+ const chunks = []; let size = 0;
550
+ try {
551
+ for (;;) {
552
+ const { done, value } = await reader.read();
553
+ if (done) break;
554
+ size += value.length;
555
+ if (size > limit) throw fail('DOWNLOAD_FAILED', 'Artifact exceeds host budget');
556
+ chunks.push(value);
557
+ }
558
+ } finally { await reader.cancel().catch(() => {}); reader.releaseLock(); }
559
+ return Buffer.concat(chunks);
560
+ }
561
+
522
562
  export async function downloadArtifact(deps, profile, url, destDir, localId) {
523
563
  const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
524
- const ac = new AbortController();
525
- const timer = setTimeout(() => ac.abort(), 60000);
564
+ const request = requestAbort(deps, 60000);
526
565
  let res;
527
566
  let body;
528
567
  try {
529
- res = await fetchImpl(url, { method: "GET", signal: ac.signal });
568
+ res = await fetchImpl(url, { method: "GET", signal: request.signal, redirect: 'error' });
530
569
  if (!res.ok) throw fail("DOWNLOAD_FAILED", `下载失败: HTTP ${res.status}`, { status: res.status });
531
- body = Buffer.from(await res.arrayBuffer());
570
+ body = await artifactBytes(deps, res);
532
571
  if (deps.maxArtifactBytes !== undefined && body.length > deps.maxArtifactBytes) throw fail('DOWNLOAD_FAILED', 'Artifact exceeds host budget');
533
572
  } catch (e) {
534
573
  if (e?.code === "DOWNLOAD_FAILED") throw e; // 非 2xx 已带 status,不二次包裹
535
574
  throw fail("DOWNLOAD_FAILED", "下载失败(签名 URL 已隐去)", { reason: String(e?.cause?.message ?? e?.message ?? e).slice(0, 200) });
536
575
  } finally {
537
- clearTimeout(timer);
576
+ request.close();
538
577
  }
539
578
  // mkdir -p 语义自裁(Task 12 离线段 A):目录不可创建(父路径被文件占据等)也是
540
579
  // 下载失败的一部分——收口 DOWNLOAD_FAILED 家族,消息只含本地落点、绝无签名 URL。
@@ -661,11 +700,10 @@ export async function restJson(deps, profile, { method = "GET", url, body, key,
661
700
  const headers = { "content-type": "application/json", authorization: `Bearer ${key}` };
662
701
  let lastErr;
663
702
  for (let attempt = 0; attempt <= retries; attempt++) {
664
- const ac = new AbortController();
665
- const timer = setTimeout(() => ac.abort(), timeoutMs);
703
+ const request = requestAbort(deps, timeoutMs);
666
704
  try {
667
705
  const res = await fetchImpl(url, {
668
- method, headers, signal: ac.signal, redirect: 'error',
706
+ method, headers, signal: request.signal, redirect: 'error',
669
707
  ...(body !== undefined ? { body } : {}), // GET 零 body(undici 拒绝 GET/HEAD body)
670
708
  });
671
709
  if (!res.ok) {
@@ -683,8 +721,9 @@ export async function restJson(deps, profile, { method = "GET", url, body, key,
683
721
  if (e?.code === "HTTP_STATUS") throw e;
684
722
  lastErr = e; // 网络错/超时/响应体非 JSON → 重试
685
723
  } finally {
686
- clearTimeout(timer);
724
+ request.close();
687
725
  }
726
+ if (deps.signal?.aborted) break;
688
727
  }
689
728
  if (lastErr?.code === "HTTP_STATUS") { // 5xx 耗尽:保 HTTP_FAILED 码,status 仍可诊断
690
729
  throw fail("HTTP_FAILED", `rest 请求失败: ${lastErr.message}`, { status: lastErr.details.status });
@@ -718,19 +757,18 @@ async function uploadImages(deps, profile, key, files) {
718
757
  }
719
758
  const form = new FormData();
720
759
  form.append("file", new Blob([raw], { type: mime }), path.basename(file));
721
- const ac = new AbortController();
722
- const timer = setTimeout(() => ac.abort(), 30000);
760
+ const request = requestAbort(deps, 30000);
723
761
  let res,text;
724
762
  try {
725
763
  res = await fetchImpl(profile.transport.baseUrl + upath, {
726
- method, headers: { authorization: `Bearer ${key}` }, body: form, signal: ac.signal, redirect: 'error',
764
+ method, headers: { authorization: `Bearer ${key}` }, body: form, signal: request.signal, redirect: 'error',
727
765
  });
728
766
  text=await providerText(deps,res);
729
767
  } catch (e) {
730
768
  throw fail("UPLOAD_FAILED", `图片上传失败(网络错): ${file}`,
731
769
  { index: i, reason: String(e?.cause?.message ?? e?.message ?? e).slice(0, 200) });
732
770
  } finally {
733
- clearTimeout(timer);
771
+ request.close();
734
772
  }
735
773
  if (!res.ok) {
736
774
  throw fail("UPLOAD_FAILED", `图片上传失败: HTTP ${res.status}`,
@@ -856,6 +894,7 @@ export async function submitFlow(deps, { provider, model: modelId, flags = {} })
856
894
  // upload 节(T12 在线段 1c):双闸通过后、submit 请求前逐图上传换 token——
857
895
  // Authorization 同源(上方同一凭证解析的 key);失败即 UPLOAD_FAILED 中断(零花费)。
858
896
  if (profile.upload) tokenByFile = await uploadImages(deps, profile, key, imgCtx.views.map(([, f]) => f));
897
+ await deps.beforeSubmit?.(); // Trusted host persists the one-send claim before the paid request.
859
898
  const raw = await restJson(deps, profile, {
860
899
  method: plan.request.method, url: plan.request.url, key,
861
900
  body: canonicalJson(buildBody(bodyTemplate, realCtx)),
@@ -16,7 +16,9 @@ const SAFE_RUN_RE = /^[0-9A-Za-z][0-9A-Za-z._-]{0,255}$/;
16
16
  const SYMBOL_RE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/;
17
17
  const TERMINAL_WORKFLOW_STATES = new Set(["done", "failed", "cancelled"]);
18
18
  const ALLOWED_CHANGES = new Set(["geometry", "transform", "interfaces", "appearance_mapping", "material"]);
19
- const STEP_KEYS = new Set(["appearance.generate", "cad.initial", "cad.revision", "cad.repair", "cad.final", "visual.review"]);
19
+ const STEP_KEYS = new Set(["appearance.generate", "appearance.revision", "mesh.generate", "structure.adapt",
20
+ "cad.initial", "cad.revision", "cad.repair", "cad.final", "visual.review"]);
21
+ const APPEARANCE_PARENT_STEPS = new Set(["appearance.revision", "mesh.generate", "structure.adapt"]);
20
22
 
21
23
  function invalid(message, details) {
22
24
  throw fail("RECEIPT_INVALID", message, details);
@@ -237,6 +239,9 @@ export function parseStudioReceiptEnvelope(value) {
237
239
  exactKeys(step, ["stepKey", "parentCandidateId", "repairOrdinal", "repairBasis"], "receipt.step");
238
240
  if (typeof step.stepKey !== "string" || !STEP_KEYS.has(step.stepKey)) invalid("receipt.step.stepKey is invalid");
239
241
  if (step.parentCandidateId !== null) uuid(step.parentCandidateId, "receipt.step.parentCandidateId");
242
+ if (APPEARANCE_PARENT_STEPS.has(step.stepKey) && step.parentCandidateId === null) {
243
+ invalid("appearance and engineering receipts require their immutable parent candidate");
244
+ }
240
245
  if (step.stepKey === "cad.final" && (!creationLineage || step.parentCandidateId === null)) {
241
246
  invalid("cad.final requires creation lineage and the reviewed preview parent");
242
247
  }
@@ -499,7 +504,8 @@ function inspectWorkflow(workdir, envelope) {
499
504
  instanceDigest: crypto.createHash("sha256").update(bytes).digest("hex"),
500
505
  validatedFields: [...validated].sort(),
501
506
  waitIdentityVerified: ["waitId", "workflowInstanceId", operationField, "localRunId", "remoteRunId", "invocationId",
502
- ...(envelope.step.stepKey === "cad.final" ? ["definitionDigest", "stepKey", "parentCandidateId"] : [])]
507
+ ...(envelope.step.stepKey === "cad.final" || APPEARANCE_PARENT_STEPS.has(envelope.step.stepKey)
508
+ ? ["definitionDigest", "stepKey", "parentCandidateId"] : [])]
503
509
  .every(field => validated.has(field)),
504
510
  resumeDisposition,
505
511
  };
@@ -3,7 +3,7 @@
3
3
  // model input can never select either value.
4
4
  import fs from "node:fs";
5
5
 
6
- export const STUDIO_CONTEXT_FILE = "/tmp/abelien-runtime-delegation/current.json";
6
+ export const STUDIO_CONTEXT_FILE = "/home/agent/workspace/abelien-runtime-delegation/current.json";
7
7
 
8
8
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
9
9
  const DIGEST = /^[0-9a-f]{64}$/;
@@ -188,6 +188,16 @@ function validateModificationRequest(value) {
188
188
  jsonSize(input, "modification input");
189
189
  return input;
190
190
  }
191
+ // Transport identities only; immutable definitions and full tool schemas remain
192
+ // broker-owned. Do not grant v2 to every tool when adding CAD adaptation.
193
+ function validateToolIdentity(value) {
194
+ const tool = record(value, "tool input");
195
+ const v1 = ["model.complete", "hardware.edit", "image.generate", "cad.build", "visual.review", "mesh.generate"];
196
+ if (!(tool.version === 1 && v1.includes(tool.tool))
197
+ && !(tool.version === 2 && tool.tool === "cad.build" && tool.purpose === "appearance_adaptation")) {
198
+ throw fail("STUDIO_INVALID", "tool input identity is invalid");
199
+ }
200
+ }
191
201
  function validateToolOperationRequest(value) {
192
202
  const input = exact(value, ["schemaVersion", "commandId", "stepKey", "parentCandidateId",
193
203
  "repairBasis", "effectIdentity", "effectRequestDigest", "waits", "input"],
@@ -214,10 +224,7 @@ function validateToolOperationRequest(value) {
214
224
  text(repair.reportDigest, "reportDigest", DIGEST, 64);
215
225
  sortedUniqueStrings(repair.issueCodes, "issueCodes", 64);
216
226
  }
217
- const tool = record(input.input, "tool input");
218
- if (!["model.complete", "hardware.edit", "image.generate", "cad.build", "visual.review"].includes(tool.tool) || tool.version !== 1) {
219
- throw fail("STUDIO_INVALID", "tool input identity is invalid");
220
- }
227
+ validateToolIdentity(input.input);
221
228
  noForbiddenKeys(input);
222
229
  jsonSize(input, "tool operation input");
223
230
  return input;
@@ -244,10 +251,7 @@ function validateInvocationRequest(value) {
244
251
  if (input.schemaVersion !== 1) throw fail("STUDIO_INVALID", "invocation schemaVersion must be 1");
245
252
  text(input.operationId, "operationId", UUID, 36);
246
253
  text(input.callSlot, "callSlot", IDENTIFIER, 128);
247
- const tool = record(input.input, "tool input");
248
- if (!["model.complete", "hardware.edit", "image.generate", "cad.build", "visual.review"].includes(tool.tool) || tool.version !== 1) {
249
- throw fail("STUDIO_INVALID", "tool input identity is invalid");
250
- }
254
+ validateToolIdentity(input.input);
251
255
  noForbiddenKeys(input);
252
256
  jsonSize(input, "invocation input");
253
257
  return input;
@@ -352,10 +356,14 @@ function safeSuccess(kind, value) {
352
356
  const row = exact(wrapper.creation, ["creationOperationId", "requestId", "inputRevision", "manifestDigest",
353
357
  "requirementsHash", "hardwareRef", "visualSpecRef", "algorithmRef", "frozenInputs", "plannedOccurrenceBindings",
354
358
  "executionDefinitionVersion", "executionDefinitionDigest", "executionPath", "repairCount", "status"],
355
- ["executionSources", "stage", "latestInvocationId", "latestInvocationStatus", "accountingStatus", "candidateId", "reviewOutcome"], "creation");
359
+ ["executionSources", "stage", "latestInvocationId", "latestInvocationStatus", "accountingStatus", "candidateId", "reviewOutcome",
360
+ "latestTool", "cadBuildAttempts", "previewCandidateId"], "creation");
356
361
  text(row.creationOperationId, "creationOperationId", UUID, 36);
357
362
  text(row.requestId, "requestId", UUID, 36);
358
363
  text(row.manifestDigest, "manifestDigest", DIGEST, 64);
364
+ if (row.latestTool != null) text(row.latestTool, "latestTool", IDENTIFIER, 128);
365
+ if (row.cadBuildAttempts !== undefined) integer(row.cadBuildAttempts, "cadBuildAttempts");
366
+ if (row.previewCandidateId != null) text(row.previewCandidateId, "previewCandidateId", UUID, 36);
359
367
  noForbiddenKeys(row, "creation response");
360
368
  return {creation: row};
361
369
  }
@@ -35,13 +35,15 @@ compatibility:
35
35
 
36
36
  产品目标是尽快展示可信真实 CAD;根据可用资产组合复用、布局和参数化补建,不强制所有部件从零 Build123d。已有可信模型可先只读预览并标注未审查,实际是否支持由宿主决定;采用/交付仍须适用检查。几何执行分 preview 与 final,预览可迭代,不是只能调用两次。需求、组件、几何、相关证据或工具链变化才失效对应缓存;纯报告措辞变化不要求重新导出 CAD。
37
37
 
38
+ 局部自由造型修改的独立 structure.adapt(cad.build@2)步骤及 analytic_carrier_faceted_decoration 范围按[外观件交接](../abelien-appearance/references/local-component-edit.md)执行:保持原接口精确几何及未改实例,不要求 Build123d 重建整个装饰网格;缺可信保护区域/接口检查时只预览,不能升级为 CAD 通过。
39
+
38
40
  ## 本包可执行的辅助工具
39
41
 
40
42
  - `python3 scripts/check_bundle.py --project <候选目录> --manifest bundle.json --baseline <宿主当前基线>`:现代候选包的同版文件、来源/候选 BOM、逐实例数量、原生比例、几何检查覆盖与视觉报告完整性门,格式见[候选包质量](references/bundle-quality.md)。明确只证明契约完整性,不能以 viewed=true 宣称真实审查;不依赖旧 workflow-state。
41
43
  - `python3 scripts/run_stage.py --project <工程目录> --plan <任务计划.json>`:显式输入/输出清单、输入与输出哈希复用、互斥执行、可配置超时、POSIX 进程组终止及独立执行回执。计划格式见建模参考。
42
44
  - `python3 scripts/check_visual_review.py --project <工程目录> --report <审查.json> --subject <本版模型文件> --require-pass`:为 appearance 保留的兼容完整性校验实现,只检查报告结构、文件及哈希、视图和目标覆盖;不执行图像比较或几何验收。视觉语义见同级 `abelien-appearance/references/visual-review.md`。
43
45
 
44
- 进程组清理保持 TERM → wait(2s) → KILL → wait。仅 Darwin 的 PermissionError 且原子进程已退出时,固定 `/bin/ps` 完整校验全表,证明原组无成员或全为同有效 UID 的 zombie 后才按已终止处理;活后代、未知探针与其他错误仍失败。边界见[执行参考](references/build123d-execution.md)。
46
+ 进程组清理保持 TERM → wait(2s) → KILL → wait。仅 Darwin 的 PermissionError 且原子进程已退出时,固定 `/bin/ps` 完整校验全表,证明原组无成员或全为同有效 UID 的 zombie 后才按已终止处理(该表接受 Darwin 的 `?` 未定运行态,它不使整表判废;原组自身成员为 `?` 仍不是 zombie,照旧失败);活后代、未知探针与其他错误仍失败。边界见[执行参考](references/build123d-execution.md)。
45
47
 
46
48
  本包不注册空 `domain.json`、虚假的 `structure.*` capability 或占位 CAD 生成服务。需要有本地执行/渲染/看图能力的宿主,或另行接通对应后端。执行辅助工具是有界前台进程,不是跨请求的后台 Worker;中断后可核验并复用已完成产物,不能恢复一个已经消失的进程。
47
49
 
@@ -52,7 +52,7 @@ python3 <本skill>/scripts/run_stage.py --project <工程目录> --plan <任务J
52
52
 
53
53
  同 task_id 的 lock 防止并发覆盖;相互影响的不同 task_id 仍需宿主串行安排或使用独立候选目录。超时或可捕获的终止信号会终止子进程组并记录失败;强制杀死宿主/断电可能留下 running 与 lock。恢复时先核对 PID 和相关进程,只有证明原任务不再运行才清理该任务自己的锁;不要删除工程或重写产品状态。脚本不自动接管旧进程。
54
54
 
55
- POSIX 清理保持 TERM → wait(2s) → KILL → wait,不提高任务预算。Darwin 对只剩 zombie 的进程组可能返回 PermissionError;只有原 leader 的 poll 已证明退出,且固定 `/bin/ps` 的有界完整 pid/pgid/uid/stat 全表成功、非空、逐行合法时,才接受原 PGID 无成员或全为同有效 UID 的 Z 状态。活成员、不同 UID、未知/非法/不完整输出、探针失败、原 leader 仍活着或非 Darwin 的 PermissionError 都保留原错误;ProcessLookupError 的已有处理不变。探针不接受调用者路径或 shell。
55
+ POSIX 清理保持 TERM → wait(2s) → KILL → wait,不提高任务预算。Darwin 对只剩 zombie 的进程组可能返回 PermissionError;只有原 leader 的 poll 已证明退出,且固定 `/bin/ps` 的有界完整 pid/pgid/uid/stat 全表成功、非空、逐行合法时,才接受原 PGID 无成员或全为同有效 UID 的 Z 状态。`?` 运行态(个别进程状态未定,man page 未列、负载下实测存在)是合法行,不使整表判废;但原 PGID 自身成员为 `?` 时它不是 Z,仍保留原错误。活成员、不同 UID、无法读取/非法/不完整输出、探针失败、原 leader 仍活着或非 Darwin 的 PermissionError 都保留原错误;ProcessLookupError 的已有处理不变。探针不接受调用者路径或 shell。
56
56
 
57
57
  不要覆盖已确认候选,使用新的候选目录并保留旧资产。失败任务的输出可能不完整,不得因文件存在就采用。日志为本地工程诊断,不直接展示给终端用户;任务命令和日志不得输出凭据。
58
58
 
@@ -109,10 +109,17 @@ def _signal_group(process: subprocess.Popen, signum: int) -> None:
109
109
  env={"LC_ALL": "C"}, timeout=2)
110
110
  if probe.returncode == 0 and not probe.stderr and probe.stdout.endswith("\n"):
111
111
  rows = [line.split() for line in probe.stdout.splitlines()]
112
+ # Darwin reports the run state as "?" when it cannot determine it
113
+ # (undocumented in the man page; observed under load as "?", "?N",
114
+ # "?E", "?NE"...). Such a row is structurally complete and must not
115
+ # invalidate the table: otherwise one unrelated process turns a
116
+ # benign exited-group cleanup into an error. An unknown-state row
117
+ # of the original group is still not a zombie, so the member check
118
+ # below keeps rejecting that case.
112
119
  valid = bool(rows) and all(
113
120
  len(row) == 4 and re.fullmatch(r"[1-9][0-9]*", row[0])
114
121
  and all(re.fullmatch(r"-?[0-9]+", value) for value in row[1:3])
115
- and re.fullmatch(r"[IRSTUZ][+<>AELNSsVWX]*", row[3])
122
+ and re.fullmatch(r"[IRSTUZ?][+<>AELNSsVWX]*", row[3])
116
123
  for row in rows)
117
124
  if valid and len({row[0] for row in rows}) == len(rows):
118
125
  members = [row for row in rows if int(row[1]) == process.pid]
@@ -22,6 +22,8 @@ compatibility:
22
22
 
23
23
  已读规则在同一会话中沿用;重新取得的是作品事实与事件。不要通过反复加载 guide/workspace 启动递归调度。
24
24
 
25
+ 局部自由造型外观件使用[现有候选与采用交接](../abelien-appearance/references/local-component-edit.md):ready 候选与 adopted current 保持原谱系,网格预览不得借 CAD 接口完成确认/导出。冻结 target occurrence 和 baseline,提交前与采用前均核对同版;unknown fit 保持预览状态。
26
+
25
27
  ## 产品入口
26
28
 
27
29
  [user-journey.json](workflows/user-journey.json) 中的 `journey` 对应本包完整旅程,供 Agent 阅读。其单个 `drive-product-journey` 步骤由使用 guide 的宿主 Agent 跨轮驱动。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abelien-workspace",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "作品旅程、PRD 与业务契约;user-journey 由 Agent 驱动,draft 供真实硬件阶段使用;不声明假 PRD、确认、报价或支付能力",
5
5
  "resources": [
6
6
  "draft"
@@ -26,4 +26,4 @@ All three new hardware definitions now share `review-hardware-candidates` -> `ha
26
26
 
27
27
  Prismer Parse pending holds the original search-hardware-web cursor as awaiting_external; no extract/merge/select runs. The controlled {runId} host resumes the same frozen args/resource with `workflow resume`; task progress uses fixed original-task GET, received synchronous POST results are reused and only undispatched original-plan operations may first POST. Unknown paid effects/failed terminal runs cannot restart. Running SIGKILL snapshots require same-ID latest-checkpoint/source/plan/fence-owner verification; see the hardware evidence architecture for exact fields.
28
28
 
29
- The fixed trusted Prismer-first chain permits one Volc search only after saved definitive primary denial/successful empty query/unsent missing Key. Source denial permits safe direct HTML/plaintext, not a Parse substitute. Saved denial/fallback receipt SIGKILL windows recover in the same run/resource; only original unstarted fallback/source work may first dispatch, and unknown effects remain closed. The v2 host uses the declared pure recovery guard against the trusted original step snapshot; see the architecture.
29
+ Prismer is temporarily off by default (P2-05.02): the chain saves unsent prismer_disabled markers, searches Volc directly with an English query and Count ≤ 5, and reads sources over public HTTPS (PDF/image sources stay unavailable). When the trusted host enables it, the Prismer-first chain permits one Volc search only after saved definitive primary denial/successful empty query/unsent missing Key. Source denial permits safe direct HTML/plaintext, not a Parse substitute. Saved denial/fallback receipt SIGKILL windows recover in the same run/resource; only original unstarted fallback/source work may first dispatch, and unknown effects remain closed. The v2 host uses the declared pure recovery guard against the trusted original step snapshot; see the architecture.
@@ -0,0 +1,229 @@
1
+ {
2
+ "name": "user-journey-hardware-algorithm",
3
+ "domain": "abelien-workspace",
4
+ "executionProtocol": "pinned-result-v1",
5
+ "description": "Library recall, suitability review, fixed Prismer-first/Volc evidence for unresolved slots, source-bound merge, then the same formal hardware.select and gate",
6
+ "input": {
7
+ "required": [
8
+ "requirements",
9
+ "reactions"
10
+ ]
11
+ },
12
+ "steps": [
13
+ {
14
+ "id": "record-requirements",
15
+ "executor": "cli",
16
+ "action": "resource.put",
17
+ "args": {
18
+ "kind": "draft",
19
+ "id": "hardware-algorithm-d-{{hash}}",
20
+ "body": {
21
+ "requirements": "{{input.requirements}}",
22
+ "reactions": "{{input.reactions}}"
23
+ }
24
+ }
25
+ },
26
+ {
27
+ "id": "classify-hardware",
28
+ "executor": "agent",
29
+ "instruction": "需求已澄清;requirements 是 clarify 的 Requirements:context 为落点与场景,reaction[] 为全部触发→反应,limits[] 每项带 strength。按全部 reaction 推导功能槽位与数量,strength=hard 的 limits 进 hard_constraints,preference 只作排序提示;缺 reaction 或只有品类标签时停止并说明。读取同级 abelien-hardware-selection/SKILL.md,并用其 scripts/load-guide.mjs 依次按需加载 entry、命中的 level1、命中的 level2、contract;仅提交功能槽位、数量、受控三级路径和需求硬约束,不提交型号或猜测数据库事实。回复 {\"slots\":[...]};若需求不足或路径不确定,停止并说明,不能伪造槽位。",
30
+ "replySchema": {
31
+ "slots": "array"
32
+ }
33
+ },
34
+ {
35
+ "id": "search-hardware-wikis",
36
+ "executor": "cli",
37
+ "action": "capability.run",
38
+ "args": {
39
+ "capability": "hardware.search",
40
+ "input": {
41
+ "operation": "search",
42
+ "query": "{{input.requirements}}",
43
+ "requirements": "{{input.requirements}}",
44
+ "slots": "{{steps.classify-hardware.result.slots}}",
45
+ "policy": {
46
+ "prefer_system_candidates": true,
47
+ "top_k_per_slot": 8,
48
+ "include_content": true
49
+ }
50
+ }
51
+ }
52
+ },
53
+ {
54
+ "id": "hardware-knowledge-recalled",
55
+ "executor": "cli",
56
+ "action": "machine.send",
57
+ "args": {
58
+ "machine": "hardware-recall",
59
+ "event": "confirmRecall",
60
+ "instance": "{{steps.search-hardware-wikis.resource.id}}"
61
+ }
62
+ },
63
+ {
64
+ "id": "review-hardware-candidates",
65
+ "executor": "agent",
66
+ "instruction": "Read the complete library candidates and original requirements/slots. Review suitability against function and every hard constraint. Reply {\"unresolved_slots\":[{\"slot_id\":\"original slot\",\"reason\":\"no candidates or specific reasons all candidates are unsuitable\",\"query\":\"official model search targeting function/category/key hard constraints\",\"count\":10}]}. Every zero-candidate slot requires supplemental search. All unsuitable candidates also require search. Preserve rechargeable built-in battery, wireless charging, and all other original requirements; do not ask the user to drop a requirement because the catalog is empty. unresolved_slots contains only slots that need search. Omit suitable library slots entirely; never add placeholder rows with an empty query or count=0. Return [] when all slots are suitable. Before committing this review, the trusted host validates the full original requirements, slots and recall with validate-web-search-plan.mjs; a VALIDATION reply preserves this pause so the same run can be corrected. Count is optional and a positive integer; Prismer search.topK and return.topK are explicit. Do not apply undocumented Prismer query/result limits or truncate requirements. The trusted fixed Prismer-first policy permits Volc only after a saved definitive denial, successful empty query or unsent missing Key; Volc alone checks its official 1-100 character Query and Count 1-50 at dispatch. Unknown dispatched effects never authorize fallback or a new invocation. Do not supply URLs, credentials, commands or providers. System reuse is optional: poor matches must not force complex whole systems. Concept visuals and the original requirements come first.",
67
+ "replySchema": {
68
+ "unresolved_slots": "array"
69
+ }
70
+ },
71
+ {
72
+ "id": "search-hardware-web",
73
+ "executor": "cli",
74
+ "action": "capability.run",
75
+ "args": {
76
+ "capability": "hardware.web-search",
77
+ "input": {
78
+ "requirements": "{{input.requirements}}",
79
+ "slots": "{{steps.classify-hardware.result.slots}}",
80
+ "recall_id": "{{steps.search-hardware-wikis.resource.id}}",
81
+ "unresolved_slots": "{{steps.review-hardware-candidates.result.unresolved_slots}}"
82
+ }
83
+ }
84
+ },
85
+ {
86
+ "id": "extract-web-hardware",
87
+ "executor": "agent",
88
+ "instruction": "When pages contain blob_ref, use resource get <evidenceResourceId> --page <pageId> to read each required full original page; never copy page bodies into replies. Read the persisted hardware-web-evidence pages from search-hardware-web in full. Search Content/snippets/provider official labels are not complete official pages or proof of authority. Extract only manufacturer pages; uncertain source authority remains unknown and third-party mirrors are not official merely because their URL contains a manufacturer domain. Reply {\"candidates\":[{\"slot_id\":\"original slot\",\"page_id\":\"persisted page ID\",\"manufacturer\":\"exact manufacturer\",\"manufacturer_quote\":\"at least 8 source characters\",\"model\":\"exact model\",\"model_quote\":\"at least 8 source characters\",\"parts\":[{\"name\":\"physical part name\",\"name_quote\":\"exact source quote\",\"shape\":\"box|cylinder|sphere|unknown\",\"shape_quote\":\"exact source quote\",\"dimensions_mm\":{\"length\":20,\"width\":12,\"height\":3},\"dimension_quotes\":{\"length\":\"exact quote identifying this part, axis, numeric value and unit\",\"width\":\"exact quote\",\"height\":\"exact quote\"}}]}]}. Quotes must include the exact part/name/shape they support. Each physical part has separate dimensions; cylinders use diameter/height, spheres use diameter. Unknown dimensions are omitted, never invented or defaulted. Conflicting versions and unknown multi-part overall dimensions remain unknown; do not combine contradictory versions. Do not submit page bodies, official/verified/CAD_available/source/geometry claims. Missing reliable pages or no network means candidates empty. This is envelope_only, not true component CAD or engineering verification. For an attributed tuple such as Back Panel: 48mm x 32mm x 0.5mm, add dimensions_quote with the entire exact source string and dimension_axes:[\"length\",\"width\",\"height\"] in original order; dimensions_mm must map each original value/unit to that explicitly declared axis. Preserve alternate imperial tuples in the quote; never use their values as mm or borrow another part's measurements. A 2-value tuple leaves height unknown. A box inferred from dimensions is only an envelope expression: actual part shape and orientation remain unknown. dimension_quotes may be empty for this tuple mode. Never invent coil thickness or total assembly dimensions.",
89
+ "replySchema": {
90
+ "candidates": "array"
91
+ }
92
+ },
93
+ {
94
+ "id": "merge-hardware-web",
95
+ "executor": "cli",
96
+ "action": "capability.run",
97
+ "args": {
98
+ "capability": "hardware.web-merge",
99
+ "input": {
100
+ "requirements": "{{input.requirements}}",
101
+ "slots": "{{steps.classify-hardware.result.slots}}",
102
+ "recall_id": "{{steps.search-hardware-wikis.resource.id}}",
103
+ "evidence_id": "{{steps.search-hardware-web.resource.id}}",
104
+ "candidates": "{{steps.extract-web-hardware.result.candidates}}"
105
+ }
106
+ }
107
+ },
108
+ {
109
+ "id": "select-hardware-from-wikis",
110
+ "executor": "agent",
111
+ "instruction": "先读 abelien-hardware-selection/references/selection-service-contract.md 与真实候选的完整 Wiki。每槽位选择一个真实 component_id,显式 quantity 等于槽位 quantity;manufacturer/model 取候选,selection_reason 非空,wiki_evidence 为至少 8 字符的 Wiki 原文引用数组。selected_models 每项需 constraint_checks,覆盖 function、slot:<hard_constraints 的键>、limit:<每项 hard limit.id>;每项包含 constraint_id、status=verified|unknown|incompatible、reason、evidence:[{component_id,quote}]。多型号另给 compatibility_checks:power、interfaces 两维须包含 status、reason、每个型号的原文 evidence,可信电气数据确定性通过时以 compatibility_report 为准;mechanical、thermal、software 只作提示,可选填写,但填 incompatible 仍阻塞。证据不足填 unknown,不可编造。status=verified|needs_confirmation,plan_mode=assemble_new|adapt_system,其余数组 selected_models/coverage_ledger/remaining_slots/unknowns/replacement_plan 必填。先比较整机,但整机继承缺 manifest 必须待确认,不凭文案抵扣数量。verified 只表示 Wiki 证据判断通过,不是独立工程兼容求解或实测,不代表采用。 Read only merge-hardware-web candidates for the final choice. Preserve structured limits.envelope and source strength; dimension metadata/name/selection-line identity are supplied by the trusted finalizer, never invented. Incompatible dimension candidates require replacement; only decision_required (user hard limit, measured/datasheet size, no axis-aligned fit) returns needs_confirmation, other risk/unknown stays advisory and risk candidates rank last. Electrically incompatible candidates (trusted data: missing bus type or count) require replacing one side; ELECTRICAL_SUPPLY_GAP means add a power or regulator part. all_indexed_candidates_rejected is scoped to this indexed category; occupancy is advisory and clear is only coarse single-part screening. Whole-system reuse is optional; a poor match never forces a complex baseline. External IDs are external:<hash>, with source authority and engineering unknowns; envelopes are not component CAD and cannot grant adoption or delivery.",
112
+ "replySchema": {
113
+ "status": "verified|needs_confirmation",
114
+ "plan_mode": "adapt_system|assemble_new",
115
+ "selected_models": "array",
116
+ "coverage_ledger": "array",
117
+ "remaining_slots": "array",
118
+ "unknowns": "array",
119
+ "replacement_plan": "array",
120
+ "compatibility_checks": "array"
121
+ }
122
+ },
123
+ {
124
+ "id": "select-hardware",
125
+ "executor": "cli",
126
+ "action": "capability.run",
127
+ "args": {
128
+ "capability": "hardware.select",
129
+ "input": {
130
+ "requirements": "{{input.requirements}}",
131
+ "slots": "{{steps.classify-hardware.result.slots}}",
132
+ "recall_id": "{{steps.merge-hardware-web.resource.id}}",
133
+ "selection_proposal": "{{steps.select-hardware-from-wikis.result}}"
134
+ }
135
+ }
136
+ },
137
+ {
138
+ "id": "record-hardware-selection",
139
+ "executor": "cli",
140
+ "action": "resource.update",
141
+ "args": {
142
+ "id": "{{steps.record-requirements.resource.id}}",
143
+ "bodyPatch": {
144
+ "hardwareSelectionId": "{{steps.select-hardware.resource.id}}",
145
+ "hardwareSelectionStatus": "{{steps.select-hardware.result.status}}",
146
+ "hardwareSelectionRevision": "{{steps.select-hardware.result.revision}}",
147
+ "hardwarePlanMode": "{{steps.select-hardware.result.plan_mode}}"
148
+ }
149
+ }
150
+ },
151
+ {
152
+ "id": "hardware-selected",
153
+ "executor": "cli",
154
+ "action": "machine.send",
155
+ "args": {
156
+ "machine": "hardware-selection",
157
+ "event": "confirmSelection",
158
+ "instance": "{{steps.record-requirements.resource.id}}"
159
+ },
160
+ "completeWhen": {
161
+ "path": "steps.hardware-selected.result.to",
162
+ "equals": "selected_pending_confirmation"
163
+ },
164
+ "completionResult": {
165
+ "selectionId": "{{steps.select-hardware.resource.id}}",
166
+ "selectionStatus": "{{steps.select-hardware.result.status}}",
167
+ "selectionRevision": "{{steps.select-hardware.result.revision}}"
168
+ }
169
+ },
170
+ {
171
+ "id": "research-algorithms",
172
+ "executor": "agent",
173
+ "instruction": "硬件选型已完成。读取同级 abelien-algorithm/SKILL.md 和 references/research-policy.md,并从 context-json 的 select-hardware.resource 读取准确厂商、型号、能力和 Wiki 证据。针对 input.reactions 先检索硬件厂商官方资料,再检索算法或模型官方资料与 Hugging Face;端侧不满足且用户允许联网时才比较成熟云服务。选择每个 reaction 的最合适实现并回复 {\"bindings\":[...],\"candidates\":[...],\"sources\":[...],\"unavailable_sources\":[...],\"unknowns\":[...],\"blocking_unknowns\":[...]};来源必须回读,binding 必须明确 edge、cloud 或 hybrid。",
174
+ "replySchema": {
175
+ "bindings": "array",
176
+ "candidates": "array",
177
+ "sources": "array",
178
+ "unavailable_sources": "array",
179
+ "unknowns": "array",
180
+ "blocking_unknowns": "array"
181
+ }
182
+ },
183
+ {
184
+ "id": "select-algorithms",
185
+ "executor": "cli",
186
+ "action": "capability.run",
187
+ "args": {
188
+ "capability": "algorithm.finalize",
189
+ "input": {
190
+ "operation": "select",
191
+ "hardware_selection_id": "{{steps.select-hardware.resource.id}}",
192
+ "hardware_selection_revision": "{{steps.select-hardware.result.revision}}",
193
+ "requirements": "{{input.requirements}}",
194
+ "reactions": "{{input.reactions}}",
195
+ "research": "{{steps.research-algorithms.result}}"
196
+ }
197
+ }
198
+ },
199
+ {
200
+ "id": "algorithm-selected",
201
+ "executor": "cli",
202
+ "action": "machine.send",
203
+ "args": {
204
+ "machine": "algorithm-selection",
205
+ "event": "confirmSelection",
206
+ "instance": "{{steps.select-algorithms.resource.id}}"
207
+ }
208
+ },
209
+ {
210
+ "id": "record-algorithm-selection",
211
+ "executor": "cli",
212
+ "action": "resource.update",
213
+ "args": {
214
+ "id": "{{steps.record-requirements.resource.id}}",
215
+ "bodyPatch": {
216
+ "algorithmSelectionId": "{{steps.select-algorithms.resource.id}}",
217
+ "algorithmSelectionStatus": "{{steps.select-algorithms.result.status}}",
218
+ "algorithmSelectionRevision": "{{steps.select-algorithms.result.revision}}"
219
+ }
220
+ }
221
+ }
222
+ ],
223
+ "result": {
224
+ "selectionId": "{{steps.select-hardware.resource.id}}",
225
+ "selectionStatus": "{{steps.select-hardware.result.status}}",
226
+ "selectionRevision": "{{steps.select-hardware.result.revision}}"
227
+ },
228
+ "legacyResume": "read_only"
229
+ }