@entrorove/skill 0.1.24 → 0.1.28

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.
Files changed (50) hide show
  1. package/CAPABILITY-MAP.md +73 -15
  2. package/CHANGELOG.md +20 -0
  3. package/SKILL.md +11 -1
  4. package/calctool-runtime-entry.mjs +2 -2
  5. package/cli.mjs +18 -1
  6. package/local-archguard-checkpoint.mjs +5 -2
  7. package/local-archguard-snapshot.mjs +18 -13
  8. package/local-autocoord17-authority.mjs +36 -0
  9. package/local-autocoord17-controller.mjs +164 -0
  10. package/local-autocoord17-peers.mjs +111 -0
  11. package/local-autocoord17-store.mjs +110 -0
  12. package/local-autocoord17-tasks.mjs +68 -0
  13. package/local-autocoord17-waits.mjs +118 -0
  14. package/local-calctool-cli.mjs +3 -1
  15. package/local-calctool-composition17.mjs +50 -0
  16. package/local-calctool-runtime.mjs +1 -1
  17. package/local-calctool.mjs +1 -1
  18. package/local-chain-bindings.mjs +93 -0
  19. package/local-chain-broker.mjs +61 -0
  20. package/local-chain-contracts.mjs +21 -0
  21. package/local-chain-controller.mjs +321 -0
  22. package/local-chain-human.mjs +48 -0
  23. package/local-chain-lock.mjs +54 -0
  24. package/local-chain-operations.mjs +7 -0
  25. package/local-chain-plan.mjs +232 -0
  26. package/local-chain-release-labels.mjs +12 -0
  27. package/local-chain-results.mjs +75 -0
  28. package/local-cli.mjs +23 -15
  29. package/local-contract-types.mjs +1 -1
  30. package/local-contracts.mjs +2 -0
  31. package/local-error.mjs +23 -0
  32. package/local-execution.mjs +1 -1
  33. package/local-final-gate16-bundle.mjs +2 -2
  34. package/local-formula-ast.mjs +23 -14
  35. package/local-formula-engine.mjs +17 -3
  36. package/local-formula.mjs +1 -1
  37. package/local-invocations.mjs +35 -0
  38. package/local-ledger.mjs +13 -2
  39. package/local-mutation-authority.mjs +13 -6
  40. package/local-mutation-binding.mjs +5 -2
  41. package/local-mutation-files.mjs +29 -4
  42. package/local-task-handoff-observation.mjs +4 -1
  43. package/local-validator-isolation17.mjs +256 -0
  44. package/manifest.json +1 -1
  45. package/package.json +22 -2
  46. package/release-state.json +48 -29
  47. package/release.mjs +22 -1
  48. package/releases.json +24 -0
  49. package/runtime/calctool-runtime-v7.0.43.mjs +40 -17
  50. package/workflow-local-request.mjs +3 -1
@@ -0,0 +1,111 @@
1
+ import { canonical, sha256 } from './local-integrity.mjs';
2
+ import { canonicalSwarmPaths, SWARM_PRIORITIES, swarmPathsOverlap } from './local-swarm-state.mjs';
3
+ import { readSwarmOwnership, swarmRepositoryStorage, validateSwarmOwnership } from './local-swarm-storage.mjs';
4
+ import { readSwarmWriteIntentHistory } from './local-swarm-priority.mjs';
5
+ import { coordDuration17, coordExact17, coordFail17, coordId17, coordMessage17, coordEvent17 } from './local-autocoord17-store.mjs';
6
+ import { coordNative17 } from './local-autocoord17-authority.mjs';
7
+
8
+ export async function physicalPeers17(task) {
9
+ const authority = await coordNative17(task.material, false), storage = await swarmRepositoryStorage(authority, 'read');
10
+ const index = await validateSwarmOwnership(await readSwarmOwnership(storage, authority, 'read'), authority);
11
+ return (await readSwarmWriteIntentHistory(index, authority)).index;
12
+ }
13
+ function earlier(left, right) {
14
+ const difference = SWARM_PRIORITIES.indexOf(left.priority) - SWARM_PRIORITIES.indexOf(right.priority);
15
+ return difference > 0 || difference === 0 && left.sequence < right.sequence;
16
+ }
17
+ export function registerIntent17(state, task, input) {
18
+ coordId17(input.intentId); coordDuration17(input.ttlMs); coordDuration17(input.coordinationTimeoutMs);
19
+ coordDuration17(input.fileLockTtlMs);
20
+ const paths = canonicalSwarmPaths(input.paths);
21
+ if (paths.some(target => !task.card.taskScope.includes(target)) || input.baselineHash !== task.card.baselineHash
22
+ || input.priority !== task.material.delegationGrant.priority) coordFail17('ENTROROVE_COORD_INTENT_BINDING', 'intent must retain original signed priority, paths and baseline');
23
+ const request = { intentId: input.intentId, taskId: task.taskId, agentId: task.agentId, chainId: task.chainId,
24
+ paths, baselineHash: input.baselineHash, ttlMs: input.ttlMs, coordinationTimeoutMs: input.coordinationTimeoutMs,
25
+ fileLockTtlMs: input.fileLockTtlMs, priority: input.priority };
26
+ const prior = state.intents.find(value => value.intentId === input.intentId);
27
+ if (prior) {
28
+ if (canonical(prior.request) !== canonical(request)) coordFail17('ENTROROVE_COORD_INTENT_CHANGED', 'intent ID cannot change its original content');
29
+ if (prior.status === 'expired') coordFail17('ENTROROVE_COORD_INTENT_EXPIRED', 'expired intent needs a new explicit identity');
30
+ return prior;
31
+ }
32
+ const now = Date.now(), expires = Math.min(now + input.ttlMs, Date.parse(task.material.delegationGrant.expiresAt));
33
+ const intent = { intentId: input.intentId, taskId: task.taskId, request, sequence: state.intents.length,
34
+ status: 'waiting', expiresAt: new Date(expires).toISOString(), deadlineAt: new Date(now + input.coordinationTimeoutMs).toISOString(),
35
+ readyPaths: [], blockedPaths: [], refetchPaths: [], spawnDepth: task.spawnOrigin ? 1 : 0, spawnRequestId: null };
36
+ state.intents.push(intent); return intent;
37
+ }
38
+ function spawnRequest(state, task, intent) {
39
+ if (intent.spawnDepth !== 0 || intent.spawnRequestId || !intent.blockedPaths.length) return;
40
+ const spawnRequestId = sha256(`entrorove.peer-spawn/1.0\0${intent.intentId}`);
41
+ intent.spawnRequestId = spawnRequestId;
42
+ const request = { schemaVersion: 'entrorove.peer-spawn-request/1.0', spawnRequestId, idempotencyKey: spawnRequestId,
43
+ status: 'pending', sourceTaskId: task.taskId, ownerId: task.material.delegationGrant.ownerId,
44
+ originalTask: structuredClone(task.originalTask), agentId: task.agentId, chainId: task.chainId,
45
+ paths: [...intent.blockedPaths], sourceIntentId: intent.intentId, spawnDepth: 1, spawnAllowed: false };
46
+ state.spawnRequests.push(request);
47
+ coordMessage17(state, task.taskId, 'peer-timeout', { spawnRequestId, paths: request.paths }, intent.intentId);
48
+ }
49
+ export function scanIntent17(state, task, intent, physical) {
50
+ const now = Date.now();
51
+ if (['completed', 'moved', 'expired'].includes(intent.status)) return peerView17(intent);
52
+ if (Date.parse(intent.expiresAt) <= now) {
53
+ intent.status = 'expired'; coordMessage17(state, task.taskId, 'peer-expired', { intentId: intent.intentId }, intent.intentId);
54
+ return peerView17(intent);
55
+ }
56
+ const ownGrant = sha256(canonical(task.material.delegationGrant));
57
+ const occupied = physical.records.filter(record => record.grantDigest !== ownGrant);
58
+ const nativeIntent = physical.intents.find(value => value.intentId === ownGrant);
59
+ const ahead = physical.intents.filter(value => value.status === 'waiting' && value.intentId !== ownGrant
60
+ && nativeIntent && earlier(value, nativeIntent));
61
+ const remaining = intent.request.paths.filter(target => !intent.movedPaths?.includes(target));
62
+ const blocked = remaining.filter(target => occupied.some(record => record.paths.some(scope => swarmPathsOverlap(scope, target)))
63
+ || ahead.some(value => value.paths.some(scope => swarmPathsOverlap(scope, target))));
64
+ const oldBlocked = intent.blockedPaths, ready = remaining.filter(target => !blocked.includes(target));
65
+ const refetch = oldBlocked.filter(target => !blocked.includes(target));
66
+ intent.blockedPaths = blocked; intent.readyPaths = ready;
67
+ intent.refetchPaths = [...new Set([...intent.refetchPaths, ...refetch])].sort();
68
+ intent.status = blocked.length ? ready.length ? 'partial' : 'waiting' : 'ready';
69
+ if (blocked.length) {
70
+ const urgent = intent.request.priority === 'urgent' && occupied.some(record => record.paths.some(scope => blocked.some(target => swarmPathsOverlap(scope, target))));
71
+ coordMessage17(state, task.taskId, 'peer-conflict', { intentId: intent.intentId, blockedPaths: blocked }, `${intent.intentId}:${canonical(blocked)}`);
72
+ for (const record of occupied.filter(record => record.paths.some(scope => blocked.some(target => swarmPathsOverlap(scope, target))))) {
73
+ const owner = state.tasks.find(value => value.material.flowId === record.flowId);
74
+ if (owner) coordMessage17(state, owner.taskId, 'peer-request', { intentId: intent.intentId, paths: blocked,
75
+ safeCheckpointRequested: urgent }, `${intent.intentId}:${record.grantDigest}`);
76
+ }
77
+ if (Date.parse(intent.deadlineAt) <= now) spawnRequest(state, task, intent);
78
+ }
79
+ if (refetch.length) {
80
+ coordMessage17(state, task.taskId, 'peer-ready', { intentId: intent.intentId, refetchPaths: refetch,
81
+ freshAimlockSnapshotRequired: true }, `${intent.intentId}:${canonical(refetch)}`);
82
+ coordEvent17(state, 'peer-ready', task.taskId, intent.intentId, { refetchPaths: refetch }, `${intent.intentId}:${canonical(refetch)}`);
83
+ }
84
+ return { ...peerView17(intent), safeCheckpointRequested: intent.request.priority === 'urgent' && blocked.length > 0 };
85
+ }
86
+ export function peerView17(intent) {
87
+ return { schemaVersion: 'entrorove.local-autocoord/1.0', intentId: intent.intentId, status: intent.status,
88
+ readyPaths: [...intent.readyPaths], blockedPaths: [...intent.blockedPaths], refetchPaths: [...intent.refetchPaths],
89
+ freshAimlockSnapshotRequired: intent.refetchPaths.length > 0, writeAuthorized: false, fileLockGranted: false,
90
+ lockRequest: { paths: [...intent.readyPaths], ttlMs: intent.request.fileLockTtlMs },
91
+ spawnRequestId: intent.spawnRequestId, spawnDepth: intent.spawnDepth, spawnAllowed: intent.spawnDepth === 0 };
92
+ }
93
+ export function bindSpawn17(state, source, input) {
94
+ coordExact17(input, ['spawnRequestId', 'targetTaskId']);
95
+ const request = state.spawnRequests.find(value => value.spawnRequestId === input.spawnRequestId && value.sourceTaskId === source.taskId);
96
+ const target = state.tasks.find(value => value.taskId === input.targetTaskId);
97
+ if (!request || !target || target.taskId === source.taskId || target.material.flowId === source.material.flowId
98
+ || target.material.delegationGrant.ownerId !== request.ownerId || canonical(target.originalTask) !== canonical(request.originalTask)
99
+ || request.paths.some(targetPath => !target.card.taskScope.includes(targetPath))) coordFail17('ENTROROVE_COORD_SPAWN_BINDING', 'spawn binding requires the actual newly registered same-owner original task and exact pending paths');
100
+ if (request.status === 'bound') {
101
+ if (request.targetTaskId !== target.taskId) coordFail17('ENTROROVE_COORD_SPAWN_CHANGED', 'original spawn request cannot be rebound');
102
+ return request;
103
+ }
104
+ if (target.spawnOrigin) coordFail17('ENTROROVE_COORD_RECURSION', 'a derived task cannot receive another spawn lineage');
105
+ request.status = 'bound'; request.targetTaskId = target.taskId; target.spawnOrigin = request.spawnRequestId;
106
+ const intent = state.intents.find(value => value.intentId === request.sourceIntentId);
107
+ intent.movedPaths = [...request.paths]; intent.status = intent.readyPaths.length ? 'ready' : 'moved'; intent.blockedPaths = [];
108
+ coordMessage17(state, source.taskId, 'peer-spawned', { spawnRequestId: request.spawnRequestId,
109
+ targetTaskId: target.taskId, paths: request.paths }, request.spawnRequestId);
110
+ return request;
111
+ }
@@ -0,0 +1,110 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { withChainLock } from './local-chain-lock.mjs';
5
+ import { chainControlFile } from './local-chain-bindings.mjs';
6
+ import { canonical, sha256 } from './local-integrity.mjs';
7
+
8
+ export const AUTOCORD17_SCHEMA = 'entrorove.local-autocoord/1.0';
9
+ export function coordFail17(code, message) { throw Object.assign(new Error(message), { code }); }
10
+ export function coordExact17(value, required, optional = []) {
11
+ if (!value || typeof value !== 'object' || Array.isArray(value) || required.some(key => !Object.hasOwn(value, key))
12
+ || Object.keys(value).some(key => !required.includes(key) && !optional.includes(key))) {
13
+ coordFail17('ENTROROVE_COORD_FIELDS', 'coordination input has missing or unknown fields');
14
+ }
15
+ }
16
+ export function coordId17(value) {
17
+ if (typeof value !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) {
18
+ coordFail17('ENTROROVE_COORD_ID', 'coordination identity must be an explicit safe ID');
19
+ }
20
+ return value;
21
+ }
22
+ export function coordDuration17(value, maximum = 3_600_000) {
23
+ if (!Number.isSafeInteger(value) || value < 1 || value > maximum) coordFail17('ENTROROVE_COORD_DURATION', 'duration must be a positive bounded integer');
24
+ return value;
25
+ }
26
+ async function directory(value, privateMode) {
27
+ if (typeof value !== 'string' || !path.isAbsolute(value)) coordFail17('ENTROROVE_COORD_DIRECTORY', 'coordination directories must be explicit absolute paths');
28
+ const stat = await fs.lstat(value), actual = await fs.realpath(value);
29
+ if (!stat.isDirectory() || stat.isSymbolicLink() || actual !== path.resolve(value)
30
+ || typeof process.getuid === 'function' && stat.uid !== process.getuid()
31
+ || privateMode && (stat.mode & 0o077)) coordFail17('ENTROROVE_COORD_DIRECTORY', 'coordination directory must be owned, real and private');
32
+ return actual;
33
+ }
34
+ async function location(input, create) {
35
+ const repositoryRoot = await directory(input.repositoryRoot, false), coordinationHome = await directory(input.coordinationHome, true);
36
+ let current = coordinationHome;
37
+ for (const name of ['.entrorove', 'autocoord17', sha256(repositoryRoot).slice(0, 32)]) {
38
+ current = path.join(current, name);
39
+ if (create) await fs.mkdir(current, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST') throw error; });
40
+ await directory(current, true);
41
+ }
42
+ return { repositoryRoot, coordinationHome, file: path.join(current, 'journal.json') };
43
+ }
44
+ function initial(ctx) {
45
+ return { schemaVersion: AUTOCORD17_SCHEMA, repositoryRoot: ctx.repositoryRoot, coordinationHome: ctx.coordinationHome,
46
+ tasks: [], intents: [], waits: [], events: [], messages: [], decisions: [], spawnRequests: [], lockQueues: [] };
47
+ }
48
+ function checked(state, ctx) {
49
+ coordExact17(state, ['schemaVersion', 'repositoryRoot', 'coordinationHome', 'tasks', 'intents', 'waits', 'events',
50
+ 'messages', 'decisions', 'spawnRequests', 'lockQueues']);
51
+ if (state.schemaVersion !== AUTOCORD17_SCHEMA || state.repositoryRoot !== ctx.repositoryRoot || state.coordinationHome !== ctx.coordinationHome
52
+ || ['tasks', 'intents', 'waits', 'events', 'messages', 'decisions', 'spawnRequests', 'lockQueues'].some(key => !Array.isArray(state[key]) || state[key].length > 1024)) {
53
+ coordFail17('ENTROROVE_COORD_JOURNAL', 'coordination controls lost their original identity or bounds');
54
+ }
55
+ return state;
56
+ }
57
+ async function load(ctx, create) {
58
+ let journal;
59
+ try { const value = await chainControlFile(ctx.file); if (value.stat.mode & 0o077) throw new Error(); journal = JSON.parse(value.bytes.toString()); }
60
+ catch (error) { if (error.code !== 'ENOENT' || !create) throw error; journal = { schemaVersion: AUTOCORD17_SCHEMA, records: [] }; }
61
+ coordExact17(journal, ['schemaVersion', 'records']);
62
+ if (journal.schemaVersion !== AUTOCORD17_SCHEMA || !Array.isArray(journal.records) || journal.records.length > 1024) {
63
+ coordFail17('ENTROROVE_COORD_JOURNAL', 'coordination journal is invalid');
64
+ }
65
+ let head = null, state = initial(ctx);
66
+ for (const [index, record] of journal.records.entries()) {
67
+ coordExact17(record, ['sequence', 'event', 'at', 'previousDigest', 'state', 'digest']);
68
+ const { digest, ...core } = record;
69
+ if (record.sequence !== index + 1 || record.previousDigest !== head || sha256(canonical(core)) !== digest) {
70
+ coordFail17('ENTROROVE_COORD_JOURNAL', 'coordination journal lineage changed');
71
+ }
72
+ head = digest; state = checked(record.state, ctx);
73
+ }
74
+ return { ctx, journal, state: structuredClone(state), sequence: journal.records.length, head };
75
+ }
76
+ async function commit(loaded, event) {
77
+ checked(loaded.state, loaded.ctx);
78
+ const core = { sequence: loaded.sequence + 1, event, at: new Date().toISOString(), previousDigest: loaded.head, state: loaded.state };
79
+ const journal = { schemaVersion: AUTOCORD17_SCHEMA, records: [...loaded.journal.records, { ...core, digest: sha256(canonical(core)) }] };
80
+ const bytes = Buffer.from(canonical(journal) + '\n');
81
+ if (bytes.length > 8 * 1024 * 1024 || journal.records.length > 1024) coordFail17('ENTROROVE_COORD_JOURNAL_LIMIT', 'coordination journal needs explicit archival');
82
+ const temporary = `${loaded.ctx.file}.${randomUUID()}.tmp`;
83
+ const handle = await fs.open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
84
+ try { await handle.writeFile(bytes); await handle.sync(); } finally { await handle.close(); }
85
+ try { await fs.rename(temporary, loaded.ctx.file); } finally { await fs.rm(temporary, { force: true }); }
86
+ const directory = await fs.open(path.dirname(loaded.ctx.file), constants.O_RDONLY);
87
+ try { await directory.sync(); } finally { await directory.close(); }
88
+ loaded.sequence = core.sequence; loaded.head = sha256(canonical(core));
89
+ }
90
+ export async function withCoordination17(input, event, action, create = false) {
91
+ const ctx = await location(input, create);
92
+ return withChainLock(ctx.file, async () => {
93
+ const loaded = await load(ctx, create), before = canonical(loaded.state), result = await action(loaded);
94
+ if (canonical(loaded.state) !== before) await commit(loaded, event);
95
+ return { ...result, sequence: loaded.sequence, head: loaded.head };
96
+ });
97
+ }
98
+ export async function readCoordination17(input) { return load(await location(input, false), false); }
99
+ export function coordMessage17(state, taskId, type, detail, key) {
100
+ const messageId = sha256(`${type}\0${taskId}\0${key}`);
101
+ if (!state.messages.some(value => value.messageId === messageId)) state.messages.push({ messageId, taskId, type, detail, at: new Date().toISOString() });
102
+ return messageId;
103
+ }
104
+ export function coordEvent17(state, type, taskId, resource, detail, key) {
105
+ const eventId = sha256(`${type}\0${taskId}\0${key}`);
106
+ const existing = state.events.find(value => value.eventId === eventId);
107
+ if (existing && canonical(existing.detail) !== canonical(detail)) coordFail17('ENTROROVE_COORD_EVENT_CHANGED', 'same event identity cannot change its content');
108
+ if (!existing) state.events.push({ eventId, type, taskId, resource, detail, at: new Date().toISOString() });
109
+ return existing ?? state.events.at(-1);
110
+ }
@@ -0,0 +1,68 @@
1
+ import { canonical } from './local-integrity.mjs';
2
+ import { canonicalSwarmPaths, swarmPathsOverlap } from './local-swarm-state.mjs';
3
+ import { coordDuration17, coordExact17, coordFail17, coordId17, coordMessage17 } from './local-autocoord17-store.mjs';
4
+
5
+ export function taskCard17(value, native) {
6
+ coordExact17(value, ['taskScope', 'plannedActions', 'deployTarget', 'eta', 'baselineHash', 'archConstraints']);
7
+ const taskScope = canonicalSwarmPaths(value.taskScope);
8
+ if (taskScope.some(target => !native.paths.includes(target)) || value.baselineHash !== native.grant.frozenManifestDigest
9
+ || !Array.isArray(value.plannedActions) || !value.plannedActions.length || value.plannedActions.some(item => typeof item !== 'string' || !item || item.length > 500)
10
+ || value.deployTarget !== null && (typeof value.deployTarget !== 'string' || !value.deployTarget || value.deployTarget.length > 500)
11
+ || !Number.isFinite(Date.parse(value.eta)) || !Array.isArray(value.archConstraints)
12
+ || value.archConstraints.some(item => typeof item !== 'string' || !/^(?:forbid-scope:[^\s]+|requirement:[^=\s]+=[^\s]+)$/.test(item))) {
13
+ coordFail17('ENTROROVE_COORD_TASKCARD', 'taskcard must preserve the original signed scope, baseline and explicit constraints');
14
+ }
15
+ return { ...structuredClone(value), taskScope };
16
+ }
17
+ export function taskConflicts17(state, task) {
18
+ return state.tasks.filter(other => other.taskId !== task.taskId).flatMap(other => {
19
+ const reasons = [];
20
+ if (other.card.taskScope.some(scope => task.card.taskScope.some(target => swarmPathsOverlap(scope, target)))) reasons.push('scope-overlap');
21
+ if (task.card.deployTarget && task.card.deployTarget === other.card.deployTarget) reasons.push('deploy-target');
22
+ for (const [left, right] of [[task, other], [other, task]]) for (const constraint of left.card.archConstraints) {
23
+ if (constraint.startsWith('forbid-scope:') && right.card.taskScope.some(target => swarmPathsOverlap(constraint.slice(13), target))) reasons.push('architecture-scope');
24
+ if (constraint.startsWith('requirement:')) {
25
+ const prefix = constraint.slice(0, constraint.indexOf('=') + 1);
26
+ if (right.card.archConstraints.some(value => value.startsWith(prefix) && value !== constraint)) reasons.push('decision-conflict');
27
+ }
28
+ }
29
+ return reasons.length ? [{ taskId: other.taskId, reasons: [...new Set(reasons)] }] : [];
30
+ });
31
+ }
32
+ export function registerTask17(state, input, native, cloud) {
33
+ coordId17(input.taskId); coordId17(input.agentId); coordId17(input.chainId);
34
+ if (input.taskId !== native.grant.taskId || input.chainId !== native.step.executionPlan.chainId
35
+ || input.material.repositoryRoot !== state.repositoryRoot) coordFail17('ENTROROVE_COORD_TASK_BINDING', 'taskcard changed its original signed task, chain or repository');
36
+ const card = taskCard17(input.card, native), task = { taskId: input.taskId, agentId: input.agentId, chainId: input.chainId,
37
+ material: structuredClone(input.material), card, ...cloud, registeredAt: new Date().toISOString() };
38
+ const existing = state.tasks.find(value => value.taskId === task.taskId);
39
+ if (existing) {
40
+ if (canonical({ ...existing, registeredAt: null }) !== canonical({ ...task, registeredAt: null })) coordFail17('ENTROROVE_COORD_TASK_CHANGED', 'same taskcard cannot change its original identity or contents');
41
+ return existing;
42
+ }
43
+ state.tasks.push(task);
44
+ for (const conflict of taskConflicts17(state, task)) for (const target of [task.taskId, conflict.taskId]) {
45
+ coordMessage17(state, target, 'task-conflict', { otherTaskId: target === task.taskId ? conflict.taskId : task.taskId,
46
+ reasons: conflict.reasons }, [task.taskId, conflict.taskId].sort().join(':'));
47
+ }
48
+ return task;
49
+ }
50
+ export function enqueueResource17(state, task, input) {
51
+ coordId17(input.queueId); coordId17(input.resource); coordDuration17(input.queueTimeoutMs);
52
+ if (!['file', 'build', 'deploy'].includes(input.kind) || input.baselineHash !== task.card.baselineHash) {
53
+ coordFail17('ENTROROVE_COORD_BASELINE', 'resource queue requires the original current taskcard baseline');
54
+ }
55
+ const paths = input.kind === 'file' ? canonicalSwarmPaths(input.paths) : [];
56
+ if (paths.some(target => !task.card.taskScope.includes(target))) coordFail17('ENTROROVE_COORD_SCOPE', 'queue paths exceed its original taskcard');
57
+ const request = { queueId: input.queueId, taskId: task.taskId, agentId: task.agentId, chainId: task.chainId,
58
+ kind: input.kind, resource: input.resource, paths, baselineHash: input.baselineHash, queueTimeoutMs: input.queueTimeoutMs };
59
+ const existing = state.lockQueues.find(value => value.queueId === request.queueId);
60
+ if (existing) {
61
+ if (canonical(existing.request) !== canonical(request)) coordFail17('ENTROROVE_COORD_QUEUE_CHANGED', 'queue identity cannot change its original request');
62
+ return existing;
63
+ }
64
+ const now = Date.now(), entry = { request, queueId: request.queueId, taskId: task.taskId, status: 'waiting',
65
+ registeredAt: new Date(now).toISOString(), deadlineAt: new Date(now + input.queueTimeoutMs).toISOString(),
66
+ grant: null, consumerAvailable: false, unsupported: input.kind === 'file' ? 'restrictive-native-subset-adapter' : `${input.kind}-consumer` };
67
+ state.lockQueues.push(entry); return entry;
68
+ }
@@ -0,0 +1,118 @@
1
+ import { canonical, sha256 } from './local-integrity.mjs';
2
+ import { canonicalSwarmPaths } from './local-swarm-state.mjs';
3
+ import { coordDuration17, coordExact17, coordFail17, coordId17, coordEvent17, coordMessage17 } from './local-autocoord17-store.mjs';
4
+
5
+ const ACTIVE = ['waiting', 'need-human'];
6
+ const EVENT_TYPES = ['signal', 'peer-ready', 'task-reported', 'task-failed', 'lease-expired', 'lock-released'];
7
+ export function requestDecision17(state, task, reason, key, paths = []) {
8
+ const decisionId = sha256(`${reason}\0${task.taskId}\0${key}`);
9
+ const existing = state.decisions.find(value => value.decisionId === decisionId);
10
+ if (existing) return existing;
11
+ const decision = { schemaVersion: 'entrorove.coordination-decision/1.0', decisionId, taskId: task.taskId,
12
+ agentId: task.agentId, chainId: task.chainId, status: 'need-human', reason, paths,
13
+ risk: 'high', request: { type: 'confirm', question: `Resolve coordination ${reason} for the original task ${task.taskId}.`,
14
+ riskDescription: 'Unresolved coordination may conflict with another original task; no engineering authority is granted.',
15
+ options: [{ id: 'resume', label: 'Resume this coordination wait' }, { id: 'cancel', label: 'Cancel this coordination wait' }],
16
+ callback: { operation: 'coordination-resolve-human', payload: { decisionId } } }, createdAt: new Date().toISOString(), audit: null };
17
+ state.decisions.push(decision); coordMessage17(state, task.taskId, 'need-human', { decisionId, reason }, key);
18
+ return decision;
19
+ }
20
+ function match(wait, event) {
21
+ return wait.request.waitFor.taskId === event.taskId && wait.request.event.type === event.type
22
+ && wait.request.event.resource === event.resource;
23
+ }
24
+ function cycle(state, start, current, seen = new Set()) {
25
+ if (current === start) return true;
26
+ if (seen.has(current)) return false;
27
+ seen.add(current);
28
+ return state.waits.filter(value => ACTIVE.includes(value.status) && value.taskId === current)
29
+ .some(value => cycle(state, start, value.request.waitFor.taskId, seen));
30
+ }
31
+ export function registerWait17(state, task, input) {
32
+ coordId17(input.waitId); coordDuration17(input.expectedWithinMs);
33
+ coordExact17(input.waitFor, ['taskId']); coordId17(input.waitFor.taskId);
34
+ coordExact17(input.event, ['type', 'resource']); coordId17(input.event.resource);
35
+ if (!EVENT_TYPES.includes(input.event.type) || !state.tasks.some(value => value.taskId === input.waitFor.taskId)
36
+ || !['resume', 'notify'].includes(input.onArrival) || !['cancel', 'notify', 'escalate-need-human'].includes(input.onTimeout)) {
37
+ coordFail17('ENTROROVE_COORD_WAIT', 'wait must name a registered original task, structured event and explicit policies');
38
+ }
39
+ const refetchPaths = input.refetchPaths.length ? canonicalSwarmPaths(input.refetchPaths) : [];
40
+ if (refetchPaths.some(target => !task.card.taskScope.includes(target))) coordFail17('ENTROROVE_COORD_SCOPE', 'wait refetch paths exceed the original taskcard');
41
+ const request = { waitId: input.waitId, waiter: { taskId: task.taskId, agentId: task.agentId, chainId: task.chainId },
42
+ waitFor: input.waitFor, event: input.event, expectedWithinMs: input.expectedWithinMs,
43
+ onArrival: input.onArrival, onTimeout: input.onTimeout, refetchPaths };
44
+ const previous = state.waits.find(value => value.waitId === input.waitId);
45
+ if (previous) {
46
+ if (canonical(previous.request) !== canonical(request)) coordFail17('ENTROROVE_COORD_WAIT_CHANGED', 'same wait ID cannot change its original dependency');
47
+ return previous;
48
+ }
49
+ const now = Date.now(), wait = { waitId: input.waitId, taskId: task.taskId, request, status: 'waiting',
50
+ registeredAt: new Date(now).toISOString(), deadlineAt: new Date(now + input.expectedWithinMs).toISOString(),
51
+ wakeup: null, decisionId: null };
52
+ state.waits.push(wait);
53
+ const existing = state.events.find(value => match(wait, value));
54
+ if (existing) arrive(state, task, wait, existing);
55
+ else if (cycle(state, task.taskId, input.waitFor.taskId)) {
56
+ const involved = state.waits.filter(value => ACTIVE.includes(value.status)
57
+ && (cycle(state, value.taskId, value.request.waitFor.taskId)));
58
+ for (const value of involved) {
59
+ const owner = state.tasks.find(item => item.taskId === value.taskId);
60
+ value.status = 'need-human'; value.decisionId = requestDecision17(state, owner, 'dependency-cycle', value.waitId, value.request.refetchPaths).decisionId;
61
+ }
62
+ }
63
+ return wait;
64
+ }
65
+ function arrive(state, task, wait, event) {
66
+ if (wait.status === 'need-human') return;
67
+ wait.status = ['task-failed', 'lease-expired'].includes(event.type) ? 'terminated' : 'arrived';
68
+ wait.wakeup = { event: structuredClone(event), refetchPaths: [...wait.request.refetchPaths],
69
+ freshAimlockSnapshotRequired: wait.request.refetchPaths.length > 0, receivedAt: new Date().toISOString() };
70
+ task.consecutiveTimeouts = 0;
71
+ coordMessage17(state, task.taskId, 'dependency-wakeup', { waitId: wait.waitId, ...wait.wakeup }, wait.waitId);
72
+ }
73
+ export function scanWaits17(state) {
74
+ const now = Date.now();
75
+ for (const wait of state.waits.filter(value => value.status === 'waiting')) {
76
+ const task = state.tasks.find(value => value.taskId === wait.taskId), event = state.events.find(value => match(wait, value));
77
+ const terminal = state.events.find(value => value.taskId === wait.request.waitFor.taskId
78
+ && ['task-failed', 'lease-expired'].includes(value.type));
79
+ if (event || terminal) { arrive(state, task, wait, event ?? terminal); continue; }
80
+ if (Date.parse(wait.deadlineAt) > now) continue;
81
+ task.consecutiveTimeouts = (task.consecutiveTimeouts ?? 0) + 1;
82
+ const human = wait.request.onTimeout === 'escalate-need-human' || task.consecutiveTimeouts >= 2;
83
+ wait.status = human ? 'need-human' : 'timed-out';
84
+ coordMessage17(state, task.taskId, 'dependency-timeout', { waitId: wait.waitId, consecutive: task.consecutiveTimeouts }, wait.waitId);
85
+ if (human) wait.decisionId = requestDecision17(state, task, 'dependency-timeout', wait.waitId, wait.request.refetchPaths).decisionId;
86
+ }
87
+ return state.waits;
88
+ }
89
+ export function cancelWait17(state, task, waitId) {
90
+ const wait = state.waits.find(value => value.waitId === waitId && value.taskId === task.taskId);
91
+ if (!wait) coordFail17('ENTROROVE_COORD_WAIT_BINDING', 'cancel must name this original task wait');
92
+ if (wait.status === 'need-human') coordFail17('ENTROROVE_COORD_HUMAN_REQUIRED', 'cancellation cannot bypass an unresolved real human decision');
93
+ if (wait.status === 'waiting') {
94
+ wait.status = 'cancelled'; coordEvent17(state, 'wait-cancelled', task.taskId, waitId, { waitId }, waitId);
95
+ }
96
+ return wait;
97
+ }
98
+ export function scanQueues17(state) {
99
+ const now = Date.now(), decisions = [];
100
+ for (const queue of state.lockQueues.filter(value => value.status === 'waiting')) {
101
+ if (!Number.isFinite(Date.parse(queue.deadlineAt))) coordFail17('ENTROROVE_COORD_QUEUE_DEADLINE', 'queue cannot borrow an ETA or lock TTL');
102
+ if (Date.parse(queue.deadlineAt) > now) continue;
103
+ queue.status = 'timed-out';
104
+ const task = state.tasks.find(value => value.taskId === queue.taskId);
105
+ coordMessage17(state, task.taskId, 'lock-denied', { queueId: queue.queueId, resource: queue.request.resource }, queue.queueId);
106
+ decisions.push(requestDecision17(state, task, 'lock-queue-timeout', queue.queueId, queue.request.paths));
107
+ }
108
+ return decisions;
109
+ }
110
+ export function coordinationBlocked17(state, task, paths = []) {
111
+ const waits = state.waits.filter(value => value.taskId === task.taskId && ACTIVE.includes(value.status));
112
+ const decisions = state.decisions.filter(value => value.taskId === task.taskId && value.status === 'need-human');
113
+ const peers = state.intents.filter(value => value.taskId === task.taskId && ['waiting', 'partial'].includes(value.status)
114
+ && (!paths.length || paths.some(target => value.blockedPaths.includes(target))));
115
+ return { blocked: waits.length > 0 || decisions.length > 0 || peers.length > 0,
116
+ waitIds: waits.map(value => value.waitId), decisionIds: decisions.map(value => value.decisionId),
117
+ intentIds: peers.map(value => value.intentId), writeAuthorized: false };
118
+ }
@@ -2,6 +2,7 @@ import { resolve } from 'node:path';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import { runCalctoolLocal } from './local-calctool.mjs';
4
4
  import { CALCTOOL_LOCAL_OPERATION_SCHEMAS } from './local-calctool-contracts.mjs';
5
+ import { localErrorEnvelope } from './local-error.mjs';
5
6
 
6
7
  export async function runCalctoolCli(args = process.argv.slice(2)) {
7
8
  let source = ''; for await (const chunk of process.stdin) source += chunk;
@@ -17,6 +18,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
17
18
  const result = await runCalctoolCli(); process.stdout.write(JSON.stringify(result) + '\n');
18
19
  if (['blocked', 'failed', 'incomplete'].includes(result.status)) process.exitCode = 2;
19
20
  } catch (cause) {
20
- process.stderr.write(JSON.stringify({ status: 'blocked', code: cause.code ?? 'CALCTOOL_INPUT_INVALID', message: cause.message }) + '\n'); process.exitCode = 2;
21
+ const error = localErrorEnvelope(cause, 'CALCTOOL_INPUT_INVALID');
22
+ process.stderr.write(`${JSON.stringify(error)}\n`); process.exitCode = error.status === 'blocked' ? 2 : 1;
21
23
  }
22
24
  }
@@ -0,0 +1,50 @@
1
+ import { createHash } 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 { runCalctoolLocal } from './local-calctool.mjs';
6
+ import { verifyFormulaProgram } from './local-formula-engine.mjs';
7
+ import { runWorkflowLocalStep } from './workflow-local-runner.mjs';
8
+
9
+ const sha = bytes => createHash('sha256').update(bytes).digest('hex');
10
+ function fail(message) { throw Object.assign(new Error(message), { code: 'ENTROROVE_CALCTOOL_COMPOSITION' }); }
11
+ export async function compileCalctoolArtifacts17(program) {
12
+ verifyFormulaProgram(program);
13
+ const root = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), 'entrorove-calctool17-control-')));
14
+ try {
15
+ const repositoryRoot = path.join(root, 'repository'); const home = path.join(root, 'home');
16
+ await fs.mkdir(repositoryRoot, { mode: 0o700 }); await fs.mkdir(home, { mode: 0o700 });
17
+ const output = await runCalctoolLocal('generate', { repositoryRoot, home, program });
18
+ const files = await Promise.all(output.files.map(async entry => {
19
+ const bytes = await fs.readFile(path.join(output.directory, entry.path));
20
+ if (sha(bytes) !== entry.sha256) fail('Native generated Calctool bytes changed');
21
+ return { path: entry.path, sha256: entry.sha256 };
22
+ }));
23
+ if (files.map(file => file.path).sort().join('|') !== 'program.json|runtime.mjs|tool.mjs') fail('Native Calctool triplet is incomplete');
24
+ return files.sort((a, b) => a.path.localeCompare(b.path));
25
+ } finally { await fs.rm(root, { recursive: true, force: true }); }
26
+ }
27
+ export function assertCalctoolStep17(composition, step) {
28
+ verifyFormulaProgram(composition.program, composition);
29
+ if (composition.schemaVersion !== 'entrorove.calctool-composition/1.0' || !Array.isArray(composition.files)
30
+ || composition.files.length !== 3) fail('Original Calctool composition is required');
31
+ for (const file of composition.files) if (!step.executionPlan.files.some(entry => entry.path === file.path && entry.sha256 === file.sha256)) {
32
+ fail('Calctool generated bytes must be in the original frozen execution manifest');
33
+ }
34
+ const tool = composition.files.find(file => file.path.endsWith('/tool.mjs'));
35
+ if (!tool || JSON.stringify(step.executionPlan.policy.args) !== JSON.stringify([tool.path, '--verify'])
36
+ || !step.executionPlan.tests.some(test => test.path === tool.path)) fail('Calctool step must execute its original Decimal sample verifier');
37
+ }
38
+ export async function runCalctoolCompositionStep17(input) {
39
+ const { composition, ...managed } = input;
40
+ const native = await compileCalctoolArtifacts17(composition.program);
41
+ const expected = native.map(file => ({ ...file, path: `${composition.directory}/${file.path}` }));
42
+ if (JSON.stringify(expected) !== JSON.stringify(composition.files)) fail('Composition differs from actual native Calctool generation');
43
+ const step = managed.acceptedPlan?.steps.find(item => item.stepId === managed.stepId);
44
+ if (!step || managed.acceptedPlan.flowId !== composition.flowId || managed.acceptedPlan.ownerId !== composition.ownerId
45
+ || managed.acceptedPlan.taskDigest !== composition.taskDigest || managed.acceptedPlan.chainDigest !== composition.chainDigest) {
46
+ fail('Calctool composition must preserve its original accepted task and chain');
47
+ }
48
+ assertCalctoolStep17(composition, step);
49
+ return runWorkflowLocalStep(managed);
50
+ }
@@ -1,7 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  const runtimeUrl = new URL('./runtime/calctool-runtime-v7.0.43.mjs', import.meta.url);
4
- const expectedSha256 = '5d3506320d6baba4a8ca42cf55d062ae2b10f186741896c8cc93e536734f961f';
4
+ const expectedSha256 = '5473427c350411f62c16ac5239fa0db7b96e7b2c19458f9046768dcb4ada87d1';
5
5
  export async function calctoolRuntimeBytes() {
6
6
  const bytes = await readFile(runtimeUrl);
7
7
  if (createHash('sha256').update(bytes).digest('hex') !== expectedSha256) throw Object.assign(new Error('Calctool packaged runtime digest mismatch'), { code: 'FORMULA_RUNTIME_INTEGRITY' });
@@ -45,7 +45,7 @@ const TOOL_SOURCE = `import { readFile } from 'node:fs/promises';
45
45
  import { evaluateCompiledProgram, verifyCompiledProgram } from './runtime.mjs';
46
46
  let input = ''; for await (const chunk of process.stdin) input += chunk;
47
47
  try { const program = JSON.parse(await readFile(new URL('./program.json', import.meta.url), 'utf8')); if(process.argv.includes('--verify')) { const result=verifyCompiledProgram(program); process.stdout.write(JSON.stringify(result)+'\\n'); process.exitCode=result.computationsPassed?0:2; } else { const values = evaluateCompiledProgram(program, JSON.parse(input)); process.stdout.write(JSON.stringify({status:'reported',programDigest:program.programDigest,values})+'\\n'); } }
48
- catch (error) { process.stderr.write(JSON.stringify({status:'blocked',code:error.code??'FORMULA_INPUT_INVALID',message:error instanceof SyntaxError?'Formula input or artifact must be valid JSON':error.message})+'\\n'); process.exitCode=2; }
48
+ catch (error) { const code=error.code??'FORMULA_INPUT_INVALID'; const status=/(?:AUTHORITY_REQUIRED|LEASE_EXPIRED|NOT_ACCEPTED|REVOKED)$/.test(code)?'blocked':'failed'; process.stderr.write(JSON.stringify({schemaVersion:'entrorove.local-error/1.0',status,code,message:error instanceof SyntaxError?'Formula input or artifact must be valid JSON':error.message})+'\\n'); process.exitCode=status==='blocked'?2:1; }
49
49
  `;
50
50
  async function store(input, generate) {
51
51
  const program = await programFrom(input);
@@ -0,0 +1,93 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { constants, promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { validateLocalInput } from './local-contracts.mjs';
6
+ import { observeWorkflowLocalStep } from './workflow-local-runner.mjs';
7
+ import { validateLocalChainPlan } from './local-chain-plan.mjs';
8
+
9
+ export const CHAIN_PACKAGE_ROOT = path.dirname(fileURLToPath(import.meta.url));
10
+ const ROOT = await fs.realpath(CHAIN_PACKAGE_ROOT);
11
+ const SKILLS = ['aimlock', 'archguard', 'blueprint', 'calctool', 'confirm-protocol', 'mergeguard', 'swarm', 'validator'];
12
+ export function chainFail(code, message) { throw Object.assign(new Error(message), { code }); }
13
+ export function chainCanonical(value) {
14
+ if (Array.isArray(value)) return `[${value.map(chainCanonical).join(',')}]`;
15
+ if (value && typeof value === 'object') return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${chainCanonical(value[key])}`).join(',')}}`;
16
+ return JSON.stringify(value);
17
+ }
18
+ export const chainSha = value => createHash('sha256').update(value).digest('hex');
19
+ export function chainExact(value, required, optional = []) {
20
+ if (!value || typeof value !== 'object' || Array.isArray(value) || required.some(key => !Object.hasOwn(value, key))
21
+ || Object.keys(value).some(key => !required.includes(key) && !optional.includes(key))) {
22
+ chainFail('ENTROROVE_CHAIN_FIELDS', 'chain input has missing or unknown fields');
23
+ }
24
+ }
25
+ export async function chainControlFile(file, maximum = 8 * 1024 * 1024) {
26
+ const descriptor = await fs.open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
27
+ try {
28
+ const before = await descriptor.stat();
29
+ if (!before.isFile() || before.nlink !== 1 || before.size > maximum
30
+ || (typeof process.getuid === 'function' && before.uid !== process.getuid())) {
31
+ chainFail('ENTROROVE_CHAIN_CONTROL_INVALID', 'chain control must be a bounded owned regular file');
32
+ }
33
+ const bytes = await descriptor.readFile(), after = await descriptor.stat(), current = await fs.lstat(file);
34
+ if (bytes.length !== before.size || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size
35
+ || before.mtimeMs !== after.mtimeMs || current.isSymbolicLink() || current.dev !== before.dev || current.ino !== before.ino) {
36
+ chainFail('ENTROROVE_CHAIN_CONTROL_CHANGED', 'chain control changed during its actual read');
37
+ }
38
+ return { bytes, stat: after };
39
+ } finally { await descriptor.close(); }
40
+ }
41
+ async function sourceNames() {
42
+ const rootNames = (await fs.readdir(ROOT)).filter(name => name.endsWith('.mjs'));
43
+ const runtimeNames = (await fs.readdir(path.join(ROOT, 'runtime'))).filter(name => name.endsWith('.mjs')).map(name => `runtime/${name}`);
44
+ return [...rootNames, ...runtimeNames, 'package.json', 'manifest.json'].sort();
45
+ }
46
+ export async function captureChainPackage(plan) {
47
+ for (const skill of plan.skills) {
48
+ if (!SKILLS.includes(skill.skillId) || await fs.realpath(skill.packageRoot) !== ROOT) {
49
+ chainFail('ENTROROVE_CHAIN_SKILL_PACKAGE', 'native chain skills must use the current fixed Entrorove package');
50
+ }
51
+ }
52
+ const files = [];
53
+ for (const name of await sourceNames()) files.push({ path: name, sha256: chainSha((await chainControlFile(path.join(ROOT, name))).bytes) });
54
+ const metadata = JSON.parse((await chainControlFile(path.join(ROOT, 'package.json'))).bytes.toString('utf8'));
55
+ if (metadata.name !== '@entrorove/skill') chainFail('ENTROROVE_CHAIN_SKILL_PACKAGE', 'installed native package identity is invalid');
56
+ return { packageRoot: ROOT, name: metadata.name, version: metadata.version, files,
57
+ authorityKeyDigest: chainSha(process.env.ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY ?? '') };
58
+ }
59
+ export async function checkChainPackage(snapshot) {
60
+ const current = await captureChainPackage(snapshot.plan);
61
+ if (chainCanonical(current) !== chainCanonical(snapshot.package)) chainFail('ENTROROVE_CHAIN_PACKAGE_CHANGED', 'installed source, metadata or authority changed after chain initialization');
62
+ }
63
+ export async function validateChainBindings(planValue, values, identity) {
64
+ const plan = validateLocalChainPlan(planValue);
65
+ if (!Array.isArray(values)) chainFail('ENTROROVE_CHAIN_BINDING', 'command bindings must be an explicit array');
66
+ const commands = plan.steps.filter(step => step.kind === 'command'), bindings = [];
67
+ if (values.length !== commands.length) chainFail('ENTROROVE_CHAIN_BINDING', 'every command requires exactly one original signed workflow mapping');
68
+ for (const step of commands) {
69
+ const selected = values.filter(value => value.stepId === step.stepId);
70
+ if (selected.length !== 1) chainFail('ENTROROVE_CHAIN_BINDING', 'command mapping must have a unique original step ID');
71
+ chainExact(selected[0], ['stepId', 'workflowInput']);
72
+ const input = validateLocalInput('workflow-run-step', structuredClone(selected[0].workflowInput));
73
+ const native = input.acceptedPlan.steps.find(value => value.stepId === input.stepId);
74
+ if (input.repositoryRoot !== identity.repositoryRoot || input.home !== identity.home || input.stepId !== step.stepId
75
+ || input.acceptedPlan.flowId !== identity.flowId || input.acceptedPlan.ownerId !== identity.ownerId
76
+ || input.lease.stepId !== step.stepId || !native || chainCanonical(native.dependsOn) !== chainCanonical(step.dependsOn)) {
77
+ chainFail('ENTROROVE_CHAIN_BINDING', 'command mapping changed its original home, repository, flow, owner or step dependencies');
78
+ }
79
+ const policy = native.executionPlan.policy;
80
+ if (chainCanonical([step.input.executable, step.input.args, step.input.environment, step.input.timeoutMs, step.input.evidenceKind])
81
+ !== chainCanonical([policy.executable, policy.args, policy.environment, policy.timeoutMs, native.executionPlan.evidencePolicy?.kind ?? 'test'])) {
82
+ chainFail('ENTROROVE_CHAIN_BINDING', 'command policy differs from its actual frozen workflow plan');
83
+ }
84
+ if (path.resolve(identity.repositoryRoot, step.input.workingDirectory) !== identity.repositoryRoot) {
85
+ chainFail('ENTROROVE_CHAIN_COMMAND_CWD_UNSUPPORTED', 'the fixed native workflow runner currently executes only at its original repository root');
86
+ }
87
+ // This real observer verifies the configured Ed25519 plan and lease without
88
+ // starting a process or inventing an execution authority.
89
+ await observeWorkflowLocalStep(input);
90
+ bindings.push({ stepId: step.stepId, workflowInput: input });
91
+ }
92
+ return bindings;
93
+ }