taskforce-loop-engineering 0.7.0 → 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 +13 -0
- package/lib/core.mjs +49 -6
- package/package.json +1 -1
- package/scripts/route-notify-self-test.mjs +51 -0
- package/skills/taskforce-loop-engineering/SKILL.md +79 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
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
|
+
|
|
9
|
+
## 0.7.1 - 2026-08-05
|
|
10
|
+
|
|
11
|
+
- Make the standalone ClawHub skill self-sufficient by documenting the official npm package, GitHub repository, Node.js requirement, license, and installation commands.
|
|
12
|
+
- Distinguish installing the skill, installing the CLI, and integrating the CLI with OpenClaw.
|
|
13
|
+
- Add plan, confirmed install, doctor, and disposable smoke commands for a verifiable OpenClaw deployment.
|
|
14
|
+
- Require checking an exact workspace source path before using the source-only CLI fallback.
|
|
15
|
+
|
|
3
16
|
## 0.7.0 - 2026-08-05
|
|
4
17
|
|
|
5
18
|
- Rename the project, npm package, GitHub repository, and bundled skill to Taskforce Loop Engineering.
|
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
|
|
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
|
-
|
|
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
|
-
...(
|
|
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?.
|
|
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 === '
|
|
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
|
@@ -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,
|
|
@@ -9,6 +9,85 @@ Use this skill only when the user explicitly invokes Loop Engineering, says `走
|
|
|
9
9
|
|
|
10
10
|
Do not route ordinary chat, research, explanations, or simple direct tasks into a loop unless the user explicitly invokes it.
|
|
11
11
|
|
|
12
|
+
## Distribution and CLI Installation
|
|
13
|
+
|
|
14
|
+
A ClawHub installation may provide only this `SKILL.md`; it does **not** prove that the Loop Engineering CLI or OpenClaw integration is installed. Before running loop commands, check the deployment explicitly:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
command -v loop-engineering
|
|
18
|
+
loop-engineering --help
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Official distribution:
|
|
22
|
+
|
|
23
|
+
- npm package: `taskforce-loop-engineering`
|
|
24
|
+
- GitHub repository: `https://github.com/ambitioncn/taskforce-loop-engineering`
|
|
25
|
+
- ClawHub skill: `https://clawhub.ai/ambitioncn/skills/taskforce-loop-engineering`
|
|
26
|
+
- license: Apache-2.0
|
|
27
|
+
- runtime requirement: Node.js 22 or newer
|
|
28
|
+
|
|
29
|
+
Install the CLI globally from npm:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
node --version
|
|
33
|
+
npm install -g taskforce-loop-engineering
|
|
34
|
+
loop-engineering --help
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
For a temporary read-only invocation without a global install:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx -p taskforce-loop-engineering loop-engineering --help
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For source-based development, clone the official repository and install its dependencies:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git clone https://github.com/ambitioncn/taskforce-loop-engineering.git
|
|
47
|
+
cd taskforce-loop-engineering
|
|
48
|
+
npm install
|
|
49
|
+
npm run check
|
|
50
|
+
node bin/loop-engineering.mjs --help
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Do not guess a workspace source path. Use `node packages/loop-engineering/bin/loop-engineering.mjs ...` only after confirming that exact path exists in the current workspace.
|
|
54
|
+
|
|
55
|
+
### OpenClaw Integration
|
|
56
|
+
|
|
57
|
+
Installing the npm package exposes the CLI, but it does not automatically route conversations, select a worker agent, or create queue wrappers. First generate a read-only installation plan:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
loop-engineering-openclaw-install \
|
|
61
|
+
--root /path/to/openclaw/workspace \
|
|
62
|
+
--queue agent-tasks
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Review the detected agents and planned files. Then install with an existing worker-agent id:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
loop-engineering-openclaw-install \
|
|
69
|
+
--root /path/to/openclaw/workspace \
|
|
70
|
+
--queue agent-tasks \
|
|
71
|
+
--worker-agent main \
|
|
72
|
+
--confirm-install
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The installer never creates the worker agent. After installation, verify wiring before using a real task:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
loop-engineering-openclaw-doctor \
|
|
79
|
+
--root /path/to/openclaw/workspace \
|
|
80
|
+
--queue agent-tasks \
|
|
81
|
+
--worker-agent main
|
|
82
|
+
|
|
83
|
+
loop-engineering-openclaw-smoke \
|
|
84
|
+
--root /path/to/openclaw/workspace \
|
|
85
|
+
--queue agent-tasks \
|
|
86
|
+
--worker-agent main
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
If the CLI or integration is missing and the user requested installation or repair, install it within the authorized host/workspace scope, then run doctor and the disposable smoke. If the user only asked what is missing, report the exact package, repository, commands, and current deployment state without mutating the system.
|
|
90
|
+
|
|
12
91
|
## Conversation Contract
|
|
13
92
|
|
|
14
93
|
Interpret explicit loop language as follows:
|