@ai-dev-methodologies/rlp-desk 0.10.1 → 0.11.1
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-0.11.1-tmux-pane-disappearance.md +260 -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 +173 -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 -9
|
@@ -64,6 +64,7 @@ function buildPaths(rootDir, slug) {
|
|
|
64
64
|
flywheelSignalFile: path.join(deskRoot, 'memos', `${slug}-flywheel-signal.json`),
|
|
65
65
|
flywheelGuardPromptFile: path.join(deskRoot, 'prompts', `${slug}.flywheel-guard.prompt.md`),
|
|
66
66
|
flywheelGuardVerdictFile: path.join(deskRoot, 'memos', `${slug}-flywheel-guard-verdict.json`),
|
|
67
|
+
laneAuditFile: path.join(campaignLogDir, 'lane-audit.json'),
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
70
|
|
|
@@ -253,6 +254,10 @@ async function readCurrentState(paths, slug, options) {
|
|
|
253
254
|
final_verifier_model: status.final_verifier_model ?? options.finalVerifierModel ?? 'opus',
|
|
254
255
|
verified_us: status.verified_us ?? [],
|
|
255
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 ?? '',
|
|
256
261
|
current_us: status.current_us ?? null,
|
|
257
262
|
session_name: status.session_name ?? null,
|
|
258
263
|
leader_pane_id: status.leader_pane_id ?? null,
|
|
@@ -334,9 +339,171 @@ async function dispatchVerifier({
|
|
|
334
339
|
return promptFile;
|
|
335
340
|
}
|
|
336
341
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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');
|
|
340
507
|
}
|
|
341
508
|
|
|
342
509
|
async function runFinalSequentialVerify({
|
|
@@ -456,6 +623,9 @@ export async function run(slug, options = {}) {
|
|
|
456
623
|
analyticsFile: paths.analyticsFile,
|
|
457
624
|
statusFile: paths.statusFile,
|
|
458
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);
|
|
459
629
|
|
|
460
630
|
if (await exists(paths.blockedSentinel)) {
|
|
461
631
|
throw new Error(`Campaign ${slug} is blocked. Run clean first.`);
|
|
@@ -495,7 +665,49 @@ export async function run(slug, options = {}) {
|
|
|
495
665
|
|
|
496
666
|
let fixContractPath = null;
|
|
497
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
|
+
|
|
498
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
|
+
|
|
499
711
|
state.current_us = getNextUs(usList, state.verified_us, state.current_us);
|
|
500
712
|
if (state.current_us === 'ALL') {
|
|
501
713
|
const finalResult = await runFinalSequentialVerify({
|
|
@@ -575,6 +787,11 @@ export async function run(slug, options = {}) {
|
|
|
575
787
|
});
|
|
576
788
|
|
|
577
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;
|
|
578
795
|
await fs.unlink(paths.flywheelSignalFile).catch(() => {});
|
|
579
796
|
|
|
580
797
|
// Flywheel Guard (independent validation of flywheel decision)
|
|
@@ -601,12 +818,28 @@ export async function run(slug, options = {}) {
|
|
|
601
818
|
|
|
602
819
|
if (guardVerdict.verdict === 'inconclusive') {
|
|
603
820
|
state.phase = 'blocked';
|
|
604
|
-
|
|
821
|
+
const guardReason = 'flywheel-guard-escalate-inconclusive';
|
|
822
|
+
await writeSentinel(paths.blockedSentinel, 'blocked', state.current_us, guardReason, _classifyBlock('flywheel_inconclusive', { state, slug }), paths);
|
|
605
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
|
+
});
|
|
606
838
|
return {
|
|
607
839
|
status: 'blocked',
|
|
608
840
|
usId: state.current_us,
|
|
609
|
-
reason:
|
|
841
|
+
reason: guardReason,
|
|
842
|
+
category: 'mission_abort',
|
|
610
843
|
guardIssues: guardVerdict.issues,
|
|
611
844
|
statusFile: paths.statusFile,
|
|
612
845
|
};
|
|
@@ -615,12 +848,25 @@ export async function run(slug, options = {}) {
|
|
|
615
848
|
if (guardVerdict.verdict === 'fail') {
|
|
616
849
|
if (state.flywheel_guard_count[state.current_us] >= 3) {
|
|
617
850
|
state.phase = 'blocked';
|
|
618
|
-
|
|
851
|
+
const exhaustReason = 'flywheel-guard-retries-exhausted';
|
|
852
|
+
await writeSentinel(paths.blockedSentinel, 'blocked', state.current_us, exhaustReason, _classifyBlock('flywheel_exhausted', { state, slug }), paths);
|
|
619
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
|
+
});
|
|
620
865
|
return {
|
|
621
866
|
status: 'blocked',
|
|
622
867
|
usId: state.current_us,
|
|
623
|
-
reason:
|
|
868
|
+
reason: exhaustReason,
|
|
869
|
+
category: 'mission_abort',
|
|
624
870
|
guardIssues: guardVerdict.issues,
|
|
625
871
|
statusFile: paths.statusFile,
|
|
626
872
|
};
|
|
@@ -679,6 +925,24 @@ export async function run(slug, options = {}) {
|
|
|
679
925
|
}
|
|
680
926
|
}
|
|
681
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
|
+
|
|
682
946
|
const usId = signal.us_id ?? state.current_us;
|
|
683
947
|
const verifierModel = deriveVerifierModel(usId, options);
|
|
684
948
|
state.phase = 'verifier';
|
|
@@ -720,7 +984,9 @@ export async function run(slug, options = {}) {
|
|
|
720
984
|
|
|
721
985
|
if (verdict.verdict === 'blocked') {
|
|
722
986
|
state.phase = 'blocked';
|
|
723
|
-
|
|
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);
|
|
724
990
|
await appendIterationAnalytics(paths, state, usId, 'blocked', options);
|
|
725
991
|
await writeStatus(paths, state, options.onStatusChange, options.now);
|
|
726
992
|
let svSummary;
|
|
@@ -747,10 +1013,14 @@ export async function run(slug, options = {}) {
|
|
|
747
1013
|
analyticsFile: paths.analyticsFile,
|
|
748
1014
|
now: resolveNow(options.now),
|
|
749
1015
|
svSummary,
|
|
1016
|
+
blockedReason,
|
|
1017
|
+
blockedCategory: blockedClassification.reason_category,
|
|
750
1018
|
});
|
|
751
1019
|
return {
|
|
752
1020
|
status: 'blocked',
|
|
753
1021
|
usId,
|
|
1022
|
+
reason: blockedReason,
|
|
1023
|
+
category: blockedClassification.reason_category,
|
|
754
1024
|
statusFile: paths.statusFile,
|
|
755
1025
|
};
|
|
756
1026
|
}
|
|
@@ -760,7 +1030,8 @@ export async function run(slug, options = {}) {
|
|
|
760
1030
|
const upgradedModel = nextWorkerModel(options.workerModel ?? state.worker_model, state.consecutive_failures);
|
|
761
1031
|
if (upgradedModel === 'BLOCKED') {
|
|
762
1032
|
state.phase = 'blocked';
|
|
763
|
-
|
|
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);
|
|
764
1035
|
await writeStatus(paths, state, options.onStatusChange, options.now);
|
|
765
1036
|
let svSummary;
|
|
766
1037
|
if (options.withSelfVerification) {
|
|
@@ -786,10 +1057,14 @@ export async function run(slug, options = {}) {
|
|
|
786
1057
|
analyticsFile: paths.analyticsFile,
|
|
787
1058
|
now: resolveNow(options.now),
|
|
788
1059
|
svSummary,
|
|
1060
|
+
blockedReason: upgradeReason,
|
|
1061
|
+
blockedCategory: 'repeat_axis',
|
|
789
1062
|
});
|
|
790
1063
|
return {
|
|
791
1064
|
status: 'blocked',
|
|
792
1065
|
usId,
|
|
1066
|
+
reason: upgradeReason,
|
|
1067
|
+
category: 'repeat_axis',
|
|
793
1068
|
statusFile: paths.statusFile,
|
|
794
1069
|
};
|
|
795
1070
|
}
|