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.
Files changed (40) hide show
  1. package/openclaw.plugin.json +4 -4
  2. package/package.json +1 -1
  3. package/templates/langs/en/skills/ai-sprint-orchestration/EXAMPLES.md +63 -0
  4. package/templates/langs/en/skills/ai-sprint-orchestration/REFERENCE.md +136 -0
  5. package/templates/langs/en/skills/ai-sprint-orchestration/SKILL.md +67 -0
  6. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +214 -0
  7. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +107 -0
  8. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +107 -0
  9. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +105 -0
  10. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +108 -0
  11. package/templates/langs/en/skills/ai-sprint-orchestration/references/workflow-v1-acceptance-checklist.md +58 -0
  12. package/templates/langs/en/skills/ai-sprint-orchestration/references/workflow-v1.4-work-unit-handoff.md +190 -0
  13. package/templates/langs/en/skills/ai-sprint-orchestration/runtime/.gitignore +2 -0
  14. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/archive.mjs +310 -0
  15. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/contract-enforcement.mjs +683 -0
  16. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/decision.mjs +604 -0
  17. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/state-store.mjs +32 -0
  18. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/task-specs.mjs +707 -0
  19. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +3419 -0
  20. package/templates/langs/zh/skills/ai-sprint-orchestration/EXAMPLES.md +63 -0
  21. package/templates/langs/zh/skills/ai-sprint-orchestration/REFERENCE.md +136 -0
  22. package/templates/langs/zh/skills/ai-sprint-orchestration/SKILL.md +67 -0
  23. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +214 -0
  24. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +107 -0
  25. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +107 -0
  26. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +105 -0
  27. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +108 -0
  28. package/templates/langs/zh/skills/ai-sprint-orchestration/references/workflow-v1-acceptance-checklist.md +58 -0
  29. package/templates/langs/zh/skills/ai-sprint-orchestration/references/workflow-v1.4-work-unit-handoff.md +190 -0
  30. package/templates/langs/zh/skills/ai-sprint-orchestration/runtime/.gitignore +2 -0
  31. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/archive.mjs +310 -0
  32. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/contract-enforcement.mjs +683 -0
  33. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/decision.mjs +604 -0
  34. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/state-store.mjs +32 -0
  35. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/task-specs.mjs +707 -0
  36. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +3419 -0
  37. package/templates/langs/zh/skills/ai-sprint-orchestration/test/archive.test.mjs +230 -0
  38. package/templates/langs/zh/skills/ai-sprint-orchestration/test/contract-enforcement.test.mjs +672 -0
  39. package/templates/langs/zh/skills/ai-sprint-orchestration/test/decision.test.mjs +1321 -0
  40. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +1419 -0
@@ -0,0 +1,683 @@
1
+ /**
2
+ * Contract Enforcement Module
3
+ *
4
+ * Defines strict schemas for agent output and provides validation functions.
5
+ * Orchestrator MUST validate reports against these contracts before consuming.
6
+ */
7
+
8
+ // ============================================================================
9
+ // Schema Definitions
10
+ // ============================================================================
11
+
12
+ /**
13
+ * Producer Report Schema
14
+ * Required sections for a valid producer report.
15
+ */
16
+ export const PRODUCER_SCHEMA = {
17
+ requiredSections: ['SUMMARY', 'CHANGES', 'EVIDENCE', 'CODE_EVIDENCE', 'KEY_EVENTS', 'HYPOTHESIS_MATRIX', 'CHECKS', 'OPEN_RISKS'],
18
+ optionalSections: ['CONTRACT'],
19
+ requiredFields: {
20
+ CHECKS: { format: 'key=value pairs', example: 'evidence=ok;tests=not-run;scope=pd-only' },
21
+ },
22
+ };
23
+
24
+ /**
25
+ * Reviewer Report Schema
26
+ * Required sections for a valid reviewer report.
27
+ */
28
+ export const REVIEWER_SCHEMA = {
29
+ requiredSections: ['VERDICT', 'BLOCKERS', 'FINDINGS', 'CODE_EVIDENCE', 'HYPOTHESIS_MATRIX', 'NEXT_FOCUS', 'CHECKS'],
30
+ optionalSections: ['DIMENSIONS'],
31
+ requiredFields: {
32
+ VERDICT: { allowedValues: ['APPROVE', 'REVISE', 'BLOCK'], format: 'exact match' },
33
+ CHECKS: { format: 'key=value pairs', example: 'criteria=met;blockers=0' },
34
+ },
35
+ };
36
+
37
+ /**
38
+ * Global Reviewer Report Schema
39
+ * Required sections for a valid global reviewer report.
40
+ */
41
+ export const GLOBAL_REVIEWER_SCHEMA = {
42
+ requiredSections: ['VERDICT', 'MACRO_ANSWERS', 'BLOCKERS', 'FINDINGS', 'CODE_EVIDENCE', 'NEXT_FOCUS', 'CHECKS'],
43
+ optionalSections: [],
44
+ requiredFields: {
45
+ VERDICT: { allowedValues: ['APPROVE', 'REVISE', 'BLOCK'], format: 'exact match' },
46
+ },
47
+ };
48
+
49
+ /**
50
+ * Output Quality Levels
51
+ */
52
+ export const OUTPUT_QUALITY = {
53
+ SHADOW_COMPLETE: 'shadow_complete',
54
+ PRODUCTION_READY: 'production_ready',
55
+ NEEDS_WORK: 'needs_work',
56
+ };
57
+
58
+ // ============================================================================
59
+ // Validation Result Types
60
+ // ============================================================================
61
+
62
+ /**
63
+ * @typedef {Object} ContractValidationResult
64
+ * @property {boolean} valid - Whether the report satisfies the contract
65
+ * @property {string[]} missingSections - Sections required but not found
66
+ * @property {string[]} invalidFields - Fields that don't match expected format
67
+ * @property {Object} extractedData - Successfully extracted structured data
68
+ * @property {string} errorSummary - Human-readable error summary
69
+ */
70
+
71
+ // ============================================================================
72
+ // Validation Functions
73
+ // ============================================================================
74
+
75
+ /**
76
+ * Check if a section exists in the report text.
77
+ * Supports both "SECTION:" and "## SECTION" markdown formats.
78
+ *
79
+ * @param {string} text - Report text
80
+ * @param {string} heading - Section heading to find
81
+ * @returns {boolean}
82
+ */
83
+ export function hasSectionStrict(text, heading) {
84
+ const source = String(text ?? '');
85
+ // Match "SECTION:" at start of line
86
+ const colonPattern = new RegExp(`(^|\\n)${heading}\\s*:`, 'i');
87
+ // Match "## SECTION" markdown heading (with optional colon after)
88
+ const mdPattern = new RegExp(`(^|\\n)##\\s+${heading}\\b`, 'i');
89
+ return colonPattern.test(source) || mdPattern.test(source);
90
+ }
91
+
92
+ /**
93
+ * Extract section content between headings.
94
+ *
95
+ * @param {string} text - Report text
96
+ * @param {string} heading - Section heading
97
+ * @returns {string|null} Section content or null if not found
98
+ */
99
+ export function extractSectionContent(text, heading) {
100
+ const source = String(text ?? '');
101
+ // Match section start
102
+ const startPattern = new RegExp(`(?:^|\n)(?:##\s+)?${heading}\s*:?\n`, 'i');
103
+ const startMatch = source.match(startPattern);
104
+ if (!startMatch) return null;
105
+
106
+ const contentStart = startMatch.index + startMatch[0].length;
107
+ const afterStart = source.slice(contentStart);
108
+
109
+ // Match next section (## HEADING or HEADING:)
110
+ const endPattern = /\n(?:##\s+)?[A-Z][A-Z_ ]+\s*(:|\n)/;
111
+ const endMatch = afterStart.match(endPattern);
112
+
113
+ return endMatch ? afterStart.slice(0, endMatch.index).trim() : afterStart.trim();
114
+ }
115
+
116
+ /**
117
+ * Validate VERDICT field against allowed values.
118
+ *
119
+ * @param {string} text - Report text
120
+ * @returns {{valid: boolean, value: string|null, error: string|null}}
121
+ */
122
+ export function validateVerdict(text) {
123
+ const source = String(text ?? '');
124
+ // Strict pattern: VERDICT: followed by exactly APPROVE, REVISE, or BLOCK
125
+ const pattern = /(?:VERDICT:\s*\*{0,2}|##\s*VERDICT\s*\n+\*{0,2}\s*)(APPROVE|REVISE|BLOCK)\b/i;
126
+ const match = source.match(pattern);
127
+
128
+ if (!match) {
129
+ // Check if VERDICT section exists but has invalid value
130
+ const loosePattern = /(?:VERDICT:\s*|##\s*VERDICT\s*\n+)([A-Z_]+)/i;
131
+ const looseMatch = source.match(loosePattern);
132
+ if (looseMatch) {
133
+ return {
134
+ valid: false,
135
+ value: looseMatch[1].toUpperCase(),
136
+ error: `Invalid VERDICT value "${looseMatch[1]}". Must be one of: APPROVE, REVISE, BLOCK`,
137
+ };
138
+ }
139
+ return {
140
+ valid: false,
141
+ value: null,
142
+ error: 'VERDICT section not found or malformed',
143
+ };
144
+ }
145
+
146
+ return {
147
+ valid: true,
148
+ value: match[1].toUpperCase(),
149
+ error: null,
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Validate CHECKS field format (key=value pairs).
155
+ *
156
+ * @param {string} text - Report text
157
+ * @returns {{valid: boolean, value: Object, error: string|null}}
158
+ */
159
+ export function validateChecks(text) {
160
+ const source = String(text ?? '');
161
+ const pattern = /CHECKS:\s*(.+?)(?:\n|$)/i;
162
+ const match = source.match(pattern);
163
+
164
+ if (!match) {
165
+ return {
166
+ valid: false,
167
+ value: {},
168
+ error: 'CHECKS field not found',
169
+ };
170
+ }
171
+
172
+ const checksStr = match[1].trim();
173
+ const checks = {};
174
+ const invalidParts = [];
175
+
176
+ for (const pair of checksStr.split(';')) {
177
+ const eq = pair.indexOf('=');
178
+ if (eq === -1) {
179
+ invalidParts.push(pair.trim());
180
+ continue;
181
+ }
182
+ const key = pair.slice(0, eq).trim();
183
+ const value = pair.slice(eq + 1).trim();
184
+ if (key) {
185
+ checks[key] = value;
186
+ }
187
+ }
188
+
189
+ if (invalidParts.length > 0) {
190
+ return {
191
+ valid: false,
192
+ value: checks,
193
+ error: `Invalid CHECKS format: "${invalidParts.join(', ')}". Expected key=value pairs separated by semicolons`,
194
+ };
195
+ }
196
+
197
+ return {
198
+ valid: true,
199
+ value: checks,
200
+ error: null,
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Validate a producer report against the producer schema.
206
+ *
207
+ * @param {string} text - Producer report text
208
+ * @param {Object} options - Additional options
209
+ * @param {string[]} options.requiredDeliverables - Contract deliverables required for this stage
210
+ * @returns {ContractValidationResult}
211
+ */
212
+ export function validateProducerReport(text, options = {}) {
213
+ const source = String(text ?? '');
214
+ const missingSections = [];
215
+ const invalidFields = [];
216
+ const extractedData = {};
217
+
218
+ // Check required sections
219
+ for (const section of PRODUCER_SCHEMA.requiredSections) {
220
+ if (!hasSectionStrict(source, section)) {
221
+ missingSections.push(section);
222
+ }
223
+ }
224
+
225
+ // Validate CHECKS field
226
+ const checksResult = validateChecks(source);
227
+ if (!checksResult.valid) {
228
+ invalidFields.push(`CHECKS: ${checksResult.error}`);
229
+ } else {
230
+ extractedData.checks = checksResult.value;
231
+ }
232
+
233
+ // Extract CONTRACT if deliverables are required
234
+ if (options.requiredDeliverables && options.requiredDeliverables.length > 0) {
235
+ if (!hasSectionStrict(source, 'CONTRACT')) {
236
+ missingSections.push('CONTRACT');
237
+ } else {
238
+ const contractContent = extractSectionContent(source, 'CONTRACT');
239
+ if (contractContent) {
240
+ extractedData.contractContent = contractContent;
241
+ }
242
+ }
243
+ }
244
+
245
+ const valid = missingSections.length === 0 && invalidFields.length === 0;
246
+
247
+ return {
248
+ valid,
249
+ missingSections,
250
+ invalidFields,
251
+ extractedData,
252
+ errorSummary: valid
253
+ ? null
254
+ : `Producer report contract violation: missing sections [${missingSections.join(', ')}], invalid fields [${invalidFields.join(', ')}]`,
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Validate a reviewer report against the reviewer schema.
260
+ *
261
+ * @param {string} text - Reviewer report text
262
+ * @param {Object} options - Additional options
263
+ * @param {string[]} options.scoringDimensions - Required scoring dimensions
264
+ * @returns {ContractValidationResult}
265
+ */
266
+ export function validateReviewerReport(text, options = {}) {
267
+ const source = String(text ?? '');
268
+ const missingSections = [];
269
+ const invalidFields = [];
270
+ const extractedData = {};
271
+
272
+ // Check required sections
273
+ for (const section of REVIEWER_SCHEMA.requiredSections) {
274
+ if (!hasSectionStrict(source, section)) {
275
+ missingSections.push(section);
276
+ }
277
+ }
278
+
279
+ // Validate VERDICT field (strict)
280
+ const verdictResult = validateVerdict(source);
281
+ if (!verdictResult.valid) {
282
+ invalidFields.push(`VERDICT: ${verdictResult.error}`);
283
+ } else {
284
+ extractedData.verdict = verdictResult.value;
285
+ }
286
+
287
+ // Validate CHECKS field
288
+ const checksResult = validateChecks(source);
289
+ if (!checksResult.valid) {
290
+ invalidFields.push(`CHECKS: ${checksResult.error}`);
291
+ } else {
292
+ extractedData.checks = checksResult.value;
293
+ }
294
+
295
+ // Check DIMENSIONS if scoring dimensions are required
296
+ if (options.scoringDimensions && options.scoringDimensions.length > 0) {
297
+ const dimsContent = extractSectionContent(source, 'DIMENSIONS');
298
+ if (!dimsContent && !hasSectionStrict(source, 'DIMENSIONS')) {
299
+ missingSections.push('DIMENSIONS');
300
+ } else if (dimsContent) {
301
+ extractedData.dimensionsContent = dimsContent;
302
+ }
303
+ }
304
+
305
+ const valid = missingSections.length === 0 && invalidFields.length === 0;
306
+
307
+ return {
308
+ valid,
309
+ missingSections,
310
+ invalidFields,
311
+ extractedData,
312
+ errorSummary: valid
313
+ ? null
314
+ : `Reviewer report contract violation: missing sections [${missingSections.join(', ')}], invalid fields [${invalidFields.join(', ')}]`,
315
+ };
316
+ }
317
+
318
+ /**
319
+ * Validate a global reviewer report against the global reviewer schema.
320
+ *
321
+ * @param {string} text - Global reviewer report text
322
+ * @param {Object} options - Additional options
323
+ * @param {string[]} options.requiredMacroQuestions - Required macro questions (Q1, Q2, etc.)
324
+ * @returns {ContractValidationResult}
325
+ */
326
+ export function validateGlobalReviewerReport(text, options = {}) {
327
+ const source = String(text ?? '');
328
+ const missingSections = [];
329
+ const invalidFields = [];
330
+ const extractedData = {};
331
+
332
+ // Check required sections
333
+ for (const section of GLOBAL_REVIEWER_SCHEMA.requiredSections) {
334
+ if (!hasSectionStrict(source, section)) {
335
+ missingSections.push(section);
336
+ }
337
+ }
338
+
339
+ // Validate VERDICT field (strict)
340
+ const verdictResult = validateVerdict(source);
341
+ if (!verdictResult.valid) {
342
+ invalidFields.push(`VERDICT: ${verdictResult.error}`);
343
+ } else {
344
+ extractedData.verdict = verdictResult.value;
345
+ }
346
+
347
+ // Check MACRO_ANSWERS completeness
348
+ if (options.requiredMacroQuestions && options.requiredMacroQuestions.length > 0) {
349
+ const macroContent = extractSectionContent(source, 'MACRO_ANSWERS');
350
+ if (!macroContent) {
351
+ missingSections.push('MACRO_ANSWERS');
352
+ } else {
353
+ const missingQuestions = [];
354
+ for (const q of options.requiredMacroQuestions) {
355
+ const qPattern = new RegExp(`\\b${q}\\b[^\\n]*`, 'i');
356
+ if (!qPattern.test(macroContent)) {
357
+ missingQuestions.push(q);
358
+ }
359
+ }
360
+ if (missingQuestions.length > 0) {
361
+ invalidFields.push(`MACRO_ANSWERS: missing answers for [${missingQuestions.join(', ')}]`);
362
+ } else {
363
+ extractedData.macroAnswersContent = macroContent;
364
+ }
365
+ }
366
+ }
367
+
368
+ const valid = missingSections.length === 0 && invalidFields.length === 0;
369
+
370
+ return {
371
+ valid,
372
+ missingSections,
373
+ invalidFields,
374
+ extractedData,
375
+ errorSummary: valid
376
+ ? null
377
+ : `Global reviewer report contract violation: missing sections [${missingSections.join(', ')}], invalid fields [${invalidFields.join(', ')}]`,
378
+ };
379
+ }
380
+
381
+ /**
382
+ * Validate all reports for a stage.
383
+ * Returns a consolidated validation result.
384
+ *
385
+ * @param {Object} reports - All role reports
386
+ * @param {string} reports.producer - Producer report text
387
+ * @param {string} reports.reviewerA - Reviewer A report text
388
+ * @param {string} reports.reviewerB - Reviewer B report text
389
+ * @param {string} [reports.globalReviewer] - Global reviewer report text (optional)
390
+ * @param {Object} options - Validation options
391
+ * @param {string[]} options.requiredDeliverables - Required contract deliverables
392
+ * @param {string[]} options.scoringDimensions - Required scoring dimensions
393
+ * @param {string[]} options.requiredMacroQuestions - Required macro questions for global reviewer
394
+ * @param {boolean} options.globalReviewerRequired - Whether global reviewer is required
395
+ * @returns {{valid: boolean, producer: ContractValidationResult, reviewerA: ContractValidationResult, reviewerB: ContractValidationResult, globalReviewer: ContractValidationResult|null, errorSummary: string|null}}
396
+ */
397
+ export function validateStageReports(reports, options = {}) {
398
+ const producer = validateProducerReport(reports.producer, {
399
+ requiredDeliverables: options.requiredDeliverables,
400
+ });
401
+
402
+ const reviewerA = validateReviewerReport(reports.reviewerA, {
403
+ scoringDimensions: options.scoringDimensions,
404
+ });
405
+
406
+ const reviewerB = validateReviewerReport(reports.reviewerB, {
407
+ scoringDimensions: options.scoringDimensions,
408
+ });
409
+
410
+ let globalReviewer = null;
411
+ if (options.globalReviewerRequired || reports.globalReviewer) {
412
+ globalReviewer = validateGlobalReviewerReport(reports.globalReviewer || '', {
413
+ requiredMacroQuestions: options.requiredMacroQuestions,
414
+ });
415
+ }
416
+
417
+ const allValid = producer.valid && reviewerA.valid && reviewerB.valid && (globalReviewer ? globalReviewer.valid : true);
418
+
419
+ const errors = [];
420
+ if (!producer.valid) errors.push(producer.errorSummary);
421
+ if (!reviewerA.valid) errors.push(reviewerA.errorSummary);
422
+ if (!reviewerB.valid) errors.push(reviewerB.errorSummary);
423
+ if (globalReviewer && !globalReviewer.valid) errors.push(globalReviewer.errorSummary);
424
+
425
+ return {
426
+ valid: allValid,
427
+ producer,
428
+ reviewerA,
429
+ reviewerB,
430
+ globalReviewer,
431
+ errorSummary: allValid ? null : errors.join('\n'),
432
+ };
433
+ }
434
+
435
+ // ============================================================================
436
+ // Output Quality Determination
437
+ // ============================================================================
438
+
439
+ /**
440
+ * Determine output quality level based on validation and metrics.
441
+ *
442
+ * SHADOW_COMPLETE criteria:
443
+ * - All reports pass contract validation
444
+ * - All reviewers APPROVE
445
+ * - No blockers
446
+ * - All required sections present
447
+ * - Dimensions meet threshold (if applicable)
448
+ * - Contract fulfilled (if applicable)
449
+ *
450
+ * PRODUCTION_READY criteria (in addition to SHADOW_COMPLETE):
451
+ * - CODE_EVIDENCE includes evidence_scope: both (cross-repo verification)
452
+ * - All scoring dimensions >= 4 (not just meeting threshold)
453
+ * - No OPEN_RISKS or OPEN_RISKS explicitly marked as "acceptable"
454
+ * - MACRO_ANSWERS all satisfied with concrete evidence references
455
+ *
456
+ * @param {Object} validation - Stage reports validation result
457
+ * @param {Object} metrics - Stage metrics from decideStage
458
+ * @param {Object} options - Additional options
459
+ * @param {number} options.productionThreshold - Minimum dimension score for production_ready (default: 4)
460
+ * @returns {{quality: string, reasons: string[]}}
461
+ */
462
+ export function determineOutputQuality(validation, metrics, options = {}) {
463
+ const productionThreshold = options.productionThreshold ?? 4;
464
+ const reasons = [];
465
+
466
+ // Check basic contract validation
467
+ if (!validation.valid) {
468
+ reasons.push('Reports do not satisfy contract validation');
469
+ return { quality: OUTPUT_QUALITY.NEEDS_WORK, reasons };
470
+ }
471
+
472
+ // Check approval status
473
+ if (metrics.approvalCount < (metrics.requiredApprovals ?? 2)) {
474
+ reasons.push(`Insufficient approvals: ${metrics.approvalCount}/${metrics.requiredApprovals ?? 2}`);
475
+ return { quality: OUTPUT_QUALITY.NEEDS_WORK, reasons };
476
+ }
477
+
478
+ // Check blockers
479
+ if (metrics.blockerCount > 0) {
480
+ reasons.push(`Unresolved blockers: ${metrics.blockerCount}`);
481
+ return { quality: OUTPUT_QUALITY.NEEDS_WORK, reasons };
482
+ }
483
+
484
+ // Check dimension failures
485
+ if (metrics.dimensionFailures && metrics.dimensionFailures.length > 0) {
486
+ reasons.push(`Dimension failures: ${metrics.dimensionFailures.join('; ')}`);
487
+ return { quality: OUTPUT_QUALITY.NEEDS_WORK, reasons };
488
+ }
489
+
490
+ // Check contract fulfillment
491
+ if (metrics.requiredDeliverables && metrics.requiredDeliverables.length > 0) {
492
+ if (!metrics.contractCheck || !metrics.contractCheck.allDone) {
493
+ reasons.push('Contract not fulfilled');
494
+ return { quality: OUTPUT_QUALITY.NEEDS_WORK, reasons };
495
+ }
496
+ }
497
+
498
+ // Check global reviewer requirements
499
+ if (metrics.globalReviewerRequired) {
500
+ if (!metrics.macroAnswersAllSatisfied) {
501
+ reasons.push('Macro answers not satisfied');
502
+ return { quality: OUTPUT_QUALITY.NEEDS_WORK, reasons };
503
+ }
504
+ }
505
+
506
+ // === SHADOW_COMPLETE threshold reached ===
507
+ // Now check for PRODUCTION_READY
508
+
509
+ const productionBlockers = [];
510
+
511
+ // Check CODE_EVIDENCE scope (production requires cross-repo verification)
512
+ // Must explicitly have evidence_scope: both for production ready
513
+ const producerScope = metrics.producerCodeEvidence?.evidenceScope;
514
+ if (!producerScope) {
515
+ productionBlockers.push('Producer CODE_EVIDENCE missing evidence_scope field (required: "both")');
516
+ } else if (producerScope !== 'both') {
517
+ productionBlockers.push(`Producer CODE_EVIDENCE scope is "${producerScope}", not "both"`);
518
+ }
519
+
520
+ // Check dimension scores for production threshold
521
+ // Check each reviewer separately - production requires ALL reviewers to score >= threshold
522
+ if (metrics.scoringDimensions && metrics.scoringDimensions.length > 0) {
523
+ for (const dim of metrics.scoringDimensions) {
524
+ const scoreA = metrics.reviewerADimensions?.[dim];
525
+ const scoreB = metrics.reviewerBDimensions?.[dim];
526
+ if (scoreA !== undefined && scoreA < productionThreshold) {
527
+ productionBlockers.push(`Dimension "${dim}" reviewer A scored ${scoreA}/5, below production threshold ${productionThreshold}`);
528
+ }
529
+ if (scoreB !== undefined && scoreB < productionThreshold) {
530
+ productionBlockers.push(`Dimension "${dim}" reviewer B scored ${scoreB}/5, below production threshold ${productionThreshold}`);
531
+ }
532
+ }
533
+ }
534
+
535
+ // If no production blockers, it's production ready
536
+ if (productionBlockers.length === 0) {
537
+ return { quality: OUTPUT_QUALITY.PRODUCTION_READY, reasons: [] };
538
+ }
539
+
540
+ // Otherwise it's shadow complete
541
+ return {
542
+ quality: OUTPUT_QUALITY.SHADOW_COMPLETE,
543
+ reasons: productionBlockers,
544
+ };
545
+ }
546
+
547
+ // ============================================================================
548
+ // Next Run Recommendation System
549
+ // ============================================================================
550
+
551
+ /**
552
+ * Next Run Types
553
+ */
554
+ export const NEXT_RUN_TYPE = {
555
+ NONE: 'none', // No follow-up run needed (production_ready)
556
+ CONTINUATION: 'continuation', // Continue work in same stage/spec
557
+ VERIFY: 'verify', // Verify the output meets higher standard
558
+ HANDOFF: 'handoff', // Hand off to different spec/team
559
+ };
560
+
561
+ /**
562
+ * Determine the recommended next run based on output quality and spec configuration.
563
+ *
564
+ * This is a GENERIC recommendation system, not PR2-specific.
565
+ * The spec can define:
566
+ * - verificationSpec: A separate spec to run for verification
567
+ * - continuationSpec: A separate spec to run for continuation
568
+ * - requireVerify: Whether shadow_complete requires verification
569
+ *
570
+ * SEMANTICS:
571
+ * - NEEDS_WORK + outcome=revise → CONTINUATION (continue current work)
572
+ * - NEEDS_WORK + outcome=halt → CONTINUATION or HANDOFF (recover from failure)
573
+ * - SHADOW_COMPLETE + spec.verificationSpec → VERIFY (run verification spec)
574
+ * - SHADOW_COMPLETE + no verificationSpec → CONTINUATION (improve to production_ready)
575
+ * - PRODUCTION_READY → NONE (no follow-up needed)
576
+ *
577
+ * @param {string} outputQuality - The output quality level
578
+ * @param {string} outcome - The stage outcome (advance/revise/halt)
579
+ * @param {Object} spec - The task spec
580
+ * @param {Object} options - Additional options
581
+ * @param {string[]} options.qualityReasons - Reasons for the quality level
582
+ * @returns {{type: string, spec: string|null, reasons: string[]}}
583
+ */
584
+ export function determineNextRunRecommendation(outputQuality, outcome, spec = {}, options = {}) {
585
+ const { qualityReasons = [] } = options;
586
+
587
+ // PRODUCTION_READY: No follow-up needed
588
+ if (outputQuality === OUTPUT_QUALITY.PRODUCTION_READY) {
589
+ return {
590
+ type: NEXT_RUN_TYPE.NONE,
591
+ spec: null,
592
+ reasons: ['Output is production-ready. No follow-up run needed.'],
593
+ };
594
+ }
595
+
596
+ // NEEDS_WORK: Requires continuation
597
+ if (outputQuality === OUTPUT_QUALITY.NEEDS_WORK) {
598
+ // If halted, might need handoff or fresh start
599
+ if (outcome === 'halt') {
600
+ // Check if spec defines a recovery spec
601
+ if (spec?.recoverySpec) {
602
+ return {
603
+ type: NEXT_RUN_TYPE.HANDOFF,
604
+ spec: spec.recoverySpec,
605
+ reasons: [
606
+ 'Stage halted without completion.',
607
+ ...qualityReasons,
608
+ `Consider recovery spec: ${spec.recoverySpec}`,
609
+ ],
610
+ };
611
+ }
612
+ // Otherwise recommend continuation to retry
613
+ return {
614
+ type: NEXT_RUN_TYPE.CONTINUATION,
615
+ spec: spec?.continuationSpec || null,
616
+ reasons: [
617
+ 'Stage halted without completion.',
618
+ ...qualityReasons,
619
+ 'Recommend retry with fresh context or adjusted parameters.',
620
+ ],
621
+ };
622
+ }
623
+
624
+ // revise or other: continue current work
625
+ return {
626
+ type: NEXT_RUN_TYPE.CONTINUATION,
627
+ spec: spec?.continuationSpec || null,
628
+ reasons: [
629
+ 'Output needs additional work.',
630
+ ...qualityReasons,
631
+ spec?.continuationSpec
632
+ ? `Recommend continuation spec: ${spec.continuationSpec}`
633
+ : 'Continue with current spec.',
634
+ ],
635
+ };
636
+ }
637
+
638
+ // SHADOW_COMPLETE: Check if verification is required
639
+ if (outputQuality === OUTPUT_QUALITY.SHADOW_COMPLETE) {
640
+ // If spec defines a verification spec, recommend verify
641
+ if (spec?.verificationSpec) {
642
+ return {
643
+ type: NEXT_RUN_TYPE.VERIFY,
644
+ spec: spec.verificationSpec,
645
+ reasons: [
646
+ 'Output is shadow-complete but not production-ready.',
647
+ ...qualityReasons,
648
+ `Recommend verification spec: ${spec.verificationSpec}`,
649
+ ],
650
+ };
651
+ }
652
+
653
+ // If spec explicitly requires verification for shadow_complete
654
+ if (spec?.requireVerify === true) {
655
+ return {
656
+ type: NEXT_RUN_TYPE.VERIFY,
657
+ spec: null, // Use same spec but in verify mode
658
+ reasons: [
659
+ 'Output is shadow-complete. Verification required by spec.',
660
+ ...qualityReasons,
661
+ ],
662
+ };
663
+ }
664
+
665
+ // No verification defined: recommend continuation to reach production_ready
666
+ return {
667
+ type: NEXT_RUN_TYPE.CONTINUATION,
668
+ spec: spec?.continuationSpec || null,
669
+ reasons: [
670
+ 'Output is shadow-complete but not production-ready.',
671
+ ...qualityReasons,
672
+ 'Recommend additional work to reach production-ready status.',
673
+ ],
674
+ };
675
+ }
676
+
677
+ // Fallback (should not reach here)
678
+ return {
679
+ type: NEXT_RUN_TYPE.NONE,
680
+ spec: null,
681
+ reasons: ['Unknown output quality level.'],
682
+ };
683
+ }