agentic-workflow-manager 9.11.11 → 9.12.0
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/src/commands/doctor.js +23 -1
- package/dist/src/commands/hooks/claude.js +61 -8
- package/dist/src/commands/hooks/resync.js +1 -1
- package/dist/src/commands/job/index.js +39 -4
- package/dist/src/commands/model-policy/index.js +253 -0
- package/dist/src/commands/plan/index.js +53 -12
- package/dist/src/commands/preflight/checks.js +18 -0
- package/dist/src/commands/watch/apply.js +71 -5
- package/dist/src/commands/watch/supervisor.js +63 -3
- package/dist/src/core/admission/index.js +20 -6
- package/dist/src/core/journal/types.js +21 -5
- package/dist/src/core/model-policy/capabilities-v2.js +152 -0
- package/dist/src/core/model-policy/capabilities.js +7 -1
- package/dist/src/core/model-policy/claude-hook-events.js +120 -0
- package/dist/src/core/model-policy/event-store.js +221 -0
- package/dist/src/core/model-policy/hook-diagnostics.js +94 -0
- package/dist/src/core/model-policy/journal.js +125 -16
- package/dist/src/core/model-policy/local-claude-scope.js +41 -0
- package/dist/src/core/model-policy/local-codex-scope.js +33 -0
- package/dist/src/core/model-policy/local-event-scope.js +13 -0
- package/dist/src/core/model-policy/machine-key.js +56 -0
- package/dist/src/core/model-policy/native-claude-agent-definition.js +82 -0
- package/dist/src/core/model-policy/native-claude-config.js +107 -0
- package/dist/src/core/model-policy/native-claude-machine.js +58 -0
- package/dist/src/core/model-policy/native-claude-transcript.js +80 -0
- package/dist/src/core/model-policy/native-claude.js +41 -0
- package/dist/src/core/model-policy/native-codex-machine.js +116 -0
- package/dist/src/core/model-policy/native-codex-rollout.js +111 -0
- package/dist/src/core/model-policy/native-codex.js +302 -0
- package/dist/src/core/model-policy/native-routing-observe.js +106 -0
- package/dist/src/core/model-policy/native-routing-proof.js +52 -0
- package/dist/src/core/model-policy/native-runtime.js +86 -0
- package/dist/src/core/model-policy/resolve.js +117 -3
- package/dist/src/core/model-policy/selection-policy-digest.js +22 -0
- package/dist/src/core/model-policy/setup-readiness.js +77 -0
- package/dist/src/providers/index.js +3 -1
- package/dist/tests/commands/doctor-routing.test.js +22 -0
- package/dist/tests/commands/doctor.test.js +5 -0
- package/dist/tests/commands/hooks/install.test.js +6 -0
- package/dist/tests/commands/hooks/resync.test.js +3 -0
- package/dist/tests/commands/hooks/status.test.js +12 -1
- package/dist/tests/commands/job/routing-file.test.js +32 -0
- package/dist/tests/commands/job/routing-report-cli.test.js +12 -5
- package/dist/tests/commands/job/routing.test.js +2 -2
- package/dist/tests/commands/model-policy/claude-hook-flow.test.js +104 -0
- package/dist/tests/commands/model-policy/index.test.js +179 -0
- package/dist/tests/commands/plan/index.test.js +2 -2
- package/dist/tests/commands/preflight/preflight.test.js +65 -0
- package/dist/tests/commands/watch/apply.test.js +116 -10
- package/dist/tests/commands/watch/dispatch-routing-identity.test.js +23 -0
- package/dist/tests/core/admission/index.test.js +21 -0
- package/dist/tests/core/model-policy/capabilities-v2.test.js +42 -0
- package/dist/tests/core/model-policy/claude-hook-events.test.js +40 -0
- package/dist/tests/core/model-policy/event-store-claude.test.js +64 -0
- package/dist/tests/core/model-policy/event-store.test.js +151 -0
- package/dist/tests/core/model-policy/hook-diagnostics.test.js +34 -0
- package/dist/tests/core/model-policy/journal.test.js +165 -12
- package/dist/tests/core/model-policy/local-claude-scope.test.js +31 -0
- package/dist/tests/core/model-policy/machine-key.test.js +38 -0
- package/dist/tests/core/model-policy/native-claude-agent-definition.test.js +31 -0
- package/dist/tests/core/model-policy/native-claude-config.test.js +53 -0
- package/dist/tests/core/model-policy/native-claude-machine.test.js +18 -0
- package/dist/tests/core/model-policy/native-claude-transcript.test.js +45 -0
- package/dist/tests/core/model-policy/native-claude.test.js +21 -0
- package/dist/tests/core/model-policy/native-codex-machine.test.js +34 -0
- package/dist/tests/core/model-policy/native-codex-rollout.test.js +44 -0
- package/dist/tests/core/model-policy/native-codex.test.js +133 -0
- package/dist/tests/core/model-policy/native-routing-observe.test.js +60 -0
- package/dist/tests/core/model-policy/native-routing-proof.test.js +30 -0
- package/dist/tests/core/model-policy/native-runtime.test.js +14 -0
- package/dist/tests/core/model-policy/resolve-event.test.js +65 -0
- package/dist/tests/core/model-policy/resolve.test.js +17 -0
- package/dist/tests/core/model-policy/setup-readiness.test.js +83 -0
- package/dist/tests/helpers/routing-approval.js +14 -0
- package/dist/tests/providers/index.test.js +1 -1
- package/package.json +2 -2
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.observeNativeRoutingChild = observeNativeRoutingChild;
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const paths_1 = require("../paths");
|
|
11
|
+
const capabilities_v2_1 = require("./capabilities-v2");
|
|
12
|
+
const event_store_1 = require("./event-store");
|
|
13
|
+
const local_event_scope_1 = require("./local-event-scope");
|
|
14
|
+
const machine_key_1 = require("./machine-key");
|
|
15
|
+
const capabilities_1 = require("./capabilities");
|
|
16
|
+
const native_codex_1 = require("./native-codex");
|
|
17
|
+
const native_routing_proof_1 = require("./native-routing-proof");
|
|
18
|
+
const selection_policy_digest_1 = require("./selection-policy-digest");
|
|
19
|
+
const store_1 = require("./store");
|
|
20
|
+
function sameSelection(left, right) {
|
|
21
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
22
|
+
}
|
|
23
|
+
/** A bounded, read-mostly native observation for an already-dispatched child.
|
|
24
|
+
* Codex may refresh only its matching claim; Claude's Stop hook is the writer.
|
|
25
|
+
* No inference is started here. */
|
|
26
|
+
async function observeNativeRoutingChild(input) {
|
|
27
|
+
if (!input || !input.attempt?.nativeEvidenceRequired || !input.attempt.reservedAt || input.attempt.state !== 'reserved'
|
|
28
|
+
|| typeof input.nativeAgentId !== 'string' || input.nativeAgentId.length === 0 || input.nativeAgentId.length > 128
|
|
29
|
+
|| typeof input.cwd !== 'string' || !path_1.default.isAbsolute(input.cwd) || !(input.now instanceof Date) || !Number.isFinite(input.now.getTime())
|
|
30
|
+
|| !Array.isArray(input.consumedEventDigests) || input.consumedEventDigests.length > 4096
|
|
31
|
+
|| input.consumedEventDigests.some(item => typeof item !== 'string' || !/^[a-f0-9]{64}$/.test(item)))
|
|
32
|
+
throw new Error('native routing observation is invalid');
|
|
33
|
+
const key = (0, machine_key_1.readMachineKey)((0, paths_1.awmHome)());
|
|
34
|
+
if (!key)
|
|
35
|
+
throw new Error('native routing machine key is missing');
|
|
36
|
+
const envelope = input.attempt.envelope;
|
|
37
|
+
const local = await (0, local_event_scope_1.queryLocalEventScope)(input.cwd, envelope.runtime.target, envelope.runtime.kind);
|
|
38
|
+
if (local.state !== 'current' || JSON.stringify(local.scope.runtime) !== JSON.stringify(envelope.runtime))
|
|
39
|
+
throw new Error('native routing runtime or account scope drifted');
|
|
40
|
+
const policy = (0, store_1.readEffectivePolicy)(input.cwd);
|
|
41
|
+
if (policy.state !== 'approved' || policy.policy.contentDigest !== envelope.policyDigest)
|
|
42
|
+
throw new Error('native routing policy drifted');
|
|
43
|
+
const mapping = policy.policy.content.mappings.find(row => row.target === envelope.runtime.target && row.runtimeKind === envelope.runtime.kind);
|
|
44
|
+
if (!mapping)
|
|
45
|
+
throw new Error('native routing mapping is absent');
|
|
46
|
+
const receipt = (0, event_store_1.readStoredEventReceipt)((0, capabilities_1.validateRuntimeKey)(envelope.runtime));
|
|
47
|
+
if (receipt.state !== 'present')
|
|
48
|
+
throw new Error('native routing receipt is absent or invalid');
|
|
49
|
+
const currentness = (0, capabilities_v2_1.evaluateEventReceipt)(receipt.receipt, local.scope, envelope.resolved, (0, selection_policy_digest_1.selectionPolicyDigest)(mapping, envelope.resolved), input.now);
|
|
50
|
+
if (currentness.state !== 'current')
|
|
51
|
+
throw new Error(`native routing receipt drifted: ${currentness.reason}`);
|
|
52
|
+
let observed;
|
|
53
|
+
let observedAt;
|
|
54
|
+
let eventDigest;
|
|
55
|
+
let source;
|
|
56
|
+
let actualModel;
|
|
57
|
+
if (envelope.runtime.target === 'codex') {
|
|
58
|
+
if (typeof input.parentThreadId !== 'string' || input.parentThreadId.length === 0 || input.parentThreadId.length > 128)
|
|
59
|
+
throw new Error('Codex parent thread identity is required');
|
|
60
|
+
const child = await (0, native_codex_1.queryCodexChildThreadObservation)({ command: 'codex', args: ['app-server', '--stdio'], timeoutMs: 10000,
|
|
61
|
+
parentThreadId: input.parentThreadId, childThreadId: input.nativeAgentId,
|
|
62
|
+
codexHome: path_1.default.resolve(process.env.CODEX_HOME ?? path_1.default.join(os_1.default.homedir(), '.codex')) });
|
|
63
|
+
if (!child.acceptedSelectionVerified || child.childRuntimeVersion !== envelope.runtime.version || !child.observedAt)
|
|
64
|
+
throw new Error('Codex child turn selection is not natively verified');
|
|
65
|
+
observed = child.configuredSelection;
|
|
66
|
+
observedAt = child.observedAt;
|
|
67
|
+
const codexObservedMs = Date.parse(observedAt);
|
|
68
|
+
if (!Number.isFinite(codexObservedMs) || codexObservedMs < Date.parse(input.attempt.reservedAt)
|
|
69
|
+
|| codexObservedMs > input.now.getTime() + 60_000 || input.now.getTime() - codexObservedMs > 15 * 60_000)
|
|
70
|
+
throw new Error('Codex native child turn is stale or predates reservation');
|
|
71
|
+
eventDigest = (0, crypto_1.createHash)('sha256').update(JSON.stringify({ parent: child.parentThreadId, child: child.childThreadId,
|
|
72
|
+
turn: child.turnId, selection: observed, runtimeVersion: child.childRuntimeVersion,
|
|
73
|
+
accountScopeDigest: local.scope.runtime.accountScopeDigest })).digest('hex');
|
|
74
|
+
source = 'codex-turn-context';
|
|
75
|
+
if (input.consumedEventDigests.includes(eventDigest))
|
|
76
|
+
throw new Error('Codex native child turn was already consumed');
|
|
77
|
+
if (sameSelection(observed, envelope.resolved)) {
|
|
78
|
+
(0, event_store_1.publishCodexObservation)({ scope: local.scope, observation: child, expectedSelection: envelope.resolved,
|
|
79
|
+
mappingDigest: (0, selection_policy_digest_1.selectionPolicyDigest)(mapping, envelope.resolved), now: input.now });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (envelope.runtime.target === 'claude-code') {
|
|
83
|
+
const claim = receipt.receipt.claims.find(item => sameSelection(item.selection, envelope.resolved));
|
|
84
|
+
const agentDigest = (0, event_store_1.claudeAgentIdDigest)(input.nativeAgentId, key);
|
|
85
|
+
const event = claim?.nativeEvents?.find(item => item.nativeAgentIdDigest === agentDigest)
|
|
86
|
+
?? (claim?.nativeAgentIdDigest === agentDigest ? { eventDigest: claim.eventDigest, observedAt: claim.observedAt } : undefined);
|
|
87
|
+
if (!claim || claim.source !== 'claude-assistant-transcript' || claim.nativeAgentType !== envelope.nativeAgentType || !event)
|
|
88
|
+
throw new Error('Claude Stop hook does not match this routed native child');
|
|
89
|
+
observed = claim.selection;
|
|
90
|
+
observedAt = event.observedAt;
|
|
91
|
+
eventDigest = event.eventDigest;
|
|
92
|
+
source = 'claude-assistant-transcript';
|
|
93
|
+
if (claim.actualModel !== 'unverified')
|
|
94
|
+
actualModel = claim.actualModel.id;
|
|
95
|
+
if (input.consumedEventDigests.includes(eventDigest))
|
|
96
|
+
throw new Error('Claude native child event was already consumed');
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
throw new Error('native routing provider is unsupported');
|
|
100
|
+
const observedMs = Date.parse(observedAt);
|
|
101
|
+
if (!Number.isFinite(observedMs) || observedMs < Date.parse(input.attempt.reservedAt)
|
|
102
|
+
|| observedMs > input.now.getTime() + 60_000 || input.now.getTime() - observedMs > 15 * 60_000)
|
|
103
|
+
throw new Error('native routing event is stale or predates reservation');
|
|
104
|
+
return (0, native_routing_proof_1.signNativeRoutingProof)({ attemptId: input.attempt.id, nativeAgentId: input.nativeAgentId, source,
|
|
105
|
+
eventDigest, observedAt, selection: observed, ...(actualModel === undefined ? {} : { actualModel }) }, key);
|
|
106
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.signNativeRoutingProof = signNativeRoutingProof;
|
|
4
|
+
exports.verifyNativeRoutingProof = verifyNativeRoutingProof;
|
|
5
|
+
const crypto_1 = require("crypto");
|
|
6
|
+
const types_1 = require("../journal/types");
|
|
7
|
+
const DOMAIN = 'AWM native routing attempt/v1\0';
|
|
8
|
+
function validBody(value) {
|
|
9
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
10
|
+
return false;
|
|
11
|
+
const row = value;
|
|
12
|
+
return Object.keys(row).length === (row.actualModel === undefined ? 6 : 7) && Object.keys(row).every(key => ['attemptId', 'nativeAgentId', 'source', 'eventDigest', 'observedAt', 'selection', 'actualModel'].includes(key))
|
|
13
|
+
&& typeof row.attemptId === 'string' && row.attemptId.length > 0 && row.attemptId.length <= 128
|
|
14
|
+
&& typeof row.nativeAgentId === 'string' && row.nativeAgentId.length > 0 && row.nativeAgentId.length <= 128
|
|
15
|
+
&& (row.source === 'codex-turn-context' || row.source === 'claude-assistant-transcript')
|
|
16
|
+
&& typeof row.eventDigest === 'string' && /^[a-f0-9]{64}$/.test(row.eventDigest)
|
|
17
|
+
&& typeof row.observedAt === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(row.observedAt)
|
|
18
|
+
&& Number.isFinite(Date.parse(row.observedAt)) && (0, types_1.isRoutingSelection)(row.selection)
|
|
19
|
+
&& (row.actualModel === undefined || (row.source === 'claude-assistant-transcript' && typeof row.actualModel === 'string' && row.actualModel.length > 0 && row.actualModel.length <= 128));
|
|
20
|
+
}
|
|
21
|
+
function signature(body, key) {
|
|
22
|
+
if (!Buffer.isBuffer(key) || key.length !== 32)
|
|
23
|
+
throw new Error('native routing machine key is invalid');
|
|
24
|
+
return (0, crypto_1.createHmac)('sha256', key).update(DOMAIN).update(JSON.stringify(body)).digest('hex');
|
|
25
|
+
}
|
|
26
|
+
function signNativeRoutingProof(input, key) {
|
|
27
|
+
if (!validBody(input))
|
|
28
|
+
throw new Error('native routing proof body is invalid');
|
|
29
|
+
const body = { attemptId: input.attemptId, nativeAgentId: input.nativeAgentId, source: input.source,
|
|
30
|
+
eventDigest: input.eventDigest, observedAt: input.observedAt, selection: input.selection,
|
|
31
|
+
...(input.actualModel === undefined ? {} : { actualModel: input.actualModel }) };
|
|
32
|
+
return { schema: 'native-routing-proof/v1', ...body, mac: signature(body, key) };
|
|
33
|
+
}
|
|
34
|
+
function verifyNativeRoutingProof(value, expected, key, now) {
|
|
35
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || !expected || typeof expected !== 'object'
|
|
36
|
+
|| !(now instanceof Date) || !Number.isFinite(now.getTime()))
|
|
37
|
+
return false;
|
|
38
|
+
const row = value;
|
|
39
|
+
if (Object.keys(row).length !== (row.actualModel === undefined ? 8 : 9) || row.schema !== 'native-routing-proof/v1' || typeof row.mac !== 'string' || !/^[a-f0-9]{64}$/.test(row.mac))
|
|
40
|
+
return false;
|
|
41
|
+
const body = { attemptId: row.attemptId, nativeAgentId: row.nativeAgentId, source: row.source,
|
|
42
|
+
eventDigest: row.eventDigest, observedAt: row.observedAt, selection: row.selection,
|
|
43
|
+
...(row.actualModel === undefined ? {} : { actualModel: row.actualModel }) };
|
|
44
|
+
if (!validBody(body) || body.attemptId !== expected.attemptId || body.nativeAgentId !== expected.nativeAgentId
|
|
45
|
+
|| !(0, types_1.isRoutingSelection)(expected.selection)
|
|
46
|
+
|| typeof expected.reservedAt !== 'string' || !Number.isFinite(Date.parse(expected.reservedAt)))
|
|
47
|
+
return false;
|
|
48
|
+
const observedMs = Date.parse(body.observedAt);
|
|
49
|
+
if (observedMs < Date.parse(expected.reservedAt) || observedMs > now.getTime() + 60_000 || now.getTime() - observedMs > 15 * 60_000)
|
|
50
|
+
return false;
|
|
51
|
+
return (0, crypto_1.timingSafeEqual)(Buffer.from(row.mac, 'hex'), Buffer.from(signature(body, key), 'hex'));
|
|
52
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.queryRuntimeExecutable = queryRuntimeExecutable;
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
function executablePath(command) {
|
|
12
|
+
if (typeof command !== 'string' || command.length === 0 || command.length > 4096 || /[\u0000-\u001f\u007f-\u009f]/.test(command))
|
|
13
|
+
throw new Error('runtime executable command is invalid');
|
|
14
|
+
const names = process.platform === 'win32' && path_1.default.extname(command) === '' ? [command, `${command}.exe`] : [command];
|
|
15
|
+
const candidates = path_1.default.isAbsolute(command) ? names : (process.env.PATH ?? '').split(path_1.default.delimiter).filter(Boolean).flatMap(dir => names.map(name => path_1.default.join(dir, name)));
|
|
16
|
+
for (const candidate of candidates) {
|
|
17
|
+
try {
|
|
18
|
+
fs_1.default.accessSync(candidate, fs_1.default.constants.X_OK);
|
|
19
|
+
const real = fs_1.default.realpathSync(candidate);
|
|
20
|
+
if (!fs_1.default.statSync(real).isFile())
|
|
21
|
+
continue;
|
|
22
|
+
return real;
|
|
23
|
+
}
|
|
24
|
+
catch { /* keep looking */ }
|
|
25
|
+
}
|
|
26
|
+
throw new Error('runtime executable is unavailable');
|
|
27
|
+
}
|
|
28
|
+
function versionOf(file, timeoutMs) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const child = (0, child_process_1.spawn)(file, ['--version'], { stdio: ['ignore', 'pipe', 'pipe'], shell: false });
|
|
31
|
+
let settled = false;
|
|
32
|
+
let output = '';
|
|
33
|
+
const finish = (error, version) => {
|
|
34
|
+
if (settled)
|
|
35
|
+
return;
|
|
36
|
+
settled = true;
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
if (error) {
|
|
39
|
+
child.kill();
|
|
40
|
+
reject(error);
|
|
41
|
+
}
|
|
42
|
+
else
|
|
43
|
+
resolve(version);
|
|
44
|
+
};
|
|
45
|
+
const timer = setTimeout(() => finish(new Error('runtime version query timed out')), timeoutMs);
|
|
46
|
+
child.on('error', () => finish(new Error('runtime executable could not start')));
|
|
47
|
+
child.stdout.setEncoding('utf8');
|
|
48
|
+
child.stdout.on('data', (chunk) => { output += chunk; if (output.length > 4096)
|
|
49
|
+
finish(new Error('runtime version output exceeds limit')); });
|
|
50
|
+
child.on('exit', code => {
|
|
51
|
+
if (code !== 0)
|
|
52
|
+
return finish(new Error('runtime version command failed'));
|
|
53
|
+
const match = output.match(/\b(?:v)?(\d+\.\d+\.\d+)\b/);
|
|
54
|
+
if (!match)
|
|
55
|
+
return finish(new Error('runtime version is unavailable'));
|
|
56
|
+
finish(undefined, match[1]);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function hashFile(file) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const before = fs_1.default.statSync(file);
|
|
63
|
+
if (!before.isFile() || before.size > 512 * 1024 * 1024)
|
|
64
|
+
return reject(new Error('runtime executable file is invalid or too large'));
|
|
65
|
+
const hash = (0, crypto_1.createHash)('sha256');
|
|
66
|
+
const stream = fs_1.default.createReadStream(file);
|
|
67
|
+
stream.on('data', chunk => hash.update(chunk));
|
|
68
|
+
stream.on('error', () => reject(new Error('runtime executable could not be read')));
|
|
69
|
+
stream.on('end', () => {
|
|
70
|
+
const after = fs_1.default.statSync(file);
|
|
71
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs)
|
|
72
|
+
return reject(new Error('runtime executable changed while hashing'));
|
|
73
|
+
resolve(hash.digest('hex'));
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Fingerprint the executable reached by PATH and query only --version. */
|
|
78
|
+
async function queryRuntimeExecutable(input) {
|
|
79
|
+
if (!input || typeof input !== 'object' || !Array.isArray(input.args) || input.args.length !== 1 || input.args[0] !== '--version'
|
|
80
|
+
|| !Number.isInteger(input.timeoutMs) || input.timeoutMs < 1 || input.timeoutMs > 30000)
|
|
81
|
+
throw new Error('runtime version query is invalid');
|
|
82
|
+
const file = executablePath(input.command);
|
|
83
|
+
const version = await versionOf(file, input.timeoutMs);
|
|
84
|
+
const binaryDigest = await hashFile(file);
|
|
85
|
+
return { version, binaryDigest, inferenceDispatched: false };
|
|
86
|
+
}
|
|
@@ -1,11 +1,125 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveEventSelection = resolveEventSelection;
|
|
4
|
+
exports.resolveEventWithFallback = resolveEventWithFallback;
|
|
5
|
+
exports.resolveEscalatedSelection = resolveEscalatedSelection;
|
|
3
6
|
exports.resolveDispatch = resolveDispatch;
|
|
4
7
|
exports.resolveV1Dispatch = resolveV1Dispatch;
|
|
5
8
|
exports.resolveSelection = resolveSelection;
|
|
9
|
+
const crypto_1 = require("crypto");
|
|
6
10
|
const capabilities_1 = require("./capabilities");
|
|
11
|
+
const capabilities_v2_1 = require("./capabilities-v2");
|
|
12
|
+
const selection_policy_digest_1 = require("./selection-policy-digest");
|
|
7
13
|
const validate_1 = require("./validate");
|
|
8
14
|
const validate_2 = require("../plan/validate");
|
|
15
|
+
/** Event-scoped v2 evidence has no clock expiry. It is never converted into a
|
|
16
|
+
* v1 approval or promoted to backend-model proof. */
|
|
17
|
+
function resolveEventSelection(input) {
|
|
18
|
+
if (!input || typeof input !== 'object' || !input.scope || !(input.now instanceof Date) || !Number.isFinite(input.now.getTime()))
|
|
19
|
+
throw new Error('event selection requires scope and finite clock');
|
|
20
|
+
if (!['implementer', 'specification-reviewer', 'code-quality-reviewer', 'final-reviewer', 'architecture', 'track-a-qa', 'track-b-qa', 'controller', 'documentation', 'retro', 'finishing'].includes(input.role))
|
|
21
|
+
throw new Error('event selection role is invalid');
|
|
22
|
+
if (!['mechanical', 'integration', 'judgment', 'full'].includes(input.requestedProfile))
|
|
23
|
+
throw new Error('event selection profile is invalid');
|
|
24
|
+
if (!input.policy)
|
|
25
|
+
return blocked('ROUTING_POLICY_ABSENT', 'An approved routing policy is required.');
|
|
26
|
+
if (!input.receipt)
|
|
27
|
+
return blocked('ROUTING_CAPABILITY_ABSENT', 'Native event enrollment is absent.');
|
|
28
|
+
let policy;
|
|
29
|
+
try {
|
|
30
|
+
policy = (0, validate_1.validateApprovedPolicy)(input.policy);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return blocked('ROUTING_POLICY_INVALID', 'Approved routing policy is corrupt.');
|
|
34
|
+
}
|
|
35
|
+
const mapping = policy.content.mappings.find(row => row.target === input.scope.runtime.target && row.runtimeKind === input.scope.runtime.kind);
|
|
36
|
+
if (!mapping)
|
|
37
|
+
return blocked('ROUTING_POLICY_MAPPING_ABSENT', 'Approved policy has no exact target/runtime mapping.');
|
|
38
|
+
const effectiveProfile = input.role === 'implementer' ? input.requestedProfile : 'full';
|
|
39
|
+
const wanted = effectiveProfile === 'full' ? mapping.fullCapability : mapping.profiles[effectiveProfile];
|
|
40
|
+
let receipt;
|
|
41
|
+
try {
|
|
42
|
+
receipt = (0, capabilities_v2_1.validateEventReceipt)(input.receipt);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return blocked('ROUTING_CAPABILITY_INVALID', 'Native event enrollment is corrupt.');
|
|
46
|
+
}
|
|
47
|
+
let currentness;
|
|
48
|
+
try {
|
|
49
|
+
currentness = (0, capabilities_v2_1.evaluateEventReceipt)(receipt, input.scope, wanted, (0, selection_policy_digest_1.selectionPolicyDigest)(mapping, wanted), input.now);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return blocked('ROUTING_CAPABILITY_INVALID', 'Native event enrollment or scope is invalid.');
|
|
53
|
+
}
|
|
54
|
+
if (currentness.state === 'drift')
|
|
55
|
+
return blocked(`ROUTING_${currentness.reason}`, `Native event enrollment is not current: ${currentness.reason}.`);
|
|
56
|
+
const claim = receipt.claims.find(item => equal(item.selection, wanted));
|
|
57
|
+
if (!claim)
|
|
58
|
+
return blocked('ROUTING_SELECTION_UNENROLLED', 'Approved selection has no native event enrollment.');
|
|
59
|
+
if (input.scope.runtime.target === 'claude-code' && !claim.nativeAgentType)
|
|
60
|
+
return blocked('ROUTING_AGENT_TYPE_UNVERIFIED', 'Claude native agent type is not bound to the enrolled selection.');
|
|
61
|
+
if (claim.actualModel === 'unverified') {
|
|
62
|
+
if (!mapping.degradation.allowMissingObservedIdentity)
|
|
63
|
+
return blocked('ROUTING_OBSERVED_IDENTITY_UNAVAILABLE', 'Actual backend model is unverified and policy does not permit operational routing.');
|
|
64
|
+
return { state: 'resolved', selection: wanted, effectiveProfile, outcome: 'degraded', policyDigest: policy.contentDigest,
|
|
65
|
+
capabilityDigest: (0, crypto_1.createHash)('sha256').update(JSON.stringify(receipt)).digest('hex'), unavailableEvidence: ['observedModelEvidence'], ...(claim.nativeAgentType ? { nativeAgentType: claim.nativeAgentType } : {}) };
|
|
66
|
+
}
|
|
67
|
+
if (wanted.selector.kind !== 'model' || claim.actualModel.id !== wanted.selector.id)
|
|
68
|
+
return blocked('ROUTING_SELECTION_MISMATCH', 'Native backend model does not match the approved selection.');
|
|
69
|
+
return { state: 'resolved', selection: wanted, effectiveProfile, outcome: 'native', policyDigest: policy.contentDigest,
|
|
70
|
+
capabilityDigest: (0, crypto_1.createHash)('sha256').update(JSON.stringify(receipt)).digest('hex'), unavailableEvidence: [], ...(claim.nativeAgentType ? { nativeAgentType: claim.nativeAgentType } : {}) };
|
|
71
|
+
}
|
|
72
|
+
function resolveEventWithFallback(input) {
|
|
73
|
+
const preferred = resolveEventSelection(input);
|
|
74
|
+
if ((preferred.state === 'resolved' && !input.circuitReason) || input.role !== 'implementer' || input.requestedProfile === 'full')
|
|
75
|
+
return preferred;
|
|
76
|
+
const full = resolveEventSelection({ ...input, role: 'code-quality-reviewer', requestedProfile: 'full' });
|
|
77
|
+
const reason = input.circuitReason ?? (preferred.state === 'blocked' ? preferred.diagnostics[0].code : 'PROVENANCE_MISSING');
|
|
78
|
+
if (full.state === 'blocked')
|
|
79
|
+
return blocked('ROUTING_FALLBACK_UNAVAILABLE', `Optimized route ${reason} and full fallback ${full.diagnostics[0].code}; enroll the missing selection with awm model-policy setup.`);
|
|
80
|
+
return { ...full, effectiveProfile: 'full', outcome: 'degraded', fallbackReason: reason };
|
|
81
|
+
}
|
|
82
|
+
function resolveEscalatedSelection(input) {
|
|
83
|
+
if (!input || input.role !== 'implementer' || !['medium', 'high', 'runtime-default'].includes(input.expectedEffort))
|
|
84
|
+
throw new Error('escalated selection requires an implementer and a supported effort');
|
|
85
|
+
const selected = resolveConfiguredSelection(input);
|
|
86
|
+
if (selected.state === 'blocked')
|
|
87
|
+
return selected;
|
|
88
|
+
if (selected.fallbackReason)
|
|
89
|
+
return selected;
|
|
90
|
+
const effort = selected.selection.effort;
|
|
91
|
+
if (input.expectedEffort === 'runtime-default')
|
|
92
|
+
return effort.kind === 'runtime-default' ? selected : blocked('ROUTING_ESCALATION_EFFORT_UNAVAILABLE', 'The approved selected effort does not match runtime-default escalation.');
|
|
93
|
+
if (input.requestedProfile !== 'judgment')
|
|
94
|
+
return effort.kind === 'explicit' && effort.value === input.expectedEffort ? selected : blocked('ROUTING_ESCALATION_EFFORT_UNAVAILABLE', 'The approved selected effort does not match the next lineage escalation.');
|
|
95
|
+
if (effort.kind === 'explicit' && effort.value === 'medium' && input.expectedEffort === 'medium')
|
|
96
|
+
return selected;
|
|
97
|
+
const full = resolveConfiguredSelection({ ...input, role: 'code-quality-reviewer', requestedProfile: 'full' });
|
|
98
|
+
if (full.state === 'blocked')
|
|
99
|
+
return full;
|
|
100
|
+
const sameModel = full.selection.selector.kind === selected.selection.selector.kind && full.selection.selector.id === selected.selection.selector.id;
|
|
101
|
+
if (!sameModel || full.selection.effort.kind !== 'explicit' || full.selection.effort.value !== 'high')
|
|
102
|
+
return blocked('ROUTING_ESCALATION_EFFORT_UNAVAILABLE', 'Judgment high requires the same approved full-capability model at explicit high effort.');
|
|
103
|
+
if (effort.kind === 'explicit' && effort.value === 'high')
|
|
104
|
+
return selected;
|
|
105
|
+
if (effort.kind === 'explicit' && effort.value === 'medium' && input.expectedEffort === 'high')
|
|
106
|
+
return { ...full, effectiveProfile: 'judgment' };
|
|
107
|
+
return blocked('ROUTING_ESCALATION_EFFORT_UNAVAILABLE', 'The approved selected effort does not match the next lineage escalation.');
|
|
108
|
+
}
|
|
109
|
+
function resolveConfiguredSelection(input) {
|
|
110
|
+
if (input.eventReceipt !== undefined || input.eventScope !== undefined) {
|
|
111
|
+
if (!input.eventReceipt || !input.eventScope)
|
|
112
|
+
return blocked('ROUTING_EVENT_SCOPE_MISSING', 'Event-scoped routing requires both a receipt and local scope.');
|
|
113
|
+
const mapping = input.policy?.content.mappings.find(item => item.target === input.eventScope?.runtime.target && item.runtimeKind === input.eventScope?.runtime.kind);
|
|
114
|
+
const wanted = mapping && input.role === 'implementer' && input.requestedProfile !== 'full' ? mapping.profiles[input.requestedProfile] : undefined;
|
|
115
|
+
const incident = wanted && input.circuitIncidents?.find(item => item.target === input.eventScope?.runtime.target
|
|
116
|
+
&& equal(item.selection, wanted)
|
|
117
|
+
&& ['PROVIDER_REJECTED', 'SELECTION_MISMATCH', 'PROVENANCE_MISSING'].includes(item.reasonCode));
|
|
118
|
+
const circuitReason = incident?.reasonCode;
|
|
119
|
+
return resolveEventWithFallback({ role: input.role, requestedProfile: input.requestedProfile, policy: input.policy, receipt: input.eventReceipt, scope: input.eventScope, now: input.now, circuitReason });
|
|
120
|
+
}
|
|
121
|
+
return resolveSelection(input);
|
|
122
|
+
}
|
|
9
123
|
/** Resolve only from the immutable validator report; a reopened plan may never
|
|
10
124
|
* provide a routing profile after validation. */
|
|
11
125
|
function resolveDispatch(input) {
|
|
@@ -25,14 +139,14 @@ function resolveDispatch(input) {
|
|
|
25
139
|
throw new Error('local routing requires a valid slice');
|
|
26
140
|
if (!local && input.sliceId !== undefined)
|
|
27
141
|
throw new Error('slice is only valid for local routing');
|
|
28
|
-
return
|
|
142
|
+
return resolveConfiguredSelection({ ...input, requestedProfile: 'full' });
|
|
29
143
|
}
|
|
30
144
|
if (typeof input.sliceId !== 'string' || input.sliceId.length === 0)
|
|
31
145
|
throw new Error('implementer routing requires a slice');
|
|
32
146
|
const slice = input.plan.manifest.slices.find(candidate => candidate.id === input.sliceId);
|
|
33
147
|
if (!slice)
|
|
34
148
|
throw new Error('routing slice does not exist in the validated plan');
|
|
35
|
-
return
|
|
149
|
+
return resolveConfiguredSelection({ ...input, requestedProfile: slice.implementerProfile });
|
|
36
150
|
}
|
|
37
151
|
function resolveV1Dispatch(input) {
|
|
38
152
|
if (!input.plan || input.plan.schema !== 'compact-slices/v1')
|
|
@@ -40,7 +154,7 @@ function resolveV1Dispatch(input) {
|
|
|
40
154
|
(0, validate_2.assertVerifiedValidPlanReport)(input.plan);
|
|
41
155
|
if (!input.optInV1)
|
|
42
156
|
return { state: 'not-required', reason: 'v1-without-opt-in' };
|
|
43
|
-
return
|
|
157
|
+
return resolveConfiguredSelection({ ...input, requestedProfile: 'full' });
|
|
44
158
|
}
|
|
45
159
|
function equal(left, right) { return left.selector.kind === right.selector.kind && left.selector.id === right.selector.id && left.effort.kind === right.effort.kind && (left.effort.kind !== 'explicit' || left.effort.value === right.effort.value); }
|
|
46
160
|
function blocked(code, message) { return { state: 'blocked', diagnostics: [(0, capabilities_1.routingDiagnostic)(code, message)] }; }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.selectionPolicyDigest = selectionPolicyDigest;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const validate_1 = require("./validate");
|
|
6
|
+
function same(left, right) {
|
|
7
|
+
return left.selector.kind === right.selector.kind && left.selector.id === right.selector.id
|
|
8
|
+
&& left.effort.kind === right.effort.kind
|
|
9
|
+
&& (left.effort.kind !== 'explicit' || left.effort.value === right.effort.value);
|
|
10
|
+
}
|
|
11
|
+
/** This digest binds what native work proved: the selected model and effort.
|
|
12
|
+
* Degradation permission is evaluated at dispatch and may be approved later
|
|
13
|
+
* without forcing a second native run for the same selection. */
|
|
14
|
+
function selectionPolicyDigest(mapping, selection) {
|
|
15
|
+
const validated = (0, validate_1.validatePolicyContent)({ schema: 'model-policy/v1', mappings: [mapping],
|
|
16
|
+
implementationBudget: { maxAttempts: 3, escalation: ['mechanical', 'integration', 'judgment'], judgmentEfforts: ['medium', 'high'] } }).mappings[0];
|
|
17
|
+
const requested = [validated.profiles.mechanical, validated.profiles.integration, validated.profiles.judgment, validated.fullCapability];
|
|
18
|
+
const found = requested.find(item => same(item, selection));
|
|
19
|
+
if (!found)
|
|
20
|
+
throw new Error('selection is not approved in this mapping');
|
|
21
|
+
return (0, crypto_1.createHash)('sha256').update(JSON.stringify({ target: validated.target, runtimeKind: validated.runtimeKind, selection: found })).digest('hex');
|
|
22
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.routingSetupGuidance = routingSetupGuidance;
|
|
4
|
+
const store_1 = require("./store");
|
|
5
|
+
const capabilities_1 = require("./capabilities");
|
|
6
|
+
const json_1 = require("../plan/json");
|
|
7
|
+
const event_store_1 = require("./event-store");
|
|
8
|
+
const capabilities_v2_1 = require("./capabilities-v2");
|
|
9
|
+
const selection_policy_digest_1 = require("./selection-policy-digest");
|
|
10
|
+
/** Local guidance only: no provider process, inference, approval or filesystem write. */
|
|
11
|
+
function locallyCurrentReceipt(mapping, now) {
|
|
12
|
+
try {
|
|
13
|
+
const raw = (0, store_1.readBoundedCandidate)((0, capabilities_1.storedCapabilityPath)(mapping.target, mapping.runtimeKind));
|
|
14
|
+
if (raw === null)
|
|
15
|
+
return false;
|
|
16
|
+
const receipt = (0, capabilities_1.validateCapabilityReceipt)((0, json_1.parseJsonNoDuplicate)(raw));
|
|
17
|
+
const caps = receipt.capabilities;
|
|
18
|
+
return receipt.runtime.target === mapping.target && receipt.runtime.kind === mapping.runtimeKind
|
|
19
|
+
&& (0, capabilities_1.receiptIsCurrent)(receipt, receipt.runtime, now)
|
|
20
|
+
&& caps.nativeSubagents === 'supported' && caps.modelOverride === 'supported' && caps.effortOverride === 'supported'
|
|
21
|
+
&& (caps.observedModelEvidence === 'supported' || mapping.degradation.allowMissingObservedIdentity);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Passive diagnostics can check sealed coverage, but only dispatch recomputes
|
|
28
|
+
* runtime/account/config fingerprints. No provider process runs here. */
|
|
29
|
+
function locallyEnrolledEvent(mapping, now, reader) {
|
|
30
|
+
if (mapping.target !== 'codex' && mapping.target !== 'claude-code')
|
|
31
|
+
return 'absent';
|
|
32
|
+
try {
|
|
33
|
+
const stored = reader({ target: mapping.target, kind: mapping.runtimeKind, version: '0.0.0', accountScopeDigest: '0'.repeat(64) });
|
|
34
|
+
if (stored.state !== 'present')
|
|
35
|
+
return stored.state;
|
|
36
|
+
const receipt = stored.receipt;
|
|
37
|
+
if (receipt.runtime.target !== mapping.target || receipt.runtime.kind !== mapping.runtimeKind)
|
|
38
|
+
return 'uncovered';
|
|
39
|
+
const scope = { runtime: receipt.runtime, binaryDigest: receipt.binaryDigest, configDigest: receipt.configDigest };
|
|
40
|
+
const selections = [...Object.values(mapping.profiles), mapping.fullCapability];
|
|
41
|
+
return selections.every(selected => {
|
|
42
|
+
const current = (0, capabilities_v2_1.evaluateEventReceipt)(receipt, scope, selected, (0, selection_policy_digest_1.selectionPolicyDigest)(mapping, selected), now);
|
|
43
|
+
if (current.state !== 'current')
|
|
44
|
+
return false;
|
|
45
|
+
const claim = receipt.claims.find(item => JSON.stringify(item.selection) === JSON.stringify(selected));
|
|
46
|
+
return claim !== undefined && (claim.actualModel !== 'unverified' || mapping.degradation.allowMissingObservedIdentity);
|
|
47
|
+
}) ? 'covered' : 'uncovered';
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return 'invalid';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function routingSetupGuidance(policy, now = new Date(), deps = {}) {
|
|
54
|
+
if (!policy || typeof policy !== 'object' || !('state' in policy))
|
|
55
|
+
throw new Error('routing policy state is invalid');
|
|
56
|
+
if (!(now instanceof Date) || !Number.isFinite(now.getTime()))
|
|
57
|
+
throw new Error('routing setup clock must be finite');
|
|
58
|
+
if (policy.state === 'absent')
|
|
59
|
+
return [];
|
|
60
|
+
if (policy.state === 'invalid') {
|
|
61
|
+
if (typeof policy.reason !== 'string' || policy.reason.length === 0 || policy.reason.length > 4096)
|
|
62
|
+
throw new Error('routing policy invalid reason is malformed');
|
|
63
|
+
return [{ state: 'policy-invalid', reason: policy.reason }];
|
|
64
|
+
}
|
|
65
|
+
if (policy.state !== 'approved' || !policy.policy || !Array.isArray(policy.policy.content?.mappings))
|
|
66
|
+
throw new Error('approved routing policy state is malformed');
|
|
67
|
+
return policy.policy.content.mappings.flatMap(mapping => {
|
|
68
|
+
if (typeof mapping.target !== 'string' || typeof mapping.runtimeKind !== 'string' || !/^[a-z0-9][a-z0-9_-]{0,127}$/.test(mapping.runtimeKind))
|
|
69
|
+
throw new Error('routing policy mapping is malformed');
|
|
70
|
+
const event = locallyEnrolledEvent(mapping, now, deps.readStoredEventReceipt ?? event_store_1.readStoredEventReceipt);
|
|
71
|
+
if (event === 'covered')
|
|
72
|
+
return [{ target: mapping.target, runtimeKind: mapping.runtimeKind, state: 'coverage-only' }];
|
|
73
|
+
if (event === 'absent' && locallyCurrentReceipt(mapping, now))
|
|
74
|
+
return [];
|
|
75
|
+
return [{ target: mapping.target, runtimeKind: mapping.runtimeKind, state: 'needs-evidence', command: `awm model-policy setup --provider ${mapping.target} --json` }];
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -53,10 +53,12 @@ const CONFIG_HOME = {
|
|
|
53
53
|
// Una corrida del playbook en una maquina con CODEX_HOME seteado encontro el hook
|
|
54
54
|
// instalado donde Codex no mira. Ver docs/decisions.md D-011.
|
|
55
55
|
codex: { envVar: 'CODEX_HOME', dir: '.codex' },
|
|
56
|
+
// Claude Code documenta CLAUDE_CONFIG_DIR para mover toda su configuracion,
|
|
57
|
+
// incluidos settings.json y transcripciones de subagentes.
|
|
58
|
+
'claude-code': { envVar: 'CLAUDE_CONFIG_DIR', dir: '.claude' },
|
|
56
59
|
// Los demas usan su directorio por defecto. `null` es una afirmacion honesta —
|
|
57
60
|
// "no le conocemos override"— y no un "no tiene". Cuando se confirme uno contra su
|
|
58
61
|
// binario, se cambia aca y el guard obliga a que todas sus rutas lo sigan.
|
|
59
|
-
'claude-code': { envVar: null, dir: '.claude' },
|
|
60
62
|
opencode: { envVar: null, dir: '.config/opencode' },
|
|
61
63
|
cursor: { envVar: null, dir: '.cursor' },
|
|
62
64
|
copilot: { envVar: null, dir: '.github' },
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const doctor_1 = require("../../src/commands/doctor");
|
|
4
|
+
describe('doctor routing setup', () => {
|
|
5
|
+
it('adds a pending, non-degrading machine configuration action only to the mapped provider', () => {
|
|
6
|
+
const report = {
|
|
7
|
+
overall: 'healthy', providers: [
|
|
8
|
+
{ id: 'codex', label: 'Codex', tier: 'hooks-native', checks: [] },
|
|
9
|
+
{ id: 'claude-code', label: 'Claude Code', tier: 'hooks-native', checks: [] },
|
|
10
|
+
],
|
|
11
|
+
};
|
|
12
|
+
const updated = (0, doctor_1.attachRoutingSetupGuidance)(report, [
|
|
13
|
+
{ target: 'codex', runtimeKind: 'native', state: 'needs-evidence', command: 'awm model-policy discover --provider codex --json' },
|
|
14
|
+
]);
|
|
15
|
+
expect(updated.providers[0].checks).toEqual([
|
|
16
|
+
{ id: 'routing.machine', state: 'pending', detail: 'native routing evidence not checked for native', remediationCode: 'awm model-policy discover --provider codex --json' },
|
|
17
|
+
]);
|
|
18
|
+
expect(updated.providers[1].checks).toEqual([]);
|
|
19
|
+
expect(updated.overall).toBe('healthy');
|
|
20
|
+
expect(report.providers[0].checks).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -39,6 +39,11 @@ describe('runDoctor legacy JSON fixtures', () => {
|
|
|
39
39
|
}
|
|
40
40
|
});
|
|
41
41
|
});
|
|
42
|
+
test('doctor labels sealed routing coverage as local-only without asking for recurring setup', () => {
|
|
43
|
+
const report = (0, doctor_1.attachRoutingSetupGuidance)({ overall: 'healthy', providers: [{ id: 'codex', label: 'Codex', tier: 'hooks-native', checks: [] }] }, [{ target: 'codex', runtimeKind: 'native', state: 'coverage-only' }]);
|
|
44
|
+
expect(report.providers[0].checks).toEqual([{ id: 'routing.machine', state: 'supported',
|
|
45
|
+
detail: 'sealed selection coverage for native only; current runtime/account/config scope is checked at routed dispatch' }]);
|
|
46
|
+
});
|
|
42
47
|
describe('runDoctor dashboard modes', () => {
|
|
43
48
|
it('CLI rejects --html without an argument before collection or writes', async () => {
|
|
44
49
|
const program = new commander_1.Command();
|
|
@@ -59,6 +59,10 @@ describe('installHook (happy path + merge)', () => {
|
|
|
59
59
|
expect(settings.hooks.SessionStart).toHaveLength(1);
|
|
60
60
|
expect(settings.hooks.SessionStart[0].matcher).toBe('startup|clear|compact');
|
|
61
61
|
expect(settings.hooks.SessionStart[0].hooks[0].command).toContain('run-hook.cmd');
|
|
62
|
+
expect(settings.hooks.SubagentStart[0].hooks[0].command).toBe('awm model-policy hook-event --event start');
|
|
63
|
+
expect(settings.hooks.SubagentStop[0].hooks[0].command).toBe('awm model-policy hook-event --event stop');
|
|
64
|
+
expect(settings.hooks.SubagentStart[0].hooks[0].async).toBe(false);
|
|
65
|
+
expect(settings.hooks.SubagentStop[0].hooks[0].async).toBe(false);
|
|
62
66
|
expect(result.backupPath).toBeNull();
|
|
63
67
|
});
|
|
64
68
|
it('merges with pre-existing SessionStart entry from another plugin', () => {
|
|
@@ -96,6 +100,8 @@ describe('installHook (happy path + merge)', () => {
|
|
|
96
100
|
expect(result2.status).toBe('already-up-to-date');
|
|
97
101
|
const settings = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpHome, '.claude/settings.json'), 'utf-8'));
|
|
98
102
|
expect(settings.hooks.SessionStart).toHaveLength(1);
|
|
103
|
+
expect(settings.hooks.SubagentStart).toHaveLength(1);
|
|
104
|
+
expect(settings.hooks.SubagentStop).toHaveLength(1);
|
|
99
105
|
});
|
|
100
106
|
it('replaces a stale AWM entry when paths change', () => {
|
|
101
107
|
const claudeDir = path_1.default.join(tmpHome, '.claude');
|
|
@@ -66,6 +66,9 @@ describe('resyncInstalledHooks', () => {
|
|
|
66
66
|
{ agent: 'claude-code', action: 'resynced' },
|
|
67
67
|
{ agent: 'codex', action: 'not-installed' },
|
|
68
68
|
]);
|
|
69
|
+
const settings = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpHome, '.claude/settings.json'), 'utf8'));
|
|
70
|
+
expect(settings.hooks.SubagentStart[0].hooks[0].command).toBe('awm model-policy hook-event --event start');
|
|
71
|
+
expect(settings.hooks.SubagentStop[0].hooks[0].command).toBe('awm model-policy hook-event --event stop');
|
|
69
72
|
const synced = fs_1.default.readFileSync(path_1.default.join(scriptsDir, 'session-start'), 'utf-8');
|
|
70
73
|
expect(synced).toContain('NEW VERSION');
|
|
71
74
|
expect(fs_1.default.lstatSync(path_1.default.join(scriptsDir, 'session-start')).isSymbolicLink()).toBe(false);
|
|
@@ -42,7 +42,9 @@ describe('computeHookStatus', () => {
|
|
|
42
42
|
SessionStart: [{
|
|
43
43
|
matcher: 'startup|clear|compact',
|
|
44
44
|
hooks: [{ type: 'command', command: `${hooksDir}/run-hook.cmd session-start`, async: false }]
|
|
45
|
-
}]
|
|
45
|
+
}],
|
|
46
|
+
SubagentStart: [{ matcher: '.+', hooks: [{ type: 'command', command: 'awm model-policy hook-event --event start', async: false }] }],
|
|
47
|
+
SubagentStop: [{ matcher: '.+', hooks: [{ type: 'command', command: 'awm model-policy hook-event --event stop', async: false }] }]
|
|
46
48
|
}
|
|
47
49
|
}, null, 2));
|
|
48
50
|
}
|
|
@@ -56,6 +58,15 @@ describe('computeHookStatus', () => {
|
|
|
56
58
|
expect(result.checks.runHookWrapper.ok).toBe(true);
|
|
57
59
|
expect(result.checks.settingsEntry.ok).toBe(true);
|
|
58
60
|
});
|
|
61
|
+
it('does not certify a non-command routing hook with matching command text', () => {
|
|
62
|
+
setupInstalledHook();
|
|
63
|
+
const settings = path_1.default.join(tmpHome, '.claude/settings.json');
|
|
64
|
+
const parsed = JSON.parse(fs_1.default.readFileSync(settings, 'utf8'));
|
|
65
|
+
parsed.hooks.SubagentStart[0].hooks[0].type = 'prompt';
|
|
66
|
+
fs_1.default.writeFileSync(settings, JSON.stringify(parsed));
|
|
67
|
+
const { computeHookStatus } = require('../../../src/commands/hooks/status');
|
|
68
|
+
expect(computeHookStatus('claude-code').checks.routingCapture.ok).toBe(false);
|
|
69
|
+
});
|
|
59
70
|
it('reports DEGRADED when bootstrap skill is missing', () => {
|
|
60
71
|
setupInstalledHook();
|
|
61
72
|
fs_1.default.unlinkSync(path_1.default.join(tmpHome, '.awm/hooks/using-awm.md'));
|