principles-disciple 1.8.2 → 1.8.3
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/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/templates/langs/en/skills/ai-sprint-orchestration/EXAMPLES.md +63 -0
- package/templates/langs/en/skills/ai-sprint-orchestration/REFERENCE.md +136 -0
- package/templates/langs/en/skills/ai-sprint-orchestration/SKILL.md +67 -0
- package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +214 -0
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +107 -0
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +107 -0
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +105 -0
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +108 -0
- package/templates/langs/en/skills/ai-sprint-orchestration/references/workflow-v1-acceptance-checklist.md +58 -0
- package/templates/langs/en/skills/ai-sprint-orchestration/references/workflow-v1.4-work-unit-handoff.md +190 -0
- package/templates/langs/en/skills/ai-sprint-orchestration/runtime/.gitignore +2 -0
- package/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/archive.mjs +310 -0
- package/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/contract-enforcement.mjs +683 -0
- package/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/decision.mjs +604 -0
- package/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/state-store.mjs +32 -0
- package/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/task-specs.mjs +707 -0
- package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +3419 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/EXAMPLES.md +63 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/REFERENCE.md +136 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/SKILL.md +67 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +214 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +107 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +107 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +105 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +108 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/workflow-v1-acceptance-checklist.md +58 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/workflow-v1.4-work-unit-handoff.md +190 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/runtime/.gitignore +2 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/archive.mjs +310 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/contract-enforcement.mjs +683 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/decision.mjs +604 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/state-store.mjs +32 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/task-specs.mjs +707 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +3419 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/test/archive.test.mjs +230 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/test/contract-enforcement.test.mjs +672 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/test/decision.test.mjs +1321 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +1419 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import { determineOutputQuality, OUTPUT_QUALITY, validateStageReports, determineNextRunRecommendation, NEXT_RUN_TYPE } from './contract-enforcement.mjs';
|
|
2
|
+
|
|
3
|
+
const VERDICT_RE = /(?:VERDICT:\s*\*{0,2}|##\s*VERDICT\s*\n+\*{0,2}\s*)(APPROVE|REVISE|BLOCK)\b/i;
|
|
4
|
+
const DIMENSIONS_RE = /^\*{0,2}DIMENSIONS\*{0,2}:\s*(.+)$/im;
|
|
5
|
+
// Match both "SECTION:" and "## SECTION" markdown heading formats
|
|
6
|
+
// Section terminator: only matches next level-2 heading (## HEADING) or ALLCAPS: at line start.
|
|
7
|
+
// Does NOT match ### subheadings or arbitrary Word: patterns inside section content.
|
|
8
|
+
const CODE_EVIDENCE_RE = /(?:##\s*)?CODE_EVIDENCE:?\s*\n([\s\S]*?)(?=\n(?:##\s+)?[A-Z][A-Z_ ]+:\s|\n##\s+[A-Z]|\n[A-Z][A-Z_ ]+:\s|$)/i;
|
|
9
|
+
// Support both bracket format [a, b] and plain comma list a, b
|
|
10
|
+
const FILES_CHECKED_RE = /files_check(?:ed|es):\s*\[([^\]]*)\]/i;
|
|
11
|
+
const FILES_CHECKED_FLAT_RE = /files_check(?:ed|es):\s*([^\n]+)/i;
|
|
12
|
+
const FILES_VERIFIED_RE = /files_verified:\s*\[([^\]]*)\]/i;
|
|
13
|
+
const FILES_VERIFIED_FLAT_RE = /files_verified:\s*([^\n]+)/i;
|
|
14
|
+
const EVIDENCE_SOURCE_RE = /evidence_source:\s*(local|remote|both)/i;
|
|
15
|
+
const SHA_RE = /sha:\s*([a-f0-9]+)/i;
|
|
16
|
+
const BRANCH_RE = /branch\/worktree:\s*([^\n]+)/i;
|
|
17
|
+
const EVIDENCE_SCOPE_RE = /evidence_scope:\s*(principles|openclaw|both)/i;
|
|
18
|
+
const MACRO_ANSWERS_RE = /(?:##\s*)?MACRO_ANSWERS:?\s*\n?([\s\S]*?)(?:\n(?:##\s*)?[A-Z_ ]+:|$)/i;
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Decision Schema Definitions
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Decision Input Schema
|
|
26
|
+
* Structured input for the decision gate.
|
|
27
|
+
*/
|
|
28
|
+
export const DECISION_INPUT_SCHEMA = {
|
|
29
|
+
stageCriteria: {
|
|
30
|
+
requiredApprovals: 'number (default: 2 or 3 if globalReviewerRequired)',
|
|
31
|
+
requiredProducerSections: 'string[]',
|
|
32
|
+
requiredReviewerSections: 'string[]',
|
|
33
|
+
requiredGlobalReviewerSections: 'string[] (optional)',
|
|
34
|
+
scoringDimensions: 'string[] (optional)',
|
|
35
|
+
dimensionThreshold: 'number (default: 3)',
|
|
36
|
+
requiredDeliverables: 'string[] (optional)',
|
|
37
|
+
globalReviewerRequired: 'boolean (optional)',
|
|
38
|
+
globalReviewerMustAnswer: 'string[] (optional)',
|
|
39
|
+
},
|
|
40
|
+
reports: {
|
|
41
|
+
producer: 'string (markdown report)',
|
|
42
|
+
reviewerA: 'string (markdown report)',
|
|
43
|
+
reviewerB: 'string (markdown report)',
|
|
44
|
+
globalReviewer: 'string | null (markdown report)',
|
|
45
|
+
},
|
|
46
|
+
state: {
|
|
47
|
+
currentRound: 'number',
|
|
48
|
+
maxRoundsPerStage: 'number',
|
|
49
|
+
},
|
|
50
|
+
metadata: {
|
|
51
|
+
reviewerViolations: 'Array<{role, violatedFile}> | null',
|
|
52
|
+
reviewerADimensionsFallback: 'Object | null',
|
|
53
|
+
reviewerBDimensionsFallback: 'Object | null',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Decision Output Schema
|
|
59
|
+
* Structured output from the decision gate.
|
|
60
|
+
*/
|
|
61
|
+
export const DECISION_OUTPUT_SCHEMA = {
|
|
62
|
+
outcome: 'advance | revise | halt',
|
|
63
|
+
outputQuality: 'shadow_complete | production_ready | needs_work',
|
|
64
|
+
blockers: 'string[]',
|
|
65
|
+
metrics: 'StageMetrics object',
|
|
66
|
+
validation: 'ContractValidationResult',
|
|
67
|
+
summary: 'string (human-readable)',
|
|
68
|
+
qualityReasons: 'string[] (reasons for outputQuality)',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export function normalizeVerdict(text) {
|
|
72
|
+
const match = String(text ?? '').match(VERDICT_RE);
|
|
73
|
+
return match ? match[1].toUpperCase() : 'REVISE';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function hasExplicitVerdict(text) {
|
|
77
|
+
return VERDICT_RE.test(String(text ?? ''));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function extractBullets(text, heading) {
|
|
81
|
+
const source = String(text ?? '');
|
|
82
|
+
const pattern = new RegExp(`${heading}:\\s*([\\s\\S]*?)(?:\\n[A-Z_ ]+:|$)`, 'i');
|
|
83
|
+
const match = source.match(pattern);
|
|
84
|
+
if (!match) return [];
|
|
85
|
+
return match[1]
|
|
86
|
+
.split(/\r?\n/)
|
|
87
|
+
.map((line) => line.trim())
|
|
88
|
+
.filter((line) => line.startsWith('-'))
|
|
89
|
+
.map((line) => line.slice(1).trim())
|
|
90
|
+
.filter((line) => Boolean(line) && !/^none\.?\s*$/i.test(line));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function hasSection(text, heading) {
|
|
94
|
+
const pattern = new RegExp(`^${heading}:|^##\\s+${heading}\\b`, 'im');
|
|
95
|
+
return pattern.test(String(text ?? ''));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function extractMetricValue(text, key) {
|
|
99
|
+
const match = String(text ?? '').match(new RegExp(`${key}:\\s*(.+)$`, 'im'));
|
|
100
|
+
return match ? match[1].trim() : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function parseDimensions(text) {
|
|
104
|
+
const source = String(text ?? '');
|
|
105
|
+
const match = source.match(DIMENSIONS_RE);
|
|
106
|
+
if (!match) return {};
|
|
107
|
+
return match[1]
|
|
108
|
+
.split(';')
|
|
109
|
+
.map((pair) => pair.trim())
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.reduce((acc, pair) => {
|
|
112
|
+
const eq = pair.indexOf('=');
|
|
113
|
+
if (eq === -1) return acc;
|
|
114
|
+
const key = pair.slice(0, eq).trim();
|
|
115
|
+
const value = Number(pair.slice(eq + 1).trim());
|
|
116
|
+
if (key && !Number.isNaN(value)) acc[key] = value;
|
|
117
|
+
return acc;
|
|
118
|
+
}, {});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function checkDimensionThresholds(dimensionScores, requiredDimensions, threshold = 3) {
|
|
122
|
+
const failures = [];
|
|
123
|
+
const checks = {};
|
|
124
|
+
for (const dim of requiredDimensions) {
|
|
125
|
+
const score = dimensionScores[dim];
|
|
126
|
+
if (score === undefined) {
|
|
127
|
+
failures.push(`Dimension "${dim}" not scored by reviewer.`);
|
|
128
|
+
checks[dim] = null;
|
|
129
|
+
} else if (score < threshold) {
|
|
130
|
+
failures.push(`Dimension "${dim}" scored ${score}/5 (below threshold ${threshold}).`);
|
|
131
|
+
checks[dim] = false;
|
|
132
|
+
} else {
|
|
133
|
+
checks[dim] = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { failures, checks };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function extractContractItems(text) {
|
|
140
|
+
// Strip markdown code fences so inner sections are visible to regex
|
|
141
|
+
const source = String(text ?? '').replace(/```[\s\S]*?```/g, (m) => m.replace(/```\w*\n?/g, ''));
|
|
142
|
+
|
|
143
|
+
// Use ## CONTRACT (markdown heading) to anchor the section start.
|
|
144
|
+
// Capture content until next ## HEADING or end of file.
|
|
145
|
+
// This prevents the old regex from matching arbitrary "WORD:" patterns
|
|
146
|
+
// in prose (e.g., "primary completion contract") as section boundaries.
|
|
147
|
+
// Falls back to legacy CONTRACT: pattern (no heading) for backwards compat.
|
|
148
|
+
let match = source.match(/##\s+CONTRACT\s*\n([\s\S]*?)(?=\n##\s+[A-Z]|$)/i);
|
|
149
|
+
if (!match) {
|
|
150
|
+
match = source.match(/(?:##\s*)?CONTRACT:?\s*\n([\s\S]*?)(?=\n##\s+[A-Z]|\n[A-Z]{2,}[A-Z_ ]+:\s|$)/i);
|
|
151
|
+
}
|
|
152
|
+
if (!match) return [];
|
|
153
|
+
const items = [];
|
|
154
|
+
const blocks = match[1]
|
|
155
|
+
.split(/\r?\n/)
|
|
156
|
+
.map((l) => l.trim())
|
|
157
|
+
.filter((l) => l.startsWith('-') && !/^[-*_]{3,}\s*$/.test(l));
|
|
158
|
+
for (const block of blocks) {
|
|
159
|
+
const deliverable = block.replace(/^-\s*/, '').trim();
|
|
160
|
+
// Primary format: "<description> status: DONE|PARTIAL|TODO"
|
|
161
|
+
let statusMatch = deliverable.match(/status:\s*(DONE|PARTIAL|TODO)/i);
|
|
162
|
+
let statusGroup = 1; // group index for DONE/PARTIAL/TODO capture
|
|
163
|
+
// Fallback: "<key>: DONE" or "<key>: DONE — <description>"
|
|
164
|
+
// Single regex with optional trailing dash-content avoids redundant second match attempt
|
|
165
|
+
if (!statusMatch) {
|
|
166
|
+
statusMatch = deliverable.match(/^(.+):\s*(DONE|PARTIAL|TODO)\s*(?:[—–-]\s*.*)?$/i);
|
|
167
|
+
if (statusMatch) statusGroup = 2;
|
|
168
|
+
}
|
|
169
|
+
// Extract deliverable name
|
|
170
|
+
let cleaned = deliverable;
|
|
171
|
+
if (statusMatch) {
|
|
172
|
+
if (statusMatch[0].startsWith('status:')) {
|
|
173
|
+
cleaned = deliverable.replace(/\s*status:\s*\w+\s*/i, '' ).replace(/evidence:\s*"[^"]*"/i, '' ).trim();
|
|
174
|
+
} else {
|
|
175
|
+
// Fallback: group 1 captures everything before the last colon
|
|
176
|
+
const nameMatch = deliverable.match(/^(.+):\s*(DONE|PARTIAL|TODO)/i);
|
|
177
|
+
if (nameMatch) cleaned = nameMatch[1].trim();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
items.push({
|
|
181
|
+
deliverable: cleaned,
|
|
182
|
+
status: statusMatch ? statusMatch[statusGroup].toUpperCase() : 'UNKNOWN',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return items;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function checkContractCompletion(contractItems) {
|
|
189
|
+
const incomplete = contractItems.filter((item) => item.status !== 'DONE');
|
|
190
|
+
return {
|
|
191
|
+
allDone: incomplete.length === 0 && contractItems.length > 0,
|
|
192
|
+
incompleteItems: incomplete,
|
|
193
|
+
totalItems: contractItems.length,
|
|
194
|
+
doneItems: contractItems.length - incomplete.length,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function extractCodeEvidence(text) {
|
|
199
|
+
const source = String(text ?? '');
|
|
200
|
+
const match = source.match(CODE_EVIDENCE_RE);
|
|
201
|
+
if (!match) return null;
|
|
202
|
+
|
|
203
|
+
const body = match[1];
|
|
204
|
+
// Try files_checked (producer style) or files_verified (reviewer style)
|
|
205
|
+
// Support both [a, b] bracket format and plain comma list format
|
|
206
|
+
const filesCheckedMatch = body.match(FILES_CHECKED_RE) || body.match(FILES_CHECKED_FLAT_RE)
|
|
207
|
+
|| body.match(FILES_VERIFIED_RE) || body.match(FILES_VERIFIED_FLAT_RE);
|
|
208
|
+
const evidenceSourceMatch = body.match(EVIDENCE_SOURCE_RE);
|
|
209
|
+
const shaMatch = body.match(SHA_RE);
|
|
210
|
+
const branchMatch = body.match(BRANCH_RE);
|
|
211
|
+
const scopeMatch = body.match(EVIDENCE_SCOPE_RE);
|
|
212
|
+
|
|
213
|
+
const parseFileList = (str) => {
|
|
214
|
+
if (!str) return [];
|
|
215
|
+
return str.split(',').map((f) => f.trim()).filter(Boolean);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
filesChecked: filesCheckedMatch ? parseFileList(filesCheckedMatch[1]) : [],
|
|
220
|
+
evidenceSource: evidenceSourceMatch ? evidenceSourceMatch[1] : null,
|
|
221
|
+
sha: shaMatch ? shaMatch[1] : null,
|
|
222
|
+
branchWorktree: branchMatch ? branchMatch[1].trim() : null,
|
|
223
|
+
evidenceScope: scopeMatch ? scopeMatch[1] : null,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function hasCodeEvidence(text) {
|
|
228
|
+
return CODE_EVIDENCE_RE.test(String(text ?? ''));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function extractMacroAnswers(text, requiredQuestions = []) {
|
|
232
|
+
const source = String(text ?? '');
|
|
233
|
+
|
|
234
|
+
// Step 1: Find MACRO_ANSWERS section header (case-insensitive)
|
|
235
|
+
const headerMatch = source.match(/(?:##\s*)?MACRO_ANSWERS:?\s*\n/i);
|
|
236
|
+
if (!headerMatch) return { found: [], satisfied: [], allSatisfied: false };
|
|
237
|
+
|
|
238
|
+
const bodyStart = headerMatch.index + headerMatch[0].length;
|
|
239
|
+
const afterHeader = source.slice(bodyStart);
|
|
240
|
+
|
|
241
|
+
// Step 2: Capture until next ## heading (case-sensitive: only ALL-CAPS words are terminators).
|
|
242
|
+
// Section headings are ## BLOCKERS, ## FINDINGS — always ALL-CAPS after ##.
|
|
243
|
+
// Regular prose with colons (e.g., "State transitions are complete:") must NOT terminate.
|
|
244
|
+
const endMatch = afterHeader.match(/\n##\s+[A-Z]{2,}[A-Z_ ]*/);
|
|
245
|
+
const body = endMatch ? afterHeader.slice(0, endMatch.index) : afterHeader;
|
|
246
|
+
|
|
247
|
+
const found = [];
|
|
248
|
+
const satisfied = [];
|
|
249
|
+
for (const q of requiredQuestions) {
|
|
250
|
+
const pattern = new RegExp(`\\b${q}\\b[^\\n]*`, 'i');
|
|
251
|
+
const qMatch = body.match(pattern);
|
|
252
|
+
if (qMatch) {
|
|
253
|
+
found.push(q);
|
|
254
|
+
// Consider satisfied if it has a non-empty answer (not just "Q1:" with nothing after)
|
|
255
|
+
const answerText = qMatch[0].replace(/^[A-Z]\d+:\s*/i, '').trim();
|
|
256
|
+
if (answerText && !/^n\/a|^none|^pending/i.test(answerText)) {
|
|
257
|
+
satisfied.push(q);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
found,
|
|
263
|
+
satisfied,
|
|
264
|
+
allSatisfied: requiredQuestions.length > 0 && satisfied.length === requiredQuestions.length,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function buildHandoff({ reviewerA, reviewerB, globalReviewer, producer, metrics, stageName, round, workUnit = null }) {
|
|
269
|
+
const blockersA = extractBullets(reviewerA, 'BLOCKERS');
|
|
270
|
+
const blockersB = extractBullets(reviewerB, 'BLOCKERS');
|
|
271
|
+
const blockersGlobal = globalReviewer ? extractBullets(globalReviewer, 'BLOCKERS') : [];
|
|
272
|
+
const focusA = extractMetricValue(reviewerA, 'NEXT_FOCUS');
|
|
273
|
+
const focusB = extractMetricValue(reviewerB, 'NEXT_FOCUS');
|
|
274
|
+
const focusGlobal = globalReviewer ? extractMetricValue(globalReviewer, 'NEXT_FOCUS') : null;
|
|
275
|
+
const producerChecks = extractMetricValue(producer, 'CHECKS');
|
|
276
|
+
const contractItems = extractContractItems(producer);
|
|
277
|
+
|
|
278
|
+
const allBlockers = [...blockersA, ...blockersB, ...blockersGlobal];
|
|
279
|
+
const accomplished = contractItems.filter((item) => item.status === 'DONE').map((item) => item.deliverable);
|
|
280
|
+
const pending = contractItems.filter((item) => item.status !== 'DONE').map((item) => item.deliverable);
|
|
281
|
+
const unitSummaryParts = [
|
|
282
|
+
accomplished.length > 0 ? `Accomplished: ${accomplished.join(', ')}` : null,
|
|
283
|
+
pending.length > 0 ? `Pending: ${pending.join(', ')}` : null,
|
|
284
|
+
allBlockers.length > 0 ? `Blockers: ${allBlockers.join('; ')}` : null,
|
|
285
|
+
].filter(Boolean);
|
|
286
|
+
const carryForwardSummaryParts = [
|
|
287
|
+
workUnit?.workUnitGoal ? `Goal: ${workUnit.workUnitGoal}` : null,
|
|
288
|
+
focusA || focusB || focusGlobal ? `Next focus: ${[focusA, focusB, focusGlobal].filter(Boolean).join('; ')}` : null,
|
|
289
|
+
allBlockers.length > 0 ? `Blockers: ${allBlockers.join('; ')}` : 'Blockers: none',
|
|
290
|
+
workUnit?.allowedFiles?.length ? `Files: ${workUnit.allowedFiles.join(', ')}` : null,
|
|
291
|
+
].filter(Boolean);
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
blockers: allBlockers,
|
|
295
|
+
focusForNextRound: [focusA, focusB, focusGlobal].filter(Boolean).join('; ') || null,
|
|
296
|
+
producerChecks,
|
|
297
|
+
dimensionScores: {
|
|
298
|
+
reviewerA: metrics?.reviewerADimensions ?? {},
|
|
299
|
+
reviewerB: metrics?.reviewerBDimensions ?? {},
|
|
300
|
+
globalReviewer: metrics?.globalReviewerVerdict ?? null,
|
|
301
|
+
},
|
|
302
|
+
contractItems,
|
|
303
|
+
producerCodeEvidence: extractCodeEvidence(producer),
|
|
304
|
+
reviewerACodeEvidence: extractCodeEvidence(reviewerA),
|
|
305
|
+
reviewerBCodeEvidence: extractCodeEvidence(reviewerB),
|
|
306
|
+
globalReviewerCodeEvidence: globalReviewer ? extractCodeEvidence(globalReviewer) : null,
|
|
307
|
+
workUnitId: workUnit?.workUnitId ?? null,
|
|
308
|
+
workUnitGoal: workUnit?.workUnitGoal ?? null,
|
|
309
|
+
allowedFiles: workUnit?.allowedFiles ?? [],
|
|
310
|
+
unitChecks: workUnit?.unitChecks ?? [],
|
|
311
|
+
unitDeliverables: workUnit?.unitDeliverables ?? [],
|
|
312
|
+
unitSummary: unitSummaryParts.join(' | ') || workUnit?.unitSummary || null,
|
|
313
|
+
carryForwardSummary: carryForwardSummaryParts.join(' | ') || workUnit?.carryForwardSummary || null,
|
|
314
|
+
stageName,
|
|
315
|
+
round,
|
|
316
|
+
generatedAt: new Date().toISOString(),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function buildStageMetrics({ stageCriteria, producer, reviewerA, reviewerB, globalReviewer, reviewerADimensionsFallback, reviewerBDimensionsFallback }) {
|
|
321
|
+
const reviewerAVerdict = normalizeVerdict(reviewerA);
|
|
322
|
+
const reviewerBVerdict = normalizeVerdict(reviewerB);
|
|
323
|
+
const globalReviewerVerdict = globalReviewer ? normalizeVerdict(globalReviewer) : null;
|
|
324
|
+
const globalReviewerRequired = stageCriteria?.globalReviewerRequired === true;
|
|
325
|
+
const blockers = [
|
|
326
|
+
...extractBullets(reviewerA, 'BLOCKERS'),
|
|
327
|
+
...extractBullets(reviewerB, 'BLOCKERS'),
|
|
328
|
+
...(globalReviewerRequired ? extractBullets(globalReviewer, 'BLOCKERS') : []),
|
|
329
|
+
];
|
|
330
|
+
const requiredProducerSections = stageCriteria?.requiredProducerSections ?? [];
|
|
331
|
+
const requiredReviewerSections = stageCriteria?.requiredReviewerSections ?? [];
|
|
332
|
+
const requiredGlobalReviewerSections = stageCriteria?.requiredGlobalReviewerSections ?? [];
|
|
333
|
+
const producerSectionChecks = Object.fromEntries(
|
|
334
|
+
requiredProducerSections.map((heading) => [heading, hasSection(producer, heading)]),
|
|
335
|
+
);
|
|
336
|
+
const reviewerSectionChecks = Object.fromEntries(
|
|
337
|
+
requiredReviewerSections.map((heading) => [
|
|
338
|
+
heading,
|
|
339
|
+
hasSection(reviewerA, heading) && hasSection(reviewerB, heading),
|
|
340
|
+
]),
|
|
341
|
+
);
|
|
342
|
+
const globalReviewerChecks = Object.fromEntries(
|
|
343
|
+
requiredGlobalReviewerSections.map((heading) => [heading, globalReviewer ? hasSection(globalReviewer, heading) : false]),
|
|
344
|
+
);
|
|
345
|
+
const requiredMacroAnswers = stageCriteria?.globalReviewerMustAnswer ?? [];
|
|
346
|
+
const macroAnswers = globalReviewer ? extractMacroAnswers(globalReviewer, requiredMacroAnswers) : { found: [], satisfied: [], allSatisfied: false };
|
|
347
|
+
const approvalCount = [
|
|
348
|
+
reviewerAVerdict,
|
|
349
|
+
reviewerBVerdict,
|
|
350
|
+
...(globalReviewerRequired ? [globalReviewerVerdict] : []),
|
|
351
|
+
].filter((v) => v === 'APPROVE').length;
|
|
352
|
+
|
|
353
|
+
const scoringDimensions = stageCriteria?.scoringDimensions ?? [];
|
|
354
|
+
const dimensionThreshold = stageCriteria?.dimensionThreshold ?? 3;
|
|
355
|
+
const parsedADims = parseDimensions(reviewerA);
|
|
356
|
+
const parsedBDims = parseDimensions(reviewerB);
|
|
357
|
+
// Fallback to reviewer state JSON dimensions when report text parsing returns empty
|
|
358
|
+
const reviewerADimensions = Object.keys(parsedADims).length > 0 ? parsedADims : (reviewerADimensionsFallback ?? {});
|
|
359
|
+
const reviewerBDimensions = Object.keys(parsedBDims).length > 0 ? parsedBDims : (reviewerBDimensionsFallback ?? {});
|
|
360
|
+
const dimensionCheckA = checkDimensionThresholds(reviewerADimensions, scoringDimensions, dimensionThreshold);
|
|
361
|
+
const dimensionCheckB = checkDimensionThresholds(reviewerBDimensions, scoringDimensions, dimensionThreshold);
|
|
362
|
+
|
|
363
|
+
const requiredDeliverables = stageCriteria?.requiredDeliverables ?? [];
|
|
364
|
+
const contractItems = extractContractItems(producer);
|
|
365
|
+
const contractCheck = checkContractCompletion(contractItems);
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
approvalCount,
|
|
369
|
+
blockerCount: blockers.length,
|
|
370
|
+
reviewerAVerdict,
|
|
371
|
+
reviewerBVerdict,
|
|
372
|
+
reviewerAHasExplicitVerdict: hasExplicitVerdict(reviewerA),
|
|
373
|
+
reviewerBHasExplicitVerdict: hasExplicitVerdict(reviewerB),
|
|
374
|
+
blockers,
|
|
375
|
+
producerSectionChecks,
|
|
376
|
+
reviewerSectionChecks,
|
|
377
|
+
producerChecks: extractMetricValue(producer, 'CHECKS'),
|
|
378
|
+
reviewerAChecks: extractMetricValue(reviewerA, 'CHECKS'),
|
|
379
|
+
reviewerBChecks: extractMetricValue(reviewerB, 'CHECKS'),
|
|
380
|
+
scoringDimensions,
|
|
381
|
+
reviewerADimensions,
|
|
382
|
+
reviewerBDimensions,
|
|
383
|
+
dimensionCheckA,
|
|
384
|
+
dimensionCheckB,
|
|
385
|
+
dimensionFailures: [...dimensionCheckA.failures, ...dimensionCheckB.failures],
|
|
386
|
+
contractItems,
|
|
387
|
+
contractCheck,
|
|
388
|
+
requiredDeliverables,
|
|
389
|
+
producerCodeEvidence: extractCodeEvidence(producer),
|
|
390
|
+
reviewerACodeEvidence: extractCodeEvidence(reviewerA),
|
|
391
|
+
reviewerBCodeEvidence: extractCodeEvidence(reviewerB),
|
|
392
|
+
producerHasCodeEvidence: hasCodeEvidence(producer),
|
|
393
|
+
reviewerAHasCodeEvidence: hasCodeEvidence(reviewerA),
|
|
394
|
+
reviewerBHasCodeEvidence: hasCodeEvidence(reviewerB),
|
|
395
|
+
// global reviewer fields
|
|
396
|
+
globalReviewerVerdict,
|
|
397
|
+
globalReviewerHasExplicitVerdict: globalReviewer ? hasExplicitVerdict(globalReviewer) : false,
|
|
398
|
+
globalReviewerChecks,
|
|
399
|
+
globalReviewerRequired,
|
|
400
|
+
macroAnswersFound: macroAnswers.found,
|
|
401
|
+
macroAnswersSatisfied: macroAnswers.satisfied,
|
|
402
|
+
macroAnswersAllSatisfied: macroAnswers.allSatisfied,
|
|
403
|
+
requiredMacroAnswers,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function decideStage({ stageCriteria, producer, reviewerA, reviewerB, globalReviewer, currentRound, maxRoundsPerStage, reviewerViolations = null, reviewerADimensionsFallback = null, reviewerBDimensionsFallback = null, skipContractValidation = false, spec = null }) {
|
|
408
|
+
const verdictA = normalizeVerdict(reviewerA);
|
|
409
|
+
const verdictB = normalizeVerdict(reviewerB);
|
|
410
|
+
const globalReviewerRequired = stageCriteria?.globalReviewerRequired === true;
|
|
411
|
+
const metrics = buildStageMetrics({ stageCriteria, producer, reviewerA, reviewerB, globalReviewer, reviewerADimensionsFallback, reviewerBDimensionsFallback });
|
|
412
|
+
|
|
413
|
+
// Perform contract validation
|
|
414
|
+
const validation = skipContractValidation ? { valid: true } : validateStageReports({
|
|
415
|
+
producer,
|
|
416
|
+
reviewerA,
|
|
417
|
+
reviewerB,
|
|
418
|
+
globalReviewer,
|
|
419
|
+
}, {
|
|
420
|
+
requiredDeliverables: stageCriteria?.requiredDeliverables,
|
|
421
|
+
scoringDimensions: stageCriteria?.scoringDimensions,
|
|
422
|
+
requiredMacroQuestions: stageCriteria?.globalReviewerMustAnswer,
|
|
423
|
+
globalReviewerRequired,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const blockers = metrics.blockers;
|
|
427
|
+
const producerSectionsSatisfied = Object.values(metrics.producerSectionChecks).every(Boolean);
|
|
428
|
+
const reviewerSectionsSatisfied = Object.values(metrics.reviewerSectionChecks).every(Boolean);
|
|
429
|
+
const globalReviewerSectionsSatisfied = globalReviewerRequired
|
|
430
|
+
? Object.values(metrics.globalReviewerChecks).every(Boolean)
|
|
431
|
+
: true;
|
|
432
|
+
const requiredApprovals = stageCriteria?.requiredApprovals ?? (globalReviewerRequired ? 3 : 2);
|
|
433
|
+
const explicitVerdictsOk = metrics.reviewerAHasExplicitVerdict &&
|
|
434
|
+
metrics.reviewerBHasExplicitVerdict &&
|
|
435
|
+
(!globalReviewerRequired || metrics.globalReviewerHasExplicitVerdict);
|
|
436
|
+
const structuralBlockers = [];
|
|
437
|
+
|
|
438
|
+
if (!explicitVerdictsOk) {
|
|
439
|
+
if (globalReviewerRequired && !metrics.globalReviewerHasExplicitVerdict) {
|
|
440
|
+
structuralBlockers.push('Global reviewer did not emit a strict VERDICT: APPROVE|REVISE|BLOCK line.');
|
|
441
|
+
}
|
|
442
|
+
if (!metrics.reviewerAHasExplicitVerdict) {
|
|
443
|
+
structuralBlockers.push('Reviewer A did not emit a strict VERDICT: APPROVE|REVISE|BLOCK line.');
|
|
444
|
+
}
|
|
445
|
+
if (!metrics.reviewerBHasExplicitVerdict) {
|
|
446
|
+
structuralBlockers.push('Reviewer B did not emit a strict VERDICT: APPROVE|REVISE|BLOCK line.');
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// NOTE: Dimension failures are NOT structural blockers. They affect output quality
|
|
451
|
+
// but do not prevent advance — dimensions are subjective judgments that reviewers
|
|
452
|
+
// may disagree on. Blocking advance on dimensions causes infinite revise loops.
|
|
453
|
+
// Low dimensions will downgrade outputQuality from production_ready → shadow_complete.
|
|
454
|
+
|
|
455
|
+
// Contract completion check: if requiredDeliverables are defined, all contract items must be DONE
|
|
456
|
+
if (metrics.requiredDeliverables.length > 0 && !metrics.contractCheck.allDone) {
|
|
457
|
+
if (metrics.contractCheck.incompleteItems.length > 0) {
|
|
458
|
+
const incomplete = metrics.contractCheck.incompleteItems
|
|
459
|
+
.map((item) => `"${item.deliverable}" is ${item.status}`)
|
|
460
|
+
.join('; ');
|
|
461
|
+
structuralBlockers.push(`Contract not fulfilled: ${incomplete}`);
|
|
462
|
+
} else {
|
|
463
|
+
// No contract items extracted at all — CONTRACT section missing or unparsable
|
|
464
|
+
structuralBlockers.push(`Contract not fulfilled: no contract items extracted (required: ${metrics.requiredDeliverables.join(', ')})`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Global reviewer macro answers check
|
|
469
|
+
if (globalReviewerRequired && metrics.requiredMacroAnswers.length > 0 && !metrics.macroAnswersAllSatisfied) {
|
|
470
|
+
const missing = metrics.requiredMacroAnswers.filter((q) => !metrics.macroAnswersSatisfied.includes(q));
|
|
471
|
+
structuralBlockers.push(`Global reviewer missing or incomplete macro answers: ${missing.join(', ')}`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Global reviewer sections check
|
|
475
|
+
if (globalReviewerRequired && !globalReviewerSectionsSatisfied) {
|
|
476
|
+
const missing = Object.entries(metrics.globalReviewerChecks)
|
|
477
|
+
.filter(([, v]) => !v)
|
|
478
|
+
.map(([k]) => k);
|
|
479
|
+
structuralBlockers.push(`Global reviewer missing required sections: ${missing.join(', ')}`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Global BLOCK is always a structural blocker
|
|
483
|
+
if (globalReviewerRequired && metrics.globalReviewerVerdict === 'BLOCK') {
|
|
484
|
+
const globalBlockers = extractBullets(globalReviewer, 'BLOCKERS');
|
|
485
|
+
if (globalBlockers.length > 0) {
|
|
486
|
+
structuralBlockers.push(...globalBlockers.map((b) => `[GLOBAL] ${b}`));
|
|
487
|
+
} else {
|
|
488
|
+
structuralBlockers.push('Global reviewer BLOCKED with no specific blockers listed.');
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Protected file violations by reviewers/global_reviewer are structural blockers
|
|
493
|
+
if (reviewerViolations && reviewerViolations.length > 0) {
|
|
494
|
+
for (const v of reviewerViolations) {
|
|
495
|
+
structuralBlockers.push(`${v.role} violated protected file ${v.violatedFile} — report invalidated`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Contract validation failures become structural blockers (cannot advance with invalid reports)
|
|
500
|
+
if (!validation.valid) {
|
|
501
|
+
if (validation.producer && !validation.producer.valid) {
|
|
502
|
+
const issues = [
|
|
503
|
+
...(validation.producer.missingSections || []).map((s) => `missing section: ${s}`),
|
|
504
|
+
...(validation.producer.invalidFields || []),
|
|
505
|
+
];
|
|
506
|
+
if (issues.length > 0) structuralBlockers.push(`Producer report contract violation: ${issues.join('; ')}`);
|
|
507
|
+
}
|
|
508
|
+
if (validation.reviewerA && !validation.reviewerA.valid) {
|
|
509
|
+
const issues = [
|
|
510
|
+
...(validation.reviewerA.missingSections || []).map((s) => `missing section: ${s}`),
|
|
511
|
+
...(validation.reviewerA.invalidFields || []),
|
|
512
|
+
];
|
|
513
|
+
if (issues.length > 0) structuralBlockers.push(`Reviewer A report contract violation: ${issues.join('; ')}`);
|
|
514
|
+
}
|
|
515
|
+
if (validation.reviewerB && !validation.reviewerB.valid) {
|
|
516
|
+
const issues = [
|
|
517
|
+
...(validation.reviewerB.missingSections || []).map((s) => `missing section: ${s}`),
|
|
518
|
+
...(validation.reviewerB.invalidFields || []),
|
|
519
|
+
];
|
|
520
|
+
if (issues.length > 0) structuralBlockers.push(`Reviewer B report contract violation: ${issues.join('; ')}`);
|
|
521
|
+
}
|
|
522
|
+
if (validation.globalReviewer && !validation.globalReviewer.valid) {
|
|
523
|
+
const issues = [
|
|
524
|
+
...(validation.globalReviewer.missingSections || []).map((s) => `missing section: ${s}`),
|
|
525
|
+
...(validation.globalReviewer.invalidFields || []),
|
|
526
|
+
];
|
|
527
|
+
if (issues.length > 0) structuralBlockers.push(`Global reviewer report contract violation: ${issues.join('; ')}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const allBlockers = [...structuralBlockers, ...blockers];
|
|
532
|
+
|
|
533
|
+
// Contract validation must be valid AND all other conditions met
|
|
534
|
+
if (
|
|
535
|
+
validation.valid &&
|
|
536
|
+
verdictA === 'APPROVE' &&
|
|
537
|
+
verdictB === 'APPROVE' &&
|
|
538
|
+
(!globalReviewerRequired || metrics.globalReviewerVerdict === 'APPROVE') &&
|
|
539
|
+
explicitVerdictsOk &&
|
|
540
|
+
metrics.approvalCount >= requiredApprovals &&
|
|
541
|
+
producerSectionsSatisfied &&
|
|
542
|
+
reviewerSectionsSatisfied &&
|
|
543
|
+
globalReviewerSectionsSatisfied &&
|
|
544
|
+
allBlockers.length === 0
|
|
545
|
+
) {
|
|
546
|
+
// Determine output quality (shadow_complete vs production_ready)
|
|
547
|
+
const qualityResult = determineOutputQuality(validation, metrics);
|
|
548
|
+
const nextRunRecommendation = determineNextRunRecommendation(
|
|
549
|
+
qualityResult.quality,
|
|
550
|
+
'advance',
|
|
551
|
+
spec,
|
|
552
|
+
{ qualityReasons: qualityResult.reasons }
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
return {
|
|
556
|
+
outcome: 'advance',
|
|
557
|
+
outputQuality: qualityResult.quality,
|
|
558
|
+
blockers: allBlockers,
|
|
559
|
+
metrics,
|
|
560
|
+
validation,
|
|
561
|
+
summary: qualityResult.quality === OUTPUT_QUALITY.PRODUCTION_READY
|
|
562
|
+
? 'All reviewers approved. Output is production-ready.'
|
|
563
|
+
: 'All reviewers approved. Output is shadow-complete.',
|
|
564
|
+
qualityReasons: qualityResult.reasons,
|
|
565
|
+
nextRunRecommendation,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (currentRound >= maxRoundsPerStage) {
|
|
570
|
+
const nextRunRecommendation = determineNextRunRecommendation(
|
|
571
|
+
OUTPUT_QUALITY.NEEDS_WORK,
|
|
572
|
+
'halt',
|
|
573
|
+
spec,
|
|
574
|
+
{ qualityReasons: ['Max rounds exceeded'] }
|
|
575
|
+
);
|
|
576
|
+
return {
|
|
577
|
+
outcome: 'halt',
|
|
578
|
+
outputQuality: OUTPUT_QUALITY.NEEDS_WORK,
|
|
579
|
+
blockers: [...structuralBlockers, ...blockers],
|
|
580
|
+
metrics,
|
|
581
|
+
validation,
|
|
582
|
+
summary: 'Stage exceeded maximum rounds without all required approvals.',
|
|
583
|
+
qualityReasons: ['Max rounds exceeded'],
|
|
584
|
+
nextRunRecommendation,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const nextRunRecommendation = determineNextRunRecommendation(
|
|
589
|
+
OUTPUT_QUALITY.NEEDS_WORK,
|
|
590
|
+
'revise',
|
|
591
|
+
spec,
|
|
592
|
+
{ qualityReasons: ['Reviewers did not approve'] }
|
|
593
|
+
);
|
|
594
|
+
return {
|
|
595
|
+
outcome: 'revise',
|
|
596
|
+
outputQuality: OUTPUT_QUALITY.NEEDS_WORK,
|
|
597
|
+
blockers: [...structuralBlockers, ...blockers],
|
|
598
|
+
metrics,
|
|
599
|
+
validation,
|
|
600
|
+
summary: 'At least one reviewer requested revision or blocked progress.',
|
|
601
|
+
qualityReasons: ['Reviewers did not approve'],
|
|
602
|
+
nextRunRecommendation,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export function ensureDir(dirPath) {
|
|
5
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function readJson(filePath) {
|
|
9
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
10
|
+
// Strip UTF-8 BOM (\uFEFF) that PowerShell/acpx may prepend
|
|
11
|
+
const cleaned = raw.replace(/^\uFEFF/, '');
|
|
12
|
+
return JSON.parse(cleaned);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function writeJson(filePath, data) {
|
|
16
|
+
ensureDir(path.dirname(filePath));
|
|
17
|
+
fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function writeText(filePath, text) {
|
|
21
|
+
ensureDir(path.dirname(filePath));
|
|
22
|
+
fs.writeFileSync(filePath, text, 'utf8');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function appendText(filePath, text) {
|
|
26
|
+
ensureDir(path.dirname(filePath));
|
|
27
|
+
fs.appendFileSync(filePath, text, 'utf8');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function fileExists(filePath) {
|
|
31
|
+
return fs.existsSync(filePath);
|
|
32
|
+
}
|