@besales/ops-framework 0.1.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 (70) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +328 -0
  3. package/bin/build-check-context.mjs +67 -0
  4. package/bin/build-execution-ledger.mjs +54 -0
  5. package/bin/estimate-llm-input.mjs +160 -0
  6. package/bin/guard-task.mjs +384 -0
  7. package/bin/hash-task-artifacts.mjs +44 -0
  8. package/bin/init-project.mjs +49 -0
  9. package/bin/intake-execution-feedback.mjs +207 -0
  10. package/bin/intake-feedback.test.mjs +73 -0
  11. package/bin/learning-loop.mjs +658 -0
  12. package/bin/learning-loop.test.mjs +175 -0
  13. package/bin/lib/bootstrap-utils.mjs +542 -0
  14. package/bin/lib/bootstrap-utils.test.mjs +156 -0
  15. package/bin/lib/check-context-utils.mjs +1448 -0
  16. package/bin/lib/check-context-utils.test.mjs +497 -0
  17. package/bin/lib/execution-ledger-utils.mjs +162 -0
  18. package/bin/lib/execution-ledger-utils.test.mjs +74 -0
  19. package/bin/lib/llm-input-pack-utils.mjs +663 -0
  20. package/bin/lib/llm-input-pack-utils.test.mjs +262 -0
  21. package/bin/lib/project-config.mjs +229 -0
  22. package/bin/lib/project-config.test.mjs +102 -0
  23. package/bin/lib/task-manifest-utils.mjs +512 -0
  24. package/bin/lib/task-manifest-utils.test.mjs +218 -0
  25. package/bin/lib/task-metrics-utils.mjs +63 -0
  26. package/bin/lib/task-metrics-utils.test.mjs +40 -0
  27. package/bin/lib/test-setup.mjs +37 -0
  28. package/bin/new-task.mjs +42 -0
  29. package/bin/ops-agent.mjs +81 -0
  30. package/bin/preflight.mjs +56 -0
  31. package/bin/providers/external-cli-checker.mjs +190 -0
  32. package/bin/providers/openai-checker.mjs +62 -0
  33. package/bin/quality-gates.mjs +92 -0
  34. package/bin/run-check.mjs +559 -0
  35. package/bin/run-plan-check-loop.mjs +392 -0
  36. package/bin/run-verify.mjs +627 -0
  37. package/bin/self-lint.mjs +88 -0
  38. package/bin/supervisor-turn.mjs +146 -0
  39. package/bin/supervisor-turn.test.mjs +72 -0
  40. package/bin/task-manifest.mjs +57 -0
  41. package/bin/task-metrics.mjs +48 -0
  42. package/bin/transition.mjs +94 -0
  43. package/bin/validate-check-artifacts.mjs +418 -0
  44. package/config/default-agents.json +100 -0
  45. package/package.json +28 -0
  46. package/playbooks/checker-context.md +9 -0
  47. package/playbooks/complexity-performance.md +13 -0
  48. package/playbooks/production-rollout.md +9 -0
  49. package/playbooks/source-sync-provider.md +9 -0
  50. package/playbooks/ui-acceptance.md +9 -0
  51. package/prompts/checker.md +170 -0
  52. package/prompts/executor.md +54 -0
  53. package/prompts/planner.md +128 -0
  54. package/prompts/researcher.md +44 -0
  55. package/prompts/supervisor.md +337 -0
  56. package/prompts/verifier.md +128 -0
  57. package/templates/brief.md +15 -0
  58. package/templates/check-resolution.md +69 -0
  59. package/templates/check-result.json +32 -0
  60. package/templates/check.md +46 -0
  61. package/templates/execution-feedback.md +25 -0
  62. package/templates/execution.md +101 -0
  63. package/templates/human-gate-summary.md +49 -0
  64. package/templates/orchestration-log.md +8 -0
  65. package/templates/plan.md +86 -0
  66. package/templates/research.md +13 -0
  67. package/templates/retrospective.md +48 -0
  68. package/templates/status.md +53 -0
  69. package/templates/verify-result.json +19 -0
  70. package/templates/verify.md +41 -0
@@ -0,0 +1,559 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import {
5
+ appendOrchestrationLog,
6
+ buildCheckerContextPack,
7
+ buildCheckContext,
8
+ buildCheckerFailureResult,
9
+ buildEvidenceMarkdown,
10
+ computePromptSha,
11
+ computeTaskContextInputs,
12
+ getFlag,
13
+ isCheckContextCurrent,
14
+ normalizeCheckResult,
15
+ parseCliArgs,
16
+ pipelineRoot,
17
+ readAgentsConfig,
18
+ readCheckContext,
19
+ readMemorySnapshot,
20
+ readPrompt,
21
+ readTaskFile,
22
+ repoRoot,
23
+ cacheRoot,
24
+ resolveConfigValue,
25
+ resolveTaskDir,
26
+ sha256Json,
27
+ writeTaskFile,
28
+ } from './lib/check-context-utils.mjs';
29
+ import {
30
+ resolveExternalCliProvider,
31
+ runExternalCliChecker,
32
+ } from './providers/external-cli-checker.mjs';
33
+ import { runOpenAiChecker } from './providers/openai-checker.mjs';
34
+ import {
35
+ buildCheckerLlmInputPack,
36
+ buildContextModeSequence,
37
+ isContextInsufficientResult,
38
+ resolveLlmContextMode,
39
+ summarizePackForConsole,
40
+ } from './lib/llm-input-pack-utils.mjs';
41
+ import { recordLlmInputUsage } from './lib/task-manifest-utils.mjs';
42
+
43
+ function main() {
44
+ runMain().catch((error) => {
45
+ console.error(`Error: ${error.message}`);
46
+ process.exit(1);
47
+ });
48
+ }
49
+
50
+ async function runMain() {
51
+ const args = parseCliArgs(process.argv.slice(2));
52
+ const taskArg = args.positional[0];
53
+ if (!taskArg) {
54
+ throw new Error('Usage: node ops/agent-pipeline/bin/run-check.mjs <TASK-id-or-task-path> [--checker-provider openai] [--checker-model model] [--dry-run]');
55
+ }
56
+
57
+ const taskDir = resolveTaskDir(taskArg);
58
+ const taskId = path.basename(taskDir);
59
+ const dryRun = getFlag(args, 'dry-run', false) === true;
60
+ const noCache = getFlag(args, 'no-cache', false) === true;
61
+ const checkerConfig = resolveCheckerConfig(args);
62
+
63
+ const checkContext = ensureFreshCheckContext(taskDir, taskId);
64
+ const initialContextMode = resolveLlmContextMode({
65
+ requestedMode: getFlag(args, 'context-mode') || getFlag(args, 'llm-context-mode'),
66
+ riskTriggers: checkContext.riskTriggers,
67
+ });
68
+ const checkerPrompt = readPrompt('checker.md');
69
+ const checkerPromptSha = computePromptSha('checker.md');
70
+
71
+ if (dryRun) {
72
+ const cacheKey = buildCheckerCacheKey({
73
+ taskId,
74
+ checkContext,
75
+ checkerConfig,
76
+ checkerPromptSha,
77
+ contextMode: initialContextMode,
78
+ });
79
+ const promptPayload = buildCheckerPromptPayload({
80
+ taskDir,
81
+ taskId,
82
+ checkerPrompt,
83
+ checkerPromptSha,
84
+ checkContext,
85
+ cacheKey,
86
+ contextMode: initialContextMode,
87
+ });
88
+ console.log(`Checker dry-run ok for ${taskId}`);
89
+ console.log(`- provider: ${checkerConfig.provider}`);
90
+ console.log(`- model: ${checkerConfig.model || '(missing)'}`);
91
+ console.log(`- reasoningEffort: ${checkerConfig.reasoningEffort}`);
92
+ console.log(`- providerConfig: ${describeProviderConfig(checkerConfig)}`);
93
+ console.log(`- cacheKeySha: ${sha256Json(cacheKey)}`);
94
+ console.log(`- promptMessages: ${promptPayload.messages.length}`);
95
+ for (const line of summarizePackForConsole(promptPayload.pack)) {
96
+ console.log(line);
97
+ }
98
+ return;
99
+ }
100
+
101
+ let providerOutput = null;
102
+ let cacheKey = null;
103
+ let cacheKeySha = null;
104
+ let promptPayload = null;
105
+ let rerunCount = 0;
106
+ const llmInputAttempts = [];
107
+
108
+ for (const contextMode of buildContextModeSequence(initialContextMode)) {
109
+ cacheKey = buildCheckerCacheKey({
110
+ taskId,
111
+ checkContext,
112
+ checkerConfig,
113
+ checkerPromptSha,
114
+ contextMode,
115
+ });
116
+ cacheKeySha = sha256Json(cacheKey);
117
+ promptPayload = buildCheckerPromptPayload({
118
+ taskDir,
119
+ taskId,
120
+ checkerPrompt,
121
+ checkerPromptSha,
122
+ checkContext,
123
+ cacheKey,
124
+ contextMode,
125
+ });
126
+
127
+ console.log(`Checker LLM input for ${taskId}`);
128
+ for (const line of summarizePackForConsole(promptPayload.pack)) {
129
+ console.log(line);
130
+ }
131
+ if (promptPayload.pack.meta.overCap && contextMode !== 'strict') {
132
+ llmInputAttempts.push(buildAttemptRecord(promptPayload.pack.meta, 'skipped_over_cap'));
133
+ appendOrchestrationLog(taskDir, `checker LLM input exceeded ${contextMode} cap; rerunning pack builder with expanded context`);
134
+ continue;
135
+ }
136
+ if (promptPayload.pack.meta.overCap) {
137
+ llmInputAttempts.push(buildAttemptRecord(promptPayload.pack.meta, 'over_cap_blocked'));
138
+ writeFailureArtifacts({
139
+ taskDir,
140
+ taskId,
141
+ checkContext,
142
+ checkerConfig,
143
+ checkerPromptSha,
144
+ cacheKey,
145
+ failureReason: 'context_overflow',
146
+ message: `Strict LLM input pack exceeds cap: estimatedTokens=${promptPayload.pack.meta.estimatedTokens}, capTokens=${promptPayload.pack.meta.capTokens}`,
147
+ rawOutput: null,
148
+ });
149
+ recordLlmInputUsage({
150
+ taskDir,
151
+ stage: 'check',
152
+ packMeta: promptPayload.pack.meta,
153
+ attempts: llmInputAttempts,
154
+ rerunCount,
155
+ });
156
+ runValidator(taskArg);
157
+ console.log(`Checker blocked for ${taskId}: strict context pack over cap`);
158
+ return;
159
+ }
160
+
161
+ if (!noCache && restoreFromCache({ taskDir, taskArg, cacheKeySha })) {
162
+ llmInputAttempts.push(buildAttemptRecord(promptPayload.pack.meta, 'cache_hit'));
163
+ recordLlmInputUsage({
164
+ taskDir,
165
+ stage: 'check',
166
+ packMeta: promptPayload.pack.meta,
167
+ attempts: llmInputAttempts,
168
+ rerunCount,
169
+ });
170
+ console.log(`Checker cache hit for ${taskId}: ${cacheKeySha}`);
171
+ return;
172
+ }
173
+
174
+ try {
175
+ providerOutput = await runProvider({
176
+ checkerConfig,
177
+ messages: promptPayload.messages,
178
+ prompt: promptPayload.prompt,
179
+ });
180
+ } catch (error) {
181
+ const failureReason = error.failureReason || 'unknown';
182
+ writeFailureArtifacts({
183
+ taskDir,
184
+ taskId,
185
+ checkContext,
186
+ checkerConfig,
187
+ checkerPromptSha,
188
+ cacheKey,
189
+ failureReason,
190
+ message: error.message,
191
+ rawOutput: error.rawOutput || null,
192
+ });
193
+ llmInputAttempts.push(buildAttemptRecord(promptPayload.pack.meta, `provider_failed:${failureReason}`));
194
+ recordLlmInputUsage({
195
+ taskDir,
196
+ stage: 'check',
197
+ packMeta: promptPayload.pack.meta,
198
+ attempts: llmInputAttempts,
199
+ rerunCount,
200
+ });
201
+ runValidator(taskArg);
202
+ throw new Error(`Checker provider failed with ${failureReason}: ${error.message}`);
203
+ }
204
+
205
+ if (!isContextInsufficientResult(providerOutput.checkResultJson) || contextMode === 'strict') {
206
+ llmInputAttempts.push(buildAttemptRecord(promptPayload.pack.meta, providerOutput.checkResultJson?.verdict || 'completed'));
207
+ break;
208
+ }
209
+ llmInputAttempts.push(buildAttemptRecord(promptPayload.pack.meta, 'context_insufficient'));
210
+ rerunCount += 1;
211
+ appendOrchestrationLog(taskDir, `checker returned context_insufficient in ${contextMode}; rerunning with expanded context`);
212
+ }
213
+
214
+ writeSuccessArtifacts({
215
+ taskDir,
216
+ taskId,
217
+ checkContext,
218
+ checkerConfig,
219
+ checkerPromptSha,
220
+ cacheKey,
221
+ providerOutput,
222
+ });
223
+ if (!isContextInsufficientResult(providerOutput.checkResultJson)) {
224
+ storeInCache({ taskDir, cacheKeySha });
225
+ }
226
+ recordLlmInputUsage({
227
+ taskDir,
228
+ stage: 'check',
229
+ packMeta: promptPayload.pack.meta,
230
+ attempts: llmInputAttempts,
231
+ rerunCount,
232
+ });
233
+ runValidator(taskArg);
234
+ console.log(`Checker run completed for ${taskId}: ${providerOutput.checkResultJson?.verdict}`);
235
+ console.log(`- finalLlmInputMode: ${promptPayload.pack.meta.mode}`);
236
+ console.log(`- finalEstimatedInputTokens: ${promptPayload.pack.meta.estimatedTokens}`);
237
+ }
238
+
239
+ function buildAttemptRecord(packMeta, outcome) {
240
+ return {
241
+ mode: packMeta.mode,
242
+ estimatedTokens: packMeta.estimatedTokens,
243
+ bytes: packMeta.bytes,
244
+ capTokens: packMeta.capTokens,
245
+ overCap: packMeta.overCap,
246
+ outcome,
247
+ };
248
+ }
249
+
250
+ function buildCheckerCacheKey({
251
+ taskId,
252
+ checkContext,
253
+ checkerConfig,
254
+ checkerPromptSha,
255
+ contextMode,
256
+ }) {
257
+ return {
258
+ taskId,
259
+ planSha: checkContext.planSha,
260
+ memorySha: checkContext.memorySha,
261
+ checkerProvider: checkerConfig.provider,
262
+ checkerModel: checkerConfig.model,
263
+ checkerReasoningEffort: checkerConfig.reasoningEffort,
264
+ checkerPromptSha,
265
+ riskProfile: checkContext.riskProfile,
266
+ contextMode,
267
+ };
268
+ }
269
+
270
+ function ensureFreshCheckContext(taskDir, taskId) {
271
+ const currentState = isCheckContextCurrent(taskDir);
272
+ if (currentState.current) {
273
+ return currentState.actual;
274
+ }
275
+
276
+ const inputs = computeTaskContextInputs(taskDir);
277
+ const checkContext = buildCheckContext({ taskId, inputs });
278
+ const evidence = buildEvidenceMarkdown({
279
+ taskId,
280
+ risk: inputs.risk,
281
+ qualityGates: inputs.qualityGates,
282
+ referencedFiles: inputs.referencedFiles,
283
+ structuralLines: inputs.structuralLines,
284
+ planFingerprint: inputs.planFingerprint,
285
+ memorySha: inputs.memorySha,
286
+ taskArtifacts: inputs.taskArtifacts,
287
+ });
288
+ const checkerContextPack = buildCheckerContextPack({
289
+ taskId,
290
+ risk: inputs.risk,
291
+ qualityGates: inputs.qualityGates,
292
+ referencedFiles: inputs.referencedFiles,
293
+ structuralLines: inputs.structuralLines,
294
+ taskArtifacts: inputs.taskArtifacts,
295
+ });
296
+
297
+ writeTaskFile(taskDir, 'check-context.json', JSON.stringify(checkContext, null, 2));
298
+ writeTaskFile(taskDir, 'check-evidence.md', evidence);
299
+ writeTaskFile(taskDir, checkContext.checkerContextPackFile, checkerContextPack);
300
+ return readCheckContext(taskDir);
301
+ }
302
+
303
+ function resolveCheckerConfig(args) {
304
+ const config = readAgentsConfig();
305
+ const configuredProvider = resolveConfigValue(config.checker?.provider);
306
+ const configuredModel = resolveConfigValue(config.checker?.model);
307
+ const configuredReasoningEffort = resolveConfigValue(config.checker?.reasoningEffort);
308
+ const provider = getFlag(args, 'checker-provider')
309
+ || process.env.CHECKER_PROVIDER
310
+ || configuredProvider
311
+ || 'codex-cli';
312
+ const providerDefaults = config.checkerProviders?.[provider] || {};
313
+ const providerDefaultModel = resolveConfigValue(providerDefaults.model);
314
+ const providerDefaultReasoningEffort = resolveConfigValue(providerDefaults.reasoningEffort);
315
+ const model = getFlag(args, 'checker-model')
316
+ || process.env.CHECKER_MODEL
317
+ || providerDefaultModel
318
+ || configuredModel
319
+ || (provider === 'cloud-cli' || provider === 'claude-cli' ? 'claude-opus-4-7' : provider === 'openai' ? 'gpt-4.1-mini' : 'gpt-5.5');
320
+ const reasoningEffort = getFlag(args, 'checker-reasoning-effort')
321
+ || process.env.CHECKER_REASONING_EFFORT
322
+ || providerDefaultReasoningEffort
323
+ || configuredReasoningEffort
324
+ || 'medium';
325
+
326
+ return {
327
+ provider,
328
+ model,
329
+ reasoningEffort,
330
+ readOnly: config.checker?.readOnly !== false,
331
+ isolatedContext: config.checker?.isolatedContext !== false,
332
+ providerConfig: resolveExternalCliProvider(provider, config),
333
+ };
334
+ }
335
+
336
+ function buildCheckerPromptPayload({
337
+ taskDir,
338
+ taskId,
339
+ checkerPrompt,
340
+ checkerPromptSha,
341
+ checkContext,
342
+ cacheKey,
343
+ contextMode,
344
+ }) {
345
+ const memorySnapshot = readMemorySnapshot(checkContext.memoryFiles);
346
+ const evidence = readTaskFile(taskDir, checkContext.evidenceFile);
347
+ const checkerContextPack = readTaskFile(taskDir, checkContext.checkerContextPackFile || 'checker-context-pack.md');
348
+ const taskManifest = readTaskFile(taskDir, 'task-manifest.json');
349
+ const pack = buildCheckerLlmInputPack({
350
+ taskDir,
351
+ taskId,
352
+ checkerPromptSha,
353
+ cacheKey,
354
+ checkContext,
355
+ checkEvidence: evidence,
356
+ checkerContextPack,
357
+ taskManifest,
358
+ projectMemory: memorySnapshot.map((item) => ({
359
+ path: item.path,
360
+ sha: item.sha,
361
+ content: item.content,
362
+ })),
363
+ mode: contextMode,
364
+ });
365
+
366
+ const system = [
367
+ checkerPrompt,
368
+ '',
369
+ 'Fresh-context enforcement:',
370
+ '- You are an independent Checker invocation.',
371
+ '- You do not have access to conversation history or Planner reasoning.',
372
+ '- Treat research.md and plan.md as claims to review, not as truth.',
373
+ '- Do not write or request source mutations. Return review artifacts only.',
374
+ '',
375
+ 'Return one JSON object with exactly:',
376
+ '- checkMarkdown: markdown content for check.md',
377
+ '- checkResultJson: object matching check.result.json contract',
378
+ ].join('\n');
379
+
380
+ const user = JSON.stringify(pack.input, null, 2);
381
+
382
+ return {
383
+ messages: [
384
+ { role: 'system', content: system },
385
+ { role: 'user', content: user },
386
+ ],
387
+ prompt: `${system}\n\n<checker_input_json>\n${user}\n</checker_input_json>\n`,
388
+ pack,
389
+ };
390
+ }
391
+
392
+ async function runProvider({ checkerConfig, messages, prompt }) {
393
+ if (checkerConfig.provider === 'openai') {
394
+ return runOpenAiChecker({
395
+ apiKey: process.env.OPENAI_API_KEY,
396
+ model: checkerConfig.model,
397
+ messages,
398
+ });
399
+ }
400
+
401
+ if (checkerConfig.providerConfig) {
402
+ return runExternalCliChecker({
403
+ providerName: checkerConfig.provider,
404
+ providerConfig: checkerConfig.providerConfig,
405
+ model: checkerConfig.model,
406
+ reasoningEffort: checkerConfig.reasoningEffort,
407
+ prompt,
408
+ cwd: repoRoot,
409
+ });
410
+ }
411
+
412
+ const error = new Error(`Unsupported checker provider: ${checkerConfig.provider}`);
413
+ error.failureReason = 'provider_unavailable';
414
+ throw error;
415
+ }
416
+
417
+ function writeSuccessArtifacts({
418
+ taskDir,
419
+ taskId,
420
+ checkContext,
421
+ checkerConfig,
422
+ checkerPromptSha,
423
+ cacheKey,
424
+ providerOutput,
425
+ }) {
426
+ const checkMarkdown = typeof providerOutput.checkMarkdown === 'string'
427
+ ? providerOutput.checkMarkdown
428
+ : '# Check\n\nChecker did not provide markdown.';
429
+ const normalizedResult = normalizeCheckResult(providerOutput.checkResultJson || {}, {
430
+ taskId,
431
+ checkContext,
432
+ checkerProvider: checkerConfig.provider,
433
+ checkerModel: checkerConfig.model,
434
+ });
435
+
436
+ writeTaskFile(taskDir, 'check.md', appendRunnerMetadata(checkMarkdown, checkerPromptSha, cacheKey));
437
+ writeTaskFile(taskDir, 'check.result.json', JSON.stringify(normalizedResult, null, 2));
438
+ if (providerOutput.rawOutput) {
439
+ writeTaskFile(taskDir, 'checker-output.raw.md', providerOutput.rawOutput);
440
+ }
441
+ }
442
+
443
+ function writeFailureArtifacts({
444
+ taskDir,
445
+ taskId,
446
+ checkContext,
447
+ checkerConfig,
448
+ checkerPromptSha,
449
+ cacheKey,
450
+ failureReason,
451
+ message,
452
+ rawOutput,
453
+ }) {
454
+ const result = buildCheckerFailureResult({
455
+ taskId,
456
+ checkContext,
457
+ checkerProvider: checkerConfig.provider,
458
+ checkerModel: checkerConfig.model,
459
+ failureReason,
460
+ });
461
+ const markdown = [
462
+ '# Check',
463
+ '',
464
+ '## Итоговая оценка',
465
+ '',
466
+ '`checker_failed`',
467
+ '',
468
+ '## Failure',
469
+ '',
470
+ `- Reason: \`${failureReason}\``,
471
+ `- Message: ${message}`,
472
+ '',
473
+ ].join('\n');
474
+
475
+ writeTaskFile(taskDir, 'check.md', appendRunnerMetadata(markdown, checkerPromptSha, cacheKey));
476
+ writeTaskFile(taskDir, 'check.result.json', JSON.stringify(result, null, 2));
477
+ if (rawOutput) {
478
+ writeTaskFile(taskDir, 'checker-output.raw.md', rawOutput);
479
+ }
480
+ }
481
+
482
+ function appendRunnerMetadata(markdown, checkerPromptSha, cacheKey) {
483
+ return `${markdown.trimEnd()}\n\n## Runner Metadata\n\n\`\`\`json\n${JSON.stringify({
484
+ checkerPromptSha,
485
+ cacheKey,
486
+ cacheKeySha: sha256Json(cacheKey),
487
+ }, null, 2)}\n\`\`\`\n`;
488
+ }
489
+
490
+ function runValidator(taskArg) {
491
+ const validatorPath = path.join(pipelineRoot, 'bin', 'validate-check-artifacts.mjs');
492
+ const result = spawnSync(process.execPath, [validatorPath, taskArg, '--skip-resolution'], {
493
+ cwd: repoRoot,
494
+ encoding: 'utf8',
495
+ });
496
+ if (result.stdout) {
497
+ process.stdout.write(result.stdout);
498
+ }
499
+ if (result.stderr) {
500
+ process.stderr.write(result.stderr);
501
+ }
502
+ if (result.status !== 0) {
503
+ throw new Error(`Checker artifacts failed validation with exit code ${result.status}.`);
504
+ }
505
+ }
506
+
507
+ function restoreFromCache({ taskDir, taskArg, cacheKeySha }) {
508
+ const cacheDir = path.join(cacheRoot, 'check-results', sanitizeCacheKey(cacheKeySha));
509
+ const cachedCheck = path.join(cacheDir, 'check.md');
510
+ const cachedResult = path.join(cacheDir, 'check.result.json');
511
+ if (!fs.existsSync(cachedCheck) || !fs.existsSync(cachedResult)) {
512
+ return false;
513
+ }
514
+
515
+ let cachedResultJson;
516
+ try {
517
+ cachedResultJson = JSON.parse(fs.readFileSync(cachedResult, 'utf8'));
518
+ } catch {
519
+ return false;
520
+ }
521
+ if (isContextInsufficientResult(cachedResultJson)) {
522
+ return false;
523
+ }
524
+ fs.copyFileSync(cachedCheck, path.join(taskDir, 'check.md'));
525
+ fs.copyFileSync(cachedResult, path.join(taskDir, 'check.result.json'));
526
+ const cachedRaw = path.join(cacheDir, 'checker-output.raw.md');
527
+ if (fs.existsSync(cachedRaw)) {
528
+ fs.copyFileSync(cachedRaw, path.join(taskDir, 'checker-output.raw.md'));
529
+ }
530
+ runValidator(taskArg);
531
+ return true;
532
+ }
533
+
534
+ function storeInCache({ taskDir, cacheKeySha }) {
535
+ const cacheDir = path.join(cacheRoot, 'check-results', sanitizeCacheKey(cacheKeySha));
536
+ fs.mkdirSync(cacheDir, { recursive: true });
537
+ for (const fileName of ['check.md', 'check.result.json', 'checker-output.raw.md']) {
538
+ const source = path.join(taskDir, fileName);
539
+ if (fs.existsSync(source)) {
540
+ fs.copyFileSync(source, path.join(cacheDir, fileName));
541
+ }
542
+ }
543
+ }
544
+
545
+ function sanitizeCacheKey(cacheKeySha) {
546
+ return cacheKeySha.replace(/^sha256:/, '');
547
+ }
548
+
549
+ function describeProviderConfig(checkerConfig) {
550
+ if (checkerConfig.provider === 'openai') {
551
+ return process.env.OPENAI_API_KEY ? 'ok' : 'missing OPENAI_API_KEY';
552
+ }
553
+ if (checkerConfig.providerConfig) {
554
+ return `external CLI command=${checkerConfig.providerConfig.command}`;
555
+ }
556
+ return 'missing provider preset';
557
+ }
558
+
559
+ main();