@entrorove/skill 0.1.59 → 0.1.61

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.61 — 2026-10-18
4
+
5
+ - 钩子改为可选:默认安装关闭钩子,IDE 可直接调用 SDK;只有明确选用技能的原会话与仓库启用拦截,普通查询、旧激活状态及无法归属的事件不再接管其他任务。保留账号、写入范围、签名和真实本地验收校验。
6
+ - Make hooks optional: default installs disable hooks and IDEs can call the SDK directly. Interception requires explicit selection of the original session and repository; discovery, legacy activation and unidentified events cannot govern unrelated work. Preserve account, mutation scope, signatures and actual local acceptance checks.
7
+
8
+ ## 0.1.60 — 2026-10-17
9
+
10
+ - 修复范围补正误用独立交接的完整依赖校验,接收原账号签名器的真实受限扫描并保留未解析依赖、原审批与预算;新增当前 IDE 会话离线 exit,识别明确停用决定并保持退出状态,不再要求重建流程。
11
+ - Fix scope amendments to verify the original member signer wire receipt while preserving unresolved imports, owner approval and native budgets. Add offline exit for the current IDE session, recognize explicit stop decisions and preserve exited governance without requiring a replacement flow.
12
+
3
13
  ## 0.1.59 — 2026-10-17
4
14
 
5
15
  - 新增同一任务的精确范围补正:原账号审核文件哈希与写入/只读角色,原S签名真实事实并重评原合同;计划、写入和结算绑定不可变范围修订,改后刷新保留原M消费证据与失败历史。
package/IDE-EXECUTION.md CHANGED
@@ -1,6 +1,11 @@
1
- Install or update the managed copy with `npx @entrorove/skill install`. In Codex, use `npx @entrorove/skill install --host codex` to configure Codex only; this leaves Claude Code hook settings and skill copies alone. The installer keeps one global managed entrorove skill, replaces its older pristine version, removes only pristine duplicate installations for the selected host, and preserves hand-edited files. An older package cannot overwrite a newer managed version.
1
+ Install or update the managed copy with `npx @entrorove/skill install --host codex`. The default installs the skill with hooks disabled; SDK operations work directly in Codex, Claude Code or other IDEs without a hook approval. An explicit default reinstall removes only the recorded Codex hooks. It leaves Claude Code settings and skill copies alone, preserves hand-edited files and cannot downgrade a newer managed skill. Ordinary commands never install the skill or restore removed callbacks.
2
2
 
3
- Host hook events, including multimodal `PostToolUse` screenshot results, accept at most 32 MiB (33,554,432 UTF-8 bytes). The complete bounded event is decoded once and follows the original session, tool-call and signature checks; image results do not grant permissions or erase a pending call. Oversized events return `ENTROROVE_IDE_INPUT_TOO_LARGE`; invalid UTF-8 or JSON objects return `ENTROROVE_IDE_INPUT_INVALID_JSON`. These fixed diagnostics do not include the event contents. Private IDE state JSON files retain their separate 1,000,000-byte limit. A hook error after a tool action does not prove the action was unexecuted: inspect the original result or status before considering another attempt.
3
+ Hooks are optional: use `install --host codex --hooks` only when the user requests interception. Registration alone is passive. An explicit prompt (`使用 entrorove` or `$entrorove`) selects that exact IDE session and physical repository. Discovery, status, SDK calls and legacy active bits never select it. Unselected callbacks create no session/queue state and cannot deny native tools or block Stop. `install --host codex --no-hooks` returns to direct SDK mode. A selected callback adds `ENTROROVE_HOOK_MODE=required` only to its rewritten SDK invocation; the CLI validates the original exact approval and account for that call. Do not set this internal marker in ordinary direct SDK commands.
4
+
5
+ Without hooks, explicitly call the returned SDK operations, preserve request IDs and signed receipts, and recover unknown outcomes through status. Account authentication, agreement, original scope, mutation passes and real test/acceptance checks are unchanged. SDK receipts cover only SDK operations; native IDE edits are not automatically intercepted, recorded or certified. Automatic prompt queue capture, callback synchronization and Stop continuation require an enabled, explicitly selected hook. Cloud sandbox preference is independent of hook mode.
6
+
7
+
8
+ Host hook events, including multimodal `PostToolUse` screenshot results, accept at most 32 MiB (33,554,432 UTF-8 bytes). The complete bounded event is decoded once and follows the original session, tool-call and signature checks; image results do not grant permissions or erase a pending call. Oversized events return `ENTROROVE_IDE_INPUT_TOO_LARGE`; invalid UTF-8 or JSON objects return `ENTROROVE_IDE_INPUT_INVALID_JSON`. These fixed diagnostics do not include the event contents. Malformed transport cannot identify a selected session, so it returns a nonblocking diagnostic rather than stopping unrelated work; errors inside a positively identified selected session still reject the affected call. Private IDE state JSON files retain their separate 1,000,000-byte limit. A hook error after a tool action does not prove the action was unexecuted: inspect the original result or status before considering another attempt.
4
9
 
5
10
  Register or sign in by email at https://entrorove.com. Enter the account key once with `npx @entrorove/skill configure` and `{"token":"..."}` on standard input. The credential persists in the current OS user's restricted local store across skill upgrades. Before asking again, use the stored credential; a missing or invalid credential must be reported explicitly. Never place the key in arguments, URLs, project files or logs. entrorove accounts and keys are independent of other services.
6
11
 
@@ -14,6 +19,8 @@ Approved Codex `exec_command` reads and SDK calls run with `/bin/sh` and `login:
14
19
 
15
20
  For a Codex task-to-task report explicitly authorized by the human user, the guard passes the exact `mcp__codex_app__send_message_to_thread` tool through unchanged, including a static `tools.mcp__codex_app__send_message_to_thread({...})` call inside `functions.exec`. Codex still decides whether the user authorized the message. Its text and response grant no Entrorove flow, file, shell or task-creation authority.
16
21
 
22
+ To stop governance only in the current IDE session after an explicit user decision, run `entrorove exit`. It needs no network, credential, agreement or hook callback and preserves original evidence and queues. Other sessions remain unchanged. `flow-cancel` and `flow-abandon` settle a flow; they do not express a decision to stop using the skill.
23
+
17
24
  To stop using Entrorove on this computer, run `entrorove uninstall` directly. It works offline, removes only the recorded Entrorove host hooks and pristine global skill, and leaves other host settings, credentials and task records in place. A hand-edited file is reported as preserved rather than deleted. This command does not inspect or change another project.
18
25
 
19
26
  Every invocation rereads that same credential file, including clients already running. After an authenticated account page supplies the current key and `configure` synchronizes it once, all tasks under the same OS user automatically use the new value. A revoked or expired key cannot obtain new permissions for itself: return to the authenticated account page and synchronize once through JSON stdin. Status recovery only queries the original request; it never rotates credentials or repeats the operation.
package/SKILL.md CHANGED
@@ -1,17 +1,18 @@
1
1
  ---
2
2
  name: entrorove
3
- description: Execute an engineering task through Entrorove, preserving its original scope, approvals, cloud task identity and verifiable result across every IDE turn.
3
+ description: Execute a task selected for Entrorove through explicit SDK operations, preserving its scope, approvals and verifiable results. Hooks are optional; unrelated IDE work remains outside its governance.
4
4
  ---
5
5
 
6
6
  # entrorove · 熵巡
7
7
 
8
- Version: 0.1.59.
8
+ Version: 0.1.61.
9
9
 
10
- Entrorove applies to the whole task once selected. Reading this document or calling an entry once does not execute the task. On every later turn, including after compaction, continue the original task and its required next operation before dependent work. Keep the original goal, scope, account, flow, cloud task and failure records. Never describe planning, a reported step or model output as verified delivery.
10
+ Entrorove applies only to the task for which it is selected; reading these instructions, discovering capabilities or installing the package never selects or governs another task. Hooks are optional. Reading this document or calling an entry once does not execute the task. On every later turn, including after compaction, continue the original task and its required next operation before dependent work. Keep the original goal, scope, account, flow, cloud task and failure records. Never describe planning, a reported step or model output as verified delivery.
11
11
 
12
12
  ## Required execution
13
13
 
14
- 1. Install `npx --yes --prefer-online @entrorove/skill@latest install --host codex` in Codex only when the user chooses to enable this skill. The managed installer writes the skill and Codex lifecycle hook configuration without changing Claude Code settings; confirm that the IDE trusts it and observe a real callback before describing enforcement as active. Do not ask the user to type a nonexistent `/hooks` command. If the host reports a blocked management call, show and allow the offline `entrorove unlock` command; it only opens the skill management plane and never grants project-write authority. New execution from a detected IDE requires the exact live host approval. An unsupported IDE adapter, missing hook callback, missing credential, or failed discovery stops execution explicitly. Do not silently use native tools instead. The guard is host-agnostic: one managed hook is installed once, and each host is described by its own adapter (config location, event names, entry shape). Codex and Claude Code are the verified adapters today; a host with no verified adapter, or a run whose host session identity is missing, is refused explicitly rather than degrading to native tools.
14
+ 1. Install `npx --yes --prefer-online @entrorove/skill@latest install --host codex` only when the user chooses to install this skill. The default installs the skill with hooks disabled and removes its recorded Codex hooks on upgrade, preserving other host settings. SDK calls work directly in an IDE without a hook callback or supported hook adapter; retain the actual host identity. Missing credentials, failed discovery and the SDK/API's scope, approval and evidence checks still return explicit failures.
15
+ Only when the user requests automatic interception, use `install --host codex --hooks`. The hook stays passive until this session receives an explicit selection such as `使用 entrorove` or `$entrorove`. SDK discovery, installation, another session, another repository and old automatic activation records cannot select it. In a selected session, the hook binds each rewritten SDK call to its exact live approval. `install --host codex --no-hooks` disables only the recorded Codex hooks; ordinary calls never reinstall removed hooks. A disabled hook cannot be described as enforced. Do not ask the user to type a nonexistent `/hooks` command.
15
16
  `entrorove unlock` is the break-glass path: run the installed CLI directly in the affected IDE terminal. It writes a short-lived lease for that IDE session only, without a hook callback, network request, credential read, agreement check or protocol lookup. The current session ID must be available from the host environment or `ENTROROVE_SESSION`. The lease permits `flow-cancel` and `flow-abandon` recovery, never project writes or another session's calls. A successful `flow-abandon` clears that session's old binding locally even without a hook callback and preserves its queued requests.
16
17
  To remove this skill and its host hooks, run `entrorove uninstall` directly. It works offline and preserves unrelated host settings, credentials and task records. If a managed file was edited, it reports the preserved path instead of deleting it.
17
18
  2. Read [IDE-EXECUTION.md](IDE-EXECUTION.md) for the exact entry, cloud callbacks, account agreement and operation contracts. Use the stored account credential; never put it in arguments, URLs, project files, output or fixtures. Entrorove is independent of other systems.
@@ -26,7 +27,7 @@ Entrorove applies to the whole task once selected. Reading this document or call
26
27
  8. Read [DELIVERY-BOUNDARIES.md](DELIVERY-BOUNDARIES.md) before claiming a delivered stage or showing consumption. A delivered stage requires a clean boundary and uses `delivered-stage-N`. Blocked or unknown work can wait with recovery data intact; it is not a delivered stage. Display consumption only with a measured baseline.
27
28
  9. For independent verified completion, submit actual independent execution evidence and run server verification and verdict. For a signed member-local plan, finish all original local steps and requirement checkpoints, then call the returned `workflow-verdict`; only its signed final `localAcceptance` means local acceptance is complete. Describe it as local execution and tests accepted, with independent validation not performed. Never invoke formal V, merge, Swarm or handoff to relabel it. Display the confirmed evaluation acknowledgement and preserve `receiptId`, `requestId`, execution generation and evaluation digest. “已回传” requires `acknowledgement.stored: true`. Missing or failed feedback remains visible. Active, waiting and unknown tasks retain recovery data.
28
29
 
29
- The Codex guard preserves this binding over subsequent prompts and compaction and denies unsupported native mutations before execution. Hooks are host guardrails, not an operating-system security boundary; unsupported tool paths or a disabled/untrusted hook cannot be described as enforced. An explicit user decision to stop using the skill ends its governance and never becomes verified Entrorove delivery. Do not infer such a decision from quoted screenshots or a service failure.
30
+ When explicitly enabled and selected, the Codex guard preserves this binding over subsequent prompts and compaction and denies unsupported native mutations before execution. Without hooks, explicitly execute the returned SDK operations and retain their real request IDs and receipts; automatic prompt queue capture, callback recovery and Stop interception are unavailable. SDK evidence covers only operations performed through the SDK, not arbitrary IDE edits. Hooks are host guardrails, not an operating-system security boundary; unsupported tool paths or a disabled/untrusted hook cannot be described as enforced. An explicit user decision to stop using the skill ends its governance and never becomes verified Entrorove delivery. Do not infer such a decision from quoted screenshots or a service failure. For that explicit decision, run `entrorove exit` in the affected IDE session. It works offline without a credential, agreement or hook callback, stops governance for that session and preserves its original flow, failed requests and queued work. It does not cancel the business goal, resume another flow or grant an Entrorove mutation pass. Do not substitute `flow-abandon` for user exit: abandonment releases a flow binding but leaves the skill selected.
30
31
 
31
32
  After the same real blocked result occurs twice, diagnose it and use its supported recovery path. Do not send a third identical execution call. Read-only status and checkpoint acknowledgements do not count as execution progress. A blocked task may wait with its original flow, queue and recovery data intact; Stop must not force repeated automatic continuation of the same blocked turn.
32
33
 
package/cli.mjs CHANGED
@@ -21,6 +21,8 @@ import { activeIdeAdapters, ideAdapters, installIdeHooks, uninstallIdeHooks } fr
21
21
  import { cleanManagedDuplicates } from './install-cleanup.mjs';
22
22
  import { assertIdeExecution, ideGuardBinding, settleBreakGlassSession } from './ide-session.mjs';
23
23
  import { writeBreakGlass } from './break-glass.mjs';
24
+ import { exitIdeSession } from './ide-exit.mjs';
25
+ import { installOptions } from './ide-command.mjs';
24
26
  const markerName = '.entrorove-managed.json';
25
27
  const installFiles = ['SKILL.md', 'CAPABILITY-MAP.md', 'AGREEMENT.md', 'manifest.json', 'IDE-EXECUTION.md', 'REUSE-DISCIPLINE.md', 'DELIVERY-BOUNDARIES.md'];
26
28
  const markerSchema = 'entrorove.managed-install/1.0';
@@ -246,8 +248,8 @@ async function updateGlobalInstall(target, bundle) {
246
248
  return true;
247
249
  }
248
250
 
249
- export async function install({ home = os.homedir(), cwd = process.cwd(), explicit = false, hostId } = {}) {
250
- const selected = hostId === undefined ? null : ideAdapters.find(adapter => adapter.id === hostId);
251
+ export async function install({ home = os.homedir(), cwd = process.cwd(), explicit = false, hostId, hooks = false } = {}) {
252
+ const selected = ideAdapters.find(adapter => adapter.id === (hostId ?? 'codex'));
251
253
  if (hostId !== undefined && hostId !== 'codex') throw new Error('Install host must be codex');
252
254
  const target = globalInstallPath(home);
253
255
  if (!explicit && !(await managedState(target))) return { installed: false, updated: false };
@@ -255,16 +257,19 @@ export async function install({ home = os.homedir(), cwd = process.cwd(), explic
255
257
  await fs.mkdir(parent, { recursive: true, mode: 0o700 });
256
258
  return withInstallLock(parent, async () => {
257
259
  const active = explicit ? null : await activeIdeAdapters({ home });
258
- if (!explicit && (!(await managedState(target)) || active.length === 0)) return { installed: false, updated: false };
260
+ if (!explicit && !(await managedState(target))) return { installed: false, updated: false };
259
261
  const bundle = await sourceBundle();
260
262
  const stale = await agreementStale(home);
261
263
  const updated = await updateGlobalInstall(target, bundle);
262
264
  const { removed, preserved } = await cleanManagedDuplicates({
263
265
  target, cwd, home, isManaged: managedState, includeProject: explicit,
264
- hostIds: selected ? [selected.id] : active?.map(adapter => adapter.id),
266
+ hostIds: explicit ? [selected.id] : active.map(adapter => adapter.id),
265
267
  });
266
- const ide = await installIdeHooks({ home, version: bundle.manifest.version,
267
- adapters: selected ? [selected] : active });
268
+ const targets = explicit ? hooks ? [selected] : [] : active;
269
+ const disabled = explicit && !hooks ? await uninstallIdeHooks({ home, adapters: [selected] }) : null;
270
+ const ide = targets.length ? await installIdeHooks({ home, version: bundle.manifest.version, adapters: targets })
271
+ : { host: selected.host, configured: false, enforcement: 'disabled', mode: 'explicit-sdk',
272
+ ...(disabled ?? {}), message: '技能已安装;钩子关闭,SDK 可直接调用。' };
268
273
  return { version: bundle.manifest.version, updated, removed, preserved, agreementStale: stale, ide };
269
274
  });
270
275
  }
@@ -427,14 +432,16 @@ async function runAccountCommand(command, args) {
427
432
 
428
433
  async function main() {
429
434
  const [command, ...args] = process.argv.slice(2);
430
- if (command === 'unlock' && args.length === 0) {
435
+ if (command === 'exit' && args.length === 0) {
436
+ process.stdout.write(`${JSON.stringify(await exitIdeSession())}\n`);
437
+ } else if (command === 'unlock' && args.length === 0) {
431
438
  process.stdout.write(`${JSON.stringify(await writeBreakGlass(os.homedir(), Date.now(), ideGuardBinding().sessionId))}\n`);
432
439
  } else if (command === 'uninstall' && args.length === 0) {
433
440
  const result = await uninstall();
434
441
  process.stdout.write(`${JSON.stringify(result)}\n`);
435
442
  if (!result.uninstalled) process.exitCode = 2;
436
- } else if (command === 'install' && (args.length === 0 || args.length === 2 && args[0] === '--host')) {
437
- process.stdout.write(JSON.stringify(await install({ explicit: true, hostId: args[1] })) + '\n');
443
+ } else if (command === 'install' && installOptions([command, ...args])) {
444
+ process.stdout.write(JSON.stringify(await install({ explicit: true, ...installOptions([command, ...args]) })) + '\n');
438
445
  } else if (command === 'configure' && args.length === 0) {
439
446
  const data = await stdinJson();
440
447
  if (Object.keys(data).length !== 1 || typeof data.token !== 'string') throw new Error('Expected {"token":"..."} on stdin');
@@ -477,7 +484,7 @@ async function main() {
477
484
  } else if (command === 'local') {
478
485
  await runLocal(args);
479
486
  } else {
480
- throw new Error('Usage: entrorove unlock | uninstall | install [--host codex] | configure | agreement | invoke | queue | native <domain> <operation> | notifications configure|status|test | scope-connect <repositoryRoot> | scope-amend <repositoryRoot> | mutation-connect <repositoryRoot> | mutation-settle <repositoryRoot> | workflow-observe-step <repositoryRoot> | host-message [adapterModulePath] | cloud-task | cloud-watch | cloud-status | feedback <requestId> [generation] | status <requestId> | flows | usage [flowId] | billing | recurring list|claim|finish|runs | review | local (JSON on stdin)');
487
+ throw new Error('Usage: entrorove exit | unlock | uninstall | install [--host codex] [--hooks|--no-hooks] | configure | agreement | invoke | queue | native <domain> <operation> | notifications configure|status|test | scope-connect <repositoryRoot> | scope-amend <repositoryRoot> | mutation-connect <repositoryRoot> | mutation-settle <repositoryRoot> | workflow-observe-step <repositoryRoot> | host-message [adapterModulePath] | cloud-task | cloud-watch | cloud-status | feedback <requestId> [generation] | status <requestId> | flows | usage [flowId] | billing | recurring list|claim|finish|runs | review | local (JSON on stdin)');
481
488
  }
482
489
  }
483
490
  if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) { const { captureIdeResults } = await import('./ide-results.mjs');
package/ide-command.mjs CHANGED
@@ -55,6 +55,8 @@ function sdkWords(parsed) {
55
55
  seen.add(key);
56
56
  if (key === 'ENTROROVE_IDE') {
57
57
  if (!/^[\p{L}\p{N} .+_-]{2,48}$/u.test(value)) return null;
58
+ } else if (key === 'ENTROROVE_HOOK_MODE') {
59
+ if (value !== 'required') return null;
58
60
  } else if (!['ENTROROVE_SESSION', 'CODEX_THREAD_ID', 'CLAUDE_SESSION_ID'].includes(key)
59
61
  || !/^[A-Za-z0-9_-]{1,200}$/u.test(value)) return null;
60
62
  }
@@ -76,13 +78,13 @@ export function sdkCommand(parsed, runtime) {
76
78
  args = words.slice(1);
77
79
  } else return null;
78
80
  const simple = new Set(['invoke', 'cloud-task', 'cloud-status', 'cloud-watch', 'agreement', 'configure', 'queue', 'unlock']);
79
- if (['unlock', 'uninstall'].includes(args[0]) && args.length === 1 && !parsed.input) return { args };
81
+ if (['exit', 'unlock', 'uninstall'].includes(args[0]) && args.length === 1 && !parsed.input) return { args };
80
82
  if (simple.has(args[0]) && args.length === 1 && parsed.input) return { args, input: parsed.input };
81
83
  if (['status', 'feedback'].includes(args[0]) && /^[a-f0-9]{64}$/u.test(args[1] ?? '') && [2,3].includes(args.length)) return { args };
82
84
  if (args[0] === 'flows' && args.length === 1 && (!parsed.input || typeof parsed.input === 'object'
83
85
  && !Array.isArray(parsed.input) && Object.keys(parsed.input).length === 0)) return { args };
84
- if (['install', 'billing'].includes(args[0]) && args.length === 1 && !parsed.input) return { args };
85
- if (args[0] === 'install' && args.length === 3 && args[1] === '--host' && args[2] === 'codex' && !parsed.input) return { args };
86
+ if (args[0] === 'billing' && args.length === 1 && !parsed.input) return { args };
87
+ if (installOptions(args) && !parsed.input) return { args };
86
88
  if (args[0] === 'recurring' && ['list', 'claim'].includes(args[1] ?? '') && args.length === 2 && !parsed.input) return { args };
87
89
  if (args[0] === 'recurring' && ['finish', 'runs'].includes(args[1] ?? '') && args.length === 3 && (!parsed.input || args[1] === 'finish')) return { args, ...(parsed.input ? { input: parsed.input } : {}) };
88
90
  if (args[0] === 'usage' && args.length <= 2 && !parsed.input
@@ -96,9 +98,23 @@ export function installerCommand(parsed) {
96
98
  if (!parsed || parsed.input || !['npx', 'npx.cmd'].includes(parsed.words[0])) return null;
97
99
  const words = [...parsed.words]; words.shift();
98
100
  while (['--yes', '--prefer-online', '--registry=https://registry.npmjs.org'].includes(words[0])) words.shift();
99
- if (![2, 4].includes(words.length) || !/^@entrorove\/skill(?:@(?:latest|\d+\.\d+\.\d+))?$/u.test(words[0]) || words[1] !== 'install'
100
- || words.length === 4 && (words[2] !== '--host' || words[3] !== 'codex')) return null;
101
- return `npx --yes --prefer-online --registry=https://registry.npmjs.org ${words[0]} install${words.length === 4 ? ' --host codex' : ''}`;
101
+ if (!/^@entrorove\/skill(?:@(?:latest|\d+\.\d+\.\d+))?$/u.test(words[0])) return null;
102
+ const options = installOptions(words.slice(1));
103
+ if (!options) return null;
104
+ return `npx --yes --prefer-online --registry=https://registry.npmjs.org ${words[0]} install${options.hostId ? ' --host codex' : ''}${options.hooks ? ' --hooks' : ''}`;
105
+ }
106
+
107
+ export function installOptions(args) {
108
+ if (args[0] !== 'install') return null;
109
+ const options = { hooks: false }, seen = new Set();
110
+ for (let index = 1; index < args.length; index++) {
111
+ const key = args[index] === '--host' ? 'hostId' : ['--hooks', '--no-hooks'].includes(args[index]) ? 'hooks' : null;
112
+ if (!key || seen.has(key)) return null;
113
+ seen.add(key);
114
+ if (key === 'hostId') { if (args[++index] !== 'codex') return null; options.hostId = 'codex'; }
115
+ else options.hooks = args[index] === '--hooks';
116
+ }
117
+ return options;
102
118
  }
103
119
 
104
120
  export function readCommand(parsed) {
@@ -170,6 +186,6 @@ export function runtimeHashRead(parsed, runtime) {
170
186
 
171
187
  export function renderSdkCommand(call, runtime, sessionId, host = process.env.ENTROROVE_IDE ?? 'Codex') {
172
188
  const quote = value => `'${String(value).replace(/'/gu, "'\\''")}'`;
173
- const header = `ENTROROVE_IDE=${quote(host)} ENTROROVE_SESSION=${quote(sessionId)} CODEX_THREAD_ID=${quote(sessionId)} ${[runtime.node, runtime.cli, ...call.args].map(quote).join(' ')}`;
189
+ const header = `ENTROROVE_HOOK_MODE='required' ENTROROVE_IDE=${quote(host)} ENTROROVE_SESSION=${quote(sessionId)} CODEX_THREAD_ID=${quote(sessionId)} ${[runtime.node, runtime.cli, ...call.args].map(quote).join(' ')}`;
174
190
  return call.input ? `${header} <<'ENTROROVE_INPUT'\n${JSON.stringify(call.input)}\nENTROROVE_INPUT` : header;
175
191
  }
package/ide-exit.mjs ADDED
@@ -0,0 +1,26 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { ideDirectory, ideDigest, ideError, ideGuardBinding, ideSession, readIdeJson, verifiedIdeGuards } from './ide-session.mjs';
4
+
5
+ const exactExit = /^(?:(?:请)?(?:停止使用|退出)\s*\$?entrorove|(?:stop using|exit)\s+\$?entrorove)[.!。!]?$/iu;
6
+ const naturalExit = /(?:^|[,,。;;!!\n])\s*(?:从现在开始|接下来|今后)?\s*(?:停止使用|不再使用|不使用)\s*(?:entrorove(?:\s*技能)?|(?:这个|该)?技能)\s*(?:(?:来)?(?:执行|处理|完成|继续)[^,,。;;!!\n]{0,40}(?:任务|工作))?[。!!]?\s*$/iu;
7
+ export function explicitIdeExit(prompt) {
8
+ if (exactExit.test(prompt)) return true;
9
+ return !/[`"'“”‘’「」『』<>]|如果|假如|假设|示例|截图|引用/u.test(prompt) && naturalExit.test(prompt);
10
+ }
11
+ export function markIdeExited(state, source) {
12
+ if (state.phase !== 'user-exited') state.exitPhase = state.phase;
13
+ state.active = false; state.phase = 'user-exited';
14
+ state.exitDecision = { source, observedAt: new Date().toISOString() };
15
+ }
16
+ export async function exitIdeSession({ home = os.homedir(), environment = process.env } = {}) {
17
+ const { host, sessionId } = ideGuardBinding(environment);
18
+ if (!Object.hasOwn(verifiedIdeGuards, host) || !sessionId) throw ideError('ENTROROVE_IDE_IDENTITY', 'Exit requires the current verified IDE session identity');
19
+ const directory = await ideDirectory(home), state = await readIdeJson(path.join(directory, `session-${ideDigest(sessionId)}.json`));
20
+ if (!state || state.sessionId !== sessionId) throw ideError('ENTROROVE_IDE_IDENTITY', 'Exit requires the existing current IDE session');
21
+ return ideSession({ session_id: sessionId, cwd: state.repositoryRoot }, current => {
22
+ markIdeExited(current, 'cli');
23
+ return { schemaVersion: 'entrorove.ide-exit/1.0', status: 'user-exited', sessionId,
24
+ offline: true, network: false, hook: 'not-required', originalFlowPreserved: true, claimsCompletion: false };
25
+ }, home);
26
+ }
package/ide-hook.mjs CHANGED
@@ -13,6 +13,8 @@ import { parseIdeResults, consumeIdeResults } from './ide-results.mjs';
13
13
  import { verifyIdeLocalAcceptance } from './ide-local-acceptance.mjs';
14
14
  import { registerIdeRequirement, ideQueueContext, unclassifiedIdeRequirements,
15
15
  advanceIdeQueue, checkQueuedStart, unfinishedIdeQueue } from './ide-queue.mjs';
16
+ import { explicitIdeExit, markIdeExited } from './ide-exit.mjs';
17
+ import { appliesIdeHook, hasIdeHookSelection, selectsIdeHook, ideUserPrompt as userPrompt } from './ide-selection.mjs';
16
18
 
17
19
  const output = (event, extra = {}) => ({ hookSpecificOutput: { hookEventName: event, ...extra } });
18
20
  const deny = reason => output('PreToolUse', { permissionDecision: 'deny', permissionDecisionReason: reason });
@@ -24,15 +26,6 @@ const inputErrors = {
24
26
  const publicId = value => typeof value === 'string' && /^[A-Za-z0-9_-]{1,200}$/u.test(value);
25
27
  const readTools = new Set(['wait', 'view_image', 'list_mcp_resources', 'list_mcp_resource_templates', 'read_mcp_resource', 'web_search', 'update_plan', 'request_user_input', 'request_user_input_async', 'get_goal']);
26
28
  const unlockCommand = 'entrorove unlock';
27
- const exitPrompt = /^(?:(?:请)?(?:停止使用|退出)\s*\$?entrorove|(?:stop using|exit)\s+\$?entrorove)[.!。!]?$/iu;
28
- const selectPrompt = /^(?:(?:请)?(?:使用|启用)\s+\$?entrorove|\$entrorove)(?:\s|[,。,::!!]|$)/iu;
29
- function userPrompt(event) {
30
- let prompt = (event.prompt ?? '').trim();
31
- for (const wrapper of ['**', '__', '`']) {
32
- if (prompt.startsWith(wrapper) && prompt.endsWith(wrapper)) prompt = prompt.slice(wrapper.length, -wrapper.length).trim();
33
- }
34
- return prompt;
35
- }
36
29
  const managementOperations = new Set(['capabilities', 'help', 'start', 'continue', 'task-describe', 'flow-cancel', 'flow-abandon', 'scope-connect', 'mutation-connect', 'mutation-settle', 'mutation-consumption-attest', 'mutation-delegate-prepare', 'status', 'feedback', 'cloud-status', 'cloud-watch', 'queue', 'unlock', 'usage', 'billing', 'recurring']);
37
30
  function operationOf(call) {
38
31
  if (!call) return null;
@@ -143,32 +136,34 @@ function originalLocalCall(pending, call, input) {
143
136
  ['taskId', 'flowId', 'stepId', 'leaseId', 'generation'].map(field => [field, input?.[field]]));
144
137
  if (operationOf(call) === 'continue') pending.continuationOperation = input?.operation;
145
138
  }
139
+ function offlineExitCall(event, runtime, state, call) {
140
+ return output('PreToolUse', { permissionDecision: 'allow', updatedInput: prepareIdeShellInput(event.tool_name,
141
+ event.tool_input, renderSdkCommand(call, runtime, state.sessionId), true) });
142
+ }
143
+ function allowInstaller(event, state, command) {
144
+ const safeInput = prepareIdeShellInput(event.tool_name, event.tool_input, command, true);
145
+ if (!safeInput) return denyUnsafeShellInput(state, event);
146
+ if (state.active) { state.readCalls ??= {};
147
+ state.readCalls[event.tool_use_id] = { toolUseId: event.tool_use_id, command: 'install' }; }
148
+ return output('PreToolUse', { permissionDecision: 'allow', updatedInput: safeInput });
149
+ }
146
150
  async function beforeTool(event, state, home) {
147
- if (state.phase === 'user-exited') return {};
151
+ if (!hasIdeHookSelection(state) || state.phase === 'user-exited') return {};
148
152
  const unlocked = await readBreakGlass(home, state.sessionId);
149
153
  const command = event.tool_input?.command ?? event.tool_input?.cmd, parsed = literalCommand(command);
150
154
  const isShell = ['Bash', 'exec_command', 'shell_command'].includes(event.tool_name);
151
155
  const installer = isShell && installerCommand(parsed);
152
- if (installer) {
153
- const safeInput = prepareIdeShellInput(event.tool_name, event.tool_input, installer, true);
154
- if (!safeInput) return denyUnsafeShellInput(state, event);
155
- if (state.active) { state.readCalls ??= {};
156
- state.readCalls[event.tool_use_id] = { toolUseId: event.tool_use_id, command: 'install' }; }
157
- return output('PreToolUse', { permissionDecision: 'allow', updatedInput: safeInput });
158
- }
159
- const namedSdk = parsed?.words.some(word => word === 'entrorove' || /^@entrorove\/skill(?:@[^ ]+)?$/u.test(word)
160
- || /(?:@entrorove\/skill|entrorove-skill)\/cli\.mjs$/u.test(word));
161
- if (!state.active && !namedSdk) return {};
156
+ if (installer) return allowInstaller(event, state, installer);
162
157
  const runtime = isShell && parsed ? await ideRuntime(home) : null;
163
158
  const call = runtime && sdkCommand(parsed, runtime);
164
159
  if (call) {
165
160
  if (!prepareIdeShellInput(event.tool_name, event.tool_input, command, true)) return denyUnsafeShellInput(state, event);
161
+ if (call.args[0] === 'exit' && call.args.length === 1) return offlineExitCall(event, runtime, state, call);
166
162
  if (call.args[0] === 'uninstall') {
167
163
  state.active = false; state.phase = 'user-exited';
168
164
  return output('PreToolUse', { permissionDecision: 'allow', updatedInput: prepareIdeShellInput(event.tool_name,
169
165
  event.tool_input, renderSdkCommand(call, runtime, state.sessionId), true) });
170
166
  }
171
- state.active = true;
172
167
  if (call.args[0] !== 'configure' && call.args[0] !== 'unlock') await bindIdeAccount(state, home);
173
168
  if (state.phase === 'setup' && !unlocked && !isManagementCall(call)) return denyBlocked(state, event, 'Entrorove:自举阶段只放行 Entrorove 管理调用及主机明确授权的任务间报告;业务与写操作需先建立原 flow。');
174
169
  if (call.args[0] === 'unlock') {
@@ -244,22 +239,23 @@ async function beforeTool(event, state, home) {
244
239
  return denyBlocked(state, event, `Entrorove:已阻止原生 ${event.tool_name ?? 'unknown'} 调用。当前阶段 ${state.phase}${state.taskNumber ? `,云端任务 ${state.taskNumber}` : ''}。写入、删除、移动及不确定的终端操作必须使用原任务的 guarded-write/guarded-delete/guarded-move 或已批准的执行计划。`);
245
240
  }
246
241
  export async function runIdeHook(event, { home } = {}) {
247
- return ideSession(event, async state => {
242
+ if (!await appliesIdeHook(event, home)) return {};
243
+ try { return await ideSession(event, async state => {
248
244
  const name = event.hook_event_name;
249
- const selected = name === 'UserPromptSubmit' && selectPrompt.test(userPrompt(event));
250
- const newSelection = selected && !state.active;
245
+ const selected = selectsIdeHook(event);
246
+ const newSelection = selected && !hasIdeHookSelection(state);
251
247
  if (name === 'UserPromptSubmit') {
252
248
  state.turnSequence = (Number.isSafeInteger(state.turnSequence) ? state.turnSequence : 0) + 1;
253
249
  delete state.stopNoticeTurn;
254
250
  }
255
251
  if (name === 'PreToolUse') state.enforcement = 'enforced';
256
- if (name === 'UserPromptSubmit' && exitPrompt.test(userPrompt(event))) {
257
- if (state.phase !== 'user-exited') state.exitPhase = state.phase;
258
- state.active = false; state.phase = 'user-exited';
252
+ if (name === 'UserPromptSubmit' && explicitIdeExit(userPrompt(event))) {
253
+ markIdeExited(state, 'user-prompt');
259
254
  return output(name, { additionalContext: '用户明确退出 Entrorove 管理;原任务仍保留,未验收、未评分为成功。' });
260
255
  }
261
256
  if (selected) {
262
257
  state.active = true;
258
+ state.hookSelection = { source: 'user-prompt', sessionId: state.sessionId, repositoryRoot: state.repositoryRoot };
263
259
  if (['ended', 'user-exited'].includes(state.phase)) {
264
260
  if (state.phase === 'ended') { delete state.flowId; delete state.taskId; delete state.taskNumber; delete state.requestId; }
265
261
  state.phase = state.exitPhase ?? 'entry'; delete state.exitPhase;
@@ -322,7 +318,7 @@ export async function runIdeHook(event, { home } = {}) {
322
318
  }
323
319
  }
324
320
  return state.active && ['SessionStart','UserPromptSubmit','PostCompact','PreCompact'].includes(name) ? output(name, { additionalContext: `${ideContext(state)}${ideQueueContext(state)}` }) : {};
325
- }, home);
321
+ }, home); } catch (cause) { cause.entroroveHookSelected = true; throw cause; }
326
322
  }
327
323
  let incomingEvent;
328
324
  async function readHookEvent() {
@@ -344,11 +340,13 @@ async function main() {
344
340
  process.stdout.write(`${JSON.stringify(result)}\n`);
345
341
  }
346
342
  if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) main().catch(cause => {
347
- // A non-zero generic exit may fail open in the host. Always return a supported
348
- // explicit denial, without including arguments, prompts, credentials or paths.
343
+ // Deny only a positively selected session. Unidentified transport failures
344
+ // must not stop unrelated host work or expose input, credentials or paths.
349
345
  const reason = Object.hasOwn(inputErrors, cause?.code) ? inputErrors[cause.code]
350
346
  : `Entrorove 安全检查失败;本次工具调用已阻止。可复制解锁命令:${unlockCommand}`;
351
- const result = incomingEvent === 'Stop' ? { decision: 'block', reason } : incomingEvent === 'PreToolUse' ? deny(reason)
352
- : { continue: false, stopReason: reason, systemMessage: reason };
347
+ const result = !cause.entroroveHookSelected ? { systemMessage: Object.hasOwn(inputErrors, cause?.code)
348
+ ? inputErrors[cause.code] : 'Entrorove hook 无法确认选用会话;本次未接管,原状态已保留。' }
349
+ : incomingEvent === 'Stop' ? { decision: 'block', reason } : incomingEvent === 'PreToolUse' ? deny(reason)
350
+ : { continue: false, stopReason: reason, systemMessage: reason };
353
351
  process.stdout.write(`${JSON.stringify(result)}\n`);
354
352
  });
package/ide-install.mjs CHANGED
@@ -5,7 +5,7 @@ import { ideDirectory, ideDigest, readIdeJson, writeIdeJson, ideError } from './
5
5
  import { withStateLock } from './local-state-lock.mjs';
6
6
 
7
7
  const source = path.dirname(fileURLToPath(import.meta.url));
8
- const files = ['ide-hook.mjs', 'ide-session.mjs', 'break-glass.mjs', 'ide-results.mjs', 'ide-local-acceptance.mjs', 'ide-queue.mjs', 'ide-command.mjs', 'ide-code-mode.mjs', 'remote-transport.mjs', 'local-credential.mjs', 'local-state-lock.mjs', 'local-stage-boundary.mjs'];
8
+ const files = ['ide-hook.mjs', 'ide-session.mjs', 'ide-selection.mjs', 'ide-exit.mjs', 'break-glass.mjs', 'ide-results.mjs', 'ide-local-acceptance.mjs', 'ide-queue.mjs', 'ide-command.mjs', 'ide-code-mode.mjs', 'remote-transport.mjs', 'local-credential.mjs', 'local-state-lock.mjs', 'local-stage-boundary.mjs'];
9
9
  const quote = text => `'${text.replace(/'/gu, "'\\''")}'`;
10
10
 
11
11
  // One shared host-agnostic hook binary; every host is described by its own
@@ -96,7 +96,7 @@ async function directoryExists(directory) {
96
96
  }
97
97
  export async function activeIdeAdapters({ home, environment = process.env } = {}) {
98
98
  const directory = await ideDirectory(home).catch(cause => {
99
- if (cause.code === 'ENOENT') return null;
99
+ if (['ENOENT', 'ENTROROVE_CREDENTIAL_MISSING'].includes(cause.code)) return null;
100
100
  throw cause;
101
101
  });
102
102
  if (!directory) return [];
@@ -166,12 +166,12 @@ export async function installIdeHooks({ home, version, packageDirectory = source
166
166
  message: `已写入 Entrorove Hook 配置(${installed.map(item => item.host).join('、')});需由 IDE 信任并收到真实回调后才能确认生效。` };
167
167
  }
168
168
 
169
- export async function uninstallIdeHooks({ home }) {
170
- const directory = await ideDirectory(home).catch(error => { if (error.code === 'ENOENT') return null; throw error; });
169
+ export async function uninstallIdeHooks({ home, adapters = ideAdapters }) {
170
+ const directory = await ideDirectory(home).catch(error => { if (['ENOENT', 'ENTROROVE_CREDENTIAL_MISSING'].includes(error.code)) return null; throw error; });
171
171
  if (!directory) return { removed: [], preserved: [] };
172
172
  const removed = [], preserved = [], records = [];
173
173
  const hookPath = path.join(directory, 'ide-hook.mjs');
174
- for (const adapter of ideAdapters) {
174
+ for (const adapter of adapters) {
175
175
  const recordPath = path.join(directory, adapter.record), record = await readIdeJson(recordPath);
176
176
  if (record && (record.schemaVersion !== 'entrorove.ide-installation/1.0' || record.host !== adapter.host)) {
177
177
  throw ideError('ENTROROVE_IDE_STORE', `${adapter.host} installation record is invalid; it was preserved`);
@@ -206,6 +206,8 @@ export async function uninstallIdeHooks({ home }) {
206
206
  if (record) await fs.rm(recordPath);
207
207
  });
208
208
  }
209
+ // Disabling Codex must not remove the shared runtime used by another host.
210
+ if ((await fs.readdir(directory)).some(name => /^installation(?:-[a-z0-9-]+)?\.json$/u.test(name))) return { removed, preserved };
209
211
  const digests = new Map();
210
212
  for (const record of records) for (const [name, digest] of Object.entries(record.files ?? {})) {
211
213
  if (!digests.has(name)) digests.set(name, new Set());
@@ -0,0 +1,31 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { ideDirectory, ideDigest, readIdeJson } from './ide-session.mjs';
4
+ import { explicitIdeExit } from './ide-exit.mjs';
5
+
6
+ const selection = /^(?:(?:请)?(?:使用|启用)\s+\$?entrorove|\$entrorove)(?:\s|[,。,::!!]|$)/iu;
7
+ export function ideUserPrompt(event) {
8
+ let prompt = typeof event.prompt === 'string' ? event.prompt.trim() : '';
9
+ for (const wrapper of ['**', '__', '`']) {
10
+ if (prompt.startsWith(wrapper) && prompt.endsWith(wrapper)) prompt = prompt.slice(wrapper.length, -wrapper.length).trim();
11
+ }
12
+ return prompt;
13
+ }
14
+ export const selectsIdeHook = event => event.hook_event_name === 'UserPromptSubmit' && selection.test(ideUserPrompt(event));
15
+ export const hasIdeHookSelection = state => state?.active === true && state.hookSelection?.source === 'user-prompt'
16
+ && state.hookSelection.sessionId === state.sessionId && state.hookSelection.repositoryRoot === state.repositoryRoot;
17
+
18
+ // Global callbacks must be inert until this exact session and repository opt in.
19
+ // In particular, discovery, an old active bit, and a broken store cannot select it.
20
+ export async function appliesIdeHook(event, home) {
21
+ if (typeof event?.session_id !== 'string' || !/^[A-Za-z0-9_-]{1,200}$/u.test(event.session_id)
22
+ || typeof event.cwd !== 'string' || !path.isAbsolute(event.cwd)) return false;
23
+ if (selectsIdeHook(event)) return true;
24
+ try {
25
+ const directory = await ideDirectory(home);
26
+ const state = await readIdeJson(path.join(directory, `session-${ideDigest(event.session_id)}.json`));
27
+ const exiting = event.hook_event_name === 'UserPromptSubmit' && explicitIdeExit(ideUserPrompt(event));
28
+ return state?.sessionId === event.session_id && (hasIdeHookSelection(state) || exiting)
29
+ && await fs.realpath(event.cwd) === state.repositoryRoot;
30
+ } catch { return false; }
31
+ }
package/ide-session.mjs CHANGED
@@ -152,8 +152,11 @@ async function declaredHost(environment) {
152
152
  return null;
153
153
  }
154
154
  export async function assertIdeExecution(args, input, { home = os.homedir(), environment = process.env } = {}) {
155
+ approvedResultContext = undefined;
156
+ if (environment.ENTROROVE_HOOK_MODE === undefined) return { mode: 'explicit-sdk', hookEnforced: false };
157
+ if (environment.ENTROROVE_HOOK_MODE !== 'required') throw ideError('ENTROROVE_IDE_MODE', 'Unknown Entrorove hook execution mode');
155
158
  const { host, sessionId } = ideGuardBinding(environment);
156
- if (!host || host === 'Entrorove CLI') return;
159
+ if (!host || host === 'Entrorove CLI') throw ideError('ENTROROVE_IDE_GUARD_UNSUPPORTED', 'Hook execution requires its actual IDE host and session');
157
160
  const lifecycle = args[0] === 'local' ? args[1] : args[0] === 'invoke' ? input?.operation : null;
158
161
  if (['flow-cancel', 'flow-abandon'].includes(lifecycle) && await readBreakGlass(home, sessionId)) {
159
162
  const flowId = args[0] === 'invoke' ? input?.input?.flowId : input?.flowId;
@@ -177,13 +180,15 @@ export async function assertIdeExecution(args, input, { home = os.homedir(), env
177
180
  const directory = await ideDirectory(home), state = await readIdeJson(path.join(directory, `session-${ideDigest(sessionId)}.json`));
178
181
  const expected = ideDigest({ args, input });
179
182
  const approved = [state?.pendingCall, ...Object.values(state?.readCalls ?? {})].find(call => call?.inputDigest === expected);
180
- if (!state?.active || !approved || state.sessionId !== sessionId
183
+ if (!state?.active || state.hookSelection?.source !== 'user-prompt' || state.hookSelection.sessionId !== sessionId
184
+ || state.hookSelection.repositoryRoot !== state.repositoryRoot || !approved || state.sessionId !== sessionId
181
185
  || Date.now() - Date.parse(state.observedAt) > 120_000) {
182
186
  throw ideError('ENTROROVE_IDE_GUARD_REQUIRED', `${host} tool guard has not approved this exact call. Run install, accept the host hook, and resume the original task.`);
183
187
  }
184
188
  await bindIdeAccount(state, home);
185
189
  approvedResultContext = { home, sessionId: state.sessionId, repositoryRoot: state.repositoryRoot,
186
190
  accountDigest: state.accountDigest, toolUseId: approved.toolUseId, inputDigest: expected };
191
+ return { mode: 'hook-enforced', hookEnforced: true };
187
192
  }
188
193
 
189
194
  export async function settleBreakGlassSession(args, input, result, { home = os.homedir(), environment = process.env } = {}) {
@@ -204,7 +209,7 @@ export async function settleBreakGlassSession(args, input, result, { home = os.h
204
209
  for (const key of ['flowId', 'taskId', 'taskNumber', 'requestId', 'remoteRequestId', 'nextStep', 'pendingCall', 'readCalls']) delete state[key];
205
210
  state.feedbackAcknowledged = false;
206
211
  if (state.phase !== 'user-exited') state.phase = 'entry';
207
- } else state.phase = 'ended';
212
+ } else if (state.phase !== 'user-exited') state.phase = 'ended';
208
213
  return true;
209
214
  }, home);
210
215
  }
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.59",
4
+ "version": "0.1.61",
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.59",
3
+ "version": "0.1.61",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "entrorove": "cli.mjs"
@@ -222,9 +222,11 @@
222
222
  "ide-code-mode.mjs",
223
223
  "ide-command.mjs",
224
224
  "ide-hook.mjs",
225
+ "ide-selection.mjs",
225
226
  "ide-install.mjs",
226
227
  "install-cleanup.mjs",
227
228
  "ide-session.mjs",
229
+ "ide-exit.mjs",
228
230
  "break-glass.mjs",
229
231
  "ide-results.mjs",
230
232
  "ide-local-acceptance.mjs",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "entrorove.release-state/1.0",
3
- "version": "0.1.59",
4
- "digest": "f5f213108d54b2dae1b49d7a399e4476d2edca251ab76f013b40cf38344617f7",
3
+ "version": "0.1.61",
4
+ "digest": "8211a23e233808dfc8f43dd0b28af5eae9b8bea5127b9550da8e8c50d9447052",
5
5
  "files": {
6
6
  "connected-cli.mjs": "79c3359303957a9ebb8987e3dfb181f189d380fa02ef4462683b786aedfc7802",
7
7
  "local-approved-process.mjs": "442856df788e63f38757439ba0657d1c689aaf797631a955fae3dd639e844e6d",
@@ -22,10 +22,10 @@
22
22
  "mutation-client.mjs": "6455c35b483d6d22e42e7be708630db07d9fd1d3cdf639294b126b69db2eae2f",
23
23
  "cloud-mutation-authority.mjs": "c7226e882a4f3be4d2f55728f515d5a815c0aa1f21aa5e4689314e6e6a8c66a2",
24
24
  "ide-code-mode.mjs": "f8ba4de33632ab4c6df52dab52f392414b4632c7e94b218b8065677970c075c7",
25
- "ide-command.mjs": "2cde4856647b755cf491de0958855abc109bf74009f39aea7ce90e7b94414ff1",
26
- "ide-hook.mjs": "24c63f14744886016d50686c9460e4e153a5ba9f7807183fdf04f22db5b8d0af",
27
- "ide-install.mjs": "6322f76d8c0f5c864d8c61f4ee90ae029ccf4bbf1e31fbca7590b78b6c7eaa78",
28
- "ide-session.mjs": "6df04bca3231954e8b83f6b1b535befff16b6c62c51ca65ae3aa2559180ad7c2",
25
+ "ide-command.mjs": "dbdb4cbf8e870c93a746ca176cbbdeffc0458de1293583d3eb9af4c256541370",
26
+ "ide-hook.mjs": "c44d37997fae55bd113ff9044947c54152aaccdf081d0f39d437179d33e9a4e2",
27
+ "ide-install.mjs": "1c80e191ddc2ac8789f0c71edc7bc8c3c8cea6bfab537cdf6baffa4a6ce3419c",
28
+ "ide-session.mjs": "5277a6ea3445bfa9b2e28847372d963da77787d472d0414c0e253d4fe29f74ec",
29
29
  "break-glass.mjs": "6ec9f97c5d68bb33121b2374c06b9d4c8eb3419b52d00849fec7124bc63e4582",
30
30
  "ide-results.mjs": "baa0618845ced901334644479bf4e0d298bad3f955bc18846752de6c283a5573",
31
31
  "ide-local-acceptance.mjs": "238eec6d7b6093fb8ad022da0ed3383a48ba2ffcf693e3ce8a241cf9624088d8",
@@ -50,7 +50,7 @@
50
50
  "runtime/validator-runtime-v7.0.44.d.mts": "7797edbfe2f7de24da370204dce4dfd22822a1bd80df93514fff900b98afe36f",
51
51
  "snapshot-retention.mjs": "ca46aeb36b1a5a24fe7e967031a71b9a3d33f40cb715d344402cbe0dc9c425cb",
52
52
  "snapshot-cleanup-journal.mjs": "76a9684cc95b4eb076e1298d78a9dff37948b00d9607b8f546c599072d3ed331",
53
- "IDE-EXECUTION.md": "4d91d679019165ec08ece4f647f0f0c8a5f665654ce6e9c1da8c90badaef3836",
53
+ "IDE-EXECUTION.md": "203aa4ae47cc45915e1a89bd7fbb56bf57aebccef444599b3a2d1d2816f01907",
54
54
  "REUSE-DISCIPLINE.md": "5b214cf313e7f715f52d54e28093a2d038f0f196ca64b15d1c49587b9d8eecf8",
55
55
  "reuse-discipline-release.mjs": "7f55ae10ec3396ed3bc5430a0c64f94c33c2dfbc57b32084caab92fd5fd1586f",
56
56
  "local-final-gate16.mjs": "083aec470670e6cdb7ad2ab27b7a51b53a9ec3a8b4121cb9cbfa17d9940892c2",
@@ -94,17 +94,19 @@
94
94
  "local-autocoord17-waits.mjs": "2b0a713987822548a94e1ed3a9a891532db45383797dfc5d5bc8d1e35593cf25",
95
95
  "local-validator-isolation17.mjs": "48e94604e28b8fddd567dfb35ff60b73fd094a5803bd49f076388b7f310f92aa",
96
96
  "local-calctool-composition17.mjs": "ac89fcb41ab0d0d9b47c1b5696945281b0894eb064820775e36a011c76314e6a",
97
- "cli.mjs": "f5f8aae8cc741c13482226659bddf9f530b1b7d06f6921a7cb99a201172d38c9",
97
+ "cli.mjs": "b28bfda3d9ec610c7865725eff4e5d551bcbdfc8847f6fd43eca845ce3881ea8",
98
+ "ide-selection.mjs": "e11e2b363c06444f1dcb4cdab1eaa0eb748a379b84354e0e71fd9c2dc79a4815",
99
+ "ide-exit.mjs": "c890c7b34fa6f12e0ccff7a7b85738d515fb13ad93f671a8547ae6570b66488d",
98
100
  "install-cleanup.mjs": "8a8e1a7dfe37427f86cd4d6e317890d0829894a9949d4216674a716b36edbc54",
99
101
  "review-client.mjs": "3491eef68ed533174b31db3c53febb6519b8f088f71a9964644714a9a4c03c77",
100
102
  "version.mjs": "1f392147d72c40af4b03b307ea607075fb815af5174fa07e254b95fe12dbf3e7",
101
- "SKILL.md": "a521b28c856304811eb6fd017561c5af578aa926a03e534fb5463a9696ed1741",
103
+ "SKILL.md": "f04566431da716ff61812118004ca69f2a8f9f24fe4e032f2a1fff5073b8071b",
102
104
  "CAPABILITY-MAP.md": "e420bbc202c27d983635ae33b823b39dd445cf5cdf40ffe88e210a755cb7424d",
103
105
  "AGREEMENT.md": "f93a68e92c457200dd024610e65f74e478b8f60860f951da818b70561f1b9bf8",
104
106
  "DELIVERY-BOUNDARIES.md": "9b9db50ed1bcce9e77ff6527b6bb7eaf12f3b913162ea9cb7fe90db06b393a4f",
105
- "package.json": "7dcd7e205c8890fc5f711dd77c59ae549150a548c8e26a6c6285e912407ea6ea",
107
+ "package.json": "ce0f78910ec9aeb36a7d61a30a68c6b117c9b29ca34cc8446a46604b251c131e",
106
108
  "manifest.json": "ab06e307795bc9da5add926752b6c4dbdaf18625090261a035aa356a679c504f",
107
- "release.mjs": "ab0f359f54e005b3ce5f76d6c4bc1d63dd983ba7467344e71ea2a084255f3f4b",
109
+ "release.mjs": "90e4c5beb05e263c2cacf09c476480ac3599a965d62e0adda68226a8baaa968c",
108
110
  "local-cli.mjs": "41e2a737c8587cb5ce469a7661f6d14b5ea4cd073f8286822fc4dc96b67f56a1",
109
111
  "local-state-lock.mjs": "1dbf64264642a0851faf043e07ee9f66d2ad0508e9f799b0626246e42fae893e",
110
112
  "local-execution.mjs": "185f34e917440a360b3c4bafd3af245243807d964ae3820b3cffaaa2e6348cc6",
package/release.mjs CHANGED
@@ -68,7 +68,7 @@ const sourceFiles = [
68
68
  'local-merge-transaction.mjs',
69
69
  ...swarmSourceFiles,
70
70
  ...coordination17SourceFiles,
71
- 'cli.mjs', 'install-cleanup.mjs', 'review-client.mjs', 'version.mjs', 'SKILL.md', 'CAPABILITY-MAP.md', 'AGREEMENT.md', 'DELIVERY-BOUNDARIES.md',
71
+ 'cli.mjs', 'ide-selection.mjs', 'ide-exit.mjs', 'install-cleanup.mjs', 'review-client.mjs', 'version.mjs', 'SKILL.md', 'CAPABILITY-MAP.md', 'AGREEMENT.md', 'DELIVERY-BOUNDARIES.md',
72
72
  'package.json', 'manifest.json', 'release.mjs',
73
73
  'local-cli.mjs', 'local-state-lock.mjs', 'local-execution.mjs', 'local-integrity.mjs', 'local-evidence-verification.mjs', 'local-formula.mjs', 'local-ledger.mjs',
74
74
  'local-error.mjs',
@@ -337,6 +337,7 @@ function releaseLabels() {
337
337
  'blueprint-criterion-wire.mjs': { zh: '蓝图验收标准执行授权', en: 'Blueprint criterion execution authority' },
338
338
  'blueprint-criterion-runner.mjs': { zh: '蓝图验收标准实际执行器', en: 'Blueprint criterion process runner' },
339
339
  'cli.mjs': { zh: '技能运行和受管安装', en: 'skill runtime and managed installation' },
340
+ 'ide-exit.mjs': { zh: '当前 IDE 会话明确退出', en: 'explicit exit for the current IDE session' },
340
341
  'install-cleanup.mjs': { zh: '旧版受管安装清理', en: 'managed installation cleanup' },
341
342
  'version.mjs': { zh: '版本安全检查', en: 'version safety checks' },
342
343
  'SKILL.md': { zh: 'IDE 工作流程说明', en: 'IDE workflow instructions' },
package/releases.json CHANGED
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.1.61",
4
+ "date": "2026-10-18",
5
+ "zh": "钩子改为可选:默认安装关闭钩子,IDE 可直接调用 SDK;只有明确选用技能的原会话与仓库启用拦截,普通查询、旧激活状态及无法归属的事件不再接管其他任务。保留账号、写入范围、签名和真实本地验收校验。",
6
+ "en": "Make hooks optional: default installs disable hooks and IDEs can call the SDK directly. Interception requires explicit selection of the original session and repository; discovery, legacy activation and unidentified events cannot govern unrelated work. Preserve account, mutation scope, signatures and actual local acceptance checks."
7
+ },
8
+ {
9
+ "version": "0.1.60",
10
+ "date": "2026-10-17",
11
+ "zh": "修复范围补正误用独立交接的完整依赖校验,接收原账号签名器的真实受限扫描并保留未解析依赖、原审批与预算;新增当前 IDE 会话离线 exit,识别明确停用决定并保持退出状态,不再要求重建流程。",
12
+ "en": "Fix scope amendments to verify the original member signer wire receipt while preserving unresolved imports, owner approval and native budgets. Add offline exit for the current IDE session, recognize explicit stop decisions and preserve exited governance without requiring a replacement flow."
13
+ },
2
14
  {
3
15
  "version": "0.1.59",
4
16
  "date": "2026-10-17",