@entrorove/skill 0.1.25 → 0.1.28

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.
package/CAPABILITY-MAP.md CHANGED
@@ -1,24 +1,76 @@
1
- # v2 capability map (AC-16)
1
+ # 能力对账图(WP-02 / AC-20)
2
2
 
3
- This is the operation-level inventory for the eight legacy domains. `equivalent` means the v2 operation preserves the contract; `narrower` means the operation is available behind a stricter v2 gate or with a smaller side-effect boundary; `uncovered` means the old operation remains intentionally unavailable and callers must receive an explicit unsupported result.
3
+ 本表把旧八域的远端公开操作与当前本地执行器分开统计。旧版 `SKILL.md` 只规定入口、状态和边界,没有逐域列出操作名;远端操作名采用随包锁定的 v2 runtime catalog 交叉核对,本地操作名直接来自 `local-contracts.mjs` 的 `LOCAL_OPERATION_SCHEMAS`。两个分母不能合并。
4
4
 
5
- ## Enforcement boundary (§2.5)
5
+ ## 边界
6
6
 
7
- The mapped controls apply only to actions executed through Entrorove. They do not control direct shell or operating-system effects performed by an agent or host IDE, including `docker rm`, `DROP DATABASE`, `cp -r`, or arbitrary file writes. Bypass effects may be recorded and can be rejected at delivery or sent back for rework, but they never receive `verified` status and never count as verified delivery. entrorove 管得住“走它的动作”,管不住“不走它的动作”。
7
+ Entrorove 只治理通过技能入口执行的动作:路径、通行证、快照和证据。它不控制智能体或 IDE 直接执行的宿主 shell/操作系统副作用,例如 `docker rm`、`DROP DATABASE`、`cp -r` 或任意写文件。绕过技能的副作用可以被记录、拒绝交付或要求重做,但永远不会获得 `verified`,也不算已验证交付。对外承诺只能是:绕过治理的动作不会被系统认证为已交付。
8
8
 
9
- | Legacy domain | Legacy operations | v2 mapping and semantics |
9
+ ## 旧 `SKILL.md` 原文基线(AC-20 的来源)
10
+
11
+ 本节固定引用基线提交 `5da9c2d`(`@entrorove/skill@0.1.24`)的原文入口和保证,不把当前代码自报的操作名当作旧能力分母。原文明确写出的入口是:
12
+
13
+ | 原文位置 | 原文入口/保证 | 对账要求 |
10
14
  | --- | --- | --- |
11
- | aimlock | capabilities, help, intake, classify, scope-contract, skill-route, propose-nodes, accept-nodes, snapshot-plan, mutate-gate, continuity-check, interrupt, keep-alive, delivery-doc, validate-json, snapshot-verify, run-status, chain-plan, chain-status, feedback | `capabilities/help/intake` → equivalent shared discovery; `classify/skill-route/scope-contract/propose-nodes/accept-nodes/continuity-check/validate-json` → equivalent through `task-describe/message-route`; `snapshot-plan/snapshot-verify/mutate-gate/interrupt/keep-alive/run-status/chain-plan/chain-status/feedback/delivery-doc` → narrower through persisted flow state, local guarded writes, cancellation, status, and evidence gates. |
12
- | blueprint | capabilities, help, intake, validate, compile-inline, acceptance-report | `capabilities/help/intake/validate/compile-inline/acceptance-report` → equivalent through `blueprint` runtime; acceptance evidence is caller supplied and cannot be self-promoted to verified. |
13
- | calctool | capabilities, help, intake, validate, compile-inline | `capabilities/help/intake/validate/compile-inline` → equivalent through `calctool`; `formula-evaluate/formula-graph/formula-final-gate` → local Decimal execution and final-gate evidence. |
14
- | swarm | capabilities, help, intake, org-chart, blueprint-bridge, dispatch, claim, report, accept, swarm-status, traffic-light, security-check, validate-json, heartbeat, reclaim | `capabilities/help/intake/org-chart/blueprint-bridge/dispatch/claim/report/accept/swarm-status/traffic-light/security-check/validate-json/heartbeat/reclaim` → narrower through server-routed coordination, bounded worker counts, leases, and persisted reported/verified states. |
15
- | validator | capabilities, help, intake, plan, validate-structure, security-scan, compliance-audit, functional-verify, verdict | All nine → equivalent through the `validator` runtime; `functional-verify` requires API-side receipt verification and `verdict` cannot turn reported evidence green. |
16
- | mergeguard | capabilities, help, intake, resolve-propose, ruleguard-scan, ruleguard-compile | All six → equivalent through `mergeguard`; `merge-verified` remains a local operation and fails closed without a valid baseline, evidence, and receipt. |
17
- | archguard | capabilities, help, intake, contract-create, contract-get, contract-update, template-list, checkpoint, rules-scan, complexity-report, drift-status, ledger-query | All twelve → equivalent through `archguard`; contract writes and ledger records are local and path/baseline bound. |
18
- | confirm-protocol | capabilities, help, interaction-request, interaction-answer, chat-render, memory-set, memory-list, memory-clear, batch-request, audit-query | All ten → equivalent for pure protocol operations; native/mobile notifier operations remain explicitly planned and are returned as unavailable. High-risk interaction keeps `default: null`, no memory, and no batch approval. |
15
+ | `SKILL.md:12` | `npx @entrorove/skill install`;只替换受管理的 pristine 副本并保留手工文件 | 安装面必须保持单一受管理副本;发布件另行复测 |
16
+ | `SKILL.md:14-18` | `configure`、协议 `agreement`、密钥只从标准输入进入、协议接受后再重试 | 凭据与协议链路不能由本地自报替代 |
17
+ | `SKILL.md:22` | `invoke`、先发现 `capabilities`、按 schema 校验、`start`、`continue`、`direct`、`requestId` 内容派生 | 发现失败即停止;不能把未列入合同的操作静默映射 |
18
+ | `SKILL.md:28-30` | merge 必须带 `baselineRef`;能力图必须说明 equivalent/narrower/uncovered | 远端八域表只作为 runtime 交叉证据,不作为旧文原文的替代 |
19
+ | `SKILL.md:32-38` | `status` 只读恢复原 request、`flows` 列可恢复流;状态含 `reported` 与 `verified` | 恢复不得重发 POST,reported 不得升绿 |
20
+ | `SKILL.md:40` | `local <operation>`、`local capabilities`、快照/通行证/预算/证据/合并闸门;状态仅存 `~/.entrorove/` | 本地操作表必须逐项对应 schema 与测试,未跑项单列 |
21
+
22
+ 旧文没有逐域操作名;因此下面的八域远端表明确标注为“锁定 runtime catalog 的交叉核对”,本地表明确标注为当前 schema 分区。两者都不能反向改写旧 `SKILL.md` 的原文基线。
23
+
24
+ ## 远端基线(旧八域)
19
25
 
20
- The local execution surface also covers `snapshot-plan`, `snapshot-verify`, `mutate-gate`, `gate-issue`, `gate-verify`, `guarded-write`, `budget-init`, `budget-context`, `budget-read`, `budget-status`, `budget-extend`, `budget-auto-renew-request`, `budget-auto-renew`, `budget-auto-renew-stop`, `peer-coordinate`, `spawn-bind`, `peer-complete`, `complete`, `lease-verify`, `evidence-sign`, `evidence-verify`, `merge-verified`, `rollback`, `final-gate`, `formula-evaluate`, `formula-graph`, `formula-final-gate`, `calculation-final-gate`, `run-approved-plan`, `branch-create`, `branch-list`, `branch-switch`, `diff-report`, `preflight`, `ledger-query`, `chain-init`, `chain-resume`, `chain-status`, `chain-answer`, `branch-create-virtual`, `chain-ledger-init`, `chain-ledger-status`, `chain-ledger-query`, `probe`, `reassess`, `task-describe`, `message-route`, `message-accept`, and `task-resume`. Local branch operations are virtual ledger records and never create Git branches or worktrees.
26
+ | 域 | 旧 runtime 公开操作 | 当前映射 | 语义边界 |
27
+ | --- | --- | --- | --- |
28
+ | aimlock | `capabilities`, `help`, `intake`, `classify`, `scope-contract`, `skill-route`, `propose-nodes`, `accept-nodes`, `snapshot-plan`, `mutate-gate`, `continuity-check`, `interrupt`, `keep-alive`, `delivery-doc`, `validate-json`, `snapshot-verify`, `run-status`, `chain-plan`, `chain-status`, `feedback` | discovery、任务路由、快照、门控、状态、取消和交付证据 | `brain-plan/check/validate/status` 没有 aimlock 公开入口,见 AC-24 |
29
+ | blueprint | `capabilities`, `help`, `intake`, `validate`, `compile-inline`, `acceptance-report` | `blueprint` runtime;验收证据由调用方提供,不能自升 `verified` | `acceptance-report` 不是自报绿灯 |
30
+ | calctool | `capabilities`, `help`, `intake`, `validate`, `compile-inline` | `calctool` runtime;公式执行和 final-gate 在本地 Decimal 层 | `research` 是 pipeline 内部阶段,不是独立公开操作 |
31
+ | swarm | `capabilities`, `help`, `intake`, `org-chart`, `blueprint-bridge`, `dispatch`, `claim`, `report`, `accept`, `swarm-status`, `traffic-light`, `security-check`, `validate-json`, `heartbeat`, `reclaim` | 服务端路由、租约和 `reported`/`verified` 状态 | 本地 peer/coordinate 是统一本地层,不能冒充旧 swarm 远端 |
32
+ | validator | `capabilities`, `help`, `intake`, `plan`, `validate-structure`, `security-scan`, `compliance-audit`, `functional-verify`, `verdict`;local-runner:`sandbox-run`, `fuzz-input`, `perf-benchmark`, `intrusive-test` | pure validator 与受控 runner 分开 | 生产验证需独立回执;`verdict` 不能把 reported 变绿 |
33
+ | mergeguard | `capabilities`, `help`, `intake`, `resolve-propose`, `ruleguard-scan`, `ruleguard-compile`;local-runner:`branch-create`, `branch-list`, `branch-switch`, `diff-report`, `preflight`, `merge-verified`, `rollback`, `ledger-query` | pure mergeguard 与本地合并闸门分开 | 虚拟分支不会创建 Git 分支/worktree |
34
+ | archguard | `capabilities`, `help`, `intake`, `contract-create`, `contract-get`, `contract-update`, `template-list`, `checkpoint`, `rules-scan`, `complexity-report`, `drift-status`, `ledger-query` | `archguard-*` 本地合同、快照、范围和账本 | 本地写入仍受路径和基线绑定 |
35
+ | confirm-protocol | `capabilities`, `help`, `interaction-request`, `interaction-answer`, `chat-render`, `memory-set`, `memory-list`, `memory-clear`, `batch-request`, `audit-query` | 纯协议操作 | IDE/native/mobile 通知不是本地层能力;高风险交互默认不批准、不记忆、不批量 |
36
+
37
+ ## 本地基线(当前 `LOCAL_OPERATION_SCHEMAS` 100 项)
38
+
39
+ | 分区 | 精确操作名 | 证据位置 |
40
+ | --- | --- | --- |
41
+ | 核心 discovery、快照、门控、预算、协调、证据、链、任务路由(54) | `capabilities`, `help`, `snapshot-plan`, `snapshot-verify`, `mutate-gate`, `gate-issue`, `mutation-snapshot-attest`, `gate-verify`, `guarded-write`, `budget-init`, `budget-handoff`, `budget-context`, `budget-read`, `budget-status`, `budget-extend`, `budget-transition`, `peer-coordinate`, `coordinate`, `spawn-bind`, `peer-complete`, `complete`, `lease-verify`, `evidence-sign`, `evidence-verify`, `rollback`, `final-gate`, `formula-evaluate`, `formula-graph`, `formula-final-gate`, `calculation-final-gate`, `run-approved-plan`, `branch-create`, `branch-create-virtual`, `branch-list`, `branch-switch`, `diff-report`, `preflight`, `ledger-query`, `chain-ledger-query`, `chain-init`, `chain-ledger-init`, `chain-resume`, `chain-status`, `chain-ledger-status`, `chain-answer`, `budget-auto-renew-request`, `budget-auto-renew`, `budget-auto-renew-stop`, `probe`, `reassess`, `task-describe`, `message-route`, `message-accept`, `task-resume` | `packages/entrorove-skill/local-contracts.mjs:14-114` |
42
+ | ArchGuard 本地适配(17) | `archguard-capabilities`, `archguard-help`, `archguard-intake`, `archguard-template-list`, `archguard-contract-create`, `archguard-contract-get`, `archguard-contract-update`, `archguard-snapshot`, `archguard-checkpoint`, `archguard-scope-request`, `archguard-rules-scan`, `archguard-complexity-report`, `archguard-drift-status`, `archguard-ledger-init`, `archguard-ledger-query`, `archguard-human-resume`, `archguard-human-direction-request` | `local-archguard-contracts.mjs:21`、`local-contracts.mjs:113` |
43
+ | Calctool 本地适配(24) | `calctool-run`, `calctool-compile`, `calctool-generate`, `calctool-compile-tool`, `calctool-verify`, `calctool-final-gate`, `calctool-estimate`, `calctool-impact`, `calctool-status`, `calctool-inventory`, `calctool-purge`, `calctool-probe-env`, `calctool-adapt-config`, `calctool-intake-round`, `calctool-brain-handshake`, `calctool-brain-invoke`, `calctool-swarm-orchestrate`, `calctool-auto-pipeline`, `calctool-brain-status`, `calctool-brain-resume`, `calctool-brain-complete`, `calctool-brain-events`, `calctool-brain-cancel`, `calctool-blueprint-orchestrate` | `local-calctool-contracts.mjs:4-42`、`local-contracts.mjs:113` |
44
+ | 合并账本本地适配(4) | `merge-verified`, `merge-freeze`, `merge-preflight`, `merge-journal-head` | `local-contracts.mjs:113`、`local-merge-contracts.mjs` |
45
+ | 工作流本地适配(1) | `workflow-run-step` | `workflow-local-contracts.mjs`、`local-contracts.mjs:64` |
46
+
47
+ 本地分区合计 `54 + 17 + 24 + 4 + 1 = 100`。`capabilities`/`help` 也计入 schema,因为它们是本地 CLI 可发现合同的一部分;不会被从分母中悄悄删除。
48
+
49
+ ## 十条核心保证对账(AC-21)
50
+
51
+ | 保证 | 仓库兑现者与状态 | 已跑绿测试 | 已发布版本 |
52
+ | --- | --- | --- | --- |
53
+ | 签名通行证 | **部分满足**:`local-mutation-authority.mjs` 拒绝过期/伪造签名;正向 signer 夹具仍缺 | `item14-mutation-authority.test.mjs`(拒绝路径) | 0.1.25 未验证 |
54
+ | 冻结基线 | **已满足**:snapshot receipt、handoff baseline、scope digest 绑定 | `item14-snapshot-integrity.test.mjs` | 0.1.25 未验证 |
55
+ | 回执验签 | **已满足**:`local-evidence-verification.mjs` 校验绑定、签名和有效期 | `item11-evidence-verification.test.mjs` | 0.1.25 未验证 |
56
+ | reported 不升 verified | **已满足**:final-gate 保持 reported/incomplete/verified 分离 | `item16-final-gate-cli.test.mjs` C21-CLI07/09 | 0.1.25 未验证 |
57
+ | 同规则三次红灯中断 | **未确认**:中断逻辑存在,合法 signer 夹具缺失 | `item17-archguard-generation.test.mjs` 仅覆盖生成器,不覆盖 signer 链 | 0.1.25 未验证 |
58
+ | 豁免强制审批与到期 | **已满足**:RuleGuard exemption 校验 actor、路径和 expiry | `item12-ruleguard-exemption-unit.test.ts` RGE01/RGE03 | 0.1.25 未验证 |
59
+ | 预算三轴 | **部分满足**:文件、token、时长字段各有实现,但缺三轴同一正向矩阵 | `item07-budget.test.mjs` B07-06/08 | 0.1.25 未验证 |
60
+ | 禁递归派生 | **部分满足**:`local-autocoord17-peers.mjs` 拒绝递归绑定;专门正反例未成套 | 读码证据,不计完整绿测 | 0.1.25 未验证 |
61
+ | 全面 fail-closed | **部分满足**:错误、过期、无回执路径阻断;“全面”矩阵未完成 | `item11-evidence-verification.test.mjs` I11-L06、`item16-final-gate-cli.test.mjs` C21-CLI09 | 0.1.25 未验证 |
62
+ | 仅 GET 恢复 | **已满足**:transport/status 只读恢复原 requestId,不重发 POST | `item18-transport.test.mjs` C04、`item17-local-chain-controller.test.mjs` LC17-EX02/03 | 0.1.25 未验证 |
63
+
64
+ ## 五个疑点(AC-22)
65
+
66
+ | 疑点 | 结论 | 证据与理由 |
67
+ | --- | --- | --- |
68
+ | `peer-status` | **缺失(开单)** | `statusLocalPeer17` 只在 `local-autocoord17-controller.mjs` 内部存在,未进入 `local-cli.mjs`/公开 schema;内部函数不算交付能力。 |
69
+ | `interrupt` | **已实现(仓库)** | `assets/aimlock-runtime-v7.0.44.mjs` operation catalog 与 policy 均有;发布件需 clean-install 后复测。 |
70
+ | `delivery-doc` | **已实现(仓库)** | 同一 aimlock runtime/source 有 operation、schema、entry;发布面保持未验证。 |
71
+ | calctool `research` | **有意不独立公开(理由)** | `calctool-runtime-v7.0.43.mjs` 将其作为 pipeline 内部 partition kind;它没有独立请求合同,拆成公开 operation 会重复 intake/验证边界。若需求改为独立调用,再开变更。 |
72
+ | aimlock 自有 `brain plan/check/validate/status` | **缺失(开单)** | 现有 `brain-*` 只在 calctool contract/runtime;aimlock 只有 chain/run-status,没有书面迁移或归属合同。 |
21
73
 
22
- The local CLI reports thrown failures as `entrorove.local-error/1.0` with `schemaVersion`, `status`, `code`, and `message`. Formula contracts require non-empty `add`/`mul` argument lists, reject zero denominators consistently, and carry an explicit `roundingPolicy` (half-away-from-zero, two decimal places by default).
74
+ ## 发布与校验边界
23
75
 
24
- The only intentionally uncovered legacy surface is an old notifier or an operation that would execute remote filesystem changes. These are represented as planned or narrowed behavior in capabilities; no adapter may silently substitute a different operation.
76
+ 本文件记录的是仓库对账,不代表 npm 发布件已验证。`@entrorove/skill@0.1.25` 必须在干净目录安装后重新执行对应 AC,未执行项单独列出,不能计入通过。生产服务、数据库和其他项目不属于本表的运行时依赖。
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.28 — 2026-10-09
4
+
5
+ - 更新了协同执行与验证隔离能力、协同执行与验证隔离能力、协同执行与验证隔离能力、协同执行与验证隔离能力、协同执行与验证隔离能力、协同执行与验证隔离能力、协同执行与验证隔离能力、协同执行与验证隔离能力、安装包配置、自动版本与更新日志、统一本地错误信封。
6
+ - Updated coordination execution and validator isolation capabilities, coordination execution and validator isolation capabilities, coordination execution and validator isolation capabilities, coordination execution and validator isolation capabilities, coordination execution and validator isolation capabilities, coordination execution and validator isolation capabilities, coordination execution and validator isolation capabilities, coordination execution and validator isolation capabilities, package configuration, automatic versioning and changelog, unified local error envelopes.
7
+
8
+ ## 0.1.27 — 2026-10-08
9
+
10
+ - 更新了IDE 工作流程说明、能力覆盖映射、Archguard 架构合同与执行守卫、工作流原执行请求恢复。
11
+ - Updated IDE workflow instructions, capability coverage map, Archguard architecture contract and execution guard, workflow original execution request recovery.
12
+
13
+ ## 0.1.26 — 2026-10-07
14
+
15
+ - 更新了能力覆盖映射、安装包配置。
16
+ - Updated capability coverage map, package configuration.
17
+
3
18
  ## 0.1.25 — 2026-10-07
4
19
 
5
20
  - 原链本地执行与固定Git计费收口,补齐macOS路径和发布夹具
package/SKILL.md CHANGED
@@ -5,7 +5,7 @@ description: An engineering skill that understands a task, chooses a fitting wor
5
5
 
6
6
  # entrorove · 熵巡
7
7
 
8
- Version: 0.1.25.
8
+ Version: 0.1.28.
9
9
 
10
10
  Read the user's goal and repository instructions first. Preserve the original task through planning, execution and verification. Use the workflow only where it helps; a small, low-risk edit can proceed directly. Never claim a reported step is a verified result.
11
11
 
@@ -25,6 +25,10 @@ On first use or after a material agreement revision, the CLI returns a Confirm P
25
25
 
26
26
  Send JSON on standard input to `npx @entrorove/skill invoke`. Before every execution call, discover `operation: "capabilities"` with `{}` and validate input against the returned operation schema. The CLI performs remote discovery automatically; local adapters discover the installed contract before execution. Discovery failure stops execution. Adapters that expose the four-step task route can call `task-describe`, `message-route`, `message-accept` and `task-resume` in order; `start` remains the one-call workflow equivalent. Then start with `operation: "start"` and an `input.task` containing the goal, work type, risk, difficulty and observed scope facts. Work types are `implementation`, `documentation`, `calculation`, `merge`, `review`, `decision` and `diagnosis`. Assess facts from the real task and repository; do not invent certainty. entrorove chooses the workflow. A small task may return `decision: "direct"`, meaning the IDE should do the work without another service call. The public envelope is `entrorove.skill.request/2.0`; `requestId` is the SHA-256 digest of `operation + canonical(input)`. Version 1.0 remains accepted for older callers.
27
27
 
28
+ ### Per-task cloud sandbox choice
29
+
30
+ When the host exposes an enabled cloud-sandbox preference and a new task is about to dispatch a cloud model, ask once whether this task should use the cloud sandbox. This is a low-risk routing preference, separate from agreement acceptance and the medium/high-risk confirmations below. Offer `use for this task` and `continue locally`; if the user gives no answer, use the cloud sandbox by the host's stated timeout default. An explicit opt-out applies only to the current task and must not change the account-wide sandbox preference. Do not ask again while continuing or resuming the same task. If the account-wide preference is disabled or unavailable, do not dispatch a cloud task and do not show a misleading per-task prompt; preserve the normal local workflow and report the prerequisite clearly. This choice never bypasses the normal task schema, mutation authority, evidence checks, or risk confirmation.
31
+
28
32
  ```json
29
33
  {"operation":"start","input":{"task":{"goal":"Update the checkout flow and verify it","workType":"implementation","risk":"medium","difficulty":"medium","crossModule":true,"parallel":false,"unclear":false,"architectureContract":false}}}
30
34
  ```
@@ -15,7 +15,10 @@ async function verifyWriteAuthorization(ctx, input, snapshot) {
15
15
  const pass = verified.pass;
16
16
  if (canonical(pass) !== canonical(savedPass) || canonical(await readJson(input.gatePassPath)) !== canonical(savedPass)) fail('ARCH_PASS_RACE', 'gate pass changed during verification');
17
17
  if (pass.snapshotId !== snapshot.snapshotId || pass.snapshotReceipt.snapshotDigest !== snapshot.snapshotDigest
18
- || pass.snapshotReceipt.contractDigest !== snapshot.contractDigest || pass.stepId !== snapshot.stepId
18
+ // The cloud pass binds the original Aimlock path contract, while the
19
+ // ArchGuard snapshot binds its independent architecture contract. The
20
+ // signed scope authorization below binds both digests before a write.
21
+ || pass.stepId !== snapshot.stepId
19
22
  || canonical(pass.paths) !== canonical([...snapshot.paths, 'arch.contract.yaml'].sort())) fail('ARCH_PASS_BINDING', 'gate pass is not bound to the exact snapshot and path set');
20
23
  const verifiedScope = await verifyAuthority(ctx, input.scopeAuthorization, 'entrorove.archguard-scope-authorization/1.0');
21
24
  const scope = verifiedScope.value;
@@ -62,8 +62,13 @@ export async function archguardSnapshot(input) {
62
62
  files.push({ path: relative, exists: value.exists, content: value.exists ? value.content.toString('base64') : null,
63
63
  sha256: value.exists ? sha256(value.content) : MISSING_SHA256, mode: value.mode });
64
64
  }
65
+ // The native ArchGuard record is bound to the architecture contract, while
66
+ // its mutation receipt must remain bound to the original Aimlock scope.
67
+ // Keep those digests separate when a cloud mutation challenge supplies the
68
+ // already verified path contract; legacy local calls retain the native
69
+ // architecture digest.
65
70
  const native = await snapshotPlan({ ...input, repositoryRoot: ctx.root, paths: files.map(file => file.path), snapshotId,
66
- contractDigest: current.contractDigest });
71
+ contractDigest: input.scopeContractDigest ?? current.contractDigest });
67
72
  const verified = await snapshotVerify({ ...input, repositoryRoot: ctx.root, snapshotId });
68
73
  for (const file of files) {
69
74
  const actual = await readBytes(path.join(ctx.root, file.path), true, ctx);
@@ -0,0 +1,36 @@
1
+ import { canonical, sha256 } from './local-integrity.mjs';
2
+ import { readSwarmCloudAuthority, readSignedSwarmMaterial } from './local-swarm-authority.mjs';
3
+ import { readTaskHandoffFence } from './local-task-handoff-fence.mjs';
4
+ import { coordFail17 } from './local-autocoord17-store.mjs';
5
+ import { createTaskMessageClient } from './remote-client.mjs';
6
+
7
+ export async function coordNative17(material, live = true) {
8
+ if (live) return readSwarmCloudAuthority(material, 'issue');
9
+ const verified = readSignedSwarmMaterial(material, 'report'), fence = await readTaskHandoffFence(material);
10
+ const attempt = fence.executionAttempts.find(value => value.authority.attemptNonce === material.executionAttemptNonce);
11
+ if (!attempt || canonical(attempt.authority) !== canonical(material.executionAuthority) || attempt.executionHome !== material.home
12
+ || ['flowId', 'taskId', 'ownerId', 'taskDigest', 'chainDigest', 'runnerKeyId'].some(key => attempt.authority[key] !== verified.grant[key])) {
13
+ coordFail17('ENTROROVE_COORD_AUTHORITY', 'coordination requires its exact original native registered authority');
14
+ }
15
+ return { ...verified, fence, attempt, root: material.repositoryRoot, home: material.home,
16
+ repositoryDigest: sha256(material.repositoryRoot).slice(0, 32), paths: verified.grant.paths,
17
+ active: fence.status === 'active' && attempt.status !== 'revoked' && !fence.revokedLeaseIds.includes(verified.lease.leaseId) };
18
+ }
19
+ export function coordTask17(state, input) {
20
+ const task = state.tasks.find(value => value.taskId === input.taskId);
21
+ if (!task || input.agentId !== task.agentId || input.chainId !== task.chainId) {
22
+ coordFail17('ENTROROVE_COORD_TASK_BINDING', 'operation must retain its original task, agent and chain');
23
+ }
24
+ return task;
25
+ }
26
+ export async function coordCloud17(material, dependencies) {
27
+ const configured = dependencies ?? (await import('./cli.mjs')).remoteDependencies(material.home);
28
+ const client = createTaskMessageClient(configured), output = await client.invoke('task-resume', { flowId: material.flowId });
29
+ const task = output.task, grant = material.delegationGrant;
30
+ if (output.flowId !== material.flowId || !task || task.flowId !== material.flowId || task.taskId !== grant.taskId
31
+ || task.ownerId !== grant.ownerId || task.taskDigest !== grant.taskDigest || !task.task || typeof task.task.goal !== 'string'
32
+ || !['accepted', 'active'].includes(task.status) || output.continuity?.handoffPaused || output.continuity?.activeTakeover) {
33
+ coordFail17('ENTROROVE_COORD_CLOUD_BINDING', 'original authenticated cloud task is unavailable or changed');
34
+ }
35
+ return { originalTask: structuredClone(task.task), apiOrigin: new URL(configured.apiOrigin()).origin };
36
+ }
@@ -0,0 +1,164 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { canonical } from './local-integrity.mjs';
3
+ import { peerCoordinate, completePeer } from './local-swarm.mjs';
4
+ import { readSwarmCloudAuthority } from './local-swarm-authority.mjs';
5
+ import { canonicalSwarmPaths } from './local-swarm-state.mjs';
6
+ import { coordNative17, coordCloud17, coordTask17 } from './local-autocoord17-authority.mjs';
7
+ import { coordDuration17, coordExact17, coordFail17, coordEvent17, readCoordination17, withCoordination17 } from './local-autocoord17-store.mjs';
8
+ import { registerTask17, taskConflicts17, enqueueResource17 } from './local-autocoord17-tasks.mjs';
9
+ import { bindSpawn17, physicalPeers17, registerIntent17, scanIntent17 } from './local-autocoord17-peers.mjs';
10
+ import { cancelWait17, coordinationBlocked17, registerWait17, scanQueues17, scanWaits17 } from './local-autocoord17-waits.mjs';
11
+
12
+ const IDENTITY = ['repositoryRoot', 'coordinationHome', 'taskId', 'agentId', 'chainId'];
13
+ function input17(input, fields = [], optional = []) {
14
+ coordExact17(input, [...IDENTITY, ...fields], optional);
15
+ try { return structuredClone(input); } catch { coordFail17('ENTROROVE_COORD_INPUT', 'coordination input must be finite original JSON'); }
16
+ }
17
+ async function storageBinding(input) {
18
+ if (typeof process.env.ENTROROVE_SWARM_COORDINATION_HOME !== 'string'
19
+ || await fs.realpath(process.env.ENTROROVE_SWARM_COORDINATION_HOME) !== await fs.realpath(input.coordinationHome)) {
20
+ coordFail17('ENTROROVE_COORD_PHYSICAL_HOME', 'the original physical Swarm coordination home must be explicitly retained');
21
+ }
22
+ }
23
+ async function scan17(loaded) {
24
+ const state = loaded.state, newDecisions = scanQueues17(state);
25
+ for (const task of state.tasks) {
26
+ const native = await coordNative17(task.material, false);
27
+ if (!native.active || Date.parse(native.grant.expiresAt) <= Date.now()) {
28
+ coordEvent17(state, 'lease-expired', task.taskId, native.lease.leaseId, { leaseId: native.lease.leaseId }, native.lease.leaseId);
29
+ }
30
+ const physical = await physicalPeers17(task);
31
+ for (const intent of state.intents.filter(value => value.taskId === task.taskId)) scanIntent17(state, task, intent, physical);
32
+ }
33
+ scanWaits17(state); return newDecisions;
34
+ }
35
+ function taskView(loaded, task) {
36
+ return { schemaVersion: 'entrorove.local-autocoord/1.0', taskId: task.taskId,
37
+ originalTask: structuredClone(task.originalTask), card: structuredClone(task.card),
38
+ conflicts: taskConflicts17(loaded.state, task), ...coordinationBlocked17(loaded.state, task),
39
+ messages: loaded.state.messages.filter(value => value.taskId === task.taskId),
40
+ waits: loaded.state.waits.filter(value => value.taskId === task.taskId),
41
+ decisions: loaded.state.decisions.filter(value => value.taskId === task.taskId),
42
+ queues: loaded.state.lockQueues.filter(value => value.taskId === task.taskId) };
43
+ }
44
+ export async function registerLocalTaskCard17(rawInput, dependencies) {
45
+ const input = input17(rawInput, ['material', 'card']); await storageBinding(input);
46
+ const native = await coordNative17(input.material), cloud = await coordCloud17(input.material, dependencies);
47
+ return withCoordination17(input, 'task-register', loaded => {
48
+ const task = registerTask17(loaded.state, input, native, cloud); return taskView(loaded, task);
49
+ }, true);
50
+ }
51
+ export async function statusLocalTaskCard17(rawInput) {
52
+ const input = input17(rawInput); await storageBinding(input);
53
+ const loaded = await readCoordination17(input), task = coordTask17(loaded.state, input);
54
+ await coordNative17(task.material, false);
55
+ return { ...taskView(loaded, task), sequence: loaded.sequence, head: loaded.head };
56
+ }
57
+ export async function coordinateLocalPeer17(rawInput) {
58
+ const input = input17(rawInput, ['intentId', 'paths', 'baselineHash', 'ttlMs', 'coordinationTimeoutMs', 'fileLockTtlMs', 'priority']);
59
+ await storageBinding(input);
60
+ const loaded = await readCoordination17(input), task = coordTask17(loaded.state, input);
61
+ await coordNative17(task.material);
62
+ // Only the existing signed native entry can persist an engineering write
63
+ // intent. Our additional partial result never reserves or grants a path.
64
+ await peerCoordinate(task.material);
65
+ return withCoordination17(input, 'peer-coordinate', async fresh => {
66
+ const current = coordTask17(fresh.state, input), intent = registerIntent17(fresh.state, current, input);
67
+ return scanIntent17(fresh.state, current, intent, await physicalPeers17(current));
68
+ });
69
+ }
70
+ export async function statusLocalPeer17(rawInput) {
71
+ const input = input17(rawInput, ['intentId']); await storageBinding(input);
72
+ return withCoordination17(input, 'peer-status', async loaded => {
73
+ const task = coordTask17(loaded.state, input), intent = loaded.state.intents.find(value => value.intentId === input.intentId && value.taskId === task.taskId);
74
+ if (!intent) coordFail17('ENTROROVE_COORD_INTENT_BINDING', 'peer status must name its original task intent');
75
+ await coordNative17(task.material, false);
76
+ const view = scanIntent17(loaded.state, task, intent, await physicalPeers17(task));
77
+ return { ...view, spawnRequest: loaded.state.spawnRequests.find(value => value.spawnRequestId === view.spawnRequestId) ?? null };
78
+ });
79
+ }
80
+ export async function completeLocalPeer17(rawInput) {
81
+ const input = input17(rawInput, ['intentId']); await storageBinding(input);
82
+ const loaded = await readCoordination17(input), task = coordTask17(loaded.state, input);
83
+ const intent = loaded.state.intents.find(value => value.intentId === input.intentId && value.taskId === task.taskId);
84
+ if (!intent) coordFail17('ENTROROVE_COORD_INTENT_BINDING', 'completion must name its original task intent');
85
+ const actual = await completePeer(task.material);
86
+ return withCoordination17(input, 'peer-complete', async fresh => {
87
+ const current = fresh.state.intents.find(value => value.intentId === input.intentId && value.taskId === task.taskId);
88
+ current.status = 'completed';
89
+ coordEvent17(fresh.state, actual.status === 'reported' ? 'task-reported' : 'task-failed', task.taskId,
90
+ task.material.lease.leaseId, { status: actual.status, leaseId: task.material.lease.leaseId }, input.intentId);
91
+ await scan17(fresh); return { ...taskView(fresh, coordTask17(fresh.state, input)), actual,
92
+ engineeringVerification: 'original-native-receipt', taskCompletionVerified: false };
93
+ });
94
+ }
95
+ export async function bindLocalPeerSpawn17(rawInput) {
96
+ const input = input17(rawInput, ['spawnRequestId', 'targetTaskId']); await storageBinding(input);
97
+ return withCoordination17(input, 'peer-spawn-bind', async loaded => {
98
+ const source = coordTask17(loaded.state, input), target = loaded.state.tasks.find(value => value.taskId === input.targetTaskId);
99
+ if (target) await coordNative17(target.material);
100
+ return bindSpawn17(loaded.state, source, { spawnRequestId: input.spawnRequestId, targetTaskId: input.targetTaskId });
101
+ });
102
+ }
103
+ export async function waitLocalDependency17(rawInput) {
104
+ const input = input17(rawInput, ['waitId', 'waitFor', 'event', 'expectedWithinMs', 'onArrival', 'onTimeout', 'refetchPaths']);
105
+ await storageBinding(input);
106
+ return withCoordination17(input, 'dependency-wait', async loaded => {
107
+ const task = coordTask17(loaded.state, input); await coordNative17(task.material, false);
108
+ const wait = registerWait17(loaded.state, task, input); scanWaits17(loaded.state);
109
+ return { wait, ...coordinationBlocked17(loaded.state, task) };
110
+ });
111
+ }
112
+ export async function cancelLocalWait17(rawInput) {
113
+ const input = input17(rawInput, ['waitId']); await storageBinding(input);
114
+ return withCoordination17(input, 'wait-cancel', loaded => {
115
+ const task = coordTask17(loaded.state, input), wait = cancelWait17(loaded.state, task, input.waitId);
116
+ return { wait, ...coordinationBlocked17(loaded.state, task) };
117
+ });
118
+ }
119
+ export async function emitLocalCoordSignal17(rawInput) {
120
+ const input = input17(rawInput, ['eventId', 'resource', 'content']); await storageBinding(input);
121
+ if (typeof input.content !== 'string' || !input.content || input.content.length > 4096) coordFail17('ENTROROVE_COORD_EVENT', 'signal content must be explicit bounded text');
122
+ return withCoordination17(input, 'event-signal', async loaded => {
123
+ const task = coordTask17(loaded.state, input); await coordNative17(task.material, false);
124
+ const event = coordEvent17(loaded.state, 'signal', task.taskId, input.resource, { content: input.content }, input.eventId);
125
+ scanWaits17(loaded.state); return { event, executionEvidence: false };
126
+ });
127
+ }
128
+ export async function queueLocalResource17(rawInput) {
129
+ const input = input17(rawInput, ['queueId', 'kind', 'resource', 'paths', 'baselineHash', 'queueTimeoutMs']); await storageBinding(input);
130
+ return withCoordination17(input, 'resource-queue', async loaded => {
131
+ const task = coordTask17(loaded.state, input); await coordNative17(task.material);
132
+ const queue = enqueueResource17(loaded.state, task, input);
133
+ return { queue, allowed: false, writeAuthorized: false, consumerAvailable: false };
134
+ });
135
+ }
136
+ export async function tickLocalCoordination17(rawInput) {
137
+ const input = input17(rawInput, [], ['now']); await storageBinding(input);
138
+ if (input.now !== undefined && !Number.isFinite(Date.parse(input.now))) coordFail17('ENTROROVE_COORD_TIME', 'observed time must be an explicit timestamp');
139
+ return withCoordination17(input, 'tick', async loaded => {
140
+ const task = coordTask17(loaded.state, input); await coordNative17(task.material, false);
141
+ const decisions = await scan17(loaded);
142
+ return { ...taskView(loaded, task), decisions, observedAt: input.now ?? null, scannedAt: new Date().toISOString() };
143
+ });
144
+ }
145
+ export async function waitLocalEvent17(rawInput) {
146
+ const input = input17(rawInput, ['waitId', 'pollTimeoutMs']); coordDuration17(input.pollTimeoutMs, 60_000);
147
+ const deadline = Date.now() + input.pollTimeoutMs;
148
+ while (true) {
149
+ const view = await tickLocalCoordination17(Object.fromEntries(IDENTITY.map(key => [key, input[key]])));
150
+ const wait = view.waits.find(value => value.waitId === input.waitId);
151
+ if (!wait) coordFail17('ENTROROVE_COORD_WAIT_BINDING', 'event host must retain its exact original wait');
152
+ if (wait.status !== 'waiting' || Date.now() >= deadline) return { wait, hostActive: true, ...view };
153
+ await new Promise(resolve => setTimeout(resolve, Math.min(25, Math.max(1, deadline - Date.now()))));
154
+ }
155
+ }
156
+ export async function assertLocalCoordinationAllowed17(rawInput) {
157
+ const input = input17(rawInput, ['paths']); await storageBinding(input);
158
+ const paths = input.paths.length ? canonicalSwarmPaths(input.paths) : [], loaded = await readCoordination17(input);
159
+ const task = coordTask17(loaded.state, input); await readSwarmCloudAuthority(task.material, 'verify');
160
+ const blocked = coordinationBlocked17(loaded.state, task, paths);
161
+ if (paths.some(target => !task.card.taskScope.includes(target))) coordFail17('ENTROROVE_COORD_SCOPE', 'work exceeds its original taskcard');
162
+ if (blocked.blocked) coordFail17('ENTROROVE_COORD_WORK_BLOCKED', 'unresolved coordination blocks this original controlled chain');
163
+ return { ...blocked, restrictiveCheckPassed: true, writeAuthorized: false };
164
+ }
@@ -0,0 +1,111 @@
1
+ import { canonical, sha256 } from './local-integrity.mjs';
2
+ import { canonicalSwarmPaths, SWARM_PRIORITIES, swarmPathsOverlap } from './local-swarm-state.mjs';
3
+ import { readSwarmOwnership, swarmRepositoryStorage, validateSwarmOwnership } from './local-swarm-storage.mjs';
4
+ import { readSwarmWriteIntentHistory } from './local-swarm-priority.mjs';
5
+ import { coordDuration17, coordExact17, coordFail17, coordId17, coordMessage17, coordEvent17 } from './local-autocoord17-store.mjs';
6
+ import { coordNative17 } from './local-autocoord17-authority.mjs';
7
+
8
+ export async function physicalPeers17(task) {
9
+ const authority = await coordNative17(task.material, false), storage = await swarmRepositoryStorage(authority, 'read');
10
+ const index = await validateSwarmOwnership(await readSwarmOwnership(storage, authority, 'read'), authority);
11
+ return (await readSwarmWriteIntentHistory(index, authority)).index;
12
+ }
13
+ function earlier(left, right) {
14
+ const difference = SWARM_PRIORITIES.indexOf(left.priority) - SWARM_PRIORITIES.indexOf(right.priority);
15
+ return difference > 0 || difference === 0 && left.sequence < right.sequence;
16
+ }
17
+ export function registerIntent17(state, task, input) {
18
+ coordId17(input.intentId); coordDuration17(input.ttlMs); coordDuration17(input.coordinationTimeoutMs);
19
+ coordDuration17(input.fileLockTtlMs);
20
+ const paths = canonicalSwarmPaths(input.paths);
21
+ if (paths.some(target => !task.card.taskScope.includes(target)) || input.baselineHash !== task.card.baselineHash
22
+ || input.priority !== task.material.delegationGrant.priority) coordFail17('ENTROROVE_COORD_INTENT_BINDING', 'intent must retain original signed priority, paths and baseline');
23
+ const request = { intentId: input.intentId, taskId: task.taskId, agentId: task.agentId, chainId: task.chainId,
24
+ paths, baselineHash: input.baselineHash, ttlMs: input.ttlMs, coordinationTimeoutMs: input.coordinationTimeoutMs,
25
+ fileLockTtlMs: input.fileLockTtlMs, priority: input.priority };
26
+ const prior = state.intents.find(value => value.intentId === input.intentId);
27
+ if (prior) {
28
+ if (canonical(prior.request) !== canonical(request)) coordFail17('ENTROROVE_COORD_INTENT_CHANGED', 'intent ID cannot change its original content');
29
+ if (prior.status === 'expired') coordFail17('ENTROROVE_COORD_INTENT_EXPIRED', 'expired intent needs a new explicit identity');
30
+ return prior;
31
+ }
32
+ const now = Date.now(), expires = Math.min(now + input.ttlMs, Date.parse(task.material.delegationGrant.expiresAt));
33
+ const intent = { intentId: input.intentId, taskId: task.taskId, request, sequence: state.intents.length,
34
+ status: 'waiting', expiresAt: new Date(expires).toISOString(), deadlineAt: new Date(now + input.coordinationTimeoutMs).toISOString(),
35
+ readyPaths: [], blockedPaths: [], refetchPaths: [], spawnDepth: task.spawnOrigin ? 1 : 0, spawnRequestId: null };
36
+ state.intents.push(intent); return intent;
37
+ }
38
+ function spawnRequest(state, task, intent) {
39
+ if (intent.spawnDepth !== 0 || intent.spawnRequestId || !intent.blockedPaths.length) return;
40
+ const spawnRequestId = sha256(`entrorove.peer-spawn/1.0\0${intent.intentId}`);
41
+ intent.spawnRequestId = spawnRequestId;
42
+ const request = { schemaVersion: 'entrorove.peer-spawn-request/1.0', spawnRequestId, idempotencyKey: spawnRequestId,
43
+ status: 'pending', sourceTaskId: task.taskId, ownerId: task.material.delegationGrant.ownerId,
44
+ originalTask: structuredClone(task.originalTask), agentId: task.agentId, chainId: task.chainId,
45
+ paths: [...intent.blockedPaths], sourceIntentId: intent.intentId, spawnDepth: 1, spawnAllowed: false };
46
+ state.spawnRequests.push(request);
47
+ coordMessage17(state, task.taskId, 'peer-timeout', { spawnRequestId, paths: request.paths }, intent.intentId);
48
+ }
49
+ export function scanIntent17(state, task, intent, physical) {
50
+ const now = Date.now();
51
+ if (['completed', 'moved', 'expired'].includes(intent.status)) return peerView17(intent);
52
+ if (Date.parse(intent.expiresAt) <= now) {
53
+ intent.status = 'expired'; coordMessage17(state, task.taskId, 'peer-expired', { intentId: intent.intentId }, intent.intentId);
54
+ return peerView17(intent);
55
+ }
56
+ const ownGrant = sha256(canonical(task.material.delegationGrant));
57
+ const occupied = physical.records.filter(record => record.grantDigest !== ownGrant);
58
+ const nativeIntent = physical.intents.find(value => value.intentId === ownGrant);
59
+ const ahead = physical.intents.filter(value => value.status === 'waiting' && value.intentId !== ownGrant
60
+ && nativeIntent && earlier(value, nativeIntent));
61
+ const remaining = intent.request.paths.filter(target => !intent.movedPaths?.includes(target));
62
+ const blocked = remaining.filter(target => occupied.some(record => record.paths.some(scope => swarmPathsOverlap(scope, target)))
63
+ || ahead.some(value => value.paths.some(scope => swarmPathsOverlap(scope, target))));
64
+ const oldBlocked = intent.blockedPaths, ready = remaining.filter(target => !blocked.includes(target));
65
+ const refetch = oldBlocked.filter(target => !blocked.includes(target));
66
+ intent.blockedPaths = blocked; intent.readyPaths = ready;
67
+ intent.refetchPaths = [...new Set([...intent.refetchPaths, ...refetch])].sort();
68
+ intent.status = blocked.length ? ready.length ? 'partial' : 'waiting' : 'ready';
69
+ if (blocked.length) {
70
+ const urgent = intent.request.priority === 'urgent' && occupied.some(record => record.paths.some(scope => blocked.some(target => swarmPathsOverlap(scope, target))));
71
+ coordMessage17(state, task.taskId, 'peer-conflict', { intentId: intent.intentId, blockedPaths: blocked }, `${intent.intentId}:${canonical(blocked)}`);
72
+ for (const record of occupied.filter(record => record.paths.some(scope => blocked.some(target => swarmPathsOverlap(scope, target))))) {
73
+ const owner = state.tasks.find(value => value.material.flowId === record.flowId);
74
+ if (owner) coordMessage17(state, owner.taskId, 'peer-request', { intentId: intent.intentId, paths: blocked,
75
+ safeCheckpointRequested: urgent }, `${intent.intentId}:${record.grantDigest}`);
76
+ }
77
+ if (Date.parse(intent.deadlineAt) <= now) spawnRequest(state, task, intent);
78
+ }
79
+ if (refetch.length) {
80
+ coordMessage17(state, task.taskId, 'peer-ready', { intentId: intent.intentId, refetchPaths: refetch,
81
+ freshAimlockSnapshotRequired: true }, `${intent.intentId}:${canonical(refetch)}`);
82
+ coordEvent17(state, 'peer-ready', task.taskId, intent.intentId, { refetchPaths: refetch }, `${intent.intentId}:${canonical(refetch)}`);
83
+ }
84
+ return { ...peerView17(intent), safeCheckpointRequested: intent.request.priority === 'urgent' && blocked.length > 0 };
85
+ }
86
+ export function peerView17(intent) {
87
+ return { schemaVersion: 'entrorove.local-autocoord/1.0', intentId: intent.intentId, status: intent.status,
88
+ readyPaths: [...intent.readyPaths], blockedPaths: [...intent.blockedPaths], refetchPaths: [...intent.refetchPaths],
89
+ freshAimlockSnapshotRequired: intent.refetchPaths.length > 0, writeAuthorized: false, fileLockGranted: false,
90
+ lockRequest: { paths: [...intent.readyPaths], ttlMs: intent.request.fileLockTtlMs },
91
+ spawnRequestId: intent.spawnRequestId, spawnDepth: intent.spawnDepth, spawnAllowed: intent.spawnDepth === 0 };
92
+ }
93
+ export function bindSpawn17(state, source, input) {
94
+ coordExact17(input, ['spawnRequestId', 'targetTaskId']);
95
+ const request = state.spawnRequests.find(value => value.spawnRequestId === input.spawnRequestId && value.sourceTaskId === source.taskId);
96
+ const target = state.tasks.find(value => value.taskId === input.targetTaskId);
97
+ if (!request || !target || target.taskId === source.taskId || target.material.flowId === source.material.flowId
98
+ || target.material.delegationGrant.ownerId !== request.ownerId || canonical(target.originalTask) !== canonical(request.originalTask)
99
+ || request.paths.some(targetPath => !target.card.taskScope.includes(targetPath))) coordFail17('ENTROROVE_COORD_SPAWN_BINDING', 'spawn binding requires the actual newly registered same-owner original task and exact pending paths');
100
+ if (request.status === 'bound') {
101
+ if (request.targetTaskId !== target.taskId) coordFail17('ENTROROVE_COORD_SPAWN_CHANGED', 'original spawn request cannot be rebound');
102
+ return request;
103
+ }
104
+ if (target.spawnOrigin) coordFail17('ENTROROVE_COORD_RECURSION', 'a derived task cannot receive another spawn lineage');
105
+ request.status = 'bound'; request.targetTaskId = target.taskId; target.spawnOrigin = request.spawnRequestId;
106
+ const intent = state.intents.find(value => value.intentId === request.sourceIntentId);
107
+ intent.movedPaths = [...request.paths]; intent.status = intent.readyPaths.length ? 'ready' : 'moved'; intent.blockedPaths = [];
108
+ coordMessage17(state, source.taskId, 'peer-spawned', { spawnRequestId: request.spawnRequestId,
109
+ targetTaskId: target.taskId, paths: request.paths }, request.spawnRequestId);
110
+ return request;
111
+ }
@@ -0,0 +1,110 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { withChainLock } from './local-chain-lock.mjs';
5
+ import { chainControlFile } from './local-chain-bindings.mjs';
6
+ import { canonical, sha256 } from './local-integrity.mjs';
7
+
8
+ export const AUTOCORD17_SCHEMA = 'entrorove.local-autocoord/1.0';
9
+ export function coordFail17(code, message) { throw Object.assign(new Error(message), { code }); }
10
+ export function coordExact17(value, required, optional = []) {
11
+ if (!value || typeof value !== 'object' || Array.isArray(value) || required.some(key => !Object.hasOwn(value, key))
12
+ || Object.keys(value).some(key => !required.includes(key) && !optional.includes(key))) {
13
+ coordFail17('ENTROROVE_COORD_FIELDS', 'coordination input has missing or unknown fields');
14
+ }
15
+ }
16
+ export function coordId17(value) {
17
+ if (typeof value !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) {
18
+ coordFail17('ENTROROVE_COORD_ID', 'coordination identity must be an explicit safe ID');
19
+ }
20
+ return value;
21
+ }
22
+ export function coordDuration17(value, maximum = 3_600_000) {
23
+ if (!Number.isSafeInteger(value) || value < 1 || value > maximum) coordFail17('ENTROROVE_COORD_DURATION', 'duration must be a positive bounded integer');
24
+ return value;
25
+ }
26
+ async function directory(value, privateMode) {
27
+ if (typeof value !== 'string' || !path.isAbsolute(value)) coordFail17('ENTROROVE_COORD_DIRECTORY', 'coordination directories must be explicit absolute paths');
28
+ const stat = await fs.lstat(value), actual = await fs.realpath(value);
29
+ if (!stat.isDirectory() || stat.isSymbolicLink() || actual !== path.resolve(value)
30
+ || typeof process.getuid === 'function' && stat.uid !== process.getuid()
31
+ || privateMode && (stat.mode & 0o077)) coordFail17('ENTROROVE_COORD_DIRECTORY', 'coordination directory must be owned, real and private');
32
+ return actual;
33
+ }
34
+ async function location(input, create) {
35
+ const repositoryRoot = await directory(input.repositoryRoot, false), coordinationHome = await directory(input.coordinationHome, true);
36
+ let current = coordinationHome;
37
+ for (const name of ['.entrorove', 'autocoord17', sha256(repositoryRoot).slice(0, 32)]) {
38
+ current = path.join(current, name);
39
+ if (create) await fs.mkdir(current, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST') throw error; });
40
+ await directory(current, true);
41
+ }
42
+ return { repositoryRoot, coordinationHome, file: path.join(current, 'journal.json') };
43
+ }
44
+ function initial(ctx) {
45
+ return { schemaVersion: AUTOCORD17_SCHEMA, repositoryRoot: ctx.repositoryRoot, coordinationHome: ctx.coordinationHome,
46
+ tasks: [], intents: [], waits: [], events: [], messages: [], decisions: [], spawnRequests: [], lockQueues: [] };
47
+ }
48
+ function checked(state, ctx) {
49
+ coordExact17(state, ['schemaVersion', 'repositoryRoot', 'coordinationHome', 'tasks', 'intents', 'waits', 'events',
50
+ 'messages', 'decisions', 'spawnRequests', 'lockQueues']);
51
+ if (state.schemaVersion !== AUTOCORD17_SCHEMA || state.repositoryRoot !== ctx.repositoryRoot || state.coordinationHome !== ctx.coordinationHome
52
+ || ['tasks', 'intents', 'waits', 'events', 'messages', 'decisions', 'spawnRequests', 'lockQueues'].some(key => !Array.isArray(state[key]) || state[key].length > 1024)) {
53
+ coordFail17('ENTROROVE_COORD_JOURNAL', 'coordination controls lost their original identity or bounds');
54
+ }
55
+ return state;
56
+ }
57
+ async function load(ctx, create) {
58
+ let journal;
59
+ try { const value = await chainControlFile(ctx.file); if (value.stat.mode & 0o077) throw new Error(); journal = JSON.parse(value.bytes.toString()); }
60
+ catch (error) { if (error.code !== 'ENOENT' || !create) throw error; journal = { schemaVersion: AUTOCORD17_SCHEMA, records: [] }; }
61
+ coordExact17(journal, ['schemaVersion', 'records']);
62
+ if (journal.schemaVersion !== AUTOCORD17_SCHEMA || !Array.isArray(journal.records) || journal.records.length > 1024) {
63
+ coordFail17('ENTROROVE_COORD_JOURNAL', 'coordination journal is invalid');
64
+ }
65
+ let head = null, state = initial(ctx);
66
+ for (const [index, record] of journal.records.entries()) {
67
+ coordExact17(record, ['sequence', 'event', 'at', 'previousDigest', 'state', 'digest']);
68
+ const { digest, ...core } = record;
69
+ if (record.sequence !== index + 1 || record.previousDigest !== head || sha256(canonical(core)) !== digest) {
70
+ coordFail17('ENTROROVE_COORD_JOURNAL', 'coordination journal lineage changed');
71
+ }
72
+ head = digest; state = checked(record.state, ctx);
73
+ }
74
+ return { ctx, journal, state: structuredClone(state), sequence: journal.records.length, head };
75
+ }
76
+ async function commit(loaded, event) {
77
+ checked(loaded.state, loaded.ctx);
78
+ const core = { sequence: loaded.sequence + 1, event, at: new Date().toISOString(), previousDigest: loaded.head, state: loaded.state };
79
+ const journal = { schemaVersion: AUTOCORD17_SCHEMA, records: [...loaded.journal.records, { ...core, digest: sha256(canonical(core)) }] };
80
+ const bytes = Buffer.from(canonical(journal) + '\n');
81
+ if (bytes.length > 8 * 1024 * 1024 || journal.records.length > 1024) coordFail17('ENTROROVE_COORD_JOURNAL_LIMIT', 'coordination journal needs explicit archival');
82
+ const temporary = `${loaded.ctx.file}.${randomUUID()}.tmp`;
83
+ const handle = await fs.open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
84
+ try { await handle.writeFile(bytes); await handle.sync(); } finally { await handle.close(); }
85
+ try { await fs.rename(temporary, loaded.ctx.file); } finally { await fs.rm(temporary, { force: true }); }
86
+ const directory = await fs.open(path.dirname(loaded.ctx.file), constants.O_RDONLY);
87
+ try { await directory.sync(); } finally { await directory.close(); }
88
+ loaded.sequence = core.sequence; loaded.head = sha256(canonical(core));
89
+ }
90
+ export async function withCoordination17(input, event, action, create = false) {
91
+ const ctx = await location(input, create);
92
+ return withChainLock(ctx.file, async () => {
93
+ const loaded = await load(ctx, create), before = canonical(loaded.state), result = await action(loaded);
94
+ if (canonical(loaded.state) !== before) await commit(loaded, event);
95
+ return { ...result, sequence: loaded.sequence, head: loaded.head };
96
+ });
97
+ }
98
+ export async function readCoordination17(input) { return load(await location(input, false), false); }
99
+ export function coordMessage17(state, taskId, type, detail, key) {
100
+ const messageId = sha256(`${type}\0${taskId}\0${key}`);
101
+ if (!state.messages.some(value => value.messageId === messageId)) state.messages.push({ messageId, taskId, type, detail, at: new Date().toISOString() });
102
+ return messageId;
103
+ }
104
+ export function coordEvent17(state, type, taskId, resource, detail, key) {
105
+ const eventId = sha256(`${type}\0${taskId}\0${key}`);
106
+ const existing = state.events.find(value => value.eventId === eventId);
107
+ if (existing && canonical(existing.detail) !== canonical(detail)) coordFail17('ENTROROVE_COORD_EVENT_CHANGED', 'same event identity cannot change its content');
108
+ if (!existing) state.events.push({ eventId, type, taskId, resource, detail, at: new Date().toISOString() });
109
+ return existing ?? state.events.at(-1);
110
+ }