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 +5 -0
- package/lib/core.mjs +30 -13
- package/package.json +1 -1
- package/scripts/final-judgement-self-test.mjs +14 -0
- package/scripts/route-notify-self-test.mjs +3 -1
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
|
-
|
|
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
|
|
1834
|
-
const
|
|
1835
|
-
if (await exists(
|
|
1836
|
-
const waitingFile = path.join(queueSubdirFor(root, queue, 'waiting'), path.basename(
|
|
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(
|
|
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
|
|
1884
|
-
const
|
|
1885
|
-
if (await exists(
|
|
1886
|
-
const waitingFile = path.join(queueSubdirFor(root, queue, 'waiting'), path.basename(
|
|
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(
|
|
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
|
@@ -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)));
|