@entrorove/skill 0.1.35 → 0.1.37
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 +1 -1
- package/CHANGELOG.md +10 -0
- package/SKILL.md +11 -7
- package/automatic-feedback-client.mjs +35 -0
- package/cli.mjs +1 -1
- package/cloud-task-client.mjs +17 -6
- package/cloud-watch-client.mjs +8 -3
- package/ide-presentation.mjs +11 -8
- package/local-error.mjs +2 -0
- package/local-git-executable.mjs +33 -0
- package/local-scope-git-reads.mjs +3 -1
- package/local-scope-repository.mjs +3 -1
- package/manifest.json +1 -1
- package/native-client.mjs +5 -2
- package/native-notification.mjs +13 -6
- package/package.json +5 -1
- package/release-state.json +22 -18
- package/release.mjs +9 -1
- package/releases.json +12 -0
- package/remote-client.mjs +10 -2
- package/remote-transport.mjs +3 -1
- package/task-entry-client.mjs +28 -0
- package/task-identity.d.mts +2 -0
- package/task-identity.mjs +9 -0
package/CAPABILITY-MAP.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
| 实际进度必须进入 IDE | `cloud-watch` 订阅本人任务 SSE,输出独立 IDE 事件;调用方必须把公开消息和摘要展示到对话进度 |
|
|
24
24
|
| 执行提示与推广可见 | `智能执行中,请勿关闭!` 与服务端选中的真实推广独立输出;保留推广 HTTPS 链接,按事件 ID 去重 |
|
|
25
25
|
| 断线恢复原任务 | `cloud-watch` 带原 `taskId/afterId` 只读续接;`status` 与 `feedback` 查询原请求,不重发业务 POST |
|
|
26
|
-
|
|
|
26
|
+
| 调用结束立即确认评价回传 | 调用完成事务保存本人原 request 的评价与结果摘要;CLI 核验后立即回传原回执身份,服务端返回 IDE 接收确认,不重复写评价或计用量;只读恢复保留原执行代次 |
|
|
27
27
|
| 未验证不可记好评 | intake、模型摘要、reported、不完整证据无评分;阻断、失败、P0/P1 不产生好评;发现和状态查询不生成新评价 |
|
|
28
28
|
| 实际 IDE 与原调用绑定 | `ENTROROVE_IDE` 或宿主检测写入原调用标签;每个 OS 用户继续复用受限凭据存储 |
|
|
29
29
|
| 最新官方技能说明生效 | 新调用使用 `npx --yes --prefer-online @entrorove/skill@latest`;安装更新后重新读取 managed `SKILL.md` |
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.37 — 2026-10-13
|
|
4
|
+
|
|
5
|
+
- 统一 IDE 与网页沙箱任务编号;新任务按账号设置显示云端选择并保留原任务数据,进入及结束通知去重且发送失败可重试;适配 Windows Git 范围元数据检查。
|
|
6
|
+
- Unified IDE and website sandbox task numbers; added account-based entry choices while preserving original task facts, deduplicated lifecycle notifications with retryable OS rejection, and adapted Git scope metadata inspection for Windows.
|
|
7
|
+
|
|
8
|
+
## 0.1.36 — 2026-10-13
|
|
9
|
+
|
|
10
|
+
- 技能调用结束立即确认 IDE 评价回传,提供原账号、原调用与执行代次绑定的接收回执;完善原回执只读恢复与公开合同校验。
|
|
11
|
+
- Confirmed IDE evaluation acknowledgements immediately after skill calls, bound reception receipts to the original account, invocation and generation, and improved read-only recovery and published contract validation.
|
|
12
|
+
|
|
3
13
|
## 0.1.35 — 2026-10-13
|
|
4
14
|
|
|
5
15
|
- 新增协作状态查询、Aimlock 专用规划与八域原生调用;接入账号和设备隔离的任务通知,修复失败接续的评价回执冲突,并完善范围门禁接续。
|
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.37.
|
|
9
9
|
|
|
10
10
|
Read the user's goal and repository instructions first. Preserve the original task through planning, execution and verification. Use the workflow only where it helps; a small, low-risk edit can proceed directly. Never claim a reported step is a verified result.
|
|
11
11
|
|
|
@@ -25,9 +25,11 @@ On first use or after a material agreement revision, the CLI returns a Confirm P
|
|
|
25
25
|
|
|
26
26
|
### IDE display and cloud planning entry
|
|
27
27
|
|
|
28
|
-
For a
|
|
28
|
+
For a new engineering task, run `npx --yes --prefer-online @entrorove/skill@latest invoke` with `operation: "start"` and the actual task facts below. The CLI reads the authenticated account's sandbox preference before dispatch. An enabled account returns `status: "needs-input"` and a cloud choice, with no model call yet. This is a routing question, not an execution failure. Render it immediately with the host's question tool or native UI; if unavailable, display `interaction.chatFallback` and collect the answer. A collapsed command log is insufficient. Offer **确认** and **退出,采用本地方案**. After confirmation call the returned `cloud-task` callback with its exact original payload and `sandboxChoice: "use"`; exit repeats the original `start` with top-level `sandboxChoice: "local"`. Preserve the original goal, risk, work type and requirements. A disabled account continues through its configured local workflow; unavailable preferences stop entry explicitly. Reuse a prior explicit choice while resuming this task. Silence never authorizes cloud consumption.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
For an explicit cloud planning request without an existing start, `cloud-task` accepts `{"goal":"<actual goal>","ideTaskId":"<current IDE task ID, when available>"}` on stdin and returns the same choice. Do not start both routes for one task.
|
|
31
|
+
|
|
32
|
+
With `sandboxChoice: "use"` (or the protocol callback's `answer: "use"`), immediately display the returned acceptance message, for example **云端任务 08921 · 已进入**. Use the returned `taskNumber` verbatim: it matches the homepage sandbox tab and notification. Retain the UUID `taskId` separately for API queries; never display the IDE chat ID or invent a number instead. Then 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 the acceptance message repeated in the stream or 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
33
|
|
|
32
34
|
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
35
|
|
|
@@ -37,7 +39,7 @@ Set `ENTROROVE_IDE` to the actual host name (for example `Codex`, `Cursor` or `C
|
|
|
37
39
|
|
|
38
40
|
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.
|
|
39
41
|
|
|
40
|
-
`start` creates a
|
|
42
|
+
The server's ordinary `start` operation creates a durable skill flow and counts a skill invocation. The IDE CLI routes new starts through the account's preference and explicit task choice described above. A real sandbox is created only by `/api/cloud-task`, which returns both `taskId` and `taskNumber` and counts sandbox usage separately. `local` changes only this task's route. Disabled entitlement or missing model prerequisites stop cloud dispatch; a `needs-input`, `blocked` or `failed` result must never be described as sandbox entry. Do not send a second ordinary `start` after cloud acceptance; continue the original bound flow returned by its watch/status.
|
|
41
43
|
|
|
42
44
|
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.
|
|
43
45
|
|
|
@@ -81,13 +83,13 @@ Local CLI failures use the machine-readable `entrorove.local-error/1.0` envelope
|
|
|
81
83
|
|
|
82
84
|
### Optional task notifications
|
|
83
85
|
|
|
84
|
-
System notifications are disabled until this account explicitly enables them. `npx @entrorove/skill notifications configure` accepts only `{"enabled":true}` or `{"enabled":false}` on stdin; `notifications status` observes the account's setting and `notifications test` submits a generic test only when enabled. macOS and Linux use their existing notification service.
|
|
86
|
+
System notifications are disabled until this account explicitly enables them. `npx @entrorove/skill notifications configure` accepts only `{"enabled":true}` or `{"enabled":false}` on stdin; `notifications status` observes the account's setting and `notifications test` submits a generic test only when enabled. macOS and Linux use their existing notification service. Cloud notifications show the same `taskNumber` as the website: one entry and one ending, including a specific model-balance warning. Intermediate steps, promotion, keep-alive and repeated feedback do not repeat notifications. A rejected OS submission is not marked as sent and remains retryable. Settings use the same home and account as the invocation; changing the credential disables the previous account's setting. OS acceptance does not prove that a notification was visibly displayed.
|
|
85
87
|
|
|
86
88
|
The website's signed-in bell control enables encrypted Web Push for the current account and device. Desktop browsers and supported phones use this same push protocol; iPhone requires a Home Screen web app. Ask permission only through the user's enable action. Logout clears the browser binding and subscription. Messages contain only generic task status, with no prompts, credentials, model output or promotion. Delivery retries keep the original event and device binding; expired endpoints and temporary tasks are cleaned up automatically. Never describe an unsupported platform or failed delivery as a successful notification.
|
|
87
89
|
|
|
88
90
|
### Automatic invocation evaluation and optional user reviews
|
|
89
91
|
|
|
90
|
-
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;
|
|
92
|
+
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; domain adapters return an idempotent receipt with the same HTTP response. The CLI verifies the original identity and evaluation digest, then immediately returns that exact receipt identity to the authenticated acknowledgement endpoint before finishing the call. Cloud watches do the same as soon as the original evaluation is available. A confirmed `acknowledgement` records the IDE and server reception time without changing the evaluation or counting another invocation. Preserve `receiptId`, `requestId` and `evaluation.digest`; never generate a score or comment locally. Immediately display the confirmed evaluation acknowledgement in visible IDE commentary before the task's final answer, deduplicated by receipt ID. Tool output alone is insufficient. Say “已回传” only with a confirmed acknowledgement; an upload failure must remain visible and cannot become successful feedback. 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. An authorized retry before side effects keeps the original request but gets a new server execution generation and immutable receipt. Default feedback reads the latest generation; `feedback <original requestId> <generation>` reads a preserved earlier receipt. Never overwrite a failed receipt or use it as the current retry result. Discovery/status queries create no evaluations.
|
|
91
93
|
|
|
92
94
|
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.
|
|
93
95
|
|
|
@@ -99,4 +101,6 @@ Feedback command example (replace the request ID with the original successful in
|
|
|
99
101
|
{"invocationRequestId":"<original successful request ID>","ide":"Codex","rating":5,"comment":"<unchanged user words>","publicConsent":true,"noticeVersion":"2026-10-07.1"}
|
|
100
102
|
```
|
|
101
103
|
|
|
102
|
-
|
|
104
|
+
Ordinary Windows scope inspection resolves an absolute Git for Windows `git.exe` from its installation or absolute PATH directories, including paths containing spaces. It never invokes `/usr/bin/git` on Windows, a shell wrapper or a Git executable inside the inspected repository. Missing Git gives `ENTROROVE_GIT_UNAVAILABLE` and requires installing Git for Windows before reconnecting the original scope.
|
|
105
|
+
|
|
106
|
+
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. Native comparisons with enforced source-read budgets remain restricted to these platforms. An unsupported native budget adapter returns `ENTROROVE_GIT_READER_UNSUPPORTED`; do not bypass its budget or claim an ordinary Windows inspection verifies that separate capability.
|
|
@@ -43,3 +43,38 @@ export async function queryAutomaticFeedback(requestId, dependencies, generation
|
|
|
43
43
|
if (!response.ok) throw new Error(`Automatic feedback unavailable: HTTP ${response.status}; requestId=${requestId}`);
|
|
44
44
|
return verifyAutomaticFeedback(result.feedback,requestId);
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
function confirmedAcknowledgement(value, original, expectedIde) {
|
|
48
|
+
verifyAutomaticFeedback(value,original.requestId,original.requestHash);
|
|
49
|
+
const ack = value.acknowledgement, generation = original.generation ?? 1;
|
|
50
|
+
if (value.receiptId !== original.receiptId || value.resultDigest !== original.resultDigest
|
|
51
|
+
|| value.evaluation.digest !== original.evaluation.digest || (value.generation ?? 1) !== generation
|
|
52
|
+
|| !ack || ack.schemaVersion !== 'entrorove.feedback-acknowledgement/1.0' || ack.stored !== true
|
|
53
|
+
|| ack.receiptId !== original.receiptId || ack.requestId !== original.requestId || ack.generation !== generation
|
|
54
|
+
|| typeof ack.ide !== 'string' || !/^[\p{L}\p{N} .+_-]{2,48}$/u.test(ack.ide)
|
|
55
|
+
|| expectedIde && ack.ide !== expectedIde || !Number.isFinite(Date.parse(ack.receivedAt))) {
|
|
56
|
+
throw new Error(`IDE feedback is not confirmed; query feedback ${original.requestId}. Do not repeat the invocation.`);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function acknowledgeAutomaticFeedback(feedback, origin, token, ideHeaders) {
|
|
62
|
+
verifyAutomaticFeedback(feedback,feedback.requestId,feedback.requestHash);
|
|
63
|
+
if (feedback.acknowledgement) return confirmedAcknowledgement(feedback,feedback);
|
|
64
|
+
const route = `/api/skill/feedback/${feedback.requestId}`;
|
|
65
|
+
const headers = { authorization: `Bearer ${token}`,...ideHeaders };
|
|
66
|
+
let response;
|
|
67
|
+
try {
|
|
68
|
+
response = await requestTransport(trustedOrigin(origin),`${route}/ack`, { method: 'POST',
|
|
69
|
+
headers: { ...headers,'content-type': 'application/json' },body: JSON.stringify({ receiptId: feedback.receiptId,
|
|
70
|
+
requestHash: feedback.requestHash,resultDigest: feedback.resultDigest,evaluationDigest: feedback.evaluation.digest,
|
|
71
|
+
generation: feedback.generation ?? 1 }) });
|
|
72
|
+
} catch (cause) {
|
|
73
|
+
if (!cause.established) throw new Error(`IDE feedback was not sent; query feedback ${feedback.requestId}. Do not repeat the invocation.`);
|
|
74
|
+
// A lost acknowledgement response is recovered only by reading this
|
|
75
|
+
// immutable receipt. Neither the execution nor its acknowledgement POST is replayed.
|
|
76
|
+
response = await requestTransport(trustedOrigin(origin),`${route}?generation=${feedback.generation ?? 1}`, { headers });
|
|
77
|
+
}
|
|
78
|
+
if (!response.ok) throw new Error(`IDE feedback acknowledgement failed: HTTP ${response.status}; query feedback ${feedback.requestId}. Do not repeat the invocation.`);
|
|
79
|
+
return confirmedAcknowledgement(parseTransportResponse(response,'IDE feedback acknowledgement').feedback,feedback,ideHeaders?.['x-entrorove-ide']);
|
|
80
|
+
}
|
package/cli.mjs
CHANGED
|
@@ -390,7 +390,7 @@ export function remoteDependencies(home = os.homedir()) {
|
|
|
390
390
|
export async function runCloudTask(input, dependencies = remoteDependencies()) {
|
|
391
391
|
const result = await dispatchCloudTask(input, dependencies);
|
|
392
392
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
393
|
-
if (['blocked', 'failed', 'incomplete'
|
|
393
|
+
if (['blocked', 'failed', 'incomplete'].includes(result.status)) process.exitCode = 2;
|
|
394
394
|
return result;
|
|
395
395
|
}
|
|
396
396
|
|
package/cloud-task-client.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { appendInvocation } from './local-invocations.mjs';
|
|
|
3
3
|
import { parseTransportResponse, requestTransport, trustedOrigin } from './remote-transport.mjs';
|
|
4
4
|
import { displayIdeEvent, ideClientHeaders } from './ide-presentation.mjs';
|
|
5
5
|
import { verifyAutomaticFeedback } from './automatic-feedback-client.mjs';
|
|
6
|
+
import { sandboxTaskNumber } from './task-identity.mjs';
|
|
6
7
|
|
|
7
8
|
const CLOUD_TASK_SCHEMA = 'entrorove.cloud-task/1.0';
|
|
8
9
|
const CLOUD_STATUS_SCHEMA = 'entrorove.cloud-status/1.0';
|
|
@@ -34,7 +35,11 @@ function validateInput(input) {
|
|
|
34
35
|
if ((input.sandboxChoice ?? input.answer) !== undefined && !['use', 'local'].includes(input.sandboxChoice ?? input.answer)) {
|
|
35
36
|
throw new Error('cloud-task sandboxChoice must be use or local');
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
+
if (input.task !== undefined && (!input.task || typeof input.task !== 'object' || Array.isArray(input.task) || input.task.goal !== input.goal)) {
|
|
39
|
+
throw new Error('Cloud task must preserve the original task goal');
|
|
40
|
+
}
|
|
41
|
+
return { goal: input.goal, ...(input.ideTaskId === undefined ? {} : { ideTaskId: input.ideTaskId }),
|
|
42
|
+
...(input.task === undefined ? {} : { task: input.task }) };
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
function validateDependencies(dependencies) {
|
|
@@ -100,7 +105,7 @@ export async function runCloudTask(input, dependencies) {
|
|
|
100
105
|
const metadata = { kind: 'remote', operation: 'cloud-task', requestId: id };
|
|
101
106
|
await appendInvocation({ ...metadata, event: 'started' }, dependencies.home);
|
|
102
107
|
displayIdeEvent({ schemaVersion: 'entrorove.ide-event/1.0',requestId: id,kind: 'status',
|
|
103
|
-
phase: 'enter',message: '正在进入云端沙箱',separateLine: true },dependencies.onIdeEvent);
|
|
108
|
+
phase: 'enter',message: '正在进入云端沙箱',separateLine: true },dependencies.onIdeEvent,{home:dependencies.home});
|
|
104
109
|
let response;
|
|
105
110
|
try {
|
|
106
111
|
response = await requestTransport(origin, '/api/cloud-task', {
|
|
@@ -129,15 +134,21 @@ export async function runCloudTask(input, dependencies) {
|
|
|
129
134
|
const code = typeof result?.error === 'string' ? result.error : 'cloud-task-rejected';
|
|
130
135
|
return envelope(task, response.status === 403 ? 'blocked' : 'failed', { code, message: result?.message ?? code, httpStatus: response.status });
|
|
131
136
|
}
|
|
132
|
-
if (!result?.ok || typeof result.taskId !== 'string' || !UUID.test(result.taskId)
|
|
137
|
+
if (!result?.ok || typeof result.taskId !== 'string' || !UUID.test(result.taskId)
|
|
138
|
+
|| result.taskNumber !== undefined && result.taskNumber !== sandboxTaskNumber(result.taskId)
|
|
139
|
+
|| result.afterId !== undefined && (typeof result.afterId !== 'string' || !/^[1-9][0-9]{0,18}$/u.test(result.afterId) || BigInt(result.afterId)>9223372036854775807n)) {
|
|
133
140
|
await appendInvocation({ ...metadata, event: 'outcome-unknown' }, dependencies.home);
|
|
134
141
|
throw new Error(`Cloud sandbox response lost its task identity; requestId=${id}`);
|
|
135
142
|
}
|
|
136
143
|
await appendInvocation({ ...metadata, event: 'completed' }, dependencies.home);
|
|
137
|
-
|
|
144
|
+
const taskNumber = sandboxTaskNumber(result.taskId);
|
|
145
|
+
const event = { schemaVersion: 'entrorove.ide-event/1.0',requestId: id,taskId: result.taskId,taskNumber,
|
|
146
|
+
kind: 'status',phase: 'accepted',message: `云端任务 ${taskNumber} · 已进入`,separateLine: true };
|
|
147
|
+
displayIdeEvent(event,dependencies.onIdeEvent,{home:dependencies.home});
|
|
148
|
+
return envelope(task, 'started', { taskId: result.taskId,taskNumber,ideMessages: [event.message],
|
|
138
149
|
taskStatus: typeof result.status === 'string' ? result.status : 'running',
|
|
139
150
|
executionIsolation: typeof result.executionIsolation === 'string' ? result.executionIsolation : 'worker',
|
|
140
|
-
nextAction: { operation: 'cloud-watch',input: { taskId: result.taskId,afterId: '0' } } });
|
|
151
|
+
nextAction: { operation: 'cloud-watch',input: { taskId: result.taskId,afterId: result.afterId ?? '0' } } });
|
|
141
152
|
}
|
|
142
153
|
|
|
143
154
|
function publicTask(task) {
|
|
@@ -187,7 +198,7 @@ export async function queryCloudTask(taskId, dependencies) {
|
|
|
187
198
|
else usageError = typeof usageBody?.error === 'string' ? usageBody.error : 'cloud-usage-unavailable';
|
|
188
199
|
} catch { usageError = 'cloud-usage-invalid-response'; }
|
|
189
200
|
await appendInvocation({ ...metadata, event: 'status' }, dependencies.home);
|
|
190
|
-
return { schemaVersion: CLOUD_STATUS_SCHEMA, status: 'observed', taskId: id,
|
|
201
|
+
return { schemaVersion: CLOUD_STATUS_SCHEMA, status: 'observed', taskId: id,taskNumber: sandboxTaskNumber(id),
|
|
191
202
|
taskStatus: task.status, task: publicTask(task), usage, ...(feedback ? { feedback } : {}),
|
|
192
203
|
...(usageError ? { usageError } : {}) };
|
|
193
204
|
}
|
package/cloud-watch-client.mjs
CHANGED
|
@@ -2,6 +2,8 @@ import { trustedOrigin } from './remote-transport.mjs';
|
|
|
2
2
|
import { consumeCloudStream } from './cloud-event-stream.mjs';
|
|
3
3
|
import { automaticFeedbackIdeEvent, cloudIdeEvent, displayIdeEvent, ideClientHeaders } from './ide-presentation.mjs';
|
|
4
4
|
import { queryCloudTask } from './cloud-task-client.mjs';
|
|
5
|
+
import { acknowledgeAutomaticFeedback } from './automatic-feedback-client.mjs';
|
|
6
|
+
import { sandboxTaskNumber } from './task-identity.mjs';
|
|
5
7
|
|
|
6
8
|
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
9
|
function inputOptions(input) {
|
|
@@ -20,7 +22,7 @@ export async function watchCloudTask(input, dependencies) {
|
|
|
20
22
|
interaction: dependencies.agreementRequired(dependencies.home) };
|
|
21
23
|
const token = await dependencies.readCredential(dependencies.home);
|
|
22
24
|
const controller = new AbortController(), deadline = setTimeout(() => controller.abort(),waitMs);
|
|
23
|
-
const events = [], emit = event => { events.push(event); displayIdeEvent(event,dependencies.onIdeEvent); };
|
|
25
|
+
const events = [], emit = event => { events.push(event); displayIdeEvent(event,dependencies.onIdeEvent,{home:dependencies.home}); };
|
|
24
26
|
let afterId = options.afterId, taskStatus = 'running', outputAvailable = false, settled = false, flowId;
|
|
25
27
|
try {
|
|
26
28
|
const response = await fetch(`${trustedOrigin(dependencies.apiOrigin())}/api/cloud-sandbox/tasks/${taskId}/events`, {
|
|
@@ -49,10 +51,13 @@ export async function watchCloudTask(input, dependencies) {
|
|
|
49
51
|
message: '云端沙箱输出完毕;实际验证状态以任务回执为准。',separateLine: true });
|
|
50
52
|
const current = await queryCloudTask(taskId,dependencies);
|
|
51
53
|
if (current.status === 'observed') taskStatus = current.taskStatus;
|
|
52
|
-
if (current.feedback)
|
|
54
|
+
if (current.feedback) {
|
|
55
|
+
current.feedback = await acknowledgeAutomaticFeedback(current.feedback,dependencies.apiOrigin(),token,ideClientHeaders());
|
|
56
|
+
emit({ ...automaticFeedbackIdeEvent(current.feedback),taskId,taskNumber:sandboxTaskNumber(taskId) });
|
|
57
|
+
}
|
|
53
58
|
emit({ schemaVersion: 'entrorove.ide-event/1.0',taskId,kind: 'status',phase: 'exit',
|
|
54
59
|
message: settled || taskStatus !== 'running' ? '云端沙箱正在退出本次进度订阅' : '云端沙箱仍在执行,下次从当前进度继续读取',separateLine: true });
|
|
55
|
-
return { schemaVersion: 'entrorove.cloud-watch/1.0',status: current.status,taskId,taskStatus,afterId,outputAvailable,
|
|
60
|
+
return { schemaVersion: 'entrorove.cloud-watch/1.0',status: current.status,taskId,taskNumber: sandboxTaskNumber(taskId),taskStatus,afterId,outputAvailable,
|
|
56
61
|
events,...(flowId ? { flowId } : {}),task: current.task,usage: current.usage,...(current.feedback ? { feedback: current.feedback } : {}),
|
|
57
62
|
nextAction: taskStatus === 'running' && !settled ? { operation: 'cloud-watch',input: { taskId,afterId,waitMs } } : null };
|
|
58
63
|
}
|
package/ide-presentation.mjs
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
const UUID = /^[0-9a-f-]{36}$/i;
|
|
2
2
|
import { notifyIdeEvent } from './native-notification.mjs';
|
|
3
|
+
import { sandboxTaskNumber } from './task-identity.mjs';
|
|
3
4
|
export function ideClientHeaders(environment = process.env) {
|
|
4
5
|
const ide = environment.ENTROROVE_IDE ?? (environment.CODEX_THREAD_ID ? 'Codex'
|
|
5
6
|
: environment.CURSOR_TRACE_ID ? 'Cursor' : environment.CLAUDE_CODE_ENTRYPOINT ? 'Claude Code' : 'Entrorove CLI');
|
|
6
7
|
if (typeof ide !== 'string' || !/^[\p{L}\p{N} .+_-]{2,48}$/u.test(ide)) throw new Error('Invalid ENTROROVE_IDE client name');
|
|
7
8
|
return { 'x-entrorove-ide': ide };
|
|
8
9
|
}
|
|
9
|
-
export function displayIdeEvent(event, sink) {
|
|
10
|
+
export function displayIdeEvent(event, sink, notifications) {
|
|
10
11
|
if (sink) sink(event);
|
|
11
12
|
else process.stderr.write(`${event.message}\n${event.detail ? event.detail + '\n' : ''}`);
|
|
12
|
-
void notifyIdeEvent(event).catch(() => process.stderr.write('Entrorove system notification unavailable; IDE progress remains visible.\n'));
|
|
13
|
+
void notifyIdeEvent(event,notifications).catch(() => process.stderr.write('Entrorove system notification unavailable; IDE progress remains visible.\n'));
|
|
13
14
|
}
|
|
14
15
|
function text(value) {
|
|
15
16
|
return typeof value === 'string' ? value.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/gu, '').slice(0,12000) : '';
|
|
@@ -21,13 +22,14 @@ export function cloudIdeEvent(value, taskId) {
|
|
|
21
22
|
}
|
|
22
23
|
const metadata = value.metadata ?? {};
|
|
23
24
|
let message = text(value.content), phase = String(metadata.stepId ?? 'workflow');
|
|
24
|
-
if (message === 'Cloud task accepted.') message = '
|
|
25
|
+
if (message === 'Cloud task accepted.') { message = `云端任务 ${sandboxTaskNumber(taskId)} · 已进入`; phase = 'accepted'; }
|
|
25
26
|
else if (message === 'Cloud workflow started.') message = '云端沙箱任务已分配';
|
|
26
27
|
else if (message === 'Cloud model classification started.') message = '云端沙箱正在思考中';
|
|
27
28
|
else if (message === 'Task classification completed.') message = '云端沙箱思考正在输出';
|
|
28
|
-
const event = { schemaVersion: 'entrorove.ide-event/1.0',taskId,eventId: value.id,kind: value.kind,
|
|
29
|
+
const event = { schemaVersion: 'entrorove.ide-event/1.0',taskId,taskNumber: sandboxTaskNumber(taskId),eventId: value.id,kind: value.kind,
|
|
29
30
|
state: metadata.state ?? 'running',phase,message,separateLine: true };
|
|
30
31
|
if (metadata.requiresUserInput === true || phase === 'confirm' && event.state === 'pending') event.requiresConfirmation = true;
|
|
32
|
+
if (metadata.errorCode === 'model-balance-insufficient') event.errorCode = metadata.errorCode;
|
|
31
33
|
if (typeof metadata.summary === 'string' && metadata.summary !== value.content
|
|
32
34
|
&& value.content !== 'Cloud model classification started.') event.detail = text(metadata.summary);
|
|
33
35
|
if (value.kind === 'notice' && metadata.url !== undefined) {
|
|
@@ -45,13 +47,13 @@ export function invocationIdeNotices(output) {
|
|
|
45
47
|
return [...new Set(strings.filter(value => typeof value === 'string').flatMap(value => value.split(/\r?\n/u))
|
|
46
48
|
.filter(value => value === '智能执行中,请勿关闭!' || /^【[^\n】]{1,32}】/u.test(value)).map(text))];
|
|
47
49
|
}
|
|
48
|
-
export function presentInvocationNotices(result, sink) {
|
|
50
|
+
export function presentInvocationNotices(result, sink, notifications) {
|
|
49
51
|
const messages = invocationIdeNotices(result?.output);
|
|
50
52
|
for (const message of messages) displayIdeEvent({ schemaVersion: 'entrorove.ide-event/1.0',
|
|
51
|
-
requestId: result.output.requestId,kind: 'notice',message,separateLine: true },sink);
|
|
53
|
+
requestId: result.output.requestId,kind: 'notice',message,separateLine: true },sink,notifications);
|
|
52
54
|
if (result?.feedback) {
|
|
53
55
|
const event = automaticFeedbackIdeEvent(result.feedback);
|
|
54
|
-
messages.push(event.message); displayIdeEvent(event,sink);
|
|
56
|
+
messages.push(event.message); displayIdeEvent(event,sink,notifications);
|
|
55
57
|
}
|
|
56
58
|
return messages.length ? { ...result,ideMessages: messages } : result;
|
|
57
59
|
}
|
|
@@ -60,5 +62,6 @@ export function automaticFeedbackIdeEvent(feedback) {
|
|
|
60
62
|
const status = disposition === 'good' ? '当前调用验证通过' : disposition === 'bad'
|
|
61
63
|
? '当前调用被阻断或验证失败' : '交付验证尚未完成,暂不评分';
|
|
62
64
|
return { schemaVersion: 'entrorove.ide-event/1.0',requestId: feedback.requestId,receiptId: feedback.receiptId,
|
|
63
|
-
kind: 'evaluation',phase: 'feedback',disposition,
|
|
65
|
+
kind: 'evaluation',phase: 'feedback',disposition,
|
|
66
|
+
message: `${feedback.acknowledgement?.stored === true ? '本次调用评价已回传至服务器' : '服务器已保存本次调用评价'}:${status}。`,separateLine: true };
|
|
64
67
|
}
|
package/local-error.mjs
CHANGED
|
@@ -8,9 +8,11 @@ export const LOCAL_ERROR_CODE_CATALOG = Object.freeze({
|
|
|
8
8
|
ENTROROVE_MUTATION_AUTHORITY_REQUIRED: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
9
9
|
ENTROROVE_BUDGET_LEASE_EXPIRED: Object.freeze({ disposition: 'retriable', stability: 'stable' }),
|
|
10
10
|
ENTROROVE_GIT_READER_UNSUPPORTED: Object.freeze({ disposition: 'terminal', stability: 'stable' }),
|
|
11
|
+
ENTROROVE_GIT_UNAVAILABLE: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
11
12
|
});
|
|
12
13
|
|
|
13
14
|
const BLOCKED_CODES = new Set([
|
|
15
|
+
'ENTROROVE_GIT_UNAVAILABLE',
|
|
14
16
|
'ENTROROVE_MUTATE_NOT_ACCEPTED', 'ENTROROVE_MUTATION_AUTHORITY_REQUIRED',
|
|
15
17
|
'ENTROROVE_MUTATION_PASS_INVALID', 'ENTROROVE_MUTATION_PASS_EXPIRED',
|
|
16
18
|
'ENTROROVE_BUDGET_LEASE_EXPIRED', 'ENTROROVE_BUDGET_AUTHORITY_REQUIRED',
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fail } from './local-budget-grants.mjs';
|
|
4
|
+
|
|
5
|
+
export function scopeGitCandidates(platform, environment) {
|
|
6
|
+
if (['darwin','linux'].includes(platform)) return ['/usr/bin/git'];
|
|
7
|
+
if (platform !== 'win32') fail('ENTROROVE_GIT_READER_UNSUPPORTED', 'Git scope inspection does not support this operating system');
|
|
8
|
+
const entries = (environment.PATH ?? environment.Path ?? '').split(';')
|
|
9
|
+
.map(value => value.trim().replace(/^"(.*)"$/u,'$1')).filter(value => path.win32.isAbsolute(value));
|
|
10
|
+
const installed = [environment.ProgramFiles,environment['ProgramFiles(x86)']]
|
|
11
|
+
.filter(value => typeof value === 'string' && path.win32.isAbsolute(value))
|
|
12
|
+
.map(value => path.win32.join(value,'Git','cmd'));
|
|
13
|
+
return [...new Set([...installed,...entries].map(value => path.win32.join(value,'git.exe')))];
|
|
14
|
+
}
|
|
15
|
+
/** Resolve an absolute executable, never a shell command or a repository-local PATH entry. */
|
|
16
|
+
export async function scopeGitExecutable({ platform = process.platform, environment = process.env, files = fs, repositoryRoot } = {}) {
|
|
17
|
+
for (const target of scopeGitCandidates(platform,environment)) {
|
|
18
|
+
try {
|
|
19
|
+
const stat = await files.lstat(target);
|
|
20
|
+
if (!stat.isFile() || stat.isSymbolicLink()) continue;
|
|
21
|
+
const actual = await files.realpath(target);
|
|
22
|
+
if (platform === 'win32' && !actual.toLowerCase().endsWith('\\git.exe')) continue;
|
|
23
|
+
if (platform === 'win32' && repositoryRoot) {
|
|
24
|
+
const relative = path.win32.relative(repositoryRoot,actual);
|
|
25
|
+
if (!path.win32.isAbsolute(relative) && relative !== '..' && !relative.startsWith('..\\')) continue;
|
|
26
|
+
}
|
|
27
|
+
return actual;
|
|
28
|
+
} catch (cause) { if (!['ENOENT','ENOTDIR'].includes(cause.code)) throw cause; }
|
|
29
|
+
}
|
|
30
|
+
fail('ENTROROVE_GIT_UNAVAILABLE', platform === 'win32'
|
|
31
|
+
? 'Install Git for Windows and add its cmd directory to PATH before reconnecting this original scope.'
|
|
32
|
+
: 'The fixed system Git executable is unavailable; install the operating system Git tools before reconnecting this original scope.');
|
|
33
|
+
}
|
|
@@ -5,6 +5,7 @@ import { promisify } from 'node:util';
|
|
|
5
5
|
import { assertBudgetReadContext } from './local-budget-reads.mjs';
|
|
6
6
|
import { readBudgetedGitBlob } from './local-budget-git.mjs';
|
|
7
7
|
import { scopeFail, scopePath } from './local-scope-files.mjs';
|
|
8
|
+
import { scopeGitExecutable } from './local-git-executable.mjs';
|
|
8
9
|
|
|
9
10
|
const execute = promisify(execFile), objectIdPattern = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
10
11
|
const configFields = new Set(['core.attributesfile', 'core.filemode', 'core.autocrlf', 'core.eol', 'diff.algorithm', 'diff.indentheuristic', 'diff.renames', 'diff.renamelimit']);
|
|
@@ -41,7 +42,8 @@ const safeFlags = ['--no-optional-locks', '-c', 'core.hooksPath=/dev/null', '-c'
|
|
|
41
42
|
export async function scopeGitMetadata(root, args, input, allowed = []) {
|
|
42
43
|
await assertBudgetReadContext(input);
|
|
43
44
|
if (!metadataCommand(args)) scopeFail('ENTROROVE_GIT_FAILED', 'Git metadata command is outside the fixed scope inspection vocabulary');
|
|
44
|
-
|
|
45
|
+
const executable = await scopeGitExecutable({ repositoryRoot: root });
|
|
46
|
+
try { return (await execute(executable, [...safeFlags, ...args], settings(root, null, 2097152, 'utf8'))).stdout; }
|
|
45
47
|
catch (error) {
|
|
46
48
|
if (error.code === 129 && args.length === 2 && args[0] === 'var' && args[1] === 'GIT_ATTR_SYSTEM') {
|
|
47
49
|
return `${await compiledGitSystemAttributes()}\n`;
|
|
@@ -12,12 +12,14 @@ import { assertBudgetRunnerAllowed } from './local-budget-reads.mjs';
|
|
|
12
12
|
import { observeBudgetScopeGit, scopeGitBudgetEnforced } from './local-scope-git.mjs';
|
|
13
13
|
import { scopeGitMetadata } from './local-scope-git-reads.mjs';
|
|
14
14
|
import { scopeTsconfigImports } from './local-scope-config-imports.mjs';
|
|
15
|
+
import { scopeGitExecutable } from './local-git-executable.mjs';
|
|
15
16
|
|
|
16
17
|
const execute = promisify(execFile);
|
|
17
18
|
async function git(root, args, allowed = [], input = { repositoryRoot: root }) {
|
|
18
19
|
await assertBudgetRunnerAllowed(input);
|
|
20
|
+
const executable = await scopeGitExecutable({ repositoryRoot: root });
|
|
19
21
|
try {
|
|
20
|
-
return (await execute(
|
|
22
|
+
return (await execute(executable, ['--no-optional-locks', '-c', 'core.hooksPath=/dev/null', '-c', 'core.fsmonitor=false', '-c', 'diff.external=', ...args],
|
|
21
23
|
{ cwd: root, encoding: 'utf8', timeout: 10_000, maxBuffer: 2_097_152,
|
|
22
24
|
env: { ...Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('GIT_'))), GIT_OPTIONAL_LOCKS: '0' } })).stdout;
|
|
23
25
|
} catch (error) {
|
package/manifest.json
CHANGED
package/native-client.mjs
CHANGED
|
@@ -41,7 +41,10 @@ export async function invokeNativeResult(domain, operation, input, dependencies)
|
|
|
41
41
|
const found = discovery.result.output;
|
|
42
42
|
const extension = (found.capabilities ?? found).responseExtensions;
|
|
43
43
|
if (extension?.automaticFeedback === 'entrorove.automatic-feedback/1.0') feedbackRequired = !extension.automaticFeedbackObservations.includes(operation);
|
|
44
|
-
|
|
44
|
+
// JSON Schema permits object keywords without an adjacent type keyword.
|
|
45
|
+
// Validate their actual semantics rather than rejecting the published
|
|
46
|
+
// contract through Ajv's optional strictTypes authoring convention.
|
|
47
|
+
const ajv = new Ajv({ strict: true, strictRequired: false, strictTypes: false, allErrors: true });
|
|
45
48
|
ajv.addKeyword({ keyword: 'optional', schemaType: 'boolean', valid: true });
|
|
46
49
|
ajv.addFormat('uuid', /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
|
|
47
50
|
ajv.addFormat('date-time', value => Number.isFinite(Date.parse(value)));
|
|
@@ -52,7 +55,7 @@ export async function invokeNativeResult(domain, operation, input, dependencies)
|
|
|
52
55
|
}
|
|
53
56
|
export async function runNative(domain, operation, input, dependencies) {
|
|
54
57
|
const { response, result } = await invokeNativeResult(domain, operation, input, dependencies);
|
|
55
|
-
process.stdout.write(`${JSON.stringify(presentInvocationNotices(result, dependencies.onIdeEvent))}\n`);
|
|
58
|
+
process.stdout.write(`${JSON.stringify(presentInvocationNotices(result, dependencies.onIdeEvent,{home:dependencies.home}))}\n`);
|
|
56
59
|
if (!response.ok || ['blocked', 'failed', 'incomplete'].includes(result.output.status)) process.exitCode = 2;
|
|
57
60
|
return result;
|
|
58
61
|
}
|
package/native-notification.mjs
CHANGED
|
@@ -5,10 +5,13 @@ import os from 'node:os';
|
|
|
5
5
|
import { sha256 } from './local-integrity.mjs';
|
|
6
6
|
import { readCredential } from './local-credential.mjs';
|
|
7
7
|
import { withStateLock } from './local-state-lock.mjs';
|
|
8
|
+
import { sandboxTaskNumber } from './task-identity.mjs';
|
|
8
9
|
|
|
9
10
|
const messageKinds = { confirmed: '任务需要你确认。', failed: '任务执行失败,请查看 IDE 中的状态。',
|
|
10
11
|
blocked: '任务已阻断,请查看 IDE 中的所需操作。',
|
|
11
|
-
reported: '任务输出已生成,请查看 IDE 中的结果。', verified: '任务验证已完成。',
|
|
12
|
+
reported: '任务输出已生成,请查看 IDE 中的结果。', verified: '任务验证已完成。',
|
|
13
|
+
entered: '沙箱已进入。', balance: '模型余额不足。', test: 'Entrorove 系统通知测试。' };
|
|
14
|
+
const shortStates = { entered:'已进入',reported:'待验收',verified:'已完成',confirmed:'待确认',failed:'执行失败',blocked:'已阻断',balance:'余额不足' };
|
|
12
15
|
function controls(home) { return path.join(home, '.entrorove', 'notifications.json'); }
|
|
13
16
|
async function readControls(home) {
|
|
14
17
|
const file = controls(home);
|
|
@@ -42,8 +45,9 @@ export async function configureNativeNotifications(input, home = os.homedir()) {
|
|
|
42
45
|
await withStateLock(controls(home), () => saveControls(home, state));
|
|
43
46
|
return notificationSettings(home);
|
|
44
47
|
}
|
|
45
|
-
export async function sendNativeNotification(kind, { platform = process.platform, execute = execFile } = {}) {
|
|
46
|
-
const message =
|
|
48
|
+
export async function sendNativeNotification(kind, { platform = process.platform, execute = execFile, taskNumber } = {}) {
|
|
49
|
+
const message = /^\d{5}$/u.test(taskNumber ?? '') && shortStates[kind]
|
|
50
|
+
? `沙箱 ${taskNumber} · ${shortStates[kind]}` : messageKinds[kind];
|
|
47
51
|
if (!message) throw new Error('Invalid notification kind');
|
|
48
52
|
let file, args;
|
|
49
53
|
if (platform === 'darwin') {
|
|
@@ -58,7 +62,9 @@ export async function sendNativeNotification(kind, { platform = process.platform
|
|
|
58
62
|
return { submitted: true, displayVerified: false };
|
|
59
63
|
}
|
|
60
64
|
export async function notifyIdeEvent(event, { home = os.homedir(), send = sendNativeNotification } = {}) {
|
|
61
|
-
const kind = event.
|
|
65
|
+
const kind = event.taskId && event.phase === 'accepted' ? 'entered'
|
|
66
|
+
: event.errorCode === 'model-balance-insufficient' ? 'balance'
|
|
67
|
+
: event.requiresConfirmation && event.kind !== 'notice' ? 'confirmed'
|
|
62
68
|
: event.kind === 'evaluation' ? event.disposition === 'bad' ? 'failed' : null
|
|
63
69
|
: event.state === 'blocked' ? event.requiresConfirmation ? 'confirmed' : 'blocked'
|
|
64
70
|
: event.phase === 'workflow' && ['reported', 'verified', 'failed'].includes(event.state) ? event.state : null;
|
|
@@ -68,9 +74,10 @@ export async function notifyIdeEvent(event, { home = os.homedir(), send = sendNa
|
|
|
68
74
|
return withStateLock(controls(home), async () => {
|
|
69
75
|
const state = await readControls(home);
|
|
70
76
|
if (!state?.enabled || state.accountBinding !== sha256(await readCredential(home))) return { submitted: false };
|
|
71
|
-
const key = sha256(`${id}:${kind}`);
|
|
77
|
+
const key = sha256(`${id}:${event.taskId ? kind === 'entered' ? 'start' : 'end' : kind}`);
|
|
72
78
|
if (state.seen.includes(key)) return { submitted: false, duplicate: true };
|
|
79
|
+
const result = await send(kind,event.taskId ? { taskNumber:sandboxTaskNumber(event.taskId) } : {});
|
|
73
80
|
state.seen = [...state.seen.slice(-99), key]; await saveControls(home, state);
|
|
74
|
-
return
|
|
81
|
+
return result;
|
|
75
82
|
});
|
|
76
83
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entrorove/skill",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.37",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"entrorove": "cli.mjs"
|
|
@@ -79,6 +79,9 @@
|
|
|
79
79
|
"remote-client.mjs",
|
|
80
80
|
"scope-runner-client.mjs",
|
|
81
81
|
"cloud-task-client.mjs",
|
|
82
|
+
"task-entry-client.mjs",
|
|
83
|
+
"task-identity.mjs",
|
|
84
|
+
"task-identity.d.mts",
|
|
82
85
|
"cloud-watch-client.mjs",
|
|
83
86
|
"cloud-event-stream.mjs",
|
|
84
87
|
"ide-presentation.mjs",
|
|
@@ -134,6 +137,7 @@
|
|
|
134
137
|
"local-scope-workspaces.mjs",
|
|
135
138
|
"local-scope-git.mjs",
|
|
136
139
|
"local-scope-git-reads.mjs",
|
|
140
|
+
"local-git-executable.mjs",
|
|
137
141
|
"local-scope-git-comparison.mjs",
|
|
138
142
|
"local-scope-git-attributes.mjs",
|
|
139
143
|
"local-scope-git-history.mjs",
|
package/release-state.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "entrorove.release-state/1.0",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"digest": "
|
|
3
|
+
"version": "0.1.37",
|
|
4
|
+
"digest": "7b17a634819efd17e06f32525d69cab7d0ef2a7c35ce9c53f9e8a3c8283710ee",
|
|
5
5
|
"files": {
|
|
6
6
|
"local-final-gate16.mjs": "b30caab08441449ad556a6e9a3cefbf91190f0b3f7370f46d2a21156375aa8e3",
|
|
7
7
|
"local-final-gate16-checks.mjs": "1dd30e9666d458d8b12ca6e4975f6f0ac8450c3ea9cbecc7401ae037f66b589a",
|
|
@@ -44,15 +44,15 @@
|
|
|
44
44
|
"local-autocoord17-waits.mjs": "2b0a713987822548a94e1ed3a9a891532db45383797dfc5d5bc8d1e35593cf25",
|
|
45
45
|
"local-validator-isolation17.mjs": "48e94604e28b8fddd567dfb35ff60b73fd094a5803bd49f076388b7f310f92aa",
|
|
46
46
|
"local-calctool-composition17.mjs": "ac89fcb41ab0d0d9b47c1b5696945281b0894eb064820775e36a011c76314e6a",
|
|
47
|
-
"cli.mjs": "
|
|
47
|
+
"cli.mjs": "6ecb24fe53e7aa5412e38028921246e8a1f7128a2c5931a6fcd80aa1153ca358",
|
|
48
48
|
"review-client.mjs": "3491eef68ed533174b31db3c53febb6519b8f088f71a9964644714a9a4c03c77",
|
|
49
49
|
"version.mjs": "1f392147d72c40af4b03b307ea607075fb815af5174fa07e254b95fe12dbf3e7",
|
|
50
|
-
"SKILL.md": "
|
|
51
|
-
"CAPABILITY-MAP.md": "
|
|
50
|
+
"SKILL.md": "eba3ac28270ff590d2602ec057d1e0a6f5dcb3921f53367d6dee588d9b3e9265",
|
|
51
|
+
"CAPABILITY-MAP.md": "40755edd3c698bbbbd4f49bc47a21c5ad9bbb8eb05e9542dd7d3a11a0254ea35",
|
|
52
52
|
"AGREEMENT.md": "f93a68e92c457200dd024610e65f74e478b8f60860f951da818b70561f1b9bf8",
|
|
53
|
-
"package.json": "
|
|
53
|
+
"package.json": "0a106634b48e5c1174fac08345bbb8f324e65a793ac4ba88fb48fe132f503a4a",
|
|
54
54
|
"manifest.json": "ab06e307795bc9da5add926752b6c4dbdaf18625090261a035aa356a679c504f",
|
|
55
|
-
"release.mjs": "
|
|
55
|
+
"release.mjs": "6b6726525ec567d7e69b7ed29dbd205b63a2a018b92766e1846e085c82eec1e6",
|
|
56
56
|
"local-cli.mjs": "9eca8aefcb086cdce792105f9558cc26e34a0c88befaf0a56f4c7bdbd35f0a05",
|
|
57
57
|
"local-state-lock.mjs": "1dbf64264642a0851faf043e07ee9f66d2ad0508e9f799b0626246e42fae893e",
|
|
58
58
|
"local-execution.mjs": "cdada6df6dc8831cebb58a629b56c4b3f8e1bf9aae4e3fdc121521ba20839383",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"local-evidence-verification.mjs": "9b7dea31777505272f63616fd62af7603338f703bb1285fc565d6451715c7ce0",
|
|
61
61
|
"local-formula.mjs": "706d5ec0cb2caf95a547531a81ca746fe246d4c53d902e03595a3b360e8c85af",
|
|
62
62
|
"local-ledger.mjs": "2009fb68aadcd7292b72f1d0d99f378d03644be1c5b4b4150d9f6a0b3f7c15ce",
|
|
63
|
-
"local-error.mjs": "
|
|
63
|
+
"local-error.mjs": "29845eec009fa946b90a0c08367ccda2408949ad5fbd2c250e408b541a5dc7d0",
|
|
64
64
|
"blueprint-criterion-wire.mjs": "fb6b7dcce0b97a699e3e8968372767a3fddd83fedc750998573f789177243072",
|
|
65
65
|
"blueprint-criterion-runner.mjs": "761c0e670b4b20c6bed53e5ca5d0178759505c3977ea2ed1b7223383c534bc4c",
|
|
66
66
|
"local-chain-bindings.mjs": "b8eff999ccd17b53c614a3c9f5376da78fcaa8632bb3e07bc1adcceda2e75f6c",
|
|
@@ -89,9 +89,10 @@
|
|
|
89
89
|
"local-scope-candidate.mjs": "8f0bd9606e70d4a0ebacdd3cd600ec3afed85e2bb3e1760aa1144cf55d4e028a",
|
|
90
90
|
"local-scope-context.mjs": "79753abfc6c76089d17f003a1be537dfaea10d6731c542f7fc53f2a9996ed164",
|
|
91
91
|
"local-scope-files.mjs": "0671ac4baef610a55ea6c1db6ebca7a4b6cfda45edb8aeaa828019e2a26f5a76",
|
|
92
|
-
"local-scope-repository.mjs": "
|
|
92
|
+
"local-scope-repository.mjs": "b6f3f9923e040bfa3d5f48f447431df1cf9bbda02c967b3d9378863c8fd44733",
|
|
93
93
|
"local-scope-git.mjs": "53bbbb6c4795db711b5bc164591a6ad79a561427da046d6bbe997dae6eb82829",
|
|
94
|
-
"local-scope-git-reads.mjs": "
|
|
94
|
+
"local-scope-git-reads.mjs": "5d8a2636bab955cb325cb693db7423e69512355887ae5b5af3b8cb615f5ddad0",
|
|
95
|
+
"local-git-executable.mjs": "9391d716529025b1de3ee006df658a9d765b6ef29ad24ef9b7436829318e651e",
|
|
95
96
|
"local-scope-git-comparison.mjs": "d86e559843087ae851795c9fb16b844dc46cc461fb5da37dbca0f6ba0d06450b",
|
|
96
97
|
"local-scope-git-attributes.mjs": "63c37db40a6789a5dd98ca431b24d9d118ba3d808b330785b88c7f9037e40518",
|
|
97
98
|
"local-scope-git-history.mjs": "47d2ee6baa0b1d3d5f0b7a1d5bb9e23c1fbd6cf2884328458448f45da5013483",
|
|
@@ -118,17 +119,20 @@
|
|
|
118
119
|
"local-scope-state.mjs": "8b600a91fc0068d7a8c0bb752ddba7ed30ada93daafb118d6cba2368fd94cb27",
|
|
119
120
|
"local-scope-workspaces.mjs": "a44c34537e5ed2dcaf193a985e5bc05957989fbb160a650f9e37ba7d66eb2de8",
|
|
120
121
|
"local-task-routing.mjs": "e26d6f1ba134ffe1da2a49b4fb6bb6dfa5617671f774a08e719d04f202cf5eca",
|
|
121
|
-
"remote-client.mjs": "
|
|
122
|
-
"cloud-task-client.mjs": "
|
|
122
|
+
"remote-client.mjs": "62a4ddf8422d279bdb568d9800dbf2bd8e3477556188d5a0ef04091a846ccf69",
|
|
123
|
+
"cloud-task-client.mjs": "77ced16a6f5dc3f08ef149da999fb488e62bb64964d448efa821f675f67cbdd7",
|
|
123
124
|
"scope-runner-client.mjs": "646ab4f80d4a8209e922fd45f40efd968f0744d9dfc03188b222c1a2e11e83b8",
|
|
124
|
-
"native-client.mjs": "
|
|
125
|
-
"native-notification.mjs": "
|
|
125
|
+
"native-client.mjs": "6f5575909bae9daba643287f876ffa0f9ff1c53a367b32a40145d740456d6605",
|
|
126
|
+
"native-notification.mjs": "8859a26c6f13d222711f763acf49ed8546b3306cf71f990643ac31a5ccaf1ba1",
|
|
126
127
|
"local-peer-status.mjs": "a971bcc69d60bb32b875ca5bb4297d948631d5d2821b812c279613ea059a8b4f",
|
|
127
|
-
"cloud-watch-client.mjs": "
|
|
128
|
+
"cloud-watch-client.mjs": "5b93935c19a104d72afed07d4a56746c9617707e075884b93d22050c95cee0c1",
|
|
128
129
|
"cloud-event-stream.mjs": "e0eec9fc3e0613094523929634f25030df5ca5c5b5fe2c1e1a4586ab10574808",
|
|
129
|
-
"ide-presentation.mjs": "
|
|
130
|
-
"automatic-feedback-client.mjs": "
|
|
131
|
-
"
|
|
130
|
+
"ide-presentation.mjs": "60a4d791790a767cec9666bf8734961a20a2db59146e74bafc090740b9f169c1",
|
|
131
|
+
"automatic-feedback-client.mjs": "75e92f50d6fbfa4f482af36e16c9ce5dd323a02e99eb9b434dcfa3faa846a532",
|
|
132
|
+
"task-entry-client.mjs": "36473ff61b869b8a8ba0eef1fc00e565b894dc54c92182d14e906276a6a03f9e",
|
|
133
|
+
"task-identity.mjs": "e0e6cf1f48bfa5ee3c5cb86a3c21adfce6d66b4220f4a164ca099263c7a09fb4",
|
|
134
|
+
"task-identity.d.mts": "55472e1ffc81b9d5e5c5bd56c0bf1d18ecde03ee5170a58a15ce9caf222d8761",
|
|
135
|
+
"remote-transport.mjs": "93caaca646fca298a4e08142678b134ecc3a657e00de5b9939e2364b2db9d6c4",
|
|
132
136
|
"local-credential.mjs": "2a302a7f67b5ea8dc73453fc96ab33db349b59ca11fa9456e48ca1d9669d877c",
|
|
133
137
|
"local-invocations.mjs": "f6ac4c41c1a5ec34de358c4cca210ca8e403b3a913150a43bb55ec9be55d9998",
|
|
134
138
|
"local-task-handoff-fence.mjs": "5532a359ee29b885f7ce0cf7178f76dba0fc9245bdd92ef460982275baf0d557",
|
package/release.mjs
CHANGED
|
@@ -65,6 +65,7 @@ const sourceFiles = [
|
|
|
65
65
|
'local-scope-repository.mjs',
|
|
66
66
|
'local-scope-git.mjs',
|
|
67
67
|
'local-scope-git-reads.mjs',
|
|
68
|
+
'local-git-executable.mjs',
|
|
68
69
|
'local-scope-git-comparison.mjs',
|
|
69
70
|
'local-scope-git-attributes.mjs',
|
|
70
71
|
'local-scope-git-history.mjs',
|
|
@@ -94,6 +95,7 @@ const sourceFiles = [
|
|
|
94
95
|
'scope-runner-client.mjs',
|
|
95
96
|
'native-client.mjs', 'native-notification.mjs', 'local-peer-status.mjs',
|
|
96
97
|
'cloud-watch-client.mjs', 'cloud-event-stream.mjs', 'ide-presentation.mjs', 'automatic-feedback-client.mjs',
|
|
98
|
+
'task-entry-client.mjs', 'task-identity.mjs', 'task-identity.d.mts',
|
|
97
99
|
'remote-transport.mjs', 'local-credential.mjs', 'local-invocations.mjs',
|
|
98
100
|
'local-task-handoff-fence.mjs',
|
|
99
101
|
'local-task-handoff-storage.mjs', 'local-task-handoff-overlay-wire.mjs', 'local-task-handoff-completion-wire.mjs',
|
|
@@ -251,6 +253,12 @@ const invocationReleaseLabels = {
|
|
|
251
253
|
'local-credential.mjs': { zh: '唯一私有凭据与原子轮换', en: 'Single private credential and atomic rotation' },
|
|
252
254
|
'local-invocations.mjs': { zh: '无明文调用台账与用量对账', en: 'Metadata invocation ledger and usage reconciliation' },
|
|
253
255
|
}
|
|
256
|
+
const taskEntryReleaseLabels = {
|
|
257
|
+
'task-entry-client.mjs': { zh: '账号沙箱偏好与任务入口确认', en: 'account sandbox preferences and task entry confirmation' },
|
|
258
|
+
'task-identity.mjs': { zh: 'IDE与网页统一任务编号', en: 'shared IDE and website task numbers' },
|
|
259
|
+
'task-identity.d.mts': { zh: '任务编号接口', en: 'task number interface' },
|
|
260
|
+
'local-git-executable.mjs': { zh: 'Windows Git范围检查兼容', en: 'Windows Git scope inspection compatibility' },
|
|
261
|
+
}
|
|
254
262
|
|
|
255
263
|
function releaseNote(changed) {
|
|
256
264
|
const suppliedZh = process.env.ENTROROVE_RELEASE_NOTE_ZH?.trim()
|
|
@@ -269,7 +277,7 @@ function releaseNote(changed) {
|
|
|
269
277
|
return { zh: `更新了${distinct.map(label => label.zh).join('、')}。`, en: `Updated ${distinct.map(label => label.en).join(', ')}.` }
|
|
270
278
|
}
|
|
271
279
|
function releaseLabels() {
|
|
272
|
-
return withFinalGate16ReleaseLabels({ ...calctoolReleaseLabels, ...mutationReleaseLabels, ...swarmReleaseLabels, ...coordination17ReleaseLabels, ...nativeReadReleaseLabels, ...feedbackReleaseLabels, ...invocationReleaseLabels, ...chainReleaseLabels,
|
|
280
|
+
return withFinalGate16ReleaseLabels({ ...calctoolReleaseLabels, ...mutationReleaseLabels, ...swarmReleaseLabels, ...coordination17ReleaseLabels, ...nativeReadReleaseLabels, ...feedbackReleaseLabels, ...invocationReleaseLabels, ...chainReleaseLabels, ...taskEntryReleaseLabels,
|
|
273
281
|
'blueprint-criterion-wire.mjs': { zh: '蓝图验收标准执行授权', en: 'Blueprint criterion execution authority' },
|
|
274
282
|
'blueprint-criterion-runner.mjs': { zh: '蓝图验收标准实际执行器', en: 'Blueprint criterion process runner' },
|
|
275
283
|
'local-archguard-ast.mjs': { zh: 'Archguard 架构合同与执行守卫', en: 'Archguard architecture contract and execution guard' },
|
package/releases.json
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.1.37",
|
|
4
|
+
"date": "2026-10-13",
|
|
5
|
+
"zh": "统一 IDE 与网页沙箱任务编号;新任务按账号设置显示云端选择并保留原任务数据,进入及结束通知去重且发送失败可重试;适配 Windows Git 范围元数据检查。",
|
|
6
|
+
"en": "Unified IDE and website sandbox task numbers; added account-based entry choices while preserving original task facts, deduplicated lifecycle notifications with retryable OS rejection, and adapted Git scope metadata inspection for Windows."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.1.36",
|
|
10
|
+
"date": "2026-10-13",
|
|
11
|
+
"zh": "技能调用结束立即确认 IDE 评价回传,提供原账号、原调用与执行代次绑定的接收回执;完善原回执只读恢复与公开合同校验。",
|
|
12
|
+
"en": "Confirmed IDE evaluation acknowledgements immediately after skill calls, bound reception receipts to the original account, invocation and generation, and improved read-only recovery and published contract validation."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.1.35",
|
|
4
16
|
"date": "2026-10-13",
|
package/remote-client.mjs
CHANGED
|
@@ -2,8 +2,9 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import Ajv from 'ajv';
|
|
3
3
|
import { parseTransportResponse, requestTransport, trustedOrigin } from './remote-transport.mjs';
|
|
4
4
|
import { appendInvocation } from './local-invocations.mjs';
|
|
5
|
-
import { verifyAutomaticFeedback } from './automatic-feedback-client.mjs';
|
|
5
|
+
import { acknowledgeAutomaticFeedback, verifyAutomaticFeedback } from './automatic-feedback-client.mjs';
|
|
6
6
|
import { ideClientHeaders, presentInvocationNotices } from './ide-presentation.mjs';
|
|
7
|
+
import { routeTaskEntry } from './task-entry-client.mjs';
|
|
7
8
|
|
|
8
9
|
export const apiUrl = 'https://entrorove.com/api/skill/invoke';
|
|
9
10
|
|
|
@@ -71,8 +72,14 @@ export async function invokeRemote(data, dependencies) {
|
|
|
71
72
|
return;
|
|
72
73
|
}
|
|
73
74
|
const token = await readCredential(home);
|
|
75
|
+
const routed = await routeTaskEntry(data, token, dependencies);
|
|
76
|
+
if (routed) {
|
|
77
|
+
process.stdout.write(JSON.stringify(routed) + '\n');
|
|
78
|
+
if (['blocked','failed','incomplete'].includes(routed.status)) process.exitCode = 2;
|
|
79
|
+
return routed;
|
|
80
|
+
}
|
|
74
81
|
const { response, result } = await invokeDiscovered(data, token, { home, apiOrigin });
|
|
75
|
-
process.stdout.write(JSON.stringify(presentInvocationNotices(result,dependencies.onIdeEvent)) + '\n');
|
|
82
|
+
process.stdout.write(JSON.stringify(presentInvocationNotices(result,dependencies.onIdeEvent,{home})) + '\n');
|
|
76
83
|
if (!response.ok || result.output.status === 'blocked' || result.output.status === 'failed'
|
|
77
84
|
|| result.output.status === 'incomplete') process.exitCode = 2;
|
|
78
85
|
}
|
|
@@ -135,6 +142,7 @@ export async function postInvocation(call, token, home, feedbackRequired = false
|
|
|
135
142
|
const expected = call.feedbackHashes ?? [call.requestId];
|
|
136
143
|
if (!expected.includes(result.feedback.requestHash)) throw new Error('Automatic feedback lost its original invocation binding');
|
|
137
144
|
verifyAutomaticFeedback(result.feedback,call.requestId,result.feedback.requestHash);
|
|
145
|
+
result.feedback = await acknowledgeAutomaticFeedback(result.feedback,new URL(call.url).origin,token,ideClientHeaders());
|
|
138
146
|
}
|
|
139
147
|
else if (feedbackRequired && !['running','pending'].includes(result.output.status)) {
|
|
140
148
|
throw new Error(`Automatic feedback receipt is missing; query feedback ${call.requestId}. Do not repeat the invocation.`);
|
package/remote-transport.mjs
CHANGED
|
@@ -57,8 +57,10 @@ export async function requestTransport(origin, pathname, options = {}) {
|
|
|
57
57
|
const trusted = trustedOrigin(origin);
|
|
58
58
|
if (typeof pathname !== 'string' || !pathname.startsWith('/api/') || pathname.includes('://')) throw new Error('Invalid Entrorove API path');
|
|
59
59
|
const url = new URL(pathname, trusted);
|
|
60
|
-
if (url.origin !== trusted || url.search || url.hash) throw new Error('Invalid Entrorove API path');
|
|
61
60
|
const configured = { timeoutMs: 15_000, method: 'GET', ...options };
|
|
61
|
+
const originalGeneration = configured.method === 'GET' && /^\/api\/skill\/feedback\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(url.pathname)
|
|
62
|
+
&& /^\?generation=[1-9][0-9]{0,14}$/.test(url.search) && Number.isSafeInteger(Number(url.searchParams.get('generation')));
|
|
63
|
+
if (url.origin !== trusted || url.hash || url.search && !originalGeneration) throw new Error('Invalid Entrorove API path');
|
|
62
64
|
if (!['GET', 'POST'].includes(configured.method)) throw new Error('Invalid Entrorove transport method');
|
|
63
65
|
for (let number = 1; number <= 3; number++) {
|
|
64
66
|
try {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { requestTransport, parseTransportResponse, trustedOrigin } from './remote-transport.mjs';
|
|
2
|
+
import { runCloudTask } from './cloud-task-client.mjs';
|
|
3
|
+
import { displayIdeEvent, ideClientHeaders } from './ide-presentation.mjs';
|
|
4
|
+
|
|
5
|
+
/** New IDE tasks use the authenticated account preference; resumes keep their original route. */
|
|
6
|
+
export async function routeTaskEntry(data, token, dependencies) {
|
|
7
|
+
if (data.operation !== 'start' || data.input?.sandboxTaskId) return null;
|
|
8
|
+
const choice = data.sandboxChoice;
|
|
9
|
+
if (choice !== undefined && !['use','local'].includes(choice)) throw new Error('sandboxChoice must be use or local');
|
|
10
|
+
if (choice === 'local') {
|
|
11
|
+
displayIdeEvent({ schemaVersion: 'entrorove.ide-event/1.0',kind: 'status',phase: 'route',
|
|
12
|
+
message: '本次采用本地方案。',separateLine: true },dependencies.onIdeEvent,{home:dependencies.home});
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const response = await requestTransport(trustedOrigin(dependencies.apiOrigin()), '/api/cloud-sandbox', {
|
|
16
|
+
headers: { authorization: `Bearer ${token}`,...ideClientHeaders() },timeoutMs: 15000,
|
|
17
|
+
});
|
|
18
|
+
const preference = parseTransportResponse(response, 'cloud-sandbox preference');
|
|
19
|
+
if (!response.ok || typeof preference?.enabled !== 'boolean') {
|
|
20
|
+
throw new Error(`Cloud sandbox preference unavailable: HTTP ${response.status}. Task entry stopped.`);
|
|
21
|
+
}
|
|
22
|
+
if (!preference.enabled && choice === undefined) return null;
|
|
23
|
+
const task = data.input?.task;
|
|
24
|
+
if (!task || typeof task.goal !== 'string') throw new Error('Task entry requires the original task goal');
|
|
25
|
+
const ideTaskId = data.ideTaskId ?? process.env.CODEX_THREAD_ID;
|
|
26
|
+
return runCloudTask({ goal: task.goal, task,
|
|
27
|
+
...(ideTaskId ? { ideTaskId } : {}),...(choice ? { sandboxChoice: choice } : {}) },dependencies);
|
|
28
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function scopedTaskNumber(identity) {
|
|
2
|
+
let hash = 2166136261;
|
|
3
|
+
for (const character of identity) {
|
|
4
|
+
hash ^= character.codePointAt(0) ?? 0;
|
|
5
|
+
hash = Math.imul(hash, 16777619);
|
|
6
|
+
}
|
|
7
|
+
return String((hash >>> 0) % 100000).padStart(5, '0');
|
|
8
|
+
}
|
|
9
|
+
export function sandboxTaskNumber(taskId) { return scopedTaskNumber(`task:${taskId}`); }
|