@entrorove/skill 0.1.38 → 0.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
- await writeIdeJson(file, { ...state, observedAt: new Date().toISOString() });
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,
@@ -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 };
@@ -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 resolved = path.posix.normalize(path.posix.join(directory, literal(value, 'path'))).replace(/\/+$/, '');
14
- if (resolved === '..' || resolved.startsWith('../') || resolved.startsWith('/')
15
- || resolved.split('/').some(part => ['node_modules', '.git', '.entrorove', '.aimlock', '.archguard'].includes(part))) {
16
- throw new Error('Scope tsconfig path escapes the authorized repository');
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('Scope tsconfig mapping requires a literal candidate within the repository');
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, literal(own.baseUrl, 'baseUrl'), true) }),
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)
@@ -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
- export async function scopeTarget(root, relative, allowMissing = false) {
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() || (index < parts.length - 1 && !stat.isDirectory())) scopeFail('ENTROROVE_FILE_INVALID', 'scope refuses symlink or non-directory ancestors');
37
- if (index === parts.length - 1) return { target: current, exists: true, stat };
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');
@@ -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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "entrorove.skill-manifest/1.0",
3
3
  "name": "entrorove",
4
- "version": "0.1.38",
4
+ "version": "0.1.40",
5
5
  "endpoint": "https://entrorove.com",
6
6
  "requestSchemaVersion": "entrorove.skill.request/2.0",
7
7
  "supportedRequestSchemaVersions": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entrorove/skill",
3
- "version": "0.1.38",
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,7 +204,9 @@
203
204
  "ide-hook.mjs",
204
205
  "ide-install.mjs",
205
206
  "ide-session.mjs",
207
+ "break-glass.mjs",
206
208
  "ide-results.mjs",
209
+ "ide-queue.mjs",
207
210
  "local-discipline-actions.mjs",
208
211
  "local-discipline-analysis.mjs",
209
212
  "local-discipline-batch.mjs",
@@ -1,14 +1,16 @@
1
1
  {
2
2
  "schemaVersion": "entrorove.release-state/1.0",
3
- "version": "0.1.38",
4
- "digest": "ccce063efbb70446cec357e3377a4947e9c9d2068c443cc33fba9050d074a1ba",
3
+ "version": "0.1.40",
4
+ "digest": "4175f36123295cacefbcd09150116696d7528716ba1541256cf345ec3f31a2f3",
5
5
  "files": {
6
6
  "ide-code-mode.mjs": "cd5318342aac23ba9719ad760b7d7b8018d31839df1bed4ed7142c2782e22def",
7
- "ide-command.mjs": "06af72622b65dcc750da59e532399d7c711d95fc39f3d4282c68440e2edd35fd",
8
- "ide-hook.mjs": "fbee62c9b1130674cfe19c65b2adeac664a702a0d3233b943157fd78f04d446b",
9
- "ide-install.mjs": "13e202ba87feed2fe92b237519118381125abfacc62ad9f9e048bae3bd4c6ea0",
10
- "ide-session.mjs": "bbe68b077e8854af36e4514ec6747bdeff8a10513d5169e6fa111ce8f95bcb88",
11
- "ide-results.mjs": "8677b062bfe44b1683ae2508ad1048b93030a0840e8ccc1fa1c5e587ab43c7a3",
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",
13
+ "ide-queue.mjs": "806e716c775fd0079417027b135977c8516f47dddb0bc5ca19e32a355dab0e9e",
12
14
  "local-discipline-actions.mjs": "2379c08d0f49c44068f8239db7e514d75885f4606b1b88a7a36e4ff19167caca",
13
15
  "local-discipline-analysis.mjs": "ebea932438a53a157d5d07a5d68366496eee121a2c67cecfb4fdb2d5aa9763eb",
14
16
  "local-discipline-batch.mjs": "2cc90dbae55efdee0c445da399c0a9acafa92e915344c25208ebca554fc4c9e9",
@@ -27,9 +29,9 @@
27
29
  "runtime/validator-runtime-v7.0.44.d.mts": "7797edbfe2f7de24da370204dce4dfd22822a1bd80df93514fff900b98afe36f",
28
30
  "snapshot-retention.mjs": "4f8919fe8ec49fb4c23e080a8ad2fa3a9282bca3fffe7cf55c64e89ef316697e",
29
31
  "snapshot-cleanup-journal.mjs": "76a9684cc95b4eb076e1298d78a9dff37948b00d9607b8f546c599072d3ed331",
30
- "IDE-EXECUTION.md": "4a84684547d79a537fee14fafab4bd71aa7cdabf6b1e0a5ebdd78c500e98e783",
32
+ "IDE-EXECUTION.md": "fe1c65819749fdd7cad4663eab7d11e6e566ef623dc30352e47effe0bd1263d9",
31
33
  "REUSE-DISCIPLINE.md": "295ecd41c931b00c21d009ab65ac160f0e2ab81b4000ed498d2e8e97c2dc6c63",
32
- "reuse-discipline-release.mjs": "bfe852da7f47bdbcf93afc809278fb27ec495a704995f61f64463e18bcf4d6b9",
34
+ "reuse-discipline-release.mjs": "20ded9c21bd71b8779c454f5eba4c7c0a943ef99d94117b9bbef891d2ab6e3de",
33
35
  "local-final-gate16.mjs": "b30caab08441449ad556a6e9a3cefbf91190f0b3f7370f46d2a21156375aa8e3",
34
36
  "local-final-gate16-checks.mjs": "1dd30e9666d458d8b12ca6e4975f6f0ac8450c3ea9cbecc7401ae037f66b589a",
35
37
  "local-final-gate16-worker.mjs": "344d64b6c1c5552b96960e0d8514a876a3dbfa9762ab9eb749038e8351727121",
@@ -71,18 +73,18 @@
71
73
  "local-autocoord17-waits.mjs": "2b0a713987822548a94e1ed3a9a891532db45383797dfc5d5bc8d1e35593cf25",
72
74
  "local-validator-isolation17.mjs": "48e94604e28b8fddd567dfb35ff60b73fd094a5803bd49f076388b7f310f92aa",
73
75
  "local-calctool-composition17.mjs": "ac89fcb41ab0d0d9b47c1b5696945281b0894eb064820775e36a011c76314e6a",
74
- "cli.mjs": "891ad9947fe5b9a83a236feead03f4ce94e3c0245428fd048a96de9e0e6251af",
76
+ "cli.mjs": "bb99f5a52c770ded055fe27a35b3067ac348441b9488df336e6bf751b803e459",
75
77
  "review-client.mjs": "3491eef68ed533174b31db3c53febb6519b8f088f71a9964644714a9a4c03c77",
76
78
  "version.mjs": "1f392147d72c40af4b03b307ea607075fb815af5174fa07e254b95fe12dbf3e7",
77
- "SKILL.md": "0ee5ae2b409a895bd66966812d60c97898ed0294b12d389ed737c271233a2bc4",
78
- "CAPABILITY-MAP.md": "93d3a45034d3cb2de804b4696a0ef3977a46f7d438adcef19ac443e90db48f44",
79
+ "SKILL.md": "d89c2a83a080a217d67d75fa0251b0f054b92e81a9e40a6c0b52c7ecf3d79099",
80
+ "CAPABILITY-MAP.md": "a0f0468a098725ced52a96dd8e22bfc7d1ce7681a8446ebf7dae3696c76c3385",
79
81
  "AGREEMENT.md": "f93a68e92c457200dd024610e65f74e478b8f60860f951da818b70561f1b9bf8",
80
- "package.json": "cd314c3ac62c1a4f63511c4e9cc49e9c9aee8e2d012c2a2f04a2ff541f43f40e",
82
+ "package.json": "a522540d3967d9348a37c6538c1abb39fe6d50872aa8f7f90b85e920590091c7",
81
83
  "manifest.json": "ab06e307795bc9da5add926752b6c4dbdaf18625090261a035aa356a679c504f",
82
- "release.mjs": "203e367ed6d7a406937c358c87f5e5c43d2055ceedda4ca882711121a53c28ec",
83
- "local-cli.mjs": "ee6319e2027689e0a4f511c8790fa6e606e520e2f3f30be5031a2e055231de4b",
84
+ "release.mjs": "5a84da222a179cdc3b6be5db2a4a1c48085de27dff378b7598eb725f0627c98b",
85
+ "local-cli.mjs": "aa4c217c5029408020f76d6c0c5dafc9a0f51216085b2624f3b6aca3cffbe555",
84
86
  "local-state-lock.mjs": "1dbf64264642a0851faf043e07ee9f66d2ad0508e9f799b0626246e42fae893e",
85
- "local-execution.mjs": "3d7db6576d902e2a5dea2045c7b78c5453f698a49f4264aacb3db8e38461d5ba",
87
+ "local-execution.mjs": "46fddd78cd954fb080c660affe90cdaa716695580427336acb820089441a8025",
86
88
  "local-integrity.mjs": "e6d6eace80c610f3c7299d4f8644b0dc023aa9e3d8d64b26535ea7fbced248f1",
87
89
  "local-evidence-verification.mjs": "9b7dea31777505272f63616fd62af7603338f703bb1285fc565d6451715c7ce0",
88
90
  "local-formula.mjs": "706d5ec0cb2caf95a547531a81ca746fe246d4c53d902e03595a3b360e8c85af",
@@ -113,10 +115,11 @@
113
115
  "local-budget-store.mjs": "2ae36c977ecd32d81f88e2eaa697ce8c228cb4405ecc5c4169cdda104c8ce138",
114
116
  "local-budget-transition.mjs": "3894f05d4b5d4d6e16e3f201ff8902e6311cf6434010262934e0d1251aa8b9f0",
115
117
  "local-budget.mjs": "e4ba1b4873a7625f9ff3c43e932b7289e7f95468c9dd88c2aeddd1242d7ea415",
118
+ "local-flow-lifecycle.mjs": "47534e16599002e5e7cf7d3e95195f4d7a364d2b651ec339d8c6c017d71c1b05",
116
119
  "local-scope-candidate.mjs": "8f0bd9606e70d4a0ebacdd3cd600ec3afed85e2bb3e1760aa1144cf55d4e028a",
117
120
  "local-scope-context.mjs": "79753abfc6c76089d17f003a1be537dfaea10d6731c542f7fc53f2a9996ed164",
118
- "local-scope-files.mjs": "0671ac4baef610a55ea6c1db6ebca7a4b6cfda45edb8aeaa828019e2a26f5a76",
119
- "local-scope-repository.mjs": "957fa72afacc148d0251a3081c387467922e63b9e939f40751d63eb6b09b427c",
121
+ "local-scope-files.mjs": "96c301f2a45e5fa22f0b11fb15e5e564f118e53187fc7749263c78d5ed87b056",
122
+ "local-scope-repository.mjs": "0c955612d2f737e8661af84c1efb6e9e7fe6bb9ec1dfadd7776c61be177261f2",
120
123
  "local-scope-git.mjs": "53bbbb6c4795db711b5bc164591a6ad79a561427da046d6bbe997dae6eb82829",
121
124
  "local-scope-git-reads.mjs": "5d8a2636bab955cb325cb693db7423e69512355887ae5b5af3b8cb615f5ddad0",
122
125
  "local-git-executable.mjs": "9391d716529025b1de3ee006df658a9d765b6ef29ad24ef9b7436829318e651e",
@@ -141,12 +144,12 @@
141
144
  "runtime/git-native-linux-v1.mjs": "cf5cb91426a105e62c0f07a8a230dec592dba46f85a3f708b4c4e8cfcd1126c6",
142
145
  "runtime/git-native-darwin-v1.mjs": "f88aab72dc86f94eb1115784872957cb8ed0f74205ae70e1ecd1939e7790e310",
143
146
  "local-scope-signature.mjs": "bd05e8cf9e23f9511a16844974362297b9e9d11930ff7b8ad2a490e8d7660fe9",
144
- "local-scope-config-imports.mjs": "16053142410dd647f13360f29fb3680c3e3a3ebc38019e43af6db05896d92a33",
147
+ "local-scope-config-imports.mjs": "53e510609066355318d886cd96f73a0fc8005a28857f6ab4eb6cb81c4857945b",
145
148
  "local-scope-snapshot.mjs": "c0f851470b98863a1d187825ed61ee6072d1c057b178fdc85ae29af02a3b250f",
146
149
  "local-scope-state.mjs": "8b600a91fc0068d7a8c0bb752ddba7ed30ada93daafb118d6cba2368fd94cb27",
147
150
  "local-scope-workspaces.mjs": "a44c34537e5ed2dcaf193a985e5bc05957989fbb160a650f9e37ba7d66eb2de8",
148
- "local-task-routing.mjs": "e26d6f1ba134ffe1da2a49b4fb6bb6dfa5617671f774a08e719d04f202cf5eca",
149
- "remote-client.mjs": "1d99cf5bfdcad0203e2550a4c1deb6c2811b5a42d702ff9b73bf7b0ded6f72d1",
151
+ "local-task-routing.mjs": "6e2e506b2fb0e2e8d8c5147d7df32849b57a4e94a23634f01a355d45735858e4",
152
+ "remote-client.mjs": "9e4ef758fb7d83e30e82dca68623740e7848cd5a527b7a90846e125d00df0a12",
150
153
  "cloud-task-client.mjs": "11acb1a033c94cdfbdfb177a3402ad97f6a5226f577138c8cf6395dc3ebb1a39",
151
154
  "scope-runner-client.mjs": "646ab4f80d4a8209e922fd45f40efd968f0744d9dfc03188b222c1a2e11e83b8",
152
155
  "native-client.mjs": "6f5575909bae9daba643287f876ffa0f9ff1c53a367b32a40145d740456d6605",
@@ -179,7 +182,7 @@
179
182
  "workflow-local-request.mjs": "4e24c543ce386292a87dc9cd41f90d23375f12079cacffc515c8cd01fd571780",
180
183
  "task-message-entry.mjs": "31d10056cb905237cd5649e8eb067009ff1612ec5dc1061e0373209fbc0b646a",
181
184
  "task-message-cli.mjs": "3ca65e96c28129b3bd5cac962d106e0859a1f17df1f6a66ee543c6efc22f918d",
182
- "local-contracts.mjs": "f7c5eff49b6efa05857f6be8e37579b91a00d5ab79f8b8f5cceea5e935ddd2cf",
185
+ "local-contracts.mjs": "d47022162233ee0b840bfd808b9d12590f4a367133ef0999233b7f76d76daa43",
183
186
  "local-contract-types.mjs": "85e71fa4dd67e351a4b67573c740bcdb1661cede53f097189f11837771e75e91",
184
187
  "local-archguard-ast.mjs": "7ff7df9dab0d98ed62983add751c80edea598bc94ef4f38ede0ebd4d41226c87",
185
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,16 @@
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
+ },
8
+ {
9
+ "version": "0.1.39",
10
+ "date": "2026-10-14",
11
+ "zh": "增加受控需求队列,支持按优先级排队或在安全检查点插入;插入任务完成并回传真实确认后自动恢复原任务与原编号。",
12
+ "en": "Added a controlled requirement queue with priority ordering and checkpointed interruption; after verified acknowledged completion, the original task and identifiers resume automatically."
13
+ },
2
14
  {
3
15
  "version": "0.1.38",
4
16
  "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'];
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',