@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 +10 -0
- package/IDE-EXECUTION.md +9 -2
- package/SKILL.md +6 -5
- package/cli.mjs +17 -10
- package/ide-command.mjs +23 -7
- package/ide-exit.mjs +26 -0
- package/ide-hook.mjs +30 -32
- package/ide-install.mjs +7 -5
- package/ide-selection.mjs +31 -0
- package/ide-session.mjs +8 -3
- package/manifest.json +1 -1
- package/package.json +3 -1
- package/release-state.json +13 -11
- package/release.mjs +2 -1
- package/releases.json +12 -0
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`.
|
|
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
|
-
|
|
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
|
|
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.
|
|
8
|
+
Version: 0.1.61.
|
|
9
9
|
|
|
10
|
-
Entrorove applies to the
|
|
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`
|
|
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
|
-
|
|
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 =
|
|
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 &&
|
|
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:
|
|
266
|
+
hostIds: explicit ? [selected.id] : active.map(adapter => adapter.id),
|
|
265
267
|
});
|
|
266
|
-
const
|
|
267
|
-
|
|
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 === '
|
|
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' && (
|
|
437
|
-
process.stdout.write(JSON.stringify(await install({ explicit: true,
|
|
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 ([
|
|
85
|
-
if (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 (
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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 =
|
|
250
|
-
const newSelection = selected && !state
|
|
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' &&
|
|
257
|
-
|
|
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
|
-
//
|
|
348
|
-
//
|
|
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 =
|
|
352
|
-
|
|
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
|
|
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
|
|
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
|
|
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')
|
|
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 ||
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entrorove/skill",
|
|
3
|
-
"version": "0.1.
|
|
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",
|
package/release-state.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "entrorove.release-state/1.0",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"digest": "
|
|
3
|
+
"version": "0.1.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": "
|
|
26
|
-
"ide-hook.mjs": "
|
|
27
|
-
"ide-install.mjs": "
|
|
28
|
-
"ide-session.mjs": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
103
|
+
"SKILL.md": "f04566431da716ff61812118004ca69f2a8f9f24fe4e032f2a1fff5073b8071b",
|
|
102
104
|
"CAPABILITY-MAP.md": "e420bbc202c27d983635ae33b823b39dd445cf5cdf40ffe88e210a755cb7424d",
|
|
103
105
|
"AGREEMENT.md": "f93a68e92c457200dd024610e65f74e478b8f60860f951da818b70561f1b9bf8",
|
|
104
106
|
"DELIVERY-BOUNDARIES.md": "9b9db50ed1bcce9e77ff6527b6bb7eaf12f3b913162ea9cb7fe90db06b393a4f",
|
|
105
|
-
"package.json": "
|
|
107
|
+
"package.json": "ce0f78910ec9aeb36a7d61a30a68c6b117c9b29ca34cc8446a46604b251c131e",
|
|
106
108
|
"manifest.json": "ab06e307795bc9da5add926752b6c4dbdaf18625090261a035aa356a679c504f",
|
|
107
|
-
"release.mjs": "
|
|
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",
|