@entrorove/skill 0.1.36 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
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
+
3
8
  ## 0.1.36 — 2026-10-13
4
9
 
5
10
  - 技能调用结束立即确认 IDE 评价回传,提供原账号、原调用与执行代次绑定的接收回执;完善原回执只读恢复与公开合同校验。
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.36.
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 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.
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
- 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.
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 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.
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,7 +83,7 @@ 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. Real IDE events notify output availability, verification, failure or required confirmation, deduplicated by original task/request and state. Promotion and keep-alive messages remain visible in the IDE and never trigger system notifications. Changing the stored account credential disables the previous account's notification setting. OS acceptance does not prove that permission allows a visible notification.
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
 
@@ -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
- 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.
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.
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', 'needs-input'].includes(result.status)) process.exitCode = 2;
393
+ if (['blocked', 'failed', 'incomplete'].includes(result.status)) process.exitCode = 2;
394
394
  return result;
395
395
  }
396
396
 
@@ -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
- return { goal: input.goal, ...(input.ideTaskId === undefined ? {} : { ideTaskId: input.ideTaskId }) };
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
- return envelope(task, 'started', { taskId: result.taskId,
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
  }
@@ -3,6 +3,7 @@ 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
5
  import { acknowledgeAutomaticFeedback } from './automatic-feedback-client.mjs';
6
+ import { sandboxTaskNumber } from './task-identity.mjs';
6
7
 
7
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;
8
9
  function inputOptions(input) {
@@ -21,7 +22,7 @@ export async function watchCloudTask(input, dependencies) {
21
22
  interaction: dependencies.agreementRequired(dependencies.home) };
22
23
  const token = await dependencies.readCredential(dependencies.home);
23
24
  const controller = new AbortController(), deadline = setTimeout(() => controller.abort(),waitMs);
24
- 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}); };
25
26
  let afterId = options.afterId, taskStatus = 'running', outputAvailable = false, settled = false, flowId;
26
27
  try {
27
28
  const response = await fetch(`${trustedOrigin(dependencies.apiOrigin())}/api/cloud-sandbox/tasks/${taskId}/events`, {
@@ -52,11 +53,11 @@ export async function watchCloudTask(input, dependencies) {
52
53
  if (current.status === 'observed') taskStatus = current.taskStatus;
53
54
  if (current.feedback) {
54
55
  current.feedback = await acknowledgeAutomaticFeedback(current.feedback,dependencies.apiOrigin(),token,ideClientHeaders());
55
- emit(automaticFeedbackIdeEvent(current.feedback));
56
+ emit({ ...automaticFeedbackIdeEvent(current.feedback),taskId,taskNumber:sandboxTaskNumber(taskId) });
56
57
  }
57
58
  emit({ schemaVersion: 'entrorove.ide-event/1.0',taskId,kind: 'status',phase: 'exit',
58
59
  message: settled || taskStatus !== 'running' ? '云端沙箱正在退出本次进度订阅' : '云端沙箱仍在执行,下次从当前进度继续读取',separateLine: true });
59
- 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,
60
61
  events,...(flowId ? { flowId } : {}),task: current.task,usage: current.usage,...(current.feedback ? { feedback: current.feedback } : {}),
61
62
  nextAction: taskStatus === 'running' && !settled ? { operation: 'cloud-watch',input: { taskId,afterId,waitMs } } : null };
62
63
  }
@@ -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
  }
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
- try { return (await execute('/usr/bin/git', [...safeFlags, ...args], settings(root, null, 2097152, 'utf8'))).stdout; }
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('git', ['--no-optional-locks', '-c', 'core.hooksPath=/dev/null', '-c', 'core.fsmonitor=false', '-c', 'diff.external=', ...args],
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "entrorove.skill-manifest/1.0",
3
3
  "name": "entrorove",
4
- "version": "0.1.36",
4
+ "version": "0.1.37",
5
5
  "endpoint": "https://entrorove.com",
6
6
  "requestSchemaVersion": "entrorove.skill.request/2.0",
7
7
  "supportedRequestSchemaVersions": [
package/native-client.mjs CHANGED
@@ -55,7 +55,7 @@ export async function invokeNativeResult(domain, operation, input, dependencies)
55
55
  }
56
56
  export async function runNative(domain, operation, input, dependencies) {
57
57
  const { response, result } = await invokeNativeResult(domain, operation, input, dependencies);
58
- process.stdout.write(`${JSON.stringify(presentInvocationNotices(result, dependencies.onIdeEvent))}\n`);
58
+ process.stdout.write(`${JSON.stringify(presentInvocationNotices(result, dependencies.onIdeEvent,{home:dependencies.home}))}\n`);
59
59
  if (!response.ok || ['blocked', 'failed', 'incomplete'].includes(result.output.status)) process.exitCode = 2;
60
60
  return result;
61
61
  }
@@ -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: '任务验证已完成。', test: 'Entrorove 系统通知测试。' };
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 = messageKinds[kind];
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.requiresConfirmation && event.kind !== 'notice' ? 'confirmed'
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 send(kind);
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.36",
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",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "entrorove.release-state/1.0",
3
- "version": "0.1.36",
4
- "digest": "31125fc03752d64a87f994186ebf770a40966984d65a99358239ef71ab926723",
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": "f0a3104ef9f4431ded2a7049c08ae6435b09ed53c4cba486c2bd52db2728d999",
47
+ "cli.mjs": "6ecb24fe53e7aa5412e38028921246e8a1f7128a2c5931a6fcd80aa1153ca358",
48
48
  "review-client.mjs": "3491eef68ed533174b31db3c53febb6519b8f088f71a9964644714a9a4c03c77",
49
49
  "version.mjs": "1f392147d72c40af4b03b307ea607075fb815af5174fa07e254b95fe12dbf3e7",
50
- "SKILL.md": "f06fd87e2008e5951c778c2e206cb42eed668d21d5c0f2a12108a0a031fde9ad",
50
+ "SKILL.md": "eba3ac28270ff590d2602ec057d1e0a6f5dcb3921f53367d6dee588d9b3e9265",
51
51
  "CAPABILITY-MAP.md": "40755edd3c698bbbbd4f49bc47a21c5ad9bbb8eb05e9542dd7d3a11a0254ea35",
52
52
  "AGREEMENT.md": "f93a68e92c457200dd024610e65f74e478b8f60860f951da818b70561f1b9bf8",
53
- "package.json": "9a7f12414556bcc150d57e64881694aeddfe0b760e98454491ac5a6585ed0421",
53
+ "package.json": "0a106634b48e5c1174fac08345bbb8f324e65a793ac4ba88fb48fe132f503a4a",
54
54
  "manifest.json": "ab06e307795bc9da5add926752b6c4dbdaf18625090261a035aa356a679c504f",
55
- "release.mjs": "eca6032eae240fb90c7646e0b1509ea6b4c94c23b90aca87e4091996c5b9b5f8",
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": "9dc42763ca7013cde607543308e029796d68f3b020a5a3b4ad9e68815e38d0f5",
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": "af7fe24408509dcebe39bdfc6fcef6adcf66f6ad09473149f920d5e921a48d1a",
92
+ "local-scope-repository.mjs": "b6f3f9923e040bfa3d5f48f447431df1cf9bbda02c967b3d9378863c8fd44733",
93
93
  "local-scope-git.mjs": "53bbbb6c4795db711b5bc164591a6ad79a561427da046d6bbe997dae6eb82829",
94
- "local-scope-git-reads.mjs": "d18d4441664aa59ad8360ee16a69fc0282e7530705d5ae7950944a8158448bb4",
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,16 +119,19 @@
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": "886ca19718b1d23209807ee3ce7d336053a17c0ad2190bb71f4c96371694dff4",
122
- "cloud-task-client.mjs": "ecb6124ea5e6249f31fb95cb68d13d821d2daea8768fe47ddf6dcf82fc3392cc",
122
+ "remote-client.mjs": "62a4ddf8422d279bdb568d9800dbf2bd8e3477556188d5a0ef04091a846ccf69",
123
+ "cloud-task-client.mjs": "77ced16a6f5dc3f08ef149da999fb488e62bb64964d448efa821f675f67cbdd7",
123
124
  "scope-runner-client.mjs": "646ab4f80d4a8209e922fd45f40efd968f0744d9dfc03188b222c1a2e11e83b8",
124
- "native-client.mjs": "ea4d9c687b62858dc4e8fb6bceca59bb049cba0c2762380be460821393e025db",
125
- "native-notification.mjs": "f116772178140d1a2a7aac4a8e361a76543b1f47c84b3823a26a797f11a8bfbb",
125
+ "native-client.mjs": "6f5575909bae9daba643287f876ffa0f9ff1c53a367b32a40145d740456d6605",
126
+ "native-notification.mjs": "8859a26c6f13d222711f763acf49ed8546b3306cf71f990643ac31a5ccaf1ba1",
126
127
  "local-peer-status.mjs": "a971bcc69d60bb32b875ca5bb4297d948631d5d2821b812c279613ea059a8b4f",
127
- "cloud-watch-client.mjs": "f9629e67d1f14f50e9fb52f65001059e07999352fb224b7c7e28590b95609a8a",
128
+ "cloud-watch-client.mjs": "5b93935c19a104d72afed07d4a56746c9617707e075884b93d22050c95cee0c1",
128
129
  "cloud-event-stream.mjs": "e0eec9fc3e0613094523929634f25030df5ca5c5b5fe2c1e1a4586ab10574808",
129
- "ide-presentation.mjs": "d8f54505cf559af98cd1a93a739817c13df3e1e68d2dff9fde90554ea14cb10c",
130
+ "ide-presentation.mjs": "60a4d791790a767cec9666bf8734961a20a2db59146e74bafc090740b9f169c1",
130
131
  "automatic-feedback-client.mjs": "75e92f50d6fbfa4f482af36e16c9ce5dd323a02e99eb9b434dcfa3faa846a532",
132
+ "task-entry-client.mjs": "36473ff61b869b8a8ba0eef1fc00e565b894dc54c92182d14e906276a6a03f9e",
133
+ "task-identity.mjs": "e0e6cf1f48bfa5ee3c5cb86a3c21adfce6d66b4220f4a164ca099263c7a09fb4",
134
+ "task-identity.d.mts": "55472e1ffc81b9d5e5c5bd56c0bf1d18ecde03ee5170a58a15ce9caf222d8761",
131
135
  "remote-transport.mjs": "93caaca646fca298a4e08142678b134ecc3a657e00de5b9939e2364b2db9d6c4",
132
136
  "local-credential.mjs": "2a302a7f67b5ea8dc73453fc96ab33db349b59ca11fa9456e48ca1d9669d877c",
133
137
  "local-invocations.mjs": "f6ac4c41c1a5ec34de358c4cca210ca8e403b3a913150a43bb55ec9be55d9998",
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,10 @@
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
+ },
2
8
  {
3
9
  "version": "0.1.36",
4
10
  "date": "2026-10-13",
package/remote-client.mjs CHANGED
@@ -4,6 +4,7 @@ import { parseTransportResponse, requestTransport, trustedOrigin } from './remot
4
4
  import { appendInvocation } from './local-invocations.mjs';
5
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
  }
@@ -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,2 @@
1
+ export function scopedTaskNumber(identity: string): string;
2
+ export function sandboxTaskNumber(taskId: string): string;
@@ -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}`); }