@entrorove/skill 0.1.41 → 0.1.43
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/DELIVERY-BOUNDARIES.md +30 -0
- package/IDE-EXECUTION.md +10 -2
- package/SKILL.md +10 -6
- package/cli.mjs +62 -76
- package/ide-code-mode.mjs +6 -0
- package/ide-command.mjs +13 -6
- package/ide-hook.mjs +37 -9
- package/ide-install.mjs +208 -40
- package/ide-session.mjs +37 -9
- package/install-cleanup.mjs +59 -0
- package/local-budget.mjs +2 -1
- package/local-discipline-repository.mjs +41 -7
- package/local-runner-discipline.mjs +7 -3
- package/local-stage-boundary.mjs +92 -0
- package/manifest.json +1 -1
- package/package.json +4 -1
- package/release-state.json +21 -18
- package/release.mjs +10 -2
- package/releases.json +12 -0
- package/remote-client.mjs +78 -1
- package/task-entry-client.mjs +4 -0
- package/workflow-local-contracts.mjs +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.43 — 2026-10-16
|
|
4
|
+
|
|
5
|
+
- 更新了Codex 后续轮次工具门禁、复用与变更一致性门禁、IDE 执行与复用纪律文档、技能运行和受管安装、旧版受管安装清理、IDE 工作流程说明、任务交付边界说明、安装包配置、自动版本与更新日志、本地三轴读取预算、本地阶段边界与可恢复停止、远端恢复客户端、账号沙箱偏好与任务入口确认、工作流本地调用契约。
|
|
6
|
+
- Updated Codex tool guards across turns, reuse and change consistency checks, IDE execution and reuse discipline documentation, skill runtime and managed installation, managed installation cleanup, IDE workflow instructions, task delivery boundaries, package configuration, automatic versioning and changelog, local three-axis read budgets, local stage boundaries and recoverable stops, remote recovery client, account sandbox preferences and task entry confirmation, workflow local invocation contracts.
|
|
7
|
+
|
|
8
|
+
## 0.1.42 — 2026-10-16
|
|
9
|
+
|
|
10
|
+
- 修复受管钩子空文件恢复、旧安装清理及仓库控制快照误扫。
|
|
11
|
+
- Restored empty managed hook configs, cleaned old installations, and excluded control snapshots from semantic scanning.
|
|
12
|
+
|
|
3
13
|
## 0.1.41 — 2026-10-15
|
|
4
14
|
|
|
5
15
|
- 修复 0.1.39 受管技能升级与 Codex hook 配置权限误拒;补齐本地 probe/reassess 的 importContexts 输入合同。
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Delivery boundaries and cost visibility
|
|
2
|
+
|
|
3
|
+
Contract for where a governed task may stop and which numbers the skill may display. Each section is marked **live** (implemented in this package) or **planned** (documented, not yet wired).
|
|
4
|
+
|
|
5
|
+
## Stop only at a boundary (live)
|
|
6
|
+
|
|
7
|
+
A stopped task may only stop at a clean boundary: the work so far is runnable, demonstrable and roll-backable. Never stop in the middle of a stage. In this package the ordered verified stages of the accepted plan are the boundaries; `local-stage-boundary.mjs` decides and reports with them.
|
|
8
|
+
|
|
9
|
+
## `delivered-stage-N` is its own terminal (live)
|
|
10
|
+
|
|
11
|
+
Stopping at a boundary reports the independent terminal status `delivered-stage-N` (N = delivered stages). It is never `failed` and never `blocked`, must not be shown as an error, and the guard keeps the `delivered` phase with Stop allowed. A new `start` for the same goal stays refused; continue the original bound flow instead. Resuming continues at the next boundary — delivered stages are never re-executed.
|
|
12
|
+
|
|
13
|
+
## Stop-loss is measured, never predicted (live)
|
|
14
|
+
|
|
15
|
+
The close decision uses measured consumption only. Without an enforced budget there is no baseline, so the decision stays explicitly `not-estimable` and no remaining amount or duration may be displayed. With an enforced budget, `budgetView.consumedRatio` reports the measured share; the caller applies its configured ratio and finishes the current stage before closing at that boundary. Threshold values are not shipped: they must be calibrated from a measured baseline first.
|
|
16
|
+
|
|
17
|
+
## What may be displayed (live)
|
|
18
|
+
|
|
19
|
+
- `npx @entrorove/skill usage [flowId]` reads measured consumption grouped per (host, model, meter kind, operation) and, with an original flow, its delivered stages. Its `hostQuota` block is `measured` only when a declared host adapter reports it, otherwise `unknown` (`no-host-adapter` / `adapter-unavailable`).
|
|
20
|
+
- `npx @entrorove/skill billing` exports measured cost per (account, host, model, task) together with that task's delivery stages; verified-only stages count as effective delivery and spend that is bound to no flow is listed separately instead of being dropped.
|
|
21
|
+
- Recurring tasks are scheduled by the service: `POST /api/recurring` registers a goal and interval, a periodic sweep creates exactly one run per due occurrence (a repeated sweep cannot duplicate it), and `npx @entrorove/skill recurring claim | finish <runId> | runs <recurringId>` picks a run up and reports its outcome. `runs` reports per-run measured cost (`durationMs`, `processedCount`); the trend stays `not-estimable` (`insufficient-samples`, then `no-calibrated-threshold`) while the measured deltas are still shown.
|
|
22
|
+
- Only measured values are shown; an empty sample answers `样本不足,暂不预估` instead of a default or a range.
|
|
23
|
+
- Quota stays `unknown / no-baseline` until a baseline exists. `invocation_limit` is a 60-second rate-limit threshold, not a quota.
|
|
24
|
+
- Every displayed number carries its basis (measured / extrapolated / tier); predicted durations and purchase prompts are forbidden.
|
|
25
|
+
|
|
26
|
+
## Planned (not yet live)
|
|
27
|
+
|
|
28
|
+
- The earlier-of-two-ledgers stop-loss (P2): the host adapter interface and its explicit `unknown` degradation are live, but a real host adapter must still be written per host.
|
|
29
|
+
- Per-member attribution inside a team account (P3).
|
|
30
|
+
- The calibration thresholds themselves (ratio, trend rise, tier boundaries, minimum sample size) — none may be filled in before they are measured.
|
package/IDE-EXECUTION.md
CHANGED
|
@@ -2,7 +2,9 @@ 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
|
|
5
|
+
If the IDE guard itself is the blocked 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 the resulting management plane to inspect or terminate the original flow; `flow-abandon` releases its task binding so the original task can start again.
|
|
6
|
+
|
|
7
|
+
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.
|
|
6
8
|
|
|
7
9
|
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.
|
|
8
10
|
|
|
@@ -28,6 +30,12 @@ Set `ENTROROVE_IDE` to the actual host name (for example `Codex`, `Cursor` or `C
|
|
|
28
30
|
|
|
29
31
|
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
32
|
|
|
33
|
+
### Delivery boundaries and stop-loss
|
|
34
|
+
|
|
35
|
+
A stopped task may only stop at a clean boundary: one where the work so far is runnable, demonstrable and roll-backable. Never stop in the middle of a stage. Such a stop is its own terminal result `delivered-stage-N` (N = delivered stages); it is never `failed` or `blocked`, must not be shown as an error, and the IDE keeps phase `delivered` with Stop allowed. A new `start` for the same goal stays refused: continue the original bound flow instead.
|
|
36
|
+
|
|
37
|
+
The close decision uses measured consumption only. Without an enforced budget there is no baseline, so the result is explicitly not-estimable and no remaining amount or duration may be displayed. When measured consumption reaches the configured share of the enforced budget, finish the current stage and close at that boundary. Resuming continues at the next boundary; delivered stages are never re-executed.
|
|
38
|
+
|
|
31
39
|
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.
|
|
32
40
|
|
|
33
41
|
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.
|
|
@@ -64,7 +72,7 @@ Computed runtime imports remain explicitly unresolved with their actual source p
|
|
|
64
72
|
|
|
65
73
|
For a decision, include `input.task.interaction` with `type` (`confirm`, `choice`, `multi` or `input`), a `question`, `options` (two or more with unique `id` and `label`, or an empty array for input), and a `callback` containing its `operation` and `payload`. Medium- and high-risk decisions also need a `riskDescription`. The decision waits for an explicit answer; it never chooses a default on timeout.
|
|
66
74
|
|
|
67
|
-
Use `npx @entrorove/skill status <requestId>` after a connection loss. It performs a read-only GET lookup and never resends a POST. Use `npx @entrorove/skill flows` to list recoverable flow steps. A v2 flow has its own `flowId` and progresses through `pending`, `running`, `reported`, `verified`, `blocked` or `failed`; a reported result is not a verified result.
|
|
75
|
+
Use `npx @entrorove/skill status <requestId>` after a connection loss. It performs a read-only GET lookup and never resends a POST. Use `npx @entrorove/skill flows` to list recoverable flow steps. `npx @entrorove/skill usage [flowId]` reads measured consumption grouped per host and model and, with an original flow, its delivered stages; it never displays an invented quota or a predicted duration. `npx @entrorove/skill billing` exports measured cost per account, host, model and task with that task's delivery stages. A declared host adapter may report the host's own remaining quota through the same module that describes its guard; when it cannot, the answer stays explicitly `unknown` and only this skill's measured ledger is used. `GET /api/skill/flows/:id/status` reports `delivery` with delivered, total and remaining stages, counted from independently verified steps only. A v2 flow has its own `flowId` and progresses through `pending`, `running`, `reported`, `verified`, `blocked` or `failed`; a reported result is not a verified result.
|
|
68
76
|
|
|
69
77
|
The local execution layer is available through `npx @entrorove/skill local <operation> [repositoryRoot]` with a JSON object on standard input. `local capabilities` lists the complete contract, operation groups and `requiresPass` metadata. `local capabilities <group>` filters that same contract. Operations include `snapshot-plan`, `snapshot-verify`, `mutate-gate`, `gate-issue`, `gate-verify`, `guarded-write`, `budget-init`, `budget-context`, `budget-read`, `budget-status`, `budget-extend`, budget renewal confirmation, `peer-coordinate`, `spawn-bind`, `peer-complete`, `lease-verify`, `probe`, `reassess`, execution ledger and virtual branch records, `chain-init`, `chain-resume`, `chain-status`, `chain-answer`, `evidence-sign`, `evidence-verify`, `merge-verified`, `rollback`, `formula-evaluate`, `formula-graph`, `formula-final-gate`, `run-approved-plan` and `final-gate`. State is kept only under `~/.entrorove/`; snapshots never create Git branches or worktrees. Guarded writes require a short-lived Ed25519 pass bound to the flow, step, snapshot digest and path set. Local evidence uses `entrorove.test-evidence/2.0`; unsigned local evidence remains incomplete and verified delivery requires a trusted receipt. Approved plan execution requires a frozen plan, matching approval and unchanged files; without a configured runner signer it reports local evidence as incomplete.
|
|
70
78
|
|
package/SKILL.md
CHANGED
|
@@ -5,25 +5,29 @@ description: Execute an engineering task through Entrorove, preserving its origi
|
|
|
5
5
|
|
|
6
6
|
# entrorove · 熵巡
|
|
7
7
|
|
|
8
|
-
Version: 0.1.
|
|
8
|
+
Version: 0.1.43.
|
|
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
|
|
14
|
+
1. Install `npx --yes --prefer-online @entrorove/skill@latest install` only when the user chooses to enable this skill. The managed installer writes the skill and IDE lifecycle hook configuration; confirm that the IDE trusts it and observe a real callback before describing enforcement as active. Do not ask the user to type a nonexistent `/hooks` command. If the host reports a blocked management call, show and allow the offline `entrorove unlock` command; it only opens the skill management plane and never grants project-write authority. New execution from a detected IDE requires the exact live host approval. An unsupported IDE adapter, missing hook callback, missing credential, or failed discovery stops execution explicitly. Do not silently use native tools instead. The guard is host-agnostic: one managed hook is installed once, and each host is described by its own adapter (config location, event names, entry shape). Codex and Claude Code are the verified adapters today; a host with no verified adapter, or a run whose host session identity is missing, is refused explicitly rather than degrading to native tools.
|
|
15
|
+
`entrorove unlock` is the break-glass path: run the installed CLI directly in a terminal. It writes a short-lived management-only lease locally, without a hook callback, network request, credential read, agreement check or protocol lookup. It never authorizes project writes; use it only to recover the management plane and then continue or terminate the original flow.
|
|
16
|
+
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.
|
|
15
17
|
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
18
|
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.
|
|
19
|
+
When the model produces a task contract, preserve its `workType`, `risk`, `difficulty`, `crossModule`, `parallel`, `unclear`, `architectureContract`, optional `baselineRef`, and optional `interaction` as returned. Valid work types are `implementation`, `documentation`, `calculation`, `merge`, `review`, `decision`, and `diagnosis`. A `merge` requires a concrete `baselineRef` identifying the branch, tag, commit, or review baseline; keep that baseline unchanged through independent verification. A `decision` requires `interaction`, including a risk description for medium or high risk. Do not fill missing required fields by guessing.
|
|
17
20
|
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 erase the old failure.
|
|
21
|
+
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. If the terminal flow truly owns a stale task slot, use the explicit `flow-abandon` lifecycle operation described below before routing the original task again.
|
|
19
22
|
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
23
|
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
|
-
8.
|
|
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
|
+
8. Read [DELIVERY-BOUNDARIES.md](DELIVERY-BOUNDARIES.md) before stopping a task or showing any consumption number. A task may only stop at a clean boundary, such a stop is `delivered-stage-N` and never `failed` or `blocked`, and a number without a measured baseline is never displayed.
|
|
25
|
+
9. 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.
|
|
24
26
|
|
|
25
27
|
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.
|
|
26
28
|
|
|
29
|
+
If the original flow is terminal and must release its task slot, invoke the discovered `flow-cancel` or `flow-abandon` operation with that same `flowId`. `flow-cancel` records an owner cancellation and keeps the task bound for read-only recovery. `flow-abandon` records the termination, clears acceptance and checkpoint state, unbinds the task and returns `nextStep.operation: "start"` so the original task can be routed again. These operations are available through the server and `local flow-cancel` / `local flow-abandon`; they never reopen the old flow.
|
|
30
|
+
|
|
27
31
|
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
32
|
|
|
29
33
|
## Operations and visible progress
|
package/cli.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
-
import { execFileSync } from 'node:child_process';
|
|
4
3
|
import { promises as fs, realpathSync } from 'node:fs';
|
|
5
4
|
import os from 'node:os';
|
|
6
5
|
import path from 'node:path';
|
|
@@ -12,17 +11,18 @@ import { runNative } from './native-client.mjs';
|
|
|
12
11
|
import { configureNativeNotifications, notificationSettings, sendNativeNotification } from './native-notification.mjs';
|
|
13
12
|
import { runTaskMessageCli } from './task-message-cli.mjs';
|
|
14
13
|
import { runScopeConnect } from './scope-runner-client.mjs';
|
|
15
|
-
import { apiUrl, buildInvocation, invokeRemote, queryFlows, queryStatus } from './remote-client.mjs';
|
|
14
|
+
import { apiUrl, buildInvocation, claimRecurring, finishRecurring, invokeRemote, listRecurring, queryBillingExport, queryFlows, queryStatus, queryUsageSummary, readRecurringRuns } from './remote-client.mjs';
|
|
16
15
|
const packageDir = path.dirname(fileURLToPath(import.meta.url));
|
|
17
16
|
import { buildReview, submitSkillFeedback, feedbackNoticeVersion } from './review-client.mjs';
|
|
18
17
|
import { queryCloudTask, runCloudTask as dispatchCloudTask } from './cloud-task-client.mjs';
|
|
19
18
|
import { watchCloudTask } from './cloud-watch-client.mjs';
|
|
20
19
|
import { queryAutomaticFeedback } from './automatic-feedback-client.mjs';
|
|
21
|
-
import { installIdeHooks } from './ide-install.mjs';
|
|
20
|
+
import { activeIdeAdapters, installIdeHooks, uninstallIdeHooks } from './ide-install.mjs';
|
|
21
|
+
import { cleanManagedDuplicates } from './install-cleanup.mjs';
|
|
22
22
|
import { assertIdeExecution } from './ide-session.mjs';
|
|
23
23
|
import { writeBreakGlass } from './break-glass.mjs';
|
|
24
24
|
const markerName = '.entrorove-managed.json';
|
|
25
|
-
const installFiles = ['SKILL.md', 'CAPABILITY-MAP.md', 'AGREEMENT.md', 'manifest.json', 'IDE-EXECUTION.md', 'REUSE-DISCIPLINE.md'];
|
|
25
|
+
const installFiles = ['SKILL.md', 'CAPABILITY-MAP.md', 'AGREEMENT.md', 'manifest.json', 'IDE-EXECUTION.md', 'REUSE-DISCIPLINE.md', 'DELIVERY-BOUNDARIES.md'];
|
|
26
26
|
const markerSchema = 'entrorove.managed-install/1.0';
|
|
27
27
|
const agreementSchema = 'entrorove.agreement-acceptance/1.0';
|
|
28
28
|
const installLockWaitMs = 10_000;
|
|
@@ -87,8 +87,8 @@ export function agreementInteraction() {
|
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
function agreementRequired(
|
|
91
|
-
const agreement = path.join(
|
|
90
|
+
function agreementRequired() {
|
|
91
|
+
const agreement = path.join(packageDir, 'AGREEMENT.md');
|
|
92
92
|
return {
|
|
93
93
|
ok: false,
|
|
94
94
|
error: 'agreement-required',
|
|
@@ -246,57 +246,35 @@ async function updateGlobalInstall(target, bundle) {
|
|
|
246
246
|
return true;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
function
|
|
250
|
-
try {
|
|
251
|
-
return execFileSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
252
|
-
} catch {
|
|
253
|
-
return null;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function gitTracked(root, relativePath) {
|
|
258
|
-
const files = execFileSync('git', ['-C', root, 'ls-files', '--cached', '--', relativePath], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
259
|
-
return files.trim().length > 0;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async function removeManagedDuplicate(directory, root) {
|
|
263
|
-
if (gitTracked(root, path.relative(root, directory))) return false;
|
|
264
|
-
if (!await managedState(directory)) return false;
|
|
265
|
-
const quarantine = `${directory}.entrorove-cleanup-${randomUUID()}`;
|
|
266
|
-
await fs.rename(directory, quarantine);
|
|
267
|
-
try {
|
|
268
|
-
if (!await managedState(quarantine)) throw new Error('Managed duplicate changed during cleanup');
|
|
269
|
-
await fs.rm(quarantine, { recursive: true });
|
|
270
|
-
} catch (error) {
|
|
271
|
-
await fs.rename(quarantine, directory);
|
|
272
|
-
throw error;
|
|
273
|
-
}
|
|
274
|
-
return true;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
async function cleanProjectDuplicates(target, cwd) {
|
|
278
|
-
const root = projectRoot(cwd);
|
|
279
|
-
if (!root) return [];
|
|
280
|
-
const removed = [];
|
|
281
|
-
for (const relative of ['.codex/skills/entrorove', '.agents/skills/entrorove', '.claude/skills/entrorove']) {
|
|
282
|
-
const directory = path.join(root, relative);
|
|
283
|
-
if (directory === target) continue;
|
|
284
|
-
if (await removeManagedDuplicate(directory, root)) removed.push(relative);
|
|
285
|
-
}
|
|
286
|
-
return removed;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
export async function install({ home = os.homedir(), cwd = process.cwd() } = {}) {
|
|
290
|
-
const bundle = await sourceBundle();
|
|
249
|
+
export async function install({ home = os.homedir(), cwd = process.cwd(), explicit = false } = {}) {
|
|
291
250
|
const target = globalInstallPath(home);
|
|
292
|
-
|
|
251
|
+
if (!explicit && !(await managedState(target))) return { installed: false, updated: false };
|
|
293
252
|
const parent = path.dirname(target);
|
|
294
253
|
await fs.mkdir(parent, { recursive: true, mode: 0o700 });
|
|
295
254
|
return withInstallLock(parent, async () => {
|
|
255
|
+
const active = explicit ? null : await activeIdeAdapters({ home });
|
|
256
|
+
if (!explicit && (!(await managedState(target)) || active.length === 0)) return { installed: false, updated: false };
|
|
257
|
+
const bundle = await sourceBundle();
|
|
258
|
+
const stale = await agreementStale(home);
|
|
296
259
|
const updated = await updateGlobalInstall(target, bundle);
|
|
297
|
-
const removed = await
|
|
298
|
-
|
|
299
|
-
|
|
260
|
+
const { removed, preserved } = await cleanManagedDuplicates({
|
|
261
|
+
target, cwd, home, isManaged: managedState, includeProject: explicit,
|
|
262
|
+
});
|
|
263
|
+
const ide = await installIdeHooks({ home, version: bundle.manifest.version, adapters: active });
|
|
264
|
+
return { version: bundle.manifest.version, updated, removed, preserved, agreementStale: stale, ide };
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function uninstall({ home = os.homedir() } = {}) {
|
|
269
|
+
const target = globalInstallPath(home), parent = path.dirname(target);
|
|
270
|
+
await fs.mkdir(parent, { recursive: true, mode: 0o700 });
|
|
271
|
+
return withInstallLock(parent, async () => {
|
|
272
|
+
const ide = await uninstallIdeHooks({ home });
|
|
273
|
+
const managed = await managedState(target);
|
|
274
|
+
if (managed) await fs.rm(target, { recursive: true });
|
|
275
|
+
return { uninstalled: managed !== false && ide.preserved.length === 0,
|
|
276
|
+
removed: [...ide.removed, ...(managed ? [target] : [])],
|
|
277
|
+
preserved: [...ide.preserved, ...(managed === false ? [{ path: target, reason: 'unmanaged-or-edited' }] : [])] };
|
|
300
278
|
});
|
|
301
279
|
}
|
|
302
280
|
|
|
@@ -407,12 +385,8 @@ export async function runCloudWatch(input, dependencies = remoteDependencies())
|
|
|
407
385
|
}
|
|
408
386
|
|
|
409
387
|
async function invoke(data) {
|
|
410
|
-
if (data?.operation === 'cloud-task')
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
if (data?.operation === 'cloud-status') {
|
|
414
|
-
return runCloudStatus(data.input, remoteDependencies());
|
|
415
|
-
}
|
|
388
|
+
if (data?.operation === 'cloud-task') return runCloudTask(data.input, remoteDependencies());
|
|
389
|
+
if (data?.operation === 'cloud-status') return runCloudStatus(data.input, remoteDependencies());
|
|
416
390
|
if (data?.operation === 'cloud-watch') return runCloudWatch(data.input);
|
|
417
391
|
return invokeRemote(data, remoteDependencies());
|
|
418
392
|
}
|
|
@@ -425,12 +399,36 @@ async function submitReview(data) {
|
|
|
425
399
|
process.stdout.write(JSON.stringify(result) + '\n');
|
|
426
400
|
}
|
|
427
401
|
|
|
402
|
+
async function runAccountCommand(command, args) {
|
|
403
|
+
await install();
|
|
404
|
+
const home = os.homedir();
|
|
405
|
+
if (!await agreementAccepted(home)) {
|
|
406
|
+
process.stdout.write(JSON.stringify(agreementRequired(home)) + '\n');
|
|
407
|
+
process.exitCode = 2;
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const dependencies = remoteDependencies();
|
|
411
|
+
if (command === 'status') return queryStatus(args[0], dependencies);
|
|
412
|
+
if (command === 'flows') return queryFlows(dependencies);
|
|
413
|
+
if (command === 'usage') return queryUsageSummary(args[0], dependencies);
|
|
414
|
+
if (command === 'billing') return queryBillingExport(dependencies);
|
|
415
|
+
if (args[0] === 'list' && args.length === 1) return listRecurring(dependencies);
|
|
416
|
+
if (args[0] === 'claim' && args.length === 1) return claimRecurring(`cli-${process.pid}-${Date.now().toString(36)}`, dependencies);
|
|
417
|
+
if (args[0] === 'finish' && args.length === 2) return finishRecurring(args[1], await stdinJson(), dependencies);
|
|
418
|
+
if (args[0] === 'runs' && args.length === 2) return readRecurringRuns(args[1], dependencies);
|
|
419
|
+
throw new Error('Usage: entrorove recurring list | claim | finish <runId> | runs <recurringId>');
|
|
420
|
+
}
|
|
421
|
+
|
|
428
422
|
async function main() {
|
|
429
423
|
const [command, ...args] = process.argv.slice(2);
|
|
430
424
|
if (command === 'unlock' && args.length === 0) {
|
|
431
425
|
process.stdout.write(`${JSON.stringify(await writeBreakGlass())}\n`);
|
|
426
|
+
} else if (command === 'uninstall' && args.length === 0) {
|
|
427
|
+
const result = await uninstall();
|
|
428
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
429
|
+
if (!result.uninstalled) process.exitCode = 2;
|
|
432
430
|
} else if (command === 'install' && args.length === 0) {
|
|
433
|
-
process.stdout.write(JSON.stringify(await install()) + '\n');
|
|
431
|
+
process.stdout.write(JSON.stringify(await install({ explicit: true })) + '\n');
|
|
434
432
|
} else if (command === 'configure' && args.length === 0) {
|
|
435
433
|
const data = await stdinJson();
|
|
436
434
|
if (Object.keys(data).length !== 1 || typeof data.token !== 'string') throw new Error('Expected {"token":"..."} on stdin');
|
|
@@ -468,26 +466,14 @@ async function main() {
|
|
|
468
466
|
process.stdout.write(`${JSON.stringify(await queryAutomaticFeedback(args[0],remoteDependencies(),args[1]))}\n`);
|
|
469
467
|
} else if (command === 'review' && args.length === 0) {
|
|
470
468
|
await submitReview(await stdinJson());
|
|
471
|
-
} else if (command === 'status' && args.length === 1
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
process.exitCode = 2;
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
await queryStatus(args[0], remoteDependencies());
|
|
479
|
-
} else if (command === 'flows' && args.length === 0) {
|
|
480
|
-
await install();
|
|
481
|
-
if (!await agreementAccepted(os.homedir())) {
|
|
482
|
-
process.stdout.write(JSON.stringify(agreementRequired(os.homedir())) + '\n');
|
|
483
|
-
process.exitCode = 2;
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
await queryFlows(remoteDependencies());
|
|
469
|
+
} else if (command === 'status' && args.length === 1 || command === 'flows' && args.length === 0
|
|
470
|
+
|| command === 'usage' && args.length <= 1 || command === 'billing' && args.length === 0
|
|
471
|
+
|| command === 'recurring') {
|
|
472
|
+
await runAccountCommand(command, args);
|
|
487
473
|
} else if (command === 'local') {
|
|
488
474
|
await runLocal(args);
|
|
489
475
|
} else {
|
|
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)');
|
|
476
|
+
throw new Error('Usage: entrorove unlock | uninstall | 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 | usage [flowId] | billing | recurring list|claim|finish|runs | review | local (JSON on stdin)');
|
|
491
477
|
}
|
|
492
478
|
}
|
|
493
479
|
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) { const { captureIdeResults } = await import('./ide-results.mjs');
|
package/ide-code-mode.mjs
CHANGED
|
@@ -29,6 +29,12 @@ export function inspectIdeCodeMode(code, runtime) {
|
|
|
29
29
|
const args = literal(node.arguments[0]), command = literalCommand(args.cmd ?? args.command);
|
|
30
30
|
if (!(readCommand(command) || sdkCommand(command, runtime))) allowed = false;
|
|
31
31
|
} catch { allowed = false; }
|
|
32
|
+
} else if (name === 'tools.update_goal' || name === 'tools.get_goal') {
|
|
33
|
+
try {
|
|
34
|
+
const args = node.arguments.length ? literal(node.arguments[0]) : {};
|
|
35
|
+
if (name === 'tools.update_goal' && !['complete', 'blocked', 'paused'].includes(args?.status)) allowed = false;
|
|
36
|
+
if (name === 'tools.get_goal' && (args === null || typeof args !== 'object' || Array.isArray(args))) allowed = false;
|
|
37
|
+
} catch { allowed = false; }
|
|
32
38
|
} else if (!['text','image','notify','Promise.all','Promise.allSettled'].includes(name)
|
|
33
39
|
&& !/^tools\.(?:view_image|web__run|list_mcp_resources|list_mcp_resource_templates|read_mcp_resource|clock__curr_time)$/u.test(name)) allowed = false;
|
|
34
40
|
}
|
package/ide-command.mjs
CHANGED
|
@@ -28,7 +28,10 @@ export function sdkCommand(parsed, runtime) {
|
|
|
28
28
|
if (!parsed) return null;
|
|
29
29
|
const words = [...parsed.words];
|
|
30
30
|
if (words[0]?.startsWith('ENTROROVE_IDE=')) {
|
|
31
|
-
if (words.shift()
|
|
31
|
+
if (!/^ENTROROVE_IDE=[\p{L}\p{N} .+_-]{2,48}$/u.test(words.shift())) return null;
|
|
32
|
+
while (['ENTROROVE_SESSION=', 'CODEX_THREAD_ID='].some(prefix => words[0]?.startsWith(prefix))) {
|
|
33
|
+
if (!/^[A-Z_]+=[A-Za-z0-9_-]{1,200}$/u.test(words.shift())) return null;
|
|
34
|
+
}
|
|
32
35
|
}
|
|
33
36
|
let args;
|
|
34
37
|
if ([runtime.node, 'node'].includes(words[0]) && words[1] === runtime.cli) args = words.slice(2);
|
|
@@ -40,11 +43,15 @@ export function sdkCommand(parsed, runtime) {
|
|
|
40
43
|
} else if (path.basename(words[0]) === 'entrorove') {
|
|
41
44
|
args = words.slice(1);
|
|
42
45
|
} else return null;
|
|
43
|
-
const simple = new Set(['invoke', 'cloud-task', 'cloud-status', 'cloud-watch', 'agreement', 'configure', 'queue']);
|
|
44
|
-
if (args[0]
|
|
46
|
+
const simple = new Set(['invoke', 'cloud-task', 'cloud-status', 'cloud-watch', 'agreement', 'configure', 'queue', 'unlock']);
|
|
47
|
+
if (['unlock', 'uninstall'].includes(args[0]) && args.length === 1 && !parsed.input) return { args };
|
|
45
48
|
if (simple.has(args[0]) && args.length === 1 && parsed.input) return { args, input: parsed.input };
|
|
46
49
|
if (['status', 'feedback'].includes(args[0]) && /^[a-f0-9]{64}$/u.test(args[1] ?? '') && [2,3].includes(args.length)) return { args };
|
|
47
|
-
if (['flows', 'install'].includes(args[0]) && args.length === 1 && !parsed.input) return { args };
|
|
50
|
+
if (['flows', 'install', 'billing'].includes(args[0]) && args.length === 1 && !parsed.input) return { args };
|
|
51
|
+
if (args[0] === 'recurring' && ['list', 'claim'].includes(args[1] ?? '') && args.length === 2 && !parsed.input) return { args };
|
|
52
|
+
if (args[0] === 'recurring' && ['finish', 'runs'].includes(args[1] ?? '') && args.length === 3 && (!parsed.input || args[1] === 'finish')) return { args, ...(parsed.input ? { input: parsed.input } : {}) };
|
|
53
|
+
if (args[0] === 'usage' && args.length <= 2 && !parsed.input
|
|
54
|
+
&& (args[1] === undefined || /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(args[1]))) return { args };
|
|
48
55
|
if (args[0] === 'local' && /^[a-z][a-z0-9-]+$/u.test(args[1] ?? '') && [2,3].includes(args.length) && parsed.input) return { args, input: parsed.input };
|
|
49
56
|
if (args[0] === 'scope-connect' && args.length === 2 && parsed.input) return { args, input: parsed.input };
|
|
50
57
|
if (args[0] === 'native' && args.length === 3 && parsed.input) return { args, input: parsed.input };
|
|
@@ -72,8 +79,8 @@ export function readCommand(parsed) {
|
|
|
72
79
|
return false;
|
|
73
80
|
}
|
|
74
81
|
|
|
75
|
-
export function renderSdkCommand(call, runtime, sessionId) {
|
|
82
|
+
export function renderSdkCommand(call, runtime, sessionId, host = process.env.ENTROROVE_IDE ?? 'Codex') {
|
|
76
83
|
const quote = value => `'${String(value).replace(/'/gu, "'\\''")}'`;
|
|
77
|
-
const header = `ENTROROVE_IDE
|
|
84
|
+
const header = `ENTROROVE_IDE=${host} ENTROROVE_SESSION=${quote(sessionId)} CODEX_THREAD_ID=${quote(sessionId)} ${[runtime.node, runtime.cli, ...call.args].map(quote).join(' ')}`;
|
|
78
85
|
return call.input ? `${header} <<'ENTROROVE_INPUT'\n${JSON.stringify(call.input)}\nENTROROVE_INPUT` : header;
|
|
79
86
|
}
|
package/ide-hook.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { realpathSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { ideSession, ideRuntime, bindIdeAccount, ideContext, ideDigest, readBreakGlass } from './ide-session.mjs';
|
|
5
|
+
import { isStageTerminal, stageOf } from './local-stage-boundary.mjs';
|
|
5
6
|
import { literalCommand, sdkCommand, readCommand, renderSdkCommand, installerCommand } from './ide-command.mjs';
|
|
6
7
|
import { inspectIdeCodeMode } from './ide-code-mode.mjs';
|
|
7
8
|
import { pathToFileURL } from 'node:url';
|
|
@@ -13,9 +14,18 @@ import { registerIdeRequirement, ideQueueContext, unclassifiedIdeRequirements,
|
|
|
13
14
|
const output = (event, extra = {}) => ({ hookSpecificOutput: { hookEventName: event, ...extra } });
|
|
14
15
|
const deny = reason => output('PreToolUse', { permissionDecision: 'deny', permissionDecisionReason: reason });
|
|
15
16
|
const publicId = value => typeof value === 'string' && /^[A-Za-z0-9_-]{1,200}$/u.test(value);
|
|
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 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']);
|
|
17
18
|
const unlockCommand = 'entrorove unlock';
|
|
18
|
-
const
|
|
19
|
+
const exitPrompt = /^(?:(?:请)?(?:停止使用|退出)\s*\$?entrorove|(?:stop using|exit)\s+\$?entrorove)[.!。!]?$/iu;
|
|
20
|
+
const selectPrompt = /^(?:(?:请)?(?:使用|启用)\s+\$?entrorove|\$entrorove)(?:\s|[,。,::!!]|$)/iu;
|
|
21
|
+
function userPrompt(event) {
|
|
22
|
+
let prompt = (event.prompt ?? '').trim();
|
|
23
|
+
for (const wrapper of ['**', '__', '`']) {
|
|
24
|
+
if (prompt.startsWith(wrapper) && prompt.endsWith(wrapper)) prompt = prompt.slice(wrapper.length, -wrapper.length).trim();
|
|
25
|
+
}
|
|
26
|
+
return prompt;
|
|
27
|
+
}
|
|
28
|
+
const managementOperations = new Set(['capabilities', 'help', 'start', 'continue', 'task-describe', 'flow-cancel', 'flow-abandon', 'scope-connect', 'status', 'feedback', 'cloud-status', 'cloud-watch', 'queue', 'unlock', 'usage', 'billing', 'recurring']);
|
|
19
29
|
function operationOf(call) {
|
|
20
30
|
if (!call) return null;
|
|
21
31
|
if (call.args[0] === 'invoke') return call.input?.operation ?? null;
|
|
@@ -66,6 +76,9 @@ function collectResult(state, values, local) {
|
|
|
66
76
|
}
|
|
67
77
|
const nextStep = value.nextStep ?? value.result?.nextStep ?? value.result?.result?.nextStep;
|
|
68
78
|
if (!lifecycle && (value.status === 'needs-input' || value.result?.status === 'needs-input' || value.interaction || value.result?.interaction)) state.phase = 'needs-input';
|
|
79
|
+
else if (isStageTerminal(value.status) || isStageTerminal(value.result?.status)) {
|
|
80
|
+
state.phase = 'delivered'; state.deliveredStage = stageOf(value.status) ?? stageOf(value.result?.status);
|
|
81
|
+
}
|
|
69
82
|
else if (!lifecycle && (['blocked', 'failed', 'incomplete'].includes(value.status) || ['blocked', 'failed', 'incomplete'].includes(value.result?.status)
|
|
70
83
|
|| ['blocked', 'failed'].includes(value.taskStatus))) state.phase = 'blocked';
|
|
71
84
|
else if (!lifecycle && (nextStep || value.nextAction)) state.phase = 'continuing';
|
|
@@ -87,6 +100,7 @@ function collectResult(state, values, local) {
|
|
|
87
100
|
return messages;
|
|
88
101
|
}
|
|
89
102
|
async function beforeTool(event, state, home) {
|
|
103
|
+
if (state.phase === 'user-exited') return {};
|
|
90
104
|
const unlocked = await readBreakGlass(home);
|
|
91
105
|
const command = event.tool_input?.command ?? event.tool_input?.cmd, parsed = literalCommand(command);
|
|
92
106
|
const isShell = ['Bash', 'exec_command', 'shell_command'].includes(event.tool_name);
|
|
@@ -105,6 +119,12 @@ async function beforeTool(event, state, home) {
|
|
|
105
119
|
const runtime = isShell && parsed ? await ideRuntime(home) : null;
|
|
106
120
|
const call = runtime && sdkCommand(parsed, runtime);
|
|
107
121
|
if (call) {
|
|
122
|
+
if (call.args[0] === 'uninstall') {
|
|
123
|
+
state.active = false; state.phase = 'user-exited';
|
|
124
|
+
return output('PreToolUse', { permissionDecision: 'allow', updatedInput: { ...event.tool_input,
|
|
125
|
+
command: renderSdkCommand(call, runtime, state.sessionId),
|
|
126
|
+
...(Object.hasOwn(event.tool_input ?? {}, 'cmd') ? { cmd: renderSdkCommand(call, runtime, state.sessionId) } : {}) } });
|
|
127
|
+
}
|
|
108
128
|
state.active = true;
|
|
109
129
|
if (call.args[0] !== 'configure' && call.args[0] !== 'unlock') await bindIdeAccount(state, home);
|
|
110
130
|
if (state.phase === 'setup' && !unlocked && !isManagementCall(call)) return denyBlocked(state, event, 'Entrorove:自举阶段只放行 Entrorove 管理调用(建 flow、改目标、scope-connect、capabilities、status、queue 等);业务与写操作需先建立原 flow。');
|
|
@@ -117,7 +137,7 @@ async function beforeTool(event, state, home) {
|
|
|
117
137
|
const newStart = call.args[0] === 'cloud-task' || call.args[0] === 'invoke' && ['start','cloud-task'].includes(call.input?.operation);
|
|
118
138
|
if (newStart && (state.flowId || state.taskId) && state.phase !== 'ended') return denyBlocked(state, event, 'Entrorove:必须继续原任务,禁止用第二次 start 或 cloud-task 绕过原门禁。');
|
|
119
139
|
const discovery = ['invoke','local'].includes(call.args[0]) && ['capabilities','help'].includes(call.input?.operation ?? call.args[1]);
|
|
120
|
-
const observer = discovery || ['status','feedback','cloud-status','configure','agreement','install','queue','flows'].includes(call.args[0]);
|
|
140
|
+
const observer = discovery || ['status','feedback','cloud-status','configure','agreement','install','queue','flows','usage','billing','recurring'].includes(call.args[0]);
|
|
121
141
|
if (unclassifiedIdeRequirements(state) && !observer && !(unlocked && isManagementCall(call))) return denyBlocked(state, event, 'Entrorove:新需求已保存,先用 queue status 读取并按轻重缓急 route 分类;禁止丢弃新需求或覆盖原任务。');
|
|
122
142
|
if (newStart && !checkQueuedStart(state,call)) return denyBlocked(state, event, 'Entrorove:新任务目标必须与实际进入队列的用户原文一致。');
|
|
123
143
|
if (call.args[0] === 'local' && !discovery && !state.flowId) return denyBlocked(state, event, 'Entrorove:本地执行前必须先受理原任务并绑定账号、flowId 与仓库;不能直接使用其它流程的通行证。');
|
|
@@ -149,21 +169,29 @@ async function beforeTool(event, state, home) {
|
|
|
149
169
|
const current = await ideRuntime(home), code = typeof event.tool_input === 'string' ? event.tool_input : event.tool_input?.code;
|
|
150
170
|
return inspectIdeCodeMode(code, current) ? output('PreToolUse', { additionalContext: ideContext(state) }) : denyBlocked(state, event, 'Entrorove:代码模式含未批准的原生执行或动态工具输入;请使用原任务的受控入口。');
|
|
151
171
|
}
|
|
172
|
+
if (event.tool_name === 'update_goal' && (state.phase === 'setup' || unlocked)) return output('PreToolUse', { additionalContext: ideContext(state) });
|
|
152
173
|
if (isShell && readCommand(parsed) || readTools.has(event.tool_name)) return output('PreToolUse', { additionalContext: ideContext(state) });
|
|
153
174
|
return denyBlocked(state, event, `Entrorove:已阻止原生 ${event.tool_name ?? 'unknown'} 调用。当前阶段 ${state.phase}${state.taskNumber ? `,云端任务 ${state.taskNumber}` : ''}。写入、删除、移动及不确定的终端操作必须使用原任务的 guarded-write/guarded-delete/guarded-move 或已批准的执行计划。`);
|
|
154
175
|
}
|
|
155
176
|
export async function runIdeHook(event, { home } = {}) {
|
|
156
177
|
return ideSession(event, async state => {
|
|
157
178
|
const name = event.hook_event_name;
|
|
158
|
-
|
|
179
|
+
const selected = name === 'UserPromptSubmit' && selectPrompt.test(userPrompt(event));
|
|
180
|
+
const newSelection = selected && !state.active;
|
|
181
|
+
if (name === 'PreToolUse') state.enforcement = 'enforced';
|
|
182
|
+
if (name === 'UserPromptSubmit' && exitPrompt.test(userPrompt(event))) {
|
|
183
|
+
if (state.phase !== 'user-exited') state.exitPhase = state.phase;
|
|
159
184
|
state.active = false; state.phase = 'user-exited';
|
|
160
185
|
return output(name, { additionalContext: '用户明确退出 Entrorove 管理;原任务仍保留,未验收、未评分为成功。' });
|
|
161
186
|
}
|
|
162
|
-
if (
|
|
187
|
+
if (selected) {
|
|
163
188
|
state.active = true;
|
|
164
|
-
if (
|
|
189
|
+
if (['ended', 'user-exited'].includes(state.phase)) {
|
|
190
|
+
if (state.phase === 'ended') { delete state.flowId; delete state.taskId; delete state.taskNumber; delete state.requestId; }
|
|
191
|
+
state.phase = state.exitPhase ?? 'entry'; delete state.exitPhase;
|
|
192
|
+
}
|
|
165
193
|
}
|
|
166
|
-
if (name === 'UserPromptSubmit') registerIdeRequirement(state,event);
|
|
194
|
+
if (name === 'UserPromptSubmit' && state.active && !newSelection) registerIdeRequirement(state,event);
|
|
167
195
|
if (name === 'PreToolUse') return beforeTool(event, state, home);
|
|
168
196
|
const pendingRead = state.readCalls?.[event.tool_use_id];
|
|
169
197
|
if (name === 'PostToolUse' && state.active && (state.pendingCall?.toolUseId === event.tool_use_id || pendingRead)) {
|
|
@@ -204,8 +232,8 @@ export async function runIdeHook(event, { home } = {}) {
|
|
|
204
232
|
}
|
|
205
233
|
if (name==='Stop' && state.active && !(await readBreakGlass(home))) {
|
|
206
234
|
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}原任务或队列尚未取得结束回执;继续可运行工作,真实阻断必须明确报告。`)};
|
|
235
|
+
if (resumed || unclassifiedIdeRequirements(state) || unfinishedIdeQueue(state) && !['blocked','needs-input','delivered'].includes(state.phase)
|
|
236
|
+
|| !['ended','blocked','needs-input','setup','delivered'].includes(state.phase)) return {decision:'block',reason:blockedReason(state, event, `${ideContext(state)}${ideQueueContext(state)}${resumed}原任务或队列尚未取得结束回执;继续可运行工作,真实阻断必须明确报告。`)};
|
|
209
237
|
}
|
|
210
238
|
return state.active && ['SessionStart','UserPromptSubmit','PostCompact','PreCompact'].includes(name) ? output(name, { additionalContext: `${ideContext(state)}${ideQueueContext(state)}` }) : {};
|
|
211
239
|
}, home);
|