@entrorove/skill 0.1.38 → 0.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CAPABILITY-MAP.md CHANGED
@@ -8,8 +8,9 @@
8
8
  | 变更纪律 | `local discipline-review/discipline-check/guarded-delete/guarded-move`、`workflow-discipline-review` | 单一事实源、用途三态、全仓差集、已签名变更分类及原账号确认;删除数据需要绑定实际快照证据。未知用途保持未知,阈值仍待实测 |
9
9
  | 并行路径等价 | `local run-approved-plan`、Validator 7.0.44 | 固定基线来自本项目既有路径的实际输出;逐字段比对,缺证据不得 pass,差异列出字段。不是其它项目的历史输出 |
10
10
  | 结束清理与回传 | 原请求 `continue`、`workflow-verdict`、反馈确认 | 仅验证完成且回传已存后清理同流程冗余快照,保留原始及最新恢复点;公开技术评分按真实已确认回执聚合 |
11
+ | 失败 flow 终止与解绑 | 服务端 `flow-cancel` / `flow-abandon`;本地 `local flow-cancel` / `local flow-abandon` | `flow-cancel` 终止并保留任务绑定供只读恢复;`flow-abandon` 清理接受与 checkpoint、解绑任务并返回新的 `start`;旧 flow 永不重开 |
11
12
 
12
- 本次仍不提供任务优先级队列、插入任务结束后自动恢复父任务或无人值守唤醒。这些需要后续单独实现和验收。源代码执行仍需原合同配置的可信运行器,范围签名器本身不授予执行权限。
13
+ 任务优先级队列已接入 IDE 私有状态:新用户原文先进入 `queue status`,再按已有四级优先级通过 `queue route` 排队或在安全检查点插入;插入任务完成并取得真实回执后恢复原 flow、taskId、taskNumber 与 requestId。队列不承诺主机断电或 IDE 进程退出时的无人值守执行。源代码执行仍需原合同配置的可信运行器,范围签名器本身不授予执行权限。
13
14
 
14
15
  ## 2026-10-13 接入:协作观测、原生规划与任务通知
15
16
 
@@ -22,7 +23,7 @@
22
23
  | 网页与手机通知 | 登录后的任务通知按钮 | 同一加密 Web Push 协议,账号与设备隔离;真实任务事务队列、持久重试、失效注销与到期清理;手机可见投递仍需设备本人授权与验收 |
23
24
  | 失败后接续 | `scope-connect` 返回 `nextInput`;`feedback <requestId> [generation]` | 范围门禁使用同一排序路径集;新执行保存新一代回执,原失败回执不可改,默认只读最新一代 |
24
25
 
25
- 本地公开 schema 已增加到 **101 项**(原 100 项加 `peer-status`)。Aimlock 运行时由 7.0.44 更新为 **7.0.45**,公开操作由 20 项增加到 24 项;其余七域的运行时版本保持原值。下方历史表保留原对账日期与当时验收边界,不表示当前仍缺上述入口。发布件与生产部署需独立核验,不能从仓库测试推断。
26
+ 本地公开 schema 已增加到 **107 项**(原 100 项加 `peer-status`,以及当前增量的 `discipline-check`、`discipline-review`、`flow-cancel`、`flow-abandon`、`guarded-delete`、`guarded-move`)。Aimlock 运行时由 7.0.44 更新为 **7.0.45**,公开操作由 20 项增加到 24 项;其余七域的运行时版本保持原值。下方历史表保留原对账日期与当时验收边界,不表示当前仍缺上述入口。发布件与生产部署需独立核验,不能从仓库测试推断。
26
27
 
27
28
  ## 2026-10-12 基线:八域 IDE 调用链
28
29
 
@@ -80,6 +81,8 @@ Entrorove 只治理通过技能入口执行的动作:路径、通行证、快
80
81
 
81
82
  `scope-connect <absolute repositoryRoot>` 是 CLI 编排入口,不改变这 100 项本地操作。它串联远端 `scope-runner-prepare/register/challenge/attest` 与已有真实快照、签名检查,为原账号与任务提交范围证据;不授予执行证据签名或写入权限。全部远端操作以实时 discovery 为准。
82
83
 
84
+ 当前增量操作(不改写上述历史分母)为:`peer-status`、`discipline-check`、`discipline-review`、`flow-cancel`、`flow-abandon`、`guarded-delete`、`guarded-move`。它们计入当前 107 项公开 schema。
85
+
83
86
  | 分区 | 精确操作名 | 证据位置 |
84
87
  | --- | --- | --- |
85
88
  | 核心 discovery、快照、门控、预算、协调、证据、链、任务路由(54) | `capabilities`, `help`, `snapshot-plan`, `snapshot-verify`, `mutate-gate`, `gate-issue`, `mutation-snapshot-attest`, `gate-verify`, `guarded-write`, `budget-init`, `budget-handoff`, `budget-context`, `budget-read`, `budget-status`, `budget-extend`, `budget-transition`, `peer-coordinate`, `coordinate`, `spawn-bind`, `peer-complete`, `complete`, `lease-verify`, `evidence-sign`, `evidence-verify`, `rollback`, `final-gate`, `formula-evaluate`, `formula-graph`, `formula-final-gate`, `calculation-final-gate`, `run-approved-plan`, `branch-create`, `branch-create-virtual`, `branch-list`, `branch-switch`, `diff-report`, `preflight`, `ledger-query`, `chain-ledger-query`, `chain-init`, `chain-ledger-init`, `chain-resume`, `chain-status`, `chain-ledger-status`, `chain-answer`, `budget-auto-renew-request`, `budget-auto-renew`, `budget-auto-renew-stop`, `probe`, `reassess`, `task-describe`, `message-route`, `message-accept`, `task-resume` | `packages/entrorove-skill/local-contracts.mjs:14-114` |
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.40 — 2026-10-15
4
+
5
+ - 修复 flow 终止与会话解绑、tsconfig 授权路径解析,并加入离线 unlock 自救入口。
6
+ - Fixed flow termination and session unbinding, tsconfig authorized-path resolution, and added an offline unlock recovery command.
7
+
8
+ ## 0.1.39 — 2026-10-14
9
+
10
+ - 增加受控需求队列,支持按优先级排队或在安全检查点插入;插入任务完成并回传真实确认后自动恢复原任务与原编号。
11
+ - Added a controlled requirement queue with priority ordering and checkpointed interruption; after verified acknowledged completion, the original task and identifiers resume automatically.
12
+
3
13
  ## 0.1.38 — 2026-10-14
4
14
 
5
15
  - 新增需宿主信任的 Codex 工具守卫,保留原任务和请求编号;接入确定性复用纪律、用途审查、变更分类、逐字段等价与全仓差集验收;仅验证完成并回传后清理冗余快照,按真实回执展示技术评分;更新日志每页 5 条。
package/IDE-EXECUTION.md CHANGED
@@ -2,6 +2,8 @@ Install or update the managed copy with `npx @entrorove/skill install`. The inst
2
2
 
3
3
  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.
4
4
 
5
+ If the IDE guard itself blocks the management step, run `entrorove unlock` directly from the installed CLI. This break-glass command is offline and management-only: it does not call a hook, read a credential, accept the agreement, inspect the protocol or authorize project writes. Use it to inspect or terminate the original flow; `flow-abandon` releases its task binding so the original task can start again.
6
+
5
7
  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.
6
8
 
7
9
  On first use or after a material agreement revision, the CLI returns a Confirm Protocol-compatible agreement interaction. Render it with the IDE's native confirmation UI, or show its `chatFallback` text when no adapter exists. The user must explicitly accept with `npx @entrorove/skill agreement` and JSON `{"version":"2026-10-07.1","answer":"accept"}` on standard input before retrying the skill request. A full-access IDE permission never counts as agreement acceptance. The combined terms, privacy and advertising notice is installed as `AGREEMENT.md`.
@@ -22,6 +24,10 @@ When a bounded watch returns `nextAction`, continue it with the returned `afterI
22
24
 
23
25
  Set `ENTROROVE_IDE` to the actual host name (for example `Codex`, `Cursor` or `Claude Code`) when host detection is unavailable. It carries only the IDE label; credentials remain in the restricted store. Use the latest official package with `--prefer-online` for new invocations, and reread the managed `SKILL.md` after installation reports an update. Recovering an uncertain request remains a read-only query of its original ID.
24
26
 
27
+ ### Interrupting and resuming requirements
28
+
29
+ User text received during an unfinished governed task is persisted as an exact, private queue entry. `queue status` reads it; `queue route` assigns the existing `background`, `normal`, `high` or `urgent` priority. `interrupt` only takes effect at a controlled-call checkpoint and keeps the parent flow, task ID, task number, request ID, repository and account binding. Once the inserted requirement has a verified completion and stored feedback acknowledgement, the parent is restored and its returned `nextStep` is the only continuation. The guard blocks Stop while an entry is unclassified, a call is unresolved or a runnable parent/child remains. The queue does not promise execution when the host is powered off or the IDE process is unavailable.
30
+
25
31
  Send JSON on standard input to `npx @entrorove/skill invoke`. Before every execution call, discover `operation: "capabilities"` with `{}` and validate input against the returned operation schema. The CLI performs remote discovery automatically; local adapters discover the installed contract before execution. Discovery failure stops execution. Adapters that expose the four-step task route can call `task-describe`, `message-route`, `message-accept` and `task-resume` in order; `start` remains the one-call workflow equivalent. Then start with `operation: "start"` and an `input.task` containing the goal, work type, risk, difficulty and observed scope facts. Work types are `implementation`, `documentation`, `calculation`, `merge`, `review`, `decision` and `diagnosis`. Assess facts from the real task and repository; do not invent certainty. entrorove chooses the workflow. A small task may return `decision: "direct"`, which skips extra planning round trips. It does not authorize native writes outside the controlled task; keep its executable acceptance assertion and the original mutation authority. The public envelope is `entrorove.skill.request/2.0`; `requestId` is the SHA-256 digest of `operation + canonical(input)`. Version 1.0 remains accepted for older callers.
26
32
 
27
33
  The server's ordinary `start` operation creates a durable skill flow and counts a skill invocation. The IDE CLI routes new starts through the account's preference and explicit task choice described above. A real sandbox is created only by `/api/cloud-task`, which returns both `taskId` and `taskNumber` and counts sandbox usage separately. `local` changes only this task's route. Disabled entitlement or missing model prerequisites stop cloud dispatch; a `needs-input`, `blocked` or `failed` result must never be described as sandbox entry. Do not send a second ordinary `start` after cloud acceptance; continue the original bound flow returned by its watch/status.
package/SKILL.md CHANGED
@@ -5,23 +5,27 @@ description: Execute an engineering task through Entrorove, preserving its origi
5
5
 
6
6
  # entrorove · 熵巡
7
7
 
8
- Version: 0.1.38.
8
+ Version: 0.1.40.
9
9
 
10
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.
11
11
 
12
12
  ## Required execution
13
13
 
14
- 1. Install `npx --yes --prefer-online @entrorove/skill@latest install`. The managed installer installs the skill and Codex lifecycle guard. Review and trust Entrorove in Codex `/hooks`; installation alone is not proof that hooks run. 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.
14
+ 1. Install `npx --yes --prefer-online @entrorove/skill@latest install`. The managed installer installs the skill and Codex lifecycle guard. The live host callback must verify that its hook runs. If the guard blocks a management call, run the installed `entrorove unlock` command directly in a terminal to open a short-lived, offline management-only recovery path. 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.
15
15
  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.
16
16
  3. Discover `capabilities`, then invoke `start` with the actual goal, task type, risk and scope facts. An enabled account returns a cloud choice before charging a model. Show that choice visibly using the host question tool. Confirmation calls its original callback; exit adopts the local route for this task. Never treat silence as permission, invent acceptance, or create both routes. Existing explicit authorization for this task persists.
17
17
  4. After real cloud acceptance, immediately display **云端任务 <taskNumber> · 已进入**. Preserve `taskNumber` verbatim: it is the homepage sandbox tab number. Keep its UUID `taskId` separately. Run the returned `cloud-watch`, show each real public event and its cursor, and continue the original accepted flow from status. A command log alone is insufficient. No accepted cloud task means no cloud ID or model-execution claim.
18
- 5. For an accepted `flowId`, execute the returned `nextStep` with its required payload. Discover the schema before constructing input. An empty payload is valid only when that operation's schema allows it. Reconnect and refresh only the original flow. A changed snapshot is a binding failure to investigate; do not invent a fresh successful baseline, rebuild a flow to evade it, or remove the old failure.
18
+ 5. For an accepted `flowId`, execute the returned `nextStep` with its required payload. Discover the schema before constructing input. An empty payload is valid only when that operation's schema allows it. Reconnect and refresh only the original flow. A changed snapshot is a binding failure to investigate; do not invent a fresh successful baseline, rebuild a flow to evade it, or erase the old failure. A terminal flow may release its task slot only through the explicit `flow-abandon` operation.
19
19
  6. All governed mutations use the original authority: `local guarded-write`, `local guarded-delete`, `local guarded-move`, or the original frozen approved runner plan. Native patches, scripts, terminal writes, database mutations and unknown MCP operations must not substitute for the controlled entry. **A failed gate or unknown outcome must never be replaced with another executor or reported as local success.** Query the original request read-only, repair the cause, and obtain the original flow's valid authority before proceeding.
20
20
  7. Read [REUSE-DISCIPLINE.md](REUSE-DISCIPLINE.md) when modifying an existing system. The comparison baseline is the existing implementation in that same repository. It never means another project, another account, or copying external systems.
21
21
  8. Before finishing, submit actual independent execution evidence, run the server's verification and verdict, and display the confirmed evaluation acknowledgement. Preserve `receiptId`, `requestId`, execution generation and evaluation digest. “已回传” requires `acknowledgement.stored: true`. Missing or failed feedback remains visible. Completed verified tasks automatically release superseded Entrorove snapshot copies, preserving origin and final rollback images; active, waiting and unknown tasks retain recovery data.
22
22
 
23
+ If the original flow is terminal, invoke the discovered `flow-cancel` or `flow-abandon` operation with that same `flowId`. `flow-cancel` records the termination and keeps the task bound for read-only recovery. `flow-abandon` clears acceptance and checkpoint state, unbinds the task and returns `nextStep.operation: "start"` so the original task can be routed again. The local equivalents are `local flow-cancel` and `local flow-abandon`; they never reopen the old flow.
24
+
23
25
  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.
24
26
 
27
+ When a new user requirement arrives while the original task is unfinished, store its exact text in the private IDE queue. Use `queue` with `{"action":"status"}` to inspect it, then `{"action":"route","messageId":"...","mode":"queued"|"interrupt","priority":"background"|"normal"|"high"|"urgent"}`. An urgent interrupt is admitted only at a completed controlled-call checkpoint; it saves the original flow, task UUID, homepage number and request ID. After the inserted task reaches a verified end acknowledgement, the queue restores that exact parent context and its next operation. Unclassified text, unresolved calls and unknown outcomes block ending the turn. Queue state is bounded, account/session/repository-bound and survives compaction and process restart; it cannot wake a powered-off host.
28
+
25
29
  ## Operations and visible progress
26
30
 
27
31
  - Invoke service operations with JSON stdin to `npx @entrorove/skill invoke` or a host-approved rewritten equivalent.
@@ -0,0 +1,56 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ export const breakGlassSchema = 'entrorove.ide-break-glass/1.0';
7
+ export const breakGlassTtlMs = 15 * 60 * 1000;
8
+
9
+ function homeDirectory(home) {
10
+ if (typeof home !== 'string' || !path.isAbsolute(home)) throw new Error('Break-glass home must be absolute');
11
+ const root = process.platform === 'win32' ? process.env.LOCALAPPDATA : home;
12
+ if (process.platform === 'win32' && (!root || !path.isAbsolute(root))) throw new Error('LOCALAPPDATA must be absolute');
13
+ return path.join(root, ...(process.platform === 'darwin'
14
+ ? ['Library', 'Application Support', 'Entrorove', 'ide'] : process.platform === 'win32'
15
+ ? ['Entrorove', 'ide'] : ['.local', 'share', 'entrorove', 'ide']));
16
+ }
17
+
18
+ async function ensurePrivateDirectory(directory, home) {
19
+ const homeStat = await fs.lstat(home).catch(() => null);
20
+ if (!homeStat?.isDirectory() || homeStat.isSymbolicLink()
21
+ || process.platform !== 'win32' && homeStat.uid !== process.getuid()) throw new Error('Break-glass home must be an owned real directory');
22
+ let current = process.platform === 'win32' ? path.parse(directory).root : await fs.realpath(home);
23
+ const relative = path.relative(process.platform === 'win32' ? path.parse(directory).root : home, directory);
24
+ if (relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error('Break-glass state escapes its home');
25
+ for (const part of relative.split(path.sep).filter(Boolean)) {
26
+ current = path.join(current, part);
27
+ await fs.mkdir(current, { mode: 0o700 }).catch(cause => { if (cause.code !== 'EEXIST') throw cause; });
28
+ const stat = await fs.lstat(current);
29
+ if (!stat.isDirectory() || stat.isSymbolicLink()
30
+ || process.platform !== 'win32' && (stat.uid !== process.getuid() || stat.mode & 0o077)) {
31
+ throw new Error('Break-glass state must be an owned private directory');
32
+ }
33
+ }
34
+ if (process.platform !== 'win32') await fs.chmod(current, 0o700);
35
+ }
36
+
37
+ export function breakGlassPath(home = os.homedir()) {
38
+ return path.join(homeDirectory(home), 'break-glass.json');
39
+ }
40
+
41
+ export async function writeBreakGlass(home = os.homedir(), now = Date.now()) {
42
+ if (!Number.isFinite(now)) throw new Error('Break-glass clock is invalid');
43
+ const file = breakGlassPath(home), directory = path.dirname(file);
44
+ await ensurePrivateDirectory(directory, home);
45
+ const value = { schemaVersion: breakGlassSchema, mode: 'management-only', createdAt: new Date(now).toISOString(),
46
+ expiresAt: new Date(now + breakGlassTtlMs).toISOString() };
47
+ const stage = `${file}.${randomUUID()}.tmp`;
48
+ await fs.writeFile(stage, `${JSON.stringify(value)}\n`, { flag: 'wx', mode: 0o600 });
49
+ try {
50
+ const prior = await fs.lstat(file).catch(cause => { if (cause.code === 'ENOENT') return null; throw cause; });
51
+ if (prior && (!prior.isFile() || prior.isSymbolicLink() || prior.nlink !== 1)) throw new Error('Break-glass state must be a private regular file');
52
+ await fs.rename(stage, file);
53
+ if (process.platform !== 'win32') await fs.chmod(file, 0o600);
54
+ } finally { await fs.rm(stage, { force: true }); }
55
+ return { offline: true, network: false, hook: 'not-required', protocol: 'not-checked', mode: value.mode, expiresAt: value.expiresAt };
56
+ }
package/cli.mjs CHANGED
@@ -20,6 +20,7 @@ import { watchCloudTask } from './cloud-watch-client.mjs';
20
20
  import { queryAutomaticFeedback } from './automatic-feedback-client.mjs';
21
21
  import { installIdeHooks } from './ide-install.mjs';
22
22
  import { assertIdeExecution } from './ide-session.mjs';
23
+ import { writeBreakGlass } from './break-glass.mjs';
23
24
  const markerName = '.entrorove-managed.json';
24
25
  const installFiles = ['SKILL.md', 'CAPABILITY-MAP.md', 'AGREEMENT.md', 'manifest.json', 'IDE-EXECUTION.md', 'REUSE-DISCIPLINE.md'];
25
26
  const markerSchema = 'entrorove.managed-install/1.0';
@@ -62,13 +63,8 @@ async function sourceBundle() {
62
63
  return { manifest, content, hashes };
63
64
  }
64
65
 
65
- function globalInstallPath(home = os.homedir()) {
66
- return path.join(home, '.codex', 'skills', 'entrorove');
67
- }
68
-
69
- function agreementPath(home = os.homedir()) {
70
- return path.join(path.dirname(credentialPath(home)), 'agreement.json');
71
- }
66
+ function globalInstallPath(home = os.homedir()) { return path.join(home, '.codex', 'skills', 'entrorove'); }
67
+ function agreementPath(home = os.homedir()) { return path.join(path.dirname(credentialPath(home)), 'agreement.json'); }
72
68
 
73
69
  export function agreementInteraction() {
74
70
  return {
@@ -431,7 +427,9 @@ async function submitReview(data) {
431
427
 
432
428
  async function main() {
433
429
  const [command, ...args] = process.argv.slice(2);
434
- if (command === 'install' && args.length === 0) {
430
+ if (command === 'unlock' && args.length === 0) {
431
+ process.stdout.write(`${JSON.stringify(await writeBreakGlass())}\n`);
432
+ } else if (command === 'install' && args.length === 0) {
435
433
  process.stdout.write(JSON.stringify(await install()) + '\n');
436
434
  } else if (command === 'configure' && args.length === 0) {
437
435
  const data = await stdinJson();
@@ -442,6 +440,8 @@ async function main() {
442
440
  await install();
443
441
  const accepted = await writeAgreementAcceptance(await stdinJson(), os.homedir());
444
442
  process.stdout.write(JSON.stringify({ accepted, version: agreementVersion }) + '\n');
443
+ } else if (command === 'queue' && args.length === 0) {
444
+ process.stdout.write(JSON.stringify(await (await import('./ide-queue.mjs')).runIdeQueue(await stdinJson())) + '\n');
445
445
  } else if (command === 'invoke' && args.length === 0) {
446
446
  await invoke(await stdinJson());
447
447
  } else if (command === 'native' && args.length === 2) {
@@ -487,14 +487,12 @@ async function main() {
487
487
  } else if (command === 'local') {
488
488
  await runLocal(args);
489
489
  } else {
490
- throw new Error('Usage: entrorove install | configure | agreement | invoke | native <domain> <operation> | notifications configure|status|test | scope-connect <repositoryRoot> | host-message [adapterModulePath] | cloud-task | cloud-watch | cloud-status | feedback <requestId> [generation] | status <requestId> | flows | review | local (JSON on stdin)');
490
+ throw new Error('Usage: entrorove unlock | install | configure | agreement | invoke | queue | native <domain> <operation> | notifications configure|status|test | scope-connect <repositoryRoot> | host-message [adapterModulePath] | cloud-task | cloud-watch | cloud-status | feedback <requestId> [generation] | status <requestId> | flows | review | local (JSON on stdin)');
491
491
  }
492
492
  }
493
- if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
494
- const { captureIdeResults } = await import('./ide-results.mjs');
493
+ if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) { const { captureIdeResults } = await import('./ide-results.mjs');
495
494
  const finish = captureIdeResults();
496
- main().catch((error) => {
497
- process.stderr.write(`${error instanceof SyntaxError ? 'Invalid JSON data' : error.message}\n`);
495
+ main().catch((error) => { process.stderr.write(`${error instanceof SyntaxError ? 'Invalid JSON data' : error.message}\n`);
498
496
  process.exitCode = 1;
499
497
  }).finally(finish).catch(() => { process.stderr.write('Entrorove IDE completion could not be saved; recover the original request.\n'); process.exitCode = 1; });
500
498
  }
package/ide-command.mjs CHANGED
@@ -37,8 +37,11 @@ export function sdkCommand(parsed, runtime) {
37
37
  while (['--yes', '--prefer-online'].includes(words[0])) words.shift();
38
38
  if (!['@entrorove/skill', '@entrorove/skill@latest', `@entrorove/skill@${runtime.version}`].includes(words.shift())) return null;
39
39
  args = words;
40
+ } else if (path.basename(words[0]) === 'entrorove') {
41
+ args = words.slice(1);
40
42
  } else return null;
41
- const simple = new Set(['invoke', 'cloud-task', 'cloud-status', 'cloud-watch', 'agreement', 'configure']);
43
+ const simple = new Set(['invoke', 'cloud-task', 'cloud-status', 'cloud-watch', 'agreement', 'configure', 'queue']);
44
+ if (args[0] === 'unlock' && args.length === 1 && !parsed.input) return { args };
42
45
  if (simple.has(args[0]) && args.length === 1 && parsed.input) return { args, input: parsed.input };
43
46
  if (['status', 'feedback'].includes(args[0]) && /^[a-f0-9]{64}$/u.test(args[1] ?? '') && [2,3].includes(args.length)) return { args };
44
47
  if (['flows', 'install'].includes(args[0]) && args.length === 1 && !parsed.input) return { args };
package/ide-hook.mjs CHANGED
@@ -1,24 +1,62 @@
1
1
  #!/usr/bin/env node
2
2
  import { realpathSync } from 'node:fs';
3
3
  import { fileURLToPath } from 'node:url';
4
- import { ideSession, ideRuntime, bindIdeAccount, ideContext, ideDigest } from './ide-session.mjs';
4
+ import { ideSession, ideRuntime, bindIdeAccount, ideContext, ideDigest, readBreakGlass } from './ide-session.mjs';
5
5
  import { literalCommand, sdkCommand, readCommand, renderSdkCommand, installerCommand } from './ide-command.mjs';
6
6
  import { inspectIdeCodeMode } from './ide-code-mode.mjs';
7
7
  import { pathToFileURL } from 'node:url';
8
8
  import path from 'node:path';
9
9
  import { parseIdeResults, consumeIdeResults } from './ide-results.mjs';
10
+ import { registerIdeRequirement, ideQueueContext, unclassifiedIdeRequirements,
11
+ advanceIdeQueue, checkQueuedStart, unfinishedIdeQueue } from './ide-queue.mjs';
10
12
 
11
13
  const output = (event, extra = {}) => ({ hookSpecificOutput: { hookEventName: event, ...extra } });
12
14
  const deny = reason => output('PreToolUse', { permissionDecision: 'deny', permissionDecisionReason: reason });
13
15
  const publicId = value => typeof value === 'string' && /^[A-Za-z0-9_-]{1,200}$/u.test(value);
14
16
  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']);
17
+ const unlockCommand = 'entrorove unlock';
18
+ const managementOperations = new Set(['capabilities', 'help', 'start', 'continue', 'task-describe', 'flow-cancel', 'flow-abandon', 'scope-connect', 'status', 'feedback', 'cloud-status', 'cloud-watch', 'queue', 'unlock']);
19
+ function operationOf(call) {
20
+ if (!call) return null;
21
+ if (call.args[0] === 'invoke') return call.input?.operation ?? null;
22
+ if (call.args[0] === 'local') return call.args[1] ?? null;
23
+ return call.args[0] ?? null;
24
+ }
25
+ function isManagementCall(call) { return managementOperations.has(operationOf(call)); }
26
+ function resetBlockStreak(state) { state.blockStreak = 0; delete state.lastBlockKey; }
27
+ function blockedReason(state, event, reason) {
28
+ const key = ideDigest({ tool: event.tool_name, phase: state.phase, reason });
29
+ const repeated = state.lastBlockKey === key;
30
+ state.blockStreak = repeated ? (state.blockStreak ?? 0) + 1 : 1;
31
+ state.lastBlockKey = key;
32
+ const mechanism = `机制:${state.stateFile ?? '会话状态文件'} 当前 phase=${state.phase}${state.pendingCall ? `,pendingCall=${state.pendingCall.toolUseId}` : ''};该阶段只放行 Entrorove 管理调用,业务与写操作需先建立原 flow 并取得 nextStep。`;
33
+ const repeat = repeated && state.blockStreak >= 2 ? `同一阻断已出现 ${state.blockStreak} 次,禁止再次检查:说明机制、给出绕过方案并执行绕过。` : '';
34
+ return `${reason} ${mechanism} ${repeat}\n绕过方案(离线、无网络、无协议、不经 hook,终端直接运行):\n${unlockCommand}\n执行后重试原管理调用;项目写入仍需原任务授权。`;
35
+ }
36
+ const denyBlocked = (state, event, reason) => deny(blockedReason(state, event, reason));
37
+ function lifecycleResult(value, depth = 0) {
38
+ if (!value || typeof value !== 'object' || depth > 4) return null;
39
+ if (value.operation === 'flow-cancel' || value.operation === 'flow-abandon') return value;
40
+ for (const key of ['output', 'result']) {
41
+ const nested = lifecycleResult(value[key], depth + 1);
42
+ if (nested) return nested;
43
+ }
44
+ return null;
45
+ }
15
46
  function collectResult(state, values, local) {
16
47
  const messages = [];
17
48
  for (const result of values) {
18
49
  const value = result.output ?? result;
19
- if (typeof value.requestId === 'string' && /^[a-f0-9]{64}$/u.test(value.requestId)) state.requestId = value.requestId;
20
- if (!local && value.flowId && value.requestId) state.remoteRequestId = value.requestId;
21
- if (publicId(value.flowId)) {
50
+ const lifecycleValue = lifecycleResult(result) ?? value;
51
+ const lifecycle = lifecycleValue.operation === 'flow-cancel' || lifecycleValue.operation === 'flow-abandon';
52
+ const releasedLifecycle = lifecycleValue.operation === 'flow-abandon' && lifecycleValue.unbound === true && lifecycleValue.flowId === state.flowId;
53
+ if (releasedLifecycle) {
54
+ delete state.flowId; delete state.taskId; delete state.taskNumber; delete state.requestId; delete state.remoteRequestId;
55
+ delete state.nextStep; state.phase = 'entry'; state.feedbackAcknowledged = false;
56
+ } else if (lifecycleValue.operation === 'flow-cancel' && lifecycleValue.flowId === state.flowId) state.phase = 'ended';
57
+ if (!releasedLifecycle && typeof value.requestId === 'string' && /^[a-f0-9]{64}$/u.test(value.requestId)) state.requestId = value.requestId;
58
+ if (!releasedLifecycle && !local && value.flowId && value.requestId) state.remoteRequestId = value.requestId;
59
+ if (!releasedLifecycle && publicId(value.flowId)) {
22
60
  if (state.flowId && state.flowId !== value.flowId) throw new Error('Returned flow replaced the original task');
23
61
  state.flowId = value.flowId;
24
62
  }
@@ -27,15 +65,15 @@ function collectResult(state, values, local) {
27
65
  state.taskId = value.taskId; state.taskNumber = value.taskNumber;
28
66
  }
29
67
  const nextStep = value.nextStep ?? value.result?.nextStep ?? value.result?.result?.nextStep;
30
- if (value.status === 'needs-input' || value.result?.status === 'needs-input' || value.interaction || value.result?.interaction) state.phase = 'needs-input';
31
- else if (['blocked', 'failed', 'incomplete'].includes(value.status) || ['blocked', 'failed', 'incomplete'].includes(value.result?.status)
32
- || ['blocked', 'failed'].includes(value.taskStatus)) state.phase = 'blocked';
33
- else if (nextStep || value.nextAction) state.phase = 'continuing';
34
- else if (!local && (value.reports?.length || value.result?.reports?.length || ['completed','cancelled'].includes(value.taskStatus))
68
+ if (!lifecycle && (value.status === 'needs-input' || value.result?.status === 'needs-input' || value.interaction || value.result?.interaction)) state.phase = 'needs-input';
69
+ else if (!lifecycle && (['blocked', 'failed', 'incomplete'].includes(value.status) || ['blocked', 'failed', 'incomplete'].includes(value.result?.status)
70
+ || ['blocked', 'failed'].includes(value.taskStatus))) state.phase = 'blocked';
71
+ else if (!lifecycle && (nextStep || value.nextAction)) state.phase = 'continuing';
72
+ else if (!lifecycle && !local && (value.reports?.length || value.result?.reports?.length || ['completed','cancelled'].includes(value.taskStatus))
35
73
  && result.feedback?.acknowledgement?.stored === true) state.phase = 'ended';
36
- else if (local && value.status === 'verified') state.phase = 'continuing';
37
- else if (value.status === 'started' || value.status === 'succeeded') state.phase = 'continuing';
38
- if (nextStep) state.nextStep = { operation: nextStep.operation, payload: nextStep.payload };
74
+ else if (!lifecycle && local && value.status === 'verified') state.phase = 'continuing';
75
+ else if (!lifecycle && (value.status === 'started' || value.status === 'succeeded')) state.phase = 'continuing';
76
+ if (!lifecycle && nextStep) state.nextStep = { operation: nextStep.operation, payload: nextStep.payload };
39
77
  const notices = [...(result.ideMessages ?? []), ...(value.ideMessages ?? []),
40
78
  ...(value.schemaVersion === 'entrorove.ide-event/1.0' ? [value.message] : []), ...(value.events ?? []).map(item => item.message)];
41
79
  for (const message of notices) {
@@ -49,6 +87,7 @@ function collectResult(state, values, local) {
49
87
  return messages;
50
88
  }
51
89
  async function beforeTool(event, state, home) {
90
+ const unlocked = await readBreakGlass(home);
52
91
  const command = event.tool_input?.command ?? event.tool_input?.cmd, parsed = literalCommand(command);
53
92
  const isShell = ['Bash', 'exec_command', 'shell_command'].includes(event.tool_name);
54
93
  const installer = isShell && installerCommand(parsed);
@@ -60,25 +99,35 @@ async function beforeTool(event, state, home) {
60
99
  return output('PreToolUse', { permissionDecision: 'allow', updatedInput: { ...event.tool_input, command: installer,
61
100
  ...(Object.hasOwn(event.tool_input ?? {}, 'cmd') ? { cmd: installer } : {}) } });
62
101
  }
63
- const namedSdk = parsed?.words.some(word => /^@entrorove\/skill(?:@[^ ]+)?$/u.test(word)
102
+ const namedSdk = parsed?.words.some(word => word === 'entrorove' || /^@entrorove\/skill(?:@[^ ]+)?$/u.test(word)
64
103
  || /(?:@entrorove\/skill|entrorove-skill)\/cli\.mjs$/u.test(word));
65
104
  if (!state.active && !namedSdk) return {};
66
105
  const runtime = isShell && parsed ? await ideRuntime(home) : null;
67
106
  const call = runtime && sdkCommand(parsed, runtime);
68
107
  if (call) {
69
108
  state.active = true;
70
- if (call.args[0] !== 'configure') await bindIdeAccount(state, home);
109
+ if (call.args[0] !== 'configure' && call.args[0] !== 'unlock') await bindIdeAccount(state, home);
110
+ if (state.phase === 'setup' && !unlocked && !isManagementCall(call)) return denyBlocked(state, event, 'Entrorove:自举阶段只放行 Entrorove 管理调用(建 flow、改目标、scope-connect、capabilities、status、queue 等);业务与写操作需先建立原 flow。');
111
+ if (call.args[0] === 'unlock') {
112
+ resetBlockStreak(state);
113
+ const rewritten = renderSdkCommand(call, runtime, state.sessionId);
114
+ return output('PreToolUse', { permissionDecision: 'allow', updatedInput: { ...event.tool_input, command: rewritten,
115
+ ...(Object.hasOwn(event.tool_input ?? {}, 'cmd') ? { cmd: rewritten } : {}) }, additionalContext: `${ideContext(state)}离线管理解锁已放行。` });
116
+ }
71
117
  const newStart = call.args[0] === 'cloud-task' || call.args[0] === 'invoke' && ['start','cloud-task'].includes(call.input?.operation);
72
- if (newStart && (state.flowId || state.taskId) && state.phase !== 'ended') return deny('Entrorove:必须继续原任务,禁止用第二次 start 或 cloud-task 绕过原门禁。');
118
+ if (newStart && (state.flowId || state.taskId) && state.phase !== 'ended') return denyBlocked(state, event, 'Entrorove:必须继续原任务,禁止用第二次 start 或 cloud-task 绕过原门禁。');
73
119
  const discovery = ['invoke','local'].includes(call.args[0]) && ['capabilities','help'].includes(call.input?.operation ?? call.args[1]);
74
- const observer = discovery || ['status','feedback','cloud-status','configure','agreement','install'].includes(call.args[0]);
75
- if (call.args[0] === 'local' && !discovery && !state.flowId) return deny('Entrorove:本地执行前必须先受理原任务并绑定账号、flowId 与仓库;不能直接使用其它流程的通行证。');
76
- if (state.pendingCall && !observer) return deny('Entrorove:原调用尚未收到结束回执;先只读恢复其状态,禁止并发重发。');
120
+ const observer = discovery || ['status','feedback','cloud-status','configure','agreement','install','queue','flows'].includes(call.args[0]);
121
+ if (unclassifiedIdeRequirements(state) && !observer && !(unlocked && isManagementCall(call))) return denyBlocked(state, event, 'Entrorove:新需求已保存,先用 queue status 读取并按轻重缓急 route 分类;禁止丢弃新需求或覆盖原任务。');
122
+ if (newStart && !checkQueuedStart(state,call)) return denyBlocked(state, event, 'Entrorove:新任务目标必须与实际进入队列的用户原文一致。');
123
+ if (call.args[0] === 'local' && !discovery && !state.flowId) return denyBlocked(state, event, 'Entrorove:本地执行前必须先受理原任务并绑定账号、flowId 与仓库;不能直接使用其它流程的通行证。');
124
+ if (state.pendingCall && !observer && !(unlocked && isManagementCall(call))) return denyBlocked(state, event, 'Entrorove:原调用尚未收到结束回执;先只读恢复其状态,禁止并发重发。');
77
125
  const input = call.args[0] === 'invoke' ? call.input?.input : call.input;
78
- if (input?.flowId && state.flowId && input.flowId !== state.flowId || input?.taskId && state.taskId && ['cloud-watch','cloud-status'].includes(call.args[0]) && input.taskId !== state.taskId) return deny('Entrorove:调用与本 IDE 任务的原编号不一致。');
126
+ if (input?.flowId && state.flowId && input.flowId !== state.flowId || input?.taskId && state.taskId && ['cloud-watch','cloud-status'].includes(call.args[0]) && input.taskId !== state.taskId) return denyBlocked(state, event, 'Entrorove:调用与本 IDE 任务的原编号不一致。');
79
127
  if (call.args[0] === 'scope-connect' && call.args[1] !== state.repositoryRoot
80
- || call.args[0] === 'local' && (input?.repositoryRoot && input.repositoryRoot !== state.repositoryRoot || call.args[2] && call.args[2] !== state.repositoryRoot)) return deny('Entrorove:调用不能替换本任务的实际仓库。');
128
+ || call.args[0] === 'local' && (input?.repositoryRoot && input.repositoryRoot !== state.repositoryRoot || call.args[2] && call.args[2] !== state.repositoryRoot)) return denyBlocked(state, event, 'Entrorove:调用不能替换本任务的实际仓库。');
81
129
  let requestId;
130
+ if (newStart) state.taskGoal = call.args[0] === 'cloud-task' ? call.input?.goal : call.input?.input?.task?.goal;
82
131
  if (call.args[0] === 'invoke' && !call.input.operation.startsWith('cloud-')) {
83
132
  const { buildInvocation } = await import(pathToFileURL(path.join(path.dirname(runtime.cli), 'remote-client.mjs')).href);
84
133
  requestId = buildInvocation(call.input.operation, call.input.input).requestId;
@@ -86,21 +135,22 @@ async function beforeTool(event, state, home) {
86
135
  const pending = { toolUseId: event.tool_use_id, inputDigest: ideDigest(call), command: call.args[0], ...(requestId ? { requestId } : {}) };
87
136
  if (observer) {
88
137
  state.readCalls ??= {};
89
- if (Object.keys(state.readCalls).length >= 32) return deny('Entrorove:只读恢复尚未返回,请等待原调用。');
138
+ if (Object.keys(state.readCalls).length >= 32) return denyBlocked(state, event, 'Entrorove:只读恢复尚未返回,请等待原调用。');
90
139
  state.readCalls[event.tool_use_id] = pending;
91
140
  } else state.pendingCall = pending;
92
141
  const installation = call.args[0] === 'install' && parsed.words.some(word => ['npx','npx.cmd'].includes(word));
93
142
  const rewritten = renderSdkCommand(call, runtime, state.sessionId);
143
+ resetBlockStreak(state);
94
144
  return output('PreToolUse', { permissionDecision: 'allow', ...(installation ? {} : { updatedInput: { ...event.tool_input, command: rewritten,
95
145
  ...(Object.hasOwn(event.tool_input ?? {}, 'cmd') ? { cmd: rewritten } : {}) } }), additionalContext: ideContext(state) });
96
146
  }
97
147
  if (!state.active) return {};
98
148
  if (['exec','functions.exec'].includes(event.tool_name)) {
99
149
  const current = await ideRuntime(home), code = typeof event.tool_input === 'string' ? event.tool_input : event.tool_input?.code;
100
- return inspectIdeCodeMode(code, current) ? output('PreToolUse', { additionalContext: ideContext(state) }) : deny('Entrorove:代码模式含未批准的原生执行或动态工具输入;请使用原任务的受控入口。');
150
+ return inspectIdeCodeMode(code, current) ? output('PreToolUse', { additionalContext: ideContext(state) }) : denyBlocked(state, event, 'Entrorove:代码模式含未批准的原生执行或动态工具输入;请使用原任务的受控入口。');
101
151
  }
102
152
  if (isShell && readCommand(parsed) || readTools.has(event.tool_name)) return output('PreToolUse', { additionalContext: ideContext(state) });
103
- return deny(`Entrorove:已阻止原生 ${event.tool_name ?? 'unknown'} 调用。当前阶段 ${state.phase}${state.taskNumber ? `,云端任务 ${state.taskNumber}` : ''}。写入、删除、移动及不确定的终端操作必须使用原任务的 guarded-write/guarded-delete/guarded-move 或已批准的执行计划;门禁未通过不得改换执行器。`);
153
+ return denyBlocked(state, event, `Entrorove:已阻止原生 ${event.tool_name ?? 'unknown'} 调用。当前阶段 ${state.phase}${state.taskNumber ? `,云端任务 ${state.taskNumber}` : ''}。写入、删除、移动及不确定的终端操作必须使用原任务的 guarded-write/guarded-delete/guarded-move 或已批准的执行计划。`);
104
154
  }
105
155
  export async function runIdeHook(event, { home } = {}) {
106
156
  return ideSession(event, async state => {
@@ -113,6 +163,7 @@ export async function runIdeHook(event, { home } = {}) {
113
163
  state.active = true;
114
164
  if (state.phase === 'ended') { delete state.flowId; delete state.taskId; delete state.taskNumber; delete state.requestId; state.phase = 'entry'; }
115
165
  }
166
+ if (name === 'UserPromptSubmit') registerIdeRequirement(state,event);
116
167
  if (name === 'PreToolUse') return beforeTool(event, state, home);
117
168
  const pendingRead = state.readCalls?.[event.tool_use_id];
118
169
  if (name === 'PostToolUse' && state.active && (state.pendingCall?.toolUseId === event.tool_use_id || pendingRead)) {
@@ -120,6 +171,10 @@ export async function runIdeHook(event, { home } = {}) {
120
171
  const recorded = await consumeIdeResults(event, state, pending, home);
121
172
  const visible = parseIdeResults(event.tool_response);
122
173
  const values = [...recorded, ...visible];
174
+ if (pending.command==='queue') {
175
+ delete state.readCalls[event.tool_use_id];
176
+ return output(name,{additionalContext:`${ideContext(state)}${ideQueueContext(state)}`});
177
+ }
123
178
  if (['install','configure','agreement'].includes(pending.command)) {
124
179
  delete state.readCalls[event.tool_use_id];
125
180
  const configured = values.some(value => pending.command === 'install' ? /^\d+\.\d+\.\d+$/u.test(value.version ?? '') && value.ide?.configured === true
@@ -143,10 +198,16 @@ export async function runIdeHook(event, { home } = {}) {
143
198
  const messages = collectResult(state, bound, pending.command === 'local');
144
199
  if (pendingRead) delete state.readCalls[event.tool_use_id];
145
200
  if (!pendingRead || recoveredOriginal) delete state.pendingCall;
201
+ const resumed = await advanceIdeQueue(state,home);
202
+ if (resumed) messages.push(resumed);
146
203
  return { ...output(name, { additionalContext: ideContext(state) }), ...(messages.length ? { systemMessage: messages.join('\n') } : {}) };
147
204
  }
148
- if (name === 'Stop' && state.active && !['ended','blocked','needs-input','setup'].includes(state.phase)) return { decision: 'block', reason: `${ideContext(state)}本轮没有取得原任务的结束回执,必须继续返回的操作或明确报告阻断原因。` };
149
- return state.active && ['SessionStart','UserPromptSubmit','PostCompact','PreCompact'].includes(name) ? output(name, { additionalContext: ideContext(state) }) : {};
205
+ if (name==='Stop' && state.active && !(await readBreakGlass(home))) {
206
+ const resumed = await advanceIdeQueue(state,home);
207
+ if (resumed || unclassifiedIdeRequirements(state) || unfinishedIdeQueue(state) && !['blocked','needs-input'].includes(state.phase)
208
+ || !['ended','blocked','needs-input','setup'].includes(state.phase)) return {decision:'block',reason:blockedReason(state, event, `${ideContext(state)}${ideQueueContext(state)}${resumed}原任务或队列尚未取得结束回执;继续可运行工作,真实阻断必须明确报告。`)};
209
+ }
210
+ return state.active && ['SessionStart','UserPromptSubmit','PostCompact','PreCompact'].includes(name) ? output(name, { additionalContext: `${ideContext(state)}${ideQueueContext(state)}` }) : {};
150
211
  }, home);
151
212
  }
152
213
  let incomingEvent;
@@ -160,7 +221,7 @@ async function main() {
160
221
  if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) main().catch(() => {
161
222
  // A non-zero generic exit may fail open in the host. Always return a supported
162
223
  // explicit denial, without including arguments, prompts, credentials or paths.
163
- const reason = 'Entrorove 安全检查失败;本次工具调用已阻止,请修复原检查。';
224
+ const reason = `Entrorove 安全检查失败;本次工具调用已阻止。可复制解锁命令:${unlockCommand}`;
164
225
  const result = incomingEvent === 'Stop' ? { decision: 'block', reason } : incomingEvent === 'PreToolUse' ? deny(reason)
165
226
  : { continue: false, stopReason: reason, systemMessage: reason };
166
227
  process.stdout.write(`${JSON.stringify(result)}\n`);
package/ide-install.mjs CHANGED
@@ -5,13 +5,14 @@ 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', 'ide-results.mjs', 'ide-command.mjs', 'ide-code-mode.mjs', 'remote-transport.mjs', 'local-credential.mjs', 'local-state-lock.mjs'];
8
+ const files = ['ide-hook.mjs', 'ide-session.mjs', 'break-glass.mjs', 'ide-results.mjs', 'ide-queue.mjs', 'ide-command.mjs', 'ide-code-mode.mjs', 'remote-transport.mjs', 'local-credential.mjs', 'local-state-lock.mjs'];
9
9
  const events = ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'PostCompact', 'Stop'];
10
10
  const quote = text => `'${text.replace(/'/gu, "'\\''")}'`;
11
11
  function definition(node, file) {
12
12
  const command = `${quote(node)} ${quote(file)}`;
13
13
  return { hooks: Object.fromEntries(events.map(event => [event, [{ hooks: [{ type: 'command', command,
14
- timeout: 10, statusMessage: 'Entrorove · 检查任务', additionalContextLimit: 1500 }] }]])) };
14
+ timeout: 10, statusMessage: 'Entrorove · 检查任务',
15
+ ...(['PostCompact','Stop'].includes(event) ? {} : {additionalContextLimit:1500}) }] }]])) };
15
16
  }
16
17
  async function prepareConfigDirectory(home) {
17
18
  const directory = path.join(home, '.codex');
package/ide-queue.mjs ADDED
@@ -0,0 +1,108 @@
1
+ import path from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { ideDigest, ideError, ideRuntime, ideSession } from './ide-session.mjs';
4
+
5
+ const fields = ['flowId','taskId','taskNumber','requestId','remoteRequestId','phase','nextStep','feedbackAcknowledged','taskGoal'];
6
+ const control = /^(?:继续(?:执行)?[.!。!]?|已信任|已验证|验证了|确认|知道了|进度|啥进度了[??]?|状态|continue[.!]?|status[?]?|confirmed|done)$/iu;
7
+ const fail = message => { throw ideError('ENTROROVE_IDE_QUEUE', message); };
8
+ const context = state => Object.fromEntries(fields.filter(key => Object.hasOwn(state,key)).map(key => [key,structuredClone(state[key])]));
9
+ function restore(state, saved) {
10
+ for (const key of fields) delete state[key];
11
+ Object.assign(state,structuredClone(saved));
12
+ }
13
+ function queue(state) {
14
+ state.queue ??= { schemaVersion:'entrorove.ide-queue/1.0', nextSequence:1, current:null, parents:[], entries:[], completed:[] };
15
+ const value = state.queue;
16
+ if (value.schemaVersion !== 'entrorove.ide-queue/1.0' || !Number.isSafeInteger(value.nextSequence)
17
+ || !Array.isArray(value.parents) || !Array.isArray(value.entries) || !Array.isArray(value.completed)) fail('Original task queue is invalid; it was preserved');
18
+ return value;
19
+ }
20
+ async function priorities(home) {
21
+ const runtime = await ideRuntime(home);
22
+ return (await import(pathToFileURL(path.join(path.dirname(runtime.cli),'local-swarm-state.mjs')).href)).SWARM_PRIORITIES;
23
+ }
24
+ function ensureCurrent(state) {
25
+ const value = queue(state);
26
+ if (!value.current && (state.flowId || state.taskId)) value.current = { messageId:`original-${ideDigest({flowId:state.flowId,taskId:state.taskId})}`,
27
+ goal:state.taskGoal ?? null, priority:'normal', sequence:0, original:true };
28
+ return value;
29
+ }
30
+ export function registerIdeRequirement(state,event) {
31
+ if (!state.active || !(state.flowId || state.taskId || state.queue?.current)) return;
32
+ const text = (event.prompt ?? '').trim();
33
+ if (!text || control.test(text) || /^<send_user_message_question_reply>/u.test(text)) return;
34
+ const value = ensureCurrent(state);
35
+ const sourceId = event.prompt_id ?? event.user_message_id;
36
+ const messageId = `requirement-${ideDigest({sessionId:state.sessionId,sourceId:sourceId ?? value.nextSequence,text})}`;
37
+ if (value.entries.some(item=>item.messageId===messageId) || value.completed.some(item=>item.messageId===messageId)) return;
38
+ if (text.length>100_000 || value.entries.length>=64) fail('Task queue capacity reached; no new requirement was discarded. Resolve the original queue before admission.');
39
+ value.entries.push({messageId,goal:text,goalDigest:ideDigest(text),sequence:value.nextSequence++,status:'unclassified',priority:'normal'});
40
+ }
41
+ export function ideQueueContext(state) {
42
+ const value = state.queue;
43
+ if (!value) return '';
44
+ const pending = value.entries.filter(item=>item.status!=='completed');
45
+ return `${value.parents.length ? `有 ${value.parents.length} 个原任务等待恢复。` : ''}${pending.length ? `尚有 ${pending.length} 条插入需求;先用 queue status 读取,再用 queue route 分类,不能忽略或提前结束。` : ''}`;
46
+ }
47
+ export function unclassifiedIdeRequirements(state) { return state.queue?.entries.some(item=>item.status==='unclassified') ?? false; }
48
+ function activate(state,entry,pause) {
49
+ const value = ensureCurrent(state);
50
+ if (state.pendingCall) fail('An unresolved original call cannot be interrupted; recover it with GET first');
51
+ if (pause && value.current) value.parents.push({item:structuredClone(value.current),context:context(state)});
52
+ value.entries = value.entries.filter(item=>item.messageId!==entry.messageId);
53
+ value.current = {...entry,status:'active'};
54
+ restore(state,{phase:'entry',taskGoal:entry.goal,feedbackAcknowledged:false});
55
+ }
56
+ function sorted(value,ranks) {
57
+ return value.entries.filter(item=>item.status==='queued').sort((a,b)=>ranks.indexOf(b.priority)-ranks.indexOf(a.priority)||a.sequence-b.sequence);
58
+ }
59
+ export async function advanceIdeQueue(state,home) {
60
+ const value = ensureCurrent(state), ranks = await priorities(home), candidate = sorted(value,ranks)[0];
61
+ if (state.phase==='ended') {
62
+ if (value.current) value.completed.push({...value.current,context:context(state),completedAt:new Date().toISOString()});
63
+ value.current = null;
64
+ const parent = value.parents.at(-1);
65
+ if (candidate && (!parent || ranks.indexOf(candidate.priority)>ranks.indexOf(parent.item.priority))) {
66
+ activate(state,candidate,false);
67
+ return `已切换队列任务 ${candidate.messageId};必须按原需求进入受控入口。`;
68
+ }
69
+ if (parent) {
70
+ value.parents.pop(); value.current = parent.item; restore(state,parent.context);
71
+ return `插入任务已结束,自动恢复原任务${state.taskNumber ? ` ${state.taskNumber}` : ''}${state.flowId ? `,flowId=${state.flowId}` : ''}。继续原 nextStep,重新验证实际快照和权限,不能重建流程。`;
72
+ }
73
+ }
74
+ if (!state.pendingCall && !unclassifiedIdeRequirements(state) && candidate
75
+ && (state.phase==='ended' || state.phase==='setup' || candidate.mode==='interrupt'
76
+ && ['continuing','blocked','needs-input'].includes(state.phase))) {
77
+ activate(state,candidate,state.phase!=='ended' && state.phase!=='setup');
78
+ return `已在受控调用结束的检查点插入需求 ${candidate.messageId};完成后必须恢复原任务。`;
79
+ }
80
+ return '';
81
+ }
82
+ export function checkQueuedStart(state,call) {
83
+ const item = state.queue?.current;
84
+ if (!item || item.original || item.status!=='active') return true;
85
+ const goal = call.args[0]==='cloud-task' ? call.input?.goal : call.input?.input?.task?.goal;
86
+ return typeof goal==='string' && ideDigest(goal)===item.goalDigest;
87
+ }
88
+ export function unfinishedIdeQueue(state) {
89
+ return Boolean(state.queue?.parents.length || state.queue?.entries.length || state.queue?.current && state.phase!=='ended');
90
+ }
91
+ export async function runIdeQueue(input,{environment=process.env,home,cwd=process.cwd()}={}) {
92
+ if (environment.ENTROROVE_IDE!=='Codex' || !environment.CODEX_THREAD_ID) fail('Task queue requires its actual guarded Codex session');
93
+ return ideSession({session_id:environment.CODEX_THREAD_ID,cwd},async state=>{
94
+ const value = ensureCurrent(state);
95
+ if (!state.active) fail('This session has not selected Entrorove');
96
+ if (input.action==='route') {
97
+ const entry = value.entries.find(item=>item.messageId===input.messageId);
98
+ if (!entry || entry.status!=='unclassified' || entry.goalDigest!==ideDigest(entry.goal)) fail('Only an actual unclassified user requirement may enter the queue');
99
+ if (!['queued','interrupt'].includes(input.mode) || !(await priorities(home)).includes(input.priority)) fail('Use a declared queue mode and the existing priority contract');
100
+ entry.mode=input.mode; entry.priority=input.priority; entry.status='queued';
101
+ entry.routedAt=new Date().toISOString();
102
+ } else if (input.action!=='status') fail('Unknown task queue action');
103
+ const notice = await advanceIdeQueue(state,home);
104
+ return {schemaVersion:'entrorove.ide-queue/1.0',sessionId:state.sessionId,current:value.current,parents:value.parents.map(parent=>({item:parent.item,...parent.context})),
105
+ entries:value.entries,completedCount:value.completed.length,...(notice ? {notice} : {}),
106
+ instruction:'Use the current requirement verbatim. Keep the original flow and task IDs, follow the controlled entry, and resume parents only after a verified end acknowledgement.'};
107
+ },home);
108
+ }