@boyingliu01/xp-gate 0.27.5 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/adapters/gate-9.sh +13 -2
  2. package/build-integrity/gate-10.ts +290 -263
  3. package/gate-10.sh +25 -6
  4. package/gate-12-file-hygiene.sh +35 -1
  5. package/gate-3.sh +78 -2
  6. package/gate-9.sh +13 -2
  7. package/hooks/gate-10.sh +25 -6
  8. package/hooks/gate-12-file-hygiene.sh +35 -1
  9. package/hooks/gate-3.sh +78 -2
  10. package/hooks/gate-9.sh +13 -2
  11. package/hooks/lib/audit-sanitize.sh +51 -0
  12. package/hooks/lib/ccn-ratchet.sh +183 -0
  13. package/hooks/lib/docs-only.sh +49 -0
  14. package/hooks/pre-commit +74 -15
  15. package/hooks/pre-push +19 -4
  16. package/lib/gate-audit.ts +9 -4
  17. package/lib/mattpocock-license-verdict.json +1 -1
  18. package/lib/retro.js +27 -6
  19. package/mock-policy/AGENTS.md +3 -3
  20. package/mutation/AGENTS.md +3 -3
  21. package/package.json +1 -1
  22. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  23. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  24. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  25. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  26. package/plugins/dsh/package.json +1 -1
  27. package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
  28. package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
  29. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
  30. package/plugins/opencode/package.json +1 -1
  31. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  32. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  33. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  34. package/plugins/qoder/plugin.json +1 -1
  35. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  36. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  37. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  38. package/principles/AGENTS.md +3 -3
  39. package/principles/__tests__/many-exports-reexport.test.ts +40 -0
  40. package/principles/adapters/typescript.ts +61 -30
  41. package/principles/boy-scout.ts +22 -17
  42. package/scripts/delphi-external-review.cjs +195 -132
  43. package/skills/delphi-review/AGENTS.md +3 -3
  44. package/skills/sprint-flow/AGENTS.md +3 -3
  45. package/skills/test-specification-alignment/AGENTS.md +3 -3
@@ -25,66 +25,87 @@ function isValidTimeoutMs(value) {
25
25
  return Number.isInteger(value) && value >= 1000 && value <= 600000;
26
26
  }
27
27
 
28
- function parseArgs(argv) {
28
+ // Flag -> args key. `null` marks flags that take no value. Keeping this as data
29
+ // means a new flag is one table row instead of one more switch arm (the switch
30
+ // had grown to ten arms plus a special case for --timeout-ms).
31
+ const VALUE_FLAGS = {
32
+ '--expert': 'expert',
33
+ '--input': 'input',
34
+ '--input-file': 'inputFile',
35
+ '--round': 'round',
36
+ '--config': 'config',
37
+ '--mode': 'mode',
38
+ '--profile': 'profile',
39
+ '--other-experts-file': 'otherExpertsFile',
40
+ };
41
+ const BOOLEAN_FLAGS = { '--fallback-local': 'fallbackLocal' };
42
+ const INTEGER_FLAGS = { '--round': (v) => parseInt(v) };
43
+
44
+ function collectRawArgs(argv) {
29
45
  const args = {};
30
46
  let timeoutMsRaw;
31
47
  for (let i = 0; i < argv.length; i++) {
32
- switch (argv[i]) {
33
- case '--expert': args.expert = argv[++i]; break;
34
- case '--input': args.input = argv[++i]; break;
35
- case '--input-file': args.inputFile = argv[++i]; break;
36
- case '--round': args.round = parseInt(argv[++i]); break;
37
- case '--config': args.config = argv[++i]; break;
38
- case '--mode': args.mode = argv[++i]; break;
39
- case '--profile': args.profile = argv[++i]; break;
40
- case '--other-experts-file': args.otherExpertsFile = argv[++i]; break;
41
- case '--timeout-ms': timeoutMsRaw = argv[++i]; break;
42
- case '--fallback-local': args.fallbackLocal = true; break;
43
- default: break;
44
- }
45
- }
48
+ const flag = argv[i];
49
+ if (flag === '--timeout-ms') { timeoutMsRaw = argv[++i]; continue; }
50
+ if (BOOLEAN_FLAGS[flag]) { args[BOOLEAN_FLAGS[flag]] = true; continue; }
51
+ const key = VALUE_FLAGS[flag];
52
+ if (!key) continue;
53
+ const value = argv[++i];
54
+ args[key] = INTEGER_FLAGS[flag] ? INTEGER_FLAGS[flag](value) : value;
55
+ }
56
+ return { args, timeoutMsRaw };
57
+ }
46
58
 
47
- // Validate required args
59
+ function fail(message) {
60
+ console.error(`[delphi-review] ERROR: ${message}`);
61
+ process.exit(1);
62
+ }
63
+
64
+ // Every rejection path used to inline its own console.error + exit. Collected
65
+ // here so the rules read as rules and each message stays verbatim.
66
+ function missingArgumentList(args) {
48
67
  const missing = [];
49
68
  if (!args.expert) missing.push('--expert');
50
69
  if (!args.input && !args.inputFile) missing.push('--input or --input-file');
51
70
  if (!args.round) missing.push('--round');
52
71
  if (!args.config) missing.push('--config');
72
+ return missing;
73
+ }
53
74
 
54
- if (missing.length > 0) {
55
- console.error(`[delphi-review] ERROR: Missing required arguments: ${missing.join(', ')}`);
56
- process.exit(1);
75
+ function applyTimeoutFlag(args, timeoutMsRaw) {
76
+ if (timeoutMsRaw === undefined) return;
77
+ const parsed = /^\d+$/.test(timeoutMsRaw) ? Number(timeoutMsRaw) : NaN;
78
+ if (!isValidTimeoutMs(parsed)) {
79
+ fail('--timeout-ms must be an integer between 1000 and 600000.');
57
80
  }
81
+ args.timeoutMs = parsed;
82
+ }
58
83
 
59
- // Validate expert role
60
- if (!VALID_EXPERTS.includes(args.expert)) {
61
- console.error(`[delphi-review] ERROR: Invalid expert role "${args.expert}". Must be one of: ${VALID_EXPERTS.join(', ')}`);
62
- process.exit(1);
84
+ function resolveMode(args) {
85
+ if (!args.mode) args.mode = 'design';
86
+ if (!VALID_MODES.includes(args.mode)) {
87
+ fail(`Invalid mode "${args.mode}". Must be one of: ${VALID_MODES.join(', ')}`);
63
88
  }
89
+ }
64
90
 
65
- // Validate mutual exclusivity of --input and --input-file
66
- if (args.input && args.inputFile) {
67
- console.error('[delphi-review] ERROR: --input and --input-file are mutually exclusive. Use only one.');
68
- process.exit(1);
91
+ function requireArgs(args, timeoutMsRaw) {
92
+ const missing = missingArgumentList(args);
93
+ if (missing.length > 0) {
94
+ fail(`Missing required arguments: ${missing.join(', ')}`);
69
95
  }
70
-
71
- // Validate --timeout-ms: integer within [1000, 600000]
72
- if (timeoutMsRaw !== undefined) {
73
- const parsed = /^\d+$/.test(timeoutMsRaw) ? Number(timeoutMsRaw) : NaN;
74
- if (!isValidTimeoutMs(parsed)) {
75
- console.error('[delphi-review] ERROR: --timeout-ms must be an integer between 1000 and 600000.');
76
- process.exit(1);
77
- }
78
- args.timeoutMs = parsed;
96
+ if (!VALID_EXPERTS.includes(args.expert)) {
97
+ fail(`Invalid expert role "${args.expert}". Must be one of: ${VALID_EXPERTS.join(', ')}`);
79
98
  }
80
-
81
- // Defaults
82
- if (!args.mode) args.mode = 'design';
83
- if (!VALID_MODES.includes(args.mode)) {
84
- console.error(`[delphi-review] ERROR: Invalid mode "${args.mode}". Must be one of: ${VALID_MODES.join(', ')}`);
85
- process.exit(1);
99
+ if (args.input && args.inputFile) {
100
+ fail('--input and --input-file are mutually exclusive. Use only one.');
86
101
  }
102
+ applyTimeoutFlag(args, timeoutMsRaw);
103
+ resolveMode(args);
104
+ }
87
105
 
106
+ function parseArgs(argv) {
107
+ const { args, timeoutMsRaw } = collectRawArgs(argv);
108
+ requireArgs(args, timeoutMsRaw);
88
109
  return args;
89
110
  }
90
111
 
@@ -223,61 +244,75 @@ function readConfig(configPath, profileOverride) {
223
244
  // ── Distinct-model validation ───────────────────────────────────────────
224
245
  const REQUIRED_EXPERT_ROLES = ['architecture', 'technical', 'feasibility'];
225
246
 
226
- function validateDistinctModels(experts, providers, consensus = {}) {
227
- const expertMap = experts && typeof experts === 'object' ? experts : {};
228
- const providerMap = providers && typeof providers === 'object' ? providers : {};
229
- const expertRoles = Object.keys(expertMap);
230
- const normalizedModels = [];
247
+ function isNonEmptyString(value) {
248
+ return typeof value === 'string' && value.trim() !== '';
249
+ }
231
250
 
251
+ // Roles must be EXACTLY the three required ones: a missing role means the
252
+ // review would silently run with two experts, an extra one means the config
253
+ // no longer matches the three-model policy. One pass reports both.
254
+ function collectModelAssignments(expertMap) {
255
+ const assignments = [];
232
256
  for (const role of REQUIRED_EXPERT_ROLES) {
233
- const expert = expertRoles.includes(role) ? expertMap[role] : undefined;
234
- if (!expert) {
235
- return { valid: false, reason: `Missing required expert role: ${role}.` };
236
- }
237
- if (typeof expert.model !== 'string' || expert.model.trim() === '') {
238
- return { valid: false, reason: `Expert ${role} must define a non-empty model.` };
257
+ const expert = expertMap[role];
258
+ if (!expert) return { error: `Missing required expert role: ${role}.` };
259
+ if (!isNonEmptyString(expert.model)) {
260
+ return { error: `Expert ${role} must define a non-empty model.` };
239
261
  }
240
- normalizedModels.push({ role, model: expert.model.trim() });
241
- }
242
-
243
- const unexpectedRole = expertRoles.find(role => !REQUIRED_EXPERT_ROLES.includes(role));
244
- if (unexpectedRole) {
245
- return { valid: false, reason: `Unsupported expert role: ${unexpectedRole}.` };
262
+ assignments.push({ role, model: expert.model.trim() });
246
263
  }
264
+ const unexpectedRole = Object.keys(expertMap).find(role => !REQUIRED_EXPERT_ROLES.includes(role));
265
+ if (unexpectedRole) return { error: `Unsupported expert role: ${unexpectedRole}.` };
266
+ return { assignments };
267
+ }
247
268
 
269
+ function findDuplicateModel(assignments) {
248
270
  const seen = new Map();
249
- for (const assignment of normalizedModels) {
250
- const previousRole = seen.get(assignment.model);
271
+ for (const { role, model } of assignments) {
272
+ const previousRole = seen.get(model);
251
273
  if (previousRole) {
252
- return {
253
- valid: false,
254
- reason: `duplicate model "${assignment.model}" assigned to ${previousRole} and ${assignment.role}.`,
255
- };
274
+ return `duplicate model "${model}" assigned to ${previousRole} and ${role}.`;
256
275
  }
257
- seen.set(assignment.model, assignment.role);
276
+ seen.set(model, role);
277
+ }
278
+ return null;
279
+ }
280
+
281
+ // Everything an expert's provider must carry to be callable. Returns the
282
+ // human-readable reason instead of throwing, so the caller keeps one shape.
283
+ function providerConfigError(role, expert, providerMap) {
284
+ if (!isNonEmptyString(expert.provider) || expert.provider === 'local') {
285
+ return `Expert ${role} must define a non-local callable provider.`;
286
+ }
287
+ const provider = providerMap[expert.provider];
288
+ if (!provider || typeof provider !== 'object' || Array.isArray(provider)) {
289
+ return `Expert ${role} provider configuration must be an object.`;
290
+ }
291
+ if (!isNonEmptyString(provider.base_url)) {
292
+ return `Expert ${role} provider must define a non-empty base_url.`;
258
293
  }
294
+ if (!isNonEmptyString(provider.api_key)) {
295
+ return `Expert ${role} provider must define a non-empty api_key.`;
296
+ }
297
+ if (provider.timeout_ms !== undefined && !isValidTimeoutMs(provider.timeout_ms)) {
298
+ return `Expert ${role} provider ${expert.provider} timeout_ms must be an integer between 1000 and 600000.`;
299
+ }
300
+ return null;
301
+ }
302
+
303
+ function validateDistinctModels(experts, providers, consensus = {}) {
304
+ const expertMap = experts && typeof experts === 'object' ? experts : {};
305
+ const providerMap = providers && typeof providers === 'object' ? providers : {};
306
+
307
+ const collected = collectModelAssignments(expertMap);
308
+ if (collected.error) return { valid: false, reason: collected.error };
309
+
310
+ const duplicate = findDuplicateModel(collected.assignments);
311
+ if (duplicate) return { valid: false, reason: duplicate };
259
312
 
260
313
  for (const role of REQUIRED_EXPERT_ROLES) {
261
- const expert = expertMap[role];
262
- if (typeof expert.provider !== 'string' || expert.provider.trim() === '' || expert.provider === 'local') {
263
- return { valid: false, reason: `Expert ${role} must define a non-local callable provider.` };
264
- }
265
- const provider = providerMap[expert.provider];
266
- if (!provider || typeof provider !== 'object' || Array.isArray(provider)) {
267
- return { valid: false, reason: `Expert ${role} provider configuration must be an object.` };
268
- }
269
- if (typeof provider.base_url !== 'string' || provider.base_url.trim() === '') {
270
- return { valid: false, reason: `Expert ${role} provider must define a non-empty base_url.` };
271
- }
272
- if (typeof provider.api_key !== 'string' || provider.api_key.trim() === '') {
273
- return { valid: false, reason: `Expert ${role} provider must define a non-empty api_key.` };
274
- }
275
- if (provider.timeout_ms !== undefined && !isValidTimeoutMs(provider.timeout_ms)) {
276
- return {
277
- valid: false,
278
- reason: `Expert ${role} provider ${expert.provider} timeout_ms must be an integer between 1000 and 600000.`,
279
- };
280
- }
314
+ const error = providerConfigError(role, expertMap[role], providerMap);
315
+ if (error) return { valid: false, reason: error };
281
316
  }
282
317
 
283
318
  if (consensus.cross_provider_required === true) {
@@ -454,25 +489,32 @@ function describePromptBudgetFailure({ bytes, maxBytes, expert }) {
454
489
  // routed seat, and a ~60 byte smoke probe on the "dead" seat passed in 7s.
455
490
  // The optional per-seat `prompt_calibration` record carries the measured
456
491
  // bounds so failures can be classified against evidence instead of guessed.
492
+ // A calibration record is only usable when it carries a measured PASS bound
493
+ // and, if it also carries a FAIL bound, that bound is strictly larger. Each
494
+ // broken shape names itself so the operator knows which field to correct.
495
+ const CALIBRATION_SHAPE_HINT = 'needs measured_at and a finite last_pass_bytes > 0 (last_fail_bytes, if present, must be larger)';
496
+
497
+ function isPositiveFinite(value) {
498
+ return Number.isFinite(value) && value > 0;
499
+ }
500
+
501
+ function calibrationProblem(raw) {
502
+ if (!isPlainObject(raw)) return 'must be an object';
503
+ const pass = raw.last_pass_bytes;
504
+ const fail = raw.last_fail_bytes;
505
+ const failOk = fail === undefined || isPositiveFinite(fail);
506
+ if (!isPositiveFinite(pass) || !failOk) return CALIBRATION_SHAPE_HINT;
507
+ if (fail !== undefined && !(fail > pass)) return 'has last_fail_bytes <= last_pass_bytes';
508
+ if (!isNonEmptyString(raw.measured_at)) return CALIBRATION_SHAPE_HINT;
509
+ return null;
510
+ }
511
+
457
512
  function resolveSeatCalibration(expertConfig) {
458
513
  const raw = expertConfig && expertConfig.prompt_calibration;
459
514
  if (raw === undefined) return { calibration: null };
460
- const warn = (detail) => {
461
- console.error(`[delphi-review] WARNING: prompt_calibration ${detail}; ignoring the record.`);
462
- };
463
- if (!isPlainObject(raw)) {
464
- warn('must be an object');
465
- return { calibration: null, warning: 'prompt_calibration_malformed' };
466
- }
467
- const hasPass = Number.isFinite(raw.last_pass_bytes) && raw.last_pass_bytes > 0;
468
- const hasFail = raw.last_fail_bytes === undefined
469
- || (Number.isFinite(raw.last_fail_bytes) && raw.last_fail_bytes > 0);
470
- const ordered = hasPass && hasFail
471
- && (raw.last_fail_bytes === undefined || raw.last_fail_bytes > raw.last_pass_bytes);
472
- if (!hasPass || !hasFail || typeof raw.measured_at !== 'string' || raw.measured_at.trim() === '' || !ordered) {
473
- warn(raw.last_fail_bytes !== undefined && hasPass && raw.last_fail_bytes <= raw.last_pass_bytes
474
- ? 'has last_fail_bytes <= last_pass_bytes'
475
- : 'needs measured_at and a finite last_pass_bytes > 0 (last_fail_bytes, if present, must be larger)');
515
+ const problem = calibrationProblem(raw);
516
+ if (problem) {
517
+ console.error(`[delphi-review] WARNING: prompt_calibration ${problem}; ignoring the record.`);
476
518
  return { calibration: null, warning: 'prompt_calibration_malformed' };
477
519
  }
478
520
  return {
@@ -492,40 +534,61 @@ function resolveSeatCalibration(expertConfig) {
492
534
  // sends operators chasing a cause that is not there (#481). Everything else
493
535
  // gets a stable error_type so orchestrators can assert on the class instead of
494
536
  // parsing prose.
537
+ // The calibrated ceiling for a seat, or null when the seat carries no usable
538
+ // record. Null is meaningful: it is the "no evidence to reclassify on" branch.
539
+ function calibratedCeiling(calibration) {
540
+ if (calibration && Number.isFinite(calibration.last_pass_bytes)) return calibration.last_pass_bytes;
541
+ return null;
542
+ }
543
+
544
+ // `error_kind` is the primary signal; the message literal stays as a
545
+ // backward-compatible fallback for callers that hand-build results.
546
+ function isPreResponseDrop(result) {
547
+ return !result.retryable
548
+ && (result.error_kind === 'pre_response_drop' || result.message === 'Network error.');
549
+ }
550
+
551
+ function sizeExceededResult(result, promptBytes, threshold) {
552
+ return {
553
+ ...result,
554
+ error_type: 'SIZE_EXCEEDED',
555
+ retryable: false,
556
+ bytes: promptBytes,
557
+ calibrated_pass_bytes: threshold,
558
+ message: `Request dropped before any response: ${promptBytes} bytes exceeds this seat's calibrated passing size `
559
+ + `(${threshold} bytes). Classified SIZE_EXCEEDED, not a network fault — trim the payload or recalibrate the seat.`,
560
+ };
561
+ }
562
+
563
+ // No calibration means no evidence to reclassify on — but an uncalibrated
564
+ // seat dying on a full-budget payload is exactly how #481 started. Keep the
565
+ // class honest (network_error) and point the operator at the protocol.
566
+ function uncalibratedBudgetHint(result, promptBytes) {
567
+ return {
568
+ ...result,
569
+ error_type: 'network_error',
570
+ bytes: promptBytes,
571
+ message: `${result.message} Note: this seat has no prompt_calibration record and the payload is at the `
572
+ + 'default budget — this pattern matches an uncalibrated size ceiling. Recalibrate per the #481 protocol.',
573
+ };
574
+ }
575
+
495
576
  function classifySeatFailure({ result, promptBytes, calibration }) {
496
577
  if (!result || !result.error) return result;
497
- const threshold = calibration && Number.isFinite(calibration.last_pass_bytes)
498
- ? calibration.last_pass_bytes
499
- : null;
500
- // `error_kind` is the primary signal; the message literal stays as a
501
- // backward-compatible fallback for callers that hand-build results.
502
- const isPreResponseDrop = !result.retryable
503
- && (result.error_kind === 'pre_response_drop' || result.message === 'Network error.');
504
- if (isPreResponseDrop && threshold !== null && Number.isFinite(promptBytes) && promptBytes > threshold) {
505
- return {
506
- ...result,
507
- error_type: 'SIZE_EXCEEDED',
508
- retryable: false,
509
- bytes: promptBytes,
510
- calibrated_pass_bytes: threshold,
511
- message: `Request dropped before any response: ${promptBytes} bytes exceeds this seat's calibrated passing size `
512
- + `(${threshold} bytes). Classified SIZE_EXCEEDED, not a network fault — trim the payload or recalibrate the seat.`,
513
- };
578
+ if (!isPreResponseDrop(result)) {
579
+ return { ...result, error_type: result.retryable ? 'retryable_error' : 'network_error' };
514
580
  }
515
- if (isPreResponseDrop && threshold === null && Number.isFinite(promptBytes)
516
- && promptBytes >= DEFAULT_PROMPT_BUDGET_BYTES) {
517
- // No calibration means no evidence to reclassify on — but an uncalibrated
518
- // seat dying on a full-budget payload is exactly how #481 started. Keep the
519
- // class honest (network_error) and point the operator at the protocol.
520
- return {
521
- ...result,
522
- error_type: 'network_error',
523
- bytes: promptBytes,
524
- message: `${result.message} Note: this seat has no prompt_calibration record and the payload is at the `
525
- + 'default budget — this pattern matches an uncalibrated size ceiling. Recalibrate per the #481 protocol.',
526
- };
581
+ const threshold = calibratedCeiling(calibration);
582
+ if (!Number.isFinite(promptBytes)) {
583
+ return { ...result, error_type: 'network_error' };
584
+ }
585
+ if (threshold !== null && promptBytes > threshold) {
586
+ return sizeExceededResult(result, promptBytes, threshold);
587
+ }
588
+ if (threshold === null && promptBytes >= DEFAULT_PROMPT_BUDGET_BYTES) {
589
+ return uncalibratedBudgetHint(result, promptBytes);
527
590
  }
528
- return { ...result, error_type: result.retryable ? 'retryable_error' : 'network_error' };
591
+ return { ...result, error_type: 'network_error' };
529
592
  }
530
593
 
531
594
  // ── Schema retry (#414 REQ-5) ──────────────────────────────────────────
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-18
4
- **Commit:** 41792f89
3
+ **Generated:** 2026-10-19
4
+ **Commit:** 07d3036f
5
5
  **Branch:** main
6
- **Version:** 0.27.5.0
6
+ **Version:** 0.28.0.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-18
4
- **Commit:** 41792f89
3
+ **Generated:** 2026-10-19
4
+ **Commit:** 07d3036f
5
5
  **Branch:** main
6
- **Version:** 0.27.5.0
6
+ **Version:** 0.28.0.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-18
4
- **Commit:** 41792f89
3
+ **Generated:** 2026-10-19
4
+ **Commit:** 07d3036f
5
5
  **Branch:** main
6
- **Version:** 0.27.5.0
6
+ **Version:** 0.28.0.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.