@besales/ops-framework 0.1.18 → 0.1.21

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.
@@ -13,12 +13,18 @@ import {
13
13
  riskRootWarnings,
14
14
  selectRelevantPlaybookNames,
15
15
  inspectComplexityPerformanceBudget,
16
+ inspectExecutionMetadata,
17
+ inspectAuditWriterModel,
18
+ inspectMigrationApplyPlan,
16
19
  inspectOptimizationStrategy,
17
20
  inspectProductionRolloutGate,
18
21
  inspectSourceSyncProviderGate,
22
+ inspectStandardsAlignment,
19
23
  inspectUiAcceptanceScenarios,
24
+ inspectVerificationLadder,
20
25
  parseMarkdownSections,
21
26
  requiresOptimizationStrategy,
27
+ requiresStandardsAlignment,
22
28
  validateExecutionEvidenceForPlan,
23
29
  } from './check-context-utils.mjs';
24
30
 
@@ -49,6 +55,176 @@ describe('agent pipeline quality gates', () => {
49
55
  expect(result.missingSignals).toContain('UI-visible risk detected but `## UI Acceptance Scenarios` is missing or incomplete.');
50
56
  });
51
57
 
58
+ it('blocks external check locally when execution metadata is placeholder-only', () => {
59
+ const plan = [
60
+ '# Plan',
61
+ '',
62
+ '## Risk tier and execution budget',
63
+ '',
64
+ '- Risk tier: `R0 | R1 | R2 | R3 | R4 | R5`',
65
+ '- Speed mode: `Fast | Standard | Strict`',
66
+ '- Approved execution target:',
67
+ '- Fast-loop allowed inside this slice:',
68
+ '- Requires return to Plan/Check if:',
69
+ ].join('\n');
70
+
71
+ const result = analyzePlanQualityGates({
72
+ planContent: plan,
73
+ risk: {
74
+ riskProfile: 'low',
75
+ riskTriggers: ['docs-only'],
76
+ },
77
+ });
78
+
79
+ expect(result.executionMetadata.present).toBe(false);
80
+ expect(result.missingSignals).toContain('Plan must include `## Risk tier and execution budget` with explicit risk tier, speed mode, execution target and budget/stop rule.');
81
+ });
82
+
83
+ it('accepts explicit execution metadata and verification ladder', () => {
84
+ const sections = parseMarkdownSections([
85
+ '# Plan',
86
+ '',
87
+ '## Risk tier and execution budget',
88
+ '',
89
+ '- Risk tier: `R2`',
90
+ '- Speed mode: `Standard`',
91
+ '- Approved execution target: local schema files and API DTOs only.',
92
+ '- Fast-loop allowed inside this slice: yes, until migration scope changes.',
93
+ '- Requires return to Plan/Check if: destructive migration or production data write appears.',
94
+ '',
95
+ '## План проверки',
96
+ '',
97
+ '### Verification ladder',
98
+ '',
99
+ '- Micro-verify during Execute: node --check.',
100
+ '- Slice-verify before completion: tests.',
101
+ '- External Verify required before closeout: no, internal_supervisor is enough.',
102
+ ].join('\n'));
103
+
104
+ expect(inspectExecutionMetadata(sections).present).toBe(true);
105
+ expect(inspectVerificationLadder(sections).present).toBe(true);
106
+ });
107
+
108
+ it('requires standards alignment only when standards references are present', () => {
109
+ expect(requiresStandardsAlignment({
110
+ referencedFiles: ['../.claude/CLAUDE.MD'],
111
+ structuralLines: [],
112
+ })).toBe(true);
113
+ expect(requiresStandardsAlignment({
114
+ referencedFiles: ['docs/01-overview.md'],
115
+ structuralLines: [],
116
+ })).toBe(false);
117
+
118
+ const missing = analyzePlanQualityGates({
119
+ planContent: '# Plan\n',
120
+ risk: {
121
+ riskProfile: 'low',
122
+ riskTriggers: ['docs-only'],
123
+ },
124
+ referencedFiles: ['../.claude/CLAUDE.MD'],
125
+ });
126
+ expect(missing.missingSignals).toContain('Standards file/reference detected but `## Global Standards Alignment` / `## Standards Alignment` is missing or incomplete.');
127
+
128
+ const sections = parseMarkdownSections([
129
+ '# Plan',
130
+ '',
131
+ '## Global Standards Alignment',
132
+ '',
133
+ '- Local interpretation: apply repo naming and validation rules to this task.',
134
+ '- Validation: run lint/test commands from the plan.',
135
+ ].join('\n'));
136
+ expect(inspectStandardsAlignment(sections).present).toBe(true);
137
+ });
138
+
139
+ it('requires a real disposable migration apply path for schema work before external check', () => {
140
+ const result = analyzePlanQualityGates({
141
+ planContent: [
142
+ '# Plan',
143
+ '',
144
+ '## Risk tier and execution budget',
145
+ '',
146
+ '- Risk tier: `R3`',
147
+ '- Speed mode: `Standard`',
148
+ '- Approved execution target: plain SQL migration.',
149
+ '- Requires return to Plan/Check if: database apply cannot run.',
150
+ '',
151
+ '## План проверки',
152
+ '',
153
+ '### Verification ladder',
154
+ '',
155
+ '- Micro-verify during Execute: static SQL review.',
156
+ '- Slice-verify before completion: blocked if Postgres unavailable.',
157
+ '- External Verify required before closeout: no.',
158
+ '',
159
+ '## Production Rollout Gate',
160
+ '',
161
+ '- Impact / blast radius: local schema migration only.',
162
+ '- Environment / deploy variables: none.',
163
+ '- Rollback / disable path: revert migration file.',
164
+ '- Post-deploy evidence: static SQL review if Postgres unavailable.',
165
+ ].join('\n'),
166
+ risk: {
167
+ riskProfile: 'high',
168
+ riskTriggers: ['prisma-schema'],
169
+ },
170
+ });
171
+
172
+ expect(result.migrationApply.required).toBe(true);
173
+ expect(result.migrationApply.present).toBe(false);
174
+ expect(result.missingSignals).toContain('Schema/migration work requires a real apply path on a disposable/scratch database before closeout; static SQL review alone is not enough.');
175
+ });
176
+
177
+ it('accepts migration apply plan with disposable database target', () => {
178
+ const sections = parseMarkdownSections([
179
+ '# Plan',
180
+ '',
181
+ '## Production Rollout Gate',
182
+ '',
183
+ '- Impact / blast radius: local schema migration only.',
184
+ '- Environment / deploy variables: temporary Railway Postgres scratch database.',
185
+ '- Rollback / disable path: drop scratch database and revert migration file.',
186
+ '- Post-deploy evidence: apply plain SQL migration with psql against throwaway Postgres; static SQL review alone is not enough.',
187
+ ].join('\n'));
188
+
189
+ const result = inspectMigrationApplyPlan(sections, ['prisma-schema']);
190
+
191
+ expect(result.present).toBe(true);
192
+ expect(result.hasApplyCommand).toBe(true);
193
+ expect(result.hasDisposableTarget).toBe(true);
194
+ });
195
+
196
+ it('requires audit writer model when entity_history audit is in scope', () => {
197
+ const sections = parseMarkdownSections([
198
+ '# Plan',
199
+ '',
200
+ '## Риски и открытые вопросы',
201
+ '',
202
+ '- Full audit triggers may expand scope; minimum is entity_history plus helper pattern.',
203
+ ].join('\n'));
204
+
205
+ const result = inspectAuditWriterModel(sections, Array.from(sections.values()).join('\n'));
206
+
207
+ expect(result.required).toBe(true);
208
+ expect(result.present).toBe(false);
209
+ expect(result.ambiguous).toBe(true);
210
+ });
211
+
212
+ it('accepts explicit DB trigger audit writer model', () => {
213
+ const sections = parseMarkdownSections([
214
+ '# Plan',
215
+ '',
216
+ '## Шаги реализации',
217
+ '',
218
+ '- Create entity_history.',
219
+ "- Create log_entity_change trigger function using current_setting('app.current_user_id') and attach triggers to core tables for before/after audit.",
220
+ ].join('\n'));
221
+
222
+ const result = inspectAuditWriterModel(sections, Array.from(sections.values()).join('\n'));
223
+
224
+ expect(result.present).toBe(true);
225
+ expect(result.writerModel).toBe('db_trigger');
226
+ });
227
+
52
228
  it('accepts UI acceptance scenarios with intent, steps, expected visible result and must-catch regressions', () => {
53
229
  const sections = parseMarkdownSections([
54
230
  '# Plan',
@@ -199,6 +375,54 @@ describe('agent pipeline quality gates', () => {
199
375
  expect(result.hasApproach).toBe(true);
200
376
  expect(result.hasAntiPatterns).toBe(true);
201
377
  expect(result.hasBudget).toBe(true);
378
+ expect(result.hasMeasurementPath).toBe(true);
379
+ });
380
+
381
+ it('requires a measurement path for O3 optimization strategy before external check', () => {
382
+ const result = analyzePlanQualityGates({
383
+ planContent: [
384
+ '# Plan',
385
+ '',
386
+ '## Risk tier and execution budget',
387
+ '',
388
+ '- Risk tier: `R3`',
389
+ '- Speed mode: `Standard`',
390
+ '- Approved execution target: worker hot path.',
391
+ '- Requires return to Plan/Check if: budget changes.',
392
+ '',
393
+ '## План проверки',
394
+ '',
395
+ '### Verification ladder',
396
+ '',
397
+ '- Micro-verify during Execute: unit test.',
398
+ '- Slice-verify before completion: smoke test.',
399
+ '- External Verify required before closeout: no.',
400
+ '',
401
+ '## Complexity / Performance Budget',
402
+ '',
403
+ '- Hot paths: worker materializer.',
404
+ '- Expected data size / row counts: 150k rows.',
405
+ '- Complexity risks: N+1 queries.',
406
+ '- Budget / stop rule: < 3 seconds.',
407
+ '',
408
+ '## Optimization Strategy',
409
+ '',
410
+ '- Optimization tier: O3 measured review.',
411
+ '- Hot paths: worker materializer.',
412
+ '- Expected data size / row counts: 150k rows.',
413
+ '- Chosen efficient approach: batch query and indexed lookup.',
414
+ '- Anti-patterns avoided: N+1 queries and repeated scans.',
415
+ '- Optimizer budget / stop rule: one focused review; defer speculative ideas.',
416
+ ].join('\n'),
417
+ risk: {
418
+ riskProfile: 'high',
419
+ riskTriggers: ['production-runtime'],
420
+ },
421
+ });
422
+
423
+ expect(result.optimizationTier).toBe('O3');
424
+ expect(result.optimizationStrategy.present).toBe(false);
425
+ expect(result.missingSignals).toContain('Optimization tier O3 detected but `## Optimization Strategy` is missing or incomplete.');
202
426
  });
203
427
 
204
428
  it('builds a checker context pack with exact quality-gate questions', () => {
@@ -431,6 +655,42 @@ describe('agent pipeline quality gates', () => {
431
655
  expect(issues).toEqual([]);
432
656
  });
433
657
 
658
+ it('pre-verify evidence gate blocks migration plans without successful scratch DB apply evidence', () => {
659
+ const plan = [
660
+ '# Plan',
661
+ '',
662
+ '## Production Rollout Gate',
663
+ '',
664
+ '- Impact / blast radius: local schema migration only.',
665
+ '- Environment / deploy variables: temporary Railway Postgres scratch database.',
666
+ '- Rollback / disable path: drop scratch database and revert migration file.',
667
+ '- Post-deploy evidence: apply plain SQL migration with psql against throwaway Postgres; static SQL review alone is not enough.',
668
+ '',
669
+ '## Шаги реализации',
670
+ '',
671
+ '- Create plain SQL migration with CREATE TABLE statements.',
672
+ ].join('\n');
673
+ const execution = [
674
+ '# Execution',
675
+ '',
676
+ '## Production Rollout Evidence',
677
+ '',
678
+ '| Check | Result | Evidence | Notes |',
679
+ '| --- | --- | --- | --- |',
680
+ '| migration | blocked | Postgres unavailable; static SQL review only | n/a |',
681
+ ].join('\n');
682
+
683
+ const issues = validateExecutionEvidenceForPlan({
684
+ planContent: plan,
685
+ executionContent: execution,
686
+ });
687
+
688
+ expect(issues).toContainEqual({
689
+ category: 'insufficient_evidence',
690
+ message: 'Migration Apply Evidence must show a successful apply/migrate/psql run against a disposable/scratch database target.',
691
+ });
692
+ });
693
+
434
694
  it('requires production rollout gate for production runtime triggers', () => {
435
695
  const result = analyzePlanQualityGates({
436
696
  planContent: '# Plan\n\n## Затронутые модули и файлы\n\n- deploy runtime env variable for worker',
@@ -33,6 +33,11 @@ export function buildExecutionLedger({
33
33
  }) {
34
34
  const git = collectGitExecutionState({ repoRoot, taskDir });
35
35
  const taskArtifacts = listTaskArtifacts(taskDir);
36
+ const executionMentionedFiles = readExecutionMentionedFiles(taskDir);
37
+ const changedFiles = git.changedFiles.map((file) => ({
38
+ ...file,
39
+ isExecutionMentioned: executionMentionedFiles.has(file.path),
40
+ }));
36
41
 
37
42
  return {
38
43
  schemaVersion: 1,
@@ -42,8 +47,8 @@ export function buildExecutionLedger({
42
47
  executionSha,
43
48
  git: {
44
49
  taskRelativePath: git.taskRelativePath,
45
- changedFiles: compactLedgerFiles(git.changedFiles),
46
- unrelatedDirtyFiles: compactLedgerFiles(git.changedFiles.filter((file) => !file.isTaskArtifact && !file.isOpsFrameworkFile)),
50
+ changedFiles: compactLedgerFiles(changedFiles),
51
+ unrelatedDirtyFiles: compactLedgerFiles(changedFiles.filter((file) => !file.isTaskArtifact && !file.isOpsFrameworkFile && !file.isExecutionMentioned)),
47
52
  },
48
53
  taskArtifacts,
49
54
  notes: [
@@ -59,6 +64,7 @@ function compactLedgerFiles(files) {
59
64
  status: file.status,
60
65
  isTaskArtifact: file.isTaskArtifact,
61
66
  isOpsFrameworkFile: file.isOpsFrameworkFile,
67
+ isExecutionMentioned: Boolean(file.isExecutionMentioned),
62
68
  }));
63
69
  }
64
70
 
@@ -145,6 +151,20 @@ function listTaskArtifacts(taskDir) {
145
151
  }));
146
152
  }
147
153
 
154
+ function readExecutionMentionedFiles(taskDir) {
155
+ const executionPath = path.join(taskDir, 'execution.md');
156
+ if (!fs.existsSync(executionPath)) {
157
+ return new Set();
158
+ }
159
+ const content = fs.readFileSync(executionPath, 'utf8');
160
+ const refs = new Set();
161
+ const pathPattern = /`([^`\n]+\/[^`\n]+)`/g;
162
+ for (const match of content.matchAll(pathPattern)) {
163
+ refs.add(normalizePath(match[1].trim()));
164
+ }
165
+ return refs;
166
+ }
167
+
148
168
  function runGitLines(repoRoot, args) {
149
169
  const result = spawnSync('git', args, {
150
170
  cwd: repoRoot,
@@ -1,5 +1,10 @@
1
1
  import { describe, expect, it } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
2
6
  import {
7
+ buildExecutionLedger,
3
8
  mergeChangedFiles,
4
9
  parseGitStatusLine,
5
10
  } from './execution-ledger-utils.mjs';
@@ -71,4 +76,59 @@ describe('execution ledger utils', () => {
71
76
  }),
72
77
  ]);
73
78
  });
79
+
80
+ it('does not classify execution-mentioned files as unrelated dirty files', () => {
81
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ops-ledger-repo-'));
82
+ run(repoRoot, ['init']);
83
+ run(repoRoot, ['config', 'user.email', 'test@example.com']);
84
+ run(repoRoot, ['config', 'user.name', 'Test User']);
85
+ fs.mkdirSync(path.join(repoRoot, 'apps', 'api'), { recursive: true });
86
+ fs.mkdirSync(path.join(repoRoot, 'docs'), { recursive: true });
87
+ fs.writeFileSync(path.join(repoRoot, 'apps', 'api', 'package.json'), '{}\n');
88
+ fs.writeFileSync(path.join(repoRoot, 'docs', 'source.md'), 'source\n');
89
+ run(repoRoot, ['add', '.']);
90
+ run(repoRoot, ['commit', '-m', 'initial']);
91
+
92
+ fs.writeFileSync(path.join(repoRoot, 'apps', 'api', 'package.json'), '{"type":"module"}\n');
93
+ fs.writeFileSync(path.join(repoRoot, 'docs', 'source.md'), 'dirty source\n');
94
+
95
+ const taskDir = path.join(repoRoot, 'ops', 'agent-pipeline', 'tasks', 'TASK-999-example');
96
+ fs.mkdirSync(taskDir, { recursive: true });
97
+ fs.writeFileSync(path.join(taskDir, 'execution.md'), [
98
+ '# Execution',
99
+ '',
100
+ '## Измененные файлы',
101
+ '',
102
+ '| File | Change summary | Planned item / reason |',
103
+ '| --- | --- | --- |',
104
+ '| `apps/api/package.json` | package setup | planned |',
105
+ ].join('\n'));
106
+
107
+ const ledger = buildExecutionLedger({
108
+ taskId: 'TASK-999-example',
109
+ taskDir,
110
+ repoRoot,
111
+ planSha: 'sha256:plan',
112
+ executionSha: 'sha256:execution',
113
+ });
114
+
115
+ expect(ledger.git.changedFiles).toContainEqual(expect.objectContaining({
116
+ path: 'apps/api/package.json',
117
+ isExecutionMentioned: true,
118
+ }));
119
+ expect(ledger.git.unrelatedDirtyFiles.map((file) => file.path)).not.toContain('apps/api/package.json');
120
+ expect(ledger.git.unrelatedDirtyFiles.map((file) => file.path)).toContain('docs/source.md');
121
+
122
+ fs.rmSync(repoRoot, { recursive: true, force: true });
123
+ });
74
124
  });
125
+
126
+ function run(cwd, args) {
127
+ const result = spawnSync('git', args, {
128
+ cwd,
129
+ encoding: 'utf8',
130
+ });
131
+ if (result.status !== 0) {
132
+ throw new Error(`git ${args.join(' ')} failed: ${result.stdout}${result.stderr}`);
133
+ }
134
+ }
@@ -254,23 +254,27 @@ export function buildVerifierLlmInputPack({
254
254
  }) {
255
255
  const selectedMode = normalizeLlmContextMode(mode) || 'standard';
256
256
  const taskArtifacts = selectedMode === 'strict'
257
- ? readArtifacts(taskDir, [
258
- 'brief.md',
259
- 'research.md',
260
- 'plan.md',
261
- 'task-manifest.json',
262
- 'check.result.json',
263
- 'check.md',
264
- 'check-resolution.md',
265
- 'human-gate-summary.md',
266
- 'execution.md',
267
- 'execution-ledger.json',
268
- 'verify.md',
269
- 'status.md',
270
- 'feedback.md',
271
- 'execution-feedback.md',
272
- 'orchestration-log.md',
273
- ], 'full')
257
+ ? {
258
+ 'brief.md': readTaskFile(taskDir, 'brief.md'),
259
+ 'research.md': readTaskFile(taskDir, 'research.md'),
260
+ 'plan.md': readTaskFile(taskDir, 'plan.md'),
261
+ 'task-manifest.json': readTaskFile(taskDir, 'task-manifest.json'),
262
+ 'check.result.json': readTaskFile(taskDir, 'check.result.json'),
263
+ 'check.md': compactCheckMarkdown({
264
+ checkMarkdown: readTaskFile(taskDir, 'check.md'),
265
+ checkResult: readOptionalJson(taskDir, 'check.result.json'),
266
+ mode: 'standard',
267
+ }),
268
+ 'check-resolution.md': compactArtifact(taskDir, 'check-resolution.md', 'standard', ['structured resolution', 'root cause', 'resolution']),
269
+ 'human-gate-summary.md': truncateMiddle(readTaskFile(taskDir, 'human-gate-summary.md'), 3500),
270
+ 'execution.md': readTaskFile(taskDir, 'execution.md'),
271
+ 'execution-ledger.json': compactExecutionLedger(readOptionalJson(taskDir, 'execution-ledger.json'), 'strict'),
272
+ 'verify.md': compactVerifierMarkdown(readTaskFile(taskDir, 'verify.md'), readOptionalJson(taskDir, 'verify.result.json'), 'standard'),
273
+ 'status.md': compactStatus(readTaskFile(taskDir, 'status.md')),
274
+ 'feedback.md': compactArtifact(taskDir, 'feedback.md', 'standard', ['feedback event', 'classification', 'supervisor decision']),
275
+ 'execution-feedback.md': compactArtifact(taskDir, 'execution-feedback.md', 'standard', ['feedback event', 'classification', 'supervisor decision']),
276
+ 'orchestration-log.md': compactOrchestrationLog(readTaskFile(taskDir, 'orchestration-log.md'), 'standard'),
277
+ }
274
278
  : {
275
279
  'brief.md': compactArtifact(taskDir, 'brief.md', selectedMode, ['goal', 'scope', 'success criteria']),
276
280
  'research.md': compactArtifact(taskDir, 'research.md', selectedMode, ['findings', 'evidence', 'repo']),
@@ -285,8 +289,8 @@ export function buildVerifierLlmInputPack({
285
289
  'check-resolution.md': truncateMiddle(readTaskFile(taskDir, 'check-resolution.md'), charLimitForMode(selectedMode, 1500, 3500)),
286
290
  'human-gate-summary.md': truncateMiddle(readTaskFile(taskDir, 'human-gate-summary.md'), charLimitForMode(selectedMode, 1200, 2500)),
287
291
  'execution.md': compactArtifact(taskDir, 'execution.md', selectedMode, VERIFY_EXECUTION_SECTIONS),
288
- 'execution-ledger.json': readTaskFile(taskDir, 'execution-ledger.json'),
289
- 'verify.md': compactArtifact(taskDir, 'verify.md', selectedMode, ['verdict', 'findings', 'residual risks', 'recommended next step']),
292
+ 'execution-ledger.json': compactExecutionLedger(readOptionalJson(taskDir, 'execution-ledger.json'), selectedMode),
293
+ 'verify.md': compactVerifierMarkdown(readTaskFile(taskDir, 'verify.md'), readOptionalJson(taskDir, 'verify.result.json'), selectedMode),
290
294
  'status.md': compactStatus(readTaskFile(taskDir, 'status.md')),
291
295
  'feedback.md': compactArtifact(taskDir, 'feedback.md', selectedMode, ['feedback event', 'classification', 'supervisor decision']),
292
296
  'orchestration-log.md': compactOrchestrationLog(readTaskFile(taskDir, 'orchestration-log.md'), selectedMode),
@@ -483,6 +487,72 @@ function compactOrchestrationLog(log, mode) {
483
487
  return markCompacted('orchestration-log.md', log, compacted);
484
488
  }
485
489
 
490
+ function compactVerifierMarkdown(verifyMarkdown, verifyResult, mode) {
491
+ if (!verifyMarkdown.trim()) {
492
+ return '';
493
+ }
494
+ const findings = Array.isArray(verifyResult?.findings) ? verifyResult.findings : [];
495
+ const lines = [
496
+ '# Verify compact excerpt',
497
+ '',
498
+ `Verdict: ${verifyResult?.verdict || 'unknown'}`,
499
+ `Verifier run: ${verifyResult?.verifierRunId || 'unknown'}`,
500
+ `Findings: ${findings.length}`,
501
+ '',
502
+ compactMarkdownSections(verifyMarkdown, ['verdict', 'findings', 'residual risks', 'recommended next step'], charLimitForMode(mode, 1600, 3200)),
503
+ '',
504
+ ...findings.map((finding) => [
505
+ `## ${finding.id || 'finding'}`,
506
+ `- Severity: ${finding.severity || 'unknown'}`,
507
+ `- Category: ${finding.claimCategory || 'unknown'}`,
508
+ `- Affected artifacts: ${truncateEnd(JSON.stringify(finding.affectedArtifacts || []), 250)}`,
509
+ '- Evidence refs:',
510
+ ...formatRefs(finding.evidenceRefs || [], 220),
511
+ `- Claim: ${truncateEnd(finding.claim || '', 700)}`,
512
+ `- Expected correction: ${truncateEnd(finding.expectedCorrection || '', 700)}`,
513
+ ].join('\n')),
514
+ ];
515
+ return markCompacted('verify.md', verifyMarkdown, lines.join('\n').trim());
516
+ }
517
+
518
+ function compactExecutionLedger(ledger, mode) {
519
+ if (!ledger || typeof ledger !== 'object' || Array.isArray(ledger)) {
520
+ return '{}';
521
+ }
522
+ const changedFiles = Array.isArray(ledger.git?.changedFiles) ? ledger.git.changedFiles : [];
523
+ const unrelatedDirtyFiles = Array.isArray(ledger.git?.unrelatedDirtyFiles) ? ledger.git.unrelatedDirtyFiles : [];
524
+ const limit = mode === 'fast' ? 40 : mode === 'standard' ? 90 : 160;
525
+ const compact = {
526
+ schemaVersion: ledger.schemaVersion,
527
+ taskId: ledger.taskId,
528
+ createdAt: ledger.createdAt,
529
+ planSha: ledger.planSha,
530
+ executionSha: ledger.executionSha,
531
+ git: {
532
+ taskRelativePath: ledger.git?.taskRelativePath || null,
533
+ changedFileCount: changedFiles.length,
534
+ unrelatedDirtyFileCount: unrelatedDirtyFiles.length,
535
+ changedFiles: changedFiles.slice(0, limit).map(compactLedgerFile),
536
+ unrelatedDirtyFiles: unrelatedDirtyFiles.slice(0, limit).map(compactLedgerFile),
537
+ truncatedChangedFiles: Math.max(0, changedFiles.length - limit),
538
+ truncatedUnrelatedDirtyFiles: Math.max(0, unrelatedDirtyFiles.length - limit),
539
+ },
540
+ taskArtifacts: Array.isArray(ledger.taskArtifacts) ? ledger.taskArtifacts : [],
541
+ notes: ledger.notes || [],
542
+ };
543
+ return markCompacted('execution-ledger.json', JSON.stringify(ledger, null, 2), JSON.stringify(compact, null, 2));
544
+ }
545
+
546
+ function compactLedgerFile(file) {
547
+ return {
548
+ path: file.path,
549
+ status: file.status,
550
+ isTaskArtifact: Boolean(file.isTaskArtifact),
551
+ isOpsFrameworkFile: Boolean(file.isOpsFrameworkFile),
552
+ isExecutionMentioned: Boolean(file.isExecutionMentioned),
553
+ };
554
+ }
555
+
486
556
  function compactProjectMemory(projectMemory, mode) {
487
557
  if (mode === 'strict') {
488
558
  return projectMemory;
@@ -62,9 +62,10 @@ describe('llm input pack utilities', () => {
62
62
  expect(pack.meta.compactedArtifacts).toContain('orchestration-log.md');
63
63
  });
64
64
 
65
- it('keeps full artifacts in strict verifier pack', () => {
66
- const taskDir = createTask({ orchestrationEvents: 8 });
65
+ it('compacts generated review artifacts in strict verifier pack', () => {
66
+ const taskDir = createTask({ orchestrationEvents: 80 });
67
67
  const fullLog = fs.readFileSync(path.join(taskDir, 'orchestration-log.md'), 'utf8');
68
+ const fullExecution = fs.readFileSync(path.join(taskDir, 'execution.md'), 'utf8');
68
69
  const pack = buildVerifierLlmInputPack({
69
70
  taskDir,
70
71
  taskId: 'TASK-999-token-pack',
@@ -74,8 +75,12 @@ describe('llm input pack utilities', () => {
74
75
  mode: 'strict',
75
76
  });
76
77
 
77
- expect(pack.input.taskArtifacts['orchestration-log.md']).toBe(fullLog);
78
- expect(pack.meta.compactedArtifacts).not.toContain('orchestration-log.md');
78
+ expect(pack.input.taskArtifacts['execution.md']).toBe(fullExecution);
79
+ expect(pack.input.taskArtifacts['orchestration-log.md']).toContain('# Orchestration Log Compact');
80
+ expect(pack.input.taskArtifacts['orchestration-log.md'].length).toBeLessThan(fullLog.length);
81
+ expect(pack.input.taskArtifacts['verify.md']).toContain('# Verify compact excerpt');
82
+ expect(pack.meta.compactedArtifacts).toContain('orchestration-log.md');
83
+ expect(pack.meta.compactedArtifacts).toContain('verify.md');
79
84
  });
80
85
 
81
86
  it('builds bounded fallback mode sequence for context insufficient results', () => {
@@ -246,6 +251,8 @@ function createTask({ orchestrationEvents = 40 } = {}) {
246
251
  write(taskDir, 'status.md', '# Status\n\n## Текущий этап\n\nverify\n\n## Следующий шаг\n\nRun verify.');
247
252
  write(taskDir, 'check.result.json', JSON.stringify({ verdict: 'ready_for_human_gate', findings: [] }, null, 2));
248
253
  write(taskDir, 'check.md', '# Check\n\n## Итоговая оценка\n\nReady.');
254
+ write(taskDir, 'verify.result.json', JSON.stringify({ verdict: 'return_to_execute', findings: [] }, null, 2));
255
+ write(taskDir, 'verify.md', '# Verify\n\n## verdict\n\nreturn_to_execute\n\n## findings\n\nPrevious finding.');
249
256
  write(taskDir, 'execution-ledger.json', JSON.stringify({ git: { changedFiles: [] } }, null, 2));
250
257
  write(taskDir, 'task-manifest.json', JSON.stringify({ context: { riskTriggers: ['panel-ui'] } }, null, 2));
251
258
  write(taskDir, 'orchestration-log.md', [
@@ -26,7 +26,18 @@ describe('task manifest utilities', () => {
26
26
  '',
27
27
  '## Risk tier and execution budget',
28
28
  '',
29
+ '- Risk tier: `R1`',
29
30
  '- Speed mode: `Fast`',
31
+ '- Approved execution target: docs/example.md only.',
32
+ '- Requires return to Plan/Check if: code changes are needed.',
33
+ '',
34
+ '## План проверки',
35
+ '',
36
+ '### Verification ladder',
37
+ '',
38
+ '- Micro-verify during Execute: markdown review.',
39
+ '- Slice-verify before completion: self-test.',
40
+ '- External Verify required before closeout: no.',
30
41
  '',
31
42
  '## Затронутые модули и файлы',
32
43
  '',
@@ -94,7 +105,18 @@ describe('task manifest utilities', () => {
94
105
  '',
95
106
  '## Risk tier and execution budget',
96
107
  '',
108
+ '- Risk tier: `R1`',
97
109
  '- Speed mode: `Fast`',
110
+ '- Approved execution target: docs/example.md only.',
111
+ '- Requires return to Plan/Check if: code changes are needed.',
112
+ '',
113
+ '## План проверки',
114
+ '',
115
+ '### Verification ladder',
116
+ '',
117
+ '- Micro-verify during Execute: markdown review.',
118
+ '- Slice-verify before completion: self-test.',
119
+ '- External Verify required before closeout: no.',
98
120
  '',
99
121
  '## Затронутые модули и файлы',
100
122
  '',