@north-light/crouter 0.3.269 → 0.3.271
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/dist/api/dto/broker.d.ts +11 -2
- package/dist/api/dto/broker.js +2 -2
- package/dist/api/dto/nodes.d.ts +4 -0
- package/dist/commands/pkg/plugin-inspect.js +2 -2
- package/dist/commands/pkg/plugin-manage.js +3 -3
- package/dist/commands/sys/doctor.js +4 -3
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +2 -2
- package/dist/core/__tests__/helpers/harness.js +1 -0
- package/dist/core/__tests__/human-deliver.test.js +1 -1
- package/dist/core/__tests__/integration/lifecycle-hooks.test.d.ts +1 -0
- package/dist/core/__tests__/integration/lifecycle-hooks.test.js +182 -0
- package/dist/core/__tests__/integration/revive.test.js +15 -15
- package/dist/core/__tests__/push-final-guard.test.js +1 -1
- package/dist/core/__tests__/revive-capacity.test.js +20 -20
- package/dist/core/__tests__/revive-parked-fresh.test.js +14 -14
- package/dist/core/__tests__/seam/broker-cap-freeze.test.js +2 -2
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +2 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +7 -7
- package/dist/core/__tests__/seam/held-deferred-human-prompt.test.js +1 -1
- package/dist/core/__tests__/seam/yield-refresh-transaction.test.js +2 -2
- package/dist/core/command-hooks/discovery.d.ts +27 -1
- package/dist/core/command-hooks/discovery.js +52 -0
- package/dist/core/command-hooks/index.d.ts +4 -3
- package/dist/core/command-hooks/index.js +2 -1
- package/dist/core/command-hooks/lifecycle-catalog.d.ts +3 -0
- package/dist/core/command-hooks/lifecycle-catalog.js +4 -0
- package/dist/core/command-hooks/report.d.ts +11 -2
- package/dist/core/command-hooks/report.js +10 -1
- package/dist/core/command-hooks/schema.d.ts +16 -1
- package/dist/core/command-hooks/schema.js +128 -40
- package/dist/core/command-hooks/transport/exec-lifecycle.d.ts +12 -0
- package/dist/core/command-hooks/transport/exec-lifecycle.js +152 -0
- package/dist/core/human/feedback-companion.js +1 -1
- package/dist/core/review/realize.js +1 -1
- package/dist/core/runtime/broker/engine-drive.d.ts +14 -3
- package/dist/core/runtime/broker/engine-drive.js +29 -1
- package/dist/core/runtime/broker/frame-client.js +3 -4
- package/dist/core/runtime/broker/rebind.js +7 -0
- package/dist/core/runtime/broker-protocol.d.ts +11 -2
- package/dist/core/runtime/broker.d.ts +1 -1
- package/dist/core/runtime/broker.js +13 -3
- package/dist/core/runtime/fleet.d.ts +5 -6
- package/dist/core/runtime/node-read.d.ts +2 -0
- package/dist/core/runtime/node-read.js +18 -9
- package/dist/core/runtime/nodes.js +6 -1
- package/dist/core/runtime/revive-all.d.ts +1 -1
- package/dist/core/runtime/revive-all.js +2 -2
- package/dist/core/runtime/revive.d.ts +2 -2
- package/dist/core/runtime/revive.js +49 -13
- package/dist/core/runtime/session-visibility.d.ts +10 -14
- package/dist/core/runtime/session-visibility.js +43 -30
- package/dist/core/runtime/stamp/channel.d.ts +10 -6
- package/dist/core/runtime/stamp/channel.js +16 -7
- package/dist/core/runtime/stamp/protocol.d.ts +4 -0
- package/dist/core/runtime/turn-visibility.d.ts +10 -0
- package/dist/core/runtime/turn-visibility.js +31 -0
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +2 -2
- package/dist/daemon/api/bridge.js +1 -1
- package/dist/daemon/api/handlers/attach.js +4 -4
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/messages.js +2 -2
- package/dist/daemon/api/handlers/nodes.js +7 -4
- package/dist/daemon/cron/sinks.js +3 -3
- package/dist/daemon/fleet.js +10 -22
- package/dist/daemon/messaging/node-message.js +1 -1
- package/dist/daemon/profile-delete.js +1 -1
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +6 -6
- package/dist/hook-authoring.d.ts +31 -1
- package/dist/hook-authoring.js +42 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/pi-extensions/canvas-stamp.js +3 -3
- package/dist/shared/env.d.ts +2 -0
- package/dist/shared/env.js +4 -0
- package/dist/shared/generated-context.d.ts +1 -2
- package/dist/shared/generated-context.js +3 -4
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -103,10 +103,10 @@ after(() => {
|
|
|
103
103
|
else
|
|
104
104
|
process.env['CRTR_HOME'] = priorHome;
|
|
105
105
|
});
|
|
106
|
-
test('a parked node with mail waiting re-enters fresh even when the caller asked to resume, and the marker clears', () => {
|
|
106
|
+
test('a parked node with mail waiting re-enters fresh even when the caller asked to resume, and the marker clears', async () => {
|
|
107
107
|
parkResident('parked', { roadmap: '# Roadmap\n\n- one open item\n' });
|
|
108
108
|
deliverMail('parked');
|
|
109
|
-
const result = reviveNode('parked', { resume: true, capacity: 'freeze' });
|
|
109
|
+
const result = await reviveNode('parked', { resume: true, capacity: 'freeze' });
|
|
110
110
|
assert.equal(result.resumed, false, 'the concluded transcript is not replayed');
|
|
111
111
|
assert.deepEqual(placements(), [{ nodeId: 'parked', resuming: false }]);
|
|
112
112
|
assert.equal(kickedOff(launches[0]), true, 'the fresh window is handed its bearings');
|
|
@@ -115,15 +115,15 @@ test('a parked node with mail waiting re-enters fresh even when the caller asked
|
|
|
115
115
|
assert.equal(reopened.intent ?? null, null, 'the park marker clears on revival');
|
|
116
116
|
assert.equal(reopened.cycle_pending, true, 'the fresh cycle is provisional until session_start confirms it');
|
|
117
117
|
});
|
|
118
|
-
test('a parked node grounded only by a goal still re-enters fresh when mail rides the revive', () => {
|
|
118
|
+
test('a parked node grounded only by a goal still re-enters fresh when mail rides the revive', async () => {
|
|
119
119
|
parkResident('goal-only', { goal: 'Watch the deploy lane.' });
|
|
120
120
|
deliverMail('goal-only');
|
|
121
|
-
assert.equal(reviveNode('goal-only', { resume: true, capacity: 'freeze' }).resumed, false);
|
|
121
|
+
assert.equal((await reviveNode('goal-only', { resume: true, capacity: 'freeze' })).resumed, false);
|
|
122
122
|
assert.equal(kickedOff(launches[0]), true);
|
|
123
123
|
});
|
|
124
|
-
test('an input-less revival of a parked node quietly resumes and injects no kickoff', () => {
|
|
124
|
+
test('an input-less revival of a parked node quietly resumes and injects no kickoff', async () => {
|
|
125
125
|
parkResident('quiet', { roadmap: '# Roadmap\n\n- one open item\n' });
|
|
126
|
-
const result = reviveNode('quiet', { resume: true, capacity: 'freeze' });
|
|
126
|
+
const result = await reviveNode('quiet', { resume: true, capacity: 'freeze' });
|
|
127
127
|
assert.equal(result.resumed, true, 'nothing arrived, so there is nothing a fresh window would answer');
|
|
128
128
|
assert.deepEqual(placements(), [{ nodeId: 'quiet', resuming: true }]);
|
|
129
129
|
assert.equal(kickedOff(launches[0]), false, 'a quiet resume generates nothing');
|
|
@@ -132,29 +132,29 @@ test('an input-less revival of a parked node quietly resumes and injects no kick
|
|
|
132
132
|
assert.equal(reopened.status, 'active');
|
|
133
133
|
assert.equal(reopened.intent ?? null, null, 'the park marker clears on a quiet resume too');
|
|
134
134
|
});
|
|
135
|
-
test('deferred mail alone is not input, so the revival still resumes quietly', () => {
|
|
135
|
+
test('deferred mail alone is not input, so the revival still resumes quietly', async () => {
|
|
136
136
|
parkResident('deferred', { roadmap: '# Roadmap\n\n- one open item\n' });
|
|
137
137
|
deliverMail('deferred', 'deferred');
|
|
138
|
-
assert.equal(reviveNode('deferred', { resume: true, capacity: 'freeze' }).resumed, true);
|
|
138
|
+
assert.equal((await reviveNode('deferred', { resume: true, capacity: 'freeze' })).resumed, true);
|
|
139
139
|
assert.equal(kickedOff(launches[0]), false);
|
|
140
140
|
assert.equal(readCursor('deferred'), undefined, 'the deferred entry rides the next natural cycle, unconsumed');
|
|
141
141
|
});
|
|
142
|
-
test('mail already covered by the cursor is not input, so the revival still resumes quietly', () => {
|
|
142
|
+
test('mail already covered by the cursor is not input, so the revival still resumes quietly', async () => {
|
|
143
143
|
parkResident('stale', { roadmap: '# Roadmap\n\n- one open item\n' });
|
|
144
144
|
writeCursor('stale', deliverMail('stale'));
|
|
145
|
-
assert.equal(reviveNode('stale', { resume: true, capacity: 'freeze' }).resumed, true, 'only the UNSEEN suffix counts as input');
|
|
145
|
+
assert.equal((await reviveNode('stale', { resume: true, capacity: 'freeze' })).resumed, true, 'only the UNSEEN suffix counts as input');
|
|
146
146
|
assert.equal(kickedOff(launches[0]), false);
|
|
147
147
|
});
|
|
148
|
-
test('a parked node with no goal and no roadmap resumes its transcript rather than waking amnesiac', () => {
|
|
148
|
+
test('a parked node with no goal and no roadmap resumes its transcript rather than waking amnesiac', async () => {
|
|
149
149
|
parkResident('bare');
|
|
150
150
|
deliverMail('bare');
|
|
151
|
-
const result = reviveNode('bare', { resume: true, capacity: 'freeze' });
|
|
151
|
+
const result = await reviveNode('bare', { resume: true, capacity: 'freeze' });
|
|
152
152
|
assert.equal(result.resumed, true, 'the degraded park has nothing on disk to ground a fresh window');
|
|
153
153
|
assert.deepEqual(placements(), [{ nodeId: 'bare', resuming: true }]);
|
|
154
154
|
});
|
|
155
|
-
test('an ordinary done node with a roadmap still resumes — only the park marker vetoes', () => {
|
|
155
|
+
test('an ordinary done node with a roadmap still resumes — only the park marker vetoes', async () => {
|
|
156
156
|
parkResident('finished', { roadmap: '# Roadmap\n\n- one open item\n' });
|
|
157
157
|
transition('finished', 'revive');
|
|
158
158
|
transition('finished', 'finish');
|
|
159
|
-
assert.equal(reviveNode('finished', { resume: true, capacity: 'freeze' }).resumed, true);
|
|
159
|
+
assert.equal((await reviveNode('finished', { resume: true, capacity: 'freeze' })).resumed, true);
|
|
160
160
|
});
|
|
@@ -117,7 +117,7 @@ test('a birth at the cap freezes instead of booting, survives saturated ticks, a
|
|
|
117
117
|
const filler = await h.spawnHeadlessChild(root, 'terminal thaw filler');
|
|
118
118
|
assert.equal(h.fleet.size(), 2, 'the parked target is non-live while the cap remains full');
|
|
119
119
|
appendInbox(second, { from: root, tier: 'normal', kind: 'message', label: 'durable wake' });
|
|
120
|
-
assert.equal(reviveNode(second, { resume: true, capacity: 'freeze' }).outcome, 'frozen');
|
|
120
|
+
assert.equal((await reviveNode(second, { resume: true, capacity: 'freeze' })).outcome, 'frozen');
|
|
121
121
|
closeDb();
|
|
122
122
|
const frozenTerminal = h.node(second);
|
|
123
123
|
assert.equal(frozenTerminal.status, 'done', 'freezing preserves the terminal status');
|
|
@@ -144,7 +144,7 @@ test('a birth at the cap freezes instead of booting, survives saturated ticks, a
|
|
|
144
144
|
created: new Date().toISOString(),
|
|
145
145
|
pi_session_id: 'resident-drain-session',
|
|
146
146
|
});
|
|
147
|
-
assert.equal(reviveNode('resident-drain', { resume: true, capacity: 'freeze' }).outcome, 'frozen');
|
|
147
|
+
assert.equal((await reviveNode('resident-drain', { resume: true, capacity: 'freeze' })).outcome, 'frozen');
|
|
148
148
|
await h.tick();
|
|
149
149
|
closeDb();
|
|
150
150
|
const drained = h.node('resident-drain');
|
|
@@ -109,6 +109,8 @@ test('a managed-cooling terminal authors a rate-limit auto fault whose deadline
|
|
|
109
109
|
seedPool('openai-codex', false);
|
|
110
110
|
const child = await h.spawnHeadlessChild(parent, 'cooling terminal', { model: OPENAI_STRONG.spec });
|
|
111
111
|
await h.waitFor(() => agentStartCount(child) >= 1, { label: 'initial cooling fixture turn started' });
|
|
112
|
+
const kickoffObserver = await attachUntil(child, 'observer', 'cooling-terminal-kickoff-settled', (a) => a.welcome.snapshot.state.isStreaming === false, 'cooling terminal kickoff settled');
|
|
113
|
+
kickoffObserver.close();
|
|
112
114
|
const baselineStarts = agentStartCount(child);
|
|
113
115
|
// A terminal managed-cooling error at agent_end: its explanatory prose avoids
|
|
114
116
|
// rate-limit words entirely; the typed diagnostic alone must classify the
|
|
@@ -79,7 +79,7 @@ test('idle-release settles its real watcher before exit; daemon revives exactly
|
|
|
79
79
|
},
|
|
80
80
|
},
|
|
81
81
|
});
|
|
82
|
-
reviveNode(manager, { resume: true, capacity: 'freeze' });
|
|
82
|
+
await reviveNode(manager, { resume: true, capacity: 'freeze' });
|
|
83
83
|
await h.awaitBoot(manager);
|
|
84
84
|
// A live managed child makes this unfocused terminal manager legitimately
|
|
85
85
|
// awaiting, so its real stophook takes the idle-release branch.
|
|
@@ -172,7 +172,7 @@ test('an unattended resident with no live obligation completes on the daemon clo
|
|
|
172
172
|
},
|
|
173
173
|
});
|
|
174
174
|
subscribe(root, nodeId, true);
|
|
175
|
-
reviveNode(nodeId, { resume: true, capacity: 'freeze' });
|
|
175
|
+
await reviveNode(nodeId, { resume: true, capacity: 'freeze' });
|
|
176
176
|
const boot = await h.awaitBoot(nodeId);
|
|
177
177
|
assert.equal(h.fleet.has(nodeId), true, 'the revived broker is fleet-owned');
|
|
178
178
|
const seenAt = 10_000;
|
|
@@ -223,7 +223,7 @@ test('a non-attending observer does not prevent unattended resident parking', {
|
|
|
223
223
|
intent: 'idle-release',
|
|
224
224
|
});
|
|
225
225
|
updateNode(nodeId, { launch: { extensions: [], tools: [], systemPrompt: '', env: {} } });
|
|
226
|
-
reviveNode(nodeId, { resume: true, capacity: 'freeze' });
|
|
226
|
+
await reviveNode(nodeId, { resume: true, capacity: 'freeze' });
|
|
227
227
|
const boot = await h.awaitBoot(nodeId);
|
|
228
228
|
await attach.attach(nodeId, 'observer', 'system-observer-test', { attends: false });
|
|
229
229
|
assert.equal(JSON.parse(readFileSync(join(h.home, 'nodes', nodeId, 'job', 'attach.json'), 'utf8')).viewers, 0, 'a non-attending hello is not persisted as human attendance');
|
|
@@ -268,7 +268,7 @@ test('a connected broker viewer keeps an obligation-free resident live', { timeo
|
|
|
268
268
|
env: {},
|
|
269
269
|
},
|
|
270
270
|
});
|
|
271
|
-
reviveNode(nodeId, { resume: true, capacity: 'freeze' });
|
|
271
|
+
await reviveNode(nodeId, { resume: true, capacity: 'freeze' });
|
|
272
272
|
const boot = await h.awaitBoot(nodeId);
|
|
273
273
|
// awaitBoot returns on the boot proof, which is written BEFORE the broker's
|
|
274
274
|
// own `persistAttachState()` lays down the truthful zero state. Writing our
|
|
@@ -311,7 +311,7 @@ test('the unattended park clock cancels an obligation-free terminal node and spa
|
|
|
311
311
|
try {
|
|
312
312
|
const boot = async (id) => {
|
|
313
313
|
updateNode(id, { launch: { extensions: [], tools: [], systemPrompt: '', env: {} } });
|
|
314
|
-
reviveNode(id, { resume: true, capacity: 'freeze' });
|
|
314
|
+
await reviveNode(id, { resume: true, capacity: 'freeze' });
|
|
315
315
|
return (await h.awaitBoot(id)).pid;
|
|
316
316
|
};
|
|
317
317
|
const settled = h.fabricateBrokerNode({ id: 'terminal-no-work', parent: root, status: 'idle', intent: 'idle-release' });
|
|
@@ -378,7 +378,7 @@ test('a resident managed child parks without finalizing while a terminal child i
|
|
|
378
378
|
subscribe(terminalManager, terminal, true);
|
|
379
379
|
const boot = async (id) => {
|
|
380
380
|
updateNode(id, { launch: { extensions: [], tools: [], systemPrompt: '', env: {} } });
|
|
381
|
-
reviveNode(id, { resume: true, capacity: 'freeze' });
|
|
381
|
+
await reviveNode(id, { resume: true, capacity: 'freeze' });
|
|
382
382
|
return (await h.awaitBoot(id)).pid;
|
|
383
383
|
};
|
|
384
384
|
const residentPid = await boot(resident);
|
|
@@ -439,7 +439,7 @@ test('an unattended resident awaiting a live child remains wakeable', { timeout:
|
|
|
439
439
|
env: {},
|
|
440
440
|
},
|
|
441
441
|
});
|
|
442
|
-
reviveNode(nodeId, { resume: true, capacity: 'freeze' });
|
|
442
|
+
await reviveNode(nodeId, { resume: true, capacity: 'freeze' });
|
|
443
443
|
const boot = await h.awaitBoot(nodeId);
|
|
444
444
|
const seenAt = 20_000;
|
|
445
445
|
await h.tick(seenAt);
|
|
@@ -42,7 +42,7 @@ test('a Jiti-loaded watcher prepends its held deferred batch to a native broker
|
|
|
42
42
|
},
|
|
43
43
|
},
|
|
44
44
|
});
|
|
45
|
-
reviveNode(nodeId, { resume: true, capacity: 'freeze' });
|
|
45
|
+
await reviveNode(nodeId, { resume: true, capacity: 'freeze' });
|
|
46
46
|
await h.awaitBoot(nodeId);
|
|
47
47
|
const entry = appendInbox(nodeId, {
|
|
48
48
|
from: 'child',
|
|
@@ -73,11 +73,11 @@ test('yield-message survives one pre-session_start retry and clears on session_s
|
|
|
73
73
|
// the retry still roots a fresh cycle carrying the note, never reopening the
|
|
74
74
|
// old session. Driving reviveNode directly is exactly what focus's revive path
|
|
75
75
|
// calls, minus a tmux pane this contract does not involve.
|
|
76
|
-
const focusRevive = reviveNode(id, { resume: true, capacity: 'freeze' });
|
|
76
|
+
const focusRevive = await reviveNode(id, { resume: true, capacity: 'freeze' });
|
|
77
77
|
const focusPid = focusRevive.launch?.pid;
|
|
78
78
|
assert.ok(focusPid != null, 'the competing focus invocation launches the replacement broker');
|
|
79
79
|
assert.equal(focusRevive.resumed, false, 'refresh intent overrides this invocation\'s strict-resume request');
|
|
80
|
-
assert.equal(h.fleet.get(id)?.pid, focusPid, 'the focus invocation
|
|
80
|
+
assert.equal(h.fleet.get(id)?.pid, focusPid, 'the focus invocation owns the authoritative fleet slot');
|
|
81
81
|
// Registering the manual launch cancels the exit policy's deferred recovery.
|
|
82
82
|
// Its pid must therefore be the successful observed boot, proving these
|
|
83
83
|
// assertions belong to the contender above rather than an already-queued boot.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { InstalledPlugin } from '../../types.js';
|
|
2
2
|
import type { CoreHookCatalog } from './catalog.js';
|
|
3
|
-
import type { HookManifestIssue, HookPhase, ValidatedHookManifest } from './schema.js';
|
|
3
|
+
import type { HookManifestIssue, HookPhase, LifecycleHookPhase, ValidatedHookManifest } from './schema.js';
|
|
4
|
+
import type { LifecycleEvent } from './lifecycle-catalog.js';
|
|
4
5
|
export type HookDiscoveryIssueCode = HookManifestIssue['code'] | 'command_hook_target_invalid' | 'command_hook_collision';
|
|
5
6
|
export interface HookDiscoveryIssue {
|
|
6
7
|
code: HookDiscoveryIssueCode;
|
|
@@ -38,6 +39,27 @@ export interface EffectiveHookPlan {
|
|
|
38
39
|
after: readonly EffectiveHook[];
|
|
39
40
|
replacements: readonly EffectiveHook[];
|
|
40
41
|
}
|
|
42
|
+
/** One lifecycle declaration resolved to the local executable that receives it. */
|
|
43
|
+
export interface EffectiveLifecycleHook {
|
|
44
|
+
executable: string;
|
|
45
|
+
plugin: HookPluginAttribution;
|
|
46
|
+
event: LifecycleEvent;
|
|
47
|
+
phase: LifecycleHookPhase;
|
|
48
|
+
op: string;
|
|
49
|
+
description: string;
|
|
50
|
+
effects: readonly string[];
|
|
51
|
+
/** Index in this plugin's lifecycle declaration array. */
|
|
52
|
+
declarationIndex: number;
|
|
53
|
+
/** Stable discovery order: scope, lexical plugin name, declaration index. */
|
|
54
|
+
order: number;
|
|
55
|
+
}
|
|
56
|
+
export interface LifecycleHookRegistry {
|
|
57
|
+
hooks: readonly EffectiveLifecycleHook[];
|
|
58
|
+
plans: ReadonlyMap<LifecycleEvent, readonly EffectiveLifecycleHook[]>;
|
|
59
|
+
validations: readonly HookPluginValidation[];
|
|
60
|
+
issues: readonly HookDiscoveryIssue[];
|
|
61
|
+
valid: boolean;
|
|
62
|
+
}
|
|
41
63
|
export interface HookPluginValidation {
|
|
42
64
|
plugin: InstalledPlugin;
|
|
43
65
|
manifestPath?: string;
|
|
@@ -74,6 +96,10 @@ export interface HookPluginCandidate {
|
|
|
74
96
|
* manifest files to validate them but never executes a plugin executable.
|
|
75
97
|
*/
|
|
76
98
|
export declare function compileHookRegistry(catalog: CoreHookCatalog, plugins: readonly InstalledPlugin[]): HookRegistry;
|
|
99
|
+
/** Compiles lifecycle declarations from resolved plugins without loading the command tree. */
|
|
100
|
+
export declare function compileLifecycleHookRegistry(plugins: readonly InstalledPlugin[]): LifecycleHookRegistry;
|
|
101
|
+
/** Discovers lifecycle hooks for one node's resolved cwd and profile. */
|
|
102
|
+
export declare function discoverLifecycleHookRegistry(startDir: string, profileId?: string | null): LifecycleHookRegistry;
|
|
77
103
|
/** Discovers and compiles the invocation-local effective registry. */
|
|
78
104
|
export declare function discoverHookRegistry(catalog: CoreHookCatalog, startDir?: string, profileId?: string | null): HookRegistry;
|
|
79
105
|
/** Compile the exact effective set a source candidate would create before its
|
|
@@ -153,6 +153,58 @@ export function compileHookRegistry(catalog, plugins) {
|
|
|
153
153
|
valid: issues.length === 0,
|
|
154
154
|
});
|
|
155
155
|
}
|
|
156
|
+
/** Compiles lifecycle declarations from resolved plugins without loading the command tree. */
|
|
157
|
+
export function compileLifecycleHookRegistry(plugins) {
|
|
158
|
+
const validations = [];
|
|
159
|
+
const hooks = [];
|
|
160
|
+
const issues = [];
|
|
161
|
+
let order = 0;
|
|
162
|
+
for (const plugin of plugins) {
|
|
163
|
+
const artifact = validatePluginHookArtifact(plugin.root, plugin.manifest);
|
|
164
|
+
const pluginIssues = artifactIssues(plugin, artifact.issues);
|
|
165
|
+
let compiled = [];
|
|
166
|
+
if (pluginIssues.length === 0 && artifact.manifest !== undefined && artifact.executablePath !== undefined) {
|
|
167
|
+
const lifecycle = artifact.manifest.schemaVersion === 2 ? artifact.manifest.lifecycle : [];
|
|
168
|
+
compiled = lifecycle.map((hook, declarationIndex) => Object.freeze({
|
|
169
|
+
executable: artifact.executablePath,
|
|
170
|
+
plugin: Object.freeze({ name: plugin.name, scope: plugin.scope, root: plugin.root }),
|
|
171
|
+
event: hook.event,
|
|
172
|
+
phase: hook.phase,
|
|
173
|
+
op: hook.op,
|
|
174
|
+
description: hook.description,
|
|
175
|
+
effects: Object.freeze([...hook.effects]),
|
|
176
|
+
declarationIndex,
|
|
177
|
+
order: order++,
|
|
178
|
+
}));
|
|
179
|
+
hooks.push(...compiled);
|
|
180
|
+
}
|
|
181
|
+
validations.push(Object.freeze({
|
|
182
|
+
plugin,
|
|
183
|
+
...(artifact.manifestPath === undefined ? {} : { manifestPath: artifact.manifestPath }),
|
|
184
|
+
...(artifact.executablePath === undefined ? {} : { executablePath: artifact.executablePath }),
|
|
185
|
+
...(artifact.manifest === undefined ? {} : { manifest: artifact.manifest }),
|
|
186
|
+
hooks: Object.freeze([]),
|
|
187
|
+
issues: Object.freeze(pluginIssues),
|
|
188
|
+
}));
|
|
189
|
+
issues.push(...pluginIssues);
|
|
190
|
+
}
|
|
191
|
+
const plans = new Map();
|
|
192
|
+
for (const hook of hooks) {
|
|
193
|
+
const entries = plans.get(hook.event) ?? [];
|
|
194
|
+
plans.set(hook.event, Object.freeze([...entries, hook]));
|
|
195
|
+
}
|
|
196
|
+
return Object.freeze({
|
|
197
|
+
hooks: Object.freeze(hooks),
|
|
198
|
+
plans,
|
|
199
|
+
validations: Object.freeze(validations),
|
|
200
|
+
issues: Object.freeze(issues),
|
|
201
|
+
valid: issues.length === 0,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
/** Discovers lifecycle hooks for one node's resolved cwd and profile. */
|
|
205
|
+
export function discoverLifecycleHookRegistry(startDir, profileId) {
|
|
206
|
+
return compileLifecycleHookRegistry(effectiveHookPlugins(startDir, profileId));
|
|
207
|
+
}
|
|
156
208
|
/** Discovers and compiles the invocation-local effective registry. */
|
|
157
209
|
export function discoverHookRegistry(catalog, startDir = process.cwd(), profileId) {
|
|
158
210
|
return compileHookRegistry(catalog, effectiveHookPlugins(startDir, profileId));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { createCoreHookCatalog, type CoreHookCatalog, type CoreHookTarget } from './catalog.js';
|
|
2
2
|
export { projectEffectiveLeafHelp } from './help.js';
|
|
3
|
-
export { compileHookRegistry, discoverCandidateHookRegistry, discoverHookRegistry, discoverHookRegistryInScopes, effectiveHookPlugins, hookDiscoveryScopes, type EffectiveHook, type EffectiveHookPlan, type HookDiscoveryIssue, type HookPluginAttribution, type HookPluginValidation, type HookRegistry, type HookScope, } from './discovery.js';
|
|
3
|
+
export { compileHookRegistry, compileLifecycleHookRegistry, discoverCandidateHookRegistry, discoverHookRegistry, discoverHookRegistryInScopes, discoverLifecycleHookRegistry, effectiveHookPlugins, hookDiscoveryScopes, type EffectiveHook, type EffectiveHookPlan, type EffectiveLifecycleHook, type HookDiscoveryIssue, type HookPluginAttribution, type HookPluginValidation, type HookRegistry, type HookScope, type LifecycleHookRegistry, } from './discovery.js';
|
|
4
4
|
export { validatePluginHookArtifact, type HookArtifactValidation } from './artifact.js';
|
|
5
|
-
export { hasDeclaredHooks, hookReport, HOOK_TRUST_WARNING, type HookDeclarationReport, type HookReport } from './report.js';
|
|
6
|
-
export { validateHookManifest, type DeclaredHook, type HookManifestIssue, type HookManifestValidation, type HookPhase, type ValidatedHookManifest, } from './schema.js';
|
|
5
|
+
export { hasDeclaredHooks, hookReport, HOOK_TRUST_WARNING, type HookDeclarationReport, type HookReport, type LifecycleHookDeclarationReport } from './report.js';
|
|
6
|
+
export { validateHookManifest, type DeclaredHook, type DeclaredLifecycleHook, type HookManifestIssue, type HookManifestValidation, type HookPhase, type LifecycleHookPhase, type ValidatedHookManifest, } from './schema.js';
|
|
7
|
+
export { LIFECYCLE_EVENTS, isLifecycleEvent, type LifecycleEvent } from './lifecycle-catalog.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { createCoreHookCatalog } from './catalog.js';
|
|
2
2
|
export { projectEffectiveLeafHelp } from './help.js';
|
|
3
|
-
export { compileHookRegistry, discoverCandidateHookRegistry, discoverHookRegistry, discoverHookRegistryInScopes, effectiveHookPlugins, hookDiscoveryScopes, } from './discovery.js';
|
|
3
|
+
export { compileHookRegistry, compileLifecycleHookRegistry, discoverCandidateHookRegistry, discoverHookRegistry, discoverHookRegistryInScopes, discoverLifecycleHookRegistry, effectiveHookPlugins, hookDiscoveryScopes, } from './discovery.js';
|
|
4
4
|
export { validatePluginHookArtifact } from './artifact.js';
|
|
5
5
|
export { hasDeclaredHooks, hookReport, HOOK_TRUST_WARNING } from './report.js';
|
|
6
6
|
export { validateHookManifest, } from './schema.js';
|
|
7
|
+
export { LIFECYCLE_EVENTS, isLifecycleEvent } from './lifecycle-catalog.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PluginManifest } from '../../types.js';
|
|
2
2
|
import type { HookDiscoveryIssue, HookPluginValidation } from './discovery.js';
|
|
3
|
-
import type { DeclaredHook } from './schema.js';
|
|
4
|
-
export declare const HOOK_TRUST_WARNING = "
|
|
3
|
+
import type { DeclaredHook, DeclaredLifecycleHook } from './schema.js';
|
|
4
|
+
export declare const HOOK_TRUST_WARNING = "Command hooks receive normalized inputs and may block or replace declared targets. Lifecycle hooks run unprompted at every declared event before node launch. Both run local executables with runtime authority and may transmit received data.";
|
|
5
5
|
export interface HookDeclarationReport {
|
|
6
6
|
target: string;
|
|
7
7
|
phase: DeclaredHook['phase'];
|
|
@@ -9,6 +9,13 @@ export interface HookDeclarationReport {
|
|
|
9
9
|
description: string;
|
|
10
10
|
effects: readonly string[];
|
|
11
11
|
}
|
|
12
|
+
export interface LifecycleHookDeclarationReport {
|
|
13
|
+
event: DeclaredLifecycleHook['event'];
|
|
14
|
+
phase: DeclaredLifecycleHook['phase'];
|
|
15
|
+
op: string;
|
|
16
|
+
description: string;
|
|
17
|
+
effects: readonly string[];
|
|
18
|
+
}
|
|
12
19
|
/** One static hook artifact report shared by plugin inspection and lifecycle
|
|
13
20
|
* output. It only reads declarations and filesystem metadata; it never runs
|
|
14
21
|
* the hook executable. */
|
|
@@ -16,6 +23,8 @@ export interface HookReport {
|
|
|
16
23
|
manifestPath?: string;
|
|
17
24
|
executablePath?: string;
|
|
18
25
|
declarations: readonly HookDeclarationReport[];
|
|
26
|
+
/** Present only for schemaVersion 2 manifests. */
|
|
27
|
+
lifecycle?: readonly LifecycleHookDeclarationReport[];
|
|
19
28
|
issues: readonly HookDiscoveryIssue[];
|
|
20
29
|
trust: typeof HOOK_TRUST_WARNING;
|
|
21
30
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const HOOK_TRUST_WARNING = '
|
|
1
|
+
export const HOOK_TRUST_WARNING = 'Command hooks receive normalized inputs and may block or replace declared targets. Lifecycle hooks run unprompted at every declared event before node launch. Both run local executables with runtime authority and may transmit received data.';
|
|
2
2
|
export function hasDeclaredHooks(manifest) {
|
|
3
3
|
return manifest.hooks !== undefined || manifest.hookExecutable !== undefined;
|
|
4
4
|
}
|
|
@@ -13,6 +13,15 @@ export function hookReport(validation, issues = validation.issues) {
|
|
|
13
13
|
description: hook.description,
|
|
14
14
|
effects: Object.freeze([...hook.effects]),
|
|
15
15
|
}))),
|
|
16
|
+
...(validation.manifest?.schemaVersion === 2 ? {
|
|
17
|
+
lifecycle: Object.freeze(validation.manifest.lifecycle.map((hook) => Object.freeze({
|
|
18
|
+
event: hook.event,
|
|
19
|
+
phase: hook.phase,
|
|
20
|
+
op: hook.op,
|
|
21
|
+
description: hook.description,
|
|
22
|
+
effects: Object.freeze([...hook.effects]),
|
|
23
|
+
}))),
|
|
24
|
+
} : {}),
|
|
16
25
|
issues: Object.freeze([...issues]),
|
|
17
26
|
trust: HOOK_TRUST_WARNING,
|
|
18
27
|
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { type LifecycleEvent } from './lifecycle-catalog.js';
|
|
1
2
|
export type HookPhase = 'before' | 'after' | 'replace';
|
|
3
|
+
export type LifecycleHookPhase = 'on';
|
|
2
4
|
export interface DeclaredHook {
|
|
3
5
|
target: string;
|
|
4
6
|
phase: HookPhase;
|
|
@@ -6,10 +8,23 @@ export interface DeclaredHook {
|
|
|
6
8
|
description: string;
|
|
7
9
|
effects: string[];
|
|
8
10
|
}
|
|
9
|
-
export interface
|
|
11
|
+
export interface DeclaredLifecycleHook {
|
|
12
|
+
event: LifecycleEvent;
|
|
13
|
+
phase: LifecycleHookPhase;
|
|
14
|
+
op: string;
|
|
15
|
+
description: string;
|
|
16
|
+
effects: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface ValidatedV1HookManifest {
|
|
10
19
|
schemaVersion: 1;
|
|
11
20
|
hooks: DeclaredHook[];
|
|
12
21
|
}
|
|
22
|
+
export interface ValidatedV2HookManifest {
|
|
23
|
+
schemaVersion: 2;
|
|
24
|
+
hooks: DeclaredHook[];
|
|
25
|
+
lifecycle: DeclaredLifecycleHook[];
|
|
26
|
+
}
|
|
27
|
+
export type ValidatedHookManifest = ValidatedV1HookManifest | ValidatedV2HookManifest;
|
|
13
28
|
export type HookIssueCode = 'hook_manifest_unreadable' | 'hook_manifest_invalid' | 'hook_schema_version' | 'hook_path_unsafe' | 'hook_not_executable';
|
|
14
29
|
export interface HookManifestIssue {
|
|
15
30
|
code: HookIssueCode;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isRecord } from '../../shared/predicates.js';
|
|
2
|
+
import { LIFECYCLE_EVENTS, isLifecycleEvent } from './lifecycle-catalog.js';
|
|
2
3
|
function typeName(value) {
|
|
3
4
|
if (value === null)
|
|
4
5
|
return 'null';
|
|
@@ -6,6 +7,98 @@ function typeName(value) {
|
|
|
6
7
|
return 'array';
|
|
7
8
|
return typeof value;
|
|
8
9
|
}
|
|
10
|
+
function validateCommandDeclaration(value, index, issue) {
|
|
11
|
+
const path = `hooks[${index}]`;
|
|
12
|
+
if (!isRecord(value)) {
|
|
13
|
+
issue('hook_manifest_invalid', 'hook must be an object', typeName(value), '{ target, phase, op, description, effects }', 'Fix the hook declaration.', path);
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const keys = Object.keys(value).filter((key) => !['target', 'phase', 'op', 'description', 'effects'].includes(key));
|
|
17
|
+
if (keys.length > 0) {
|
|
18
|
+
issue('hook_manifest_invalid', 'unknown hook keys', keys.join(', '), 'only: target, phase, op, description, effects', 'Remove the unknown keys.', path);
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
for (const field of ['target', 'op', 'description']) {
|
|
22
|
+
if (typeof value[field] !== 'string' || value[field].length === 0) {
|
|
23
|
+
issue('hook_manifest_invalid', `${field} must be a non-empty string`, typeName(value[field]), 'a non-empty string', `Set ${field}.`, `${path}.${field}`);
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (value['phase'] !== 'before' && value['phase'] !== 'after' && value['phase'] !== 'replace') {
|
|
28
|
+
issue('hook_manifest_invalid', 'phase must be before|after|replace', String(value['phase']), 'before | after | replace', 'Set phase to a supported hook phase.', `${path}.phase`);
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
if (!isEffects(value['effects'])) {
|
|
32
|
+
issue('hook_manifest_invalid', 'effects must be a non-empty string array', typeName(value['effects']), 'a non-empty array of non-empty strings', 'Declare every persistent effect.', `${path}.effects`);
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
target: value['target'],
|
|
37
|
+
phase: value['phase'],
|
|
38
|
+
op: value['op'],
|
|
39
|
+
description: value['description'],
|
|
40
|
+
effects: value['effects'],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function validateLifecycleDeclaration(value, index, issue) {
|
|
44
|
+
const path = `lifecycle[${index}]`;
|
|
45
|
+
if (!isRecord(value)) {
|
|
46
|
+
issue('hook_manifest_invalid', 'lifecycle hook must be an object', typeName(value), '{ event, phase, op, description, effects }', 'Fix the lifecycle declaration.', path);
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const keys = Object.keys(value).filter((key) => !['event', 'phase', 'op', 'description', 'effects'].includes(key));
|
|
50
|
+
if (keys.length > 0) {
|
|
51
|
+
issue('hook_manifest_invalid', 'unknown lifecycle hook keys', keys.join(', '), 'only: event, phase, op, description, effects', 'Remove the unknown keys.', path);
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
for (const field of ['event', 'op', 'description']) {
|
|
55
|
+
if (typeof value[field] !== 'string' || value[field].length === 0) {
|
|
56
|
+
issue('hook_manifest_invalid', `${field} must be a non-empty string`, typeName(value[field]), 'a non-empty string', `Set ${field}.`, `${path}.${field}`);
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!isLifecycleEvent(value['event'])) {
|
|
61
|
+
issue('hook_manifest_invalid', 'event must name a supported lifecycle event', String(value['event']), LIFECYCLE_EVENTS.join(' | '), `Set event to one of: ${LIFECYCLE_EVENTS.join(', ')}.`, `${path}.event`);
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
if (value['phase'] !== 'on') {
|
|
65
|
+
issue('hook_manifest_invalid', 'phase must be on', String(value['phase']), 'on', 'Set phase to on.', `${path}.phase`);
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
if (!isEffects(value['effects'])) {
|
|
69
|
+
issue('hook_manifest_invalid', 'effects must be a non-empty string array', typeName(value['effects']), 'a non-empty array of non-empty strings', 'Declare every persistent effect.', `${path}.effects`);
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
event: value['event'],
|
|
74
|
+
phase: 'on',
|
|
75
|
+
op: value['op'],
|
|
76
|
+
description: value['description'],
|
|
77
|
+
effects: value['effects'],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function isEffects(value) {
|
|
81
|
+
return Array.isArray(value) && value.length > 0 && value.every((effect) => typeof effect === 'string' && effect.length > 0);
|
|
82
|
+
}
|
|
83
|
+
function validateV1(raw, issue) {
|
|
84
|
+
const unknown = Object.keys(raw).filter((key) => key !== 'schemaVersion' && key !== 'hooks');
|
|
85
|
+
if (unknown.length > 0) {
|
|
86
|
+
issue('hook_manifest_invalid', 'unknown top-level keys', unknown.join(', '), 'only: schemaVersion, hooks', 'Remove the unknown keys.');
|
|
87
|
+
return { issues: [] };
|
|
88
|
+
}
|
|
89
|
+
if (!Array.isArray(raw['hooks']) || raw['hooks'].length === 0) {
|
|
90
|
+
issue('hook_manifest_invalid', 'hooks must be a non-empty array', typeName(raw['hooks']), 'a non-empty array of hook declarations', 'Declare at least one hook.', 'hooks');
|
|
91
|
+
return { issues: [] };
|
|
92
|
+
}
|
|
93
|
+
const hooks = [];
|
|
94
|
+
for (let index = 0; index < raw['hooks'].length; index++) {
|
|
95
|
+
const hook = validateCommandDeclaration(raw['hooks'][index], index, issue);
|
|
96
|
+
if (hook === undefined)
|
|
97
|
+
return { issues: [] };
|
|
98
|
+
hooks.push(hook);
|
|
99
|
+
}
|
|
100
|
+
return { manifest: { schemaVersion: 1, hooks }, issues: [] };
|
|
101
|
+
}
|
|
9
102
|
/** Validates the declarative hooks.json payload without filesystem access. */
|
|
10
103
|
export function validateHookManifest(raw) {
|
|
11
104
|
const issues = [];
|
|
@@ -16,53 +109,48 @@ export function validateHookManifest(raw) {
|
|
|
16
109
|
issue('hook_manifest_invalid', 'manifest must be an object', typeName(raw), '{ schemaVersion, hooks }', 'Provide a valid hooks.json manifest.');
|
|
17
110
|
return { issues };
|
|
18
111
|
}
|
|
19
|
-
|
|
112
|
+
if (raw['schemaVersion'] === 1) {
|
|
113
|
+
const validation = validateV1(raw, issue);
|
|
114
|
+
return { ...(validation.manifest === undefined ? {} : { manifest: validation.manifest }), issues };
|
|
115
|
+
}
|
|
116
|
+
if (raw['schemaVersion'] !== 2) {
|
|
117
|
+
issue('hook_schema_version', 'schemaVersion must be exactly 1 or 2', String(raw['schemaVersion']), '1 | 2', 'Update the manifest schema version to 1 or 2.', 'schemaVersion');
|
|
118
|
+
return { issues };
|
|
119
|
+
}
|
|
120
|
+
const unknown = Object.keys(raw).filter((key) => key !== 'schemaVersion' && key !== 'hooks' && key !== 'lifecycle');
|
|
20
121
|
if (unknown.length > 0) {
|
|
21
|
-
issue('hook_manifest_invalid', 'unknown top-level keys', unknown.join(', '), 'only: schemaVersion, hooks', 'Remove the unknown keys.');
|
|
122
|
+
issue('hook_manifest_invalid', 'unknown top-level keys', unknown.join(', '), 'only: schemaVersion, hooks, lifecycle', 'Remove the unknown keys.');
|
|
22
123
|
return { issues };
|
|
23
124
|
}
|
|
24
|
-
|
|
25
|
-
|
|
125
|
+
const rawHooks = raw['hooks'];
|
|
126
|
+
const rawLifecycle = raw['lifecycle'];
|
|
127
|
+
if (rawHooks !== undefined && !Array.isArray(rawHooks)) {
|
|
128
|
+
issue('hook_manifest_invalid', 'hooks must be an array when present', typeName(rawHooks), 'an array of hook declarations', 'Fix hooks.', 'hooks');
|
|
26
129
|
return { issues };
|
|
27
130
|
}
|
|
28
|
-
if (!Array.isArray(
|
|
29
|
-
issue('hook_manifest_invalid', '
|
|
131
|
+
if (rawLifecycle !== undefined && !Array.isArray(rawLifecycle)) {
|
|
132
|
+
issue('hook_manifest_invalid', 'lifecycle must be an array when present', typeName(rawLifecycle), 'an array of lifecycle hook declarations', 'Fix lifecycle.', 'lifecycle');
|
|
30
133
|
return { issues };
|
|
31
134
|
}
|
|
32
|
-
const hooks = [];
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
issue('hook_manifest_invalid', 'unknown hook keys', keys.join(', '), 'only: target, phase, op, description, effects', 'Remove the unknown keys.', path);
|
|
43
|
-
return { issues };
|
|
44
|
-
}
|
|
45
|
-
for (const field of ['target', 'op', 'description']) {
|
|
46
|
-
if (typeof value[field] !== 'string' || value[field].length === 0) {
|
|
47
|
-
issue('hook_manifest_invalid', `${field} must be a non-empty string`, typeName(value[field]), 'a non-empty string', `Set ${field}.`, `${path}.${field}`);
|
|
48
|
-
return { issues };
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
if (value['phase'] !== 'before' && value['phase'] !== 'after' && value['phase'] !== 'replace') {
|
|
52
|
-
issue('hook_manifest_invalid', 'phase must be before|after|replace', String(value['phase']), 'before | after | replace', 'Set phase to a supported hook phase.', `${path}.phase`);
|
|
135
|
+
const hooks = rawHooks ?? [];
|
|
136
|
+
const lifecycle = rawLifecycle ?? [];
|
|
137
|
+
if (hooks.length === 0 && lifecycle.length === 0) {
|
|
138
|
+
issue('hook_manifest_invalid', 'hooks or lifecycle must be a non-empty array', 'both arrays were empty or absent', 'at least one command or lifecycle hook declaration', 'Declare at least one hook.', 'hooks');
|
|
139
|
+
return { issues };
|
|
140
|
+
}
|
|
141
|
+
const declaredHooks = [];
|
|
142
|
+
for (let index = 0; index < hooks.length; index++) {
|
|
143
|
+
const hook = validateCommandDeclaration(hooks[index], index, issue);
|
|
144
|
+
if (hook === undefined)
|
|
53
145
|
return { issues };
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
146
|
+
declaredHooks.push(hook);
|
|
147
|
+
}
|
|
148
|
+
const declaredLifecycle = [];
|
|
149
|
+
for (let index = 0; index < lifecycle.length; index++) {
|
|
150
|
+
const hook = validateLifecycleDeclaration(lifecycle[index], index, issue);
|
|
151
|
+
if (hook === undefined)
|
|
57
152
|
return { issues };
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
phase: value['phase'],
|
|
62
|
-
op: value['op'],
|
|
63
|
-
description: value['description'],
|
|
64
|
-
effects: value['effects'],
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
return { manifest: { schemaVersion: 1, hooks }, issues };
|
|
153
|
+
declaredLifecycle.push(hook);
|
|
154
|
+
}
|
|
155
|
+
return { manifest: { schemaVersion: 2, hooks: declaredHooks, lifecycle: declaredLifecycle }, issues };
|
|
68
156
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LifecycleHookRequest } from '../../../hook-authoring.js';
|
|
2
|
+
import type { EffectiveLifecycleHook } from '../discovery.js';
|
|
3
|
+
export declare const LIFECYCLE_HOOK_TIMEOUT_MS = 5000;
|
|
4
|
+
export declare const LIFECYCLE_EVENT_BUDGET_MS = 10000;
|
|
5
|
+
export interface LifecycleHookInvocation {
|
|
6
|
+
node: LifecycleHookRequest['node'];
|
|
7
|
+
runtime: LifecycleHookRequest['runtime'];
|
|
8
|
+
}
|
|
9
|
+
/** Runs one lifecycle hook. The node launch remains authoritative on every failure. */
|
|
10
|
+
export declare function invokeLifecycleHook(hook: EffectiveLifecycleHook, invocation: LifecycleHookInvocation, timeoutMs: number): Promise<void>;
|
|
11
|
+
/** Executes lifecycle hooks in discovery order, within one bounded start-event budget. */
|
|
12
|
+
export declare function invokeLifecycleHooks(hooks: readonly EffectiveLifecycleHook[], invocation: LifecycleHookInvocation): Promise<void>;
|