taskforce-loop-engineering 0.8.2 → 0.8.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 CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.4 - 2026-08-07
4
+
5
+ - Reclaim queue locks immediately when their recorded owner PID no longer
6
+ exists, even if the lease has not expired. Scheduler status no longer reports
7
+ those dead-owner locks as live, and an orphaned `active/` task forces recovery
8
+ on the next timer wake-up instead of waiting for scheduler backoff or lease
9
+ expiry.
10
+
11
+ - Distinguish one-time secret human inputs from durable non-sensitive decisions
12
+ and attestations, so review approvals remain available as structured evidence
13
+ while OTPs, passwords, tokens, and credential values are still destroyed after
14
+ consumption.
15
+ - Restrict human-gate reconciliation to the final judgement's effective
16
+ checkpoint set for every outcome, preventing a project-in-progress tick from
17
+ resurrecting historical waiting gates after a newer checkpoint clears them.
18
+ - Allow an explicitly superseded human gate to requeue a task from `waiting/`
19
+ through the normal queue CLI instead of requiring a manual file move.
20
+
21
+ ## 0.8.3 - 2026-08-07
22
+
23
+ - Select the latest single-milestone checkpoint by durable checkpoint identity/sequence instead of regenerated acceptance-review timestamps, preventing a lexically late legacy `cp9` review from overriding a blocked `cp46+` checkpoint.
24
+ - Limit blocked human-input notification to the final judgement's effective checkpoint set, move blocked tasks from `failed/` into `waiting/`, and make the local Ironman scheduler run human-gate and terminal notification reconciliation after each notified tick.
25
+
3
26
  ## 0.8.2 - 2026-08-07
4
27
 
5
28
  - Classify transcript-compaction timeouts and selected transport failures as recoverable runtime interruptions instead of development revisions. Preserve accepted checkpoints, rotate the worker session, and return the same project task to the queue with bounded recovery attempts.
package/README.md CHANGED
@@ -370,6 +370,11 @@ loop-engineering queue-human-input-resolve \
370
370
  The human-input notifier scans active and terminal tasks, sends the concrete
371
371
  checkpoint blocker, and records a `waiting_for_human` gate. Resolution is
372
372
  idempotent; a terminal blocked task is requeued with the response attached.
373
+ OTP, password, token, credential, and verification-code gates are inferred as
374
+ one-time secrets and destroyed after consumption. Review decisions, approvals,
375
+ assignments, and attestations remain available in the gate event as durable
376
+ non-sensitive evidence. Use `--secret-input` or `--non-secret-input` to override
377
+ the inference when gate wording is ambiguous.
373
378
 
374
379
  Goal-directed controllers can use the exported `normalizeGoalDecision`,
375
380
  `goalLoopTransition`, and `goalStrategyFingerprint` helpers. They distinguish
@@ -154,6 +154,8 @@ function parseArgs(argv) {
154
154
  else if (a === '--notify-command') args.notifyCommand = argv[++i];
155
155
  else if (a === '--gate-id') args.gateId = argv[++i];
156
156
  else if (a === '--input') args.input = argv[++i];
157
+ else if (a === '--secret-input') args.secretInput = true;
158
+ else if (a === '--non-secret-input') args.nonSecretInput = true;
157
159
  else if (a === '--source-channel') args.sourceChannel = argv[++i];
158
160
  else if (a === '--source-target') args.sourceTarget = argv[++i];
159
161
  else if (a === '--source-account') args.sourceAccount = argv[++i];
@@ -2077,7 +2079,7 @@ async function queueRequeueCommand(args) {
2077
2079
  if (!args.taskId) throw new Error('queue-requeue requires --task-id.');
2078
2080
  const config = await loadQueueConfig(args.root, args.config);
2079
2081
  const options = mergeQueueOptions(config, args);
2080
- const result = await queueRequeue(args.root, options.queue, args.taskId);
2082
+ const result = await queueRequeue(args.root, options.queue, args.taskId, { from: args.from });
2081
2083
  if (args.json) console.log(JSON.stringify(result, null, 2));
2082
2084
  else console.log(`requeued ${result.taskId}: ${result.file}`);
2083
2085
  return 0;
package/lib/core.mjs CHANGED
@@ -1822,7 +1822,18 @@ export async function notifyHumanInputRequests(root, options = {}) {
1822
1822
  for (const [taskId, entry] of tasks) {
1823
1823
  if (!entry.task.source) continue;
1824
1824
  const checkpointsDir = path.join(taskRuntimeDirFor(root, queue, taskId), 'checkpoints');
1825
- for (const file of await listJson(checkpointsDir)) {
1825
+ let checkpointFiles = await listJson(checkpointsDir);
1826
+ const judgementFile = path.join(taskRuntimeDirFor(root, queue, taskId), 'final_judgement.json');
1827
+ if (await exists(judgementFile)) {
1828
+ const judgement = await readJson(judgementFile);
1829
+ const effectiveIds = Array.isArray(judgement?.coverage?.effective_review_ids)
1830
+ ? new Set(judgement.coverage.effective_review_ids)
1831
+ : null;
1832
+ if (effectiveIds?.size > 0) {
1833
+ checkpointFiles = checkpointFiles.filter((file) => effectiveIds.has(path.basename(file, '.json')));
1834
+ }
1835
+ }
1836
+ for (const file of checkpointFiles) {
1826
1837
  const checkpoint = await readJson(path.join(checkpointsDir, file));
1827
1838
  if (!['needs_human_input', 'blocked'].includes(checkpoint?.status)) continue;
1828
1839
  const checkpointId = checkpoint.checkpoint_id ?? path.basename(file, '.json');
@@ -1830,17 +1841,17 @@ export async function notifyHumanInputRequests(root, options = {}) {
1830
1841
  const ledgerFile = path.join(gatesDir, `${safeTaskId(taskId)}.${normalizeLoopId(checkpointId)}.json`);
1831
1842
  if (await exists(ledgerFile)) {
1832
1843
  const gate = await readJson(ledgerFile);
1833
- if (gate.status === 'waiting_for_human' && entry.subdir === 'inbox') {
1834
- const inboxFile = path.join(queueSubdirFor(root, queue, 'inbox'), `${safeTaskId(taskId)}.json`);
1835
- if (await exists(inboxFile)) {
1836
- const waitingFile = path.join(queueSubdirFor(root, queue, 'waiting'), path.basename(inboxFile));
1844
+ if (gate.status === 'waiting_for_human' && ['inbox', 'failed'].includes(entry.subdir)) {
1845
+ const sourceFile = path.join(queueSubdirFor(root, queue, entry.subdir), `${safeTaskId(taskId)}.json`);
1846
+ if (await exists(sourceFile)) {
1847
+ const waitingFile = path.join(queueSubdirFor(root, queue, 'waiting'), path.basename(sourceFile));
1837
1848
  await writeJson(waitingFile, {
1838
1849
  ...entry.task,
1839
1850
  status: 'waiting_for_human',
1840
1851
  waitingGateId: gateId,
1841
1852
  waitingSince: gate.requested_at ?? new Date().toISOString()
1842
1853
  });
1843
- await rm(inboxFile, { force: true });
1854
+ await rm(sourceFile, { force: true });
1844
1855
  }
1845
1856
  }
1846
1857
  results.push({ taskId, checkpointId, gateId, outcome: gate.status === 'resolved' ? 'resolved' : 'already_notified', ledger: path.relative(root, ledgerFile) });
@@ -1880,17 +1891,17 @@ export async function notifyHumanInputRequests(root, options = {}) {
1880
1891
  requested_at: new Date().toISOString(),
1881
1892
  notification: compactCommandResult(result)
1882
1893
  });
1883
- if (entry.subdir === 'inbox') {
1884
- const inboxFile = path.join(queueSubdirFor(root, queue, 'inbox'), `${safeTaskId(taskId)}.json`);
1885
- if (await exists(inboxFile)) {
1886
- const waitingFile = path.join(queueSubdirFor(root, queue, 'waiting'), path.basename(inboxFile));
1894
+ if (['inbox', 'failed'].includes(entry.subdir)) {
1895
+ const sourceFile = path.join(queueSubdirFor(root, queue, entry.subdir), `${safeTaskId(taskId)}.json`);
1896
+ if (await exists(sourceFile)) {
1897
+ const waitingFile = path.join(queueSubdirFor(root, queue, 'waiting'), path.basename(sourceFile));
1887
1898
  await writeJson(waitingFile, {
1888
1899
  ...entry.task,
1889
1900
  status: 'waiting_for_human',
1890
1901
  waitingGateId: gateId,
1891
1902
  waitingSince: new Date().toISOString()
1892
1903
  });
1893
- await rm(inboxFile, { force: true });
1904
+ await rm(sourceFile, { force: true });
1894
1905
  }
1895
1906
  }
1896
1907
  results.push({ taskId, checkpointId, gateId, outcome: 'sent', ledger: path.relative(root, ledgerFile) });
@@ -1921,28 +1932,40 @@ export async function resolveHumanInput(root, options = {}) {
1921
1932
  if (['resolved', 'consumed', 'satisfied'].includes(gate.status)) return { gate, outcome: 'already_resolved', ledger: path.relative(root, ledgerFile) };
1922
1933
  const receivedAt = new Date().toISOString();
1923
1934
  const response = options.input.trim();
1935
+ const requestText = `${gate.request ?? ''}\n${JSON.stringify(gate.needed ?? '')}`.toLowerCase();
1936
+ const inferredSecret = /(?:otp|one[- ]time|verification code|sms code|验证码|校验码|口令|password|api[ _-]?key|access[ _-]?token|secret|credential value|密钥|令牌)/i.test(requestText);
1937
+ const inputKind = options.secretInput === true
1938
+ ? 'secret'
1939
+ : options.nonSecretInput === true
1940
+ ? 'attestation'
1941
+ : inferredSecret ? 'secret' : 'attestation';
1942
+ const secretReceived = inputKind === 'secret';
1924
1943
  const secretDir = path.join(queueDirFor(root, queue), 'human-input', 'secrets');
1925
1944
  const eventsDir = path.join(queueDirFor(root, queue), 'human-input', 'events');
1926
- await mkdir(secretDir, { recursive: true, mode: 0o700 });
1927
1945
  await mkdir(eventsDir, { recursive: true });
1928
- const secretFile = path.join(secretDir, `${safeTaskId(taskId)}.${checkpointId}.${isoStamp()}.secret`);
1929
- const handle = await open(secretFile, 'wx', 0o600);
1930
- try {
1931
- await handle.writeFile(response);
1932
- } finally {
1933
- await handle.close();
1946
+ let secretFile = null;
1947
+ if (secretReceived) {
1948
+ await mkdir(secretDir, { recursive: true, mode: 0o700 });
1949
+ secretFile = path.join(secretDir, `${safeTaskId(taskId)}.${checkpointId}.${isoStamp()}.secret`);
1950
+ const handle = await open(secretFile, 'wx', 0o600);
1951
+ try {
1952
+ await handle.writeFile(response);
1953
+ } finally {
1954
+ await handle.close();
1955
+ }
1934
1956
  }
1935
1957
  const responseSha256 = createHash('sha256').update(response).digest('hex');
1936
1958
  const resolved = {
1937
1959
  ...gate,
1938
1960
  status: 'resolved',
1939
- secret_received: true,
1961
+ input_kind: inputKind,
1962
+ secret_received: secretReceived,
1940
1963
  response_sha256: responseSha256,
1941
- response_ref: path.relative(root, secretFile),
1964
+ ...(secretReceived ? { response_ref: path.relative(root, secretFile) } : { response }),
1942
1965
  response_message_id: options.sourceMessageId ?? null,
1943
1966
  resolved_at: receivedAt
1944
1967
  };
1945
- delete resolved.response;
1968
+ if (secretReceived) delete resolved.response;
1946
1969
  await writeJson(ledgerFile, resolved);
1947
1970
  const eventFile = path.join(eventsDir, `${safeTaskId(taskId)}.${checkpointId}.${isoStamp()}.json`);
1948
1971
  await writeJson(eventFile, {
@@ -1952,9 +1975,10 @@ export async function resolveHumanInput(root, options = {}) {
1952
1975
  task_id: taskId,
1953
1976
  checkpoint_id: checkpointId,
1954
1977
  status: 'pending_consumption',
1955
- secret_received: true,
1978
+ input_kind: inputKind,
1979
+ secret_received: secretReceived,
1956
1980
  response_sha256: responseSha256,
1957
- response_ref: path.relative(root, secretFile),
1981
+ ...(secretReceived ? { response_ref: path.relative(root, secretFile) } : { response }),
1958
1982
  response_message_id: options.sourceMessageId ?? null,
1959
1983
  created_at: receivedAt
1960
1984
  });
@@ -1968,7 +1992,8 @@ export async function resolveHumanInput(root, options = {}) {
1968
1992
  humanInput: {
1969
1993
  gate_id: options.gateId,
1970
1994
  checkpoint_id: checkpointId,
1971
- secret_received: true,
1995
+ input_kind: inputKind,
1996
+ secret_received: secretReceived,
1972
1997
  response_sha256: responseSha256,
1973
1998
  event: path.relative(root, eventFile),
1974
1999
  received_at: receivedAt
@@ -2019,8 +2044,10 @@ async function prepareHumanInputContext(root, queue, taskId) {
2019
2044
  checkpoint_id: consumed.checkpoint_id,
2020
2045
  status: consumed.status,
2021
2046
  secret_received: Boolean(consumed.secret_received),
2047
+ input_kind: consumed.input_kind ?? (consumed.secret_received ? 'secret' : 'attestation'),
2022
2048
  response_sha256: consumed.response_sha256 ?? null,
2023
2049
  response_ref: consumed.response_ref ? path.join(root, consumed.response_ref) : null,
2050
+ response: consumed.secret_received ? null : consumed.response ?? null,
2024
2051
  resolved_at: consumed.resolved_at ?? null,
2025
2052
  consumed_at: consumed.consumed_at ?? null
2026
2053
  });
@@ -2845,6 +2872,7 @@ export async function writeAcceptanceReviews(root, queue, task, taskContract, ac
2845
2872
  milestoneId: checkpoint.milestone_id ?? null,
2846
2873
  revisesCheckpointId: checkpoint.revises_checkpoint_id ?? null,
2847
2874
  sequence: Number(checkpoint.sequence ?? String(checkpoint.checkpoint_id ?? '').match(/(\d+)$/)?.[1] ?? 0),
2875
+ checkpointCreatedAt: checkpoint.created_at ?? null,
2848
2876
  createdAt: checkpoint.created_at ?? review.created_at,
2849
2877
  projectCompletion: checkpoint.project_completion ?? null,
2850
2878
  status: review.status,
@@ -2871,11 +2899,16 @@ function compareReviewSequence(a, b) {
2871
2899
  }
2872
2900
 
2873
2901
  function compareReviewRecency(a, b) {
2874
- const createdDelta = String(a.createdAt ?? '').localeCompare(String(b.createdAt ?? ''));
2875
- if (createdDelta !== 0 && a.createdAt && b.createdAt) return createdDelta;
2876
2902
  const aCheckpoint = Number(String(a.checkpointId ?? '').match(/(\d+)$/)?.[1] ?? 0);
2877
2903
  const bCheckpoint = Number(String(b.checkpointId ?? '').match(/(\d+)$/)?.[1] ?? 0);
2878
2904
  if (aCheckpoint !== bCheckpoint) return aCheckpoint - bCheckpoint;
2905
+ // Review artifacts are regenerated as a batch. Their generated_at values
2906
+ // therefore describe directory traversal order, not checkpoint recency.
2907
+ // Only trust a timestamp that came from the checkpoint itself.
2908
+ const checkpointCreatedDelta = String(a.checkpointCreatedAt ?? '').localeCompare(String(b.checkpointCreatedAt ?? ''));
2909
+ if (checkpointCreatedDelta !== 0 && a.checkpointCreatedAt && b.checkpointCreatedAt) return checkpointCreatedDelta;
2910
+ const sequenceDelta = Number(a.sequence ?? 0) - Number(b.sequence ?? 0);
2911
+ if (sequenceDelta !== 0) return sequenceDelta;
2879
2912
  return compareReviewSequence(a, b);
2880
2913
  }
2881
2914
 
@@ -3174,6 +3207,7 @@ export async function queueStatus(root, queue) {
3174
3207
  await ensureQueueDirs(root, queue);
3175
3208
  const activeFiles = await listJson(queueSubdirFor(root, queue, 'active'));
3176
3209
  const lock = await readQueueLock(root, queue);
3210
+ const lockOwnerAlive = queueLockOwnerAlive(lock);
3177
3211
  return {
3178
3212
  queue,
3179
3213
  queued: (await listJson(queueSubdirFor(root, queue, 'inbox'))).length,
@@ -3183,8 +3217,10 @@ export async function queueStatus(root, queue) {
3183
3217
  failed: (await listJson(queueSubdirFor(root, queue, 'failed'))).length,
3184
3218
  canceled: (await listJson(queueSubdirFor(root, queue, 'canceled'))).length,
3185
3219
  runs: (await listJson(queueSubdirFor(root, queue, 'runs'))).length,
3186
- locked: Boolean(lock && Date.parse(lock.expiresAt) > Date.now()),
3187
- lockExpiresAt: lock?.expiresAt ?? null
3220
+ locked: Boolean(lock && Date.parse(lock.expiresAt) > Date.now() && lockOwnerAlive),
3221
+ lockExpiresAt: lock?.expiresAt ?? null,
3222
+ lockPid: lock?.pid ?? null,
3223
+ lockOwnerAlive
3188
3224
  };
3189
3225
  }
3190
3226
 
@@ -3492,7 +3528,13 @@ export async function queueSchedulerTick(root, options) {
3492
3528
  const previous = await readQueueSchedulerState(root, queue);
3493
3529
  const previousProgress = await readQueueProgressState(root, queue);
3494
3530
  const statusBefore = await queueStatus(root, queue);
3495
- const due = !previous?.nextRunAt || Date.parse(previous.nextRunAt) <= nowMs || Boolean(options.forceDue);
3531
+ // An active task without a live lock owner is an orphan. Do not let a
3532
+ // previously scheduled backoff postpone recovery until nextRunAt.
3533
+ const orphanedActive = statusBefore.active > 0 && !statusBefore.locked;
3534
+ const due = orphanedActive
3535
+ || !previous?.nextRunAt
3536
+ || Date.parse(previous.nextRunAt) <= nowMs
3537
+ || Boolean(options.forceDue);
3496
3538
  let runResult = null;
3497
3539
  let executed = false;
3498
3540
  let status = due ? 'due' : 'not_due';
@@ -3751,11 +3793,27 @@ async function readQueueLock(root, queue) {
3751
3793
  }
3752
3794
  }
3753
3795
 
3796
+ function processIsAlive(pid) {
3797
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false;
3798
+ try {
3799
+ process.kill(pid, 0);
3800
+ return true;
3801
+ } catch (err) {
3802
+ // EPERM means the process exists but this user cannot signal it.
3803
+ return Boolean(err && err.code === 'EPERM');
3804
+ }
3805
+ }
3806
+
3807
+ function queueLockOwnerAlive(lock) {
3808
+ return Boolean(lock && processIsAlive(lock.pid));
3809
+ }
3810
+
3754
3811
  async function acquireQueueLock(root, queue, leaseMs) {
3755
3812
  const lockFile = path.join(queueDirFor(root, queue), 'queue.lock');
3756
3813
  const now = Date.now();
3757
3814
  const existing = await readQueueLock(root, queue);
3758
- if (existing && Date.parse(existing.expiresAt) > now) {
3815
+ const existingOwnerAlive = queueLockOwnerAlive(existing);
3816
+ if (existing && Date.parse(existing.expiresAt) > now && existingOwnerAlive) {
3759
3817
  return { acquired: false, lock: existing };
3760
3818
  }
3761
3819
  if (existing) await rm(lockFile, { force: true });
@@ -3770,7 +3828,14 @@ async function acquireQueueLock(root, queue, leaseMs) {
3770
3828
  try {
3771
3829
  handle = await open(lockFile, 'wx');
3772
3830
  await handle.writeFile(`${JSON.stringify(lock, null, 2)}\n`);
3773
- return { acquired: true, lock };
3831
+ return {
3832
+ acquired: true,
3833
+ lock,
3834
+ reclaimedLock: existing ? {
3835
+ lock: existing,
3836
+ reason: existingOwnerAlive ? 'expired_lease' : 'dead_owner_pid'
3837
+ } : null
3838
+ };
3774
3839
  } catch (err) {
3775
3840
  if (err && err.code === 'EEXIST') {
3776
3841
  return { acquired: false, lock: await readQueueLock(root, queue) };
@@ -3846,7 +3911,9 @@ export async function queueCancel(root, queue, taskId, options = {}) {
3846
3911
  export async function queueRequeue(root, queue, taskId, options = {}) {
3847
3912
  const normalized = normalizeLoopId(queue);
3848
3913
  await ensureQueueDirs(root, normalized);
3849
- const found = await findTaskFile(root, normalized, taskId, options.from ? [options.from] : ['failed', 'active', 'canceled']);
3914
+ const allowedSources = ['failed', 'active', 'waiting', 'canceled'];
3915
+ if (options.from && !allowedSources.includes(options.from)) throw new Error(`Unsupported requeue source: ${options.from}`);
3916
+ const found = await findTaskFile(root, normalized, taskId, options.from ? [options.from] : allowedSources);
3850
3917
  if (!found) throw new Error(`Task not found in requeueable state: ${taskId}`);
3851
3918
  const task = await readJson(found.file);
3852
3919
  const inboxFile = path.join(queueSubdirFor(root, normalized, 'inbox'), path.basename(found.file));
@@ -7757,7 +7824,7 @@ async function recoverStaleActive(root, queue, staleActiveMs) {
7757
7824
  return recovered;
7758
7825
  }
7759
7826
 
7760
- async function recoverOrphanActive(root, queue) {
7827
+ async function recoverOrphanActive(root, queue, recoveryReason = 'queue_lock_reacquired_with_active_task') {
7761
7828
  const activeDir = queueSubdirFor(root, queue, 'active');
7762
7829
  const inboxDir = queueSubdirFor(root, queue, 'inbox');
7763
7830
  const files = await listJson(activeDir);
@@ -7776,7 +7843,7 @@ async function recoverOrphanActive(root, queue) {
7776
7843
  orphanRecoveryCount: (task.orphanRecoveryCount ?? 0) + 1,
7777
7844
  requeuedAt: recoveredAt,
7778
7845
  requeuedFrom: 'active',
7779
- recoveryReason: 'queue_lock_reacquired_with_active_task'
7846
+ recoveryReason
7780
7847
  });
7781
7848
  recovered.push({
7782
7849
  taskId: task.id,
@@ -8176,7 +8243,10 @@ export async function runQueueOnce(root, options) {
8176
8243
  // queue lease. Any task left in active/ is therefore an orphan from an
8177
8244
  // interrupted parent runner. Requeue it immediately so existing
8178
8245
  // checkpoints can be reviewed and execution can resume in this tick.
8179
- const orphanRecovered = await recoverOrphanActive(root, queue);
8246
+ const recoveryReason = lockResult.reclaimedLock?.reason === 'dead_owner_pid'
8247
+ ? 'dead_queue_lock_owner_pid'
8248
+ : 'queue_lock_reacquired_with_active_task';
8249
+ const orphanRecovered = await recoverOrphanActive(root, queue, recoveryReason);
8180
8250
  if (orphanRecovered.length > 0) {
8181
8251
  progress.emit('queue', 'orphan_recovered', `Recovered ${orphanRecovered.length} orphan active task(s)`, {
8182
8252
  queue,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskforce-loop-engineering",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "private": false,
5
5
  "description": "OpenClaw-native loop engineering CLI, templates, and skill for verifiable agent work loops.",
6
6
  "type": "module",
@@ -16,6 +16,20 @@ const baseContract = { task_id: 't1', risk_level: 'L1', requires_human_gate: fal
16
16
  assert.equal(selectEffectiveAcceptanceReviews(devPlan, reviews)[0].checkpointId, 'cp10');
17
17
  }
18
18
 
19
+ {
20
+ const devPlan = { checkpoints: [{ id: 'cp1' }] };
21
+ const reviews = {
22
+ reviews: [
23
+ // Acceptance reviews are regenerated in filename traversal order. A
24
+ // legacy cp9 review can consequently have a later review timestamp than
25
+ // the real latest cp46 checkpoint; that timestamp must not win.
26
+ { checkpointId: 'cp46', sequence: 9, createdAt: '2026-08-07T18:27:18.990Z', status: 'blocked' },
27
+ { checkpointId: 'cp9', sequence: 1, createdAt: '2026-08-07T18:27:18.992Z', status: 'accepted' }
28
+ ]
29
+ };
30
+ assert.equal(selectEffectiveAcceptanceReviews(devPlan, reviews)[0].checkpointId, 'cp46');
31
+ }
32
+
19
33
  {
20
34
  const classification = dispatchFailureClassification({
21
35
  exitCode: 1,
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import assert from 'node:assert/strict';
3
- import { mkdtemp, readdir, rename, rm } from 'node:fs/promises';
3
+ import { access, mkdtemp, readdir, rename, rm } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import path from 'node:path';
6
6
  import {
@@ -11,6 +11,7 @@ import {
11
11
  notifyHumanInputRequests,
12
12
  notifyTerminalTasks,
13
13
  queueHumanDecision,
14
+ queueDirFor,
14
15
  queueStatus,
15
16
  queueSubdirFor,
16
17
  readJson,
@@ -92,6 +93,14 @@ const orphanInbox = path.join(queueSubdirFor(root, orphanQueue, 'inbox'), orphan
92
93
  const orphanActive = path.join(queueSubdirFor(root, orphanQueue, 'active'), orphanName);
93
94
  await writeJson(orphanActive, { ...(await readJson(orphanInbox)), status: 'active', startedAt: new Date().toISOString() });
94
95
  await rm(orphanInbox, { force: true });
96
+ // A future lease owned by a dead PID must not hide the orphan until expiry.
97
+ await writeJson(path.join(queueDirFor(root, orphanQueue), 'queue.lock'), {
98
+ version: 1,
99
+ queue: orphanQueue,
100
+ pid: 2_147_483_647,
101
+ acquiredAt: new Date().toISOString(),
102
+ expiresAt: new Date(Date.now() + 60_000).toISOString()
103
+ });
95
104
  await writeJson(path.join(taskRuntimeDirFor(root, orphanQueue, orphanRouted.task.id), 'checkpoints', 'cp-before-crash.json'), {
96
105
  version: 1,
97
106
  task_id: orphanRouted.task.id,
@@ -118,6 +127,7 @@ assert.ok(orphanRun.progress.some((event) => event.status === 'orphan_recovered'
118
127
  const orphanTerminal = await readJson(path.join(root, orphanRun.taskPath));
119
128
  assert.equal(orphanTerminal.orphanRecoveryCount, 1);
120
129
  assert.equal(orphanTerminal.requeuedFrom, 'active');
130
+ assert.equal(orphanTerminal.recoveryReason, 'dead_queue_lock_owner_pid');
121
131
  assert.equal((await readJson(path.join(taskRuntimeDirFor(root, orphanQueue, orphanRouted.task.id), 'checkpoints', 'cp-before-crash.json'))).checkpoint_id, 'cp-before-crash');
122
132
 
123
133
  const reviewQueue = 'human-review-smoke';
@@ -371,7 +381,9 @@ assert.match(gateDryRun.results[0].message, /Provide the SMS code/);
371
381
  const gateSent = await notifyHumanInputRequests(root, { queue, notifyCommand: '/bin/true' });
372
382
  assert.equal(gateSent.sent, 1);
373
383
  const gateId = gateSent.results[0].gateId;
374
- const resolved = await resolveHumanInput(root, { queue, gateId, input: '123456', sourceMessageId: 'reply-1' });
384
+ assert.equal((await readJson(path.join(queueSubdirFor(root, queue, 'waiting'), path.basename(failedFile)))).status, 'waiting_for_human');
385
+ await assert.rejects(access(failedFile));
386
+ const resolved = await resolveHumanInput(root, { queue, gateId, input: '123456', sourceMessageId: 'reply-1', secretInput: true });
375
387
  assert.equal(resolved.outcome, 'resolved_and_requeued');
376
388
  const requeuedTask = await readJson(path.join(queueSubdirFor(root, queue, 'inbox'), path.basename(failedFile)));
377
389
  assert.equal(requeuedTask.humanInput.secret_received, true);
@@ -380,6 +392,43 @@ const resolvedGate = await readJson(path.join(root, resolved.ledger ?? gateSent.
380
392
  assert.equal(JSON.stringify(resolvedGate).includes('123456'), false);
381
393
  assert.equal(resolvedGate.response_sha256.length, 64);
382
394
 
395
+ // Non-sensitive decisions and attestations remain available to the next worker
396
+ // tick instead of being destroyed like OTPs and credentials.
397
+ const attestationCheckpoint = path.join(taskRuntimeDirFor(root, queue, routed.task.id), 'checkpoints', 'cp-attestation.json');
398
+ await writeJson(attestationCheckpoint, {
399
+ version: 1, task_id: routed.task.id, checkpoint_id: 'cp-attestation', status: 'needs_human_input',
400
+ blockers: [{ id: 'review-decision', reason: 'Provide the independent review decision.' }],
401
+ verification: [], risks: [], next_action: 'wait'
402
+ });
403
+ await notifyHumanInputRequests(root, { queue, notifyCommand: '/bin/true' });
404
+ const attestationGateId = `${routed.task.id}:cp-attestation`;
405
+ const attestationResolved = await resolveHumanInput(root, {
406
+ queue,
407
+ gateId: attestationGateId,
408
+ input: 'Reviewer accepts the candidate for the next gated stage.'
409
+ });
410
+ assert.equal(attestationResolved.gate.input_kind, 'attestation');
411
+ assert.equal(attestationResolved.gate.secret_received, false);
412
+ assert.equal(attestationResolved.gate.response, 'Reviewer accepts the candidate for the next gated stage.');
413
+ assert.equal(attestationResolved.gate.response_ref, undefined);
414
+
415
+ // A project-in-progress judgement with a newer effective checkpoint must not
416
+ // resurrect historical waiting gates from older checkpoints.
417
+ const attestationDoneCheckpoint = path.join(taskRuntimeDirFor(root, queue, routed.task.id), 'checkpoints', 'cp-attestation-done.json');
418
+ await writeJson(attestationDoneCheckpoint, {
419
+ version: 1, task_id: routed.task.id, checkpoint_id: 'cp-attestation-done',
420
+ revises_checkpoint_id: 'cp-attestation', sequence: 2, status: 'ready_for_acceptance',
421
+ blockers: [], verification: [{ observation: 'attestation', result: 'accepted' }], risks: [], next_action: 'continue'
422
+ });
423
+ const finalJudgementFile = path.join(taskRuntimeDirFor(root, queue, routed.task.id), 'final_judgement.json');
424
+ await writeJson(finalJudgementFile, {
425
+ version: 1, task_id: routed.task.id, outcome: 'project_in_progress',
426
+ coverage: { effective_review_ids: ['cp-attestation-done'] }
427
+ });
428
+ const noHistoricalGateReplay = await notifyHumanInputRequests(root, { queue, dryRun: true });
429
+ assert.equal(noHistoricalGateReplay.inspected, 0);
430
+ await rm(finalJudgementFile, { force: true });
431
+
383
432
  // Queued tasks receive a non-sensitive event reference without being moved.
384
433
  const queuedCheckpoint = path.join(taskRuntimeDirFor(root, queue, routed.task.id), 'checkpoints', 'cp-queued.json');
385
434
  await writeJson(queuedCheckpoint, {
@@ -216,7 +216,7 @@ loop-engineering queue-revision-next --root <workspace> --queue <queue> --task-i
216
216
  loop-engineering queue-lineage --root <workspace> --queue <queue> --task-id <id>
217
217
  loop-engineering queue-lineage-bundle --root <workspace> --queue <queue> --task-id <id>
218
218
  loop-engineering queue-human-decision --root <workspace> --queue <queue> --task-id <id> --decision approve|request_changes|reject
219
- loop-engineering queue-human-input-resolve --root <workspace> --queue <queue> --gate-id <task:checkpoint> --input "<response>"
219
+ loop-engineering queue-human-input-resolve --root <workspace> --queue <queue> --gate-id <task:checkpoint> --input "<response>" [--secret-input|--non-secret-input]
220
220
  loop-engineering queue-terminal-notify --root <workspace> --queue <queue> (--notify-command "<command>" | --dry-run)
221
221
  loop-engineering queue-human-input-notify --root <workspace> --queue <queue> (--notify-command "<command>" | --dry-run)
222
222
  ```