agentic-workflow-manager 9.14.0 → 9.15.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/context-budget/budget.js +50 -4
- package/dist/src/commands/doctor.js +4 -1
- package/dist/src/commands/hooks/cursor.js +359 -0
- package/dist/src/commands/hooks/install.js +3 -0
- package/dist/src/commands/hooks/resync.js +10 -0
- package/dist/src/commands/hooks/status.js +3 -0
- package/dist/src/commands/hooks/uninstall.js +3 -0
- package/dist/src/commands/update.js +4 -0
- package/dist/src/core/admission/index.js +12 -6
- package/dist/src/core/admission/registry-contracts.js +14 -1
- package/dist/src/core/context/strategies/codex-agents.js +6 -2
- package/dist/src/core/cursor-budgets.js +8 -0
- package/dist/src/core/dashboard/sanitize.js +2 -2
- package/dist/src/core/diagnostics/provider-checks.js +11 -2
- package/dist/src/core/init/migrate-cursor-mdc.js +72 -0
- package/dist/src/core/init/steps.js +4 -0
- package/dist/src/providers/index.js +18 -4
- package/dist/tests/commands/add.test.js +5 -7
- package/dist/tests/commands/context-budget/budget.test.js +68 -0
- package/dist/tests/commands/doctor.test.js +2 -2
- package/dist/tests/commands/hooks/cursor.test.js +257 -0
- package/dist/tests/commands/hooks/resync.test.js +8 -0
- package/dist/tests/commands/init.test.js +23 -22
- package/dist/tests/commands/plan/index.test.js +82 -2
- package/dist/tests/commands/sensors/exec.test.js +18 -9
- package/dist/tests/core/admission/index.test.js +39 -0
- package/dist/tests/core/bundle-install.test.js +8 -14
- package/dist/tests/core/context/strategies/codex-agents.test.js +31 -12
- package/dist/tests/core/diagnostics/provider-tier.test.js +46 -96
- package/dist/tests/core/diagnostics/rendered-artifact-visibility.test.js +6 -4
- package/dist/tests/core/init/migrate-cursor-mdc.test.js +74 -0
- package/dist/tests/core/install-planner.test.js +16 -12
- package/dist/tests/integration/cursor-subset-gate.test.js +123 -0
- package/dist/tests/providers/index.test.js +34 -21
- package/dist/tests/structural/provider-paths-honor-config-home.test.js +3 -2
- 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
|
@@ -7,10 +7,14 @@ exports.CONFIG_FILE = exports.DEFAULT_FILES = void 0;
|
|
|
7
7
|
exports.estimateTokens = estimateTokens;
|
|
8
8
|
exports.measure = measure;
|
|
9
9
|
exports.readConfig = readConfig;
|
|
10
|
+
exports.readCursorHookByteCaps = readCursorHookByteCaps;
|
|
11
|
+
exports.evaluateCursorHookBudgets = evaluateCursorHookBudgets;
|
|
10
12
|
exports.writeConfig = writeConfig;
|
|
11
13
|
exports.checkBudget = checkBudget;
|
|
12
14
|
const fs_1 = __importDefault(require("fs"));
|
|
13
15
|
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const paths_1 = require("../../core/paths");
|
|
17
|
+
const cursor_budgets_1 = require("../../core/cursor-budgets");
|
|
14
18
|
/**
|
|
15
19
|
* The feedforward context budget.
|
|
16
20
|
*
|
|
@@ -69,15 +73,53 @@ function readConfig(cwd) {
|
|
|
69
73
|
const raw = JSON.parse(fs_1.default.readFileSync(path_1.default.join(cwd, exports.CONFIG_FILE), 'utf-8'));
|
|
70
74
|
if (typeof raw.maxBytes !== 'number' || !Number.isFinite(raw.maxBytes))
|
|
71
75
|
return null;
|
|
72
|
-
|
|
76
|
+
const config = {
|
|
73
77
|
files: Array.isArray(raw.files) && raw.files.length ? raw.files : exports.DEFAULT_FILES,
|
|
74
78
|
maxBytes: raw.maxBytes,
|
|
75
79
|
};
|
|
80
|
+
if (typeof raw[cursor_budgets_1.CURSOR_SESSION_START_PIN] === 'number' && Number.isFinite(raw[cursor_budgets_1.CURSOR_SESSION_START_PIN])) {
|
|
81
|
+
config[cursor_budgets_1.CURSOR_SESSION_START_PIN] = raw[cursor_budgets_1.CURSOR_SESSION_START_PIN];
|
|
82
|
+
}
|
|
83
|
+
if (typeof raw[cursor_budgets_1.CURSOR_REANCHOR_PIN] === 'number' && Number.isFinite(raw[cursor_budgets_1.CURSOR_REANCHOR_PIN])) {
|
|
84
|
+
config[cursor_budgets_1.CURSOR_REANCHOR_PIN] = raw[cursor_budgets_1.CURSOR_REANCHOR_PIN];
|
|
85
|
+
}
|
|
86
|
+
return config;
|
|
76
87
|
}
|
|
77
88
|
catch {
|
|
78
89
|
return null;
|
|
79
90
|
}
|
|
80
91
|
}
|
|
92
|
+
/** Read MAX_*_BYTES assignments from an installed Cursor session-start script. */
|
|
93
|
+
function readCursorHookByteCaps(scriptPath) {
|
|
94
|
+
if (!fs_1.default.existsSync(scriptPath))
|
|
95
|
+
return { session: null, reanchor: null };
|
|
96
|
+
const text = fs_1.default.readFileSync(scriptPath, 'utf8');
|
|
97
|
+
const session = /MAX_SESSION_BYTES\s*=\s*(\d+)\s*\*\s*(\d+)/.exec(text);
|
|
98
|
+
const reanchor = /MAX_REANCHOR_BYTES\s*=\s*(\d+)\s*\*\s*(\d+)/.exec(text);
|
|
99
|
+
return {
|
|
100
|
+
session: session ? Number(session[1]) * Number(session[2]) : null,
|
|
101
|
+
reanchor: reanchor ? Number(reanchor[1]) * Number(reanchor[2]) : null,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function evaluateCursorHookBudgets(config) {
|
|
105
|
+
const scriptPath = path_1.default.join((0, paths_1.awmHome)(), 'hooks/cursor/session-start');
|
|
106
|
+
if (!fs_1.default.existsSync(scriptPath)) {
|
|
107
|
+
return undefined; // unmeasurable / absent — do not fail
|
|
108
|
+
}
|
|
109
|
+
const pinSession = config?.[cursor_budgets_1.CURSOR_SESSION_START_PIN] ?? cursor_budgets_1.CURSOR_SESSION_START_MAX_BYTES;
|
|
110
|
+
const pinReanchor = config?.[cursor_budgets_1.CURSOR_REANCHOR_PIN] ?? cursor_budgets_1.CURSOR_REANCHOR_MAX_BYTES;
|
|
111
|
+
const observed = readCursorHookByteCaps(scriptPath);
|
|
112
|
+
const sessionOk = observed.session === null || observed.session <= pinSession;
|
|
113
|
+
const reanchorOk = observed.reanchor === null || observed.reanchor <= pinReanchor;
|
|
114
|
+
const constantsOk = cursor_budgets_1.CURSOR_SESSION_START_MAX_BYTES <= pinSession && cursor_budgets_1.CURSOR_REANCHOR_MAX_BYTES <= pinReanchor;
|
|
115
|
+
return {
|
|
116
|
+
status: sessionOk && reanchorOk && constantsOk ? 'within' : 'over',
|
|
117
|
+
sessionStartMaxBytes: pinSession,
|
|
118
|
+
reanchorMaxBytes: pinReanchor,
|
|
119
|
+
observedSessionStartMaxBytes: observed.session,
|
|
120
|
+
observedReanchorMaxBytes: observed.reanchor,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
81
123
|
function writeConfig(cwd, config) {
|
|
82
124
|
fs_1.default.mkdirSync(path_1.default.join(cwd, '.awm'), { recursive: true });
|
|
83
125
|
const body = {
|
|
@@ -97,6 +139,7 @@ function writeConfig(cwd, config) {
|
|
|
97
139
|
*/
|
|
98
140
|
function checkBudget(cwd) {
|
|
99
141
|
const config = readConfig(cwd);
|
|
142
|
+
const cursorHooks = evaluateCursorHookBudgets(config);
|
|
100
143
|
if (!config) {
|
|
101
144
|
const { total, breakdown } = measure(cwd, exports.DEFAULT_FILES);
|
|
102
145
|
// Fijar sobre CERO archivos es una trampa, no una linea base.
|
|
@@ -110,16 +153,19 @@ function checkBudget(cwd) {
|
|
|
110
153
|
//
|
|
111
154
|
// Se reporta y NO se escribe config: el proximo run, ya con contexto, fija bien.
|
|
112
155
|
if (breakdown.length === 0) {
|
|
113
|
-
return { status: 'unmeasurable', totalBytes: 0, maxBytes: 0, breakdown };
|
|
156
|
+
return { status: 'unmeasurable', totalBytes: 0, maxBytes: 0, breakdown, ...(cursorHooks ? { cursorHooks } : {}) };
|
|
114
157
|
}
|
|
115
158
|
writeConfig(cwd, { files: exports.DEFAULT_FILES, maxBytes: total });
|
|
116
|
-
return { status: 'pinned', totalBytes: total, maxBytes: total, breakdown };
|
|
159
|
+
return { status: 'pinned', totalBytes: total, maxBytes: total, breakdown, ...(cursorHooks ? { cursorHooks } : {}) };
|
|
117
160
|
}
|
|
118
161
|
const { total, breakdown } = measure(cwd, config.files);
|
|
162
|
+
const fileStatus = total > config.maxBytes ? 'over' : 'within';
|
|
163
|
+
const status = fileStatus === 'over' || cursorHooks?.status === 'over' ? 'over' : fileStatus;
|
|
119
164
|
return {
|
|
120
|
-
status
|
|
165
|
+
status,
|
|
121
166
|
totalBytes: total,
|
|
122
167
|
maxBytes: config.maxBytes,
|
|
123
168
|
breakdown,
|
|
169
|
+
...(cursorHooks ? { cursorHooks } : {}),
|
|
124
170
|
};
|
|
125
171
|
}
|
|
@@ -76,6 +76,7 @@ const CHECK_LABELS = {
|
|
|
76
76
|
'agents.native': 'native agents',
|
|
77
77
|
'workflows.global': 'global workflows',
|
|
78
78
|
'context.global': 'global context',
|
|
79
|
+
'context.overlap': 'Claude/Cursor delivery overlap',
|
|
79
80
|
'hook.trust': 'hook SessionStart',
|
|
80
81
|
'guidance.project': 'project guidance',
|
|
81
82
|
'constitution.delivery': 'constitution delivery',
|
|
@@ -100,7 +101,7 @@ function attachRoutingSetupGuidance(report, guidance) {
|
|
|
100
101
|
return { ...report, providers };
|
|
101
102
|
}
|
|
102
103
|
const OK_STATES = ['supported', 'healthy', 'shared', 'delivered'];
|
|
103
|
-
const PENDING_STATES = ['pending', 'pending-trust'];
|
|
104
|
+
const PENDING_STATES = ['pending', 'pending-trust', 'pending-first-run'];
|
|
104
105
|
const WARN_STATES = ['stale'];
|
|
105
106
|
function providerGlyph(state) {
|
|
106
107
|
if (OK_STATES.includes(state))
|
|
@@ -116,6 +117,8 @@ function providerCheckDetail(check) {
|
|
|
116
117
|
return picocolors_1.default.dim(` (${check.target})`);
|
|
117
118
|
if (check.state === 'pending-trust')
|
|
118
119
|
return picocolors_1.default.dim(' (pending trust)');
|
|
120
|
+
if (check.state === 'pending-first-run')
|
|
121
|
+
return picocolors_1.default.dim(' (pending first run)');
|
|
119
122
|
if (check.detail)
|
|
120
123
|
return picocolors_1.default.dim(` (${check.detail})`);
|
|
121
124
|
return '';
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// cli/src/commands/hooks/cursor.ts
|
|
3
|
+
//
|
|
4
|
+
// Cursor hook adapter: merges AWM entries into ~/.cursor/hooks.json (version: 1)
|
|
5
|
+
// for sessionStart / preCompact / postToolUse, syncs the three Plan A scripts into
|
|
6
|
+
// <AWM_HOME>/hooks/cursor/, and materializes using-awm.md beside them.
|
|
7
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
|
+
};
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.CURSOR_HOOK_EVENTS = exports.CURSOR_HOOKS_REGISTRY_FLOOR = void 0;
|
|
12
|
+
exports.shouldDisableDeferredReanchor = shouldDisableDeferredReanchor;
|
|
13
|
+
exports.assertCursorRegistryFloor = assertCursorRegistryFloor;
|
|
14
|
+
exports.resolveInstalledBaselineVersion = resolveInstalledBaselineVersion;
|
|
15
|
+
exports.awmCursorEntry = awmCursorEntry;
|
|
16
|
+
exports.isDeadAwmCursorEntry = isDeadAwmCursorEntry;
|
|
17
|
+
exports.isStaleAwmCursorEntry = isStaleAwmCursorEntry;
|
|
18
|
+
exports.installCursorHook = installCursorHook;
|
|
19
|
+
exports.computeCursorHookStatus = computeCursorHookStatus;
|
|
20
|
+
exports.uninstallCursorHook = uninstallCursorHook;
|
|
21
|
+
exports.resyncCursorHookFiles = resyncCursorHookFiles;
|
|
22
|
+
exports.cursorResyncSourcesExist = cursorResyncSourcesExist;
|
|
23
|
+
const fs_1 = __importDefault(require("fs"));
|
|
24
|
+
const path_1 = __importDefault(require("path"));
|
|
25
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
26
|
+
const child_process_1 = require("child_process");
|
|
27
|
+
const providers_1 = require("../../providers");
|
|
28
|
+
const shared_1 = require("./shared");
|
|
29
|
+
const atomic_file_1 = require("../../core/atomic-file");
|
|
30
|
+
const provider_1 = require("../../core/context/provider");
|
|
31
|
+
const orchestrators_1 = require("../../core/orchestrators");
|
|
32
|
+
const registries_1 = require("../../core/registries");
|
|
33
|
+
const versioning_1 = require("../../core/versioning");
|
|
34
|
+
exports.CURSOR_HOOKS_REGISTRY_FLOOR = '4.9.0';
|
|
35
|
+
exports.CURSOR_HOOK_EVENTS = ['sessionStart', 'preCompact', 'postToolUse'];
|
|
36
|
+
const SCRIPT_BY_EVENT = {
|
|
37
|
+
sessionStart: { source: 'hooks/cursor-session-start', dest: 'session-start' },
|
|
38
|
+
preCompact: { source: 'hooks/cursor-pre-compact', dest: 'pre-compact' },
|
|
39
|
+
postToolUse: { source: 'hooks/cursor-post-tool-use', dest: 'post-tool-use' },
|
|
40
|
+
};
|
|
41
|
+
/** R16.2 — disable deferred re-anchor when delivery is unverified or p95 exceeds 50ms. */
|
|
42
|
+
function shouldDisableDeferredReanchor(metrics) {
|
|
43
|
+
return metrics.deliveryVerified === false || metrics.p95Ms > 50;
|
|
44
|
+
}
|
|
45
|
+
function assertCursorRegistryFloor(version) {
|
|
46
|
+
if (version === null) {
|
|
47
|
+
throw new Error(`Cursor hooks require baseline registry >= ${exports.CURSOR_HOOKS_REGISTRY_FLOOR}; installed version is unverifiable. Run 'awm update -y'.`);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
if ((0, versioning_1.compareSemver)(version, exports.CURSOR_HOOKS_REGISTRY_FLOOR) < 0) {
|
|
51
|
+
throw new Error(`Cursor hooks require baseline registry >= ${exports.CURSOR_HOOKS_REGISTRY_FLOOR}; installed is ${version}. Run 'awm update -y'.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error instanceof Error && error.message.startsWith('Cursor hooks require'))
|
|
56
|
+
throw error;
|
|
57
|
+
throw new Error(`Cursor hooks require baseline registry >= ${exports.CURSOR_HOOKS_REGISTRY_FLOOR}; installed version is unverifiable. Run 'awm update -y'.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function syncDescribeExactTag(repoDir) {
|
|
61
|
+
try {
|
|
62
|
+
const out = (0, child_process_1.execFileSync)('git', ['describe', '--tags', '--exact-match', 'HEAD'], {
|
|
63
|
+
cwd: repoDir,
|
|
64
|
+
encoding: 'utf8',
|
|
65
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
66
|
+
timeout: 2_000,
|
|
67
|
+
maxBuffer: 64 * 1024,
|
|
68
|
+
windowsHide: true,
|
|
69
|
+
}).trim();
|
|
70
|
+
const match = /^v(\d+\.\d+\.\d+)$/.exec(out);
|
|
71
|
+
return match ? match[1] : null;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Resolve installed baseline version (pin wins; else exact tag on checkout). */
|
|
78
|
+
function resolveInstalledBaselineVersion() {
|
|
79
|
+
const { pin } = (0, versioning_1.machineVersionOpts)('baseline');
|
|
80
|
+
if (pin)
|
|
81
|
+
return (0, versioning_1.normalizePin)(pin);
|
|
82
|
+
let root;
|
|
83
|
+
try {
|
|
84
|
+
root = (0, registries_1.listRegistries)().find((registry) => registry.name === 'baseline')?.contentRoot;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (!root || !fs_1.default.existsSync(root))
|
|
90
|
+
return null;
|
|
91
|
+
return syncDescribeExactTag(root);
|
|
92
|
+
}
|
|
93
|
+
function cursorConfig() {
|
|
94
|
+
const config = (0, providers_1.getHookConfig)('cursor');
|
|
95
|
+
if (!config || config.type !== 'cursor-hooks-json') {
|
|
96
|
+
throw new Error('Cursor hook configuration is unavailable');
|
|
97
|
+
}
|
|
98
|
+
return config;
|
|
99
|
+
}
|
|
100
|
+
function writeMaterializedSkill(skillDest, registryRoot) {
|
|
101
|
+
const skillSource = path_1.default.join(registryRoot, 'skills/using-awm/SKILL.md');
|
|
102
|
+
if (!fs_1.default.existsSync(skillSource)) {
|
|
103
|
+
throw new Error(`using-awm skill not found at ${skillSource}. Run 'awm update' first.`);
|
|
104
|
+
}
|
|
105
|
+
const ctx = (0, provider_1.buildContext)({
|
|
106
|
+
registryRoot,
|
|
107
|
+
profileExtensions: [],
|
|
108
|
+
declaredOrchestrators: (0, orchestrators_1.collectAndWarn)(),
|
|
109
|
+
});
|
|
110
|
+
fs_1.default.mkdirSync(path_1.default.dirname(skillDest), { recursive: true });
|
|
111
|
+
const tmpPath = path_1.default.join(path_1.default.dirname(skillDest), `.using-awm.md.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
112
|
+
fs_1.default.writeFileSync(tmpPath, ctx.markdown, 'utf-8');
|
|
113
|
+
fs_1.default.renameSync(tmpPath, skillDest);
|
|
114
|
+
}
|
|
115
|
+
function awmCursorEntry(scriptsDir, event) {
|
|
116
|
+
return { command: path_1.default.join(scriptsDir, SCRIPT_BY_EVENT[event].dest) };
|
|
117
|
+
}
|
|
118
|
+
function isAwmCursorEntry(entry, scriptsDir, event) {
|
|
119
|
+
const command = entry?.command;
|
|
120
|
+
return typeof command === 'string' && command === path_1.default.join(scriptsDir, SCRIPT_BY_EVENT[event].dest);
|
|
121
|
+
}
|
|
122
|
+
/** Dead AWM entry: our script basename, not under current scriptsDir, path missing. */
|
|
123
|
+
function isDeadAwmCursorEntry(entry, currentScriptsDir) {
|
|
124
|
+
const command = entry?.command;
|
|
125
|
+
if (typeof command !== 'string' || command.length === 0)
|
|
126
|
+
return false;
|
|
127
|
+
const basename = path_1.default.basename(command);
|
|
128
|
+
if (!Object.values(SCRIPT_BY_EVENT).some((script) => script.dest === basename))
|
|
129
|
+
return false;
|
|
130
|
+
if (path_1.default.dirname(command) === currentScriptsDir)
|
|
131
|
+
return false;
|
|
132
|
+
return !fs_1.default.existsSync(command);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Stale Cursor AWM entries (legacy shapes). Today Cursor only ever wrote the
|
|
136
|
+
* flat `{ command }` form, so there is no legacy matcher/wrapper to retire —
|
|
137
|
+
* live parallel AWM_HOME installs are preserved (same rule as Codex).
|
|
138
|
+
*/
|
|
139
|
+
function isStaleAwmCursorEntry(_entry, _currentScriptsDir) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
function activeEvents(disableDeferred) {
|
|
143
|
+
return disableDeferred ? ['sessionStart'] : [...exports.CURSOR_HOOK_EVENTS];
|
|
144
|
+
}
|
|
145
|
+
function readCursorHooksDocument(settingsPath) {
|
|
146
|
+
const current = (0, shared_1.readStrictJson)(settingsPath);
|
|
147
|
+
if (Object.keys(current).length === 0) {
|
|
148
|
+
return { version: 1, hooks: {} };
|
|
149
|
+
}
|
|
150
|
+
if (current.version !== undefined && current.version !== 1) {
|
|
151
|
+
throw new Error(`${settingsPath} has unsupported hooks.json version ${String(current.version)} (expected 1).`);
|
|
152
|
+
}
|
|
153
|
+
return current;
|
|
154
|
+
}
|
|
155
|
+
function eventArray(hooks, event, settingsPath) {
|
|
156
|
+
if (!(event in hooks) || hooks[event] === undefined)
|
|
157
|
+
return [];
|
|
158
|
+
if (!Array.isArray(hooks[event])) {
|
|
159
|
+
throw new Error(`${settingsPath} hooks.${event} must be an array.`);
|
|
160
|
+
}
|
|
161
|
+
return hooks[event];
|
|
162
|
+
}
|
|
163
|
+
function installCursorHook(options) {
|
|
164
|
+
const config = cursorConfig();
|
|
165
|
+
const baselineVersion = options.baselineRegistryVersion !== undefined
|
|
166
|
+
? options.baselineRegistryVersion
|
|
167
|
+
: resolveInstalledBaselineVersion();
|
|
168
|
+
assertCursorRegistryFloor(baselineVersion ?? null);
|
|
169
|
+
for (const event of exports.CURSOR_HOOK_EVENTS) {
|
|
170
|
+
const source = path_1.default.join(options.registryRoot, SCRIPT_BY_EVENT[event].source);
|
|
171
|
+
if (!fs_1.default.existsSync(source)) {
|
|
172
|
+
throw new Error(`Cursor hook source missing: ${source}. Run 'awm update' first.`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const disableDeferred = options.disableDeferredReanchor === true;
|
|
176
|
+
const events = activeEvents(disableDeferred);
|
|
177
|
+
const current = readCursorHooksDocument(config.settingsPath);
|
|
178
|
+
const hooks = current.hooks && typeof current.hooks === 'object' && !Array.isArray(current.hooks)
|
|
179
|
+
? { ...current.hooks }
|
|
180
|
+
: {};
|
|
181
|
+
let pruned = false;
|
|
182
|
+
let needsWrite = false;
|
|
183
|
+
const nextHooks = { ...hooks };
|
|
184
|
+
for (const event of exports.CURSOR_HOOK_EVENTS) {
|
|
185
|
+
const raw = eventArray(hooks, event, config.settingsPath);
|
|
186
|
+
const kept = raw.filter((entry) => !isDeadAwmCursorEntry(entry, config.scriptsDir)
|
|
187
|
+
&& !isStaleAwmCursorEntry(entry, config.scriptsDir));
|
|
188
|
+
if (kept.length !== raw.length)
|
|
189
|
+
pruned = true;
|
|
190
|
+
if (!events.includes(event)) {
|
|
191
|
+
// Deferred disabled: drop our AWM entries for deferred events, keep foreign ones.
|
|
192
|
+
const withoutOurs = kept.filter((entry) => !isAwmCursorEntry(entry, config.scriptsDir, event));
|
|
193
|
+
if (withoutOurs.length !== raw.length)
|
|
194
|
+
needsWrite = true;
|
|
195
|
+
if (withoutOurs.length === 0)
|
|
196
|
+
delete nextHooks[event];
|
|
197
|
+
else
|
|
198
|
+
nextHooks[event] = withoutOurs;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const matches = kept.filter((entry) => isAwmCursorEntry(entry, config.scriptsDir, event));
|
|
202
|
+
if (matches.length > 1) {
|
|
203
|
+
throw new Error(`multiple AWM ${event} entries in Cursor hooks.json`);
|
|
204
|
+
}
|
|
205
|
+
const newEntry = awmCursorEntry(config.scriptsDir, event);
|
|
206
|
+
if (matches.length === 1 && JSON.stringify(matches[0]) === JSON.stringify(newEntry)) {
|
|
207
|
+
nextHooks[event] = kept;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
needsWrite = true;
|
|
211
|
+
nextHooks[event] = matches.length === 1
|
|
212
|
+
? kept.map((entry) => (isAwmCursorEntry(entry, config.scriptsDir, event) ? newEntry : entry))
|
|
213
|
+
: [...kept, newEntry];
|
|
214
|
+
}
|
|
215
|
+
if (!pruned && !needsWrite) {
|
|
216
|
+
return {
|
|
217
|
+
status: 'already-up-to-date',
|
|
218
|
+
scriptsDir: config.scriptsDir,
|
|
219
|
+
settingsPath: config.settingsPath,
|
|
220
|
+
backupPath: null,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const merged = { ...current, version: 1, hooks: nextHooks };
|
|
224
|
+
const backupPath = (0, shared_1.backupManagedFile)(config.settingsPath);
|
|
225
|
+
fs_1.default.mkdirSync(config.scriptsDir, { recursive: true });
|
|
226
|
+
for (const event of events) {
|
|
227
|
+
const meta = SCRIPT_BY_EVENT[event];
|
|
228
|
+
(0, shared_1.syncExecutable)(path_1.default.join(options.registryRoot, meta.source), path_1.default.join(config.scriptsDir, meta.dest), options.installMethod);
|
|
229
|
+
}
|
|
230
|
+
// When deferred is disabled, leave previously synced deferred scripts alone (harmless).
|
|
231
|
+
writeMaterializedSkill(path_1.default.join(config.scriptsDir, 'using-awm.md'), options.registryRoot);
|
|
232
|
+
fs_1.default.mkdirSync(path_1.default.dirname(config.settingsPath), { recursive: true });
|
|
233
|
+
(0, atomic_file_1.writeFileAtomic)(config.settingsPath, JSON.stringify(merged, null, 2) + '\n');
|
|
234
|
+
return {
|
|
235
|
+
status: 'installed',
|
|
236
|
+
scriptsDir: config.scriptsDir,
|
|
237
|
+
settingsPath: config.settingsPath,
|
|
238
|
+
backupPath,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function hashFile(file) {
|
|
242
|
+
return crypto_1.default.createHash('sha256').update(fs_1.default.readFileSync(file)).digest('hex');
|
|
243
|
+
}
|
|
244
|
+
function computeCursorTrust(scriptCheck, scriptPath, heartbeatPath) {
|
|
245
|
+
if (!scriptCheck.ok)
|
|
246
|
+
return undefined;
|
|
247
|
+
if (!fs_1.default.existsSync(heartbeatPath))
|
|
248
|
+
return 'pending-first-run';
|
|
249
|
+
let heartbeat;
|
|
250
|
+
try {
|
|
251
|
+
heartbeat = JSON.parse(fs_1.default.readFileSync(heartbeatPath, 'utf-8'));
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return 'stale';
|
|
255
|
+
}
|
|
256
|
+
return heartbeat?.hash === hashFile(scriptPath) ? 'healthy' : 'stale';
|
|
257
|
+
}
|
|
258
|
+
function checkCursorSettingsEntry(settingsPath, scriptsDir, disableDeferred = false) {
|
|
259
|
+
if (!fs_1.default.existsSync(settingsPath)) {
|
|
260
|
+
return { ok: false, detail: `hooks.json not found: ${settingsPath}` };
|
|
261
|
+
}
|
|
262
|
+
let parsed;
|
|
263
|
+
try {
|
|
264
|
+
parsed = JSON.parse(fs_1.default.readFileSync(settingsPath, 'utf-8'));
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return { ok: false, detail: 'hooks.json is not valid JSON' };
|
|
268
|
+
}
|
|
269
|
+
if (parsed.version !== 1) {
|
|
270
|
+
return { ok: false, detail: 'hooks.json version is not 1' };
|
|
271
|
+
}
|
|
272
|
+
const events = activeEvents(disableDeferred);
|
|
273
|
+
for (const event of events) {
|
|
274
|
+
const entries = parsed.hooks?.[event];
|
|
275
|
+
if (!Array.isArray(entries) || !entries.some((entry) => isAwmCursorEntry(entry, scriptsDir, event))) {
|
|
276
|
+
return { ok: false, detail: `no AWM ${event} entry in ${settingsPath}` };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return { ok: true, detail: settingsPath };
|
|
280
|
+
}
|
|
281
|
+
function computeCursorHookStatus(agent) {
|
|
282
|
+
const config = (0, providers_1.getHookConfig)(agent);
|
|
283
|
+
if (!config || config.type !== 'cursor-hooks-json') {
|
|
284
|
+
throw new Error('Cursor hook configuration is unavailable');
|
|
285
|
+
}
|
|
286
|
+
const scriptPath = path_1.default.join(config.scriptsDir, 'session-start');
|
|
287
|
+
const heartbeatPath = path_1.default.join(config.scriptsDir, 'heartbeat.json');
|
|
288
|
+
const sessionStartScript = (0, shared_1.checkExecutable)(scriptPath);
|
|
289
|
+
const bootstrapSkill = (0, shared_1.checkFile)(path_1.default.join(config.scriptsDir, 'using-awm.md'));
|
|
290
|
+
const settingsEntry = checkCursorSettingsEntry(config.settingsPath, config.scriptsDir);
|
|
291
|
+
const allOk = sessionStartScript.ok && settingsEntry.ok && bootstrapSkill.ok;
|
|
292
|
+
const settingsOnlyMissing = !settingsEntry.ok && sessionStartScript.ok;
|
|
293
|
+
const trust = settingsEntry.ok
|
|
294
|
+
? computeCursorTrust(sessionStartScript, scriptPath, heartbeatPath)
|
|
295
|
+
: undefined;
|
|
296
|
+
let overall;
|
|
297
|
+
if (!allOk)
|
|
298
|
+
overall = settingsOnlyMissing && bootstrapSkill.ok ? 'NOT_INSTALLED' : 'DEGRADED';
|
|
299
|
+
else if (trust === 'stale')
|
|
300
|
+
overall = 'DEGRADED';
|
|
301
|
+
else if (trust === 'pending-first-run')
|
|
302
|
+
overall = 'PENDING_TRUST';
|
|
303
|
+
else
|
|
304
|
+
overall = 'HEALTHY';
|
|
305
|
+
return {
|
|
306
|
+
overall,
|
|
307
|
+
trust,
|
|
308
|
+
checks: { sessionStartScript, settingsEntry, bootstrapSkill },
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function uninstallCursorHook(agent) {
|
|
312
|
+
const config = (0, providers_1.getHookConfig)(agent);
|
|
313
|
+
if (!config || config.type !== 'cursor-hooks-json') {
|
|
314
|
+
throw new Error('Cursor hook configuration is unavailable');
|
|
315
|
+
}
|
|
316
|
+
if (!fs_1.default.existsSync(config.settingsPath)) {
|
|
317
|
+
return { status: 'not-installed', backupPath: null };
|
|
318
|
+
}
|
|
319
|
+
const current = (0, shared_1.readStrictJson)(config.settingsPath);
|
|
320
|
+
const hooks = current.hooks && typeof current.hooks === 'object' && !Array.isArray(current.hooks)
|
|
321
|
+
? { ...current.hooks }
|
|
322
|
+
: {};
|
|
323
|
+
let removed = false;
|
|
324
|
+
const nextHooks = { ...hooks };
|
|
325
|
+
for (const event of exports.CURSOR_HOOK_EVENTS) {
|
|
326
|
+
if (!Array.isArray(hooks[event]))
|
|
327
|
+
continue;
|
|
328
|
+
const entries = hooks[event];
|
|
329
|
+
const filtered = entries.filter((entry) => !isAwmCursorEntry(entry, config.scriptsDir, event));
|
|
330
|
+
if (filtered.length !== entries.length)
|
|
331
|
+
removed = true;
|
|
332
|
+
if (filtered.length === 0)
|
|
333
|
+
delete nextHooks[event];
|
|
334
|
+
else
|
|
335
|
+
nextHooks[event] = filtered;
|
|
336
|
+
}
|
|
337
|
+
if (!removed)
|
|
338
|
+
return { status: 'not-installed', backupPath: null };
|
|
339
|
+
const backupPath = (0, shared_1.backupManagedFile)(config.settingsPath);
|
|
340
|
+
const nextConfig = { ...current, hooks: nextHooks };
|
|
341
|
+
if (Object.keys(nextHooks).length === 0)
|
|
342
|
+
delete nextConfig.hooks;
|
|
343
|
+
(0, atomic_file_1.writeFileAtomic)(config.settingsPath, JSON.stringify(nextConfig, null, 2) + '\n');
|
|
344
|
+
return { status: 'uninstalled', backupPath };
|
|
345
|
+
}
|
|
346
|
+
function resyncCursorHookFiles(config, registryRoot, method) {
|
|
347
|
+
for (const event of exports.CURSOR_HOOK_EVENTS) {
|
|
348
|
+
const meta = SCRIPT_BY_EVENT[event];
|
|
349
|
+
const source = path_1.default.join(registryRoot, meta.source);
|
|
350
|
+
if (!fs_1.default.existsSync(source)) {
|
|
351
|
+
throw new Error(`Cursor hook source missing: ${source}. Run 'awm update' first.`);
|
|
352
|
+
}
|
|
353
|
+
(0, shared_1.syncExecutable)(source, path_1.default.join(config.scriptsDir, meta.dest), method);
|
|
354
|
+
}
|
|
355
|
+
writeMaterializedSkill(path_1.default.join(config.scriptsDir, 'using-awm.md'), registryRoot);
|
|
356
|
+
}
|
|
357
|
+
function cursorResyncSourcesExist(registryRoot) {
|
|
358
|
+
return exports.CURSOR_HOOK_EVENTS.every((event) => fs_1.default.existsSync(path_1.default.join(registryRoot, SCRIPT_BY_EVENT[event].source)));
|
|
359
|
+
}
|
|
@@ -4,6 +4,7 @@ exports.installHook = installHook;
|
|
|
4
4
|
const providers_1 = require("../../providers");
|
|
5
5
|
const claude_1 = require("./claude");
|
|
6
6
|
const codex_1 = require("./codex");
|
|
7
|
+
const cursor_1 = require("./cursor");
|
|
7
8
|
function installHook(options) {
|
|
8
9
|
const config = (0, providers_1.getHookConfig)(options.agent);
|
|
9
10
|
if (!config) {
|
|
@@ -14,6 +15,8 @@ function installHook(options) {
|
|
|
14
15
|
return (0, claude_1.installClaudeHook)(options);
|
|
15
16
|
case 'codex-hooks-json':
|
|
16
17
|
return (0, codex_1.installCodexHook)(options);
|
|
18
|
+
case 'cursor-hooks-json':
|
|
19
|
+
return (0, cursor_1.installCursorHook)(options);
|
|
17
20
|
/* istanbul ignore next -- HookConfig['type'] is exhaustively handled above */
|
|
18
21
|
default: {
|
|
19
22
|
const exhaustive = config.type;
|
|
@@ -10,6 +10,7 @@ const providers_1 = require("../../providers");
|
|
|
10
10
|
const status_1 = require("./status");
|
|
11
11
|
const claude_1 = require("./claude");
|
|
12
12
|
const codex_1 = require("./codex");
|
|
13
|
+
const cursor_1 = require("./cursor");
|
|
13
14
|
function detectInstallMethod(scriptsDir) {
|
|
14
15
|
try {
|
|
15
16
|
return fs_1.default.lstatSync(path_1.default.join(scriptsDir, 'session-start')).isSymbolicLink() ? 'symlink' : 'copy';
|
|
@@ -49,6 +50,15 @@ function resyncInstalledHooks(registryRoot, targets = [...providers_1.AGENT_TARG
|
|
|
49
50
|
results.push({ agent, action: 'resynced' });
|
|
50
51
|
break;
|
|
51
52
|
}
|
|
53
|
+
case 'cursor-hooks-json': {
|
|
54
|
+
if (!(0, cursor_1.cursorResyncSourcesExist)(registryRoot)) {
|
|
55
|
+
results.push({ agent, action: 'registry-missing' });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
(0, cursor_1.resyncCursorHookFiles)(config, registryRoot, method);
|
|
59
|
+
results.push({ agent, action: 'resynced' });
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
52
62
|
/* istanbul ignore next -- HookConfig['type'] is exhaustively handled above */
|
|
53
63
|
default: {
|
|
54
64
|
const exhaustive = config.type;
|
|
@@ -4,6 +4,7 @@ exports.computeHookStatus = computeHookStatus;
|
|
|
4
4
|
const providers_1 = require("../../providers");
|
|
5
5
|
const claude_1 = require("./claude");
|
|
6
6
|
const codex_1 = require("./codex");
|
|
7
|
+
const cursor_1 = require("./cursor");
|
|
7
8
|
function computeHookStatus(agent) {
|
|
8
9
|
const config = (0, providers_1.getHookConfig)(agent);
|
|
9
10
|
if (!config) {
|
|
@@ -14,6 +15,8 @@ function computeHookStatus(agent) {
|
|
|
14
15
|
return (0, claude_1.computeClaudeHookStatus)(agent);
|
|
15
16
|
case 'codex-hooks-json':
|
|
16
17
|
return (0, codex_1.computeCodexHookStatus)(agent);
|
|
18
|
+
case 'cursor-hooks-json':
|
|
19
|
+
return (0, cursor_1.computeCursorHookStatus)(agent);
|
|
17
20
|
/* istanbul ignore next -- HookConfig['type'] is exhaustively handled above */
|
|
18
21
|
default: {
|
|
19
22
|
const exhaustive = config.type;
|
|
@@ -4,6 +4,7 @@ exports.uninstallHook = uninstallHook;
|
|
|
4
4
|
const providers_1 = require("../../providers");
|
|
5
5
|
const claude_1 = require("./claude");
|
|
6
6
|
const codex_1 = require("./codex");
|
|
7
|
+
const cursor_1 = require("./cursor");
|
|
7
8
|
function uninstallHook(options) {
|
|
8
9
|
const config = (0, providers_1.getHookConfig)(options.agent);
|
|
9
10
|
if (!config) {
|
|
@@ -14,6 +15,8 @@ function uninstallHook(options) {
|
|
|
14
15
|
return (0, claude_1.uninstallClaudeHook)(options.agent);
|
|
15
16
|
case 'codex-hooks-json':
|
|
16
17
|
return (0, codex_1.uninstallCodexHook)(options.agent);
|
|
18
|
+
case 'cursor-hooks-json':
|
|
19
|
+
return (0, cursor_1.uninstallCursorHook)(options.agent);
|
|
17
20
|
/* istanbul ignore next -- HookConfig['type'] is exhaustively handled above */
|
|
18
21
|
default: {
|
|
19
22
|
const exhaustive = config.type;
|
|
@@ -32,6 +32,7 @@ const reconciliation_1 = require("../core/reconciliation");
|
|
|
32
32
|
const bundles_1 = require("../core/bundles");
|
|
33
33
|
const install_transaction_1 = require("../core/install-transaction");
|
|
34
34
|
const resync_1 = require("../commands/hooks/resync");
|
|
35
|
+
const migrate_cursor_mdc_1 = require("../core/init/migrate-cursor-mdc");
|
|
35
36
|
const defaultDeps = {
|
|
36
37
|
syncRegistries: registries_1.syncRegistries,
|
|
37
38
|
verifyMinCliVersions: registries_1.verifyMinCliVersions,
|
|
@@ -165,6 +166,9 @@ async function runUpdateCore(options = {}, deps = {}) {
|
|
|
165
166
|
if (artifactResult.installed.length > 0) {
|
|
166
167
|
console.log(picocolors_1.default.green(` ✓ Reconciled artifacts: ${artifactResult.installed.join(', ')}`));
|
|
167
168
|
}
|
|
169
|
+
if (selectedAgents.includes('cursor')) {
|
|
170
|
+
(0, migrate_cursor_mdc_1.migrateCursorMdcSkills)({});
|
|
171
|
+
}
|
|
168
172
|
const hooksRoot = (0, registries_1.capabilityRoot)('hooks');
|
|
169
173
|
if (hooksRoot) {
|
|
170
174
|
try {
|
|
@@ -166,7 +166,7 @@ function sanitizeAdmissionReport(report) {
|
|
|
166
166
|
invalidAdmission();
|
|
167
167
|
if (value.cliCurrentness !== undefined && !CLI_CURRENTNESS.has(value.cliCurrentness))
|
|
168
168
|
invalidAdmission();
|
|
169
|
-
if (value.sensorEvidence !== undefined && value.sensorEvidence !== 'generation-bound')
|
|
169
|
+
if (value.sensorEvidence !== undefined && value.sensorEvidence !== 'generation-bound' && value.sensorEvidence !== 'r8-registry-opt-out')
|
|
170
170
|
invalidAdmission();
|
|
171
171
|
if (value.planDigest !== undefined && (typeof value.planDigest !== 'string' || !/^[a-f0-9]{64}$/.test(value.planDigest)))
|
|
172
172
|
invalidAdmission();
|
|
@@ -225,12 +225,18 @@ async function admitPlan(input) {
|
|
|
225
225
|
if (input.compatibilityDiagnostics?.length)
|
|
226
226
|
return blocked(input, input.compatibilityDiagnostics, { planDigest: plan.planDigest, provider, executionMode: mode, currentness: 'unverifiable', cliCurrentness });
|
|
227
227
|
}
|
|
228
|
+
let sensorsReported = 'not-required';
|
|
229
|
+
let sensorEvidence = {};
|
|
228
230
|
if (input.verifySensors) {
|
|
229
231
|
if (!input.sensors)
|
|
230
232
|
return blocked(input, [diagnostic('ADMISSION_SENSORS_REQUIRED', 'Sensor evidence was required but not supplied.')], { planDigest: plan.planDigest, provider, executionMode: mode, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness });
|
|
231
233
|
const sensors = sensorVerdict(input.sensors);
|
|
232
|
-
|
|
234
|
+
const registryOptOut = sensors === 'not-certified' && input.sensorClosurePolicy === 'r8-registry-opt-out' && input.sensors.mode === 'opt-out';
|
|
235
|
+
if (sensors !== 'pass' && !registryOptOut)
|
|
233
236
|
return blocked(input, [diagnostic('ADMISSION_SENSORS_BLOCKED', `Sensor verdict is ${sensors}.`)], { planDigest: plan.planDigest, provider, executionMode: mode, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors });
|
|
237
|
+
sensorsReported = sensors;
|
|
238
|
+
if (registryOptOut)
|
|
239
|
+
sensorEvidence = { sensorEvidence: 'r8-registry-opt-out' };
|
|
234
240
|
}
|
|
235
241
|
if (mode === 'desatendido') {
|
|
236
242
|
// The durable controller (journal + awm watch + jobs) is opt-in, as it
|
|
@@ -241,7 +247,7 @@ async function admitPlan(input) {
|
|
|
241
247
|
const observed = journalStatus(input, plan);
|
|
242
248
|
const journal = observed === 'missing' && plan.schema === 'compact-slices/v1' ? 'not-required' : observed;
|
|
243
249
|
if (journal !== 'current' && journal !== 'not-required')
|
|
244
|
-
return blocked(input, [diagnostic('ADMISSION_JOURNAL_BINDING_REQUIRED', 'Unattended execution requires a healthy schema-2 journal binding for this exact plan; run watch --init --plan.')], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors:
|
|
250
|
+
return blocked(input, [diagnostic('ADMISSION_JOURNAL_BINDING_REQUIRED', 'Unattended execution requires a healthy schema-2 journal binding for this exact plan; run watch --init --plan.')], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors: sensorsReported, ...sensorEvidence });
|
|
245
251
|
const capabilities = unattendedCapabilities(provider, input.controllerAutonomy);
|
|
246
252
|
const resolution = { outcome: capabilities.unattendedController === 'supported' ? 'native' : 'blocked', provider, capabilities, evidenceVersion: 'r1-v1', diagnostics: [] };
|
|
247
253
|
if (resolution.outcome === 'blocked') {
|
|
@@ -252,17 +258,17 @@ async function admitPlan(input) {
|
|
|
252
258
|
const reason = exports.PROVIDER_EXECUTION_CAPABILITIES[provider].unattendedController !== 'supported'
|
|
253
259
|
? diagnostic('ADMISSION_CAPABILITY_UNVERIFIED', `Provider ${provider} has no verified unattended execution capability.`)
|
|
254
260
|
: diagnostic('ADMISSION_CONTROLLER_AUTONOMY_REQUIRED', `Unattended dispatch on ${provider} requires an explicit controller autonomy posture; without it the controller blocks on its first tool call.`);
|
|
255
|
-
return blocked(input, [reason], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors:
|
|
261
|
+
return blocked(input, [reason], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors: sensorsReported, ...sensorEvidence, capabilityResolution: resolution });
|
|
256
262
|
}
|
|
257
263
|
const routing = plan.schema === 'compact-slices/v2' ? input.routing ?? await input.routingReader?.() : input.routing;
|
|
258
|
-
return completeAdmission({ ...input, routing, routingReader: undefined }, plan, provider, mode, journal, input.requireCurrent ? 'current' : 'not-checked',
|
|
264
|
+
return { ...completeAdmission({ ...input, routing, routingReader: undefined }, plan, provider, mode, journal, input.requireCurrent ? 'current' : 'not-checked', sensorsReported, resolution, cliCurrentness), ...sensorEvidence };
|
|
259
265
|
}
|
|
260
266
|
const capabilities = exports.PROVIDER_EXECUTION_CAPABILITIES[provider];
|
|
261
267
|
const resolution = { outcome: capabilities.interactiveExecution === 'supported' ? 'native' : 'blocked', provider, capabilities, evidenceVersion: 'r1-v1', diagnostics: [] };
|
|
262
268
|
if (resolution.outcome === 'blocked')
|
|
263
269
|
return blocked(input, [diagnostic('ADMISSION_CAPABILITY_UNVERIFIED', `Provider ${provider} has no verified interactive execution capability.`)], { planDigest: plan.planDigest, provider, executionMode: mode, capabilityResolution: resolution });
|
|
264
270
|
const routing = plan.schema === 'compact-slices/v2' ? input.routing ?? await input.routingReader?.() : input.routing;
|
|
265
|
-
return completeAdmission({ ...input, routing, routingReader: undefined }, plan, provider, mode, 'not-required', input.requireCurrent ? 'current' : 'not-checked',
|
|
271
|
+
return { ...completeAdmission({ ...input, routing, routingReader: undefined }, plan, provider, mode, 'not-required', input.requireCurrent ? 'current' : 'not-checked', sensorsReported, resolution, cliCurrentness), ...sensorEvidence };
|
|
266
272
|
}
|
|
267
273
|
function completeAdmission(input, plan, provider, executionMode, journal, currentness, sensors, capabilityResolution, cliCurrentness = 'not-checked') {
|
|
268
274
|
if (plan.schema === 'compact-slices/v1')
|