taskforce-loop-engineering 0.7.1 → 0.7.2

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.2 - 2026-08-06
4
+
5
+ - Keep `ready_for_human_review` tasks out of `done/` until an explicit human decision is recorded; emit a scoped acceptance notification, fail closed when delivery routing is missing, and transition approved tasks to `completed` only after approval.
6
+ - Forward configured human-gate policy into generated task contracts so queue state and final judgement agree.
7
+ - Add regression coverage for the complete `ready_for_human_review → approve → completed` lifecycle.
8
+
3
9
  ## 0.7.1 - 2026-08-05
4
10
 
5
11
  - Make the standalone ClawHub skill self-sufficient by documenting the official npm package, GitHub repository, Node.js requirement, license, and installation commands.
package/lib/core.mjs CHANGED
@@ -1673,13 +1673,20 @@ export async function routeLoopMessage(root, options = {}) {
1673
1673
  }
1674
1674
 
1675
1675
  function terminalNotificationMessage(queue, task) {
1676
- const needsHuman = ['needs_human_input', 'blocked'].includes(task.status);
1676
+ const needsReview = task.status === 'ready_for_human_review';
1677
+ const needsHuman = ['needs_human_input', 'blocked', 'ready_for_human_review'].includes(task.status);
1677
1678
  return [
1678
- needsHuman ? 'Loop task needs human input' : 'Loop task reached a terminal state',
1679
+ needsReview ? 'Loop task is ready for human acceptance'
1680
+ : needsHuman ? 'Loop task needs human input'
1681
+ : 'Loop task reached a terminal state',
1679
1682
  `task: ${task.title}`,
1680
1683
  `queue: ${queue}`,
1681
1684
  `status: ${task.status}`,
1682
- ...(needsHuman ? ['next: inspect the task final judgement and checkpoints, resolve the blocker, then explicitly continue or requeue.'] : [])
1685
+ ...(needsReview
1686
+ ? [`next: review the final judgement and record approve, request_changes, or reject for task ${task.id}.`]
1687
+ : needsHuman
1688
+ ? ['next: inspect the task final judgement and checkpoints, resolve the blocker, then explicitly continue or requeue.']
1689
+ : [])
1683
1690
  ].join('\n');
1684
1691
  }
1685
1692
 
@@ -1696,7 +1703,16 @@ export async function notifyTerminalTasks(root, options = {}) {
1696
1703
  const dir = queueSubdirFor(root, queue, subdir);
1697
1704
  for (const file of await listJson(dir)) {
1698
1705
  const task = await readJson(path.join(dir, file));
1699
- if (!task?.source || !task?.id || !task?.status) continue;
1706
+ if (!task?.id || !task?.status) continue;
1707
+ if (!task?.source?.channel || !task?.source?.target) {
1708
+ results.push({
1709
+ taskId: task.id,
1710
+ status: task.status,
1711
+ outcome: 'failed',
1712
+ error: 'missing source.channel/source.target; refusing unscoped terminal delivery'
1713
+ });
1714
+ continue;
1715
+ }
1700
1716
  const key = `${safeTaskId(task.id)}.${normalizeLoopId(task.status)}.json`;
1701
1717
  const ledgerFile = path.join(notificationDir, key);
1702
1718
  if (await exists(ledgerFile)) {
@@ -2792,7 +2808,8 @@ function queueStatusFromFinalJudgement(currentStatus, finalJudgement) {
2792
2808
  if (!finalJudgement?.judgement) return currentStatus;
2793
2809
  if (currentStatus !== 'completed') return currentStatus;
2794
2810
  const outcome = finalJudgement.judgement.outcome;
2795
- if (outcome === 'ready_for_human_review' || outcome === 'ready_to_apply') return currentStatus;
2811
+ if (outcome === 'ready_to_apply') return currentStatus;
2812
+ if (outcome === 'ready_for_human_review') return outcome;
2796
2813
  return outcome;
2797
2814
  }
2798
2815
 
@@ -4069,6 +4086,27 @@ export async function queueHumanDecision(root, queue, taskId, options = {}) {
4069
4086
  };
4070
4087
  await writeJson(file, artifact);
4071
4088
 
4089
+ let transitionedTask = null;
4090
+ if (decision === 'approve' && found.subdir !== 'done') {
4091
+ const completedAt = artifact.created_at;
4092
+ transitionedTask = {
4093
+ ...task,
4094
+ status: 'completed',
4095
+ humanApprovedAt: completedAt,
4096
+ humanReviewDecision: path.relative(root, file)
4097
+ };
4098
+ const completedFile = path.join(queueSubdirFor(root, normalized, 'done'), path.basename(found.file));
4099
+ await writeJson(completedFile, transitionedTask);
4100
+ await rm(found.file, { force: true });
4101
+ artifact.effects.queueTransition = {
4102
+ from: found.subdir,
4103
+ to: 'done',
4104
+ status: 'completed',
4105
+ file: path.relative(root, completedFile)
4106
+ };
4107
+ await writeJson(file, artifact);
4108
+ }
4109
+
4072
4110
  let revisionRequest = null;
4073
4111
  if (decision === 'request_changes') {
4074
4112
  const revisionFile = path.join(dir, 'human_revision_request.json');
@@ -4098,6 +4136,7 @@ export async function queueHumanDecision(root, queue, taskId, options = {}) {
4098
4136
  decisionFile: path.relative(root, file),
4099
4137
  revisionRequestFile: revisionRequest?.path ?? null,
4100
4138
  revisionNext,
4139
+ transitionedTask,
4101
4140
  artifact
4102
4141
  };
4103
4142
  }
@@ -7901,7 +7940,11 @@ export async function runQueueOnce(root, options) {
7901
7940
  let revisionRequest = null;
7902
7941
 
7903
7942
  try {
7904
- taskContract = await writeTaskContract(root, queue, task);
7943
+ taskContract = await writeTaskContract(root, queue, task, {
7944
+ riskLevel: options.riskLevel,
7945
+ riskReasons: options.riskReasons,
7946
+ requiresHumanGate: options.requiresHumanGate
7947
+ });
7905
7948
  progress.emit('planning', 'task_contract', `Wrote task contract (${taskContract.contract.risk_level})`, {
7906
7949
  taskId: task.id,
7907
7950
  artifact: taskContract.file,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskforce-loop-engineering",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "private": false,
5
5
  "description": "OpenClaw-native loop engineering CLI, templates, and skill for verifiable agent work loops.",
6
6
  "type": "module",
@@ -10,6 +10,8 @@ import {
10
10
  normalizeGoalDecision,
11
11
  notifyHumanInputRequests,
12
12
  notifyTerminalTasks,
13
+ queueHumanDecision,
14
+ queueStatus,
13
15
  queueSubdirFor,
14
16
  readJson,
15
17
  routeLoopMessage,
@@ -76,6 +78,55 @@ const contract = await readJson(path.join(taskRuntimeDirFor(root, queue, routed.
76
78
  assert.equal(contract.risk_level, 'model_assessed');
77
79
  assert.equal(contract.requires_human_gate, false);
78
80
 
81
+ const reviewQueue = 'human-review-smoke';
82
+ const reviewTask = await routeLoopMessage(root, {
83
+ route: true,
84
+ confirmExecute: true,
85
+ queue: reviewQueue,
86
+ message: '走 loop 生成需要人工验收的交付物',
87
+ sourceChannel: 'feishu',
88
+ sourceTarget: 'user-1'
89
+ });
90
+ let reviewCheckpointWrite = null;
91
+ const reviewRun = await runQueueOnce(root, {
92
+ queue: reviewQueue,
93
+ dispatcher: '/bin/sleep 0.1',
94
+ requiresHumanGate: true,
95
+ timeoutMs: 10_000,
96
+ leaseMs: 20_000,
97
+ staleActiveMs: 60_000,
98
+ onProgress: (event) => {
99
+ if (event.phase !== 'dispatch' || event.status !== 'running' || reviewCheckpointWrite) return;
100
+ reviewCheckpointWrite = writeJson(path.join(taskRuntimeDirFor(root, reviewQueue, reviewTask.task.id), 'checkpoints', 'cp-review.json'), {
101
+ version: 1,
102
+ task_id: reviewTask.task.id,
103
+ checkpoint_id: 'cp-review',
104
+ status: 'ready_for_acceptance',
105
+ summary: 'The deliverable is complete and explicitly awaits human acceptance.',
106
+ files_changed: [],
107
+ verification: [{ command: '/bin/true', outcome: 'passed' }],
108
+ blockers: [],
109
+ risks: [],
110
+ next_action: 'human_acceptance'
111
+ });
112
+ }
113
+ });
114
+ await reviewCheckpointWrite;
115
+ assert.equal(reviewRun.status, 'ready_for_human_review');
116
+ assert.match(reviewRun.taskPath, /failed/);
117
+ const reviewStatus = await queueStatus(root, reviewQueue);
118
+ assert.equal(reviewStatus.done, 0);
119
+ assert.equal(reviewStatus.failed, 1);
120
+ const reviewNotice = await notifyTerminalTasks(root, { queue: reviewQueue, dryRun: true });
121
+ assert.equal(reviewNotice.results[0].outcome, 'dry_run');
122
+ assert.match(reviewNotice.results[0].message, /ready for human acceptance/);
123
+ assert.match(reviewNotice.results[0].message, /approve, request_changes, or reject/);
124
+ const reviewDecision = await queueHumanDecision(root, reviewQueue, reviewTask.task.id, { decision: 'approve' });
125
+ assert.equal(reviewDecision.transitionedTask.status, 'completed');
126
+ const approvedStatus = await queueStatus(root, reviewQueue);
127
+ assert.equal(approvedStatus.done, 1);
128
+ assert.equal(approvedStatus.failed, 0);
129
+
79
130
  for (const suffix of ['second', 'third']) {
80
131
  await routeLoopMessage(root, {
81
132
  route: true,