ai-maestro 1.0.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +15 -0
  3. package/README.md +727 -0
  4. package/bin/maestro.mjs +246 -0
  5. package/package.json +29 -0
  6. package/src/checkpoint.mjs +102 -0
  7. package/src/codex-diagnostics.mjs +682 -0
  8. package/src/codex-home-inspect.mjs +252 -0
  9. package/src/codex-home.mjs +258 -0
  10. package/src/commands.mjs +809 -0
  11. package/src/config.mjs +11 -0
  12. package/src/debug.mjs +164 -0
  13. package/src/encoding-guard.mjs +125 -0
  14. package/src/engines/ai-router-engine.mjs +37 -0
  15. package/src/engines/codex-engine.mjs +21 -0
  16. package/src/engines/engine.mjs +21 -0
  17. package/src/engines/registry.mjs +29 -0
  18. package/src/files.mjs +65 -0
  19. package/src/format.mjs +132 -0
  20. package/src/lock.mjs +439 -0
  21. package/src/memory-log.mjs +18 -0
  22. package/src/memory.mjs +1 -0
  23. package/src/orchestration/attempt-chain-runtime.mjs +627 -0
  24. package/src/orchestration/budget-manager.mjs +121 -0
  25. package/src/orchestration/capability-registry.mjs +108 -0
  26. package/src/orchestration/consolidator.mjs +772 -0
  27. package/src/orchestration/delegation-executor.mjs +262 -0
  28. package/src/orchestration/delegation-manager.mjs +116 -0
  29. package/src/orchestration/deployment-intent.mjs +36 -0
  30. package/src/orchestration/engine-history.mjs +110 -0
  31. package/src/orchestration/engine-policy.mjs +73 -0
  32. package/src/orchestration/engine-selector.mjs +187 -0
  33. package/src/orchestration/execution-context.mjs +45 -0
  34. package/src/orchestration/failure-classifier.mjs +136 -0
  35. package/src/orchestration/failure-evidence.mjs +237 -0
  36. package/src/orchestration/fallback-chain-lock.mjs +217 -0
  37. package/src/orchestration/fallback-chain-trail.mjs +218 -0
  38. package/src/orchestration/fallback-executor.mjs +146 -0
  39. package/src/orchestration/fallback-graph.mjs +106 -0
  40. package/src/orchestration/fallback-recommendation.mjs +73 -0
  41. package/src/orchestration/file-conflict-detector.mjs +126 -0
  42. package/src/orchestration/host-validation.mjs +241 -0
  43. package/src/orchestration/orchestration-loop.mjs +1971 -0
  44. package/src/orchestration/orchestration-runtime.mjs +1019 -0
  45. package/src/orchestration/orchestration-scheduler.mjs +135 -0
  46. package/src/orchestration/orchestration-trail.mjs +192 -0
  47. package/src/orchestration/preflight.mjs +212 -0
  48. package/src/orchestration/provider-health.mjs +566 -0
  49. package/src/orchestration/provider-router.mjs +352 -0
  50. package/src/orchestration/rc-check-adapters.mjs +817 -0
  51. package/src/orchestration/rc-check.mjs +544 -0
  52. package/src/orchestration/rc-policy.mjs +200 -0
  53. package/src/orchestration/runtime-gate.mjs +146 -0
  54. package/src/orchestration/sector-managers.mjs +215 -0
  55. package/src/orchestration/self-hosting-canary.mjs +231 -0
  56. package/src/orchestration/self-hosting-readiness.mjs +244 -0
  57. package/src/orchestration/spec-planner.mjs +877 -0
  58. package/src/orchestration/subtask-planner.mjs +176 -0
  59. package/src/orchestration/task-classifier.mjs +241 -0
  60. package/src/orchestration/verifier.mjs +1608 -0
  61. package/src/orchestration-commands.mjs +279 -0
  62. package/src/planner.mjs +38 -0
  63. package/src/project.mjs +1 -0
  64. package/src/run-diagnostics.mjs +641 -0
  65. package/src/runner.mjs +243 -0
  66. package/src/safety.mjs +116 -0
  67. package/src/schema.mjs +371 -0
  68. package/src/session-commands.mjs +521 -0
  69. package/src/shell.mjs +93 -0
  70. package/src/smart-planner.mjs +249 -0
  71. package/src/task-commands.mjs +1182 -0
  72. package/src/tasks.mjs +134 -0
  73. package/src/ui/commands.mjs +76 -0
  74. package/src/ui/events.mjs +45 -0
  75. package/src/ui/public/app.js +600 -0
  76. package/src/ui/public/index.html +88 -0
  77. package/src/ui/public/style.css +460 -0
  78. package/src/ui/server.mjs +112 -0
  79. package/src/ui/state.mjs +504 -0
  80. package/src/usage.mjs +178 -0
  81. package/src/workspace-diff.mjs +228 -0
@@ -0,0 +1,817 @@
1
+ import crypto from 'crypto';
2
+ import fs from 'fs/promises';
3
+ import os from 'os';
4
+ import path from 'path';
5
+
6
+ import { CONFIG } from '../config.mjs';
7
+ import { redactCredentials, redactObject, truncatePreview } from '../format.mjs';
8
+ import { execFileRaw } from '../shell.mjs';
9
+ import { schemaCheck, doctor } from '../session-commands.mjs';
10
+ import { validateBudget, validateEnginesFile } from '../schema.mjs';
11
+ import { loadEngineRegistry } from './capability-registry.mjs';
12
+ import { loadBudget } from './budget-manager.mjs';
13
+ import { getFullHealthSnapshot } from './provider-health.mjs';
14
+ import {
15
+ FALLBACK_CHAIN_TERMINAL_EVENTS,
16
+ groupChainsById,
17
+ readFallbackChainEvents,
18
+ replayFallbackChain,
19
+ validateFallbackChainEvent
20
+ } from './fallback-chain-trail.mjs';
21
+ import { inspectFallbackChainLock } from './fallback-chain-lock.mjs';
22
+ import { runReadinessCheck } from './self-hosting-readiness.mjs';
23
+ import { runControlledCanary } from './self-hosting-canary.mjs';
24
+
25
+ const SENSITIVE_PATH_RE = /(^|[\\/])\.env($|[\\/])|(^|[\\/])package-lock\.json$/i;
26
+ const SOURCE_PATH_RE = /^(src|bin|tests)[\\/]|^package\.json$/i;
27
+ const QUIET_TRUNCATION = { maxBytes: 4096, maxLines: 80 };
28
+ const OLD_TEMP_MS = 24 * 60 * 60 * 1000;
29
+ const TEMP_YOUNG_MS = 60 * 60 * 1000;
30
+
31
+ function result(input = {}) {
32
+ return redactObject({
33
+ checkId: input.checkId,
34
+ category: input.category,
35
+ status: input.status || 'PASS',
36
+ severity: input.severity || 'INFO',
37
+ required: input.required !== false,
38
+ executed: input.executed !== false,
39
+ skipped: input.skipped === true,
40
+ skipReason: input.skipReason == null ? null : String(input.skipReason),
41
+ durationMs: input.durationMs || 0,
42
+ timeoutMs: input.timeoutMs || 0,
43
+ exitCode: input.exitCode == null ? null : input.exitCode,
44
+ summary: redactCredentials(input.summary || ''),
45
+ evidence: Array.isArray(input.evidence) ? input.evidence.map(evidence => ({
46
+ kind: evidence.kind || 'note',
47
+ path: evidence.path || null,
48
+ line: Number.isInteger(evidence.line) ? evidence.line : null,
49
+ identifier: evidence.identifier || null,
50
+ redacted: evidence.redacted !== false,
51
+ details: redactCredentials(evidence.details || '')
52
+ })) : [],
53
+ sideEffectClass: input.sideEffectClass || 'PURE_READ_ONLY',
54
+ remediation: input.remediation == null ? null : redactCredentials(input.remediation)
55
+ });
56
+ }
57
+
58
+ function commandOutputEvidence(commandResult) {
59
+ const stdout = truncatePreview(redactCredentials(commandResult.stdout || ''), QUIET_TRUNCATION);
60
+ const stderr = truncatePreview(redactCredentials(commandResult.stderr || ''), QUIET_TRUNCATION);
61
+ return [
62
+ { kind: 'stdout', details: stdout.text + (stdout.truncated ? '\n[truncated]' : '') },
63
+ { kind: 'stderr', details: stderr.text + (stderr.truncated ? '\n[truncated]' : '') }
64
+ ];
65
+ }
66
+
67
+ function npmCommand(commandArgs) {
68
+ if (process.platform === 'win32') {
69
+ return {
70
+ command: 'cmd.exe',
71
+ args: ['/d', '/s', '/c', ['npm', ...commandArgs].join(' ')]
72
+ };
73
+ }
74
+ return { command: 'npm', args: commandArgs };
75
+ }
76
+
77
+ async function runNpmAdapter(checkId, commandArgs, context, category = 'test') {
78
+ const deps = context.dependencies || {};
79
+ const runner = deps.execFileRaw || execFileRaw;
80
+ const invocation = npmCommand(commandArgs);
81
+ const commandResult = await runner(invocation.command, invocation.args, {
82
+ cwd: context.projectRoot,
83
+ quiet: true,
84
+ timeoutMs: context.timeoutMs
85
+ });
86
+ const ok = commandResult.code === 0 && commandResult.timedOut !== true;
87
+ return result({
88
+ checkId,
89
+ category,
90
+ status: commandResult.timedOut ? 'TIMEOUT' : (ok ? 'PASS' : 'FAIL'),
91
+ severity: ok ? 'INFO' : 'BLOCKER',
92
+ exitCode: commandResult.code,
93
+ timeoutMs: context.timeoutMs,
94
+ summary: ok ? checkId + ' passed' : checkId + ' failed',
95
+ evidence: commandOutputEvidence(commandResult),
96
+ sideEffectClass: checkId === 'npm-test' ? 'TEMPORARY_SANDBOX_WRITE' : 'READ_ONLY_WITH_DIAGNOSTIC_OUTPUT',
97
+ remediation: ok ? null : 'Run ' + ['npm', ...commandArgs].join(' ') + ' locally and fix failures.'
98
+ });
99
+ }
100
+
101
+ async function captureConsole(fn) {
102
+ const original = {
103
+ log: console.log,
104
+ warn: console.warn,
105
+ error: console.error
106
+ };
107
+ const lines = [];
108
+ console.log = (...args) => { lines.push(args.join(' ')); };
109
+ console.warn = (...args) => { lines.push(args.join(' ')); };
110
+ console.error = (...args) => { lines.push(args.join(' ')); };
111
+ try {
112
+ const value = await fn();
113
+ return { value, output: lines.join('\n') };
114
+ } finally {
115
+ console.log = original.log;
116
+ console.warn = original.warn;
117
+ console.error = original.error;
118
+ }
119
+ }
120
+
121
+ function normalizeStatusLine(line) {
122
+ if (!line) return null;
123
+ const status = line.slice(0, 2);
124
+ const rel = line.slice(3).trim().replace(/^"|"$/g, '');
125
+ if (!rel) return null;
126
+ return { status, path: rel };
127
+ }
128
+
129
+ function pathIsKnownDirty(relPath, policy = {}) {
130
+ return (policy.knownDirtyPaths || []).some(prefix => relPath === prefix || relPath.startsWith(prefix.replace(/[\\/]+$/, '') + path.sep) || relPath.startsWith(prefix.replace(/[\\/]+$/, '') + '/'));
131
+ }
132
+
133
+ function classifyGitEntries(entries, policy, branch, detached) {
134
+ const blockers = [];
135
+ const warnings = [];
136
+ const human = [];
137
+ for (const entry of entries) {
138
+ const rel = entry.path.replace(/\\/g, '/');
139
+ const staged = entry.status[0] !== ' ' && entry.status[0] !== '?';
140
+ const untracked = entry.status === '??';
141
+ if (rel === 'package-lock.json') blockers.push({ rel, reason: 'unexpected package-lock change' });
142
+ else if (SENSITIVE_PATH_RE.test(rel)) blockers.push({ rel, reason: 'sensitive path changed' });
143
+ else if (SOURCE_PATH_RE.test(rel) && (staged || untracked || entry.status.trim())) blockers.push({ rel, reason: staged ? 'staged/source dirty' : 'source dirty' });
144
+ else if (rel.startsWith('.maestro/') || rel.startsWith('.memory/') || pathIsKnownDirty(rel, policy)) warnings.push({ rel, reason: 'known control-plane or memory dirty path' });
145
+ else warnings.push({ rel, reason: 'non-source dirty path' });
146
+ }
147
+ if (!branch || branch === 'HEAD' || detached) human.push({ rel: null, reason: 'detached HEAD or unknown branch' });
148
+ else if (branch !== 'master') {
149
+ if (policy.strictGitState) blockers.push({ rel: null, reason: 'unexpected branch in strictGitState: ' + branch });
150
+ else human.push({ rel: null, reason: 'unexpected branch: ' + branch });
151
+ }
152
+ return { blockers, warnings, human };
153
+ }
154
+
155
+ function statusForFindings({ blockers, warnings, human }, strict) {
156
+ if (blockers.length) return { status: 'FAIL', severity: 'BLOCKER' };
157
+ if (human.length) return { status: 'NEEDS_HUMAN', severity: 'HUMAN' };
158
+ if (warnings.length && strict) return { status: 'FAIL', severity: 'BLOCKER' };
159
+ if (warnings.length) return { status: 'WARNING', severity: 'INFRA_WARNING' };
160
+ return { status: 'PASS', severity: 'INFO' };
161
+ }
162
+
163
+ async function gitStateAdapterRun(context) {
164
+ const deps = context.dependencies || {};
165
+ if (deps.gitState) {
166
+ const fake = deps.gitState;
167
+ const findings = classifyGitEntries((fake.entries || []).map(entry => typeof entry === 'string' ? normalizeStatusLine(entry) : entry).filter(Boolean), context.policy, fake.branch, fake.detached);
168
+ const classified = statusForFindings(findings, context.strict || context.policy.strictGitState);
169
+ return result({
170
+ checkId: 'git-state',
171
+ category: 'git',
172
+ ...classified,
173
+ summary: classified.status === 'PASS' ? 'Git state is clean' : 'Git state requires attention',
174
+ evidence: [
175
+ { kind: 'git', identifier: 'head', details: fake.head || 'unknown' },
176
+ { kind: 'git', identifier: 'branch', details: fake.branch || 'unknown' },
177
+ ...[...findings.blockers, ...findings.warnings, ...findings.human].map(item => ({ kind: 'git-status', path: item.rel, details: item.reason }))
178
+ ],
179
+ remediation: classified.status === 'PASS' ? null : 'Review Git status without staging unrelated files.'
180
+ });
181
+ }
182
+ const runner = deps.execFileRaw || execFileRaw;
183
+ const [head, branch, status] = await Promise.all([
184
+ runner('git', ['rev-parse', 'HEAD'], { cwd: context.projectRoot, quiet: true, timeoutMs: context.timeoutMs }),
185
+ runner('git', ['branch', '--show-current'], { cwd: context.projectRoot, quiet: true, timeoutMs: context.timeoutMs }),
186
+ runner('git', ['status', '--porcelain'], { cwd: context.projectRoot, quiet: true, timeoutMs: context.timeoutMs })
187
+ ]);
188
+ if (head.code !== 0 || status.code !== 0) {
189
+ return result({
190
+ checkId: 'git-state',
191
+ category: 'git',
192
+ status: 'FAIL',
193
+ severity: 'BLOCKER',
194
+ exitCode: head.code || status.code,
195
+ summary: 'Git inspection failed',
196
+ evidence: [...commandOutputEvidence(head), ...commandOutputEvidence(status)],
197
+ remediation: 'Fix local Git repository access before RC classification.'
198
+ });
199
+ }
200
+ const branchName = (branch.stdout || '').trim() || 'HEAD';
201
+ const entries = (status.stdout || '').split(/\r?\n/).map(normalizeStatusLine).filter(Boolean);
202
+ const findings = classifyGitEntries(entries, context.policy, branchName, branchName === 'HEAD');
203
+ const classified = statusForFindings(findings, context.strict || context.policy.strictGitState);
204
+ return result({
205
+ checkId: 'git-state',
206
+ category: 'git',
207
+ ...classified,
208
+ summary: classified.status === 'PASS' ? 'Git state is clean' : 'Git state requires attention',
209
+ evidence: [
210
+ { kind: 'git', identifier: 'head', details: (head.stdout || '').trim() },
211
+ { kind: 'git', identifier: 'branch', details: branchName },
212
+ ...[...findings.blockers, ...findings.warnings, ...findings.human].map(item => ({ kind: 'git-status', path: item.rel, details: item.reason }))
213
+ ],
214
+ remediation: classified.status === 'PASS' ? null : 'Commit or account for dirty paths before release.'
215
+ });
216
+ }
217
+
218
+ function healthEntryState(snapshot, targetId) {
219
+ return snapshot && snapshot.targets ? snapshot.targets[targetId] : null;
220
+ }
221
+
222
+ async function providerHealthAdapterRun(context) {
223
+ const deps = context.dependencies || {};
224
+ const snapshot = deps.providerHealthSnapshot || await (deps.getFullHealthSnapshot || getFullHealthSnapshot)({ projectRoot: context.projectRoot });
225
+ if (snapshot.diagnostics && snapshot.diagnostics.degradedReason === 'io-error') {
226
+ return result({
227
+ checkId: 'provider-health',
228
+ category: 'health',
229
+ status: 'FAIL',
230
+ severity: 'BLOCKER',
231
+ summary: 'Provider health could not be read',
232
+ evidence: [{ kind: 'provider-health', details: 'degradedReason=io-error' }],
233
+ remediation: 'Fix provider health files or filesystem access.'
234
+ });
235
+ }
236
+ if (snapshot.diagnostics && snapshot.diagnostics.degraded && snapshot.diagnostics.degradedReason === 'corrupted-json') {
237
+ return result({
238
+ checkId: 'provider-health',
239
+ category: 'health',
240
+ status: 'NEEDS_HUMAN',
241
+ severity: 'HUMAN',
242
+ summary: 'Provider health snapshot is corrupted',
243
+ evidence: [{ kind: 'provider-health', details: 'degradedReason=corrupted-json' }],
244
+ remediation: 'Inspect .maestro/PROVIDER_HEALTH.json manually.'
245
+ });
246
+ }
247
+ const evidence = [];
248
+ let blocked = false;
249
+ let human = false;
250
+ let warning = false;
251
+ for (const targetId of context.policy.requiredHealthTargets || []) {
252
+ const entry = healthEntryState(snapshot, targetId);
253
+ if (!entry) {
254
+ human = true;
255
+ evidence.push({ kind: 'required-health-target', identifier: targetId, details: 'missing' });
256
+ } else if (entry.state === 'OPEN' || entry.state === 'HALF_OPEN') {
257
+ blocked = true;
258
+ evidence.push({ kind: 'required-health-target', identifier: targetId, details: entry.state });
259
+ } else if (entry.state === 'CLOSED') {
260
+ evidence.push({ kind: 'required-health-target', identifier: targetId, details: 'CLOSED' });
261
+ } else {
262
+ human = true;
263
+ evidence.push({ kind: 'required-health-target', identifier: targetId, details: 'unknown state' });
264
+ }
265
+ }
266
+ for (const targetId of context.policy.optionalHealthTargets || []) {
267
+ const entry = healthEntryState(snapshot, targetId);
268
+ if (!entry) {
269
+ warning = true;
270
+ evidence.push({ kind: 'optional-health-target', identifier: targetId, details: 'missing history' });
271
+ } else if (entry.state === 'OPEN' || entry.state === 'HALF_OPEN') {
272
+ warning = true;
273
+ evidence.push({ kind: 'optional-health-target', identifier: targetId, details: entry.state });
274
+ } else if (entry.state !== 'CLOSED') {
275
+ human = true;
276
+ evidence.push({ kind: 'optional-health-target', identifier: targetId, details: 'unknown state' });
277
+ }
278
+ }
279
+ const targetCount = Object.keys(snapshot.targets || {}).length;
280
+ if (!evidence.length) evidence.push({ kind: 'provider-health', details: 'observed targets=' + targetCount + '; no required providers inferred' });
281
+ const status = blocked ? 'FAIL' : human ? 'NEEDS_HUMAN' : warning ? 'WARNING' : 'PASS';
282
+ const severity = blocked ? 'BLOCKER' : human ? 'HUMAN' : warning ? 'INFRA_WARNING' : 'INFO';
283
+ return result({
284
+ checkId: 'provider-health',
285
+ category: 'health',
286
+ status,
287
+ severity,
288
+ summary: status === 'PASS' ? 'Provider health query-only check passed' : 'Provider health requires attention',
289
+ evidence,
290
+ remediation: status === 'PASS' ? null : 'Inspect provider health and required health target policy.'
291
+ });
292
+ }
293
+
294
+ async function schemaCheckAdapterRun(context) {
295
+ const deps = context.dependencies || {};
296
+ const captured = await captureConsole(() => (deps.schemaCheck || schemaCheck)());
297
+ return result({
298
+ checkId: 'schema-check',
299
+ category: 'schema',
300
+ status: captured.value && captured.value.ok ? 'PASS' : 'FAIL',
301
+ severity: captured.value && captured.value.ok ? 'INFO' : 'BLOCKER',
302
+ summary: captured.value && captured.value.ok ? 'schema-check passed' : 'schema-check failed',
303
+ evidence: [
304
+ { kind: 'schema-check', details: JSON.stringify(redactObject(captured.value && captured.value.checks ? captured.value.checks : [])) },
305
+ { kind: 'diagnostic-output', details: truncatePreview(redactCredentials(captured.output), QUIET_TRUNCATION).text }
306
+ ],
307
+ remediation: captured.value && captured.value.ok ? null : 'Run node bin/maestro.mjs schema-check and fix schema errors.'
308
+ });
309
+ }
310
+
311
+ async function readJsonOrStatus(filePath) {
312
+ try {
313
+ return { exists: true, value: JSON.parse(await fs.readFile(filePath, 'utf-8')) };
314
+ } catch (error) {
315
+ if (error.code === 'ENOENT') return { exists: false, value: null };
316
+ if (error instanceof SyntaxError) return { exists: true, corrupted: true, error: error.message };
317
+ throw error;
318
+ }
319
+ }
320
+
321
+ async function engineConfigAdapterRun(context) {
322
+ const deps = context.dependencies || {};
323
+ if (deps.engineConfig) {
324
+ const validation = validateEnginesFile(deps.engineConfig);
325
+ return result({
326
+ checkId: 'engine-config',
327
+ category: 'config',
328
+ status: validation.ok ? 'PASS' : 'FAIL',
329
+ severity: validation.ok ? 'INFO' : 'BLOCKER',
330
+ summary: validation.ok ? 'Engine config is valid' : 'Engine config is invalid',
331
+ evidence: validation.errors.map(error => ({ kind: 'engine-config', details: error })),
332
+ remediation: validation.ok ? null : 'Fix .maestro/engines.json.'
333
+ });
334
+ }
335
+ const filePath = path.join(context.projectRoot, CONFIG.maestroPath, 'engines.json');
336
+ const parsed = await readJsonOrStatus(filePath);
337
+ if (!parsed.exists) {
338
+ return result({ checkId: 'engine-config', category: 'config', status: 'NEEDS_HUMAN', severity: 'HUMAN', summary: 'Engine config is absent', evidence: [{ kind: 'engine-config', path: filePath, details: 'missing' }], remediation: 'Create or verify .maestro/engines.json before release.' });
339
+ }
340
+ if (parsed.corrupted) {
341
+ return result({ checkId: 'engine-config', category: 'config', status: 'FAIL', severity: 'BLOCKER', summary: 'Engine config JSON is invalid', evidence: [{ kind: 'engine-config', path: filePath, details: parsed.error }], remediation: 'Fix .maestro/engines.json syntax.' });
342
+ }
343
+ await (deps.loadEngineRegistry || loadEngineRegistry)();
344
+ const validation = validateEnginesFile(parsed.value);
345
+ return result({ checkId: 'engine-config', category: 'config', status: validation.ok ? 'PASS' : 'FAIL', severity: validation.ok ? 'INFO' : 'BLOCKER', summary: validation.ok ? 'Engine config is valid' : 'Engine config is invalid', evidence: validation.errors.map(error => ({ kind: 'engine-config', path: filePath, details: error })), remediation: validation.ok ? null : 'Fix .maestro/engines.json.' });
346
+ }
347
+
348
+ async function budgetConfigAdapterRun(context) {
349
+ const deps = context.dependencies || {};
350
+ const budget = deps.budgetConfig || await (deps.loadBudget || loadBudget)();
351
+ const validation = validateBudget(budget);
352
+ return result({
353
+ checkId: 'budget-config',
354
+ category: 'config',
355
+ status: validation.ok ? 'PASS' : 'FAIL',
356
+ severity: validation.ok ? 'INFO' : 'BLOCKER',
357
+ summary: validation.ok ? 'Budget config is valid' : 'Budget config is invalid',
358
+ evidence: validation.errors.map(error => ({ kind: 'budget-config', details: error })),
359
+ remediation: validation.ok ? null : 'Fix .maestro/budget.json.'
360
+ });
361
+ }
362
+
363
+ async function safetyConfigAdapterRun(context) {
364
+ const deps = context.dependencies || {};
365
+ const value = deps.safetyConfig;
366
+ let parsed = value ? { exists: true, value } : await readJsonOrStatus(path.join(context.projectRoot, CONFIG.maestroPath, 'safety.json'));
367
+ if (!parsed.exists) {
368
+ return result({ checkId: 'safety-config', category: 'config', status: 'WARNING', severity: 'INFRA_WARNING', summary: 'Safety config absent; runtime defaults apply', evidence: [{ kind: 'safety-config', details: 'missing safety.json; not created by rc-check' }] });
369
+ }
370
+ if (parsed.corrupted) {
371
+ return result({ checkId: 'safety-config', category: 'config', status: 'FAIL', severity: 'BLOCKER', summary: 'Safety config JSON is invalid', evidence: [{ kind: 'safety-config', details: parsed.error }], remediation: 'Fix .maestro/safety.json syntax.' });
372
+ }
373
+ const ok = parsed.value && typeof parsed.value === 'object' && !Array.isArray(parsed.value);
374
+ return result({ checkId: 'safety-config', category: 'config', status: ok ? 'PASS' : 'FAIL', severity: ok ? 'INFO' : 'BLOCKER', summary: ok ? 'Safety config is readable' : 'Safety config is invalid', evidence: [], remediation: ok ? null : 'Fix .maestro/safety.json.' });
375
+ }
376
+
377
+ function isPidAlive(pid) {
378
+ if (!Number.isInteger(pid) || pid <= 0) return false;
379
+ try {
380
+ process.kill(pid, 0);
381
+ return true;
382
+ } catch (error) {
383
+ return error && error.code === 'EPERM';
384
+ }
385
+ }
386
+
387
+ async function listTaskLockFiles(projectRoot) {
388
+ const dir = path.join(projectRoot, CONFIG.maestroPath, 'locks', 'tasks');
389
+ try {
390
+ const entries = await fs.readdir(dir, { withFileTypes: true });
391
+ return entries.filter(entry => entry.isFile() && entry.name.endsWith('.lock.json')).map(entry => path.join(dir, entry.name));
392
+ } catch (error) {
393
+ if (error.code === 'ENOENT') return [];
394
+ throw error;
395
+ }
396
+ }
397
+
398
+ async function readLockFile(filePath) {
399
+ try {
400
+ const lock = JSON.parse(await fs.readFile(filePath, 'utf-8'));
401
+ const pidAlive = isPidAlive(lock && lock.pid);
402
+ return { exists: true, lock, corrupted: false, pidAlive };
403
+ } catch (error) {
404
+ if (error.code === 'ENOENT') return { exists: false, lock: null, corrupted: false, pidAlive: false };
405
+ if (error instanceof SyntaxError) return { exists: true, lock: null, corrupted: true, error: error.message, pidAlive: false };
406
+ throw error;
407
+ }
408
+ }
409
+
410
+ async function lockObservationAdapterRun(context) {
411
+ const deps = context.dependencies || {};
412
+ const observations = deps.lockObservations || [];
413
+ const evidence = [];
414
+ let blocked = false;
415
+ let human = false;
416
+ const addObservation = (label, obs, filePath = null) => {
417
+ if (!obs || !obs.exists) return;
418
+ if (obs.corrupted || obs.lockCorrupted) {
419
+ blocked = true;
420
+ evidence.push({ kind: 'lock', path: filePath, identifier: label, details: 'corrupted' });
421
+ } else if (obs.heldByActive || obs.pidAlive) {
422
+ human = true;
423
+ evidence.push({ kind: 'lock', path: filePath, identifier: label, details: 'active pid=' + (obs.lock && obs.lock.pid) });
424
+ } else {
425
+ human = true;
426
+ evidence.push({ kind: 'lock', path: filePath, identifier: label, details: 'orphan lock observed; not removed' });
427
+ }
428
+ };
429
+ if (observations.length) {
430
+ for (const item of observations) addObservation(item.label || 'lock', item, item.path || null);
431
+ } else {
432
+ addObservation('fallback-chain-global', await (deps.inspectFallbackChainLock || inspectFallbackChainLock)({ projectRoot: context.projectRoot }), path.join(context.projectRoot, CONFIG.maestroPath, 'locks', 'fallback-chain-global.lock.json'));
433
+ addObservation('maestro-global', await readLockFile(path.join(context.projectRoot, CONFIG.maestroPath, '.lock.json')), path.join(context.projectRoot, CONFIG.maestroPath, '.lock.json'));
434
+ for (const filePath of await listTaskLockFiles(context.projectRoot)) addObservation('task-lock', await readLockFile(filePath), filePath);
435
+ }
436
+ return result({
437
+ checkId: 'lock-observation',
438
+ category: 'lock',
439
+ status: blocked ? 'FAIL' : human ? 'NEEDS_HUMAN' : 'PASS',
440
+ severity: blocked ? 'BLOCKER' : human ? 'HUMAN' : 'INFO',
441
+ summary: evidence.length ? 'Locks require attention' : 'No blocking locks observed',
442
+ evidence,
443
+ remediation: evidence.length ? 'Inspect locks manually; rc-check does not remove or recover locks.' : null
444
+ });
445
+ }
446
+
447
+ async function fallbackChainAdapterRun(context) {
448
+ const deps = context.dependencies || {};
449
+ let events;
450
+ try {
451
+ events = deps.fallbackEvents || await (deps.readFallbackChainEvents || readFallbackChainEvents)({ projectRoot: context.projectRoot });
452
+ } catch (error) {
453
+ return result({ checkId: 'fallback-chain-state', category: 'fallback', status: 'FAIL', severity: 'BLOCKER', summary: 'Fallback trail is corrupted or unreadable', evidence: [{ kind: 'fallback-trail', details: error.message || String(error) }], remediation: 'Fix .maestro/FALLBACK_CHAIN_EVENTS.jsonl manually.' });
454
+ }
455
+ const evidence = [];
456
+ let blocked = false;
457
+ let human = false;
458
+ for (const event of events) {
459
+ const validation = validateFallbackChainEvent(event);
460
+ if (!validation.ok) {
461
+ blocked = true;
462
+ evidence.push({ kind: 'fallback-event', identifier: event && event.eventId, details: validation.errors.join('; ') });
463
+ }
464
+ if (event && event.engineId && event.healthTargetId && event.engineId !== event.healthTargetId) {
465
+ human = true;
466
+ evidence.push({ kind: 'fallback-event', identifier: event.eventId, details: 'healthTargetId differs from engineId' });
467
+ }
468
+ }
469
+ const groups = groupChainsById(events);
470
+ for (const [chainId, chainEvents] of groups.entries()) {
471
+ const replayed = replayFallbackChain(chainEvents);
472
+ if (replayed.terminal && replayed.latestEvent && FALLBACK_CHAIN_TERMINAL_EVENTS.has(replayed.latestEvent.eventType)) {
473
+ evidence.push({ kind: 'fallback-chain', identifier: chainId, details: 'terminal=' + replayed.latestEvent.eventType });
474
+ } else if (replayed.inProgressAttempt) {
475
+ human = true;
476
+ evidence.push({ kind: 'fallback-chain', identifier: chainId, details: 'in-progress attempt without terminal event' });
477
+ } else if (chainEvents.length) {
478
+ human = true;
479
+ evidence.push({ kind: 'fallback-chain', identifier: chainId, details: 'incomplete chain' });
480
+ }
481
+ }
482
+ return result({
483
+ checkId: 'fallback-chain-state',
484
+ category: 'fallback',
485
+ status: blocked ? 'FAIL' : human ? 'NEEDS_HUMAN' : 'PASS',
486
+ severity: blocked ? 'BLOCKER' : human ? 'HUMAN' : 'INFO',
487
+ summary: blocked ? 'Fallback trail is corrupted' : human ? 'Fallback chain state needs human review' : 'Fallback trail is consistent or empty',
488
+ evidence,
489
+ remediation: blocked || human ? 'Inspect fallback chain trail and locks manually.' : null
490
+ });
491
+ }
492
+
493
+ async function walk(root, options = {}) {
494
+ const maxFiles = options.maxFiles || 2000;
495
+ const files = [];
496
+ async function visit(dir) {
497
+ if (files.length >= maxFiles) return;
498
+ let entries;
499
+ try {
500
+ entries = await fs.readdir(dir, { withFileTypes: true });
501
+ } catch {
502
+ return;
503
+ }
504
+ for (const entry of entries) {
505
+ const full = path.join(dir, entry.name);
506
+ const rel = path.relative(root, full).replace(/\\/g, '/');
507
+ if (entry.isDirectory()) {
508
+ if (['.git', 'node_modules', 'DESCARTE'].includes(entry.name)) continue;
509
+ await visit(full);
510
+ } else if (entry.isFile()) {
511
+ files.push({ full, rel });
512
+ }
513
+ }
514
+ }
515
+ await visit(root);
516
+ return files;
517
+ }
518
+
519
+ function credentialFindingForLine(line, rel, lineNumber) {
520
+ if (/^tests\//i.test(rel)) return null;
521
+ const text = line.trim();
522
+ if (!text || /(<[^>]*(KEY|TOKEN|SECRET|PASSWORD)[^>]*>|example|fixture|synthetic|placeholder|dummy|fake|test-only|REDACTED)/i.test(text)) return null;
523
+ const patterns = [
524
+ ['authorization-header', /\bAuthorization\s*:\s*(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{12,}/i],
525
+ ['cookie', /\b(Set-Cookie|Cookie)\s*:\s*[^;\s=]+=[A-Za-z0-9._~+/=-]{12,}/i],
526
+ ['url-credential', /https?:\/\/[^\/\s:@]+:[^@\s\/]+@/i],
527
+ ['openai-like-token', /\b(sk|nr)-[A-Za-z0-9_-]{16,}\b/],
528
+ ['key-value-secret', /\b(NINEROUTER_API_KEY|api[_-]?key|token|secret|password|passwd|credential)\s*[:=]\s*['"]?[A-Za-z0-9._~+/=-]{12,}/i]
529
+ ];
530
+ for (const [kind, pattern] of patterns) {
531
+ if (pattern.test(text)) {
532
+ const hash = 'sha256:' + cryptoHash(text).slice(0, 12);
533
+ const ambiguous = /tests?\//i.test(rel) || /\.(md|txt)$/i.test(rel);
534
+ return { kind, path: rel, line: lineNumber, identifier: hash, ambiguous };
535
+ }
536
+ }
537
+ return null;
538
+ }
539
+
540
+ function cryptoHash(text) {
541
+ return crypto.createHash('sha256').update(text).digest('hex');
542
+ }
543
+
544
+ async function credentialAuditAdapterRun(context) {
545
+ const deps = context.dependencies || {};
546
+ const findings = deps.credentialFindings || [];
547
+ const observedFindings = findings.length ? findings : [];
548
+ if (!findings.length) {
549
+ const files = await walk(context.projectRoot, { maxFiles: 2500 });
550
+ for (const file of files) {
551
+ if (!/(\.mjs|\.js|\.json|\.jsonl|\.md|\.txt|\.env)$/i.test(file.rel) && !file.rel.endsWith('.env')) continue;
552
+ let content;
553
+ try {
554
+ const stat = await fs.stat(file.full);
555
+ if (stat.size > 1024 * 1024) continue;
556
+ content = await fs.readFile(file.full, 'utf-8');
557
+ } catch {
558
+ continue;
559
+ }
560
+ const lines = content.split(/\r?\n/);
561
+ for (let index = 0; index < lines.length; index += 1) {
562
+ const finding = credentialFindingForLine(lines[index], file.rel, index + 1);
563
+ if (finding) observedFindings.push(finding);
564
+ }
565
+ }
566
+ }
567
+ const real = observedFindings.filter(item => item.real === true || item.ambiguous !== true);
568
+ const ambiguous = observedFindings.filter(item => item.real !== true && item.ambiguous === true);
569
+ const status = real.length ? 'FAIL' : ambiguous.length ? 'NEEDS_HUMAN' : 'PASS';
570
+ const severity = real.length ? 'BLOCKER' : ambiguous.length ? 'HUMAN' : 'INFO';
571
+ return result({
572
+ checkId: 'credential-audit',
573
+ category: 'credential',
574
+ status,
575
+ severity,
576
+ summary: status === 'PASS' ? 'No real credentials detected' : 'Credential audit found redacted risks',
577
+ evidence: observedFindings.map(item => ({
578
+ kind: item.kind || 'credential-risk',
579
+ path: item.path || null,
580
+ line: item.line || null,
581
+ identifier: item.identifier || null,
582
+ redacted: true,
583
+ details: item.ambiguous ? 'ambiguous credential-like value' : 'credential-like value redacted'
584
+ })),
585
+ remediation: status === 'PASS' ? null : 'Remove real credentials or mark synthetic fixtures unambiguously.'
586
+ });
587
+ }
588
+
589
+ function tempKind(rel) {
590
+ const base = path.basename(rel);
591
+ if (/\.recovery$/i.test(base) || base === '.recovery') return 'recovery';
592
+ if (/canary/i.test(base) && /(^|[-_.])(clone|tmp|temp|sandbox)([-_.]|$)/i.test(base)) return 'canary-clone';
593
+ if (/report/i.test(base) && /\.(tmp|partial|incomplete)$/i.test(base)) return 'incomplete-report';
594
+ if (/^\.tmp-|^tmp-|^temp-|^sandbox-|\.lock\.tmp$/i.test(base)) return 'temp';
595
+ return null;
596
+ }
597
+
598
+ async function tempFilesAdapterRun(context) {
599
+ const deps = context.dependencies || {};
600
+ const now = deps.nowMs || Date.now();
601
+ const items = deps.tempFiles || [];
602
+ const observed = items.length ? items : [];
603
+ if (!items.length) {
604
+ const files = await walk(context.projectRoot, { maxFiles: 2500 });
605
+ for (const file of files) {
606
+ const kind = tempKind(file.rel);
607
+ if (!kind) continue;
608
+ const stat = await fs.stat(file.full).catch(() => null);
609
+ observed.push({ path: file.rel, kind, ageMs: stat ? now - stat.mtimeMs : 0 });
610
+ }
611
+ }
612
+ const evidence = [];
613
+ let warning = false;
614
+ let human = false;
615
+ let blocked = false;
616
+ for (const item of observed) {
617
+ const ageMs = Number.isFinite(item.ageMs) ? item.ageMs : 0;
618
+ const kind = item.kind || tempKind(item.path || '') || 'unknown';
619
+ if (item.associatedCorruptedLock) blocked = true;
620
+ else if (kind === 'recovery' && ageMs > TEMP_YOUNG_MS) human = true;
621
+ else if (kind === 'unknown') human = true;
622
+ else if (ageMs > OLD_TEMP_MS || kind === 'canary-clone' || kind === 'incomplete-report') warning = true;
623
+ evidence.push({ kind: 'temp-file', path: item.path || null, identifier: kind, details: 'ageMs=' + Math.round(ageMs) + (item.associatedCorruptedLock ? '; associated corrupted lock' : '') });
624
+ }
625
+ return result({
626
+ checkId: 'temp-files',
627
+ category: 'temp',
628
+ status: blocked ? 'FAIL' : human ? 'NEEDS_HUMAN' : warning ? 'WARNING' : 'PASS',
629
+ severity: blocked ? 'BLOCKER' : human ? 'HUMAN' : warning ? 'INFRA_WARNING' : 'INFO',
630
+ summary: evidence.length ? 'Temporary files observed; nothing removed' : 'No relevant temporary files observed',
631
+ evidence,
632
+ remediation: evidence.length ? 'Inspect temporary files manually; rc-check does not clean them.' : null
633
+ });
634
+ }
635
+
636
+ async function listProcessesNative(context) {
637
+ if (process.platform !== 'win32') {
638
+ return { supported: false, processes: [], reason: 'process observation not implemented for this platform' };
639
+ }
640
+ const deps = context.dependencies || {};
641
+ const runner = deps.execFileRaw || execFileRaw;
642
+ const script = "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine,ExecutablePath | ConvertTo-Json -Compress";
643
+ const output = await runner('powershell', ['-NoProfile', '-Command', script], { quiet: true, timeoutMs: context.timeoutMs });
644
+ if (output.code !== 0) return { supported: false, processes: [], reason: 'native process query failed' };
645
+ const parsed = output.stdout.trim() ? JSON.parse(output.stdout) : [];
646
+ const rows = Array.isArray(parsed) ? parsed : [parsed];
647
+ return {
648
+ supported: true,
649
+ processes: rows.map(row => ({
650
+ pid: row.ProcessId,
651
+ ppid: row.ParentProcessId,
652
+ commandLine: row.CommandLine || '',
653
+ cwd: null
654
+ }))
655
+ };
656
+ }
657
+
658
+ async function processObservationAdapterRun(context) {
659
+ const deps = context.dependencies || {};
660
+ const observed = deps.processes ? { supported: true, processes: deps.processes } : await listProcessesNative(context);
661
+ if (!observed.supported) {
662
+ return result({
663
+ checkId: 'process-observation',
664
+ category: 'process',
665
+ status: 'WARNING',
666
+ severity: 'INFRA_WARNING',
667
+ required: false,
668
+ summary: 'Process observation has limited platform support',
669
+ evidence: [{ kind: 'process', details: observed.reason || 'unsupported' }],
670
+ remediation: 'Perform manual process inspection if release state depends on locks.'
671
+ });
672
+ }
673
+ const evidence = [];
674
+ let blocked = false;
675
+ let human = false;
676
+ const normalizedRoot = path.resolve(context.projectRoot).toLowerCase();
677
+ for (const proc of observed.processes || []) {
678
+ if (proc.pid === process.pid) continue;
679
+ const commandLine = String(proc.commandLine || '');
680
+ const cwd = proc.cwd ? path.resolve(proc.cwd).toLowerCase() : '';
681
+ const relatedToRoot = cwd === normalizedRoot || commandLine.toLowerCase().includes(normalizedRoot);
682
+ const maestro = /\bmaestro(\.mjs)?\b|bin[\\/]maestro\.mjs/i.test(commandLine);
683
+ const codexClaude = /\b(codex|claude)\b/i.test(commandLine);
684
+ if (maestro && relatedToRoot && proc.orphan === true) {
685
+ blocked = true;
686
+ evidence.push({ kind: 'process', identifier: String(proc.pid), details: 'orphan Maestro process ppid=' + (proc.ppid || 'unknown') + ' cwd=' + (proc.cwd || 'unknown') });
687
+ } else if (maestro && relatedToRoot) {
688
+ human = true;
689
+ evidence.push({ kind: 'process', identifier: String(proc.pid), details: 'active Maestro process ppid=' + (proc.ppid || 'unknown') + ' cwd=' + (proc.cwd || 'unknown') });
690
+ } else if (proc.lockRelated === true) {
691
+ human = true;
692
+ evidence.push({ kind: 'process', identifier: String(proc.pid), details: 'unknown process related to lock' });
693
+ } else if (codexClaude) {
694
+ evidence.push({ kind: 'process', identifier: String(proc.pid), details: 'external Codex/Claude process not linked to projectRoot' });
695
+ }
696
+ }
697
+ return result({
698
+ checkId: 'process-observation',
699
+ category: 'process',
700
+ status: blocked ? 'FAIL' : human ? 'NEEDS_HUMAN' : 'PASS',
701
+ severity: blocked ? 'BLOCKER' : human ? 'HUMAN' : 'INFO',
702
+ summary: blocked ? 'Orphan Maestro process observed' : human ? 'Project-related process needs human review' : 'No project-blocking processes observed',
703
+ evidence,
704
+ remediation: blocked || human ? 'Inspect processes manually; rc-check never terminates processes.' : null
705
+ });
706
+ }
707
+
708
+ async function readinessAdapterRun(context) {
709
+ const deps = context.dependencies || {};
710
+ const captured = await captureConsole(() => (deps.runReadinessCheck || runReadinessCheck)({ quiet: true }));
711
+ const ok = captured.value && captured.value.ok === true;
712
+ const infraWarning = captured.value && captured.value.infrastructure === true;
713
+ return result({
714
+ checkId: 'readiness',
715
+ category: 'release',
716
+ status: ok ? 'PASS' : infraWarning ? 'WARNING' : 'FAIL',
717
+ severity: ok ? 'INFO' : infraWarning ? 'INFRA_WARNING' : 'BLOCKER',
718
+ summary: ok ? 'Readiness passed' : 'Readiness did not pass',
719
+ evidence: [
720
+ { kind: 'readiness', path: captured.value && captured.value.reportPath, details: JSON.stringify(redactObject({ ok: captured.value && captured.value.ok, tempRemoved: captured.value && captured.value.tempRemoved })) },
721
+ { kind: 'diagnostic-output', details: truncatePreview(redactCredentials(captured.output), QUIET_TRUNCATION).text }
722
+ ],
723
+ sideEffectClass: 'PROJECT_CONTROL_PLANE_WRITE',
724
+ remediation: ok ? null : 'Review readiness output and .maestro/READINESS_CHECK.md.'
725
+ });
726
+ }
727
+
728
+ async function doctorAdapterRun(context) {
729
+ const deps = context.dependencies || {};
730
+ const captured = await captureConsole(() => (deps.doctor || doctor)());
731
+ const hasError = /\bERRO\b|\bERROR\b|FAIL/i.test(captured.output);
732
+ return result({
733
+ checkId: 'doctor',
734
+ category: 'release',
735
+ status: hasError ? 'NEEDS_HUMAN' : 'PASS',
736
+ severity: hasError ? 'HUMAN' : 'INFO',
737
+ summary: hasError ? 'Doctor output needs human review' : 'Doctor completed without obvious errors',
738
+ evidence: [{ kind: 'diagnostic-output', details: truncatePreview(redactCredentials(captured.output), QUIET_TRUNCATION).text }],
739
+ sideEffectClass: 'PROJECT_CONTROL_PLANE_WRITE',
740
+ remediation: hasError ? 'Run doctor manually and inspect local environment diagnostics.' : null
741
+ });
742
+ }
743
+
744
+ async function canaryAdapterRun(context) {
745
+ const deps = context.dependencies || {};
746
+ const targetPath = deps.canaryTargetPath || path.join(os.tmpdir(), 'maestro-rc-canary-' + process.pid + '-' + Date.now());
747
+ const canary = await (deps.runControlledCanary || runControlledCanary)({ targetPath, timeoutMs: context.timeoutMs });
748
+ const required = context.policy.canaryRequired === true;
749
+ let status = 'NEEDS_HUMAN';
750
+ let severity = 'HUMAN';
751
+ if (canary.outcome === 'PASSED' && canary.ok === true) {
752
+ status = 'PASS';
753
+ severity = 'INFO';
754
+ } else if (canary.outcome === 'INFRASTRUCTURE_UNAVAILABLE') {
755
+ status = required && context.policy.allowInfraWarning === false ? 'FAIL' : 'WARNING';
756
+ severity = status === 'FAIL' ? 'BLOCKER' : 'INFRA_WARNING';
757
+ } else if (canary.reason && /preflight/i.test(canary.reason)) {
758
+ status = 'FAIL';
759
+ severity = 'BLOCKER';
760
+ } else if (canary.outcome === 'FAILED') {
761
+ status = 'FAIL';
762
+ severity = 'BLOCKER';
763
+ }
764
+ const correlatedHead = !canary.head || !context.head || canary.head === context.head;
765
+ if (!correlatedHead) {
766
+ status = 'NEEDS_HUMAN';
767
+ severity = 'HUMAN';
768
+ }
769
+ if (canary.cleanupFailure === true) {
770
+ status = 'NEEDS_HUMAN';
771
+ severity = 'HUMAN';
772
+ }
773
+ return result({
774
+ checkId: 'canary',
775
+ category: 'canary',
776
+ status,
777
+ severity,
778
+ required,
779
+ summary: status === 'PASS' ? 'Canary passed' : 'Canary did not provide clean release evidence',
780
+ evidence: [
781
+ { kind: 'canary', path: canary.registeredTargetPath || targetPath, identifier: correlatedHead ? 'head-correlated' : 'head-mismatch', details: 'outcome=' + (canary.outcome || 'unknown') }
782
+ ],
783
+ sideEffectClass: 'REAL_ENGINE_EXECUTION',
784
+ remediation: status === 'PASS' ? null : 'Review canary result; canary remains opt-in or policy-required only.'
785
+ });
786
+ }
787
+
788
+ function adapter(definition) {
789
+ return {
790
+ mayUseNetwork: false,
791
+ mayUseRealEngine: false,
792
+ canRunByDefault: true,
793
+ ...definition
794
+ };
795
+ }
796
+
797
+ export function createRcCheckAdapters() {
798
+ return [
799
+ adapter({ checkId: 'git-state', category: 'git', defaultRequired: true, sideEffectClass: 'PURE_READ_ONLY', defaultTimeoutMs: 10000, run: gitStateAdapterRun }),
800
+ adapter({ checkId: 'npm-run-check', category: 'code', defaultRequired: true, sideEffectClass: 'READ_ONLY_WITH_DIAGNOSTIC_OUTPUT', defaultTimeoutMs: 120000, run: context => runNpmAdapter('npm-run-check', ['run', 'check'], context, 'code') }),
801
+ adapter({ checkId: 'npm-test', category: 'test', defaultRequired: true, sideEffectClass: 'TEMPORARY_SANDBOX_WRITE', defaultTimeoutMs: 600000, run: context => runNpmAdapter('npm-test', ['test'], context, 'test') }),
802
+ adapter({ checkId: 'schema-check', category: 'schema', defaultRequired: true, sideEffectClass: 'PURE_READ_ONLY', defaultTimeoutMs: 30000, run: schemaCheckAdapterRun }),
803
+ adapter({ checkId: 'provider-health', category: 'health', defaultRequired: true, sideEffectClass: 'PURE_READ_ONLY', defaultTimeoutMs: 10000, run: providerHealthAdapterRun }),
804
+ adapter({ checkId: 'engine-config', category: 'config', defaultRequired: true, sideEffectClass: 'PURE_READ_ONLY', defaultTimeoutMs: 10000, run: engineConfigAdapterRun }),
805
+ adapter({ checkId: 'budget-config', category: 'config', defaultRequired: true, sideEffectClass: 'PURE_READ_ONLY', defaultTimeoutMs: 10000, run: budgetConfigAdapterRun }),
806
+ adapter({ checkId: 'safety-config', category: 'config', defaultRequired: true, sideEffectClass: 'PURE_READ_ONLY', defaultTimeoutMs: 10000, run: safetyConfigAdapterRun }),
807
+ adapter({ checkId: 'credential-audit', category: 'credential', defaultRequired: true, sideEffectClass: 'PURE_READ_ONLY', defaultTimeoutMs: 60000, run: credentialAuditAdapterRun }),
808
+ adapter({ checkId: 'lock-observation', category: 'lock', defaultRequired: true, sideEffectClass: 'PURE_READ_ONLY', defaultTimeoutMs: 30000, run: lockObservationAdapterRun }),
809
+ adapter({ checkId: 'fallback-chain-state', category: 'fallback', defaultRequired: true, sideEffectClass: 'PURE_READ_ONLY', defaultTimeoutMs: 30000, run: fallbackChainAdapterRun }),
810
+ adapter({ checkId: 'temp-files', category: 'temp', defaultRequired: true, sideEffectClass: 'PURE_READ_ONLY', defaultTimeoutMs: 30000, run: tempFilesAdapterRun }),
811
+ adapter({ checkId: 'process-observation', category: 'process', defaultRequired: false, sideEffectClass: 'PURE_READ_ONLY', defaultTimeoutMs: 30000, run: processObservationAdapterRun }),
812
+ adapter({ checkId: 'readiness', category: 'release', defaultRequired: false, sideEffectClass: 'PROJECT_CONTROL_PLANE_WRITE', defaultTimeoutMs: 300000, canRunByDefault: false, run: readinessAdapterRun }),
813
+ adapter({ checkId: 'doctor', category: 'release', defaultRequired: false, sideEffectClass: 'PROJECT_CONTROL_PLANE_WRITE', defaultTimeoutMs: 120000, canRunByDefault: false, run: doctorAdapterRun }),
814
+ adapter({ checkId: 'canary', category: 'canary', defaultRequired: false, sideEffectClass: 'REAL_ENGINE_EXECUTION', mayUseRealEngine: true, defaultTimeoutMs: 180000, canRunByDefault: false, run: canaryAdapterRun }),
815
+ adapter({ checkId: 'release-check', category: 'release', defaultRequired: false, sideEffectClass: 'NOT_ALLOWED_IN_RC_CHECK', defaultTimeoutMs: 0, canRunByDefault: false, run: async () => result({ checkId: 'release-check', category: 'release', status: 'SKIPPED', severity: 'INFO', executed: false, skipped: true, skipReason: 'NOT_ALLOWED_IN_RC_CHECK', sideEffectClass: 'NOT_ALLOWED_IN_RC_CHECK', summary: 'release-check is not allowed inside rc-check' }) })
816
+ ];
817
+ }