@entrorove/skill 0.1.33 → 0.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CAPABILITY-MAP.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # 能力对账图(WP-02 / AC-20)
2
2
 
3
+ ## 2026-10-12 补充:旧版八技能的 IDE 调用链
4
+
5
+ 逐项对照旧版八个技能的已安装说明、入口和服务实现,本次补回以下共同保证。后面的 AC-20~24 保留其历史基线与当时测试边界。
6
+
7
+ | 旧版保证 | 当前入口与行为 |
8
+ | --- | --- |
9
+ | 每个任务选择云端/本地,原生确认不可用时仍须展示问题 | `cloud-task` 返回 Confirm Protocol 和 `chatFallback`;确认才提交,退出保留原任务并采用本地方案 |
10
+ | 实际进度必须进入 IDE | `cloud-watch` 订阅本人任务 SSE,输出独立 IDE 事件;调用方必须把公开消息和摘要展示到对话进度 |
11
+ | 执行提示与推广可见 | `智能执行中,请勿关闭!` 与服务端选中的真实推广独立输出;保留推广 HTTPS 链接,按事件 ID 去重 |
12
+ | 断线恢复原任务 | `cloud-watch` 带原 `taskId/afterId` 只读续接;`status` 与 `feedback` 查询原请求,不重发业务 POST |
13
+ | 服务端自动评价,无第二次评价写入 | 调用完成事务保存本人原 request 的评价与结果摘要;CLI 校验回执。旧式适配器使用幂等持久化并在同一 HTTP 响应返回 |
14
+ | 未验证不可记好评 | intake、模型摘要、reported、不完整证据无评分;阻断、失败、P0/P1 不产生好评;发现和状态查询不生成新评价 |
15
+ | 实际 IDE 与原调用绑定 | `ENTROROVE_IDE` 或宿主检测写入原调用标签;每个 OS 用户继续复用受限凭据存储 |
16
+ | 最新官方技能说明生效 | 新调用使用 `npx --yes --prefer-online @entrorove/skill@latest`;安装更新后重新读取 managed `SKILL.md` |
17
+ | 调用次数与沙箱次数分开 | 普通调用与旧式 UUID 记录技能用量;`cloud-task` 才创建沙箱。所有查询和评价按账号权限过滤 |
18
+
19
+ 自动技术评价是私有调用记录。公开用户评价仍须使用本人原话、真实评分和明确公开同意。原生系统通知、手机推送未实现;确认使用 IDE 问题工具或可见文本回退。思考详情只展示服务端公开的规划摘要。
20
+
3
21
  本表把旧八域的远端公开操作与当前本地执行器分开统计。旧版 `SKILL.md` 只规定入口、状态和边界,没有逐域列出操作名;远端操作名采用随包锁定的 v2 runtime catalog 交叉核对,本地操作名直接来自 `local-contracts.mjs` 的 `LOCAL_OPERATION_SCHEMAS`。两个分母不能合并。
4
22
 
5
23
  ## 边界
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.34 — 2026-10-12
4
+
5
+ - 补回旧版八技能的 IDE 可见云端选择、真实事件进度与独立推广提示;服务端自动评价绑定原调用并返回可验证回执,支持只读恢复;修复旧式 UUID 调用漏计用量,保持账号隔离及沙箱/调用计数分离。
6
+ - Restore visible IDE cloud choice, authoritative live progress and separate promotion notices from the original eight-skill workflow. Persist account-bound automatic invocation evaluations with verifiable receipts and read-only recovery; meter legacy UUID calls without mixing skill and sandbox counters.
7
+
3
8
  ## 0.1.33 — 2026-10-12
4
9
 
5
10
  - 补齐账号与任务隔离的本地签名器和真实快照接入,解析实际别名及类型声明;动态依赖仅进入受限规划,保留独立写入核验。
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.33.
8
+ Version: 0.1.34.
9
9
 
10
10
  Read the user's goal and repository instructions first. Preserve the original task through planning, execution and verification. Use the workflow only where it helps; a small, low-risk edit can proceed directly. Never claim a reported step is a verified result.
11
11
 
@@ -23,11 +23,23 @@ On first use or after a material agreement revision, the CLI returns a Confirm P
23
23
 
24
24
  ## Work with a task
25
25
 
26
+ ### IDE display and cloud planning entry
27
+
28
+ For a task using this skill's cloud planning, first run `npx --yes --prefer-online @entrorove/skill@latest cloud-task` with `{"goal":"<the user's actual goal>","ideTaskId":"<the current IDE task ID, when available>"}` on stdin. Render the returned choice with the host's question tool or native UI. If no native adapter exists, show `interaction.chatFallback` in the conversation and collect its answer. Tool output alone is not a displayed question. Offer **确认** and **退出,采用本地方案**. Exit or an explicit cancellation continues the original task locally; it must not stop the task or change account preferences. An unanswered question does not authorize cloud consumption. Reuse a prior explicit choice for this same task rather than asking again.
29
+
30
+ With `sandboxChoice: "use"` (or the protocol callback's `answer: "use"`), retain the real returned task ID and immediately run the returned `nextAction`, `cloud-watch`. It subscribes to that account's authoritative SSE stream, returns newline-delimited `entrorove.ide-event/1.0` events, then one `entrorove.cloud-watch/1.0` result. **Mirror each new event's public `message` and optional `detail` into visible IDE progress/commentary.** Deduplicate by task ID and event ID, including events repeated in the final result. The same rule applies to normal invocation `ideMessages` and notice events. A JSON field or collapsed command log does not satisfy this display requirement. Show `智能执行中,请勿关闭!` and the selected promotion on separate lines; preserve the promotion's actual HTTPS link. Use only the server's selected promotion; do not insert a local test advertisement.
31
+
32
+ Display the actual entering, allocation, thinking, public output and exit events as they arrive. Do not generate model reasoning, invent intermediate events or replace `reported` with verified completion. The public model summary is the planning output available to show. `云端沙箱输出完毕` means the current planning response has arrived; inspect its real task/verification status before dependent execution. Exit here closes the progress subscription and does not cancel the persisted task.
33
+
34
+ When a bounded watch returns `nextAction`, continue it with the returned `afterId`; reconnect only with GET and that cursor. Never repeat cloud-task POST to recover progress. For an available planning result, query `status <feedback.requestId>` to obtain the original accepted flow and required next step, then continue that flow. Do not create a second `start` for the same cloud goal. If the task is still running but no events arrive, say so and keep reading its real status. Missing credentials, blocked entitlement, expiry and stream errors must be visible.
35
+
36
+ Set `ENTROROVE_IDE` to the actual host name (for example `Codex`, `Cursor` or `Claude Code`) when host detection is unavailable. It carries only the IDE label; credentials remain in the restricted store. Use the latest official package with `--prefer-online` for new invocations, and reread the managed `SKILL.md` after installation reports an update. Recovering an uncertain request remains a read-only query of its original ID.
37
+
26
38
  Send JSON on standard input to `npx @entrorove/skill invoke`. Before every execution call, discover `operation: "capabilities"` with `{}` and validate input against the returned operation schema. The CLI performs remote discovery automatically; local adapters discover the installed contract before execution. Discovery failure stops execution. Adapters that expose the four-step task route can call `task-describe`, `message-route`, `message-accept` and `task-resume` in order; `start` remains the one-call workflow equivalent. Then start with `operation: "start"` and an `input.task` containing the goal, work type, risk, difficulty and observed scope facts. Work types are `implementation`, `documentation`, `calculation`, `merge`, `review`, `decision` and `diagnosis`. Assess facts from the real task and repository; do not invent certainty. entrorove chooses the workflow. A small task may return `decision: "direct"`, meaning the IDE should do the work without another service call. The public envelope is `entrorove.skill.request/2.0`; `requestId` is the SHA-256 digest of `operation + canonical(input)`. Version 1.0 remains accepted for older callers.
27
39
 
28
40
  `start` creates a normal durable skill flow. It does not create a cloud-sandbox task or contribute to cloud-sandbox usage. To create a real cloud-sandbox task, call `operation: "cloud-task"` with `input.goal`; the CLI returns a Confirm Protocol choice interaction before any cloud request. Continue the same request with `sandboxChoice: "use"` or `sandboxChoice: "local"`. `use` calls the authenticated `/api/cloud-task` endpoint and returns the real `taskId`; `local` skips only this task. The server is authoritative for account entitlement and the account-wide sandbox preference: a disabled or unavailable account returns `blocked` and creates no task. A `needs-input`, `blocked` or `failed` result is not a successful dispatch and must be shown to the user.
29
41
 
30
- After dispatch, query the real task without resending it with the CLI-only `operation: "cloud-status"` and `input.taskId`, or `npx @entrorove/skill cloud-status` with the same JSON. This special CLI route reads the authoritative task list and usage endpoints; it is not part of the `/api/skill/invoke` capability map. The read-only result includes the current task status, public title/summary, and recorded model usage when available. A missing task is reported explicitly; it is never treated as completed. This is the skill-side status and usage check for cloud tasks; the web dock may additionally subscribe to its event stream.
42
+ After dispatch, use `cloud-watch` for live IDE progress and `cloud-status` for a read-only snapshot with `input.taskId`. Both are CLI-only routes, also accepted by `invoke`; they do not pass through `/api/skill/invoke`. Status includes public title/summary, actual model usage and the stored automatic evaluation when available. A missing task is explicit and never treated as completed. The IDE and website consume the same account-bound event stream.
31
43
 
32
44
  ### Per-task cloud sandbox choice
33
45
 
@@ -59,6 +71,12 @@ The local execution layer is available through `npx @entrorove/skill local <oper
59
71
 
60
72
  Local CLI failures use the machine-readable `entrorove.local-error/1.0` envelope `{schemaVersion,status,code,disposition,message}` on stderr. `status` is `failed` for invalid input or computation errors and `blocked` for missing mutation, lease, snapshot or handoff authority. Formula `add` and `mul` require at least one argument; `div`, `safeDivide` and `percentOf` reject a zero denominator with `DIV_ZERO`. The formula engine declares `round` as half-away-from-zero with two decimal places by default; an engine may declare another `decimalPlaces` value from 0 through 28 in its `roundingPolicy`.
61
73
 
74
+ ### Automatic invocation evaluation and optional user reviews
75
+
76
+ Every completed or failed execution call receives a server-generated `feedback` receipt bound to the original account, request hash and result digest. Durable calls commit this evaluation in the same transaction as the result; legacy adapters return an idempotent receipt with the same HTTP response. The CLI verifies the original identity and evaluation digest. Preserve `receiptId`, `requestId` and `evaluation.digest`; never generate a score or comment locally. Display the returned evaluation acknowledgement in IDE progress, deduplicated by receipt ID. Read the saved receipt with `npx @entrorove/skill feedback <original requestId>` after a connection loss; this is GET only and never repeats execution or submits another evaluation. Discovery/status queries create no evaluations.
77
+
78
+ Only server-verified progress or a validated pure operation earns a positive automatic evaluation. Reported steps, intake, model summaries and incomplete evidence remain `pending` with no score. Blocked/failed results and P0/P1 findings cannot become positive. These are private technical evaluations; they do not create a public user review or masquerade as the user's words. If a server advertising this extension returns no valid receipt, show the missing receipt and query its original ID. Do not silently claim that evaluation was reported.
79
+
62
80
  When the user gives an actual evaluation after using the skill, offer to publish that feedback. Only after they explicitly agree to public display under the current notice, send their unchanged words, IDE name and 1–5 rating as JSON on standard input to `npx @entrorove/skill review`, including `invocationRequestId` from their actual successful call, `publicConsent: true` and `noticeVersion: "2026-10-07.1"`. The same account must own that successful call within the last 30 days; a recorded usage event, failed call or unknown result does not qualify. Three public reviews per account per day share one server quota across API replicas. Public output exposes only IDE, rating, original text and time, never account or invocation identity. Never invent, rewrite or infer a review from task completion. The website reads accepted reviews directly from entrorove's independent database.
63
81
 
64
82
  Feedback command example (replace the request ID with the original successful invocation ID, and use only the user’s actual words after explicit public consent):
@@ -0,0 +1,43 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { parseTransportResponse, requestTransport, trustedOrigin } from './remote-transport.mjs';
3
+
4
+ function canonical(value) {
5
+ if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
6
+ if (value && typeof value === 'object') return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
7
+ return JSON.stringify(value);
8
+ }
9
+ function digest(value) { return createHash('sha256').update(canonical(value)).digest('hex'); }
10
+ export function verifyAutomaticFeedback(feedback, requestId, requestHash) {
11
+ const evaluation = feedback?.evaluation;
12
+ if (!feedback || feedback.schemaVersion !== 'entrorove.automatic-feedback/1.0' || feedback.stored !== true
13
+ || !/^[0-9a-f-]{36}$/i.test(feedback.receiptId) || feedback.requestId !== requestId
14
+ || !/^[a-f0-9]{64}$/.test(feedback.requestHash) || requestHash && feedback.requestHash !== requestHash
15
+ || !/^[a-f0-9]{64}$/.test(feedback.resultDigest) || !evaluation
16
+ || evaluation.schemaVersion !== 'entrorove.automatic-evaluation/1.0' || evaluation.requestId !== requestId
17
+ || !['good','bad','pending'].includes(evaluation.disposition)
18
+ || !['passed','failed','incomplete'].includes(evaluation.validation)
19
+ || !Number.isSafeInteger(evaluation.durationMs) || evaluation.durationMs < 0
20
+ || !['findingCount','p0Count','p1Count'].every(key => Number.isSafeInteger(evaluation[key]) && evaluation[key] >= 0)
21
+ || typeof evaluation.comment !== 'string' || !evaluation.comment.trim()) {
22
+ throw new Error(`Invalid automatic feedback receipt; query feedback ${requestId} without repeating the invocation`);
23
+ }
24
+ const { digest: checksum,...core } = evaluation;
25
+ if (digest(core) !== checksum || !['succeeded','reported','blocked','failed','incomplete'].includes(evaluation.status)
26
+ || evaluation.findingCount < evaluation.p0Count + evaluation.p1Count
27
+ || evaluation.disposition === 'pending' && (evaluation.score !== null || evaluation.validation !== 'incomplete')
28
+ || evaluation.disposition === 'good' && (evaluation.status !== 'succeeded' || evaluation.validation !== 'passed' || evaluation.p0Count || evaluation.p1Count)
29
+ || evaluation.disposition === 'bad' && evaluation.validation !== 'failed'
30
+ || evaluation.disposition !== 'pending' && (!Number.isInteger(evaluation.score) || evaluation.score < 0 || evaluation.score > 100)) {
31
+ throw new Error('Automatic feedback digest or validation state is inconsistent');
32
+ }
33
+ return feedback;
34
+ }
35
+ export async function queryAutomaticFeedback(requestId, dependencies) {
36
+ if (typeof requestId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(requestId)) throw new Error('feedback requires its original request ID');
37
+ const token = await dependencies.readCredential(dependencies.home);
38
+ const response = await requestTransport(trustedOrigin(dependencies.apiOrigin()),`/api/skill/feedback/${requestId}`,
39
+ { headers: { authorization: `Bearer ${token}` } });
40
+ const result = parseTransportResponse(response,'automatic feedback');
41
+ if (!response.ok) throw new Error(`Automatic feedback unavailable: HTTP ${response.status}; requestId=${requestId}`);
42
+ return verifyAutomaticFeedback(result.feedback,requestId);
43
+ }
package/cli.mjs CHANGED
@@ -14,6 +14,8 @@ import { apiUrl, buildInvocation, invokeRemote, queryFlows, queryStatus } from '
14
14
  const packageDir = path.dirname(fileURLToPath(import.meta.url));
15
15
  import { buildReview, submitSkillFeedback, feedbackNoticeVersion } from './review-client.mjs';
16
16
  import { queryCloudTask, runCloudTask as dispatchCloudTask } from './cloud-task-client.mjs';
17
+ import { watchCloudTask } from './cloud-watch-client.mjs';
18
+ import { queryAutomaticFeedback } from './automatic-feedback-client.mjs';
17
19
  const markerName = '.entrorove-managed.json';
18
20
  const installFiles = ['SKILL.md', 'CAPABILITY-MAP.md', 'AGREEMENT.md', 'manifest.json'];
19
21
  const markerSchema = 'entrorove.managed-install/1.0';
@@ -396,6 +398,13 @@ export async function runCloudStatus(input, dependencies = remoteDependencies())
396
398
  if (['blocked', 'failed', 'missing'].includes(result.status)) process.exitCode = 2;
397
399
  return result;
398
400
  }
401
+ export async function runCloudWatch(input, dependencies = remoteDependencies()) {
402
+ const result = await watchCloudTask(input,{ ...dependencies,
403
+ onIdeEvent: event => process.stdout.write(`${JSON.stringify(event)}\n`) });
404
+ process.stdout.write(`${JSON.stringify(result)}\n`);
405
+ if (['blocked','failed','missing'].includes(result.status) || ['blocked','failed'].includes(result.taskStatus)) process.exitCode = 2;
406
+ return result;
407
+ }
399
408
 
400
409
  async function invoke(data) {
401
410
  if (data?.operation === 'cloud-task') {
@@ -404,6 +413,7 @@ async function invoke(data) {
404
413
  if (data?.operation === 'cloud-status') {
405
414
  return runCloudStatus(data.input, remoteDependencies());
406
415
  }
416
+ if (data?.operation === 'cloud-watch') return runCloudWatch(data.input);
407
417
  return invokeRemote(data, remoteDependencies());
408
418
  }
409
419
 
@@ -440,6 +450,10 @@ async function main() {
440
450
  await runCloudTask(await stdinJson());
441
451
  } else if (command === 'cloud-status' && args.length === 0) {
442
452
  await runCloudStatus(await stdinJson());
453
+ } else if (command === 'cloud-watch' && args.length === 0) {
454
+ await runCloudWatch(await stdinJson());
455
+ } else if (command === 'feedback' && args.length === 1) {
456
+ process.stdout.write(`${JSON.stringify(await queryAutomaticFeedback(args[0],remoteDependencies()))}\n`);
443
457
  } else if (command === 'review' && args.length === 0) {
444
458
  await submitReview(await stdinJson());
445
459
  } else if (command === 'status' && args.length === 1) {
@@ -461,7 +475,7 @@ async function main() {
461
475
  } else if (command === 'local') {
462
476
  await runLocal(args);
463
477
  } else {
464
- throw new Error('Usage: entrorove install | configure | agreement | invoke | scope-connect <repositoryRoot> | host-message [adapterModulePath] | cloud-task | cloud-status | status <requestId> | flows | review | local (JSON on stdin)');
478
+ throw new Error('Usage: entrorove install | configure | agreement | invoke | scope-connect <repositoryRoot> | host-message [adapterModulePath] | cloud-task | cloud-watch | cloud-status | feedback <requestId> | status <requestId> | flows | review | local (JSON on stdin)');
465
479
  }
466
480
  }
467
481
  if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
@@ -0,0 +1,27 @@
1
+ export async function consumeCloudStream(body, onFrame) {
2
+ if (!body) throw new Error('Cloud event response has no stream');
3
+ const reader = body.getReader(), decoder = new TextDecoder('utf-8',{ fatal: true });
4
+ let pending = '', size = 0;
5
+ try {
6
+ while (true) {
7
+ const { value,done } = await reader.read();
8
+ if (value) size += value.byteLength;
9
+ if (size > 16_777_216) throw new Error('Cloud event stream exceeded its bounded read');
10
+ pending += decoder.decode(value,{ stream: !done });
11
+ let match;
12
+ while ((match = /\r?\n\r?\n/u.exec(pending))) {
13
+ const frame = pending.slice(0,match.index); pending = pending.slice(match.index + match[0].length);
14
+ if (frame.length > 262144) throw new Error('Cloud event frame exceeded its bounded read');
15
+ let event = 'message', id, data = [];
16
+ for (const line of frame.split(/\r?\n/u)) {
17
+ if (line.startsWith('event:')) event = line.slice(6).trim();
18
+ else if (line.startsWith('id:')) id = line.slice(3).trim();
19
+ else if (line.startsWith('data:')) data.push(line.slice(5).replace(/^ /u,''));
20
+ }
21
+ if (data.length && await onFrame({ event,id,data: JSON.parse(data.join('\n')) }) === false) return;
22
+ }
23
+ if (pending.length > 262144) throw new Error('Cloud event frame exceeded its bounded read');
24
+ if (done) { if (pending.trim()) throw new Error('Cloud event response ended with an incomplete frame'); return; }
25
+ }
26
+ } finally { await reader.cancel().catch(() => undefined); reader.releaseLock(); }
27
+ }
@@ -1,6 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { appendInvocation } from './local-invocations.mjs';
3
3
  import { parseTransportResponse, requestTransport, trustedOrigin } from './remote-transport.mjs';
4
+ import { displayIdeEvent, ideClientHeaders } from './ide-presentation.mjs';
5
+ import { verifyAutomaticFeedback } from './automatic-feedback-client.mjs';
4
6
 
5
7
  const CLOUD_TASK_SCHEMA = 'entrorove.cloud-task/1.0';
6
8
  const CLOUD_STATUS_SCHEMA = 'entrorove.cloud-status/1.0';
@@ -26,7 +28,10 @@ function validateInput(input) {
26
28
  if (input.ideTaskId !== undefined && (typeof input.ideTaskId !== 'string' || !input.ideTaskId.trim() || input.ideTaskId.length > 200)) {
27
29
  throw new Error('cloud-task ideTaskId must be a non-empty string of at most 200 characters');
28
30
  }
29
- if (input.sandboxChoice !== undefined && !['use', 'local'].includes(input.sandboxChoice)) {
31
+ if (input.sandboxChoice !== undefined && input.answer !== undefined && input.sandboxChoice !== input.answer) {
32
+ throw new Error('Conflicting cloud sandbox choices');
33
+ }
34
+ if ((input.sandboxChoice ?? input.answer) !== undefined && !['use', 'local'].includes(input.sandboxChoice ?? input.answer)) {
30
35
  throw new Error('cloud-task sandboxChoice must be use or local');
31
36
  }
32
37
  return { goal: input.goal, ...(input.ideTaskId === undefined ? {} : { ideTaskId: input.ideTaskId }) };
@@ -48,18 +53,19 @@ function authenticatedHeaders(token, headers = {}) {
48
53
  return { ...headers, authorization: `Bearer ${token}` };
49
54
  }
50
55
 
51
- export function sandboxChoiceInteraction(id) {
56
+ export function sandboxChoiceInteraction(id, task = {}) {
52
57
  return {
53
58
  schemaVersion: 'confirm.interaction/1.0', requestId: id, type: 'choice',
54
- question: '是否为本任务使用云端沙箱?',
59
+ question: '是否启用云端沙箱执行思考规划?',
55
60
  options: [
56
- { id: 'use', label: '本任务使用云端沙箱', hint: '创建真实云端任务并持续记录进度' },
57
- { id: 'local', label: '继续本地执行', hint: '仅当前任务退出云端沙箱' },
61
+ { id: 'use', label: '确认', hint: '为当前任务创建云端思考规划并显示真实进度' },
62
+ { id: 'local', label: '退出,采用本地方案', hint: '继续当前任务,不改变账号开关' },
58
63
  ],
59
- default: 'local', timeout: 8_000, timeoutAction: 'local', risk: 'low',
64
+ default: 'local', timeout: null, timeoutAction: 'local', risk: 'low',
60
65
  riskDescription: '这是当前任务的执行位置选择,不改变账号级云端沙箱开关。',
61
66
  rememberable: false, memoryKey: '',
62
- callback: { operation: 'cloud-task', payload: { sandboxChoice: 'use' } },
67
+ callback: { operation: 'cloud-task', payload: { ...task } },
68
+ chatFallback: '是否启用云端沙箱执行思考规划?\n1. 确认:当前任务使用云端沙箱\n2. 退出:自动采用本地方案,继续当前任务',
63
69
  };
64
70
  }
65
71
 
@@ -76,24 +82,30 @@ export async function runCloudTask(input, dependencies) {
76
82
  const task = validateInput(input);
77
83
  validateDependencies(dependencies);
78
84
  const id = requestId(task);
85
+ const choice = input.sandboxChoice ?? input.answer;
79
86
  await dependencies.install();
80
87
  if (!await dependencies.agreementAccepted(dependencies.home)) {
81
88
  return envelope(task, 'blocked', { code: 'agreement-required', interaction: dependencies.agreementRequired(dependencies.home) });
82
89
  }
83
- if (input.sandboxChoice === undefined) return envelope(task, 'needs-input', {
84
- task, interaction: sandboxChoiceInteraction(id),
90
+ if (choice === undefined) return envelope(task, 'needs-input', {
91
+ task, interaction: sandboxChoiceInteraction(id,task),
92
+ });
93
+ if (choice === 'local') return envelope(task, 'skipped', {
94
+ reason: 'task', executionIsolation: 'local', message: '已退出云端沙箱,采用本地方案继续当前任务。',
95
+ continuation: { goal: task.goal,...(task.ideTaskId ? { ideTaskId: task.ideTaskId } : {}) },
85
96
  });
86
- if (input.sandboxChoice === 'local') return envelope(task, 'skipped', { reason: 'task' });
87
97
 
88
98
  const token = await dependencies.readCredential(dependencies.home);
89
99
  const origin = trustedOrigin(dependencies.apiOrigin());
90
100
  const metadata = { kind: 'remote', operation: 'cloud-task', requestId: id };
91
101
  await appendInvocation({ ...metadata, event: 'started' }, dependencies.home);
102
+ displayIdeEvent({ schemaVersion: 'entrorove.ide-event/1.0',requestId: id,kind: 'status',
103
+ phase: 'enter',message: '正在进入云端沙箱',separateLine: true },dependencies.onIdeEvent);
92
104
  let response;
93
105
  try {
94
106
  response = await requestTransport(origin, '/api/cloud-task', {
95
107
  method: 'POST', timeoutMs: 30_000,
96
- headers: authenticatedHeaders(token, { 'content-type': 'application/json' }),
108
+ headers: authenticatedHeaders(token, { 'content-type': 'application/json',...ideClientHeaders() }),
97
109
  body: JSON.stringify(task),
98
110
  });
99
111
  } catch (cause) {
@@ -124,7 +136,8 @@ export async function runCloudTask(input, dependencies) {
124
136
  await appendInvocation({ ...metadata, event: 'completed' }, dependencies.home);
125
137
  return envelope(task, 'started', { taskId: result.taskId,
126
138
  taskStatus: typeof result.status === 'string' ? result.status : 'running',
127
- executionIsolation: typeof result.executionIsolation === 'string' ? result.executionIsolation : 'worker' });
139
+ executionIsolation: typeof result.executionIsolation === 'string' ? result.executionIsolation : 'worker',
140
+ nextAction: { operation: 'cloud-watch',input: { taskId: result.taskId,afterId: '0' } } });
128
141
  }
129
142
 
130
143
  function publicTask(task) {
@@ -162,6 +175,7 @@ export async function queryCloudTask(taskId, dependencies) {
162
175
  code: typeof taskBody?.error === 'string' ? taskBody.error : taskResponse.status === 404 ? 'cloud-task-not-found' : 'cloud-status-unavailable', httpStatus: taskResponse.status };
163
176
  }
164
177
  const task = taskBody.task;
178
+ const feedback = taskBody.feedback ? verifyAutomaticFeedback(taskBody.feedback,taskBody.feedback.requestId) : undefined;
165
179
  let usage = [];
166
180
  let usageError;
167
181
  const usageResponse = await requestTransport(origin, `/api/cloud-sandbox/tasks/${id}/usage`, {
@@ -174,6 +188,7 @@ export async function queryCloudTask(taskId, dependencies) {
174
188
  } catch { usageError = 'cloud-usage-invalid-response'; }
175
189
  await appendInvocation({ ...metadata, event: 'status' }, dependencies.home);
176
190
  return { schemaVersion: CLOUD_STATUS_SCHEMA, status: 'observed', taskId: id,
177
- taskStatus: task.status, task: publicTask(task), usage, ...(usageError ? { usageError } : {}) };
191
+ taskStatus: task.status, task: publicTask(task), usage, ...(feedback ? { feedback } : {}),
192
+ ...(usageError ? { usageError } : {}) };
178
193
  }
179
194
 
@@ -0,0 +1,58 @@
1
+ import { trustedOrigin } from './remote-transport.mjs';
2
+ import { consumeCloudStream } from './cloud-event-stream.mjs';
3
+ import { automaticFeedbackIdeEvent, cloudIdeEvent, displayIdeEvent, ideClientHeaders } from './ide-presentation.mjs';
4
+ import { queryCloudTask } from './cloud-task-client.mjs';
5
+
6
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7
+ function inputOptions(input) {
8
+ if (!input || !UUID.test(input.taskId) || Object.keys(input).some(key => !['taskId','afterId','waitMs'].includes(key))) {
9
+ throw new Error('cloud-watch requires its real taskId and optional afterId and waitMs');
10
+ }
11
+ const afterId = input.afterId ?? '0', waitMs = input.waitMs ?? 15000;
12
+ if (typeof afterId !== 'string' || !/^(0|[1-9][0-9]{0,18})$/u.test(afterId) || BigInt(afterId) > 9223372036854775807n
13
+ || !Number.isInteger(waitMs) || waitMs < 1 || waitMs > 45000) throw new Error('Invalid cloud-watch cursor or bounded wait');
14
+ return { taskId: input.taskId,afterId,waitMs };
15
+ }
16
+ export async function watchCloudTask(input, dependencies) {
17
+ const options = inputOptions(input), { taskId,waitMs } = options;
18
+ await dependencies.install();
19
+ if (!await dependencies.agreementAccepted(dependencies.home)) return { status: 'blocked',code: 'agreement-required',
20
+ interaction: dependencies.agreementRequired(dependencies.home) };
21
+ const token = await dependencies.readCredential(dependencies.home);
22
+ const controller = new AbortController(), deadline = setTimeout(() => controller.abort(),waitMs);
23
+ const events = [], emit = event => { events.push(event); displayIdeEvent(event,dependencies.onIdeEvent); };
24
+ let afterId = options.afterId, taskStatus = 'running', outputAvailable = false, settled = false, flowId;
25
+ try {
26
+ const response = await fetch(`${trustedOrigin(dependencies.apiOrigin())}/api/cloud-sandbox/tasks/${taskId}/events`, {
27
+ redirect: 'error',signal: controller.signal,headers: { authorization: `Bearer ${token}`,
28
+ accept: 'text/event-stream','last-event-id': afterId,...ideClientHeaders() } });
29
+ if (!response.ok) throw new Error(`Cloud event stream unavailable: HTTP ${response.status}; taskId=${taskId}`);
30
+ if (!response.headers.get('content-type')?.startsWith('text/event-stream')) throw new Error('Cloud event response is not an event stream');
31
+ await consumeCloudStream(response.body,frame => {
32
+ if (frame.event === 'stream-error') throw new Error(`Cloud event stream stopped: ${frame.data?.code ?? 'stream-failed'}`);
33
+ if (frame.event === 'status') {
34
+ if (frame.data?.taskId !== taskId || !['running','completed','blocked','failed','cancelled'].includes(frame.data.status)) throw new Error('Invalid cloud task status event');
35
+ taskStatus = frame.data.status; return taskStatus === 'running';
36
+ }
37
+ if (!/^[1-9][0-9]{0,18}$/u.test(frame.id ?? '') || frame.data?.id !== frame.id) throw new Error('Invalid cloud event cursor');
38
+ if (BigInt(frame.id) <= BigInt(afterId)) return;
39
+ const event = cloudIdeEvent(frame.data,taskId); afterId = frame.id; emit(event);
40
+ if (typeof frame.data.metadata?.flowId === 'string') flowId = frame.data.metadata.flowId;
41
+ settled = event.phase === 'workflow' && ['reported','verified','blocked','failed'].includes(event.state);
42
+ outputAvailable = settled && ['reported','verified'].includes(event.state);
43
+ if (settled) return false;
44
+ });
45
+ } catch (cause) {
46
+ if (!controller.signal.aborted) throw cause;
47
+ } finally { clearTimeout(deadline); controller.abort(); }
48
+ if (outputAvailable) emit({ schemaVersion: 'entrorove.ide-event/1.0',taskId,kind: 'status',phase: 'output',
49
+ message: '云端沙箱输出完毕;实际验证状态以任务回执为准。',separateLine: true });
50
+ const current = await queryCloudTask(taskId,dependencies);
51
+ if (current.status === 'observed') taskStatus = current.taskStatus;
52
+ if (current.feedback) emit(automaticFeedbackIdeEvent(current.feedback));
53
+ emit({ schemaVersion: 'entrorove.ide-event/1.0',taskId,kind: 'status',phase: 'exit',
54
+ message: settled || taskStatus !== 'running' ? '云端沙箱正在退出本次进度订阅' : '云端沙箱仍在执行,下次从当前进度继续读取',separateLine: true });
55
+ return { schemaVersion: 'entrorove.cloud-watch/1.0',status: current.status,taskId,taskStatus,afterId,outputAvailable,
56
+ events,...(flowId ? { flowId } : {}),task: current.task,usage: current.usage,...(current.feedback ? { feedback: current.feedback } : {}),
57
+ nextAction: taskStatus === 'running' && !settled ? { operation: 'cloud-watch',input: { taskId,afterId,waitMs } } : null };
58
+ }
@@ -0,0 +1,58 @@
1
+ const UUID = /^[0-9a-f-]{36}$/i;
2
+ export function ideClientHeaders(environment = process.env) {
3
+ const ide = environment.ENTROROVE_IDE ?? (environment.CODEX_THREAD_ID ? 'Codex'
4
+ : environment.CURSOR_TRACE_ID ? 'Cursor' : environment.CLAUDE_CODE_ENTRYPOINT ? 'Claude Code' : 'Entrorove CLI');
5
+ if (typeof ide !== 'string' || !/^[\p{L}\p{N} .+_-]{2,48}$/u.test(ide)) throw new Error('Invalid ENTROROVE_IDE client name');
6
+ return { 'x-entrorove-ide': ide };
7
+ }
8
+ export function displayIdeEvent(event, sink) {
9
+ if (sink) sink(event);
10
+ else process.stderr.write(`${event.message}\n${event.detail ? event.detail + '\n' : ''}`);
11
+ }
12
+ function text(value) {
13
+ return typeof value === 'string' ? value.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/gu, '').slice(0,12000) : '';
14
+ }
15
+ export function cloudIdeEvent(value, taskId) {
16
+ if (!value || value.taskId !== taskId || !UUID.test(taskId) || !/^[1-9][0-9]{0,18}$/.test(value.id)
17
+ || !['thinking','execution','notice','error'].includes(value.kind) || typeof value.content !== 'string') {
18
+ throw new Error('Cloud event lost its original task identity');
19
+ }
20
+ const metadata = value.metadata ?? {};
21
+ let message = text(value.content), phase = String(metadata.stepId ?? 'workflow');
22
+ if (message === 'Cloud task accepted.') message = '已进入云端沙箱,正在分配沙箱任务';
23
+ else if (message === 'Cloud workflow started.') message = '云端沙箱任务已分配';
24
+ else if (message === 'Cloud model classification started.') message = '云端沙箱正在思考中';
25
+ else if (message === 'Task classification completed.') message = '云端沙箱思考正在输出';
26
+ const event = { schemaVersion: 'entrorove.ide-event/1.0',taskId,eventId: value.id,kind: value.kind,
27
+ state: metadata.state ?? 'running',phase,message,separateLine: true };
28
+ if (typeof metadata.summary === 'string' && metadata.summary !== value.content
29
+ && value.content !== 'Cloud model classification started.') event.detail = text(metadata.summary);
30
+ if (value.kind === 'notice' && metadata.url !== undefined) {
31
+ let url;
32
+ try { url = new URL(metadata.url); } catch { throw new Error('Cloud promotion link is invalid'); }
33
+ if (url.protocol !== 'https:' || url.username || url.password) throw new Error('Cloud promotion requires a public HTTPS link');
34
+ event.url = url.href;
35
+ event.message = `[${message.replace(/[\\[\]]/gu, '\\$&')}](<${url.href}>)`;
36
+ }
37
+ return event;
38
+ }
39
+ export function invocationIdeNotices(output) {
40
+ const strings = [output?.message,output?.result?.message,
41
+ ...(Array.isArray(output?.progress) ? output.progress.filter(item => item.kind === 'notice').map(item => item.summary) : [])];
42
+ return [...new Set(strings.filter(value => typeof value === 'string').flatMap(value => value.split(/\r?\n/u))
43
+ .filter(value => value === '智能执行中,请勿关闭!' || /^【[^\n】]{1,32}】/u.test(value)).map(text))];
44
+ }
45
+ export function presentInvocationNotices(result, sink) {
46
+ const messages = invocationIdeNotices(result?.output);
47
+ if (result?.feedback) messages.push(automaticFeedbackIdeEvent(result.feedback).message);
48
+ for (const message of messages) displayIdeEvent({ schemaVersion: 'entrorove.ide-event/1.0',
49
+ requestId: result.output.requestId,kind: 'notice',message,separateLine: true },sink);
50
+ return messages.length ? { ...result,ideMessages: messages } : result;
51
+ }
52
+ export function automaticFeedbackIdeEvent(feedback) {
53
+ const disposition = feedback.evaluation.disposition;
54
+ const status = disposition === 'good' ? '当前调用验证通过' : disposition === 'bad'
55
+ ? '当前调用被阻断或验证失败' : '交付验证尚未完成,暂不评分';
56
+ return { schemaVersion: 'entrorove.ide-event/1.0',requestId: feedback.requestId,receiptId: feedback.receiptId,
57
+ kind: 'evaluation',phase: 'feedback',message: `本次调用评价已回传至服务器:${status}。`,separateLine: true };
58
+ }
package/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.33",
4
+ "version": "0.1.34",
5
5
  "endpoint": "https://entrorove.com",
6
6
  "requestSchemaVersion": "entrorove.skill.request/2.0",
7
7
  "supportedRequestSchemaVersions": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entrorove/skill",
3
- "version": "0.1.33",
3
+ "version": "0.1.34",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "entrorove": "cli.mjs"
@@ -78,6 +78,10 @@
78
78
  "remote-client.mjs",
79
79
  "scope-runner-client.mjs",
80
80
  "cloud-task-client.mjs",
81
+ "cloud-watch-client.mjs",
82
+ "cloud-event-stream.mjs",
83
+ "ide-presentation.mjs",
84
+ "automatic-feedback-client.mjs",
81
85
  "remote-transport.mjs",
82
86
  "local-credential.mjs",
83
87
  "local-invocations.mjs",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "entrorove.release-state/1.0",
3
- "version": "0.1.33",
4
- "digest": "79b9167ef252402e374202c08093e75e98fe3bf41227a53f34a44fa757834fe2",
3
+ "version": "0.1.34",
4
+ "digest": "88b67ef8c4be3ddf86777218a7324b095e37b2e6df30a002ac267a48548e2a31",
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": "d06e372e7f54f726b700b0d909669616c5f35aa7eaa6c67c008d0670ac18678a",
47
+ "cli.mjs": "b890c2173195edc9ccbaa1f9d1b45a845cad0f1838c300d90a77b1f776c8f178",
48
48
  "review-client.mjs": "3491eef68ed533174b31db3c53febb6519b8f088f71a9964644714a9a4c03c77",
49
49
  "version.mjs": "1f392147d72c40af4b03b307ea607075fb815af5174fa07e254b95fe12dbf3e7",
50
- "SKILL.md": "726140d61da8561a6a87ff818931964994ad5356c56c45afeb6b5133609697ab",
51
- "CAPABILITY-MAP.md": "4b8479928481289b82540ff2624ebb16d1f0a5b6697b6eec52d2d459f86a0001",
50
+ "SKILL.md": "11469bd5f51e008c26492d2e01401959451c259e04699dd4b8c833701c27d0b2",
51
+ "CAPABILITY-MAP.md": "f1dd06d99f191daa27f7b1520a3d6d26b1ceba139d30cd287d2b9bfd79459923",
52
52
  "AGREEMENT.md": "f93a68e92c457200dd024610e65f74e478b8f60860f951da818b70561f1b9bf8",
53
- "package.json": "2afd7c74c464da6f8fa7dfa58d465104e77c1d332ec2b4adb17a8df518b0d3ba",
53
+ "package.json": "559c14f546a781839ed5e741602d26f888a9c98399a397b788c0fc17da1c4083",
54
54
  "manifest.json": "ab06e307795bc9da5add926752b6c4dbdaf18625090261a035aa356a679c504f",
55
- "release.mjs": "e33a80cb7463f4db532af18d26880b198de637d376ce1952d751668bbd8df44d",
55
+ "release.mjs": "1b1925df27dd26164874d291c2dbac9a13010fca7892a9d5b529a3e6e99c0139",
56
56
  "local-cli.mjs": "a79037d7383a29822b465e6db1c862a43d47cc344f05b6651c849633c49d19f7",
57
57
  "local-state-lock.mjs": "1dbf64264642a0851faf043e07ee9f66d2ad0508e9f799b0626246e42fae893e",
58
58
  "local-execution.mjs": "cdada6df6dc8831cebb58a629b56c4b3f8e1bf9aae4e3fdc121521ba20839383",
@@ -118,9 +118,13 @@
118
118
  "local-scope-state.mjs": "8b600a91fc0068d7a8c0bb752ddba7ed30ada93daafb118d6cba2368fd94cb27",
119
119
  "local-scope-workspaces.mjs": "a44c34537e5ed2dcaf193a985e5bc05957989fbb160a650f9e37ba7d66eb2de8",
120
120
  "local-task-routing.mjs": "e26d6f1ba134ffe1da2a49b4fb6bb6dfa5617671f774a08e719d04f202cf5eca",
121
- "remote-client.mjs": "6df222e3e55a893fda2aa7a15feb545db5ff9d74e33025a1b0e3f03b849f5d14",
122
- "cloud-task-client.mjs": "d519e2c020e19a67e8c6e32d2da39bc3fa45538b773e7baf02605505d065daaf",
121
+ "remote-client.mjs": "3c95a81bcad3e53f63071a87929dae4b7c71e6a45034263269c731d9d494d529",
122
+ "cloud-task-client.mjs": "ecb6124ea5e6249f31fb95cb68d13d821d2daea8768fe47ddf6dcf82fc3392cc",
123
123
  "scope-runner-client.mjs": "940c841e2c56e92793e5bf65e713bf4087151f721d0ecb659e0a75d6fdefeab9",
124
+ "cloud-watch-client.mjs": "bfbe636c10af467855776f9d29008d9ce13420b66bcc91b5bf8f6ef79fb2aefe",
125
+ "cloud-event-stream.mjs": "e0eec9fc3e0613094523929634f25030df5ca5c5b5fe2c1e1a4586ab10574808",
126
+ "ide-presentation.mjs": "026bf425d06276300ca748a8e22f66596ee52ff268fe0e6ffeff230885533d9a",
127
+ "automatic-feedback-client.mjs": "817e76b7ca4b5a282b9408ac9cf3daf7e09085db5aacbb49d74d8b6bcc4bb611",
124
128
  "remote-transport.mjs": "5e5a8c0b490371f7f5a052cffbeee6202d4cf713dff1c43b6d910fe944f8429c",
125
129
  "local-credential.mjs": "2a302a7f67b5ea8dc73453fc96ab33db349b59ca11fa9456e48ca1d9669d877c",
126
130
  "local-invocations.mjs": "f6ac4c41c1a5ec34de358c4cca210ca8e403b3a913150a43bb55ec9be55d9998",
package/release.mjs CHANGED
@@ -92,6 +92,7 @@ const sourceFiles = [
92
92
  'local-scope-workspaces.mjs',
93
93
  'local-task-routing.mjs', 'remote-client.mjs', 'cloud-task-client.mjs',
94
94
  'scope-runner-client.mjs',
95
+ 'cloud-watch-client.mjs', 'cloud-event-stream.mjs', 'ide-presentation.mjs', 'automatic-feedback-client.mjs',
95
96
  'remote-transport.mjs', 'local-credential.mjs', 'local-invocations.mjs',
96
97
  'local-task-handoff-fence.mjs',
97
98
  'local-task-handoff-storage.mjs', 'local-task-handoff-overlay-wire.mjs', 'local-task-handoff-completion-wire.mjs',
@@ -354,6 +355,10 @@ function releaseLabels() {
354
355
  'task-message-entry.mjs': { zh: '任务消息实际投递入口', en: 'task message host delivery entry' },
355
356
  'task-message-cli.mjs': { zh: '任务消息命令与适配器', en: 'task message command and adapter' },
356
357
  'cloud-task-client.mjs': { zh: '云端沙箱任务路由与逐任务选择', en: 'cloud sandbox task routing and per-task choice' },
358
+ 'cloud-watch-client.mjs': { zh: 'IDE 实时云端进度与推广展示', en: 'live IDE cloud progress and promotion display' },
359
+ 'cloud-event-stream.mjs': { zh: 'IDE 实时云端进度与推广展示', en: 'live IDE cloud progress and promotion display' },
360
+ 'ide-presentation.mjs': { zh: 'IDE 实时云端进度与推广展示', en: 'live IDE cloud progress and promotion display' },
361
+ 'automatic-feedback-client.mjs': { zh: '原调用自动评价回执核验', en: 'original invocation automatic evaluation receipt verification' },
357
362
  })
358
363
  }
359
364
 
package/releases.json CHANGED
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.1.34",
4
+ "date": "2026-10-12",
5
+ "zh": "补回旧版八技能的 IDE 可见云端选择、真实事件进度与独立推广提示;服务端自动评价绑定原调用并返回可验证回执,支持只读恢复;修复旧式 UUID 调用漏计用量,保持账号隔离及沙箱/调用计数分离。",
6
+ "en": "Restore visible IDE cloud choice, authoritative live progress and separate promotion notices from the original eight-skill workflow. Persist account-bound automatic invocation evaluations with verifiable receipts and read-only recovery; meter legacy UUID calls without mixing skill and sandbox counters."
7
+ },
2
8
  {
3
9
  "version": "0.1.33",
4
10
  "date": "2026-10-12",
package/remote-client.mjs CHANGED
@@ -2,6 +2,8 @@ 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';
6
+ import { ideClientHeaders, presentInvocationNotices } from './ide-presentation.mjs';
5
7
 
6
8
  export const apiUrl = 'https://entrorove.com/api/skill/invoke';
7
9
 
@@ -39,6 +41,11 @@ export function buildInvocation(operation, input) {
39
41
  const id = requestId(operation, input);
40
42
  return { url: apiUrl, requestId: id, body: { schemaVersion: 'entrorove.skill.request/2.0', requestId: id, operation, input } };
41
43
  }
44
+ function requiresAutomaticFeedback(operation, input, capabilities) {
45
+ const actual = operation === 'continue' ? input.operation : operation;
46
+ return capabilities?.responseExtensions?.automaticFeedback === 'entrorove.automatic-feedback/1.0'
47
+ && !capabilities.responseExtensions.automaticFeedbackObservations?.includes(actual);
48
+ }
42
49
 
43
50
  export function validateDiscoveredChecks(input, checks = []) {
44
51
  for (const check of checks) {
@@ -65,7 +72,7 @@ export async function invokeRemote(data, dependencies) {
65
72
  }
66
73
  const token = await readCredential(home);
67
74
  const { response, result } = await invokeDiscovered(data, token, { home, apiOrigin });
68
- process.stdout.write(JSON.stringify(result) + '\n');
75
+ process.stdout.write(JSON.stringify(presentInvocationNotices(result,dependencies.onIdeEvent)) + '\n');
69
76
  if (!response.ok || result.output.status === 'blocked' || result.output.status === 'failed'
70
77
  || result.output.status === 'incomplete') process.exitCode = 2;
71
78
  }
@@ -76,20 +83,22 @@ export async function invokeRemoteResult(data, dependencies) {
76
83
  }
77
84
  async function invokeDiscovered(data, token, { home, apiOrigin }) {
78
85
  const call = { ...buildInvocation(data.operation, data.input), url: `${trustedOrigin(apiOrigin())}/api/skill/invoke` };
86
+ let feedbackRequired = false;
79
87
  if (data.operation !== 'capabilities') {
80
88
  const discovery = await postInvocation({ ...buildInvocation('capabilities', {}), url: call.url }, token, home);
81
89
  if (!discovery.response.ok || discovery.result.output.status !== 'succeeded') throw new Error(`Capability discovery failed: HTTP ${discovery.response.status}`);
82
90
  const capabilities = discovery.result.output.capabilities;
91
+ feedbackRequired = requiresAutomaticFeedback(data.operation,data.input,capabilities);
83
92
  const schema = capabilities?.operationSchemas?.[data.operation];
84
93
  if (!capabilities?.operations?.includes(data.operation) || !schema) throw new Error(`Capability discovery does not support ${data.operation}`);
85
94
  const validate = discoveredAjv().compile(schema);
86
95
  if (!validate(data.input)) throw new Error(`Invalid ${data.operation} input: ${validate.errors.map(error => `${error.instancePath || '/'} ${error.message}`).join('; ')}`);
87
96
  validateDiscoveredChecks(data.input, capabilities.inputChecks?.[data.operation]);
88
97
  }
89
- return postInvocation(call, token, home);
98
+ return postInvocation(call, token, home,feedbackRequired);
90
99
  }
91
100
 
92
- async function postInvocation(call, token, home) {
101
+ async function postInvocation(call, token, home, feedbackRequired = false) {
93
102
  const metadata = { kind: 'remote', operation: call.body.operation, requestId: call.requestId };
94
103
  const logged = async value => {
95
104
  try { return await appendInvocation({ ...metadata, ...value }, home); }
@@ -100,7 +109,7 @@ async function postInvocation(call, token, home) {
100
109
  try {
101
110
  response = await requestTransport(new URL(call.url).origin, '/api/skill/invoke', {
102
111
  method: 'POST', timeoutMs: 135_000,
103
- headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` }, body: JSON.stringify(call.body),
112
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${token}`,...ideClientHeaders() }, body: JSON.stringify(call.body),
104
113
  });
105
114
  } catch (cause) {
106
115
  if (cause.established) await logged({ event: 'outcome-unknown' });
@@ -122,6 +131,10 @@ async function postInvocation(call, token, home) {
122
131
  await logged({ event: 'outcome-unknown' }); throw unknownInvocation(call.requestId);
123
132
  }
124
133
  await logged({ event: response.ok ? 'completed' : 'rejected', usage: result.usage ?? [] });
134
+ if (result.feedback) verifyAutomaticFeedback(result.feedback,call.requestId,call.requestId);
135
+ else if (feedbackRequired && !['running','pending'].includes(result.output.status)) {
136
+ throw new Error(`Automatic feedback receipt is missing; query feedback ${call.requestId}. Do not repeat the invocation.`);
137
+ }
125
138
  return { response, result };
126
139
  }
127
140
 
@@ -158,13 +171,16 @@ export function createTaskMessageClient(dependencies) {
158
171
  async invoke(operation, input) {
159
172
  const original = buildInvocation(operation, input);
160
173
  const token = await machineCredential(dependencies); const origin = machineOrigin(dependencies);
174
+ let feedbackRequired = false;
161
175
  if (operation !== 'capabilities') {
162
176
  const discovery = buildInvocation('capabilities', {});
163
177
  const discovered = await postInvocation({ ...discovery, url: `${origin}/api/skill/invoke` }, token, dependencies.home);
164
178
  if (!discovered.response.ok || discovered.result.output.status !== 'succeeded') throw new Error('Task message capability discovery failed');
165
179
  validateMachineCapability(operation, input, discovered.result.output.capabilities);
180
+ feedbackRequired = requiresAutomaticFeedback(operation,input,discovered.result.output.capabilities);
166
181
  }
167
- const { response, result } = await postInvocation({ ...original, url: `${origin}/api/skill/invoke` }, token, dependencies.home);
182
+ const { response, result } = await postInvocation({ ...original, url: `${origin}/api/skill/invoke` }, token, dependencies.home,feedbackRequired);
183
+ presentInvocationNotices(result,dependencies.onIdeEvent);
168
184
  if (stoppedOriginalResume(response, result.output, operation, input)) return result.output;
169
185
  if (!response.ok || ['blocked', 'failed', 'incomplete'].includes(result.output.status)) {
170
186
  throw new Error(`Task message invocation was rejected: HTTP ${response.status}; requestId=${original.requestId}`);
@@ -178,6 +194,8 @@ export function createTaskMessageClient(dependencies) {
178
194
  { authorization: `Bearer ${token}` }, 'original task message status');
179
195
  const result = await readResponse(response, 'task message status');
180
196
  if (!response.ok || !result?.output || result.output.requestId !== id) throw new Error('Original task message status is unavailable or invalid');
197
+ if (result.feedback) verifyAutomaticFeedback(result.feedback,id);
198
+ presentInvocationNotices(result,dependencies.onIdeEvent);
181
199
  await appendInvocation({ kind: 'remote', operation: 'status', requestId: id, event: 'status', usage: result.usage ?? [] }, dependencies.home);
182
200
  return result.output;
183
201
  },
@@ -191,7 +209,8 @@ export async function queryStatus(requestId, dependencies) {
191
209
  const result = await readResponse(response, 'status');
192
210
  if (!response.ok || result?.output?.requestId !== requestId) throw new Error(`entrorove status failed: HTTP ${response.status}; requestId=${requestId}`);
193
211
  await appendInvocation({ kind: 'remote', operation: 'status', requestId, event: 'status', usage: result.usage ?? [] }, dependencies.home);
194
- process.stdout.write(JSON.stringify(result) + '\n');
212
+ if (result.feedback) verifyAutomaticFeedback(result.feedback,requestId);
213
+ process.stdout.write(JSON.stringify(presentInvocationNotices(result,dependencies.onIdeEvent)) + '\n');
195
214
  }
196
215
 
197
216
  export async function queryFlows(dependencies) {