@ai-dev-methodologies/rlp-desk 0.15.2 → 0.15.4
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/CHANGELOG.md +82 -0
- package/README.md +34 -4
- package/docs/rlp-desk/failure-modes.md +191 -0
- package/package.json +3 -2
- package/src/node/runner/campaign-main-loop.mjs +174 -11
- package/src/node/util/debug-log.mjs +10 -6
- package/src/node/util/lifecycle-metrics.mjs +102 -0
- package/src/scripts/lib_ralph_desk.zsh +141 -0
- package/src/scripts/run_ralph_desk.zsh +44 -0
- package/docs/plans/bug-report-overhaul-backlog.md +0 -49
- package/docs/plans/bug-report-overhaul-v0.md +0 -238
- package/docs/plans/bug-report-overhaul-v1.md +0 -319
- package/docs/plans/native-agent-revert.md +0 -184
- package/docs/plans/polished-gliding-toucan.md +0 -234
- package/docs/plans/spicy-booping-galaxy.md +0 -717
- package/docs/plans/strategic-review/rlp-desk-strategic-review.md +0 -125
- package/docs/plans/v0.15-stabilization-plan.md +0 -178
|
@@ -6,15 +6,19 @@
|
|
|
6
6
|
// SHOULD use debugLog() instead of console/manual writes.
|
|
7
7
|
//
|
|
8
8
|
// Categories (governance §1f traceability):
|
|
9
|
-
// - GOV
|
|
10
|
-
// - DECIDE: leader decisions (model selection, fix contracts, escalation)
|
|
11
|
-
// - OPTION: configuration snapshot at loop start
|
|
12
|
-
// - FLOW
|
|
9
|
+
// - GOV : governance enforcement (IL, CB triggers, scope locks, verdicts)
|
|
10
|
+
// - DECIDE : leader decisions (model selection, fix contracts, escalation)
|
|
11
|
+
// - OPTION : configuration snapshot at loop start
|
|
12
|
+
// - FLOW : execution progress (worker/verifier dispatch, signal reads, transitions)
|
|
13
|
+
// - LIFECYCLE : v0.15.4 PR-B4 — tmux/process lifecycle metrics gated on
|
|
14
|
+
// RLP_LIFECYCLE_METRICS=1. Emission rules: see plan v3 §B4
|
|
15
|
+
// Table (5 metrics). Helper is no-op when flag unset (verified
|
|
16
|
+
// by tests/node/test-campaign-jsonl-shape.mjs).
|
|
13
17
|
|
|
14
18
|
import fs from 'node:fs/promises';
|
|
15
19
|
import path from 'node:path';
|
|
16
20
|
|
|
17
|
-
const VALID_CATEGORIES = new Set(['GOV', 'DECIDE', 'OPTION', 'FLOW']);
|
|
21
|
+
const VALID_CATEGORIES = new Set(['GOV', 'DECIDE', 'OPTION', 'FLOW', 'LIFECYCLE']);
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
24
|
* Append a structured log line to debug.log. Format mirrors zsh log_debug:
|
|
@@ -22,7 +26,7 @@ const VALID_CATEGORIES = new Set(['GOV', 'DECIDE', 'OPTION', 'FLOW']);
|
|
|
22
26
|
*
|
|
23
27
|
* @param {Object} args
|
|
24
28
|
* @param {string} args.debugLogPath — absolute path to debug.log
|
|
25
|
-
* @param {'GOV'|'DECIDE'|'OPTION'|'FLOW'} args.category
|
|
29
|
+
* @param {'GOV'|'DECIDE'|'OPTION'|'FLOW'|'LIFECYCLE'} args.category
|
|
26
30
|
* @param {Object<string,string|number|boolean>} args.fields — flat key/value
|
|
27
31
|
* pairs, serialized as `key=value`. Avoid nested objects; pre-stringify.
|
|
28
32
|
* @returns {Promise<void>} — resolves even on filesystem errors (best-effort).
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// v0.15.4 PR-B4 — Lifecycle observability helper.
|
|
2
|
+
//
|
|
3
|
+
// Plan: docs/plans/v0.15-phase-b-plan-v3.md §B4.
|
|
4
|
+
// Audit: docs/plans/v0.15-phase-b-lifecycle-audit.md §3 Table 2.
|
|
5
|
+
//
|
|
6
|
+
// Five metrics tracked, all gated on RLP_LIFECYCLE_METRICS=1 env flag:
|
|
7
|
+
// - iter_signal_write_to_read_ms leader-poll-resolves vs worker-FS-write
|
|
8
|
+
// - verdict_write_to_read_ms leader-poll-resolves vs verifier-FS-write
|
|
9
|
+
// - pane_eof_to_cleanup_ms pane process exit vs killPaneProcess return
|
|
10
|
+
// - pane_reap_latency_ms done-claim observed vs C-c×2 + waitForExit
|
|
11
|
+
// - sentinel_lock_to_unlock_ms per type, _lock vs _unlock (object)
|
|
12
|
+
//
|
|
13
|
+
// Emission discipline:
|
|
14
|
+
// - debug.log: tagged [LIFECYCLE] per record (when flag set)
|
|
15
|
+
// - campaign.jsonl: ONE batched lifecycle_metrics object per iteration
|
|
16
|
+
// (the collector accumulates, the iter-end flush emits)
|
|
17
|
+
// When flag is unset:
|
|
18
|
+
// - record() is a no-op (early return) — zero overhead beyond a Map check
|
|
19
|
+
// - flush() returns null so analytics writer can branch on the field
|
|
20
|
+
|
|
21
|
+
const ENV_FLAG_NAME = 'RLP_LIFECYCLE_METRICS';
|
|
22
|
+
|
|
23
|
+
export function lifecycleMetricsEnabled(env = process.env) {
|
|
24
|
+
return env[ENV_FLAG_NAME] === '1';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class LifecycleMetricsCollector {
|
|
28
|
+
constructor({ env = process.env, debugLog = null } = {}) {
|
|
29
|
+
this._enabled = lifecycleMetricsEnabled(env);
|
|
30
|
+
this._debugLog = debugLog;
|
|
31
|
+
this._records = [];
|
|
32
|
+
this._sentinelLockTimes = new Map();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get enabled() {
|
|
36
|
+
return this._enabled;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Record a single timing metric. value is in milliseconds. ctx is a flat
|
|
40
|
+
// object of audit fields (iter, us_id, pane_id, sentinel_type, etc).
|
|
41
|
+
record(name, valueMs, ctx = {}) {
|
|
42
|
+
if (!this._enabled) return;
|
|
43
|
+
const entry = {
|
|
44
|
+
metric: name,
|
|
45
|
+
value_ms: Math.max(0, Math.round(valueMs)),
|
|
46
|
+
ts: new Date().toISOString(),
|
|
47
|
+
...ctx,
|
|
48
|
+
};
|
|
49
|
+
this._records.push(entry);
|
|
50
|
+
if (this._debugLog) {
|
|
51
|
+
// Best-effort fire-and-forget. The debug-log helper is itself best-
|
|
52
|
+
// effort (appendFile error swallowed), so we don't await it.
|
|
53
|
+
this._debugLog('LIFECYCLE', { metric: name, value_ms: entry.value_ms, ...ctx });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Convenience: pair-bookkeeping for sentinel_lock_to_unlock_ms (object-
|
|
58
|
+
// valued metric keyed by sentinel type). Call markLockStart at chmod 0o444
|
|
59
|
+
// time, markUnlock at chmod 0o644 time (or end-of-iter for never-unlocked).
|
|
60
|
+
//
|
|
61
|
+
// v0.15.4 audit H2: done-claim is intentionally NOT instrumented with this
|
|
62
|
+
// pair. In production happy path done-claim is locked-but-never-unlocked
|
|
63
|
+
// (campaign-main-loop unlocks only signalFile + verdictFile at iter start);
|
|
64
|
+
// markUnlock for done-claim never fires, so the metric would silently never
|
|
65
|
+
// emit. Future work: emit at lib_ralph_desk.zsh:602 archival site if needed.
|
|
66
|
+
//
|
|
67
|
+
// v0.15.4 audit H3: callers must invoke markLockStart BEFORE the chmod
|
|
68
|
+
// operation, not after, so the metric covers full lock duration including
|
|
69
|
+
// chmod execution time. Sub-ms skew, but semantically correct.
|
|
70
|
+
markLockStart(sentinelType, t = Date.now()) {
|
|
71
|
+
if (!this._enabled) return;
|
|
72
|
+
this._sentinelLockTimes.set(sentinelType, t);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
markUnlock(sentinelType, ctx = {}, t = Date.now()) {
|
|
76
|
+
if (!this._enabled) return;
|
|
77
|
+
const start = this._sentinelLockTimes.get(sentinelType);
|
|
78
|
+
if (start === undefined) return;
|
|
79
|
+
this.record('sentinel_lock_to_unlock_ms', t - start, {
|
|
80
|
+
...ctx,
|
|
81
|
+
sentinel_type: sentinelType,
|
|
82
|
+
});
|
|
83
|
+
this._sentinelLockTimes.delete(sentinelType);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Snapshot + reset for end-of-iteration flush. Returns null when disabled
|
|
87
|
+
// so the analytics writer can omit the field cleanly.
|
|
88
|
+
flush() {
|
|
89
|
+
if (!this._enabled) return null;
|
|
90
|
+
const records = this._records;
|
|
91
|
+
this._records = [];
|
|
92
|
+
// Group by metric name for compact campaign.jsonl shape:
|
|
93
|
+
// { iter_signal_write_to_read_ms: [{value_ms,ts,...}, ...], ... }
|
|
94
|
+
const grouped = {};
|
|
95
|
+
for (const r of records) {
|
|
96
|
+
const { metric, ...rest } = r;
|
|
97
|
+
if (!grouped[metric]) grouped[metric] = [];
|
|
98
|
+
grouped[metric].push(rest);
|
|
99
|
+
}
|
|
100
|
+
return grouped;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -261,6 +261,19 @@ _kill_pane_process() {
|
|
|
261
261
|
if typeset -f log_debug >/dev/null 2>&1; then
|
|
262
262
|
log_debug "[bug7] kill_pane_process pane=$pane_id role=$role"
|
|
263
263
|
fi
|
|
264
|
+
# v0.15.4 PR-B4: pane_eof_to_cleanup_ms instrumentation (flag-gated).
|
|
265
|
+
# Records the wallclock from kill-start to wait_for_pane_ready return so
|
|
266
|
+
# B3 can value-assert the substrate fix actually closes the race window.
|
|
267
|
+
# Uses zsh native $EPOCHREALTIME (microsec) — portable to macOS BSD where
|
|
268
|
+
# `date +%N` is not supported.
|
|
269
|
+
local _b4_t0_ms=0
|
|
270
|
+
if [[ "${RLP_LIFECYCLE_METRICS:-0}" == "1" ]]; then
|
|
271
|
+
zmodload -e zsh/datetime || zmodload zsh/datetime 2>/dev/null
|
|
272
|
+
if [[ -n "${EPOCHREALTIME:-}" ]]; then
|
|
273
|
+
local _b4_t0_str="${EPOCHREALTIME//./}"
|
|
274
|
+
_b4_t0_ms=${_b4_t0_str:0:13}
|
|
275
|
+
fi
|
|
276
|
+
fi
|
|
264
277
|
tmux send-keys -t "$pane_id" C-c 2>/dev/null
|
|
265
278
|
sleep 0.5
|
|
266
279
|
tmux send-keys -t "$pane_id" C-c 2>/dev/null
|
|
@@ -268,6 +281,12 @@ _kill_pane_process() {
|
|
|
268
281
|
if typeset -f wait_for_pane_ready >/dev/null 2>&1; then
|
|
269
282
|
wait_for_pane_ready "$pane_id" 5 2>/dev/null || true
|
|
270
283
|
fi
|
|
284
|
+
if (( _b4_t0_ms > 0 )); then
|
|
285
|
+
local _b4_t1_str="${EPOCHREALTIME//./}"
|
|
286
|
+
local _b4_t1_ms=${_b4_t1_str:0:13}
|
|
287
|
+
log_lifecycle_metric "pane_eof_to_cleanup_ms" $((_b4_t1_ms - _b4_t0_ms)) \
|
|
288
|
+
"pane=$pane_id role=$role"
|
|
289
|
+
fi
|
|
271
290
|
return 0
|
|
272
291
|
}
|
|
273
292
|
|
|
@@ -285,6 +304,53 @@ _unlock_sentinel() {
|
|
|
285
304
|
return 0
|
|
286
305
|
}
|
|
287
306
|
|
|
307
|
+
# =============================================================================
|
|
308
|
+
# v0.15.4 PR-B4: Lifecycle observability — log_lifecycle_metric
|
|
309
|
+
# =============================================================================
|
|
310
|
+
# Plan: docs/plans/v0.15-phase-b-plan-v3.md §B4 (P2.1 critic-round-2 fix).
|
|
311
|
+
# Helper is GATED on $RLP_LIFECYCLE_METRICS=1 (no-op when unset). Emits to
|
|
312
|
+
# debug.log via log_debug, in a backgrounded subshell so the caller does not
|
|
313
|
+
# block on the FS write. The Node-side mirror is src/node/util/lifecycle-
|
|
314
|
+
# metrics.mjs LifecycleMetricsCollector.
|
|
315
|
+
#
|
|
316
|
+
# v0.15.4 audit M2: concurrent-appender semantics — `( ... ) &!` spawns a
|
|
317
|
+
# disowned subshell per metric. Multiple metrics can fire in rapid succession
|
|
318
|
+
# (e.g., during iter teardown) and race on debug.log. POSIX guarantees atomic
|
|
319
|
+
# append for writes <= PIPE_BUF (4096 bytes). A single LIFECYCLE line is
|
|
320
|
+
# ~150 bytes, well under the limit, so on local filesystems (APFS, ext4, xfs)
|
|
321
|
+
# concurrent appends produce intact non-interleaved lines. On NFS / FUSE /
|
|
322
|
+
# some Docker overlay setups PIPE_BUF guarantees may not hold; in those
|
|
323
|
+
# environments, expect possible interleaving. This is best-effort logging
|
|
324
|
+
# by design — the metric values land in campaign.jsonl via the Node leader's
|
|
325
|
+
# batched flush as the canonical authoritative record. debug.log is an
|
|
326
|
+
# audit aid, not the source of truth.
|
|
327
|
+
#
|
|
328
|
+
# Args:
|
|
329
|
+
# $1 metric_name e.g. iter_signal_write_to_read_ms
|
|
330
|
+
# $2 value_ms integer milliseconds (will be coerced via printf %d)
|
|
331
|
+
# $3 context (optional, free-form key=val pairs joined with spaces)
|
|
332
|
+
#
|
|
333
|
+
# Side effects:
|
|
334
|
+
# - When flag unset: returns 0 immediately (no fork, no FS call).
|
|
335
|
+
# - When flag set: forks `( log_debug "..." ) &!` to debug.log.
|
|
336
|
+
#
|
|
337
|
+
# Examples:
|
|
338
|
+
# log_lifecycle_metric "iter_signal_write_to_read_ms" "$delta" \
|
|
339
|
+
# "iter=$ITERATION us=$us_id pane=$WORKER_PANE"
|
|
340
|
+
# log_lifecycle_metric "pane_reap_latency_ms" "$delta" \
|
|
341
|
+
# "iter=$ITERATION sentinel=done-claim"
|
|
342
|
+
log_lifecycle_metric() {
|
|
343
|
+
[[ "${RLP_LIFECYCLE_METRICS:-0}" == "1" ]] || return 0
|
|
344
|
+
local metric="$1"
|
|
345
|
+
local value_ms="$2"
|
|
346
|
+
local ctx="${3:-}"
|
|
347
|
+
[[ -n "$metric" && -n "$value_ms" ]] || return 0
|
|
348
|
+
if typeset -f log_debug >/dev/null 2>&1; then
|
|
349
|
+
( log_debug "[LIFECYCLE] metric=$metric value_ms=$value_ms $ctx" ) &!
|
|
350
|
+
fi
|
|
351
|
+
return 0
|
|
352
|
+
}
|
|
353
|
+
|
|
288
354
|
# PR-A (Bug #10) — validate operator-written manual recovery artifacts.
|
|
289
355
|
# Returns 0 when all 5 checks pass; 1 otherwise. Sets RECOVERY_FAIL_REASON
|
|
290
356
|
# (global) on failure for caller logging. Mirrors the Node-side helper
|
|
@@ -369,6 +435,81 @@ _validate_operator_recovery_artifacts() {
|
|
|
369
435
|
return 0
|
|
370
436
|
}
|
|
371
437
|
|
|
438
|
+
# PR-E (Phase C1, stabilization) — operator-cleared BLOCKED recovery validator.
|
|
439
|
+
# Pair to PR-A (_validate_operator_recovery_artifacts above). Together they
|
|
440
|
+
# close two recovery surfaces: phase=verify (PR-A) and phase=blocked
|
|
441
|
+
# sentinel-cleared (PR-E this helper).
|
|
442
|
+
#
|
|
443
|
+
# Returns 0 when all 4 checks pass; 1 otherwise. Sets BLOCKED_RECOVERY_FAIL_REASON
|
|
444
|
+
# (global) on failure for caller logging. Mirrors Node `_validateBlockedRecovery`
|
|
445
|
+
# in src/node/runner/campaign-main-loop.mjs.
|
|
446
|
+
#
|
|
447
|
+
# Args:
|
|
448
|
+
# $1 blocked sentinel path (.md)
|
|
449
|
+
# $2 blocked sidecar path (.json)
|
|
450
|
+
# $3 status.json path
|
|
451
|
+
_validate_blocked_recovery() {
|
|
452
|
+
local sentinel_md="$1" sidecar_json="$2" status_file="$3"
|
|
453
|
+
BLOCKED_RECOVERY_FAIL_REASON=""
|
|
454
|
+
|
|
455
|
+
# Check 1: precondition — caller verified phase=blocked already
|
|
456
|
+
# (passed in via status read; no need to re-read here)
|
|
457
|
+
|
|
458
|
+
# Check 2: sentinel cleared by operator
|
|
459
|
+
if [[ -f "$sentinel_md" ]]; then
|
|
460
|
+
BLOCKED_RECOVERY_FAIL_REASON="blocked sentinel still present (operator did not clear)"
|
|
461
|
+
return 1
|
|
462
|
+
fi
|
|
463
|
+
|
|
464
|
+
# Check 3: status.json must exist + counters non-zero
|
|
465
|
+
if [[ ! -f "$status_file" ]]; then
|
|
466
|
+
BLOCKED_RECOVERY_FAIL_REASON="status.json missing"
|
|
467
|
+
return 1
|
|
468
|
+
fi
|
|
469
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
470
|
+
BLOCKED_RECOVERY_FAIL_REASON="jq unavailable; cannot validate"
|
|
471
|
+
return 1
|
|
472
|
+
fi
|
|
473
|
+
local fails blocks
|
|
474
|
+
fails=$(jq -r '.consecutive_failures // 0' "$status_file" 2>/dev/null)
|
|
475
|
+
blocks=$(jq -r '.consecutive_blocks // 0' "$status_file" 2>/dev/null)
|
|
476
|
+
if [[ "$fails" == "0" && "$blocks" == "0" ]]; then
|
|
477
|
+
BLOCKED_RECOVERY_FAIL_REASON="counters already zero, nothing to recover"
|
|
478
|
+
return 1
|
|
479
|
+
fi
|
|
480
|
+
|
|
481
|
+
# Check 4: sidecar safety — if sidecar exists and recoverable=false, fall through
|
|
482
|
+
if [[ -f "$sidecar_json" ]]; then
|
|
483
|
+
if ! jq -e . "$sidecar_json" >/dev/null 2>&1; then
|
|
484
|
+
BLOCKED_RECOVERY_FAIL_REASON="blocked.json sidecar parse error"
|
|
485
|
+
return 1
|
|
486
|
+
fi
|
|
487
|
+
local recoverable category
|
|
488
|
+
recoverable=$(jq -r '.recoverable' "$sidecar_json" 2>/dev/null)
|
|
489
|
+
category=$(jq -r '.reason_category // "unknown"' "$sidecar_json" 2>/dev/null)
|
|
490
|
+
if [[ "$recoverable" == "false" ]]; then
|
|
491
|
+
BLOCKED_RECOVERY_FAIL_REASON="non-recoverable category $category from sidecar (use clean to reset)"
|
|
492
|
+
return 1
|
|
493
|
+
fi
|
|
494
|
+
fi
|
|
495
|
+
|
|
496
|
+
return 0
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
# PR-E helper: rename the recovered sidecar so operator can audit what was
|
|
500
|
+
# recovered from. Best-effort — failure is non-fatal.
|
|
501
|
+
#
|
|
502
|
+
# Args:
|
|
503
|
+
# $1 blocked sidecar path (.json)
|
|
504
|
+
_archive_recovered_sidecar() {
|
|
505
|
+
local sidecar_json="$1"
|
|
506
|
+
[[ -f "$sidecar_json" ]] || return 0
|
|
507
|
+
local iso
|
|
508
|
+
iso=$(date -u +%Y-%m-%dT%H-%M-%SZ)
|
|
509
|
+
mv "$sidecar_json" "${sidecar_json}.recovered-${iso}" 2>/dev/null || true
|
|
510
|
+
return 0
|
|
511
|
+
}
|
|
512
|
+
|
|
372
513
|
# PR-0b-narrow (Plan v6) — stamp leader handshake ack onto the sentinel.
|
|
373
514
|
# Mirror of src/node/shared/fs.mjs::stampAckField. Best-effort, audit-only:
|
|
374
515
|
# any failure is silently swallowed. Sequence:
|
|
@@ -710,6 +710,10 @@ handle_worker_exit_codex() {
|
|
|
710
710
|
dc_us_id=$(jq -r '.us_id // "unknown"' "$DONE_CLAIM_FILE" 2>/dev/null)
|
|
711
711
|
log " Codex worker completed with done-claim (us_id=$dc_us_id) and clean tree. Auto-generating signal."
|
|
712
712
|
echo '{"iteration":'"$iter"',"status":"verify","us_id":"'"$dc_us_id"'","summary":"auto-generated after codex exit (clean tree)","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}' > "$signal_file"
|
|
713
|
+
# v0.15.4 PR-B2-FIX: codex worker pane already exited — reaper would no-op,
|
|
714
|
+
# but lock done-claim as defense-in-depth so any orphaned subprocess cannot
|
|
715
|
+
# rewrite the file before lib_ralph_desk.zsh:602 archives it.
|
|
716
|
+
_lock_sentinel "$DONE_CLAIM_FILE"
|
|
713
717
|
_emit_a4_fallback_audit "$dc_us_id" "$iter" "codex_exit_with_done_claim_clean"
|
|
714
718
|
return 0
|
|
715
719
|
}
|
|
@@ -2292,6 +2296,15 @@ poll_for_signal() {
|
|
|
2292
2296
|
if _bug8_check_synth_allowed "$ITERATION" "$dc_us_id" "inline_polling_a4_clean"; then
|
|
2293
2297
|
log " WARNING: done-claim exists for $dc_us_id but no iter-signal. Tree clean — auto-generating signal (A4 fallback)."
|
|
2294
2298
|
log_debug "[GOV] iter=$ITERATION done_claim_without_signal=true us_id=$dc_us_id action=auto_generate_signal"
|
|
2299
|
+
# v0.15.4 PR-B2-FIX: Worker pane is alive and idling post-done-claim
|
|
2300
|
+
# (the canonical Bug #5/7 race window). Reap before synthesizing the
|
|
2301
|
+
# signal so the worker cannot revise done-claim or emit a late
|
|
2302
|
+
# iter-signal that races the leader's synthesized one. Mirror of
|
|
2303
|
+
# Bug #7 Fix-Q parity at run_ralph_desk.zsh:3181 — kill before lock,
|
|
2304
|
+
# lock before synth-write so the next leader read sees a frozen
|
|
2305
|
+
# done-claim and a fresh signal_file in that order.
|
|
2306
|
+
_kill_pane_process "$pane_id" "worker-a4"
|
|
2307
|
+
_lock_sentinel "$DONE_CLAIM_FILE"
|
|
2295
2308
|
echo '{"iteration":'"$ITERATION"',"status":"verify","us_id":"'"$dc_us_id"'","summary":"auto-generated by A4 fallback (done-claim + clean tree)","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}' > "$signal_file"
|
|
2296
2309
|
_emit_a4_fallback_audit "$dc_us_id" "$ITERATION" "inline_polling_a4_clean"
|
|
2297
2310
|
return 0
|
|
@@ -3069,6 +3082,32 @@ main() {
|
|
|
3069
3082
|
fi
|
|
3070
3083
|
fi
|
|
3071
3084
|
|
|
3085
|
+
# PR-E (Phase C1, stabilization): operator-cleared BLOCKED recovery.
|
|
3086
|
+
# Pair to PR-A above. Runs AFTER PR-A (so phase=verify wins) and skipped
|
|
3087
|
+
# when SKIP_NEXT_WORKER=1 (PR-A already honored). Resets stale counters
|
|
3088
|
+
# in status.json when operator manually deleted the BLOCKED sentinel.
|
|
3089
|
+
# Mirrors Node `_validateBlockedRecovery` + branch in campaign-main-loop.mjs.
|
|
3090
|
+
if [[ "$LAST_PHASE" == "blocked" && "$SKIP_NEXT_WORKER" -eq 0 ]]; then
|
|
3091
|
+
local _blocked_sidecar="$MEMOS_DIR/${SLUG}-blocked.json"
|
|
3092
|
+
if _validate_blocked_recovery \
|
|
3093
|
+
"$BLOCKED_SENTINEL" "$_blocked_sidecar" "$STATUS_FILE"; then
|
|
3094
|
+
local _prev_reason
|
|
3095
|
+
_prev_reason=$(jq -r '.last_block_reason // ""' "$STATUS_FILE" 2>/dev/null)
|
|
3096
|
+
log "[recovery] Operator-cleared BLOCKED detected (was: ${_prev_reason:-unrecorded}). Resetting counters and resuming as worker. iter=$ITERATION"
|
|
3097
|
+
log_debug "[recovery] iter=$ITERATION blocked_recovery=applied reason=\"${BLOCKED_RECOVERY_FAIL_REASON:-sidecar absent or recoverable=true}\""
|
|
3098
|
+
# Reset counters in-process. update_status writes fresh status when
|
|
3099
|
+
# next phase transition fires. Operator's intent was a clean restart.
|
|
3100
|
+
CONSECUTIVE_FAILURES=0
|
|
3101
|
+
CONSECUTIVE_BLOCKS=0
|
|
3102
|
+
LAST_BLOCK_REASON=""
|
|
3103
|
+
# Archive sidecar (rename, not delete) for audit trail.
|
|
3104
|
+
_archive_recovered_sidecar "$_blocked_sidecar"
|
|
3105
|
+
else
|
|
3106
|
+
log "[recovery] phase=blocked ignored: ${BLOCKED_RECOVERY_FAIL_REASON}"
|
|
3107
|
+
log_debug "[recovery] iter=$ITERATION blocked_recovery=skipped reason=\"${BLOCKED_RECOVERY_FAIL_REASON}\""
|
|
3108
|
+
fi
|
|
3109
|
+
fi
|
|
3110
|
+
|
|
3072
3111
|
if (( ! SKIP_NEXT_WORKER )); then
|
|
3073
3112
|
# --- governance.md s7 step 8 (cleanup): Clean previous iteration signals ---
|
|
3074
3113
|
# Bug #7 Fix-R cleanup: unlock 0o444 sentinels written by the previous
|
|
@@ -3154,6 +3193,11 @@ main() {
|
|
|
3154
3193
|
# self-review and rewrite iter-signal.json (1m43s drift observed).
|
|
3155
3194
|
_kill_pane_process "$WORKER_PANE" "worker"
|
|
3156
3195
|
_lock_sentinel "$SIGNAL_FILE"
|
|
3196
|
+
# v0.15.4 PR-B2-FIX: same worker pass also produced done-claim. Freeze
|
|
3197
|
+
# it alongside iter-signal so Bug #8 gates and the iter-NNN-done-claim
|
|
3198
|
+
# archive (lib_ralph_desk.zsh:602) read a snapshot the worker can no
|
|
3199
|
+
# longer revise. Symmetric with iter-signal/verdict lock contract.
|
|
3200
|
+
_lock_sentinel "$DONE_CLAIM_FILE"
|
|
3157
3201
|
# PR-0b-narrow: stamp leader handshake ack on the iter-signal (audit-only).
|
|
3158
3202
|
_stamp_ack_field "$SIGNAL_FILE"
|
|
3159
3203
|
else
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# Bug Report Overhaul — P2/P3 Backlog
|
|
2
|
-
|
|
3
|
-
> Companion to `bug-report-overhaul-v1.md` (PR-A/B/C plan).
|
|
4
|
-
> User stop-rule: ralplan iterates only until P0+P1 = 0; P2 and below are captured here, NOT blockers.
|
|
5
|
-
> Re-prioritize from this file in a future ralplan when the operator-minutes-saved metric from PR-A/B/C lands.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## P2 — should fix in a follow-up PR after PR-A/B/C land
|
|
10
|
-
|
|
11
|
-
### From v0 plan (Option C/D, deferred features)
|
|
12
|
-
|
|
13
|
-
- **Heartbeat-warning sidecar (Option B from v0)** — emit `<slug>-warning.{md,json}` when heartbeat anomaly crosses 50% of `iter-timeout`. Lets operator pre-empt a BLOCKED before the 30-min wall hits. Decoupled from this PR set because (a) report-quality is the dominant pain (D1), and (b) warning sidecar adds a second sentinel surface that risks false-positive fatigue. Revisit after PR-A/B land and we measure how many BLOCKEDs would have been pre-empted.
|
|
14
|
-
- **GitHub Issues integration (Option D from v0)** — POST blocked context to a configured GitHub repo issue. Requires per-repo authn story (token storage, network retry, rate-limits) — violates principle 3 in the current PR set. Re-evaluate after a credible authn proposal exists.
|
|
15
|
-
- **Pattern-learning loop** — mine `~/.claude/ralph-desk/analytics/*/bug-reports/` for emerging clusters. Auto-extends `docs/bug-patterns.json` with new candidate signatures for human review.
|
|
16
|
-
- **Cross-campaign bug-report dashboard in `/rlp-desk analytics`** — surface patterns across projects.
|
|
17
|
-
- **Auto-suggest "this looks like Bug #N — try fix-X" inline in CLI output** — operationalize PR-C's `pattern_match` data with an inline suggestion. Held back so the deterministic Jaccard implementation can be calibrated against real campaign data first.
|
|
18
|
-
- **Operator-CLI `/rlp-desk recover <slug> --to verify`** — write the manual recovery artifacts (`iter-signal.json`, `done-claim.json`, `status.json` patch) deterministically. Currently a hand-rolled `jq` pipeline per Bug #10 §7 workaround.
|
|
19
|
-
|
|
20
|
-
### From Codex Critic Round 2 (BACKLOG)
|
|
21
|
-
|
|
22
|
-
- **[P2-1]** PR-A `_validateOperatorRecoveryArtifacts` return shape — current pseudo-code mixes `if (valid)` (boolean coercion) with `valid.reason` (object access). Resolve at implementation time to either `{ ok: bool, reason: string }` (object) or pure boolean + separate side-channel for the warning text. Affects the audit log line shape.
|
|
23
|
-
- **[P2-2]** PR-A test summary in §5 says "5 ACs (R1–R5)" but §8 added AC-R6 (`_skipNextWorkerDispatch` cleared after one use). Update §5 to "6 ACs (R1–R6)" for consistency before PR-A merges.
|
|
24
|
-
|
|
25
|
-
### From Codex Critic Round 3 (BACKLOG)
|
|
26
|
-
|
|
27
|
-
- **[P2-3]** §9 step 5 banner-aware diff command only covers `run_ralph_desk.zsh`. PR-A and PR-B both also touch `lib_ralph_desk.zsh`. Add a matching `diff <(cat src/scripts/lib_ralph_desk.zsh) <(tail -n +N ~/.claude/ralph-desk/scripts/lib_ralph_desk.zsh)` step in the implementation runbook (verify the right `tail -n +N` offset at impl time — `lib_*.zsh` is sourced and may have no shebang). Extend to `init_ralph_desk.zsh` if PR-B touches it.
|
|
28
|
-
|
|
29
|
-
## P3 — nice-to-have polish
|
|
30
|
-
|
|
31
|
-
### From Codex Critic Round 2
|
|
32
|
-
|
|
33
|
-
- **[P3-1]** Option C/D/E rejection rationale in v1 §4 says "Same as v0" — acceptable because v0 is co-located, but inline one-sentence rationale would make the v1 plan self-contained for future readers who do not have the v0 file.
|
|
34
|
-
|
|
35
|
-
### From Architect Round 1 (residual notes)
|
|
36
|
-
|
|
37
|
-
- Validate the `bug-patterns.json` Jaccard threshold (0.7) against actual past blocks once we have ≥20 historical reports — current threshold is hand-picked. Likely needs a small calibration script in `scripts/`.
|
|
38
|
-
- Consider whether `bug-reports/` should ship in the npm tarball default `.gitignore` of newly initialized projects — currently the schema doc only recommends operators add it themselves.
|
|
39
|
-
|
|
40
|
-
---
|
|
41
|
-
|
|
42
|
-
## Promotion criteria (when to re-ralplan one of these)
|
|
43
|
-
|
|
44
|
-
A backlog item moves back into a planner draft when **any** of these is true:
|
|
45
|
-
|
|
46
|
-
1. PR-A/B/C lands and we measure ≥3 BLOCKEDs where the deferred item would have moved D1 by ≥10 minutes (e.g. heartbeat warning would have pre-empted a 30-min wait).
|
|
47
|
-
2. Operator hand-files ≥2 bug reports about the same backlog gap (signal that the deferral was wrong).
|
|
48
|
-
3. The `bug-patterns.json` seed becomes too large for human authoring (≥30 entries) — triggers the pattern-learning loop item.
|
|
49
|
-
4. A user explicitly asks for one (e.g. operator-CLI `/rlp-desk recover` once they fatigue of jq pipelines).
|