@entrorove/skill 0.1.32 → 0.1.34
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 +20 -0
- package/CHANGELOG.md +10 -0
- package/SKILL.md +31 -5
- package/automatic-feedback-client.mjs +43 -0
- package/cli.mjs +20 -1
- package/cloud-event-stream.mjs +27 -0
- package/cloud-task-client.mjs +34 -15
- package/cloud-watch-client.mjs +58 -0
- package/ide-presentation.mjs +58 -0
- package/local-cli.mjs +12 -1
- package/local-contract-types.mjs +9 -3
- package/local-contracts.mjs +31 -2
- package/local-error.mjs +19 -1
- package/local-execution.mjs +23 -1
- package/local-scope-config-imports.mjs +192 -0
- package/local-scope-repository.mjs +45 -9
- package/manifest.json +1 -1
- package/package.json +7 -1
- package/release-state.json +21 -15
- package/release.mjs +16 -6
- package/releases.json +12 -0
- package/remote-client.mjs +37 -9
- package/scope-runner-client.mjs +125 -0
package/CAPABILITY-MAP.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# 能力对账图(WP-02 / AC-20)
|
|
2
2
|
|
|
3
|
+
## 2026-10-12 补充:旧版八技能的 IDE 调用链
|
|
4
|
+
|
|
5
|
+
逐项对照旧版八个技能的已安装说明、入口和服务实现,本次补回以下共同保证。后面的 AC-20~24 保留其历史基线与当时测试边界。
|
|
6
|
+
|
|
7
|
+
| 旧版保证 | 当前入口与行为 |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| 每个任务选择云端/本地,原生确认不可用时仍须展示问题 | `cloud-task` 返回 Confirm Protocol 和 `chatFallback`;确认才提交,退出保留原任务并采用本地方案 |
|
|
10
|
+
| 实际进度必须进入 IDE | `cloud-watch` 订阅本人任务 SSE,输出独立 IDE 事件;调用方必须把公开消息和摘要展示到对话进度 |
|
|
11
|
+
| 执行提示与推广可见 | `智能执行中,请勿关闭!` 与服务端选中的真实推广独立输出;保留推广 HTTPS 链接,按事件 ID 去重 |
|
|
12
|
+
| 断线恢复原任务 | `cloud-watch` 带原 `taskId/afterId` 只读续接;`status` 与 `feedback` 查询原请求,不重发业务 POST |
|
|
13
|
+
| 服务端自动评价,无第二次评价写入 | 调用完成事务保存本人原 request 的评价与结果摘要;CLI 校验回执。旧式适配器使用幂等持久化并在同一 HTTP 响应返回 |
|
|
14
|
+
| 未验证不可记好评 | intake、模型摘要、reported、不完整证据无评分;阻断、失败、P0/P1 不产生好评;发现和状态查询不生成新评价 |
|
|
15
|
+
| 实际 IDE 与原调用绑定 | `ENTROROVE_IDE` 或宿主检测写入原调用标签;每个 OS 用户继续复用受限凭据存储 |
|
|
16
|
+
| 最新官方技能说明生效 | 新调用使用 `npx --yes --prefer-online @entrorove/skill@latest`;安装更新后重新读取 managed `SKILL.md` |
|
|
17
|
+
| 调用次数与沙箱次数分开 | 普通调用与旧式 UUID 记录技能用量;`cloud-task` 才创建沙箱。所有查询和评价按账号权限过滤 |
|
|
18
|
+
|
|
19
|
+
自动技术评价是私有调用记录。公开用户评价仍须使用本人原话、真实评分和明确公开同意。原生系统通知、手机推送未实现;确认使用 IDE 问题工具或可见文本回退。思考详情只展示服务端公开的规划摘要。
|
|
20
|
+
|
|
3
21
|
本表把旧八域的远端公开操作与当前本地执行器分开统计。旧版 `SKILL.md` 只规定入口、状态和边界,没有逐域列出操作名;远端操作名采用随包锁定的 v2 runtime catalog 交叉核对,本地操作名直接来自 `local-contracts.mjs` 的 `LOCAL_OPERATION_SCHEMAS`。两个分母不能合并。
|
|
4
22
|
|
|
5
23
|
## 边界
|
|
@@ -36,6 +54,8 @@ Entrorove 只治理通过技能入口执行的动作:路径、通行证、快
|
|
|
36
54
|
|
|
37
55
|
## 本地基线(当前 `LOCAL_OPERATION_SCHEMAS` 100 项)
|
|
38
56
|
|
|
57
|
+
`scope-connect <absolute repositoryRoot>` 是 CLI 编排入口,不改变这 100 项本地操作。它串联远端 `scope-runner-prepare/register/challenge/attest` 与已有真实快照、签名检查,为原账号与任务提交范围证据;不授予执行证据签名或写入权限。全部远端操作以实时 discovery 为准。
|
|
58
|
+
|
|
39
59
|
| 分区 | 精确操作名 | 证据位置 |
|
|
40
60
|
| --- | --- | --- |
|
|
41
61
|
| 核心 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` |
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.34 — 2026-10-12
|
|
4
|
+
|
|
5
|
+
- 补回旧版八技能的 IDE 可见云端选择、真实事件进度与独立推广提示;服务端自动评价绑定原调用并返回可验证回执,支持只读恢复;修复旧式 UUID 调用漏计用量,保持账号隔离及沙箱/调用计数分离。
|
|
6
|
+
- Restore visible IDE cloud choice, authoritative live progress and separate promotion notices from the original eight-skill workflow. Persist account-bound automatic invocation evaluations with verifiable receipts and read-only recovery; meter legacy UUID calls without mixing skill and sandbox counters.
|
|
7
|
+
|
|
8
|
+
## 0.1.33 — 2026-10-12
|
|
9
|
+
|
|
10
|
+
- 补齐账号与任务隔离的本地签名器和真实快照接入,解析实际别名及类型声明;动态依赖仅进入受限规划,保留独立写入核验。
|
|
11
|
+
- Connected account-bound scope signers and real snapshots, resolved actual aliases and declarations, and retained restricted planning for unresolved dynamic imports with separate mutation verification.
|
|
12
|
+
|
|
3
13
|
## 0.1.32 — 2026-10-11
|
|
4
14
|
|
|
5
15
|
- 增加云端沙箱状态与用量只读查询,并支持按任务直接读取真实状态。
|
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.
|
|
8
|
+
Version: 0.1.34.
|
|
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
|
|
|
@@ -23,15 +23,27 @@ On first use or after a material agreement revision, the CLI returns a Confirm P
|
|
|
23
23
|
|
|
24
24
|
## Work with a task
|
|
25
25
|
|
|
26
|
+
### IDE display and cloud planning entry
|
|
27
|
+
|
|
28
|
+
For a task using this skill's cloud planning, first run `npx --yes --prefer-online @entrorove/skill@latest cloud-task` with `{"goal":"<the user's actual goal>","ideTaskId":"<the current IDE task ID, when available>"}` on stdin. Render the returned choice with the host's question tool or native UI. If no native adapter exists, show `interaction.chatFallback` in the conversation and collect its answer. Tool output alone is not a displayed question. Offer **确认** and **退出,采用本地方案**. Exit or an explicit cancellation continues the original task locally; it must not stop the task or change account preferences. An unanswered question does not authorize cloud consumption. Reuse a prior explicit choice for this same task rather than asking again.
|
|
29
|
+
|
|
30
|
+
With `sandboxChoice: "use"` (or the protocol callback's `answer: "use"`), retain the real returned task ID and immediately run the returned `nextAction`, `cloud-watch`. It subscribes to that account's authoritative SSE stream, returns newline-delimited `entrorove.ide-event/1.0` events, then one `entrorove.cloud-watch/1.0` result. **Mirror each new event's public `message` and optional `detail` into visible IDE progress/commentary.** Deduplicate by task ID and event ID, including events repeated in the final result. The same rule applies to normal invocation `ideMessages` and notice events. A JSON field or collapsed command log does not satisfy this display requirement. Show `智能执行中,请勿关闭!` and the selected promotion on separate lines; preserve the promotion's actual HTTPS link. Use only the server's selected promotion; do not insert a local test advertisement.
|
|
31
|
+
|
|
32
|
+
Display the actual entering, allocation, thinking, public output and exit events as they arrive. Do not generate model reasoning, invent intermediate events or replace `reported` with verified completion. The public model summary is the planning output available to show. `云端沙箱输出完毕` means the current planning response has arrived; inspect its real task/verification status before dependent execution. Exit here closes the progress subscription and does not cancel the persisted task.
|
|
33
|
+
|
|
34
|
+
When a bounded watch returns `nextAction`, continue it with the returned `afterId`; reconnect only with GET and that cursor. Never repeat cloud-task POST to recover progress. For an available planning result, query `status <feedback.requestId>` to obtain the original accepted flow and required next step, then continue that flow. Do not create a second `start` for the same cloud goal. If the task is still running but no events arrive, say so and keep reading its real status. Missing credentials, blocked entitlement, expiry and stream errors must be visible.
|
|
35
|
+
|
|
36
|
+
Set `ENTROROVE_IDE` to the actual host name (for example `Codex`, `Cursor` or `Claude Code`) when host detection is unavailable. It carries only the IDE label; credentials remain in the restricted store. Use the latest official package with `--prefer-online` for new invocations, and reread the managed `SKILL.md` after installation reports an update. Recovering an uncertain request remains a read-only query of its original ID.
|
|
37
|
+
|
|
26
38
|
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
39
|
|
|
28
40
|
`start` creates a normal durable skill flow. It does not create a cloud-sandbox task or contribute to cloud-sandbox usage. To create a real cloud-sandbox task, call `operation: "cloud-task"` with `input.goal`; the CLI returns a Confirm Protocol choice interaction before any cloud request. Continue the same request with `sandboxChoice: "use"` or `sandboxChoice: "local"`. `use` calls the authenticated `/api/cloud-task` endpoint and returns the real `taskId`; `local` skips only this task. The server is authoritative for account entitlement and the account-wide sandbox preference: a disabled or unavailable account returns `blocked` and creates no task. A `needs-input`, `blocked` or `failed` result is not a successful dispatch and must be shown to the user.
|
|
29
41
|
|
|
30
|
-
After dispatch,
|
|
42
|
+
After dispatch, use `cloud-watch` for live IDE progress and `cloud-status` for a read-only snapshot with `input.taskId`. Both are CLI-only routes, also accepted by `invoke`; they do not pass through `/api/skill/invoke`. Status includes public title/summary, actual model usage and the stored automatic evaluation when available. A missing task is explicit and never treated as completed. The IDE and website consume the same account-bound event stream.
|
|
31
43
|
|
|
32
44
|
### Per-task cloud sandbox choice
|
|
33
45
|
|
|
34
|
-
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,
|
|
46
|
+
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, continue locally. Cloud submission requires an explicit choice or the user’s existing authorization for this task; silence never authorizes cloud consumption. 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 host cannot preflight the account preference, show the choice only as a routing decision and let the server's authoritative `/api/cloud-task` check return a clear `blocked` prerequisite; never claim that a task started after a blocked response. This choice never bypasses the normal task schema, mutation authority, evidence checks, or risk confirmation.
|
|
35
47
|
|
|
36
48
|
```json
|
|
37
49
|
{"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}}}
|
|
@@ -45,13 +57,25 @@ For a returned `flowId`, continue with `{"operation":"continue","input":{"flowId
|
|
|
45
57
|
|
|
46
58
|
The v2 progress sequence is durable and ordered: the first runtime step reports `intake`, then the applicability `gate` is recorded and verified, then the selected contract or plan steps run. Every step can be `pending`, `running`, `reported`, `verified`, `blocked` or `failed`; only a completed verdict with trusted evidence is green.
|
|
47
59
|
|
|
60
|
+
When intake returns `workflow-gate`, connect the current repository before invoking that gate: run `npx @entrorove/skill scope-connect <absolute repositoryRoot>` with JSON `{"flowId":"<original flowId>","paths":["<existing source file>"]}` on stdin. Use the concrete existing source files in the task's scope; new files belong in the later plan's declared artifacts. The adapter enrolls an OS-private Ed25519 scope signer for this accepted account and task, answers a server challenge, copies and verifies real source files, and submits the signed facts. It sends hashes and scope metadata rather than source contents or the private key. Enrollment grants scope inspection only; execution evidence and write passes retain their separate authority checks. An operator-configured signer cannot be replaced. After a successful connection, invoke the original `workflow-gate` with the same paths. Refresh an expired snapshot by reconnecting the same flow and path set. Show an explicit blocked result; query the original request ID after an unknown transport outcome before considering another attempt.
|
|
61
|
+
|
|
62
|
+
Scope inspection resolves aliases from actual `tsconfig.json` or `jsconfig.json`, including relative `extends`. For plugin source compiled by a host project whose config is outside the source's ancestors, include `importContexts: [{"sourcePrefix":"<plugin source directory>","configPath":"<actual host tsconfig.json>"}]` in `scope-connect` input. Inspect the real build configuration first; never invent an alias or mark an unresolved import as external. These context paths remain inside the repository, their hashes enter the signed facts, and the local task binding freezes them.
|
|
63
|
+
|
|
64
|
+
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.
|
|
65
|
+
|
|
48
66
|
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.
|
|
49
67
|
|
|
50
68
|
Use `npx @entrorove/skill status <requestId>` after a connection loss. It performs a read-only GET lookup and never resends a POST. Use `npx @entrorove/skill flows` to list recoverable flow steps. A v2 flow has its own `flowId` and progresses through `pending`, `running`, `reported`, `verified`, `blocked` or `failed`; a reported result is not a verified result.
|
|
51
69
|
|
|
52
|
-
The local execution layer is available through `npx @entrorove/skill local <operation> [repositoryRoot]` with a JSON object on standard input. `local capabilities` lists the complete contract. Operations include `snapshot-plan`, `snapshot-verify`, `mutate-gate`, `gate-issue`, `gate-verify`, `guarded-write`, `budget-init`, `budget-context`, `budget-read`, `budget-status`, `budget-extend`, budget renewal confirmation, `peer-coordinate`, `spawn-bind`, `peer-complete`, `lease-verify`, `probe`, `reassess`, execution ledger and virtual branch records, `chain-init`, `chain-resume`, `chain-status`, `chain-answer`, `evidence-sign`, `evidence-verify`, `merge-verified`, `rollback`, `formula-evaluate`, `formula-graph`, `formula-final-gate`, `run-approved-plan` and `final-gate`. State is kept only under `~/.entrorove/`; snapshots never create Git branches or worktrees. Guarded writes require a short-lived Ed25519 pass bound to the flow, step, snapshot digest and path set. Local evidence uses `entrorove.test-evidence/2.0`; unsigned local evidence remains incomplete and verified delivery requires a trusted receipt. Approved plan execution requires a frozen plan, matching approval and unchanged files; without a configured runner signer it reports local evidence as incomplete.
|
|
70
|
+
The local execution layer is available through `npx @entrorove/skill local <operation> [repositoryRoot]` with a JSON object on standard input. `local capabilities` lists the complete contract, operation groups and `requiresPass` metadata. `local capabilities <group>` filters that same contract. Operations include `snapshot-plan`, `snapshot-verify`, `mutate-gate`, `gate-issue`, `gate-verify`, `guarded-write`, `budget-init`, `budget-context`, `budget-read`, `budget-status`, `budget-extend`, budget renewal confirmation, `peer-coordinate`, `spawn-bind`, `peer-complete`, `lease-verify`, `probe`, `reassess`, execution ledger and virtual branch records, `chain-init`, `chain-resume`, `chain-status`, `chain-answer`, `evidence-sign`, `evidence-verify`, `merge-verified`, `rollback`, `formula-evaluate`, `formula-graph`, `formula-final-gate`, `run-approved-plan` and `final-gate`. State is kept only under `~/.entrorove/`; snapshots never create Git branches or worktrees. Guarded writes require a short-lived Ed25519 pass bound to the flow, step, snapshot digest and path set. Local evidence uses `entrorove.test-evidence/2.0`; unsigned local evidence remains incomplete and verified delivery requires a trusted receipt. Approved plan execution requires a frozen plan, matching approval and unchanged files; without a configured runner signer it reports local evidence as incomplete.
|
|
71
|
+
|
|
72
|
+
Local CLI failures use the machine-readable `entrorove.local-error/1.0` envelope `{schemaVersion,status,code,disposition,message}` on stderr. `status` is `failed` for invalid input or computation errors and `blocked` for missing mutation, lease, snapshot or handoff authority. Formula `add` and `mul` require at least one argument; `div`, `safeDivide` and `percentOf` reject a zero denominator with `DIV_ZERO`. The formula engine declares `round` as half-away-from-zero with two decimal places by default; an engine may declare another `decimalPlaces` value from 0 through 28 in its `roundingPolicy`.
|
|
53
73
|
|
|
54
|
-
|
|
74
|
+
### Automatic invocation evaluation and optional user reviews
|
|
75
|
+
|
|
76
|
+
Every completed or failed execution call receives a server-generated `feedback` receipt bound to the original account, request hash and result digest. Durable calls commit this evaluation in the same transaction as the result; legacy adapters return an idempotent receipt with the same HTTP response. The CLI verifies the original identity and evaluation digest. Preserve `receiptId`, `requestId` and `evaluation.digest`; never generate a score or comment locally. Display the returned evaluation acknowledgement in IDE progress, deduplicated by receipt ID. Read the saved receipt with `npx @entrorove/skill feedback <original requestId>` after a connection loss; this is GET only and never repeats execution or submits another evaluation. Discovery/status queries create no evaluations.
|
|
77
|
+
|
|
78
|
+
Only server-verified progress or a validated pure operation earns a positive automatic evaluation. Reported steps, intake, model summaries and incomplete evidence remain `pending` with no score. Blocked/failed results and P0/P1 findings cannot become positive. These are private technical evaluations; they do not create a public user review or masquerade as the user's words. If a server advertising this extension returns no valid receipt, show the missing receipt and query its original ID. Do not silently claim that evaluation was reported.
|
|
55
79
|
|
|
56
80
|
When the user gives an actual evaluation after using the skill, offer to publish that feedback. Only after they explicitly agree to public display under the current notice, send their unchanged words, IDE name and 1–5 rating as JSON on standard input to `npx @entrorove/skill review`, including `invocationRequestId` from their actual successful call, `publicConsent: true` and `noticeVersion: "2026-10-07.1"`. The same account must own that successful call within the last 30 days; a recorded usage event, failed call or unknown result does not qualify. Three public reviews per account per day share one server quota across API replicas. Public output exposes only IDE, rating, original text and time, never account or invocation identity. Never invent, rewrite or infer a review from task completion. The website reads accepted reviews directly from entrorove's independent database.
|
|
57
81
|
|
|
@@ -60,3 +84,5 @@ Feedback command example (replace the request ID with the original successful in
|
|
|
60
84
|
```json
|
|
61
85
|
{"invocationRequestId":"<original successful request ID>","ide":"Codex","rating":5,"comment":"<unchanged user words>","publicConsent":true,"noticeVersion":"2026-10-07.1"}
|
|
62
86
|
```
|
|
87
|
+
|
|
88
|
+
The fixed Git byte-metering adapter ships hash-pinned precompiled libraries for Darwin and glibc Linux on arm64/x64. It extracts the selected library on first use; it never invokes a C compiler on the user machine. Darwin requires Apple’s trusted Git (Xcode or Command Line Tools), xcrun and codesign; Linux requires the trusted `/usr/bin/git` and glibc. Missing or unsupported tools return `ENTROROVE_GIT_READER_UNSUPPORTED` explicitly and do not fall back to unmetered source reads.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { parseTransportResponse, requestTransport, trustedOrigin } from './remote-transport.mjs';
|
|
3
|
+
|
|
4
|
+
function canonical(value) {
|
|
5
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
6
|
+
if (value && typeof value === 'object') return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
|
|
7
|
+
return JSON.stringify(value);
|
|
8
|
+
}
|
|
9
|
+
function digest(value) { return createHash('sha256').update(canonical(value)).digest('hex'); }
|
|
10
|
+
export function verifyAutomaticFeedback(feedback, requestId, requestHash) {
|
|
11
|
+
const evaluation = feedback?.evaluation;
|
|
12
|
+
if (!feedback || feedback.schemaVersion !== 'entrorove.automatic-feedback/1.0' || feedback.stored !== true
|
|
13
|
+
|| !/^[0-9a-f-]{36}$/i.test(feedback.receiptId) || feedback.requestId !== requestId
|
|
14
|
+
|| !/^[a-f0-9]{64}$/.test(feedback.requestHash) || requestHash && feedback.requestHash !== requestHash
|
|
15
|
+
|| !/^[a-f0-9]{64}$/.test(feedback.resultDigest) || !evaluation
|
|
16
|
+
|| evaluation.schemaVersion !== 'entrorove.automatic-evaluation/1.0' || evaluation.requestId !== requestId
|
|
17
|
+
|| !['good','bad','pending'].includes(evaluation.disposition)
|
|
18
|
+
|| !['passed','failed','incomplete'].includes(evaluation.validation)
|
|
19
|
+
|| !Number.isSafeInteger(evaluation.durationMs) || evaluation.durationMs < 0
|
|
20
|
+
|| !['findingCount','p0Count','p1Count'].every(key => Number.isSafeInteger(evaluation[key]) && evaluation[key] >= 0)
|
|
21
|
+
|| typeof evaluation.comment !== 'string' || !evaluation.comment.trim()) {
|
|
22
|
+
throw new Error(`Invalid automatic feedback receipt; query feedback ${requestId} without repeating the invocation`);
|
|
23
|
+
}
|
|
24
|
+
const { digest: checksum,...core } = evaluation;
|
|
25
|
+
if (digest(core) !== checksum || !['succeeded','reported','blocked','failed','incomplete'].includes(evaluation.status)
|
|
26
|
+
|| evaluation.findingCount < evaluation.p0Count + evaluation.p1Count
|
|
27
|
+
|| evaluation.disposition === 'pending' && (evaluation.score !== null || evaluation.validation !== 'incomplete')
|
|
28
|
+
|| evaluation.disposition === 'good' && (evaluation.status !== 'succeeded' || evaluation.validation !== 'passed' || evaluation.p0Count || evaluation.p1Count)
|
|
29
|
+
|| evaluation.disposition === 'bad' && evaluation.validation !== 'failed'
|
|
30
|
+
|| evaluation.disposition !== 'pending' && (!Number.isInteger(evaluation.score) || evaluation.score < 0 || evaluation.score > 100)) {
|
|
31
|
+
throw new Error('Automatic feedback digest or validation state is inconsistent');
|
|
32
|
+
}
|
|
33
|
+
return feedback;
|
|
34
|
+
}
|
|
35
|
+
export async function queryAutomaticFeedback(requestId, dependencies) {
|
|
36
|
+
if (typeof requestId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(requestId)) throw new Error('feedback requires its original request ID');
|
|
37
|
+
const token = await dependencies.readCredential(dependencies.home);
|
|
38
|
+
const response = await requestTransport(trustedOrigin(dependencies.apiOrigin()),`/api/skill/feedback/${requestId}`,
|
|
39
|
+
{ headers: { authorization: `Bearer ${token}` } });
|
|
40
|
+
const result = parseTransportResponse(response,'automatic feedback');
|
|
41
|
+
if (!response.ok) throw new Error(`Automatic feedback unavailable: HTTP ${response.status}; requestId=${requestId}`);
|
|
42
|
+
return verifyAutomaticFeedback(result.feedback,requestId);
|
|
43
|
+
}
|
package/cli.mjs
CHANGED
|
@@ -9,10 +9,13 @@ import { compareVersions } from './version.mjs';
|
|
|
9
9
|
import { credentialPath, readCredential, writeCredential } from './local-credential.mjs';
|
|
10
10
|
import { runLocal } from './local-cli.mjs';
|
|
11
11
|
import { runTaskMessageCli } from './task-message-cli.mjs';
|
|
12
|
+
import { runScopeConnect } from './scope-runner-client.mjs';
|
|
12
13
|
import { apiUrl, buildInvocation, invokeRemote, queryFlows, queryStatus } from './remote-client.mjs';
|
|
13
14
|
const packageDir = path.dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
import { buildReview, submitSkillFeedback, feedbackNoticeVersion } from './review-client.mjs';
|
|
15
16
|
import { queryCloudTask, runCloudTask as dispatchCloudTask } from './cloud-task-client.mjs';
|
|
17
|
+
import { watchCloudTask } from './cloud-watch-client.mjs';
|
|
18
|
+
import { queryAutomaticFeedback } from './automatic-feedback-client.mjs';
|
|
16
19
|
const markerName = '.entrorove-managed.json';
|
|
17
20
|
const installFiles = ['SKILL.md', 'CAPABILITY-MAP.md', 'AGREEMENT.md', 'manifest.json'];
|
|
18
21
|
const markerSchema = 'entrorove.managed-install/1.0';
|
|
@@ -395,6 +398,13 @@ export async function runCloudStatus(input, dependencies = remoteDependencies())
|
|
|
395
398
|
if (['blocked', 'failed', 'missing'].includes(result.status)) process.exitCode = 2;
|
|
396
399
|
return result;
|
|
397
400
|
}
|
|
401
|
+
export async function runCloudWatch(input, dependencies = remoteDependencies()) {
|
|
402
|
+
const result = await watchCloudTask(input,{ ...dependencies,
|
|
403
|
+
onIdeEvent: event => process.stdout.write(`${JSON.stringify(event)}\n`) });
|
|
404
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
405
|
+
if (['blocked','failed','missing'].includes(result.status) || ['blocked','failed'].includes(result.taskStatus)) process.exitCode = 2;
|
|
406
|
+
return result;
|
|
407
|
+
}
|
|
398
408
|
|
|
399
409
|
async function invoke(data) {
|
|
400
410
|
if (data?.operation === 'cloud-task') {
|
|
@@ -403,6 +413,7 @@ async function invoke(data) {
|
|
|
403
413
|
if (data?.operation === 'cloud-status') {
|
|
404
414
|
return runCloudStatus(data.input, remoteDependencies());
|
|
405
415
|
}
|
|
416
|
+
if (data?.operation === 'cloud-watch') return runCloudWatch(data.input);
|
|
406
417
|
return invokeRemote(data, remoteDependencies());
|
|
407
418
|
}
|
|
408
419
|
|
|
@@ -429,12 +440,20 @@ async function main() {
|
|
|
429
440
|
process.stdout.write(JSON.stringify({ accepted, version: agreementVersion }) + '\n');
|
|
430
441
|
} else if (command === 'invoke' && args.length === 0) {
|
|
431
442
|
await invoke(await stdinJson());
|
|
443
|
+
} else if (command === 'scope-connect' && args.length === 1) {
|
|
444
|
+
const result = await runScopeConnect(args[0], await stdinJson(), remoteDependencies());
|
|
445
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
446
|
+
if (result.status !== 'succeeded') process.exitCode = 2;
|
|
432
447
|
} else if (command === 'host-message') {
|
|
433
448
|
await runTaskMessageCli(args, await stdinJson(), remoteDependencies());
|
|
434
449
|
} else if (command === 'cloud-task' && args.length === 0) {
|
|
435
450
|
await runCloudTask(await stdinJson());
|
|
436
451
|
} else if (command === 'cloud-status' && args.length === 0) {
|
|
437
452
|
await runCloudStatus(await stdinJson());
|
|
453
|
+
} else if (command === 'cloud-watch' && args.length === 0) {
|
|
454
|
+
await runCloudWatch(await stdinJson());
|
|
455
|
+
} else if (command === 'feedback' && args.length === 1) {
|
|
456
|
+
process.stdout.write(`${JSON.stringify(await queryAutomaticFeedback(args[0],remoteDependencies()))}\n`);
|
|
438
457
|
} else if (command === 'review' && args.length === 0) {
|
|
439
458
|
await submitReview(await stdinJson());
|
|
440
459
|
} else if (command === 'status' && args.length === 1) {
|
|
@@ -456,7 +475,7 @@ async function main() {
|
|
|
456
475
|
} else if (command === 'local') {
|
|
457
476
|
await runLocal(args);
|
|
458
477
|
} else {
|
|
459
|
-
throw new Error('Usage: entrorove install | configure | agreement | invoke | host-message [adapterModulePath] | cloud-task | cloud-status | status <requestId> | flows | review | local (JSON on stdin)');
|
|
478
|
+
throw new Error('Usage: entrorove install | configure | agreement | invoke | scope-connect <repositoryRoot> | host-message [adapterModulePath] | cloud-task | cloud-watch | cloud-status | feedback <requestId> | status <requestId> | flows | review | local (JSON on stdin)');
|
|
460
479
|
}
|
|
461
480
|
}
|
|
462
481
|
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export async function consumeCloudStream(body, onFrame) {
|
|
2
|
+
if (!body) throw new Error('Cloud event response has no stream');
|
|
3
|
+
const reader = body.getReader(), decoder = new TextDecoder('utf-8',{ fatal: true });
|
|
4
|
+
let pending = '', size = 0;
|
|
5
|
+
try {
|
|
6
|
+
while (true) {
|
|
7
|
+
const { value,done } = await reader.read();
|
|
8
|
+
if (value) size += value.byteLength;
|
|
9
|
+
if (size > 16_777_216) throw new Error('Cloud event stream exceeded its bounded read');
|
|
10
|
+
pending += decoder.decode(value,{ stream: !done });
|
|
11
|
+
let match;
|
|
12
|
+
while ((match = /\r?\n\r?\n/u.exec(pending))) {
|
|
13
|
+
const frame = pending.slice(0,match.index); pending = pending.slice(match.index + match[0].length);
|
|
14
|
+
if (frame.length > 262144) throw new Error('Cloud event frame exceeded its bounded read');
|
|
15
|
+
let event = 'message', id, data = [];
|
|
16
|
+
for (const line of frame.split(/\r?\n/u)) {
|
|
17
|
+
if (line.startsWith('event:')) event = line.slice(6).trim();
|
|
18
|
+
else if (line.startsWith('id:')) id = line.slice(3).trim();
|
|
19
|
+
else if (line.startsWith('data:')) data.push(line.slice(5).replace(/^ /u,''));
|
|
20
|
+
}
|
|
21
|
+
if (data.length && await onFrame({ event,id,data: JSON.parse(data.join('\n')) }) === false) return;
|
|
22
|
+
}
|
|
23
|
+
if (pending.length > 262144) throw new Error('Cloud event frame exceeded its bounded read');
|
|
24
|
+
if (done) { if (pending.trim()) throw new Error('Cloud event response ended with an incomplete frame'); return; }
|
|
25
|
+
}
|
|
26
|
+
} finally { await reader.cancel().catch(() => undefined); reader.releaseLock(); }
|
|
27
|
+
}
|
package/cloud-task-client.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { appendInvocation } from './local-invocations.mjs';
|
|
3
3
|
import { parseTransportResponse, requestTransport, trustedOrigin } from './remote-transport.mjs';
|
|
4
|
+
import { displayIdeEvent, ideClientHeaders } from './ide-presentation.mjs';
|
|
5
|
+
import { verifyAutomaticFeedback } from './automatic-feedback-client.mjs';
|
|
4
6
|
|
|
5
7
|
const CLOUD_TASK_SCHEMA = 'entrorove.cloud-task/1.0';
|
|
6
8
|
const CLOUD_STATUS_SCHEMA = 'entrorove.cloud-status/1.0';
|
|
@@ -26,7 +28,10 @@ function validateInput(input) {
|
|
|
26
28
|
if (input.ideTaskId !== undefined && (typeof input.ideTaskId !== 'string' || !input.ideTaskId.trim() || input.ideTaskId.length > 200)) {
|
|
27
29
|
throw new Error('cloud-task ideTaskId must be a non-empty string of at most 200 characters');
|
|
28
30
|
}
|
|
29
|
-
if (input.sandboxChoice !== undefined &&
|
|
31
|
+
if (input.sandboxChoice !== undefined && input.answer !== undefined && input.sandboxChoice !== input.answer) {
|
|
32
|
+
throw new Error('Conflicting cloud sandbox choices');
|
|
33
|
+
}
|
|
34
|
+
if ((input.sandboxChoice ?? input.answer) !== undefined && !['use', 'local'].includes(input.sandboxChoice ?? input.answer)) {
|
|
30
35
|
throw new Error('cloud-task sandboxChoice must be use or local');
|
|
31
36
|
}
|
|
32
37
|
return { goal: input.goal, ...(input.ideTaskId === undefined ? {} : { ideTaskId: input.ideTaskId }) };
|
|
@@ -44,18 +49,23 @@ function validateTaskId(taskId) {
|
|
|
44
49
|
return taskId;
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
|
|
52
|
+
function authenticatedHeaders(token, headers = {}) {
|
|
53
|
+
return { ...headers, authorization: `Bearer ${token}` };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function sandboxChoiceInteraction(id, task = {}) {
|
|
48
57
|
return {
|
|
49
58
|
schemaVersion: 'confirm.interaction/1.0', requestId: id, type: 'choice',
|
|
50
|
-
question: '
|
|
59
|
+
question: '是否启用云端沙箱执行思考规划?',
|
|
51
60
|
options: [
|
|
52
|
-
{ id: 'use', label: '
|
|
53
|
-
{ id: 'local', label: '
|
|
61
|
+
{ id: 'use', label: '确认', hint: '为当前任务创建云端思考规划并显示真实进度' },
|
|
62
|
+
{ id: 'local', label: '退出,采用本地方案', hint: '继续当前任务,不改变账号开关' },
|
|
54
63
|
],
|
|
55
|
-
default: '
|
|
64
|
+
default: 'local', timeout: null, timeoutAction: 'local', risk: 'low',
|
|
56
65
|
riskDescription: '这是当前任务的执行位置选择,不改变账号级云端沙箱开关。',
|
|
57
66
|
rememberable: false, memoryKey: '',
|
|
58
|
-
callback: { operation: 'cloud-task', payload: {
|
|
67
|
+
callback: { operation: 'cloud-task', payload: { ...task } },
|
|
68
|
+
chatFallback: '是否启用云端沙箱执行思考规划?\n1. 确认:当前任务使用云端沙箱\n2. 退出:自动采用本地方案,继续当前任务',
|
|
59
69
|
};
|
|
60
70
|
}
|
|
61
71
|
|
|
@@ -72,24 +82,30 @@ export async function runCloudTask(input, dependencies) {
|
|
|
72
82
|
const task = validateInput(input);
|
|
73
83
|
validateDependencies(dependencies);
|
|
74
84
|
const id = requestId(task);
|
|
85
|
+
const choice = input.sandboxChoice ?? input.answer;
|
|
75
86
|
await dependencies.install();
|
|
76
87
|
if (!await dependencies.agreementAccepted(dependencies.home)) {
|
|
77
88
|
return envelope(task, 'blocked', { code: 'agreement-required', interaction: dependencies.agreementRequired(dependencies.home) });
|
|
78
89
|
}
|
|
79
|
-
if (
|
|
80
|
-
task, interaction: sandboxChoiceInteraction(id),
|
|
90
|
+
if (choice === undefined) return envelope(task, 'needs-input', {
|
|
91
|
+
task, interaction: sandboxChoiceInteraction(id,task),
|
|
92
|
+
});
|
|
93
|
+
if (choice === 'local') return envelope(task, 'skipped', {
|
|
94
|
+
reason: 'task', executionIsolation: 'local', message: '已退出云端沙箱,采用本地方案继续当前任务。',
|
|
95
|
+
continuation: { goal: task.goal,...(task.ideTaskId ? { ideTaskId: task.ideTaskId } : {}) },
|
|
81
96
|
});
|
|
82
|
-
if (input.sandboxChoice === 'local') return envelope(task, 'skipped', { reason: 'task' });
|
|
83
97
|
|
|
84
98
|
const token = await dependencies.readCredential(dependencies.home);
|
|
85
99
|
const origin = trustedOrigin(dependencies.apiOrigin());
|
|
86
100
|
const metadata = { kind: 'remote', operation: 'cloud-task', requestId: id };
|
|
87
101
|
await appendInvocation({ ...metadata, event: 'started' }, dependencies.home);
|
|
102
|
+
displayIdeEvent({ schemaVersion: 'entrorove.ide-event/1.0',requestId: id,kind: 'status',
|
|
103
|
+
phase: 'enter',message: '正在进入云端沙箱',separateLine: true },dependencies.onIdeEvent);
|
|
88
104
|
let response;
|
|
89
105
|
try {
|
|
90
106
|
response = await requestTransport(origin, '/api/cloud-task', {
|
|
91
107
|
method: 'POST', timeoutMs: 30_000,
|
|
92
|
-
headers: { 'content-type': 'application/json'
|
|
108
|
+
headers: authenticatedHeaders(token, { 'content-type': 'application/json',...ideClientHeaders() }),
|
|
93
109
|
body: JSON.stringify(task),
|
|
94
110
|
});
|
|
95
111
|
} catch (cause) {
|
|
@@ -120,7 +136,8 @@ export async function runCloudTask(input, dependencies) {
|
|
|
120
136
|
await appendInvocation({ ...metadata, event: 'completed' }, dependencies.home);
|
|
121
137
|
return envelope(task, 'started', { taskId: result.taskId,
|
|
122
138
|
taskStatus: typeof result.status === 'string' ? result.status : 'running',
|
|
123
|
-
executionIsolation: typeof result.executionIsolation === 'string' ? result.executionIsolation : 'worker'
|
|
139
|
+
executionIsolation: typeof result.executionIsolation === 'string' ? result.executionIsolation : 'worker',
|
|
140
|
+
nextAction: { operation: 'cloud-watch',input: { taskId: result.taskId,afterId: '0' } } });
|
|
124
141
|
}
|
|
125
142
|
|
|
126
143
|
function publicTask(task) {
|
|
@@ -149,7 +166,7 @@ export async function queryCloudTask(taskId, dependencies) {
|
|
|
149
166
|
const token = await dependencies.readCredential(dependencies.home);
|
|
150
167
|
const origin = trustedOrigin(dependencies.apiOrigin());
|
|
151
168
|
const metadata = { kind: 'remote', operation: 'cloud-status', requestId: requestId({ taskId: id }) };
|
|
152
|
-
const taskResponse = await requestTransport(origin, `/api/cloud-sandbox/tasks/${id}`, { headers:
|
|
169
|
+
const taskResponse = await requestTransport(origin, `/api/cloud-sandbox/tasks/${id}`, { headers: authenticatedHeaders(token) });
|
|
153
170
|
let taskBody;
|
|
154
171
|
try { taskBody = await responseBody(taskResponse); } catch { taskBody = null; }
|
|
155
172
|
if (!taskResponse.ok || !taskBody?.task || taskBody.task.id !== id) {
|
|
@@ -158,10 +175,11 @@ export async function queryCloudTask(taskId, dependencies) {
|
|
|
158
175
|
code: typeof taskBody?.error === 'string' ? taskBody.error : taskResponse.status === 404 ? 'cloud-task-not-found' : 'cloud-status-unavailable', httpStatus: taskResponse.status };
|
|
159
176
|
}
|
|
160
177
|
const task = taskBody.task;
|
|
178
|
+
const feedback = taskBody.feedback ? verifyAutomaticFeedback(taskBody.feedback,taskBody.feedback.requestId) : undefined;
|
|
161
179
|
let usage = [];
|
|
162
180
|
let usageError;
|
|
163
181
|
const usageResponse = await requestTransport(origin, `/api/cloud-sandbox/tasks/${id}/usage`, {
|
|
164
|
-
headers:
|
|
182
|
+
headers: authenticatedHeaders(token),
|
|
165
183
|
});
|
|
166
184
|
try {
|
|
167
185
|
const usageBody = await responseBody(usageResponse);
|
|
@@ -170,6 +188,7 @@ export async function queryCloudTask(taskId, dependencies) {
|
|
|
170
188
|
} catch { usageError = 'cloud-usage-invalid-response'; }
|
|
171
189
|
await appendInvocation({ ...metadata, event: 'status' }, dependencies.home);
|
|
172
190
|
return { schemaVersion: CLOUD_STATUS_SCHEMA, status: 'observed', taskId: id,
|
|
173
|
-
taskStatus: task.status, task: publicTask(task), usage, ...(
|
|
191
|
+
taskStatus: task.status, task: publicTask(task), usage, ...(feedback ? { feedback } : {}),
|
|
192
|
+
...(usageError ? { usageError } : {}) };
|
|
174
193
|
}
|
|
175
194
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { trustedOrigin } from './remote-transport.mjs';
|
|
2
|
+
import { consumeCloudStream } from './cloud-event-stream.mjs';
|
|
3
|
+
import { automaticFeedbackIdeEvent, cloudIdeEvent, displayIdeEvent, ideClientHeaders } from './ide-presentation.mjs';
|
|
4
|
+
import { queryCloudTask } from './cloud-task-client.mjs';
|
|
5
|
+
|
|
6
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
7
|
+
function inputOptions(input) {
|
|
8
|
+
if (!input || !UUID.test(input.taskId) || Object.keys(input).some(key => !['taskId','afterId','waitMs'].includes(key))) {
|
|
9
|
+
throw new Error('cloud-watch requires its real taskId and optional afterId and waitMs');
|
|
10
|
+
}
|
|
11
|
+
const afterId = input.afterId ?? '0', waitMs = input.waitMs ?? 15000;
|
|
12
|
+
if (typeof afterId !== 'string' || !/^(0|[1-9][0-9]{0,18})$/u.test(afterId) || BigInt(afterId) > 9223372036854775807n
|
|
13
|
+
|| !Number.isInteger(waitMs) || waitMs < 1 || waitMs > 45000) throw new Error('Invalid cloud-watch cursor or bounded wait');
|
|
14
|
+
return { taskId: input.taskId,afterId,waitMs };
|
|
15
|
+
}
|
|
16
|
+
export async function watchCloudTask(input, dependencies) {
|
|
17
|
+
const options = inputOptions(input), { taskId,waitMs } = options;
|
|
18
|
+
await dependencies.install();
|
|
19
|
+
if (!await dependencies.agreementAccepted(dependencies.home)) return { status: 'blocked',code: 'agreement-required',
|
|
20
|
+
interaction: dependencies.agreementRequired(dependencies.home) };
|
|
21
|
+
const token = await dependencies.readCredential(dependencies.home);
|
|
22
|
+
const controller = new AbortController(), deadline = setTimeout(() => controller.abort(),waitMs);
|
|
23
|
+
const events = [], emit = event => { events.push(event); displayIdeEvent(event,dependencies.onIdeEvent); };
|
|
24
|
+
let afterId = options.afterId, taskStatus = 'running', outputAvailable = false, settled = false, flowId;
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch(`${trustedOrigin(dependencies.apiOrigin())}/api/cloud-sandbox/tasks/${taskId}/events`, {
|
|
27
|
+
redirect: 'error',signal: controller.signal,headers: { authorization: `Bearer ${token}`,
|
|
28
|
+
accept: 'text/event-stream','last-event-id': afterId,...ideClientHeaders() } });
|
|
29
|
+
if (!response.ok) throw new Error(`Cloud event stream unavailable: HTTP ${response.status}; taskId=${taskId}`);
|
|
30
|
+
if (!response.headers.get('content-type')?.startsWith('text/event-stream')) throw new Error('Cloud event response is not an event stream');
|
|
31
|
+
await consumeCloudStream(response.body,frame => {
|
|
32
|
+
if (frame.event === 'stream-error') throw new Error(`Cloud event stream stopped: ${frame.data?.code ?? 'stream-failed'}`);
|
|
33
|
+
if (frame.event === 'status') {
|
|
34
|
+
if (frame.data?.taskId !== taskId || !['running','completed','blocked','failed','cancelled'].includes(frame.data.status)) throw new Error('Invalid cloud task status event');
|
|
35
|
+
taskStatus = frame.data.status; return taskStatus === 'running';
|
|
36
|
+
}
|
|
37
|
+
if (!/^[1-9][0-9]{0,18}$/u.test(frame.id ?? '') || frame.data?.id !== frame.id) throw new Error('Invalid cloud event cursor');
|
|
38
|
+
if (BigInt(frame.id) <= BigInt(afterId)) return;
|
|
39
|
+
const event = cloudIdeEvent(frame.data,taskId); afterId = frame.id; emit(event);
|
|
40
|
+
if (typeof frame.data.metadata?.flowId === 'string') flowId = frame.data.metadata.flowId;
|
|
41
|
+
settled = event.phase === 'workflow' && ['reported','verified','blocked','failed'].includes(event.state);
|
|
42
|
+
outputAvailable = settled && ['reported','verified'].includes(event.state);
|
|
43
|
+
if (settled) return false;
|
|
44
|
+
});
|
|
45
|
+
} catch (cause) {
|
|
46
|
+
if (!controller.signal.aborted) throw cause;
|
|
47
|
+
} finally { clearTimeout(deadline); controller.abort(); }
|
|
48
|
+
if (outputAvailable) emit({ schemaVersion: 'entrorove.ide-event/1.0',taskId,kind: 'status',phase: 'output',
|
|
49
|
+
message: '云端沙箱输出完毕;实际验证状态以任务回执为准。',separateLine: true });
|
|
50
|
+
const current = await queryCloudTask(taskId,dependencies);
|
|
51
|
+
if (current.status === 'observed') taskStatus = current.taskStatus;
|
|
52
|
+
if (current.feedback) emit(automaticFeedbackIdeEvent(current.feedback));
|
|
53
|
+
emit({ schemaVersion: 'entrorove.ide-event/1.0',taskId,kind: 'status',phase: 'exit',
|
|
54
|
+
message: settled || taskStatus !== 'running' ? '云端沙箱正在退出本次进度订阅' : '云端沙箱仍在执行,下次从当前进度继续读取',separateLine: true });
|
|
55
|
+
return { schemaVersion: 'entrorove.cloud-watch/1.0',status: current.status,taskId,taskStatus,afterId,outputAvailable,
|
|
56
|
+
events,...(flowId ? { flowId } : {}),task: current.task,usage: current.usage,...(current.feedback ? { feedback: current.feedback } : {}),
|
|
57
|
+
nextAction: taskStatus === 'running' && !settled ? { operation: 'cloud-watch',input: { taskId,afterId,waitMs } } : null };
|
|
58
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const UUID = /^[0-9a-f-]{36}$/i;
|
|
2
|
+
export function ideClientHeaders(environment = process.env) {
|
|
3
|
+
const ide = environment.ENTROROVE_IDE ?? (environment.CODEX_THREAD_ID ? 'Codex'
|
|
4
|
+
: environment.CURSOR_TRACE_ID ? 'Cursor' : environment.CLAUDE_CODE_ENTRYPOINT ? 'Claude Code' : 'Entrorove CLI');
|
|
5
|
+
if (typeof ide !== 'string' || !/^[\p{L}\p{N} .+_-]{2,48}$/u.test(ide)) throw new Error('Invalid ENTROROVE_IDE client name');
|
|
6
|
+
return { 'x-entrorove-ide': ide };
|
|
7
|
+
}
|
|
8
|
+
export function displayIdeEvent(event, sink) {
|
|
9
|
+
if (sink) sink(event);
|
|
10
|
+
else process.stderr.write(`${event.message}\n${event.detail ? event.detail + '\n' : ''}`);
|
|
11
|
+
}
|
|
12
|
+
function text(value) {
|
|
13
|
+
return typeof value === 'string' ? value.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/gu, '').slice(0,12000) : '';
|
|
14
|
+
}
|
|
15
|
+
export function cloudIdeEvent(value, taskId) {
|
|
16
|
+
if (!value || value.taskId !== taskId || !UUID.test(taskId) || !/^[1-9][0-9]{0,18}$/.test(value.id)
|
|
17
|
+
|| !['thinking','execution','notice','error'].includes(value.kind) || typeof value.content !== 'string') {
|
|
18
|
+
throw new Error('Cloud event lost its original task identity');
|
|
19
|
+
}
|
|
20
|
+
const metadata = value.metadata ?? {};
|
|
21
|
+
let message = text(value.content), phase = String(metadata.stepId ?? 'workflow');
|
|
22
|
+
if (message === 'Cloud task accepted.') message = '已进入云端沙箱,正在分配沙箱任务';
|
|
23
|
+
else if (message === 'Cloud workflow started.') message = '云端沙箱任务已分配';
|
|
24
|
+
else if (message === 'Cloud model classification started.') message = '云端沙箱正在思考中';
|
|
25
|
+
else if (message === 'Task classification completed.') message = '云端沙箱思考正在输出';
|
|
26
|
+
const event = { schemaVersion: 'entrorove.ide-event/1.0',taskId,eventId: value.id,kind: value.kind,
|
|
27
|
+
state: metadata.state ?? 'running',phase,message,separateLine: true };
|
|
28
|
+
if (typeof metadata.summary === 'string' && metadata.summary !== value.content
|
|
29
|
+
&& value.content !== 'Cloud model classification started.') event.detail = text(metadata.summary);
|
|
30
|
+
if (value.kind === 'notice' && metadata.url !== undefined) {
|
|
31
|
+
let url;
|
|
32
|
+
try { url = new URL(metadata.url); } catch { throw new Error('Cloud promotion link is invalid'); }
|
|
33
|
+
if (url.protocol !== 'https:' || url.username || url.password) throw new Error('Cloud promotion requires a public HTTPS link');
|
|
34
|
+
event.url = url.href;
|
|
35
|
+
event.message = `[${message.replace(/[\\[\]]/gu, '\\$&')}](<${url.href}>)`;
|
|
36
|
+
}
|
|
37
|
+
return event;
|
|
38
|
+
}
|
|
39
|
+
export function invocationIdeNotices(output) {
|
|
40
|
+
const strings = [output?.message,output?.result?.message,
|
|
41
|
+
...(Array.isArray(output?.progress) ? output.progress.filter(item => item.kind === 'notice').map(item => item.summary) : [])];
|
|
42
|
+
return [...new Set(strings.filter(value => typeof value === 'string').flatMap(value => value.split(/\r?\n/u))
|
|
43
|
+
.filter(value => value === '智能执行中,请勿关闭!' || /^【[^\n】]{1,32}】/u.test(value)).map(text))];
|
|
44
|
+
}
|
|
45
|
+
export function presentInvocationNotices(result, sink) {
|
|
46
|
+
const messages = invocationIdeNotices(result?.output);
|
|
47
|
+
if (result?.feedback) messages.push(automaticFeedbackIdeEvent(result.feedback).message);
|
|
48
|
+
for (const message of messages) displayIdeEvent({ schemaVersion: 'entrorove.ide-event/1.0',
|
|
49
|
+
requestId: result.output.requestId,kind: 'notice',message,separateLine: true },sink);
|
|
50
|
+
return messages.length ? { ...result,ideMessages: messages } : result;
|
|
51
|
+
}
|
|
52
|
+
export function automaticFeedbackIdeEvent(feedback) {
|
|
53
|
+
const disposition = feedback.evaluation.disposition;
|
|
54
|
+
const status = disposition === 'good' ? '当前调用验证通过' : disposition === 'bad'
|
|
55
|
+
? '当前调用被阻断或验证失败' : '交付验证尚未完成,暂不评分';
|
|
56
|
+
return { schemaVersion: 'entrorove.ide-event/1.0',requestId: feedback.requestId,receiptId: feedback.receiptId,
|
|
57
|
+
kind: 'evaluation',phase: 'feedback',message: `本次调用评价已回传至服务器:${status}。`,separateLine: true };
|
|
58
|
+
}
|
package/local-cli.mjs
CHANGED
|
@@ -60,6 +60,17 @@ const operations = {
|
|
|
60
60
|
|
|
61
61
|
export const LOCAL_OPERATIONS = Object.freeze(operations);
|
|
62
62
|
|
|
63
|
+
function capabilitiesFor(group) {
|
|
64
|
+
if (group === undefined) return LOCAL_CAPABILITIES;
|
|
65
|
+
if (typeof group !== 'string' || !LOCAL_CAPABILITIES.groups.includes(group)) {
|
|
66
|
+
throw Object.assign(new Error(`Unknown local capability group: ${String(group)}`), { code: 'ENTROROVE_CAPABILITY_GROUP_UNKNOWN' });
|
|
67
|
+
}
|
|
68
|
+
const operations = LOCAL_CAPABILITIES.operations.filter(operation => LOCAL_CAPABILITIES.operationMetadata[operation]?.group === group);
|
|
69
|
+
return { ...LOCAL_CAPABILITIES, groups: [group], operations,
|
|
70
|
+
operationSchemas: Object.fromEntries(operations.map(operation => [operation, LOCAL_CAPABILITIES.operationSchemas[operation]])),
|
|
71
|
+
operationMetadata: Object.fromEntries(operations.map(operation => [operation, LOCAL_CAPABILITIES.operationMetadata[operation]])) };
|
|
72
|
+
}
|
|
73
|
+
|
|
63
74
|
async function checkedLocalInput(operation, input) {
|
|
64
75
|
try { return validateLocalInput(operation, input); }
|
|
65
76
|
catch (cause) {
|
|
@@ -73,7 +84,7 @@ export async function runLocal(args = [], dependencies) {
|
|
|
73
84
|
const operation = args[0];
|
|
74
85
|
if (operation === 'capabilities' || operation === 'help') {
|
|
75
86
|
validateLocalInput(operation, process.stdin.isTTY ? {} : await stdinJson());
|
|
76
|
-
process.stdout.write(`${JSON.stringify(
|
|
87
|
+
process.stdout.write(`${JSON.stringify(capabilitiesFor(args[1]))}\n`); return;
|
|
77
88
|
}
|
|
78
89
|
const handler = operations[operation];
|
|
79
90
|
if (!handler) throw new Error(`Usage: entrorove local <${['capabilities', ...Object.keys(operations)].join('|')}> [repositoryRoot]`);
|
package/local-contract-types.mjs
CHANGED
|
@@ -30,13 +30,19 @@ export const evidenceArray = array(evidence, 1);
|
|
|
30
30
|
export const change = object({ gatePassPath: text, targetPath: relativePath, content: {}, expectedSha256: nullable(sha256), workerId: identifier, leaseId: identifier }, ['gatePassPath', 'targetPath', 'content']);
|
|
31
31
|
export const decimal = { anyOf: [{ type: 'string', pattern: '^\\s*[+-]?(?:[0-9]+(?:\\.[0-9]*)?|\\.[0-9]+)\\s*$' }, { type: 'number' }] };
|
|
32
32
|
export const formula = { $ref: '#/$defs/formula' };
|
|
33
|
+
const formulaBranch = (schema, title, description) => ({ ...schema, title, description });
|
|
33
34
|
function expression(operators, minItems, maxItems) {
|
|
34
|
-
|
|
35
|
+
const name = operators.join('|');
|
|
36
|
+
const arity = maxItems === undefined ? `${minItems} or more` : minItems === maxItems ? `exactly ${minItems}` : `${minItems} to ${maxItems}`;
|
|
37
|
+
return formulaBranch(object({ op: enumeration(operators), args: { ...array(formula, minItems), ...(maxItems === undefined ? {} : { maxItems }) } }, ['op', 'args']),
|
|
38
|
+
`${name} formula node`, `Operator ${name} requires ${arity} arguments.`);
|
|
35
39
|
}
|
|
36
40
|
export const formulaDefinitions = { formula: { oneOf: [
|
|
37
|
-
object({ ref: text }, ['ref']),
|
|
41
|
+
formulaBranch(object({ ref: text }, ['ref']), 'Reference formula node', 'Reads one named input field.'),
|
|
42
|
+
formulaBranch(object({ lit: decimal }, ['lit']), 'Literal formula node', 'Contains one decimal literal.'),
|
|
38
43
|
expression(['add', 'mul'], 1), expression(['sub', 'div', 'safeDivide', 'percentOf'], 2, 2),
|
|
39
|
-
expression(['round'], 1, 1),
|
|
44
|
+
formulaBranch(expression(['round'], 1, 1), 'round formula node', 'Uses half-away-from-zero rounding at the declared two decimal places.'),
|
|
45
|
+
expression(['if'], 3, 3),
|
|
40
46
|
] } };
|
|
41
47
|
export const decimalValues = { type: 'object', additionalProperties: decimal };
|
|
42
48
|
export const formulaEntry = { type: 'object', properties: { key: text, expression: formula }, required: ['key', 'expression'] };
|