taskforce-loop-engineering 0.8.2 → 0.8.3

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.3 - 2026-08-07
4
+
5
+ - 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.
6
+ - 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.
7
+
3
8
  ## 0.8.2 - 2026-08-07
4
9
 
5
10
  - 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/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 = judgement?.outcome === 'blocked' && 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) });
@@ -2845,6 +2856,7 @@ export async function writeAcceptanceReviews(root, queue, task, taskContract, ac
2845
2856
  milestoneId: checkpoint.milestone_id ?? null,
2846
2857
  revisesCheckpointId: checkpoint.revises_checkpoint_id ?? null,
2847
2858
  sequence: Number(checkpoint.sequence ?? String(checkpoint.checkpoint_id ?? '').match(/(\d+)$/)?.[1] ?? 0),
2859
+ checkpointCreatedAt: checkpoint.created_at ?? null,
2848
2860
  createdAt: checkpoint.created_at ?? review.created_at,
2849
2861
  projectCompletion: checkpoint.project_completion ?? null,
2850
2862
  status: review.status,
@@ -2871,11 +2883,16 @@ function compareReviewSequence(a, b) {
2871
2883
  }
2872
2884
 
2873
2885
  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
2886
  const aCheckpoint = Number(String(a.checkpointId ?? '').match(/(\d+)$/)?.[1] ?? 0);
2877
2887
  const bCheckpoint = Number(String(b.checkpointId ?? '').match(/(\d+)$/)?.[1] ?? 0);
2878
2888
  if (aCheckpoint !== bCheckpoint) return aCheckpoint - bCheckpoint;
2889
+ // Review artifacts are regenerated as a batch. Their generated_at values
2890
+ // therefore describe directory traversal order, not checkpoint recency.
2891
+ // Only trust a timestamp that came from the checkpoint itself.
2892
+ const checkpointCreatedDelta = String(a.checkpointCreatedAt ?? '').localeCompare(String(b.checkpointCreatedAt ?? ''));
2893
+ if (checkpointCreatedDelta !== 0 && a.checkpointCreatedAt && b.checkpointCreatedAt) return checkpointCreatedDelta;
2894
+ const sequenceDelta = Number(a.sequence ?? 0) - Number(b.sequence ?? 0);
2895
+ if (sequenceDelta !== 0) return sequenceDelta;
2879
2896
  return compareReviewSequence(a, b);
2880
2897
  }
2881
2898
 
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.3",
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 {
@@ -371,6 +371,8 @@ assert.match(gateDryRun.results[0].message, /Provide the SMS code/);
371
371
  const gateSent = await notifyHumanInputRequests(root, { queue, notifyCommand: '/bin/true' });
372
372
  assert.equal(gateSent.sent, 1);
373
373
  const gateId = gateSent.results[0].gateId;
374
+ assert.equal((await readJson(path.join(queueSubdirFor(root, queue, 'waiting'), path.basename(failedFile)))).status, 'waiting_for_human');
375
+ await assert.rejects(access(failedFile));
374
376
  const resolved = await resolveHumanInput(root, { queue, gateId, input: '123456', sourceMessageId: 'reply-1' });
375
377
  assert.equal(resolved.outcome, 'resolved_and_requeued');
376
378
  const requeuedTask = await readJson(path.join(queueSubdirFor(root, queue, 'inbox'), path.basename(failedFile)));