@ai-dev-methodologies/rlp-desk 0.10.0 → 0.11.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.
- package/docs/blueprints/sv-architecture-rethink.md +84 -0
- package/docs/multi-mission-orchestration.md +154 -0
- package/docs/plans/rlp-desk-0.11-handoff-7fixes.md +352 -0
- package/docs/plans/rlp-desk-elegant-papert-agent-a8cd695ffca2a3ad8.md +84 -0
- package/docs/plans/rlp-desk-elegant-papert.md +270 -0
- package/docs/protocol-reference.md +82 -0
- package/package.json +1 -1
- package/src/commands/rlp-desk.md +5 -0
- package/src/governance.md +160 -0
- package/src/node/reporting/campaign-reporting.mjs +4 -0
- package/src/node/run.mjs +23 -1
- package/src/node/runner/campaign-main-loop.mjs +284 -10
- package/docs/superpowers/plans/2026-04-12-flywheel-redesign.md +0 -704
- package/docs/superpowers/specs/2026-04-12-flywheel-redesign.md +0 -161
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
|
|
25
25
|
const execFileAsync = promisify(execFile);
|
|
26
26
|
const REQUIRED_SCAFFOLD_NAMES = ['workerPrompt', 'verifierPrompt', 'memoryFile', 'prdFile', 'testSpecFile'];
|
|
27
|
-
const CLAUDE_MODELS = new Set(['haiku', 'sonnet', 'opus']);
|
|
28
27
|
const MODEL_UPGRADES = {
|
|
29
28
|
'gpt-5.5:medium': 'gpt-5.5:high',
|
|
30
29
|
'gpt-5.5:high': 'gpt-5.5:xhigh',
|
|
@@ -65,6 +64,7 @@ function buildPaths(rootDir, slug) {
|
|
|
65
64
|
flywheelSignalFile: path.join(deskRoot, 'memos', `${slug}-flywheel-signal.json`),
|
|
66
65
|
flywheelGuardPromptFile: path.join(deskRoot, 'prompts', `${slug}.flywheel-guard.prompt.md`),
|
|
67
66
|
flywheelGuardVerdictFile: path.join(deskRoot, 'memos', `${slug}-flywheel-guard-verdict.json`),
|
|
67
|
+
laneAuditFile: path.join(campaignLogDir, 'lane-audit.json'),
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -254,6 +254,10 @@ async function readCurrentState(paths, slug, options) {
|
|
|
254
254
|
final_verifier_model: status.final_verifier_model ?? options.finalVerifierModel ?? 'opus',
|
|
255
255
|
verified_us: status.verified_us ?? [],
|
|
256
256
|
consecutive_failures: status.consecutive_failures ?? 0,
|
|
257
|
+
// US-021 R9 P2-I consecutive_blocks counter (governance §8). Tracks repeated
|
|
258
|
+
// same-canonical-reason worker blocks; verify_fail uses consecutive_failures.
|
|
259
|
+
consecutive_blocks: status.consecutive_blocks ?? 0,
|
|
260
|
+
last_block_reason: status.last_block_reason ?? '',
|
|
257
261
|
current_us: status.current_us ?? null,
|
|
258
262
|
session_name: status.session_name ?? null,
|
|
259
263
|
leader_pane_id: status.leader_pane_id ?? null,
|
|
@@ -335,9 +339,171 @@ async function dispatchVerifier({
|
|
|
335
339
|
return promptFile;
|
|
336
340
|
}
|
|
337
341
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
342
|
+
// P1-E Lane Enforcement (governance §7e). WARN-only by default; opt-in
|
|
343
|
+
// strict escalates lane violations to BLOCKED with downgraded action
|
|
344
|
+
// (recoverable=true, retry_after_fix). audit log file is initialized to
|
|
345
|
+
// `[]` so the file always exists, simplifying wrapper polling.
|
|
346
|
+
async function _initLaneAuditLog(paths) {
|
|
347
|
+
await fs.mkdir(path.dirname(paths.laneAuditFile), { recursive: true });
|
|
348
|
+
if (!(await exists(paths.laneAuditFile))) {
|
|
349
|
+
await fs.writeFile(paths.laneAuditFile, '[]\n', 'utf8');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// US-020 R8 P1-H Blocked exit hygiene (governance §1f, 5th channel).
|
|
354
|
+
// Worker must update memory.md (Blocking History) and latest.md (Known Issues)
|
|
355
|
+
// before signalling blocked. We compare mtimes against `now`; either file older
|
|
356
|
+
// than 5 minutes means the worker skipped the hygiene step. Returns true when violated.
|
|
357
|
+
async function _checkBlockedHygiene(paths, now = Date.now()) {
|
|
358
|
+
const threshold = 5 * 60 * 1000; // 5 minutes
|
|
359
|
+
const targets = [paths.memoryFile, paths.contextFile].filter(Boolean);
|
|
360
|
+
for (const file of targets) {
|
|
361
|
+
try {
|
|
362
|
+
const stat = await fs.stat(file);
|
|
363
|
+
if (now - stat.mtimeMs > threshold) {
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
} catch {
|
|
367
|
+
// Missing file counts as violated — worker had nothing to update.
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function _snapshotLaneMtimes(paths) {
|
|
375
|
+
// PRD / test-spec are read-only artifacts the worker MUST NOT modify.
|
|
376
|
+
// memos and context are leader-owned; worker writes them via signal
|
|
377
|
+
// files only, never by direct edit.
|
|
378
|
+
const targets = [paths.prdFile, paths.testSpecFile, paths.contextFile];
|
|
379
|
+
const snapshot = {};
|
|
380
|
+
for (const file of targets) {
|
|
381
|
+
try {
|
|
382
|
+
const stat = await fs.stat(file);
|
|
383
|
+
snapshot[file] = stat.mtimeMs;
|
|
384
|
+
} catch {
|
|
385
|
+
snapshot[file] = null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return snapshot;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function _checkLaneViolations(paths, snapshotBefore, snapshotAfter, state, options) {
|
|
392
|
+
const violations = [];
|
|
393
|
+
for (const [file, before] of Object.entries(snapshotBefore)) {
|
|
394
|
+
const after = snapshotAfter[file];
|
|
395
|
+
if (before !== null && after !== null && after !== before) {
|
|
396
|
+
violations.push({
|
|
397
|
+
file,
|
|
398
|
+
mtime_before: before,
|
|
399
|
+
mtime_after: after,
|
|
400
|
+
iter: state.iteration ?? 0,
|
|
401
|
+
lane_mode: options.laneStrict ? 'strict' : 'warn',
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (violations.length === 0) return null;
|
|
406
|
+
// Append to audit log (best-effort).
|
|
407
|
+
try {
|
|
408
|
+
const existing = JSON.parse(await fs.readFile(paths.laneAuditFile, 'utf8'));
|
|
409
|
+
await fs.writeFile(paths.laneAuditFile, `${JSON.stringify([...existing, ...violations], null, 2)}\n`, 'utf8');
|
|
410
|
+
} catch {
|
|
411
|
+
// log file corrupted or missing — re-initialize and write fresh entries.
|
|
412
|
+
await fs.writeFile(paths.laneAuditFile, `${JSON.stringify(violations, null, 2)}\n`, 'utf8');
|
|
413
|
+
}
|
|
414
|
+
return violations;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// P1-D Cross-US dependency token list (governance §1f). Keep in sync with
|
|
418
|
+
// the zsh helper _classify_cross_us_or_metric in lib_ralph_desk.zsh.
|
|
419
|
+
const CROSS_US_TOKEN_RE = /depends on US-|blocking US-|awaits US-|post-iter US-|requires US-\d+|cross-US|US-\d+ 산출물|신규 US-|post-iter/i;
|
|
420
|
+
|
|
421
|
+
// P1-D Failure Taxonomy classifier. governance §1f locks the 6 reason_category
|
|
422
|
+
// values + recoverable + suggested_action defaults per source. wrapper MUST
|
|
423
|
+
// branch on reason_category; failure_category is diagnostic only.
|
|
424
|
+
function _classifyBlock(source, { verdict, state, slug } = {}) {
|
|
425
|
+
let category;
|
|
426
|
+
let recoverable;
|
|
427
|
+
let action;
|
|
428
|
+
let failureCategory = null;
|
|
429
|
+
switch (source) {
|
|
430
|
+
case 'flywheel_inconclusive':
|
|
431
|
+
case 'flywheel_exhausted':
|
|
432
|
+
category = 'mission_abort';
|
|
433
|
+
recoverable = false;
|
|
434
|
+
action = 'terminal_alert';
|
|
435
|
+
break;
|
|
436
|
+
case 'model_upgrade':
|
|
437
|
+
category = 'repeat_axis';
|
|
438
|
+
recoverable = false;
|
|
439
|
+
action = 'next_mission_chain';
|
|
440
|
+
break;
|
|
441
|
+
case 'verifier': {
|
|
442
|
+
const text = `${verdict?.reason ?? ''} ${verdict?.summary ?? ''}`;
|
|
443
|
+
category = CROSS_US_TOKEN_RE.test(text) ? 'cross_us_dep' : 'metric_failure';
|
|
444
|
+
recoverable = true;
|
|
445
|
+
action = 'retry_after_fix';
|
|
446
|
+
failureCategory = verdict?.failure_category ?? null;
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
default:
|
|
450
|
+
category = 'metric_failure';
|
|
451
|
+
recoverable = false;
|
|
452
|
+
action = 'terminal_alert';
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
reason_category: category,
|
|
456
|
+
failure_category: failureCategory,
|
|
457
|
+
recoverable,
|
|
458
|
+
suggested_action: action,
|
|
459
|
+
iteration: state?.iteration ?? 0,
|
|
460
|
+
slug,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function writeSentinel(filePath, status, usId, reason, classification = null, paths = null) {
|
|
465
|
+
// governance §1f BLOCKED Surfacing: BLOCKED is surfaced on FIVE channels —
|
|
466
|
+
// sentinel (markdown + JSON sidecar), status, console (stderr), report,
|
|
467
|
+
// and (US-020 R8 P1-H, 5th channel) memory.md/latest.md hygiene update.
|
|
468
|
+
// Legacy 1-line parsers still work because line 1 is unchanged.
|
|
469
|
+
const lines = [`${status.toUpperCase()}: ${usId}`];
|
|
470
|
+
if (reason) lines.push(`Reason: ${reason}`);
|
|
471
|
+
if (classification?.reason_category) {
|
|
472
|
+
lines.push(`Category: ${classification.reason_category}`);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// P1-D Write Order Contract:
|
|
476
|
+
// 1. JSON sidecar FIRST (atomic per-file rename via writeFile).
|
|
477
|
+
// 2. markdown sentinel SECOND.
|
|
478
|
+
// Invariant: markdown exists ⇒ JSON exists. Wrappers watch markdown,
|
|
479
|
+
// then read JSON; if JSON not yet visible (rare race), retry up to 5×50ms.
|
|
480
|
+
if (status === 'blocked' && classification) {
|
|
481
|
+
const jsonPath = filePath.replace(/\.md$/, '.json');
|
|
482
|
+
let hygieneViolated = false;
|
|
483
|
+
if (paths) {
|
|
484
|
+
try {
|
|
485
|
+
hygieneViolated = await _checkBlockedHygiene(paths);
|
|
486
|
+
} catch {
|
|
487
|
+
hygieneViolated = false;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const jsonBody = {
|
|
491
|
+
schema_version: '2.0',
|
|
492
|
+
slug: classification.slug ?? null,
|
|
493
|
+
us_id: usId,
|
|
494
|
+
blocked_at_iter: classification.iteration ?? 0,
|
|
495
|
+
blocked_at_utc: new Date().toISOString(),
|
|
496
|
+
reason_category: classification.reason_category,
|
|
497
|
+
reason_detail: reason ?? null,
|
|
498
|
+
failure_category: classification.failure_category ?? null,
|
|
499
|
+
recoverable: classification.recoverable ?? false,
|
|
500
|
+
suggested_action: classification.suggested_action ?? 'terminal_alert',
|
|
501
|
+
meta: { blocked_hygiene_violated: hygieneViolated },
|
|
502
|
+
};
|
|
503
|
+
await fs.writeFile(jsonPath, `${JSON.stringify(jsonBody, null, 2)}\n`, 'utf8');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
await fs.writeFile(filePath, `${lines.join('\n')}\n`, 'utf8');
|
|
341
507
|
}
|
|
342
508
|
|
|
343
509
|
async function runFinalSequentialVerify({
|
|
@@ -457,6 +623,9 @@ export async function run(slug, options = {}) {
|
|
|
457
623
|
analyticsFile: paths.analyticsFile,
|
|
458
624
|
statusFile: paths.statusFile,
|
|
459
625
|
});
|
|
626
|
+
// P1-E Lane Enforcement: initialize audit log to `[]` so the file always
|
|
627
|
+
// exists. Wrappers can then poll/tail without ENOENT special-cases.
|
|
628
|
+
await _initLaneAuditLog(paths);
|
|
460
629
|
|
|
461
630
|
if (await exists(paths.blockedSentinel)) {
|
|
462
631
|
throw new Error(`Campaign ${slug} is blocked. Run clean first.`);
|
|
@@ -496,7 +665,49 @@ export async function run(slug, options = {}) {
|
|
|
496
665
|
|
|
497
666
|
let fixContractPath = null;
|
|
498
667
|
|
|
668
|
+
// P1-E Lane Enforcement: snapshot lane mtimes before each iteration,
|
|
669
|
+
// compare at the top of the next iteration. Drift on read-only artifacts
|
|
670
|
+
// (PRD, test-spec, context) emits a lane_violation_warning event + audit
|
|
671
|
+
// log entry. governance §7e. Strict mode escalation hook is wired below
|
|
672
|
+
// (sentinel BLOCKED with infra_failure + recoverable=true downgrade).
|
|
673
|
+
let _laneSnapshot = await _snapshotLaneMtimes(paths);
|
|
674
|
+
|
|
499
675
|
while (state.iteration <= maxIterations) {
|
|
676
|
+
// Audit drift from the prior iteration before doing anything new.
|
|
677
|
+
const _laneSnapshotAfter = await _snapshotLaneMtimes(paths);
|
|
678
|
+
const _laneViolations = await _checkLaneViolations(paths, _laneSnapshot, _laneSnapshotAfter, state, options);
|
|
679
|
+
if (_laneViolations) {
|
|
680
|
+
for (const v of _laneViolations) {
|
|
681
|
+
await appendIterationAnalytics(paths, state, state.current_us ?? 'ALL', 'lane_violation_warning', { ...options, lane_violation: v });
|
|
682
|
+
}
|
|
683
|
+
if (options.laneStrict) {
|
|
684
|
+
// Strict mode: escalate to BLOCKED with downgrade
|
|
685
|
+
// (recoverable=true, retry_after_fix). governance §7e justifies
|
|
686
|
+
// the downgrade — the mtime audit is best-effort and should not
|
|
687
|
+
// terminally kill a campaign.
|
|
688
|
+
state.phase = 'blocked';
|
|
689
|
+
const laneReason = `lane_violation: ${_laneViolations.length} read-only artifact(s) modified during prior iteration`;
|
|
690
|
+
const laneClassification = {
|
|
691
|
+
reason_category: 'infra_failure',
|
|
692
|
+
failure_category: null,
|
|
693
|
+
recoverable: true,
|
|
694
|
+
suggested_action: 'retry_after_fix',
|
|
695
|
+
iteration: state.iteration,
|
|
696
|
+
slug,
|
|
697
|
+
};
|
|
698
|
+
await writeSentinel(paths.blockedSentinel, 'blocked', state.current_us ?? 'ALL', laneReason, laneClassification, paths);
|
|
699
|
+
await writeStatus(paths, state, options.onStatusChange, options.now);
|
|
700
|
+
return {
|
|
701
|
+
status: 'blocked',
|
|
702
|
+
usId: state.current_us ?? 'ALL',
|
|
703
|
+
reason: laneReason,
|
|
704
|
+
category: 'infra_failure',
|
|
705
|
+
statusFile: paths.statusFile,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
_laneSnapshot = _laneSnapshotAfter;
|
|
710
|
+
|
|
500
711
|
state.current_us = getNextUs(usList, state.verified_us, state.current_us);
|
|
501
712
|
if (state.current_us === 'ALL') {
|
|
502
713
|
const finalResult = await runFinalSequentialVerify({
|
|
@@ -576,6 +787,11 @@ export async function run(slug, options = {}) {
|
|
|
576
787
|
});
|
|
577
788
|
|
|
578
789
|
state.last_flywheel_decision = flywheelSignal.decision;
|
|
790
|
+
// P0-A multi-mission orchestration: optionally captured from flywheel signal.
|
|
791
|
+
// null when the flywheel did not suggest a next mission. Consumer wrappers
|
|
792
|
+
// poll status.next_mission_candidate to chain missions without code edits.
|
|
793
|
+
// See docs/multi-mission-orchestration.md.
|
|
794
|
+
state.next_mission_candidate = flywheelSignal.next_mission_candidate ?? null;
|
|
579
795
|
await fs.unlink(paths.flywheelSignalFile).catch(() => {});
|
|
580
796
|
|
|
581
797
|
// Flywheel Guard (independent validation of flywheel decision)
|
|
@@ -602,12 +818,28 @@ export async function run(slug, options = {}) {
|
|
|
602
818
|
|
|
603
819
|
if (guardVerdict.verdict === 'inconclusive') {
|
|
604
820
|
state.phase = 'blocked';
|
|
605
|
-
|
|
821
|
+
const guardReason = 'flywheel-guard-escalate-inconclusive';
|
|
822
|
+
await writeSentinel(paths.blockedSentinel, 'blocked', state.current_us, guardReason, _classifyBlock('flywheel_inconclusive', { state, slug }), paths);
|
|
606
823
|
await writeStatus(paths, state, options.onStatusChange, options.now);
|
|
824
|
+
// governance §1f three-channel: sentinel + report + return value all
|
|
825
|
+
// carry the same blocked reason. SV is intentionally not generated
|
|
826
|
+
// here because the guard fires before the iteration runs to
|
|
827
|
+
// completion; the campaign report uses the default SV message.
|
|
828
|
+
await generateCampaignReport({
|
|
829
|
+
slug,
|
|
830
|
+
reportFile: paths.reportFile,
|
|
831
|
+
prdFile: paths.prdFile,
|
|
832
|
+
statusFile: paths.statusFile,
|
|
833
|
+
analyticsFile: paths.analyticsFile,
|
|
834
|
+
now: resolveNow(options.now),
|
|
835
|
+
blockedReason: guardReason,
|
|
836
|
+
blockedCategory: 'mission_abort',
|
|
837
|
+
});
|
|
607
838
|
return {
|
|
608
839
|
status: 'blocked',
|
|
609
840
|
usId: state.current_us,
|
|
610
|
-
reason:
|
|
841
|
+
reason: guardReason,
|
|
842
|
+
category: 'mission_abort',
|
|
611
843
|
guardIssues: guardVerdict.issues,
|
|
612
844
|
statusFile: paths.statusFile,
|
|
613
845
|
};
|
|
@@ -616,12 +848,25 @@ export async function run(slug, options = {}) {
|
|
|
616
848
|
if (guardVerdict.verdict === 'fail') {
|
|
617
849
|
if (state.flywheel_guard_count[state.current_us] >= 3) {
|
|
618
850
|
state.phase = 'blocked';
|
|
619
|
-
|
|
851
|
+
const exhaustReason = 'flywheel-guard-retries-exhausted';
|
|
852
|
+
await writeSentinel(paths.blockedSentinel, 'blocked', state.current_us, exhaustReason, _classifyBlock('flywheel_exhausted', { state, slug }), paths);
|
|
620
853
|
await writeStatus(paths, state, options.onStatusChange, options.now);
|
|
854
|
+
// governance §1f three-channel: see comment above.
|
|
855
|
+
await generateCampaignReport({
|
|
856
|
+
slug,
|
|
857
|
+
reportFile: paths.reportFile,
|
|
858
|
+
prdFile: paths.prdFile,
|
|
859
|
+
statusFile: paths.statusFile,
|
|
860
|
+
analyticsFile: paths.analyticsFile,
|
|
861
|
+
now: resolveNow(options.now),
|
|
862
|
+
blockedReason: exhaustReason,
|
|
863
|
+
blockedCategory: 'mission_abort',
|
|
864
|
+
});
|
|
621
865
|
return {
|
|
622
866
|
status: 'blocked',
|
|
623
867
|
usId: state.current_us,
|
|
624
|
-
reason:
|
|
868
|
+
reason: exhaustReason,
|
|
869
|
+
category: 'mission_abort',
|
|
625
870
|
guardIssues: guardVerdict.issues,
|
|
626
871
|
statusFile: paths.statusFile,
|
|
627
872
|
};
|
|
@@ -680,6 +925,24 @@ export async function run(slug, options = {}) {
|
|
|
680
925
|
}
|
|
681
926
|
}
|
|
682
927
|
|
|
928
|
+
// US-019 R7 P1-G: verify_partial malformed downgrade.
|
|
929
|
+
// verify_partial requires verified_acs[] to be a non-empty array. Otherwise the verifier
|
|
930
|
+
// has nothing to evaluate and we must treat the signal as broken contract → blocked.
|
|
931
|
+
if (signal && signal.status === 'verify_partial') {
|
|
932
|
+
const acs = Array.isArray(signal.verified_acs) ? signal.verified_acs : null;
|
|
933
|
+
if (!acs || acs.length === 0) {
|
|
934
|
+
const malformedUs = signal.us_id ?? state.current_us;
|
|
935
|
+
const malformedClassification = {
|
|
936
|
+
reason_category: 'mission_abort',
|
|
937
|
+
recoverable: true,
|
|
938
|
+
suggested_action: 'retry_after_fix',
|
|
939
|
+
failure_category: 'spec',
|
|
940
|
+
};
|
|
941
|
+
await writeSentinel(paths.blockedSentinel, 'blocked', malformedUs, 'verify_partial_malformed', malformedClassification, paths);
|
|
942
|
+
return { status: 'blocked', usId: malformedUs, reason: 'verify_partial_malformed', category: 'mission_abort' };
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
683
946
|
const usId = signal.us_id ?? state.current_us;
|
|
684
947
|
const verifierModel = deriveVerifierModel(usId, options);
|
|
685
948
|
state.phase = 'verifier';
|
|
@@ -721,7 +984,9 @@ export async function run(slug, options = {}) {
|
|
|
721
984
|
|
|
722
985
|
if (verdict.verdict === 'blocked') {
|
|
723
986
|
state.phase = 'blocked';
|
|
724
|
-
|
|
987
|
+
const blockedReason = verdict.reason || verdict.summary || 'verifier-blocked';
|
|
988
|
+
const blockedClassification = _classifyBlock('verifier', { verdict, state, slug });
|
|
989
|
+
await writeSentinel(paths.blockedSentinel, 'blocked', usId, blockedReason, blockedClassification, paths);
|
|
725
990
|
await appendIterationAnalytics(paths, state, usId, 'blocked', options);
|
|
726
991
|
await writeStatus(paths, state, options.onStatusChange, options.now);
|
|
727
992
|
let svSummary;
|
|
@@ -748,10 +1013,14 @@ export async function run(slug, options = {}) {
|
|
|
748
1013
|
analyticsFile: paths.analyticsFile,
|
|
749
1014
|
now: resolveNow(options.now),
|
|
750
1015
|
svSummary,
|
|
1016
|
+
blockedReason,
|
|
1017
|
+
blockedCategory: blockedClassification.reason_category,
|
|
751
1018
|
});
|
|
752
1019
|
return {
|
|
753
1020
|
status: 'blocked',
|
|
754
1021
|
usId,
|
|
1022
|
+
reason: blockedReason,
|
|
1023
|
+
category: blockedClassification.reason_category,
|
|
755
1024
|
statusFile: paths.statusFile,
|
|
756
1025
|
};
|
|
757
1026
|
}
|
|
@@ -761,7 +1030,8 @@ export async function run(slug, options = {}) {
|
|
|
761
1030
|
const upgradedModel = nextWorkerModel(options.workerModel ?? state.worker_model, state.consecutive_failures);
|
|
762
1031
|
if (upgradedModel === 'BLOCKED') {
|
|
763
1032
|
state.phase = 'blocked';
|
|
764
|
-
|
|
1033
|
+
const upgradeReason = `model-upgrade-exhausted (worker_model=${state.worker_model}, consecutive_failures=${state.consecutive_failures})`;
|
|
1034
|
+
await writeSentinel(paths.blockedSentinel, 'blocked', usId, upgradeReason, _classifyBlock('model_upgrade', { state, slug }), paths);
|
|
765
1035
|
await writeStatus(paths, state, options.onStatusChange, options.now);
|
|
766
1036
|
let svSummary;
|
|
767
1037
|
if (options.withSelfVerification) {
|
|
@@ -787,10 +1057,14 @@ export async function run(slug, options = {}) {
|
|
|
787
1057
|
analyticsFile: paths.analyticsFile,
|
|
788
1058
|
now: resolveNow(options.now),
|
|
789
1059
|
svSummary,
|
|
1060
|
+
blockedReason: upgradeReason,
|
|
1061
|
+
blockedCategory: 'repeat_axis',
|
|
790
1062
|
});
|
|
791
1063
|
return {
|
|
792
1064
|
status: 'blocked',
|
|
793
1065
|
usId,
|
|
1066
|
+
reason: upgradeReason,
|
|
1067
|
+
category: 'repeat_axis',
|
|
794
1068
|
statusFile: paths.statusFile,
|
|
795
1069
|
};
|
|
796
1070
|
}
|