@entrorove/skill 0.1.39 → 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 +4 -1
- package/CHANGELOG.md +5 -0
- package/IDE-EXECUTION.md +2 -0
- package/SKILL.md +5 -3
- package/break-glass.mjs +56 -0
- package/cli.mjs +7 -9
- package/ide-command.mjs +3 -0
- package/ide-hook.mjs +73 -28
- package/ide-install.mjs +1 -1
- package/ide-results.mjs +2 -1
- package/ide-session.mjs +14 -2
- package/local-cli.mjs +2 -0
- package/local-contracts.mjs +2 -0
- package/local-execution.mjs +2 -0
- package/local-flow-lifecycle.mjs +95 -0
- package/local-scope-config-imports.mjs +14 -9
- package/local-scope-files.mjs +32 -11
- package/local-scope-repository.mjs +2 -1
- package/local-task-routing.mjs +27 -0
- package/manifest.json +1 -1
- package/package.json +3 -1
- package/release-state.json +24 -22
- package/release.mjs +3 -1
- package/releases.json +6 -0
- package/remote-client.mjs +1 -1
- package/reuse-discipline-release.mjs +1 -1
package/CAPABILITY-MAP.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
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
|
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
| 网页与手机通知 | 登录后的任务通知按钮 | 同一加密 Web Push 协议,账号与设备隔离;真实任务事务队列、持久重试、失效注销与到期清理;手机可见投递仍需设备本人授权与验收 |
|
|
23
24
|
| 失败后接续 | `scope-connect` 返回 `nextInput`;`feedback <requestId> [generation]` | 范围门禁使用同一排序路径集;新执行保存新一代回执,原失败回执不可改,默认只读最新一代 |
|
|
24
25
|
|
|
25
|
-
本地公开 schema 已增加到 **
|
|
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,10 @@
|
|
|
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
|
+
|
|
3
8
|
## 0.1.39 — 2026-10-14
|
|
4
9
|
|
|
5
10
|
- 增加受控需求队列,支持按优先级排队或在安全检查点插入;插入任务完成并回传真实确认后自动恢复原任务与原编号。
|
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`.
|
package/SKILL.md
CHANGED
|
@@ -5,21 +5,23 @@ 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.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.
|
|
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
|
|
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
|
|
|
25
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.
|
package/break-glass.mjs
ADDED
|
@@ -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
|
-
|
|
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 === '
|
|
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();
|
|
@@ -489,7 +487,7 @@ async function main() {
|
|
|
489
487
|
} else if (command === 'local') {
|
|
490
488
|
await runLocal(args);
|
|
491
489
|
} else {
|
|
492
|
-
throw new Error('Usage: entrorove 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)');
|
|
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)');
|
|
493
491
|
}
|
|
494
492
|
}
|
|
495
493
|
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) { const { captureIdeResults } = await import('./ide-results.mjs');
|
package/ide-command.mjs
CHANGED
|
@@ -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
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,7 +1,7 @@
|
|
|
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';
|
|
@@ -14,13 +14,49 @@ const output = (event, extra = {}) => ({ hookSpecificOutput: { hookEventName: ev
|
|
|
14
14
|
const deny = reason => output('PreToolUse', { permissionDecision: 'deny', permissionDecisionReason: reason });
|
|
15
15
|
const publicId = value => typeof value === 'string' && /^[A-Za-z0-9_-]{1,200}$/u.test(value);
|
|
16
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
|
+
}
|
|
17
46
|
function collectResult(state, values, local) {
|
|
18
47
|
const messages = [];
|
|
19
48
|
for (const result of values) {
|
|
20
49
|
const value = result.output ?? result;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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)) {
|
|
24
60
|
if (state.flowId && state.flowId !== value.flowId) throw new Error('Returned flow replaced the original task');
|
|
25
61
|
state.flowId = value.flowId;
|
|
26
62
|
}
|
|
@@ -29,15 +65,15 @@ function collectResult(state, values, local) {
|
|
|
29
65
|
state.taskId = value.taskId; state.taskNumber = value.taskNumber;
|
|
30
66
|
}
|
|
31
67
|
const nextStep = value.nextStep ?? value.result?.nextStep ?? value.result?.result?.nextStep;
|
|
32
|
-
if (value.status === 'needs-input' || value.result?.status === 'needs-input' || value.interaction || value.result?.interaction) state.phase = 'needs-input';
|
|
33
|
-
else if (['blocked', 'failed', 'incomplete'].includes(value.status) || ['blocked', 'failed', 'incomplete'].includes(value.result?.status)
|
|
34
|
-
|| ['blocked', 'failed'].includes(value.taskStatus)) state.phase = 'blocked';
|
|
35
|
-
else if (nextStep || value.nextAction) state.phase = 'continuing';
|
|
36
|
-
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))
|
|
37
73
|
&& result.feedback?.acknowledgement?.stored === true) state.phase = 'ended';
|
|
38
|
-
else if (local && value.status === 'verified') state.phase = 'continuing';
|
|
39
|
-
else if (value.status === 'started' || value.status === 'succeeded') state.phase = 'continuing';
|
|
40
|
-
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 };
|
|
41
77
|
const notices = [...(result.ideMessages ?? []), ...(value.ideMessages ?? []),
|
|
42
78
|
...(value.schemaVersion === 'entrorove.ide-event/1.0' ? [value.message] : []), ...(value.events ?? []).map(item => item.message)];
|
|
43
79
|
for (const message of notices) {
|
|
@@ -51,6 +87,7 @@ function collectResult(state, values, local) {
|
|
|
51
87
|
return messages;
|
|
52
88
|
}
|
|
53
89
|
async function beforeTool(event, state, home) {
|
|
90
|
+
const unlocked = await readBreakGlass(home);
|
|
54
91
|
const command = event.tool_input?.command ?? event.tool_input?.cmd, parsed = literalCommand(command);
|
|
55
92
|
const isShell = ['Bash', 'exec_command', 'shell_command'].includes(event.tool_name);
|
|
56
93
|
const installer = isShell && installerCommand(parsed);
|
|
@@ -62,26 +99,33 @@ async function beforeTool(event, state, home) {
|
|
|
62
99
|
return output('PreToolUse', { permissionDecision: 'allow', updatedInput: { ...event.tool_input, command: installer,
|
|
63
100
|
...(Object.hasOwn(event.tool_input ?? {}, 'cmd') ? { cmd: installer } : {}) } });
|
|
64
101
|
}
|
|
65
|
-
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)
|
|
66
103
|
|| /(?:@entrorove\/skill|entrorove-skill)\/cli\.mjs$/u.test(word));
|
|
67
104
|
if (!state.active && !namedSdk) return {};
|
|
68
105
|
const runtime = isShell && parsed ? await ideRuntime(home) : null;
|
|
69
106
|
const call = runtime && sdkCommand(parsed, runtime);
|
|
70
107
|
if (call) {
|
|
71
108
|
state.active = true;
|
|
72
|
-
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
|
+
}
|
|
73
117
|
const newStart = call.args[0] === 'cloud-task' || call.args[0] === 'invoke' && ['start','cloud-task'].includes(call.input?.operation);
|
|
74
|
-
if (newStart && (state.flowId || state.taskId) && state.phase !== 'ended') return
|
|
118
|
+
if (newStart && (state.flowId || state.taskId) && state.phase !== 'ended') return denyBlocked(state, event, 'Entrorove:必须继续原任务,禁止用第二次 start 或 cloud-task 绕过原门禁。');
|
|
75
119
|
const discovery = ['invoke','local'].includes(call.args[0]) && ['capabilities','help'].includes(call.input?.operation ?? call.args[1]);
|
|
76
|
-
const observer = discovery || ['status','feedback','cloud-status','configure','agreement','install','queue'].includes(call.args[0]);
|
|
77
|
-
if (unclassifiedIdeRequirements(state) && !observer) return
|
|
78
|
-
if (newStart && !checkQueuedStart(state,call)) return
|
|
79
|
-
if (call.args[0] === 'local' && !discovery && !state.flowId) return
|
|
80
|
-
if (state.pendingCall && !observer) return
|
|
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:原调用尚未收到结束回执;先只读恢复其状态,禁止并发重发。');
|
|
81
125
|
const input = call.args[0] === 'invoke' ? call.input?.input : call.input;
|
|
82
|
-
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
|
|
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 任务的原编号不一致。');
|
|
83
127
|
if (call.args[0] === 'scope-connect' && call.args[1] !== state.repositoryRoot
|
|
84
|
-
|| call.args[0] === 'local' && (input?.repositoryRoot && input.repositoryRoot !== state.repositoryRoot || call.args[2] && call.args[2] !== state.repositoryRoot)) return
|
|
128
|
+
|| call.args[0] === 'local' && (input?.repositoryRoot && input.repositoryRoot !== state.repositoryRoot || call.args[2] && call.args[2] !== state.repositoryRoot)) return denyBlocked(state, event, 'Entrorove:调用不能替换本任务的实际仓库。');
|
|
85
129
|
let requestId;
|
|
86
130
|
if (newStart) state.taskGoal = call.args[0] === 'cloud-task' ? call.input?.goal : call.input?.input?.task?.goal;
|
|
87
131
|
if (call.args[0] === 'invoke' && !call.input.operation.startsWith('cloud-')) {
|
|
@@ -91,21 +135,22 @@ async function beforeTool(event, state, home) {
|
|
|
91
135
|
const pending = { toolUseId: event.tool_use_id, inputDigest: ideDigest(call), command: call.args[0], ...(requestId ? { requestId } : {}) };
|
|
92
136
|
if (observer) {
|
|
93
137
|
state.readCalls ??= {};
|
|
94
|
-
if (Object.keys(state.readCalls).length >= 32) return
|
|
138
|
+
if (Object.keys(state.readCalls).length >= 32) return denyBlocked(state, event, 'Entrorove:只读恢复尚未返回,请等待原调用。');
|
|
95
139
|
state.readCalls[event.tool_use_id] = pending;
|
|
96
140
|
} else state.pendingCall = pending;
|
|
97
141
|
const installation = call.args[0] === 'install' && parsed.words.some(word => ['npx','npx.cmd'].includes(word));
|
|
98
142
|
const rewritten = renderSdkCommand(call, runtime, state.sessionId);
|
|
143
|
+
resetBlockStreak(state);
|
|
99
144
|
return output('PreToolUse', { permissionDecision: 'allow', ...(installation ? {} : { updatedInput: { ...event.tool_input, command: rewritten,
|
|
100
145
|
...(Object.hasOwn(event.tool_input ?? {}, 'cmd') ? { cmd: rewritten } : {}) } }), additionalContext: ideContext(state) });
|
|
101
146
|
}
|
|
102
147
|
if (!state.active) return {};
|
|
103
148
|
if (['exec','functions.exec'].includes(event.tool_name)) {
|
|
104
149
|
const current = await ideRuntime(home), code = typeof event.tool_input === 'string' ? event.tool_input : event.tool_input?.code;
|
|
105
|
-
return inspectIdeCodeMode(code, current) ? output('PreToolUse', { additionalContext: ideContext(state) }) :
|
|
150
|
+
return inspectIdeCodeMode(code, current) ? output('PreToolUse', { additionalContext: ideContext(state) }) : denyBlocked(state, event, 'Entrorove:代码模式含未批准的原生执行或动态工具输入;请使用原任务的受控入口。');
|
|
106
151
|
}
|
|
107
152
|
if (isShell && readCommand(parsed) || readTools.has(event.tool_name)) return output('PreToolUse', { additionalContext: ideContext(state) });
|
|
108
|
-
return
|
|
153
|
+
return denyBlocked(state, event, `Entrorove:已阻止原生 ${event.tool_name ?? 'unknown'} 调用。当前阶段 ${state.phase}${state.taskNumber ? `,云端任务 ${state.taskNumber}` : ''}。写入、删除、移动及不确定的终端操作必须使用原任务的 guarded-write/guarded-delete/guarded-move 或已批准的执行计划。`);
|
|
109
154
|
}
|
|
110
155
|
export async function runIdeHook(event, { home } = {}) {
|
|
111
156
|
return ideSession(event, async state => {
|
|
@@ -157,10 +202,10 @@ export async function runIdeHook(event, { home } = {}) {
|
|
|
157
202
|
if (resumed) messages.push(resumed);
|
|
158
203
|
return { ...output(name, { additionalContext: ideContext(state) }), ...(messages.length ? { systemMessage: messages.join('\n') } : {}) };
|
|
159
204
|
}
|
|
160
|
-
if (name==='Stop' && state.active) {
|
|
205
|
+
if (name==='Stop' && state.active && !(await readBreakGlass(home))) {
|
|
161
206
|
const resumed = await advanceIdeQueue(state,home);
|
|
162
207
|
if (resumed || unclassifiedIdeRequirements(state) || unfinishedIdeQueue(state) && !['blocked','needs-input'].includes(state.phase)
|
|
163
|
-
|| !['ended','blocked','needs-input','setup'].includes(state.phase)) return {decision:'block',reason
|
|
208
|
+
|| !['ended','blocked','needs-input','setup'].includes(state.phase)) return {decision:'block',reason:blockedReason(state, event, `${ideContext(state)}${ideQueueContext(state)}${resumed}原任务或队列尚未取得结束回执;继续可运行工作,真实阻断必须明确报告。`)};
|
|
164
209
|
}
|
|
165
210
|
return state.active && ['SessionStart','UserPromptSubmit','PostCompact','PreCompact'].includes(name) ? output(name, { additionalContext: `${ideContext(state)}${ideQueueContext(state)}` }) : {};
|
|
166
211
|
}, home);
|
|
@@ -176,7 +221,7 @@ async function main() {
|
|
|
176
221
|
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) main().catch(() => {
|
|
177
222
|
// A non-zero generic exit may fail open in the host. Always return a supported
|
|
178
223
|
// explicit denial, without including arguments, prompts, credentials or paths.
|
|
179
|
-
const reason =
|
|
224
|
+
const reason = `Entrorove 安全检查失败;本次工具调用已阻止。可复制解锁命令:${unlockCommand}`;
|
|
180
225
|
const result = incomingEvent === 'Stop' ? { decision: 'block', reason } : incomingEvent === 'PreToolUse' ? deny(reason)
|
|
181
226
|
: { continue: false, stopReason: reason, systemMessage: reason };
|
|
182
227
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
package/ide-install.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { ideDirectory, ideDigest, readIdeJson, writeIdeJson, ideError } from './
|
|
|
5
5
|
import { withStateLock } from './local-state-lock.mjs';
|
|
6
6
|
|
|
7
7
|
const source = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const files = ['ide-hook.mjs', 'ide-session.mjs', 'ide-results.mjs', 'ide-queue.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) {
|
package/ide-results.mjs
CHANGED
|
@@ -14,9 +14,10 @@ export function parseIdeResults(response) {
|
|
|
14
14
|
function resultMetadata(value, depth = 0) {
|
|
15
15
|
if (!value || typeof value !== 'object' || depth > 3) return undefined;
|
|
16
16
|
const metadata = {};
|
|
17
|
-
for (const key of ['schemaVersion', 'requestId', 'flowId', 'status', 'taskId', 'taskNumber', 'taskStatus']) {
|
|
17
|
+
for (const key of ['schemaVersion', 'requestId', 'flowId', 'status', 'operation', 'termination', 'flowStatus', 'taskId', 'taskNumber', 'taskStatus']) {
|
|
18
18
|
if (typeof value[key] === 'string' && value[key].length <= 200) metadata[key] = value[key];
|
|
19
19
|
}
|
|
20
|
+
if (typeof value.unbound === 'boolean') metadata.unbound = value.unbound;
|
|
20
21
|
for (const key of ['output', 'result']) if (value[key]) metadata[key] = resultMetadata(value[key], depth + 1);
|
|
21
22
|
for (const key of ['nextStep', 'nextAction']) if (typeof value[key]?.operation === 'string') metadata[key] = { operation: value[key].operation };
|
|
22
23
|
if (value.interaction) metadata.interaction = true;
|
package/ide-session.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { credentialDirectory, readCredential } from './local-credential.mjs';
|
|
6
6
|
import { withStateLock } from './local-state-lock.mjs';
|
|
7
7
|
import { requestTransport, parseTransportResponse } from './remote-transport.mjs';
|
|
8
|
+
import { breakGlassPath, breakGlassSchema, writeBreakGlass as writeOfflineBreakGlass } from './break-glass.mjs';
|
|
8
9
|
|
|
9
10
|
export const ideDigest = value => createHash('sha256').update(typeof value === 'string' ? value : JSON.stringify(value)).digest('hex');
|
|
10
11
|
export const ideError = (code, message) => Object.assign(new Error(message), { code });
|
|
@@ -32,15 +33,26 @@ export async function writeIdeJson(file, value) {
|
|
|
32
33
|
await fs.writeFile(stage, `${JSON.stringify(value)}\n`, { flag: 'wx', mode: 0o600 });
|
|
33
34
|
try { await fs.rename(stage, file); } finally { await fs.rm(stage, { force: true }); }
|
|
34
35
|
}
|
|
36
|
+
export const writeBreakGlass = writeOfflineBreakGlass;
|
|
37
|
+
export async function readBreakGlass(home = os.homedir()) {
|
|
38
|
+
await ideDirectory(home, true);
|
|
39
|
+
const value = await readIdeJson(breakGlassPath(home));
|
|
40
|
+
if (!value || value.schemaVersion !== breakGlassSchema || value.mode !== 'management-only'
|
|
41
|
+
|| typeof value.expiresAt !== 'string' || !Number.isFinite(Date.parse(value.expiresAt))
|
|
42
|
+
|| Date.parse(value.expiresAt) <= Date.now()) return null;
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
35
45
|
export async function ideSession(event, action, home = os.homedir()) {
|
|
36
46
|
if (typeof event.session_id !== 'string' || !/^[A-Za-z0-9_-]{1,200}$/u.test(event.session_id) || !path.isAbsolute(event.cwd ?? '')) throw ideError('ENTROROVE_IDE_IDENTITY', 'Host hook requires its actual session and repository');
|
|
37
47
|
const directory = await ideDirectory(home, true), file = path.join(directory, `session-${ideDigest(event.session_id)}.json`);
|
|
38
48
|
return withStateLock(file, async () => {
|
|
39
49
|
const root = await fs.realpath(event.cwd), prior = await readIdeJson(file);
|
|
40
50
|
if (prior && (prior.schemaVersion !== 'entrorove.ide-session/1.0' || prior.sessionId !== event.session_id || prior.repositoryRoot !== root)) throw ideError('ENTROROVE_IDE_IDENTITY', 'A session cannot replace its original physical repository');
|
|
41
|
-
const state = prior ?? { schemaVersion: 'entrorove.ide-session/1.0', sessionId: event.session_id, repositoryRoot: root, active: false, phase: 'entry', seen: [] };
|
|
51
|
+
const state = prior ?? { schemaVersion: 'entrorove.ide-session/1.0', sessionId: event.session_id, repositoryRoot: root, active: false, phase: 'entry', seen: [], blockStreak: 0 };
|
|
52
|
+
state.stateFile = file;
|
|
42
53
|
const result = await action(state);
|
|
43
|
-
|
|
54
|
+
const { stateFile, ...persisted } = state;
|
|
55
|
+
await writeIdeJson(file, { ...persisted, observedAt: new Date().toISOString() });
|
|
44
56
|
return result;
|
|
45
57
|
});
|
|
46
58
|
}
|
package/local-cli.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
LOCAL_LEDGER_OPERATIONS,
|
|
13
13
|
LOCAL_RENEWAL_OPERATIONS, LOCAL_PROBE_OPERATIONS,
|
|
14
14
|
LOCAL_TASK_ROUTING_OPERATIONS,
|
|
15
|
+
LOCAL_FLOW_LIFECYCLE_OPERATIONS,
|
|
15
16
|
} from './local-execution.mjs';
|
|
16
17
|
import { validateLocalInput } from './local-contracts.mjs';
|
|
17
18
|
import { prepareMergeArtifacts } from './local-merge-artifacts.mjs';
|
|
@@ -58,6 +59,7 @@ const operations = {
|
|
|
58
59
|
...LOCAL_LEDGER_OPERATIONS, ...LOCAL_CHAIN_OPERATIONS, ...LOCAL_BUDGET_TRANSITION_OPERATIONS,
|
|
59
60
|
...LOCAL_RENEWAL_OPERATIONS, ...LOCAL_PROBE_OPERATIONS,
|
|
60
61
|
...LOCAL_TASK_ROUTING_OPERATIONS,
|
|
62
|
+
...LOCAL_FLOW_LIFECYCLE_OPERATIONS,
|
|
61
63
|
...ARCHGUARD_LOCAL_OPERATIONS,
|
|
62
64
|
...LOCAL_BUDGET_HANDOFF_OPERATIONS,
|
|
63
65
|
...CALCTOOL_LOCAL_OPERATIONS,
|
package/local-contracts.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { array, binding, change, confirmation, enumeration, evidence, evidenceAr
|
|
|
15
15
|
const schemas = {};
|
|
16
16
|
const base = { repositoryRoot: text, home: text };
|
|
17
17
|
const flow = { ...base, flowId: identifier };
|
|
18
|
+
const flowLifecycle = { ...base, flowId: identifier, reason: { ...text, minLength: 1, maxLength: 500, pattern: '\\S' } };
|
|
18
19
|
const fixedRoundingPolicy = object({ decimalPlaces: { const: 2 }, mode: { const: 'half-away-from-zero' } }, ['decimalPlaces', 'mode']);
|
|
19
20
|
const step = { ...flow, stepId: identifier };
|
|
20
21
|
function add(names, properties, required = [], alternatives) {
|
|
@@ -24,6 +25,7 @@ function add(names, properties, required = [], alternatives) {
|
|
|
24
25
|
for (const name of names.split('|')) schemas[name] = schema;
|
|
25
26
|
}
|
|
26
27
|
add('capabilities|help', {});
|
|
28
|
+
add('flow-cancel|flow-abandon', flowLifecycle, ['repositoryRoot', 'flowId']);
|
|
27
29
|
add('snapshot-plan', { ...step, paths, snapshotId: identifier, contractDigest: nullable(sha256), handoffCandidate: HANDOFF_SNAPSHOT_CANDIDATE_SCHEMA }, ['repositoryRoot', 'flowId', 'stepId', 'paths']);
|
|
28
30
|
add('snapshot-verify', { ...step, snapshotId: identifier }, ['repositoryRoot', 'flowId', 'stepId', 'snapshotId']);
|
|
29
31
|
const gate = { ...step, taskId: identifier, mutationPass: record };
|
package/local-execution.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { receiverClaim } from './local-task-handoff-overlay-wire.mjs';
|
|
|
25
25
|
import { assertTaskHandoffActive, assertTaskHandoffSnapshotBinding, readTaskHandoffSnapshotBinding,
|
|
26
26
|
withTaskHandoffIssueFence, withTaskHandoffWriteFence } from './local-task-handoff-fence.mjs';
|
|
27
27
|
import { LOCAL_OPERATION_SCHEMAS } from './local-contracts.mjs';
|
|
28
|
+
import { LOCAL_FLOW_LIFECYCLE_OPERATIONS, flowCancel, flowAbandon } from './local-flow-lifecycle.mjs';
|
|
28
29
|
export { LOCAL_OPERATION_SCHEMAS };
|
|
29
30
|
export const LOCAL_SCHEMA = 'entrorove.local-execution/1.0';
|
|
30
31
|
export const SNAPSHOT_SCHEMA = 'entrorove.snapshot/1.0';
|
|
@@ -314,3 +315,4 @@ export { LOCAL_RENEWAL_OPERATIONS, LOCAL_PROBE_OPERATIONS };
|
|
|
314
315
|
export { branchCreate, branchList, branchSwitch, diffReport, preflight, ledgerQuery, appendLedgerRecord, chainInit, chainResume, chainStatus, chainAnswer };
|
|
315
316
|
export { budgetAutoRenewRequest, budgetAutoRenew, budgetAutoRenewStop, probe, reassess };
|
|
316
317
|
export { LOCAL_TASK_ROUTING_OPERATIONS, taskDescribe, messageRoute, messageAccept, taskResume };
|
|
318
|
+
export { LOCAL_FLOW_LIFECYCLE_OPERATIONS, flowCancel, flowAbandon };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { withStateLock } from './local-state-lock.mjs';
|
|
6
|
+
import { settleTaskFlow } from './local-task-routing.mjs';
|
|
7
|
+
|
|
8
|
+
export const FLOW_LIFECYCLE_SCHEMA = 'entrorove.flow-lifecycle/1.0';
|
|
9
|
+
function fail(code, message) { throw Object.assign(new Error(message), { code }); }
|
|
10
|
+
function digest(value) { return createHash('sha256').update(value).digest('hex'); }
|
|
11
|
+
function canonical(value) {
|
|
12
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
13
|
+
if (value && typeof value === 'object') return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
|
|
14
|
+
return JSON.stringify(value);
|
|
15
|
+
}
|
|
16
|
+
function rootOf(value) {
|
|
17
|
+
if (typeof value !== 'string' || !path.isAbsolute(value)) fail('ENTROROVE_ROOT_INVALID', 'repositoryRoot must be absolute');
|
|
18
|
+
return fs.realpath(value);
|
|
19
|
+
}
|
|
20
|
+
function homeOf(value) {
|
|
21
|
+
const home = value ?? os.homedir();
|
|
22
|
+
if (typeof home !== 'string' || !path.isAbsolute(home)) fail('ENTROROVE_HOME_INVALID', 'home must be absolute');
|
|
23
|
+
return home;
|
|
24
|
+
}
|
|
25
|
+
async function privateDir(directory) {
|
|
26
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
27
|
+
const stat = await fs.lstat(directory);
|
|
28
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) fail('ENTROROVE_STATE_INVALID', 'flow lifecycle state directory must be a real directory');
|
|
29
|
+
if (process.platform !== 'win32' && (stat.mode & 0o777) !== 0o700) await fs.chmod(directory, 0o700);
|
|
30
|
+
}
|
|
31
|
+
async function location(input) {
|
|
32
|
+
const root = await rootOf(input.repositoryRoot); const home = homeOf(input.home);
|
|
33
|
+
const repositoryDigest = digest(root).slice(0, 32);
|
|
34
|
+
const directory = path.join(home, '.entrorove', 'flow-lifecycle', repositoryDigest);
|
|
35
|
+
return { root, home, file: path.join(directory, 'state.json'), directory, repositoryDigest };
|
|
36
|
+
}
|
|
37
|
+
async function save(file, state) {
|
|
38
|
+
await privateDir(path.dirname(file));
|
|
39
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
40
|
+
await fs.writeFile(temporary, `${canonical(state)}\n`, { flag: 'wx', mode: 0o600 });
|
|
41
|
+
await fs.rename(temporary, file);
|
|
42
|
+
if (process.platform !== 'win32') await fs.chmod(file, 0o600);
|
|
43
|
+
}
|
|
44
|
+
async function load(input) {
|
|
45
|
+
const place = await location(input); await privateDir(place.directory);
|
|
46
|
+
try {
|
|
47
|
+
const stat = await fs.lstat(place.file);
|
|
48
|
+
if (!stat.isFile() || stat.isSymbolicLink()) fail('ENTROROVE_STATE_INVALID', 'flow lifecycle state must be a regular file');
|
|
49
|
+
const state = JSON.parse(await fs.readFile(place.file, 'utf8'));
|
|
50
|
+
if (state.schemaVersion !== FLOW_LIFECYCLE_SCHEMA || state.repositoryDigest !== place.repositoryDigest
|
|
51
|
+
|| !Array.isArray(state.flows) || !Array.isArray(state.records)) fail('ENTROROVE_STATE_INVALID', 'flow lifecycle state identity is invalid');
|
|
52
|
+
return { ...place, state };
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error.code !== 'ENOENT') throw error;
|
|
55
|
+
return { ...place, state: { schemaVersion: FLOW_LIFECYCLE_SCHEMA, repositoryDigest: place.repositoryDigest, sequence: 0, flows: [], records: [] } };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function identifier(value, label) {
|
|
59
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(value)) fail('ENTROROVE_ID_INVALID', `${label} must be a safe identifier`);
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function reason(value, operation) {
|
|
63
|
+
if (value === undefined) return operation === 'flow-abandon' ? 'flow-abandoned-by-owner' : 'flow-cancelled-by-owner';
|
|
64
|
+
if (typeof value !== 'string' || !value.trim() || value.length > 500 || /[\u0000-\u001f\u007f]/.test(value)) fail('ENTROROVE_INPUT_INVALID', 'reason must be 1 to 500 printable characters');
|
|
65
|
+
return value.trim();
|
|
66
|
+
}
|
|
67
|
+
async function runUnlocked(operation, input) {
|
|
68
|
+
const flowId = identifier(input.flowId, 'flowId'); const authority = await load(input);
|
|
69
|
+
const prior = authority.state.flows.find(flow => flow.flowId === flowId);
|
|
70
|
+
if (prior?.status === 'abandoned' || (prior?.status === 'cancelled' && operation === 'flow-cancel')) {
|
|
71
|
+
return { schemaVersion: 'entrorove.local-execution/1.0', status: 'reported', operation, flowId,
|
|
72
|
+
termination: prior.status === 'abandoned' ? 'abandoned' : 'cancelled', reason: prior.reason, unbound: prior.unbound === true, duplicate: true };
|
|
73
|
+
}
|
|
74
|
+
const now = new Date().toISOString(); const termination = operation === 'flow-abandon' ? 'abandoned' : 'cancelled';
|
|
75
|
+
const settledTask = await settleTaskFlow(input, operation);
|
|
76
|
+
const unbound = operation === 'flow-abandon';
|
|
77
|
+
const record = { flowId, status: termination, reason: reason(input.reason, operation), unbound, updatedAt: now };
|
|
78
|
+
const flows = [...authority.state.flows.filter(flow => flow.flowId !== flowId), record];
|
|
79
|
+
const event = { sequence: authority.state.sequence + 1, event: operation, flowId, at: now, reason: record.reason,
|
|
80
|
+
unbound: record.unbound, digest: digest(canonical({ operation, flowId, reason: record.reason, unbound: record.unbound })) };
|
|
81
|
+
const state = { ...authority.state, sequence: event.sequence, flows, records: [...authority.state.records, event] };
|
|
82
|
+
await save(authority.file, state);
|
|
83
|
+
return { schemaVersion: 'entrorove.local-execution/1.0', status: 'reported', operation, flowId,
|
|
84
|
+
termination, reason: record.reason, unbound: record.unbound,
|
|
85
|
+
...(settledTask?.task ? { taskId: settledTask.task.taskId, taskStatus: settledTask.task.status } : {}),
|
|
86
|
+
progress: state.records, nextStep: record.unbound
|
|
87
|
+
? { operation: 'start', instruction: 'The old local flow is released; start the original task again.' } : null };
|
|
88
|
+
}
|
|
89
|
+
export async function flowCancel(input) {
|
|
90
|
+
const place = await location(input); return withStateLock(place.file, () => runUnlocked('flow-cancel', input));
|
|
91
|
+
}
|
|
92
|
+
export async function flowAbandon(input) {
|
|
93
|
+
const place = await location(input); return withStateLock(place.file, () => runUnlocked('flow-abandon', input));
|
|
94
|
+
}
|
|
95
|
+
export const LOCAL_FLOW_LIFECYCLE_OPERATIONS = Object.freeze({ 'flow-cancel': flowCancel, 'flow-abandon': flowAbandon });
|
|
@@ -9,15 +9,20 @@ function literal(value, field) {
|
|
|
9
9
|
}
|
|
10
10
|
return value;
|
|
11
11
|
}
|
|
12
|
-
function within(directory, value, allowRoot = false) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
function within(directory, value, allowRoot = false, field = 'path') {
|
|
13
|
+
const raw = literal(value, field);
|
|
14
|
+
const resolved = path.posix.normalize(path.posix.join(directory, raw)).replace(/\/+$/, '');
|
|
15
|
+
const reason = resolved === '..' || resolved.startsWith('../') || resolved.startsWith('/')
|
|
16
|
+
? 'the resolved path is outside the repository root'
|
|
17
|
+
: resolved.split('/').some(part => ['.git', '.entrorove', '.aimlock', '.archguard'].includes(part))
|
|
18
|
+
? 'the resolved path enters a reserved control directory'
|
|
19
|
+
: null;
|
|
20
|
+
if (reason) {
|
|
21
|
+
throw new Error(`Scope tsconfig path escapes the authorized repository: ${JSON.stringify(raw)} resolves to ${JSON.stringify(resolved || '.')} from ${JSON.stringify(directory || '.')} because ${reason}`);
|
|
17
22
|
}
|
|
18
23
|
if (resolved === '.') {
|
|
19
24
|
if (!allowRoot)
|
|
20
|
-
throw new Error(
|
|
25
|
+
throw new Error(`Scope tsconfig mapping requires a literal candidate within the repository: ${JSON.stringify(raw)} resolves to the repository root from ${JSON.stringify(directory || '.')}`);
|
|
21
26
|
return '';
|
|
22
27
|
}
|
|
23
28
|
return resolved;
|
|
@@ -149,14 +154,14 @@ export async function scopeTsconfigImports(read, fromFile, configPath) {
|
|
|
149
154
|
const extended = literal(config.extends, 'extends');
|
|
150
155
|
if (!extended.startsWith('./') && !extended.startsWith('../'))
|
|
151
156
|
throw new Error('Scope tsconfig npm extends is unsupported; use an actual repository-relative extends');
|
|
152
|
-
inherited = await load(within(directory, extended), new Set([...ancestors, relative]), depth + 1);
|
|
157
|
+
inherited = await load(within(directory, extended, false, 'extends'), new Set([...ancestors, relative]), depth + 1);
|
|
153
158
|
}
|
|
154
159
|
const options = config.compilerOptions === undefined ? {} : config.compilerOptions;
|
|
155
160
|
if (!options || typeof options !== 'object' || Array.isArray(options))
|
|
156
161
|
throw new Error('Scope tsconfig compilerOptions must be an object');
|
|
157
162
|
const own = options;
|
|
158
163
|
return { ...inherited,
|
|
159
|
-
...(own.baseUrl === undefined ? {} : { baseUrl: within(directory,
|
|
164
|
+
...(own.baseUrl === undefined ? {} : { baseUrl: within(directory, own.baseUrl, true, 'baseUrl') }),
|
|
160
165
|
...(own.paths === undefined ? {} : { paths: pathsOption(own.paths), pathsOrigin: directory }) };
|
|
161
166
|
};
|
|
162
167
|
let directory = path.posix.dirname(canonical);
|
|
@@ -181,7 +186,7 @@ export async function scopeTsconfigImports(read, fromFile, configPath) {
|
|
|
181
186
|
if (resolution?.paths)
|
|
182
187
|
for (const targets of Object.values(resolution.paths))
|
|
183
188
|
for (const target of targets) {
|
|
184
|
-
within(resolution.baseUrl ?? resolution.pathsOrigin, target.replace('*', '__scope_wildcard__'));
|
|
189
|
+
within(resolution.baseUrl ?? resolution.pathsOrigin, target.replace('*', '__scope_wildcard__'), false, 'mapping');
|
|
185
190
|
}
|
|
186
191
|
return { candidates: specifier => importCandidates(resolution, specifier), mapped: specifier => {
|
|
187
192
|
if (typeof specifier !== 'string' || !specifier)
|
package/local-scope-files.mjs
CHANGED
|
@@ -23,9 +23,23 @@ export async function scopeRoot(value) {
|
|
|
23
23
|
if (!stat.isDirectory() || stat.isSymbolicLink()) scopeFail('ENTROROVE_ROOT_INVALID', 'repositoryRoot must be a real directory');
|
|
24
24
|
return fs.realpath(value);
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
function dependencyReference(value) { return value.split('/').includes('node_modules'); }
|
|
27
|
+
function pathSegments(value) { return value.split(path.sep).filter(Boolean); }
|
|
28
|
+
async function assertDependencyReference(root, target, relative) {
|
|
29
|
+
const resolved = await fs.realpath(target), rootReal = await fs.realpath(root);
|
|
30
|
+
const inside = resolved === rootReal || resolved.startsWith(`${rootReal}${path.sep}`);
|
|
31
|
+
const reserved = pathSegments(resolved).some(part => ['.git', '.entrorove', '.aimlock', '.archguard'].includes(part));
|
|
32
|
+
if (reserved || (!inside && !dependencyReference(resolved.split(path.sep).join('/')))) {
|
|
33
|
+
const reason = reserved ? 'the resolved target enters a reserved control directory'
|
|
34
|
+
: 'the resolved target is outside the authorized root and is not a dependency directory';
|
|
35
|
+
scopeFail('ENTROROVE_PATH_UNSAFE', `Scope dependency reference "${relative}" resolves to "${resolved}" because ${reason}`);
|
|
36
|
+
}
|
|
37
|
+
return resolved;
|
|
38
|
+
}
|
|
39
|
+
export async function scopeTarget(root, relative, allowMissing = false, allowDependency = false) {
|
|
27
40
|
let current = root;
|
|
28
41
|
const parts = scopePath(relative).split('/');
|
|
42
|
+
const followDependency = allowDependency && dependencyReference(relative);
|
|
29
43
|
for (const [index, part] of parts.entries()) {
|
|
30
44
|
current = path.join(current, part);
|
|
31
45
|
let stat;
|
|
@@ -33,30 +47,37 @@ export async function scopeTarget(root, relative, allowMissing = false) {
|
|
|
33
47
|
if (error.code === 'ENOENT' && allowMissing) return { target: path.join(root, relative), exists: false, stat: null };
|
|
34
48
|
throw error;
|
|
35
49
|
}
|
|
36
|
-
if (stat.isSymbolicLink()
|
|
37
|
-
|
|
50
|
+
if (stat.isSymbolicLink()) {
|
|
51
|
+
if (!followDependency) scopeFail('ENTROROVE_FILE_INVALID', 'scope refuses symlink or non-directory ancestors');
|
|
52
|
+
await assertDependencyReference(root, current, relative);
|
|
53
|
+
stat = await fs.stat(current);
|
|
54
|
+
}
|
|
55
|
+
if (index < parts.length - 1 && !stat.isDirectory()) scopeFail('ENTROROVE_FILE_INVALID', 'scope refuses symlink or non-directory ancestors');
|
|
56
|
+
if (index === parts.length - 1) return { target: current, exists: true, stat, dependency: followDependency };
|
|
38
57
|
}
|
|
39
58
|
}
|
|
40
|
-
async function safeScopeRead(root, relative, input) {
|
|
41
|
-
const target = await scopeTarget(root, relative);
|
|
59
|
+
async function safeScopeRead(root, relative, input, allowDependency = true) {
|
|
60
|
+
const dependency = allowDependency && dependencyReference(relative), target = await scopeTarget(root, relative, false, dependency);
|
|
42
61
|
if (!target.stat.isFile()) scopeFail('ENTROROVE_FILE_INVALID', 'scope source must be a regular file');
|
|
43
62
|
if (target.stat.size > 16_777_216) scopeFail('ENTROROVE_SCOPE_LIMIT', 'scope source exceeds the 16 MiB inspection bound');
|
|
44
|
-
const handle = await fs.open(target.target, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
63
|
+
const handle = await fs.open(target.target, constants.O_RDONLY | (dependency ? 0 : (constants.O_NOFOLLOW ?? 0)));
|
|
45
64
|
try {
|
|
46
65
|
const before = await handle.stat();
|
|
66
|
+
const beforeReal = dependency ? await fs.realpath(target.target) : null;
|
|
47
67
|
const content = input ? await readBudgetedProjectFile(input, target.target, { sourcePath: relative }) : await handle.readFile();
|
|
48
68
|
const after = await handle.stat();
|
|
49
|
-
const latest = await fs.lstat(target.target);
|
|
69
|
+
const latest = dependency ? await fs.stat(target.target) : await fs.lstat(target.target);
|
|
70
|
+
const afterReal = dependency ? await fs.realpath(target.target) : null;
|
|
50
71
|
if (before.ino !== target.stat.ino || before.dev !== target.stat.dev || after.size !== before.size
|
|
51
72
|
|| after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs || latest.ino !== after.ino
|
|
52
|
-
|| latest.dev !== after.dev || latest.isSymbolicLink()) scopeFail('ENTROROVE_SCOPE_CHANGED', 'scope source changed during inspection');
|
|
53
|
-
await scopeTarget(root, relative);
|
|
73
|
+
|| latest.dev !== after.dev || !dependency && latest.isSymbolicLink() || beforeReal !== afterReal) scopeFail('ENTROROVE_SCOPE_CHANGED', 'scope source changed during inspection');
|
|
74
|
+
await scopeTarget(root, relative, false, dependency);
|
|
54
75
|
return { content, mode: after.mode & 0o777, sha256: sha256(content), bytes: content.length };
|
|
55
76
|
} finally { await handle.close(); }
|
|
56
77
|
}
|
|
57
|
-
export const scopeRead = (root, relative, input = { repositoryRoot: root }) => safeScopeRead(root, relative, input);
|
|
78
|
+
export const scopeRead = (root, relative, input = { repositoryRoot: root }) => safeScopeRead(root, relative, input, true);
|
|
58
79
|
// Internal control roles only: signed manifest metadata and already validated private signer files.
|
|
59
|
-
export const scopeControlRead = (root, relative) => safeScopeRead(root, relative, null);
|
|
80
|
+
export const scopeControlRead = (root, relative) => safeScopeRead(root, relative, null, false);
|
|
60
81
|
const excluded = new Set(['.git', 'node_modules', '.entrorove', '.aimlock', '.archguard']);
|
|
61
82
|
export async function scopeFiles(root, requested, knownFiles = []) {
|
|
62
83
|
if (!Array.isArray(requested) || !requested.length) scopeFail('ENTROROVE_SCOPE_EMPTY', 'a non-empty target scope is required');
|
|
@@ -214,11 +214,12 @@ async function resolveImport(root, from, specifier, workspace, input, aliases, c
|
|
|
214
214
|
const relative = localPackage ?? path.posix.normalize(path.posix.join(path.posix.dirname(from), specifier));
|
|
215
215
|
return resolveLocalImport(root, relative);
|
|
216
216
|
}
|
|
217
|
+
const dependencyReference = relative => relative.split('/').includes('node_modules');
|
|
217
218
|
async function resolveLocalImport(root, relative, required = true) {
|
|
218
219
|
if (relative.startsWith('../') || relative === '..') scopeFail('ENTROROVE_PATH_UNSAFE', 'local import escapes the repository');
|
|
219
220
|
const replacements = /\.js$/.test(relative) ? [relative.replace(/\.js$/, '.ts'), relative.replace(/\.js$/, '.tsx')] : [];
|
|
220
221
|
for (const candidate of [relative, ...replacements, ...['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.vue', '.json', '.d.ts', '.d.mts', '.d.cts', '/index.js', '/index.ts', '/index.d.ts'].map(ext => relative + ext)]) {
|
|
221
|
-
const item = await scopeTarget(root, candidate, true);
|
|
222
|
+
const item = await scopeTarget(root, candidate, true, dependencyReference(candidate));
|
|
222
223
|
if (item.exists && item.stat.isFile()) return candidate;
|
|
223
224
|
}
|
|
224
225
|
if (required) scopeFail('ENTROROVE_IMPORT_UNRESOLVED', 'a local import has no resolvable source file');
|
package/local-task-routing.mjs
CHANGED
|
@@ -74,6 +74,9 @@ function taskRecord(state, taskId) {
|
|
|
74
74
|
if (!task) fail('ENTROROVE_TASK_UNKNOWN', `unknown task: ${taskId}`);
|
|
75
75
|
return task;
|
|
76
76
|
}
|
|
77
|
+
function taskFlowId(task) {
|
|
78
|
+
return task?.flowId ?? task?.checkpoint?.flowId ?? task?.route?.flowId ?? task?.acceptance?.flowId;
|
|
79
|
+
}
|
|
77
80
|
async function record(authority, event, data, nextState = authority.state) {
|
|
78
81
|
const entry = { sequence: authority.state.sequence + 1, event, at: new Date().toISOString(), ...data };
|
|
79
82
|
const state = { ...nextState, sequence: entry.sequence,
|
|
@@ -208,9 +211,33 @@ async function resumeUnlocked(input) {
|
|
|
208
211
|
}
|
|
209
212
|
return { schemaVersion: TASK_ROUTING_SCHEMA, status: 'reported', ...recovered };
|
|
210
213
|
}
|
|
214
|
+
async function settleTaskFlowUnlocked(input, operation) {
|
|
215
|
+
if (!['flow-cancel', 'flow-abandon'].includes(operation)) fail('ENTROROVE_OPERATION_INVALID', 'Unknown flow lifecycle operation');
|
|
216
|
+
const flowId = requireId(input.flowId, 'flowId');
|
|
217
|
+
const authority = await load(input);
|
|
218
|
+
const taskId = input.taskId === undefined ? undefined : requireId(input.taskId, 'taskId');
|
|
219
|
+
const matches = authority.state.tasks.filter(task => (taskId ? task.taskId === taskId : true)
|
|
220
|
+
&& taskFlowId(task) === flowId);
|
|
221
|
+
if (!matches.length) return { schemaVersion: TASK_ROUTING_SCHEMA, status: 'reported', flowId, unbound: false, task: null };
|
|
222
|
+
const released = matches.map(task => {
|
|
223
|
+
if (operation === 'flow-abandon' && task.status === 'completed') fail('ENTROROVE_TASK_COMPLETED', 'Completed task cannot be abandoned');
|
|
224
|
+
if (operation === 'flow-cancel') return task.status === 'completed' ? task : { ...task, status: 'blocked', resumedAt: new Date().toISOString() };
|
|
225
|
+
const checkpoint = task.checkpoint && typeof task.checkpoint === 'object' && !Array.isArray(task.checkpoint)
|
|
226
|
+
? { ...task.checkpoint, flowId: null } : task.checkpoint;
|
|
227
|
+
return { ...task, status: 'pending', flowId: null, route: null, ...(checkpoint ? { checkpoint } : {}), resumedAt: new Date().toISOString() };
|
|
228
|
+
});
|
|
229
|
+
const releasedById = new Map(released.map(task => [task.taskId, task]));
|
|
230
|
+
const state = { ...authority.state, tasks: authority.state.tasks.map(task => releasedById.get(task.taskId) ?? task) };
|
|
231
|
+
const unbound = operation === 'flow-abandon';
|
|
232
|
+
const entry = await record(authority, unbound ? 'flow-unbind' : 'flow-cancel', { flowId, taskIds: released.map(task => task.taskId) }, state);
|
|
233
|
+
return { schemaVersion: TASK_ROUTING_SCHEMA, status: 'reported', flowId, unbound,
|
|
234
|
+
task: released.length === 1 ? released[0] : undefined, taskIds: released.map(task => task.taskId), record: entry };
|
|
235
|
+
}
|
|
211
236
|
export async function taskDescribe(input) { return routingLock(input, () => describeUnlocked(input)); }
|
|
212
237
|
export async function messageRoute(input) { return routingLock(input, () => routeUnlocked(input)); }
|
|
213
238
|
export async function messageAccept(input) { return routingLock(input, () => acceptUnlocked(input)); }
|
|
214
239
|
export async function taskResume(input) { return resumeUnlocked(input); }
|
|
240
|
+
export async function settleTaskFlow(input, operation) { return routingLock(input, () => settleTaskFlowUnlocked(input, operation)); }
|
|
241
|
+
export async function releaseTaskFlow(input) { return settleTaskFlow(input, 'flow-abandon'); }
|
|
215
242
|
export const LOCAL_TASK_ROUTING_OPERATIONS = Object.freeze({ 'task-describe': taskDescribe, 'message-route': messageRoute, 'message-accept': messageAccept, 'task-resume': taskResume });
|
|
216
243
|
export const LOCAL_TASK_ROUTING_SCHEMAS = operationSchemasFor(LOCAL_TASK_ROUTING_OPERATIONS);
|
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entrorove/skill",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.40",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"entrorove": "cli.mjs"
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"workflow-local-contracts.mjs",
|
|
66
66
|
"local-budget-renewal.mjs",
|
|
67
67
|
"local-probe.mjs",
|
|
68
|
+
"local-flow-lifecycle.mjs",
|
|
68
69
|
"local-task-routing.mjs",
|
|
69
70
|
"local-task-handoff-fence.mjs",
|
|
70
71
|
"local-task-handoff-storage.mjs",
|
|
@@ -203,6 +204,7 @@
|
|
|
203
204
|
"ide-hook.mjs",
|
|
204
205
|
"ide-install.mjs",
|
|
205
206
|
"ide-session.mjs",
|
|
207
|
+
"break-glass.mjs",
|
|
206
208
|
"ide-results.mjs",
|
|
207
209
|
"ide-queue.mjs",
|
|
208
210
|
"local-discipline-actions.mjs",
|
package/release-state.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "entrorove.release-state/1.0",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"digest": "
|
|
3
|
+
"version": "0.1.40",
|
|
4
|
+
"digest": "4175f36123295cacefbcd09150116696d7528716ba1541256cf345ec3f31a2f3",
|
|
5
5
|
"files": {
|
|
6
6
|
"ide-code-mode.mjs": "cd5318342aac23ba9719ad760b7d7b8018d31839df1bed4ed7142c2782e22def",
|
|
7
|
-
"ide-command.mjs": "
|
|
8
|
-
"ide-hook.mjs": "
|
|
9
|
-
"ide-install.mjs": "
|
|
10
|
-
"ide-session.mjs": "
|
|
11
|
-
"
|
|
7
|
+
"ide-command.mjs": "e50a7e0b68bfc1812568f60562598a463ae169984a1abe9d401229c2b66c78c2",
|
|
8
|
+
"ide-hook.mjs": "567593a4c06911889954dc6499bd22be0ab67c78f0d8d46ad671bf55934f9041",
|
|
9
|
+
"ide-install.mjs": "65814d1e73cbdccdae4d8703cf9e8573492a3fd6a549ef8562a3f9d703f0ba09",
|
|
10
|
+
"ide-session.mjs": "964c5088454a9069fbf5672d2456ba136718c3b2e9eec31c96f7c5943972731b",
|
|
11
|
+
"break-glass.mjs": "2ad6f084c788d63fec11e64412864ac715bcb1a282dd2d8f03dc14e89e82135e",
|
|
12
|
+
"ide-results.mjs": "dfe0d3f773c65a9fc21056538d7005012c982ed4509bc0410d7f62813d0863fd",
|
|
12
13
|
"ide-queue.mjs": "806e716c775fd0079417027b135977c8516f47dddb0bc5ca19e32a355dab0e9e",
|
|
13
14
|
"local-discipline-actions.mjs": "2379c08d0f49c44068f8239db7e514d75885f4606b1b88a7a36e4ff19167caca",
|
|
14
15
|
"local-discipline-analysis.mjs": "ebea932438a53a157d5d07a5d68366496eee121a2c67cecfb4fdb2d5aa9763eb",
|
|
@@ -28,9 +29,9 @@
|
|
|
28
29
|
"runtime/validator-runtime-v7.0.44.d.mts": "7797edbfe2f7de24da370204dce4dfd22822a1bd80df93514fff900b98afe36f",
|
|
29
30
|
"snapshot-retention.mjs": "4f8919fe8ec49fb4c23e080a8ad2fa3a9282bca3fffe7cf55c64e89ef316697e",
|
|
30
31
|
"snapshot-cleanup-journal.mjs": "76a9684cc95b4eb076e1298d78a9dff37948b00d9607b8f546c599072d3ed331",
|
|
31
|
-
"IDE-EXECUTION.md": "
|
|
32
|
+
"IDE-EXECUTION.md": "fe1c65819749fdd7cad4663eab7d11e6e566ef623dc30352e47effe0bd1263d9",
|
|
32
33
|
"REUSE-DISCIPLINE.md": "295ecd41c931b00c21d009ab65ac160f0e2ab81b4000ed498d2e8e97c2dc6c63",
|
|
33
|
-
"reuse-discipline-release.mjs": "
|
|
34
|
+
"reuse-discipline-release.mjs": "20ded9c21bd71b8779c454f5eba4c7c0a943ef99d94117b9bbef891d2ab6e3de",
|
|
34
35
|
"local-final-gate16.mjs": "b30caab08441449ad556a6e9a3cefbf91190f0b3f7370f46d2a21156375aa8e3",
|
|
35
36
|
"local-final-gate16-checks.mjs": "1dd30e9666d458d8b12ca6e4975f6f0ac8450c3ea9cbecc7401ae037f66b589a",
|
|
36
37
|
"local-final-gate16-worker.mjs": "344d64b6c1c5552b96960e0d8514a876a3dbfa9762ab9eb749038e8351727121",
|
|
@@ -72,18 +73,18 @@
|
|
|
72
73
|
"local-autocoord17-waits.mjs": "2b0a713987822548a94e1ed3a9a891532db45383797dfc5d5bc8d1e35593cf25",
|
|
73
74
|
"local-validator-isolation17.mjs": "48e94604e28b8fddd567dfb35ff60b73fd094a5803bd49f076388b7f310f92aa",
|
|
74
75
|
"local-calctool-composition17.mjs": "ac89fcb41ab0d0d9b47c1b5696945281b0894eb064820775e36a011c76314e6a",
|
|
75
|
-
"cli.mjs": "
|
|
76
|
+
"cli.mjs": "bb99f5a52c770ded055fe27a35b3067ac348441b9488df336e6bf751b803e459",
|
|
76
77
|
"review-client.mjs": "3491eef68ed533174b31db3c53febb6519b8f088f71a9964644714a9a4c03c77",
|
|
77
78
|
"version.mjs": "1f392147d72c40af4b03b307ea607075fb815af5174fa07e254b95fe12dbf3e7",
|
|
78
|
-
"SKILL.md": "
|
|
79
|
-
"CAPABILITY-MAP.md": "
|
|
79
|
+
"SKILL.md": "d89c2a83a080a217d67d75fa0251b0f054b92e81a9e40a6c0b52c7ecf3d79099",
|
|
80
|
+
"CAPABILITY-MAP.md": "a0f0468a098725ced52a96dd8e22bfc7d1ce7681a8446ebf7dae3696c76c3385",
|
|
80
81
|
"AGREEMENT.md": "f93a68e92c457200dd024610e65f74e478b8f60860f951da818b70561f1b9bf8",
|
|
81
|
-
"package.json": "
|
|
82
|
+
"package.json": "a522540d3967d9348a37c6538c1abb39fe6d50872aa8f7f90b85e920590091c7",
|
|
82
83
|
"manifest.json": "ab06e307795bc9da5add926752b6c4dbdaf18625090261a035aa356a679c504f",
|
|
83
|
-
"release.mjs": "
|
|
84
|
-
"local-cli.mjs": "
|
|
84
|
+
"release.mjs": "5a84da222a179cdc3b6be5db2a4a1c48085de27dff378b7598eb725f0627c98b",
|
|
85
|
+
"local-cli.mjs": "aa4c217c5029408020f76d6c0c5dafc9a0f51216085b2624f3b6aca3cffbe555",
|
|
85
86
|
"local-state-lock.mjs": "1dbf64264642a0851faf043e07ee9f66d2ad0508e9f799b0626246e42fae893e",
|
|
86
|
-
"local-execution.mjs": "
|
|
87
|
+
"local-execution.mjs": "46fddd78cd954fb080c660affe90cdaa716695580427336acb820089441a8025",
|
|
87
88
|
"local-integrity.mjs": "e6d6eace80c610f3c7299d4f8644b0dc023aa9e3d8d64b26535ea7fbced248f1",
|
|
88
89
|
"local-evidence-verification.mjs": "9b7dea31777505272f63616fd62af7603338f703bb1285fc565d6451715c7ce0",
|
|
89
90
|
"local-formula.mjs": "706d5ec0cb2caf95a547531a81ca746fe246d4c53d902e03595a3b360e8c85af",
|
|
@@ -114,10 +115,11 @@
|
|
|
114
115
|
"local-budget-store.mjs": "2ae36c977ecd32d81f88e2eaa697ce8c228cb4405ecc5c4169cdda104c8ce138",
|
|
115
116
|
"local-budget-transition.mjs": "3894f05d4b5d4d6e16e3f201ff8902e6311cf6434010262934e0d1251aa8b9f0",
|
|
116
117
|
"local-budget.mjs": "e4ba1b4873a7625f9ff3c43e932b7289e7f95468c9dd88c2aeddd1242d7ea415",
|
|
118
|
+
"local-flow-lifecycle.mjs": "47534e16599002e5e7cf7d3e95195f4d7a364d2b651ec339d8c6c017d71c1b05",
|
|
117
119
|
"local-scope-candidate.mjs": "8f0bd9606e70d4a0ebacdd3cd600ec3afed85e2bb3e1760aa1144cf55d4e028a",
|
|
118
120
|
"local-scope-context.mjs": "79753abfc6c76089d17f003a1be537dfaea10d6731c542f7fc53f2a9996ed164",
|
|
119
|
-
"local-scope-files.mjs": "
|
|
120
|
-
"local-scope-repository.mjs": "
|
|
121
|
+
"local-scope-files.mjs": "96c301f2a45e5fa22f0b11fb15e5e564f118e53187fc7749263c78d5ed87b056",
|
|
122
|
+
"local-scope-repository.mjs": "0c955612d2f737e8661af84c1efb6e9e7fe6bb9ec1dfadd7776c61be177261f2",
|
|
121
123
|
"local-scope-git.mjs": "53bbbb6c4795db711b5bc164591a6ad79a561427da046d6bbe997dae6eb82829",
|
|
122
124
|
"local-scope-git-reads.mjs": "5d8a2636bab955cb325cb693db7423e69512355887ae5b5af3b8cb615f5ddad0",
|
|
123
125
|
"local-git-executable.mjs": "9391d716529025b1de3ee006df658a9d765b6ef29ad24ef9b7436829318e651e",
|
|
@@ -142,12 +144,12 @@
|
|
|
142
144
|
"runtime/git-native-linux-v1.mjs": "cf5cb91426a105e62c0f07a8a230dec592dba46f85a3f708b4c4e8cfcd1126c6",
|
|
143
145
|
"runtime/git-native-darwin-v1.mjs": "f88aab72dc86f94eb1115784872957cb8ed0f74205ae70e1ecd1939e7790e310",
|
|
144
146
|
"local-scope-signature.mjs": "bd05e8cf9e23f9511a16844974362297b9e9d11930ff7b8ad2a490e8d7660fe9",
|
|
145
|
-
"local-scope-config-imports.mjs": "
|
|
147
|
+
"local-scope-config-imports.mjs": "53e510609066355318d886cd96f73a0fc8005a28857f6ab4eb6cb81c4857945b",
|
|
146
148
|
"local-scope-snapshot.mjs": "c0f851470b98863a1d187825ed61ee6072d1c057b178fdc85ae29af02a3b250f",
|
|
147
149
|
"local-scope-state.mjs": "8b600a91fc0068d7a8c0bb752ddba7ed30ada93daafb118d6cba2368fd94cb27",
|
|
148
150
|
"local-scope-workspaces.mjs": "a44c34537e5ed2dcaf193a985e5bc05957989fbb160a650f9e37ba7d66eb2de8",
|
|
149
|
-
"local-task-routing.mjs": "
|
|
150
|
-
"remote-client.mjs": "
|
|
151
|
+
"local-task-routing.mjs": "6e2e506b2fb0e2e8d8c5147d7df32849b57a4e94a23634f01a355d45735858e4",
|
|
152
|
+
"remote-client.mjs": "9e4ef758fb7d83e30e82dca68623740e7848cd5a527b7a90846e125d00df0a12",
|
|
151
153
|
"cloud-task-client.mjs": "11acb1a033c94cdfbdfb177a3402ad97f6a5226f577138c8cf6395dc3ebb1a39",
|
|
152
154
|
"scope-runner-client.mjs": "646ab4f80d4a8209e922fd45f40efd968f0744d9dfc03188b222c1a2e11e83b8",
|
|
153
155
|
"native-client.mjs": "6f5575909bae9daba643287f876ffa0f9ff1c53a367b32a40145d740456d6605",
|
|
@@ -180,7 +182,7 @@
|
|
|
180
182
|
"workflow-local-request.mjs": "4e24c543ce386292a87dc9cd41f90d23375f12079cacffc515c8cd01fd571780",
|
|
181
183
|
"task-message-entry.mjs": "31d10056cb905237cd5649e8eb067009ff1612ec5dc1061e0373209fbc0b646a",
|
|
182
184
|
"task-message-cli.mjs": "3ca65e96c28129b3bd5cac962d106e0859a1f17df1f6a66ee543c6efc22f918d",
|
|
183
|
-
"local-contracts.mjs": "
|
|
185
|
+
"local-contracts.mjs": "d47022162233ee0b840bfd808b9d12590f4a367133ef0999233b7f76d76daa43",
|
|
184
186
|
"local-contract-types.mjs": "85e71fa4dd67e351a4b67573c740bcdb1661cede53f097189f11837771e75e91",
|
|
185
187
|
"local-archguard-ast.mjs": "7ff7df9dab0d98ed62983add751c80edea598bc94ef4f38ede0ebd4d41226c87",
|
|
186
188
|
"local-archguard-checkpoint.mjs": "37896902520cc995720c96288e4a5ed3585952b55c9c35be281f91fa754c71ec",
|
package/release.mjs
CHANGED
|
@@ -60,7 +60,7 @@ const sourceFiles = [
|
|
|
60
60
|
'local-budget-grants.mjs', 'local-budget-handoff.mjs', 'local-budget-handoff-contracts.mjs', 'local-budget-lock.mjs',
|
|
61
61
|
'local-budget-handoff-completion.mjs',
|
|
62
62
|
'local-budget-reads.mjs', 'local-budget-source-bindings.mjs', 'local-budget-store.mjs',
|
|
63
|
-
'local-budget-transition.mjs', 'local-budget.mjs',
|
|
63
|
+
'local-budget-transition.mjs', 'local-budget.mjs', 'local-flow-lifecycle.mjs',
|
|
64
64
|
'local-scope-candidate.mjs',
|
|
65
65
|
'local-scope-context.mjs',
|
|
66
66
|
'local-scope-files.mjs',
|
|
@@ -328,11 +328,13 @@ function releaseLabels() {
|
|
|
328
328
|
'local-budget-store.mjs': { zh: '预算追加台账和完整性校验', en: 'budget append-only journal and integrity checks' },
|
|
329
329
|
'local-budget-transition.mjs': { zh: '候选范围与正式范围预算转换', en: 'candidate and accepted scope budget transition' },
|
|
330
330
|
'local-budget.mjs': { zh: '本地三轴读取预算', en: 'local three-axis read budgets' },
|
|
331
|
+
'local-flow-lifecycle.mjs': { zh: 'Flow 终止与任务解绑', en: 'flow termination and task unbinding' },
|
|
331
332
|
'local-probe.mjs': { zh: '范围探测与分档', en: 'scope probing and tiering' },
|
|
332
333
|
'local-scope-candidate.mjs': { zh: '临时接管范围独立验签', en: 'temporary takeover scope verification' },
|
|
333
334
|
'local-scope-context.mjs': { zh: '精确符号范围绑定', en: 'exact symbol scope binding' },
|
|
334
335
|
'local-scope-files.mjs': { zh: '范围文件与安全读取', en: 'scope files and guarded reads' },
|
|
335
336
|
'local-scope-repository.mjs': { zh: '真实仓库范围事实', en: 'actual repository scope facts' },
|
|
337
|
+
'local-scope-config-imports.mjs': { zh: 'tsconfig 范围路径与依赖只读引用', en: 'tsconfig scope paths and read-only dependency references' },
|
|
336
338
|
'local-scope-git.mjs': { zh: '实际Git范围与变更事实', en: 'actual Git scope and change facts' },
|
|
337
339
|
'local-scope-git-reads.mjs': { zh: '固定Git元数据与源码读取', en: 'fixed Git metadata and source reads' },
|
|
338
340
|
'local-scope-git-comparison.mjs': { zh: 'Git范围比较接线', en: 'Git scope comparison wiring' },
|
package/releases.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.1.40",
|
|
4
|
+
"date": "2026-10-15",
|
|
5
|
+
"zh": "修复 flow 终止与会话解绑、tsconfig 授权路径解析,并加入离线 unlock 自救入口。",
|
|
6
|
+
"en": "Fixed flow termination and session unbinding, tsconfig authorized-path resolution, and added an offline unlock recovery command."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.1.39",
|
|
4
10
|
"date": "2026-10-14",
|
package/remote-client.mjs
CHANGED
|
@@ -34,7 +34,7 @@ function discoveredAjv() {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function buildInvocation(operation, input) {
|
|
37
|
-
if (!['capabilities', 'task-describe', 'message-route', 'message-accept', 'task-resume', 'task-checkpoint',
|
|
37
|
+
if (!['capabilities', 'task-describe', 'message-route', 'message-accept', 'task-resume', 'task-checkpoint', 'flow-cancel', 'flow-abandon',
|
|
38
38
|
'message-status', 'message-delivery-start', 'message-delivery-report', 'message-resolve', 'message-complete',
|
|
39
39
|
'handoff-prepare', 'handoff-release', 'handoff-claim', 'handoff-status', 'handoff-resume', 'handoff-execution-authority', 'mutation-snapshot-challenge', 'mutation-pass-issue',
|
|
40
40
|
'scope-runner-prepare', 'scope-runner-register', 'scope-runner-challenge', 'scope-runner-attest',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const ide = ['ide-code-mode.mjs', 'ide-command.mjs', 'ide-hook.mjs', 'ide-install.mjs', 'ide-session.mjs', 'ide-results.mjs', 'ide-queue.mjs'];
|
|
1
|
+
const ide = ['ide-code-mode.mjs', 'ide-command.mjs', 'ide-hook.mjs', 'ide-install.mjs', 'ide-session.mjs', 'break-glass.mjs', 'ide-results.mjs', 'ide-queue.mjs'];
|
|
2
2
|
const discipline = ['local-discipline-actions.mjs', 'local-discipline-analysis.mjs', 'local-discipline-batch.mjs',
|
|
3
3
|
'local-discipline-contracts.mjs', 'local-discipline-contracts.d.mts', 'local-discipline-gate.mjs',
|
|
4
4
|
'local-discipline-native.mjs', 'local-discipline-operations.mjs', 'local-discipline-repository.mjs',
|