@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.
- package/CHANGELOG.md +18 -0
- package/bin/initiative.mjs +61 -0
- package/bin/initiative.test.mjs +114 -0
- package/bin/lib/check-context-utils.mjs +251 -2
- package/bin/lib/check-context-utils.test.mjs +260 -0
- package/bin/lib/execution-ledger-utils.mjs +22 -2
- package/bin/lib/execution-ledger-utils.test.mjs +60 -0
- package/bin/lib/llm-input-pack-utils.mjs +89 -19
- package/bin/lib/llm-input-pack-utils.test.mjs +11 -4
- package/bin/lib/task-manifest-utils.test.mjs +22 -0
- package/bin/run-check.mjs +92 -0
- package/bin/run-verify.mjs +106 -0
- package/package.json +1 -1
|
@@ -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(
|
|
46
|
-
unrelatedDirtyFiles: compactLedgerFiles(
|
|
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
|
-
?
|
|
258
|
-
'brief.md',
|
|
259
|
-
'research.md',
|
|
260
|
-
'plan.md',
|
|
261
|
-
'task-manifest.json',
|
|
262
|
-
'check.result.json',
|
|
263
|
-
'check.md'
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
'
|
|
269
|
-
'
|
|
270
|
-
'
|
|
271
|
-
'execution-
|
|
272
|
-
'
|
|
273
|
-
|
|
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':
|
|
289
|
-
'verify.md':
|
|
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('
|
|
66
|
-
const taskDir = createTask({ orchestrationEvents:
|
|
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['
|
|
78
|
-
expect(pack.
|
|
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
|
'',
|