@entrorove/skill 0.1.52 → 0.1.54

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 (50) hide show
  1. package/CAPABILITY-MAP.md +14 -2
  2. package/CHANGELOG.md +12 -0
  3. package/DELIVERY-BOUNDARIES.md +4 -2
  4. package/IDE-EXECUTION.md +25 -3
  5. package/SKILL.md +8 -4
  6. package/cli.mjs +4 -6
  7. package/cloud-mutation-authority.mjs +96 -0
  8. package/connected-cli.mjs +20 -0
  9. package/ide-code-mode.mjs +30 -8
  10. package/ide-command.mjs +27 -1
  11. package/ide-hook.mjs +70 -17
  12. package/ide-results.mjs +8 -6
  13. package/ide-session.mjs +44 -1
  14. package/local-approved-plan.mjs +3 -21
  15. package/local-approved-process.mjs +54 -0
  16. package/local-approved-supervisor.mjs +99 -0
  17. package/local-cli.mjs +13 -2
  18. package/local-contracts.mjs +2 -0
  19. package/local-delegated-modify-analysis.mjs +232 -0
  20. package/local-discipline-actions.mjs +2 -0
  21. package/local-discipline-gate.mjs +3 -1
  22. package/local-error.mjs +3 -0
  23. package/local-execution.mjs +1 -1
  24. package/local-mutation-authority.mjs +23 -5
  25. package/local-mutation-batch.mjs +1 -0
  26. package/local-mutation-binding.mjs +14 -5
  27. package/local-mutation-consumption.mjs +93 -0
  28. package/local-mutation-pass-store.mjs +6 -4
  29. package/local-mutation-wire.mjs +21 -4
  30. package/local-preserved-mutation-protection.mjs +51 -0
  31. package/local-preserved-mutation-signer.mjs +81 -0
  32. package/local-preserved-mutation-state.mjs +96 -0
  33. package/local-preserved-mutation-wire.mjs +49 -0
  34. package/local-scope-create-require.mjs +142 -0
  35. package/local-scope-import-expressions.mjs +2 -2
  36. package/local-scope-repository.mjs +42 -15
  37. package/local-scope-require-resolution.mjs +78 -0
  38. package/manifest.json +1 -1
  39. package/mutation-client.mjs +290 -0
  40. package/package.json +21 -2
  41. package/release-state.json +48 -29
  42. package/release.mjs +29 -0
  43. package/releases.json +12 -0
  44. package/remote-client.mjs +2 -1
  45. package/workflow-observation-client.mjs +44 -0
  46. package/workflow-observation-connection.mjs +35 -0
  47. package/workflow-observation-remote.mjs +84 -0
  48. package/workflow-observation-runner.mjs +87 -0
  49. package/workflow-observation-state.mjs +36 -0
  50. package/workflow-observation-wire.mjs +69 -0
package/CAPABILITY-MAP.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # 能力对账图(WP-02 / AC-20)
2
2
 
3
+ ## 2026-10-17 增量:受限本地编辑与停止恢复
4
+
5
+ | 能力 | 正常公开入口 | 验证与边界 |
6
+ | --- | --- | --- |
7
+ | 成员受限编辑 | `mutation-connect <repositoryRoot>`、`local mutation-delegate-prepare` | 原任务、冻结修改声明及租约绑定;观察、编辑、独立验证三把密钥分离。运行时依赖保持未知且只读,只允许现存文件的受限修改;不授予通用执行、合并或交付权限 |
8
+ | 连续编辑结算 | `mutation-settle <repositoryRoot>`、`local mutation-consumption-attest`、服务端 `mutation-pass-settle` | 实际写后 M 回执与 fresh S 后验共同确认原 pass;下一轮使用 `previousPassId`,不伪造架构验证事件 |
9
+ | 沙箱关闭后的本地测试 | `workflow-observe-step <repositoryRoot>`、`member-observation-admit/report/status` | 仅运行原冻结命令,对照已结算源文件和保护依赖;结果独立保存为 local / pending-independent,不升格为 V 验证或交付 |
10
+ | 续跑入口 | 原 `execution-claim`、原 flow 恢复 | 成员受限范围返回原字段的 `localAction`,客户端串联真实快照、签名、云端通行证及本地安装;网络结果不明时恢复原请求 |
11
+ | 同一阻断停止 | IDE `Stop` 回调 | 真实重复阻断可等待;状态查询及检查点不能重置阻断,宿主重复 Stop 不再生成无限续跑。原 flow、需求队列与恢复数据保留 |
12
+
13
+ 上述编辑授权与独立验证分别核验。实际写入成功不等于业务测试通过;缺少独立验证运行器时不能认证交付。以下历史能力数量保留原日期口径。
14
+
3
15
  ## 2026-10-14 接入:受控执行与复用一致性
4
16
 
5
17
  | 能力 | 正常公开入口 | 验证与边界 |
@@ -23,7 +35,7 @@
23
35
  | 网页与手机通知 | 登录后的任务通知按钮 | 同一加密 Web Push 协议,账号与设备隔离;真实任务事务队列、持久重试、失效注销与到期清理;手机可见投递仍需设备本人授权与验收 |
24
36
  | 失败后接续 | `scope-connect` 返回 `nextInput`;`feedback <requestId> [generation]` | 范围门禁使用同一排序路径集;新执行保存新一代回执,原失败回执不可改,默认只读最新一代 |
25
37
 
26
- 本地公开 schema 已增加到 **107 项**(原 100 项加 `peer-status`,以及当前增量的 `discipline-check`、`discipline-review`、`flow-cancel`、`flow-abandon`、`guarded-delete`、`guarded-move`)。Aimlock 运行时由 7.0.44 更新为 **7.0.45**,公开操作由 20 项增加到 24 项;其余七域的运行时版本保持原值。下方历史表保留原对账日期与当时验收边界,不表示当前仍缺上述入口。发布件与生产部署需独立核验,不能从仓库测试推断。
38
+ 本地公开 schema 已增加到 **109 项**(原 100 项加 `peer-status`,以及当前增量的 `discipline-check`、`discipline-review`、`flow-cancel`、`flow-abandon`、`guarded-delete`、`guarded-move`、`mutation-delegate-prepare`、`mutation-consumption-attest`)。Aimlock 运行时由 7.0.44 更新为 **7.0.45**,公开操作由 20 项增加到 24 项;其余七域的运行时版本保持原值。下方历史表保留原对账日期与当时验收边界,不表示当前仍缺上述入口。发布件与生产部署需独立核验,不能从仓库测试推断。
27
39
 
28
40
  ## 2026-10-12 基线:八域 IDE 调用链
29
41
 
@@ -81,7 +93,7 @@ Entrorove 只治理通过技能入口执行的动作:路径、通行证、快
81
93
 
82
94
  `scope-connect <absolute repositoryRoot>` 是 CLI 编排入口,不改变这 100 项本地操作。它串联远端 `scope-runner-prepare/register/challenge/attest` 与已有真实快照、签名检查,为原账号与任务提交范围证据;不授予执行证据签名或写入权限。全部远端操作以实时 discovery 为准。
83
95
 
84
- 当前增量操作(不改写上述历史分母)为:`peer-status`、`discipline-check`、`discipline-review`、`flow-cancel`、`flow-abandon`、`guarded-delete`、`guarded-move`。它们计入当前 107 项公开 schema。
96
+ 当前增量操作(不改写上述历史分母)为:`peer-status`、`discipline-check`、`discipline-review`、`flow-cancel`、`flow-abandon`、`guarded-delete`、`guarded-move`、`mutation-delegate-prepare`、`mutation-consumption-attest`。它们计入当前 109 项公开 schema。
85
97
 
86
98
  | 分区 | 精确操作名 | 证据位置 |
87
99
  | --- | --- | --- |
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.54 — 2026-10-17
4
+
5
+ - 修复受管 Node 只读 SHA-256 命令在 Codex 工具门禁中的静态识别与安全改写;按已证明的 createRequire 原目录解析实际安装依赖并记录只读元数据,保留未知上下文和越界拒绝。
6
+ - Fixed static recognition and safe rewriting of managed Node SHA-256 reads in Codex tool guards; resolved installed dependencies from proven createRequire anchors with read-only metadata while preserving unknown-context and path-boundary refusals.
7
+
8
+ ## 0.1.53 — 2026-10-17
9
+
10
+ - 补齐原任务受限编辑、真实写后结算与连续修改,保留未知依赖和独立验证边界;沙箱关闭后可运行原批准命令并保存本地观察。修复前置条件、断线恢复和停止循环,独立监督器约束崩溃后的子进程。本地成功不代表独立验证或交付。
11
+ - Added original-task restricted editing, real write settlement and subsequent modifications while preserving unknown dependencies and independent validation. With cloud sandbox disabled, retain actual observations of the approved command. Fixed prerequisite recovery, disconnections and Stop loops; an independent supervisor bounds child processes after CLI failure. Local success does not certify independent verification or delivery.
12
+ - 补齐实际写入后的 M 消费结算与 fresh S 后验,按原租约和 `previousPassId` 连续编辑;关闭云端沙箱后可运行原批准命令并独立保存本地测试观察。修复前置条件恢复、客户端漏回结算编号和退出说明冲突;独立监督器在 CLI 崩溃后仍约束实际子进程。结果明确为主机权限执行、外部副作用未核实,不能替代独立验证。
13
+ - Added real M write-consumption settlement, fresh S confirmation and subsequent edits bound to the original lease and previous pass. With cloud sandbox disabled, run the approved command and retain its local observation separately. Fixed prerequisite recovery, missing settlement IDs and conflicting Stop guidance. An independent supervisor enforces child deadlines after CLI failure. Results explicitly retain host permissions, unverified external effects and pending independent validation.
14
+
3
15
  ## 0.1.52 — 2026-10-17
4
16
 
5
17
  - 修复 Vue/TypeScript 的 Function 类型和真实 Vue 回调 prop 被误判为动态依赖;解析不可变 const 的有限语言模块导入,记录实际依赖与哈希。补齐 Vue 候选依赖检查;未知运行时导入报告文件、行列和原因。测试采用隔离用户目录,越界、动态执行、未知删除与原任务写入校验继续保留。
@@ -1,10 +1,12 @@
1
1
  # Delivery boundaries and cost visibility
2
2
 
3
- Contract for where a governed task may stop and which numbers the skill may display. Each section is marked **live** (implemented in this package) or **planned** (documented, not yet wired).
3
+ Contract for claiming stage delivery and displaying measured consumption. Each section is marked **live** (implemented in this package) or **planned** (documented, not yet wired).
4
4
 
5
5
  ## Stop only at a boundary (live)
6
6
 
7
- A stopped task may only stop at a clean boundary: the work so far is runnable, demonstrable and roll-backable. Never stop in the middle of a stage. In this package the ordered verified stages of the accepted plan are the boundaries; `local-stage-boundary.mjs` decides and reports with them.
7
+ A delivered stage requires a clean boundary: the work so far is runnable, demonstrable and roll-backable. Do not claim delivery in the middle of a stage. The ordered verified stages of the accepted plan are the boundaries; `local-stage-boundary.mjs` decides and reports with them.
8
+
9
+ Blocked or unknown work may wait without a delivered stage. Preserve the original flow, queued requirements, request identity and recovery evidence. After the same real block occurs twice, diagnose and use the supported recovery; do not force a third execution or repeated automatic Stop continuation. Waiting does not grant a replacement executor or verified completion.
8
10
 
9
11
  ## `delivered-stage-N` is its own terminal (live)
10
12
 
package/IDE-EXECUTION.md CHANGED
@@ -30,11 +30,11 @@ Set `ENTROROVE_IDE` to the actual host name (for example `Codex`, `Cursor` or `C
30
30
 
31
31
  ### Interrupting and resuming requirements
32
32
 
33
- User text received during an unfinished governed task is persisted as an exact, private queue entry. `queue status` reads it; `queue route` assigns the existing `background`, `normal`, `high` or `urgent` priority. `interrupt` only takes effect at a controlled-call checkpoint and keeps the parent flow, task ID, task number, request ID, repository and account binding. Once the inserted requirement has a verified completion and stored feedback acknowledgement, the parent is restored and its returned `nextStep` is the only continuation. The guard blocks Stop while an entry is unclassified, a call is unresolved or a runnable parent/child remains. The queue does not promise execution when the host is powered off or the IDE process is unavailable.
33
+ User text received during an unfinished governed task is persisted as an exact, private queue entry. `queue status` reads it; `queue route` assigns the existing `background`, `normal`, `high` or `urgent` priority. `interrupt` only takes effect at a controlled-call checkpoint and keeps the parent flow, task ID, task number, request ID, repository and account binding. Once the inserted requirement has a verified completion and stored feedback acknowledgement, the parent is restored and its returned `nextStep` is the only continuation. Stop may request one bounded continuation to classify a queued requirement or recover an unresolved request read only. A persisted repeated blocker may wait without terminating or advancing the original queue; automatic continuation is limited to one attempt per user turn and respects the host's `stop_hook_active` signal. Status queries and checkpoints do not erase the blocker. The queue does not promise execution when the host is powered off or the IDE process is unavailable.
34
34
 
35
35
  ### Delivery boundaries and stop-loss
36
36
 
37
- A stopped task may only stop at a clean boundary: one where the work so far is runnable, demonstrable and roll-backable. Never stop in the middle of a stage. Such a stop is its own terminal result `delivered-stage-N` (N = delivered stages); it is never `failed` or `blocked`, must not be shown as an error, and the IDE keeps phase `delivered` with Stop allowed. A new `start` for the same goal stays refused: continue the original bound flow instead.
37
+ A delivered stage requires a clean boundary where the work so far is runnable, demonstrable and roll-backable. Its terminal result is `delivered-stage-N` (N = delivered stages); it is never `failed` or `blocked`, must not be shown as an error, and the IDE keeps phase `delivered` with Stop allowed. Blocked or unknown work can instead wait without a delivery claim and retain its original recovery data. A new `start` for the same goal stays refused: continue the original bound flow or use its explicit lifecycle termination operation.
38
38
 
39
39
  The close decision uses measured consumption only. Without an enforced budget there is no baseline, so the result is explicitly not-estimable and no remaining amount or duration may be displayed. When measured consumption reaches the configured share of the enforced budget, finish the current stage and close at that boundary. Resuming continues at the next boundary; delivered stages are never re-executed.
40
40
 
@@ -70,7 +70,29 @@ Scope inspection resolves aliases from actual `tsconfig.json` or `jsconfig.json`
70
70
 
71
71
  Keep snapshot before-images distinct from a frozen execution plan's expected final file hashes. Controlled writes consume the original approved authority; the runner then verifies the actual planned final bytes. Never edit a signed plan or silently substitute a newer snapshot after an unknown or failed gate. Existing changes outside that authority require explicit revalidation of the same original flow.
72
72
 
73
- Computed runtime imports remain explicitly unresolved with their actual source paths and hashes. A member's scope-only signer may submit these observations to enter restricted planning; the cloud returns `planningOnly: true` and `requires-complete-import-graph-before-mutation`. Do not treat this as full dependency verification, execution permission, or business completion. Resolve the dynamic dependency contract and obtain complete verified facts before mutation; never set `complete` manually or replace the original signer or paths to bypass the restriction.
73
+ Computed runtime imports remain explicitly unresolved with their actual source paths and hashes. A member's scope-only signer may submit these observations to enter restricted planning; the cloud returns `planningOnly: true` and `requires-complete-import-graph-before-mutation`. Do not treat this as full dependency verification, execution permission, or business completion. Obtain the limited original-task editing delegation below when eligible, or resolve and verify the dependency contract. Never set `complete` manually or replace the original signer or paths.
74
+
75
+ ### Restricted member editing
76
+
77
+ When the accepted plan returns a `localAction` for `mutation-connect`, run `npx @entrorove/skill mutation-connect <original repositoryRoot>` with that action's JSON `input` on stdin. The input contains the original `taskId`, `flowId`, `stepId`, `leaseId`, `generation`, scope `runnerId` and exact `paths`. Use the same local home, account and scope signer. This route requires an accepted frozen modify policy and a live original lease; it does not create a second task or invoke a generic execution authority.
78
+
79
+ The connector discovers the public cloud verification key through the authenticated official HTTPS capability endpoint and fixes it in private local storage. An explicit operator pin must match, and a changed key or origin is rejected rather than silently replaced. Subsequent local mutation calls load this public pin offline; discovery and `unlock` do not depend on that cache.
80
+
81
+ The connector creates a separate local editing key, requests the cloud's original signed snapshot challenge, copies and verifies the actual files, signs their fresh observation, obtains a limited cloud pass and installs it. The local editing key differs from both the scope observation key and the independent validator key. It is stored privately outside the repository; no manual key configuration is required. An uncertain network result returns a structured blocked result with the original request ID. Reusing the identical connector input restores its saved stage through the original status GET and keeps its existing challenge and attestation; it never replaces an uncertain mutation POST.
82
+
83
+ Use the returned `gatePassPath` with the discovered `local guarded-write` contract and preserve its exact flow, step and repository. Only existing declared modify targets are eligible. Runtime loader sources and observed project configuration stay protected by their original hashes and physical file identity. The local preflight rejects changed imports, removed definitions, public contract changes, new unknown dependencies and unsupported source formats. Unknown dependency coverage remains unknown after an eligible edit.
84
+
85
+ After the pass's actual writes finish, rerun the original `scope-connect` with its complete original input, including `importContexts`. Run `npx @entrorove/skill mutation-settle <original repositoryRoot>` with `{taskId,flowId,stepId,gatePassPath}`. The local M signer observes the real completed consumption, and the service requires a fresh S scan captured after those writes. This settlement preserves both before and after hashes, unchanged modes, pass nonces and the original lease. It never creates an architecture verification event. Further edits use the returned `localAction`, whose `previousPassId` identifies that exact settled pass; do not resend an already consumed write.
86
+
87
+ If settlement reports `ENTROROVE_SCOPE_AFTER_WRITE_REQUIRED`, refresh only the original scope and retry the same consumption request and receipt. A lost network response is different: rerunning the identical connector restores its original GET result without a replacement POST or signature.
88
+
89
+ With cloud sandbox disabled, the returned `observationAction` runs `npx @entrorove/skill workflow-observe-step <original repositoryRoot>` with `{taskId,flowId,stepId,leaseId,generation,settlementId}`. The service admits only the original frozen command, executable hash, environment and limits, with source hashes updated solely by accepted settlements. The local runner checks the actual source and protected dependencies before and after execution. No validator private key is needed. A real successful process remains `status: incomplete`, `runner: local`, `receipt: null`, `independentValidation: pending`; its result is stored separately and read with `member-observation-status`. It cannot advance formal verified dependencies or task delivery. Reentry preserves a completed process result; an unknown process outcome never authorizes an automatic second launch.
90
+
91
+ This command runs with the host OS user's permissions, not inside an OS sandbox. Its repository checks do not prove that external files, networks or databases were unchanged: `executionIsolation` remains `host-permissions`, and `externalEffects` remains `unverified`. The independent supervisor enforces the admitted start deadline and command timeout and reclaims its own process group if the CLI disappears. Only the original approved command can be launched; these observations grant no general shell or project-write permission.
92
+
93
+ For a lost `mutation-settle` response, rerun that exact command and input to restore its original HTTP request. For observation network recovery, rerun the identical `workflow-observe-step` input or invoke `member-observation-status` with `{observationId}` through the normal `invoke` entry; both preserve the original process identity. A feedback acknowledgement confirms receipt of a result, not independent verification of that result. Exact host approval means the existing guard callback for the tool call, not another request for human business approval.
94
+
95
+ An editing pass does not certify test execution, independent validation, merge or delivery. Keep those existing verification requirements and distinguish an actual edited file from a verified business result. A missing independent runner is an explicit prerequisite; never substitute the editing key for that runner or describe a local edit as completed delivery.
74
96
 
75
97
  For a decision, include `input.task.interaction` with `type` (`confirm`, `choice`, `multi` or `input`), a `question`, `options` (two or more with unique `id` and `label`, or an empty array for input), and a `callback` containing its `operation` and `payload`. Medium- and high-risk decisions also need a `riskDescription`. The decision waits for an explicit answer; it never chooses a default on timeout.
76
98
 
package/SKILL.md CHANGED
@@ -5,7 +5,7 @@ description: Execute an engineering task through Entrorove, preserving its origi
5
5
 
6
6
  # entrorove · 熵巡
7
7
 
8
- Version: 0.1.52.
8
+ Version: 0.1.54.
9
9
 
10
10
  Entrorove applies to the whole task once selected. Reading this document or calling an entry once does not execute the task. On every later turn, including after compaction, continue the original task and its required next operation before dependent work. Keep the original goal, scope, account, flow, cloud task and failure records. Never describe planning, a reported step or model output as verified delivery.
11
11
 
@@ -16,19 +16,23 @@ Entrorove applies to the whole task once selected. Reading this document or call
16
16
  To remove this skill and its host hooks, run `entrorove uninstall` directly. It works offline and preserves unrelated host settings, credentials and task records. If a managed file was edited, it reports the preserved path instead of deleting it.
17
17
  2. Read [IDE-EXECUTION.md](IDE-EXECUTION.md) for the exact entry, cloud callbacks, account agreement and operation contracts. Use the stored account credential; never put it in arguments, URLs, project files, output or fixtures. Entrorove is independent of other systems.
18
18
  3. Discover `capabilities`, then invoke `start` with the actual goal, task type, risk and scope facts. An enabled account returns a cloud choice before charging a model. Show that choice visibly using the host question tool. Confirmation calls its original callback; exit adopts the local route for this task. Never treat silence as permission, invent acceptance, or create both routes. Existing explicit authorization for this task persists.
19
+ With cloud sandbox disabled, the IDE performs its own planning and continues the original API flow. Do not require a cloud model call or create a sandbox task to perform local work.
19
20
  When the model produces a task contract, preserve its `workType`, `risk`, `difficulty`, `crossModule`, `parallel`, `unclear`, `architectureContract`, optional `baselineRef`, and optional `interaction` as returned. Valid work types are `implementation`, `documentation`, `calculation`, `merge`, `review`, `decision`, and `diagnosis`. A `merge` requires a concrete `baselineRef` identifying the branch, tag, commit, or review baseline; keep that baseline unchanged through independent verification. A `decision` requires `interaction`, including a risk description for medium or high risk. Do not fill missing required fields by guessing.
20
21
  4. After real cloud acceptance, immediately display **云端任务 <taskNumber> · 已进入**. Preserve `taskNumber` verbatim: it is the homepage sandbox tab number. Keep its UUID `taskId` separately. Run the returned `cloud-watch`, show each real public event and its cursor, and continue the original accepted flow from status. A command log alone is insufficient. No accepted cloud task means no cloud ID or model-execution claim.
21
22
  5. For an accepted `flowId`, execute the returned `nextStep` with its required payload. Discover the schema before constructing input. An empty payload is valid only when that operation's schema allows it. Reconnect and refresh only the original flow. A changed snapshot is a binding failure to investigate; do not invent a fresh successful baseline, rebuild a flow to evade it, or erase the old failure. If the terminal flow truly owns a stale task slot, use the explicit `flow-abandon` lifecycle operation described below before routing the original task again.
22
23
  6. All governed mutations use the original authority: `local guarded-write`, `local guarded-delete`, `local guarded-move`, or the original frozen approved runner plan. Native patches, scripts, terminal writes, database mutations and unknown MCP operations must not substitute for the controlled entry. **A failed gate or unknown outcome must never be replaced with another executor or reported as local success.** Query the original request read-only, repair the cause, and obtain the original flow's valid authority before proceeding.
24
+ For a restricted member scope, follow a returned `localAction` before submitting execution evidence. `mutation-connect` obtains only the original task's limited editing pass; it keeps unresolved loader files and project configuration read only. After actual writes, refresh the original scope and run `mutation-settle`. Use the returned `previousPassId` for further edits. With sandbox disabled, `workflow-observe-step` runs the original approved command and preserves its real local result without a validator private key; independent verification remains pending. See [IDE-EXECUTION.md](IDE-EXECUTION.md#restricted-member-editing).
23
25
  7. Read [REUSE-DISCIPLINE.md](REUSE-DISCIPLINE.md) when modifying an existing system. The comparison baseline is the existing implementation in that same repository. It never means another project, another account, or copying external systems.
24
- 8. Read [DELIVERY-BOUNDARIES.md](DELIVERY-BOUNDARIES.md) before stopping a task or showing any consumption number. A task may only stop at a clean boundary, such a stop is `delivered-stage-N` and never `failed` or `blocked`, and a number without a measured baseline is never displayed.
25
- 9. Before finishing, submit actual independent execution evidence, run the server's verification and verdict, and display the confirmed evaluation acknowledgement. Preserve `receiptId`, `requestId`, execution generation and evaluation digest. “已回传” requires `acknowledgement.stored: true`. Missing or failed feedback remains visible. Completed verified tasks automatically release superseded Entrorove snapshot copies, preserving origin and final rollback images; active, waiting and unknown tasks retain recovery data.
26
+ 8. Read [DELIVERY-BOUNDARIES.md](DELIVERY-BOUNDARIES.md) before claiming a delivered stage or showing consumption. A delivered stage requires a clean boundary and uses `delivered-stage-N`. Blocked or unknown work can wait with recovery data intact; it is not a delivered stage. Display consumption only with a measured baseline.
27
+ 9. Before claiming verified completion, submit actual independent execution evidence, run the server's verification and verdict, and display the confirmed evaluation acknowledgement. Preserve `receiptId`, `requestId`, execution generation and evaluation digest. “已回传” requires `acknowledgement.stored: true`. Missing or failed feedback remains visible. Completed verified tasks automatically release superseded Entrorove snapshot copies, preserving origin and final rollback images; active, waiting and unknown tasks retain recovery data.
26
28
 
27
29
  The Codex guard preserves this binding over subsequent prompts and compaction and denies unsupported native mutations before execution. Hooks are host guardrails, not an operating-system security boundary; unsupported tool paths or a disabled/untrusted hook cannot be described as enforced. An explicit user decision to stop using the skill ends its governance and never becomes verified Entrorove delivery. Do not infer such a decision from quoted screenshots or a service failure.
28
30
 
31
+ After the same real blocked result occurs twice, diagnose it and use its supported recovery path. Do not send a third identical execution call. Read-only status and checkpoint acknowledgements do not count as execution progress. A blocked task may wait with its original flow, queue and recovery data intact; Stop must not force repeated automatic continuation of the same blocked turn.
32
+
29
33
  If the original flow is terminal and must release its task slot, invoke the discovered `flow-cancel` or `flow-abandon` operation with that same `flowId`. `flow-cancel` records an owner cancellation and keeps the task bound for read-only recovery. `flow-abandon` records the termination, clears acceptance and checkpoint state, unbinds the task and returns `nextStep.operation: "start"` so the original task can be routed again. These operations are available through the server and `local flow-cancel` / `local flow-abandon`; they never reopen the old flow.
30
34
 
31
- When a new user requirement arrives while the original task is unfinished, store its exact text in the private IDE queue. Use `queue` with `{"action":"status"}` to inspect it, then `{"action":"route","messageId":"...","mode":"queued"|"interrupt","priority":"background"|"normal"|"high"|"urgent"}`. An urgent interrupt is admitted only at a completed controlled-call checkpoint; it saves the original flow, task UUID, homepage number and request ID. After the inserted task reaches a verified end acknowledgement, the queue restores that exact parent context and its next operation. Unclassified text, unresolved calls and unknown outcomes block ending the turn. Queue state is bounded, account/session/repository-bound and survives compaction and process restart; it cannot wake a powered-off host.
35
+ When a new user requirement arrives while the original task is unfinished, store its exact text in the private IDE queue. Use `queue` with `{"action":"status"}` to inspect it, then `{"action":"route","messageId":"...","mode":"queued"|"interrupt","priority":"background"|"normal"|"high"|"urgent"}`. An urgent interrupt is admitted only at a completed controlled-call checkpoint; it saves the original flow, task UUID, homepage number and request ID. After the inserted task reaches a verified end acknowledgement, the queue restores that exact parent context and its next operation. Unclassified text and unresolved calls block dependent execution, not waiting for recovery. Unknown outcomes permit only the original read-only recovery. Queue state is bounded, account/session/repository-bound and survives compaction and process restart; it cannot wake a powered-off host.
32
36
 
33
37
  ## Operations and visible progress
34
38
 
package/cli.mjs CHANGED
@@ -10,7 +10,7 @@ import { runLocal } from './local-cli.mjs';
10
10
  import { runNative } from './native-client.mjs';
11
11
  import { configureNativeNotifications, notificationSettings, sendNativeNotification } from './native-notification.mjs';
12
12
  import { runTaskMessageCli } from './task-message-cli.mjs';
13
- import { runScopeConnect } from './scope-runner-client.mjs';
13
+ import { isConnectedCommand, runConnectedCommand } from './connected-cli.mjs';
14
14
  import { apiUrl, buildInvocation, claimRecurring, finishRecurring, invokeRemote, listRecurring, queryBillingExport, queryFlows, queryStatus, queryUsageSummary, readRecurringRuns } from './remote-client.mjs';
15
15
  const packageDir = path.dirname(fileURLToPath(import.meta.url));
16
16
  import { buildReview, submitSkillFeedback, feedbackNoticeVersion } from './review-client.mjs';
@@ -456,10 +456,8 @@ async function main() {
456
456
  : args[0] === 'test' && (await notificationSettings()).enabled ? await sendNativeNotification('test') : null;
457
457
  if (!result) throw new Error('Use notifications configure|status|test; enable this account before sending a test');
458
458
  process.stdout.write(`${JSON.stringify(result)}\n`);
459
- } else if (command === 'scope-connect' && args.length === 1) {
460
- const result = await runScopeConnect(args[0], await stdinJson(), remoteDependencies());
461
- process.stdout.write(`${JSON.stringify(result)}\n`);
462
- if (result.status !== 'succeeded') process.exitCode = 2;
459
+ } else if (isConnectedCommand(command) && args.length === 1) {
460
+ await runConnectedCommand(command, args[0], await stdinJson(), remoteDependencies());
463
461
  } else if (command === 'host-message') {
464
462
  await runTaskMessageCli(args, await stdinJson(), remoteDependencies());
465
463
  } else if (command === 'cloud-task' && args.length === 0) {
@@ -479,7 +477,7 @@ async function main() {
479
477
  } else if (command === 'local') {
480
478
  await runLocal(args);
481
479
  } else {
482
- throw new Error('Usage: entrorove unlock | uninstall | install [--host codex] | configure | agreement | invoke | queue | native <domain> <operation> | notifications configure|status|test | scope-connect <repositoryRoot> | host-message [adapterModulePath] | cloud-task | cloud-watch | cloud-status | feedback <requestId> [generation] | status <requestId> | flows | usage [flowId] | billing | recurring list|claim|finish|runs | review | local (JSON on stdin)');
480
+ throw new Error('Usage: entrorove unlock | uninstall | install [--host codex] | configure | agreement | invoke | queue | native <domain> <operation> | notifications configure|status|test | scope-connect <repositoryRoot> | mutation-connect <repositoryRoot> | mutation-settle <repositoryRoot> | workflow-observe-step <repositoryRoot> | host-message [adapterModulePath] | cloud-task | cloud-watch | cloud-status | feedback <requestId> [generation] | status <requestId> | flows | usage [flowId] | billing | recurring list|claim|finish|runs | review | local (JSON on stdin)');
483
481
  }
484
482
  }
485
483
  if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) { const { captureIdeResults } = await import('./ide-results.mjs');
@@ -0,0 +1,96 @@
1
+ import path from 'node:path';
2
+ import { createPublicKey } from 'node:crypto';
3
+ import { sha256 } from './local-integrity.mjs';
4
+ import { withStateLock } from './local-state-lock.mjs';
5
+ import { mutationStateRoot14, mutationFile14, mutationFail14, privateMutationDirectory14,
6
+ readMutationJson14, atomicMutationJson14 } from './local-mutation-files.mjs';
7
+ import { invokeRemoteResult } from './remote-client.mjs';
8
+ import { trustedOrigin } from './remote-transport.mjs';
9
+
10
+ const pinSchema = 'entrorove.cloud-mutation-authority-pin/1.0';
11
+ const keySchema = 'entrorove.mutation-authority-key/1.0';
12
+ const keyEnvironment = 'ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY';
13
+ const fail = () => mutationFail14('ENTROROVE_MUTATION_AUTHORITY_PIN', 'Cloud mutation public authority must retain its trusted origin, canonical Ed25519 key and original private local pin');
14
+ function exact(value, fields) {
15
+ if (!value || typeof value !== 'object' || Array.isArray(value)
16
+ || Object.keys(value).sort().join('\0') !== [...fields].sort().join('\0')) fail();
17
+ }
18
+ function authorityOrigin(value) {
19
+ let origin;
20
+ try { origin = trustedOrigin(value); } catch { fail(); }
21
+ if (origin !== value) fail();
22
+ const url = new URL(origin), ownedTest = process.env.NODE_ENV === 'test' && url.protocol === 'http:'
23
+ && ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname);
24
+ if (origin !== 'https://entrorove.com' && !ownedTest) fail();
25
+ return origin;
26
+ }
27
+ function publicIdentity(encoded) {
28
+ try {
29
+ if (typeof encoded !== 'string' || encoded.length > 4096 || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) fail();
30
+ const der = Buffer.from(encoded, 'base64'), key = createPublicKey({ key: der, type: 'spki', format: 'der' });
31
+ if (key.asymmetricKeyType !== 'ed25519' || der.toString('base64') !== encoded
32
+ || key.export({ type: 'spki', format: 'der' }).toString('base64') !== encoded) fail();
33
+ return { publicKey: encoded, keyId: sha256(der) };
34
+ } catch { fail(); }
35
+ }
36
+ function authorityKey(value) {
37
+ if (value?.status === 'unavailable') {
38
+ exact(value, ['schemaVersion', 'purpose', 'status', 'code']);
39
+ if (value.schemaVersion !== keySchema || value.purpose !== 'mutation-pass-verification'
40
+ || value.code !== 'ENTROROVE_MUTATION_AUTHORITY_UNAVAILABLE') fail();
41
+ mutationFail14('ENTROROVE_MUTATION_AUTHORITY_UNAVAILABLE', 'The authenticated server has no available public cloud mutation authority');
42
+ }
43
+ exact(value, ['schemaVersion', 'purpose', 'algorithm', 'keyId', 'publicKey']);
44
+ if (value.schemaVersion !== keySchema || value.purpose !== 'mutation-pass-verification' || value.algorithm !== 'Ed25519'
45
+ || !/^[a-f0-9]{64}$/.test(value.keyId) || publicIdentity(value.publicKey).keyId !== value.keyId) fail();
46
+ return value;
47
+ }
48
+ function matchEnvironment(authority) {
49
+ if (process.env[keyEnvironment] !== undefined && publicIdentity(process.env[keyEnvironment]).publicKey !== authority.publicKey) fail();
50
+ }
51
+ async function pinLocation(home) {
52
+ const stateRoot = await mutationStateRoot14({ home });
53
+ return { stateRoot, file: path.join(stateRoot, 'cloud-mutation-authority.json') };
54
+ }
55
+ async function readPin(location, origin) {
56
+ const parent = await mutationFile14(location.stateRoot, { allowMissing: true, final: 'directory' });
57
+ if (!parent.exists) return null;
58
+ if (process.platform !== 'win32' && (parent.stat.mode & 0o077) || process.getuid && parent.stat.uid !== process.getuid()) fail();
59
+ const file = await mutationFile14(location.file, { allowMissing: true });
60
+ if (!file.exists) return null;
61
+ if (file.stat.nlink !== 1 || file.stat.size > 8192) fail();
62
+ const pin = await readMutationJson14(location.file);
63
+ exact(pin, ['schemaVersion', 'apiOrigin', 'authority']);
64
+ if (pin.schemaVersion !== pinSchema || authorityOrigin(pin.apiOrigin) !== origin) fail();
65
+ authorityKey(pin.authority); matchEnvironment(pin.authority);
66
+ return pin;
67
+ }
68
+ export async function hydrateCloudMutationAuthority(home, apiOrigin) {
69
+ const origin = authorityOrigin(apiOrigin), pin = await readPin(await pinLocation(home), origin);
70
+ if (!pin) return false;
71
+ process.env[keyEnvironment] = pin.authority.publicKey;
72
+ return true;
73
+ }
74
+ export async function bootstrapCloudMutationAuthority(dependencies) {
75
+ const origin = authorityOrigin(dependencies.apiOrigin()), location = await pinLocation(dependencies.home);
76
+ const previous = await readPin(location, origin);
77
+ if (process.env[keyEnvironment] !== undefined) publicIdentity(process.env[keyEnvironment]);
78
+ let result, response;
79
+ try { ({ result, response } = await invokeRemoteResult({ operation: 'capabilities', input: {} }, dependencies)); }
80
+ catch {
81
+ throw Object.assign(new Error('Authenticated cloud mutation authority discovery is unavailable; no mutation operation was transmitted'),
82
+ { code: 'ENTROROVE_MUTATION_AUTHORITY_DISCOVERY', knownBeforeTransmission: true });
83
+ }
84
+ if (!response.ok || result?.output?.status !== 'succeeded') fail();
85
+ const authority = authorityKey(result.output.capabilities?.mutationAuthority);
86
+ matchEnvironment(authority);
87
+ if (previous && previous.authority.publicKey !== authority.publicKey) fail();
88
+ await privateMutationDirectory14(location.stateRoot, { privateFrom: location.stateRoot });
89
+ return withStateLock(location.file, async () => {
90
+ const current = await readPin(location, origin);
91
+ if (current && current.authority.publicKey !== authority.publicKey) fail();
92
+ if (!current) await atomicMutationJson14(location.file, { schemaVersion: pinSchema, apiOrigin: origin, authority }, { privateFrom: location.stateRoot });
93
+ process.env[keyEnvironment] = authority.publicKey;
94
+ return { keyId: authority.keyId, apiOrigin: origin };
95
+ });
96
+ }
@@ -0,0 +1,20 @@
1
+ import { runScopeConnect } from './scope-runner-client.mjs';
2
+ import { runMutationConnect, runMutationSettle } from './mutation-client.mjs';
3
+ import { runWorkflowObserveStep } from './workflow-observation-client.mjs';
4
+
5
+ const commands = Object.freeze({
6
+ 'scope-connect': runScopeConnect,
7
+ 'mutation-connect': runMutationConnect,
8
+ 'mutation-settle': runMutationSettle,
9
+ 'workflow-observe-step': runWorkflowObserveStep,
10
+ });
11
+ export const isConnectedCommand = command => Object.hasOwn(commands, command);
12
+
13
+ export async function runConnectedCommand(command, root, input, dependencies) {
14
+ if (!isConnectedCommand(command)) throw new Error('Unsupported connected command');
15
+ const result = await commands[command](root, input, dependencies);
16
+ process.stdout.write(`${JSON.stringify(result)}\n`);
17
+ const expected = command === 'mutation-settle' ? ['reported', 'succeeded'] : ['succeeded'];
18
+ if (!expected.includes(result.status)) process.exitCode = 2;
19
+ return result;
20
+ }
package/ide-code-mode.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  import { createRequire } from 'node:module';
2
- import { literalCommand, sdkCommand, readCommand } from './ide-command.mjs';
2
+ import { literalCommand, sdkCommand, readCommand, runtimeHashRead } from './ide-command.mjs';
3
3
 
4
- export function inspectIdeCodeMode(code, runtime) {
5
- if (typeof code !== 'string' || code.length > 1_000_000) return false;
4
+ export function prepareIdeCodeMode(code, runtime) {
5
+ if (typeof code !== 'string' || code.length > 1_000_000) return { allowed: false };
6
6
  const ts = createRequire(runtime.cli)('typescript');
7
7
  const source = ts.createSourceFile('host-call.js', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
8
- if (source.parseDiagnostics.length) return false;
9
- let allowed = true;
8
+ if (source.parseDiagnostics.length) return { allowed: false };
9
+ let allowed = true; const replacements = [];
10
10
  const literal = node => {
11
11
  if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
12
12
  if (ts.isNumericLiteral(node)) return Number(node.text);
@@ -26,8 +26,22 @@ export function inspectIdeCodeMode(code, runtime) {
26
26
  const name = node.expression.getText(source);
27
27
  if (name === 'tools.exec_command') {
28
28
  try {
29
- const args = literal(node.arguments[0]), command = literalCommand(args.cmd ?? args.command);
30
- if (!(readCommand(command) || sdkCommand(command, runtime))) allowed = false;
29
+ const argument = node.arguments[0], args = literal(argument), command = literalCommand(args.cmd ?? args.command);
30
+ const hash = runtimeHashRead(command, runtime);
31
+ if (hash) {
32
+ const safe = node.arguments.length === 1 && typeof args.cmd === 'string' && !Object.hasOwn(args, 'command')
33
+ && Object.keys(args).every(key => ['cmd', 'workdir', 'max_output_tokens', 'yield_time_ms', 'login', 'shell'].includes(key))
34
+ && (args.shell === undefined || args.shell === '/bin/sh')
35
+ && (args.login === undefined || args.login === false)
36
+ && (args.workdir === undefined || typeof args.workdir === 'string')
37
+ && (args.max_output_tokens === undefined || Number.isSafeInteger(args.max_output_tokens) && args.max_output_tokens > 0)
38
+ && (args.yield_time_ms === undefined || Number.isSafeInteger(args.yield_time_ms) && args.yield_time_ms >= 0);
39
+ if (!safe) allowed = false;
40
+ else {
41
+ const fixed = { ...args, cmd: hash, shell: '/bin/sh', login: false };
42
+ replacements.push({ start: argument.getStart(source), end: argument.getEnd(), text: JSON.stringify(fixed) });
43
+ }
44
+ } else if (!(readCommand(command) || sdkCommand(command, runtime))) allowed = false;
31
45
  } catch { allowed = false; }
32
46
  } else if (name === 'tools.update_goal' || name === 'tools.get_goal') {
33
47
  try {
@@ -40,5 +54,13 @@ export function inspectIdeCodeMode(code, runtime) {
40
54
  }
41
55
  ts.forEachChild(node, visit);
42
56
  };
43
- visit(source); return allowed;
57
+ visit(source);
58
+ if (!allowed) return { allowed: false };
59
+ let prepared = code;
60
+ for (const item of replacements.sort((a, b) => b.start - a.start)) {
61
+ prepared = `${prepared.slice(0, item.start)}${item.text}${prepared.slice(item.end)}`;
62
+ }
63
+ return { allowed: true, code: prepared };
44
64
  }
65
+
66
+ export function inspectIdeCodeMode(code, runtime) { return prepareIdeCodeMode(code, runtime).allowed; }
package/ide-command.mjs CHANGED
@@ -86,7 +86,7 @@ export function sdkCommand(parsed, runtime) {
86
86
  if (args[0] === 'usage' && args.length <= 2 && !parsed.input
87
87
  && (args[1] === undefined || /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(args[1]))) return { args };
88
88
  if (args[0] === 'local' && /^[a-z][a-z0-9-]+$/u.test(args[1] ?? '') && [2,3].includes(args.length) && parsed.input) return { args, input: parsed.input };
89
- if (args[0] === 'scope-connect' && args.length === 2 && parsed.input) return { args, input: parsed.input };
89
+ if (['scope-connect', 'mutation-connect', 'mutation-settle', 'workflow-observe-step'].includes(args[0]) && args.length === 2 && parsed.input) return { args, input: parsed.input };
90
90
  if (args[0] === 'native' && args.length === 3 && parsed.input) return { args, input: parsed.input };
91
91
  return null;
92
92
  }
@@ -113,6 +113,32 @@ export function readCommand(parsed) {
113
113
  return false;
114
114
  }
115
115
 
116
+ const runtimeHashTool = process.platform === 'darwin'
117
+ ? { name: 'shasum', executable: '/usr/bin/shasum', flags: ['-a', '256'] }
118
+ : process.platform === 'linux'
119
+ ? { name: 'sha256sum', executable: '/usr/bin/sha256sum', flags: [] } : null;
120
+
121
+ export function runtimeHashCommand(runtime) {
122
+ const node = runtime?.node;
123
+ if (!runtimeHashTool || typeof node !== 'string' || node.length > 4096 || !path.isAbsolute(node)
124
+ || path.basename(node) !== 'node' || path.normalize(node) !== node
125
+ || !/^[A-Za-z0-9/._+@ -]+$/u.test(node)) return null;
126
+ return `/usr/bin/env -i PATH=/usr/bin:/bin ${runtimeHashTool.executable} ${[...runtimeHashTool.flags, '--'].join(' ')} '${node}'`;
127
+ }
128
+
129
+ export function runtimeHashRead(parsed, runtime) {
130
+ const canonical = runtimeHashCommand(runtime);
131
+ if (!canonical || !parsed || parsed.input) return null;
132
+ const words = parsed.words, { name, executable, flags } = runtimeHashTool;
133
+ const normal = [name, ...flags, runtime.node];
134
+ const absolute = [executable, ...flags, runtime.node];
135
+ const withSeparator = [executable, ...flags, '--', runtime.node];
136
+ const protectedCommand = ['/usr/bin/env', '-i', 'PATH=/usr/bin:/bin', ...withSeparator];
137
+ if ([normal, absolute, withSeparator, protectedCommand].some(expected =>
138
+ words.length === expected.length && words.every((word, index) => word === expected[index]))) return canonical;
139
+ return null;
140
+ }
141
+
116
142
  export function renderSdkCommand(call, runtime, sessionId, host = process.env.ENTROROVE_IDE ?? 'Codex') {
117
143
  const quote = value => `'${String(value).replace(/'/gu, "'\\''")}'`;
118
144
  const header = `ENTROROVE_IDE=${quote(host)} ENTROROVE_SESSION=${quote(sessionId)} CODEX_THREAD_ID=${quote(sessionId)} ${[runtime.node, runtime.cli, ...call.args].map(quote).join(' ')}`;