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,544 @@
1
+ import crypto from 'crypto';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+
5
+ import { redactCredentials, redactObject, truncatePreview } from '../format.mjs';
6
+ import { loadRcPolicy, hashPolicyInfo } from './rc-policy.mjs';
7
+ import { createRcCheckAdapters } from './rc-check-adapters.mjs';
8
+
9
+ export const RC_CHECK_SCHEMA_VERSION = 1;
10
+
11
+ export const RC_CLASSIFICATIONS = Object.freeze({
12
+ READY: 'RC_READY',
13
+ READY_WITH_INFRA_WARNING: 'RC_READY_WITH_INFRA_WARNING',
14
+ NEEDS_HUMAN: 'RC_NEEDS_HUMAN',
15
+ BLOCKED: 'RC_BLOCKED'
16
+ });
17
+
18
+ export const RC_EXIT_CODES = Object.freeze({
19
+ RC_READY: 0,
20
+ RC_READY_WITH_INFRA_WARNING: 0,
21
+ RC_BLOCKED: 1,
22
+ RC_NEEDS_HUMAN: 2
23
+ });
24
+
25
+ export const USAGE_ERROR_EXIT_CODE = 64;
26
+ export const INTERNAL_ERROR_EXIT_CODE = 70;
27
+
28
+ export const CHECK_STATUSES = Object.freeze(['PASS', 'FAIL', 'WARNING', 'NEEDS_HUMAN', 'SKIPPED', 'TIMEOUT']);
29
+ export const CHECK_SEVERITIES = Object.freeze(['INFO', 'INFRA_WARNING', 'HUMAN', 'BLOCKER']);
30
+ export const CHECK_CATEGORIES = Object.freeze([
31
+ 'code',
32
+ 'git',
33
+ 'schema',
34
+ 'health',
35
+ 'provider',
36
+ 'circuit',
37
+ 'fallback',
38
+ 'lock',
39
+ 'process',
40
+ 'temp',
41
+ 'credential',
42
+ 'canary',
43
+ 'report',
44
+ 'config',
45
+ 'test',
46
+ 'release'
47
+ ]);
48
+ export const SIDE_EFFECT_CLASSES = Object.freeze([
49
+ 'PURE_READ_ONLY',
50
+ 'READ_ONLY_WITH_DIAGNOSTIC_OUTPUT',
51
+ 'TEMPORARY_SANDBOX_WRITE',
52
+ 'PROJECT_CONTROL_PLANE_WRITE',
53
+ 'NETWORK_OBSERVATION',
54
+ 'REAL_ENGINE_EXECUTION',
55
+ 'NOT_ALLOWED_IN_RC_CHECK'
56
+ ]);
57
+
58
+ export const DEFAULT_CHECK_TIMEOUTS_MS = Object.freeze({
59
+ policy: 5000,
60
+ 'git-state': 10000,
61
+ 'engine-config': 10000,
62
+ 'budget-config': 10000,
63
+ 'safety-config': 10000,
64
+ 'schema-check': 30000,
65
+ 'provider-health': 10000,
66
+ 'credential-audit': 60000,
67
+ 'temp-files': 30000,
68
+ 'lock-observation': 30000,
69
+ 'process-observation': 30000,
70
+ 'fallback-chain-state': 30000,
71
+ 'npm-run-check': 120000,
72
+ 'npm-test': 600000,
73
+ readiness: 300000,
74
+ doctor: 120000,
75
+ canary: 180000
76
+ });
77
+
78
+ function durationSince(startedAt) {
79
+ return Math.max(0, Date.now() - startedAt);
80
+ }
81
+
82
+ function safeEvidence(evidence) {
83
+ if (!Array.isArray(evidence)) return [];
84
+ return evidence.map(item => redactObject({
85
+ kind: typeof item.kind === 'string' ? item.kind : 'note',
86
+ path: typeof item.path === 'string' ? item.path : null,
87
+ line: Number.isInteger(item.line) ? item.line : null,
88
+ identifier: typeof item.identifier === 'string' ? item.identifier : null,
89
+ redacted: item.redacted !== false,
90
+ details: typeof item.details === 'string' ? item.details : ''
91
+ }));
92
+ }
93
+
94
+ export function makeCheckResult(input = {}) {
95
+ return redactObject({
96
+ schemaVersion: RC_CHECK_SCHEMA_VERSION,
97
+ checkId: String(input.checkId || 'unknown'),
98
+ category: input.category || 'code',
99
+ status: input.status || 'NEEDS_HUMAN',
100
+ severity: input.severity || severityForStatus(input.status || 'NEEDS_HUMAN', input.required !== false),
101
+ required: input.required !== false,
102
+ executed: input.executed !== false,
103
+ skipped: input.skipped === true,
104
+ skipReason: input.skipReason == null ? null : String(input.skipReason),
105
+ durationMs: Number.isFinite(input.durationMs) ? Math.max(0, Math.round(input.durationMs)) : 0,
106
+ timeoutMs: Number.isFinite(input.timeoutMs) ? Math.max(0, Math.round(input.timeoutMs)) : 0,
107
+ exitCode: input.exitCode == null ? null : input.exitCode,
108
+ summary: redactCredentials(String(input.summary || '')),
109
+ evidence: safeEvidence(input.evidence),
110
+ sideEffectClass: input.sideEffectClass || 'PURE_READ_ONLY',
111
+ remediation: input.remediation == null ? null : redactCredentials(String(input.remediation))
112
+ });
113
+ }
114
+
115
+ export function severityForStatus(status, required = true) {
116
+ if (status === 'FAIL' || (status === 'TIMEOUT' && required)) return 'BLOCKER';
117
+ if (status === 'NEEDS_HUMAN') return 'HUMAN';
118
+ if (status === 'WARNING' || status === 'TIMEOUT') return 'INFRA_WARNING';
119
+ return 'INFO';
120
+ }
121
+
122
+ export function validateCheckResult(value) {
123
+ const errors = [];
124
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
125
+ return { ok: false, errors: ['result must be an object'] };
126
+ }
127
+ if (value.schemaVersion !== RC_CHECK_SCHEMA_VERSION) errors.push('schemaVersion must be 1');
128
+ if (typeof value.checkId !== 'string' || value.checkId.length === 0) errors.push('checkId must be a non-empty string');
129
+ if (!CHECK_CATEGORIES.includes(value.category)) errors.push('category is invalid');
130
+ if (!CHECK_STATUSES.includes(value.status)) errors.push('status is invalid');
131
+ if (!CHECK_SEVERITIES.includes(value.severity)) errors.push('severity is invalid');
132
+ if (typeof value.required !== 'boolean') errors.push('required must be boolean');
133
+ if (typeof value.executed !== 'boolean') errors.push('executed must be boolean');
134
+ if (typeof value.skipped !== 'boolean') errors.push('skipped must be boolean');
135
+ if (typeof value.durationMs !== 'number') errors.push('durationMs must be number');
136
+ if (typeof value.timeoutMs !== 'number') errors.push('timeoutMs must be number');
137
+ if (typeof value.summary !== 'string') errors.push('summary must be string');
138
+ if (!Array.isArray(value.evidence)) errors.push('evidence must be array');
139
+ if (!SIDE_EFFECT_CLASSES.includes(value.sideEffectClass)) errors.push('sideEffectClass is invalid');
140
+ return { ok: errors.length === 0, errors };
141
+ }
142
+
143
+ export function invalidResult(checkId, errors, input = {}) {
144
+ return makeCheckResult({
145
+ checkId,
146
+ category: input.category || 'code',
147
+ status: 'FAIL',
148
+ severity: 'BLOCKER',
149
+ required: input.required !== false,
150
+ durationMs: input.durationMs || 0,
151
+ timeoutMs: input.timeoutMs || 0,
152
+ summary: 'Invalid check result schema',
153
+ evidence: [{ kind: 'schema', details: errors.join('; ') }],
154
+ sideEffectClass: input.sideEffectClass || 'PURE_READ_ONLY',
155
+ remediation: 'Fix the rc-check adapter contract before release.'
156
+ });
157
+ }
158
+
159
+ function summarizeChecks(checks) {
160
+ const summary = { passed: 0, warnings: 0, needsHuman: 0, blocked: 0, skipped: 0 };
161
+ for (const check of checks) {
162
+ if (check.skipped || check.status === 'SKIPPED') summary.skipped += 1;
163
+ if (check.status === 'PASS') summary.passed += 1;
164
+ if (check.severity === 'INFRA_WARNING' || check.status === 'WARNING') summary.warnings += 1;
165
+ if (check.severity === 'HUMAN' || check.status === 'NEEDS_HUMAN') summary.needsHuman += 1;
166
+ if (check.severity === 'BLOCKER' || check.status === 'FAIL' || (check.status === 'TIMEOUT' && check.required)) summary.blocked += 1;
167
+ }
168
+ return summary;
169
+ }
170
+
171
+ export function classifyReleaseCandidate(input = {}) {
172
+ const policyInfo = input.policy || { valid: true, errors: [], policy: {} };
173
+ const normalizedChecks = [];
174
+ for (const raw of input.checks || []) {
175
+ const result = makeCheckResult(raw);
176
+ const validation = validateCheckResult(result);
177
+ normalizedChecks.push(validation.ok ? result : invalidResult(result.checkId, validation.errors, result));
178
+ }
179
+ if (policyInfo.valid === false) {
180
+ normalizedChecks.unshift(makeCheckResult({
181
+ checkId: 'rc-policy',
182
+ category: 'config',
183
+ status: 'NEEDS_HUMAN',
184
+ severity: 'HUMAN',
185
+ required: true,
186
+ summary: 'RC policy is invalid',
187
+ evidence: (policyInfo.errors || []).map(error => ({ kind: 'policy', details: error })),
188
+ remediation: 'Fix .maestro/rc-policy.json or remove it to use safe defaults.'
189
+ }));
190
+ }
191
+
192
+ let classification = RC_CLASSIFICATIONS.READY;
193
+ if (normalizedChecks.some(check => check.severity === 'BLOCKER' || check.status === 'FAIL' || (check.status === 'TIMEOUT' && check.required))) {
194
+ classification = RC_CLASSIFICATIONS.BLOCKED;
195
+ } else if (normalizedChecks.some(check => check.severity === 'HUMAN' || check.status === 'NEEDS_HUMAN')) {
196
+ classification = RC_CLASSIFICATIONS.NEEDS_HUMAN;
197
+ } else if (normalizedChecks.some(check => check.severity === 'INFRA_WARNING' || check.status === 'WARNING' || (!check.required && check.status === 'TIMEOUT'))) {
198
+ classification = RC_CLASSIFICATIONS.READY_WITH_INFRA_WARNING;
199
+ }
200
+
201
+ const strict = input.strict === true || (policyInfo.policy && policyInfo.policy.strictGitState === true);
202
+ const allowInfraWarning = policyInfo.policy ? policyInfo.policy.allowInfraWarning !== false : true;
203
+ if (strict && classification === RC_CLASSIFICATIONS.READY_WITH_INFRA_WARNING && allowInfraWarning === false) {
204
+ classification = RC_CLASSIFICATIONS.BLOCKED;
205
+ }
206
+
207
+ return {
208
+ classification,
209
+ exitCode: RC_EXIT_CODES[classification],
210
+ summary: summarizeChecks(normalizedChecks),
211
+ checks: normalizedChecks
212
+ };
213
+ }
214
+
215
+ function shouldRunAdapter(adapter, policy, options) {
216
+ const required = (policy.requiredChecks || []).includes(adapter.checkId) || adapter.defaultRequired === true;
217
+ if ((policy.skippedChecks || []).includes(adapter.checkId) && !(policy.requiredChecks || []).includes(adapter.checkId)) {
218
+ return { run: false, required: false, reason: 'skipped by policy' };
219
+ }
220
+ if (adapter.checkId === 'canary') {
221
+ const run = options.includeCanary === true || policy.canaryRequired === true;
222
+ return { run, required: policy.canaryRequired === true, reason: run ? null : 'canary is opt-in' };
223
+ }
224
+ if (adapter.checkId === 'readiness' || adapter.checkId === 'doctor') {
225
+ const run = options.strict === true && (policy.strictExtraChecks || []).includes(adapter.checkId);
226
+ return { run, required: false, reason: run ? null : 'strictExtraChecks did not request ' + adapter.checkId };
227
+ }
228
+ if (adapter.sideEffectClass === 'NOT_ALLOWED_IN_RC_CHECK') {
229
+ return { run: false, required: false, reason: 'NOT_ALLOWED_IN_RC_CHECK' };
230
+ }
231
+ if (adapter.canRunByDefault === false) {
232
+ return { run: false, required, reason: 'adapter is not allowed by default' };
233
+ }
234
+ return { run: true, required, reason: null };
235
+ }
236
+
237
+ async function runAdapterWithTimeout(adapter, context, timeoutMs, required) {
238
+ const startedAt = Date.now();
239
+ let timeout = null;
240
+ try {
241
+ const timeoutPromise = new Promise(resolve => {
242
+ timeout = setTimeout(() => {
243
+ resolve(makeCheckResult({
244
+ checkId: adapter.checkId,
245
+ category: adapter.category,
246
+ status: 'TIMEOUT',
247
+ severity: required ? 'BLOCKER' : 'INFRA_WARNING',
248
+ required,
249
+ durationMs: durationSince(startedAt),
250
+ timeoutMs,
251
+ exitCode: 124,
252
+ summary: adapter.checkId + ' timed out after ' + timeoutMs + 'ms',
253
+ evidence: [{ kind: 'timeout', details: 'Timeout limits only subprocesses started by rc-check when the adapter starts a subprocess; no kill-tree is promised.' }],
254
+ sideEffectClass: adapter.sideEffectClass,
255
+ remediation: required ? 'Run the check manually and investigate the timeout.' : 'Review optional timeout evidence.'
256
+ }));
257
+ }, timeoutMs);
258
+ });
259
+ const result = await Promise.race([
260
+ adapter.run({ ...context, timeoutMs, required, startedAt }),
261
+ timeoutPromise
262
+ ]);
263
+ return makeCheckResult({
264
+ ...result,
265
+ required,
266
+ durationMs: result.durationMs == null ? durationSince(startedAt) : result.durationMs,
267
+ timeoutMs,
268
+ sideEffectClass: result.sideEffectClass || adapter.sideEffectClass
269
+ });
270
+ } catch (error) {
271
+ return makeCheckResult({
272
+ checkId: adapter.checkId,
273
+ category: adapter.category,
274
+ status: required ? 'FAIL' : 'NEEDS_HUMAN',
275
+ severity: required ? 'BLOCKER' : 'HUMAN',
276
+ required,
277
+ durationMs: durationSince(startedAt),
278
+ timeoutMs,
279
+ summary: adapter.checkId + ' failed unexpectedly',
280
+ evidence: [{ kind: 'exception', details: error.message || String(error) }],
281
+ sideEffectClass: adapter.sideEffectClass,
282
+ remediation: 'Investigate the adapter exception.'
283
+ });
284
+ } finally {
285
+ if (timeout) clearTimeout(timeout);
286
+ }
287
+ }
288
+
289
+ function normalizeSideEffects(checks) {
290
+ const seen = new Set();
291
+ const effects = [];
292
+ for (const check of checks) {
293
+ if (check.executed && !check.skipped && !seen.has(check.sideEffectClass)) {
294
+ seen.add(check.sideEffectClass);
295
+ effects.push(check.sideEffectClass);
296
+ }
297
+ }
298
+ return effects;
299
+ }
300
+
301
+ function canarySummary(checks, policy, options) {
302
+ const check = checks.find(item => item.checkId === 'canary');
303
+ return {
304
+ required: policy.canaryRequired === true,
305
+ executed: !!(check && check.executed && !check.skipped),
306
+ reused: false,
307
+ outcome: check && check.executed && !check.skipped ? check.status : null,
308
+ correlatedHead: !!(check && check.evidence.some(evidence => evidence.identifier === 'head-correlated')),
309
+ optIn: options.includeCanary === true
310
+ };
311
+ }
312
+
313
+ export async function runRcCheck(options = {}) {
314
+ const startedAt = Date.now();
315
+ const projectRoot = options.projectRoot || process.cwd();
316
+ const clock = options.clock || (() => new Date());
317
+ const policyInfo = options.policyInfo || await (options.loadPolicy || loadRcPolicy)({ projectRoot });
318
+ const policy = policyInfo.policy;
319
+ const adapters = options.adapters || createRcCheckAdapters(options.dependencies || {});
320
+ const context = {
321
+ projectRoot,
322
+ policy,
323
+ policyInfo,
324
+ strict: options.strict === true,
325
+ includeCanary: options.includeCanary === true,
326
+ head: options.head || null,
327
+ branch: options.branch || null,
328
+ dependencies: options.dependencies || {}
329
+ };
330
+ const checks = [];
331
+ for (const adapter of adapters) {
332
+ const decision = shouldRunAdapter(adapter, policy, options);
333
+ const timeoutMs = policy.timeouts && policy.timeouts[adapter.checkId]
334
+ ? policy.timeouts[adapter.checkId]
335
+ : (adapter.defaultTimeoutMs || DEFAULT_CHECK_TIMEOUTS_MS[adapter.checkId] || 30000);
336
+ if (!decision.run) {
337
+ checks.push(makeCheckResult({
338
+ checkId: adapter.checkId,
339
+ category: adapter.category,
340
+ status: 'SKIPPED',
341
+ severity: 'INFO',
342
+ required: decision.required,
343
+ executed: false,
344
+ skipped: true,
345
+ skipReason: decision.reason,
346
+ timeoutMs,
347
+ summary: decision.reason,
348
+ sideEffectClass: adapter.sideEffectClass
349
+ }));
350
+ continue;
351
+ }
352
+ const check = await runAdapterWithTimeout(adapter, context, timeoutMs, decision.required);
353
+ checks.push(check);
354
+ if (check.checkId === 'git-state') {
355
+ context.head = check.evidence.find(item => item.identifier === 'head')?.details || context.head;
356
+ context.branch = check.evidence.find(item => item.identifier === 'branch')?.details || context.branch;
357
+ }
358
+ }
359
+
360
+ const classified = classifyReleaseCandidate({ checks, policy: policyInfo, strict: options.strict === true });
361
+ const gitCheck = classified.checks.find(check => check.checkId === 'git-state');
362
+ const head = options.head || (gitCheck && gitCheck.evidence.find(item => item.identifier === 'head')?.details) || null;
363
+ const branch = options.branch || (gitCheck && gitCheck.evidence.find(item => item.identifier === 'branch')?.details) || null;
364
+ const result = redactObject({
365
+ schemaVersion: RC_CHECK_SCHEMA_VERSION,
366
+ command: 'rc-check',
367
+ generatedAt: clock().toISOString(),
368
+ projectRoot,
369
+ head,
370
+ branch,
371
+ classification: classified.classification,
372
+ exitCode: classified.exitCode,
373
+ policy: {
374
+ source: policyInfo.source,
375
+ path: policyInfo.path || null,
376
+ valid: policyInfo.valid !== false,
377
+ hash: policyInfo.hash || null,
378
+ defaultsApplied: policyInfo.defaultsApplied === true,
379
+ errors: policyInfo.errors || []
380
+ },
381
+ summary: classified.summary,
382
+ checks: classified.checks,
383
+ sideEffects: normalizeSideEffects(classified.checks),
384
+ canary: canarySummary(classified.checks, policy, options),
385
+ reportPath: null,
386
+ durationMs: durationSince(startedAt)
387
+ });
388
+
389
+ const reportPath = options.reportPath || (policy.reportOutput && policy.reportOutput.enabled ? policy.reportOutput.path : null);
390
+ if (reportPath) {
391
+ const report = await writeMarkdownReport(result, reportPath, { projectRoot });
392
+ result.reportPath = report.path;
393
+ result.report = report;
394
+ }
395
+ return result;
396
+ }
397
+
398
+ function listBySeverity(result, severity) {
399
+ return result.checks
400
+ .filter(check => check.severity === severity || (severity === 'INFRA_WARNING' && check.status === 'WARNING'))
401
+ .map(check => '- ' + check.checkId + ': ' + check.summary + (check.remediation ? ' | remediation: ' + check.remediation : ''));
402
+ }
403
+
404
+ export function renderHumanRcCheck(result) {
405
+ const lines = [];
406
+ lines.push('AI-MAESTRO rc-check');
407
+ lines.push('Classification: ' + result.classification);
408
+ lines.push('Exit code: ' + result.exitCode);
409
+ lines.push('HEAD: ' + (result.head || 'unknown'));
410
+ lines.push('Branch: ' + (result.branch || 'unknown'));
411
+ lines.push('Policy source: ' + result.policy.source + (result.policy.path ? ' (' + result.policy.path + ')' : ''));
412
+ lines.push('');
413
+ lines.push('Checks:');
414
+ for (const check of result.checks) {
415
+ lines.push('- ' + check.checkId + ' [' + check.status + '/' + check.severity + '] ' + check.summary);
416
+ }
417
+ const warnings = listBySeverity(result, 'INFRA_WARNING');
418
+ const blockers = listBySeverity(result, 'BLOCKER');
419
+ const human = listBySeverity(result, 'HUMAN');
420
+ lines.push('');
421
+ lines.push('Summary: passed=' + result.summary.passed + ' warnings=' + result.summary.warnings + ' needsHuman=' + result.summary.needsHuman + ' blocked=' + result.summary.blocked + ' skipped=' + result.summary.skipped);
422
+ lines.push('Warnings:');
423
+ lines.push(...(warnings.length ? warnings : ['- none']));
424
+ lines.push('Blockers:');
425
+ lines.push(...(blockers.length ? blockers : ['- none']));
426
+ lines.push('Human review needed:');
427
+ lines.push(...(human.length ? human : ['- none']));
428
+ lines.push('Canary: required=' + result.canary.required + ' executed=' + result.canary.executed + ' optIn=' + result.canary.optIn);
429
+ lines.push('Report: ' + (result.reportPath || 'not written'));
430
+ return redactCredentials(lines.join('\n') + '\n');
431
+ }
432
+
433
+ export function renderJsonRcCheck(result) {
434
+ return redactCredentials(JSON.stringify(redactObject(result), null, 2) + '\n');
435
+ }
436
+
437
+ function atomicSuffix(targetPath) {
438
+ const digest = crypto.createHash('sha256').update(targetPath + String(Date.now()) + String(process.pid)).digest('hex').slice(0, 12);
439
+ return targetPath + '.tmp-' + process.pid + '-' + digest;
440
+ }
441
+
442
+ export function renderMarkdownReport(result) {
443
+ const policyHash = result.policy.hash || hashPolicyInfo({ policy: result.policy });
444
+ const lines = [];
445
+ lines.push('# AI-MAESTRO RC Check');
446
+ lines.push('');
447
+ lines.push('- Generated at: ' + result.generatedAt);
448
+ lines.push('- HEAD: ' + (result.head || 'unknown'));
449
+ lines.push('- Branch: ' + (result.branch || 'unknown'));
450
+ lines.push('- Policy source: ' + result.policy.source);
451
+ lines.push('- Policy hash: ' + policyHash);
452
+ lines.push('- Classification: ' + result.classification);
453
+ lines.push('- Exit code: ' + result.exitCode);
454
+ lines.push('');
455
+ lines.push('## Checks');
456
+ for (const check of result.checks) {
457
+ lines.push('- ' + check.checkId + ': ' + check.status + '/' + check.severity + ' - ' + check.summary);
458
+ for (const evidence of check.evidence || []) {
459
+ lines.push(' - evidence: ' + [evidence.kind, evidence.path, evidence.line, evidence.identifier, evidence.details].filter(Boolean).join(' | '));
460
+ }
461
+ if (check.remediation) lines.push(' - remediation: ' + check.remediation);
462
+ }
463
+ lines.push('');
464
+ lines.push('## Canary');
465
+ lines.push('- Required: ' + result.canary.required);
466
+ lines.push('- Executed: ' + result.canary.executed);
467
+ lines.push('- Correlated HEAD: ' + result.canary.correlatedHead);
468
+ lines.push('');
469
+ lines.push('## Side Effects');
470
+ for (const item of result.sideEffects || []) lines.push('- ' + item);
471
+ return redactCredentials(lines.join('\n') + '\n');
472
+ }
473
+
474
+ export async function writeMarkdownReport(result, targetPath, options = {}) {
475
+ const projectRoot = options.projectRoot || process.cwd();
476
+ const resolved = path.resolve(projectRoot, targetPath);
477
+ const exists = await fs.access(resolved).then(() => true, () => false);
478
+ if (exists && options.overwrite !== true) {
479
+ const error = new Error('report path already exists: ' + resolved);
480
+ error.code = 'EEXIST';
481
+ throw error;
482
+ }
483
+ const markdown = renderMarkdownReport(result);
484
+ await fs.mkdir(path.dirname(resolved), { recursive: true });
485
+ const tempPath = atomicSuffix(resolved);
486
+ await fs.writeFile(tempPath, markdown, 'utf-8');
487
+ await fs.rename(tempPath, resolved);
488
+ const bytes = Buffer.byteLength(markdown, 'utf8');
489
+ return { path: resolved, bytes, redacted: true, atomic: true };
490
+ }
491
+
492
+ export function parseRcCheckArgs(args = []) {
493
+ const options = { json: false, strict: false, includeCanary: false, reportPath: null };
494
+ for (let index = 0; index < args.length; index += 1) {
495
+ const arg = args[index];
496
+ if (arg === '--json') options.json = true;
497
+ else if (arg === '--strict') options.strict = true;
498
+ else if (arg === '--include-canary') options.includeCanary = true;
499
+ else if (arg === '--report') {
500
+ const next = args[index + 1];
501
+ if (!next || next.startsWith('--')) return { ok: false, exitCode: USAGE_ERROR_EXIT_CODE, error: '--report requires a path' };
502
+ options.reportPath = next;
503
+ index += 1;
504
+ } else {
505
+ return { ok: false, exitCode: USAGE_ERROR_EXIT_CODE, error: 'Unknown rc-check flag: ' + arg };
506
+ }
507
+ }
508
+ return { ok: true, options };
509
+ }
510
+
511
+ export async function rcCheckCommand(args = [], dependencies = {}) {
512
+ const parsed = parseRcCheckArgs(args);
513
+ if (!parsed.ok) {
514
+ console.error(parsed.error);
515
+ process.exitCode = parsed.exitCode;
516
+ return { usageError: true, exitCode: parsed.exitCode };
517
+ }
518
+ try {
519
+ const result = await runRcCheck({ ...parsed.options, dependencies });
520
+ process.stdout.write(parsed.options.json ? renderJsonRcCheck(result) : renderHumanRcCheck(result));
521
+ process.exitCode = result.exitCode;
522
+ return result;
523
+ } catch (error) {
524
+ if (parsed.options.json) {
525
+ process.stdout.write(JSON.stringify({
526
+ schemaVersion: RC_CHECK_SCHEMA_VERSION,
527
+ command: 'rc-check',
528
+ classification: RC_CLASSIFICATIONS.BLOCKED,
529
+ exitCode: INTERNAL_ERROR_EXIT_CODE,
530
+ error: redactCredentials(error.message || String(error))
531
+ }, null, 2) + '\n');
532
+ } else {
533
+ console.error('rc-check internal error: ' + redactCredentials(error.message || String(error)));
534
+ }
535
+ process.exitCode = INTERNAL_ERROR_EXIT_CODE;
536
+ return { internalError: true, exitCode: INTERNAL_ERROR_EXIT_CODE };
537
+ }
538
+ }
539
+
540
+ export function summarizeCommandOutput(result, maxBytes = 4096) {
541
+ const stdout = truncatePreview(redactCredentials(result.stdout || ''), { maxBytes, maxLines: 80 });
542
+ const stderr = truncatePreview(redactCredentials(result.stderr || ''), { maxBytes, maxLines: 80 });
543
+ return { stdout, stderr };
544
+ }