taskforce-loop-engineering 0.15.15 → 0.15.17

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
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.15.17 - 2026-09-13
6
+
7
+ - Stop OpenClaw doctor and disposable smoke probes from attaching the synthetic `doctor` account to dry-run notifications.
8
+ - Preserve normal account forwarding for real source-bound notifications while remaining compatible with OpenClaw installations that reject unknown account ids during dry-run validation.
9
+ - Add strict installer regression coverage that rejects any synthetic `--account doctor` delivery.
10
+
11
+ ## 0.15.16 - 2026-09-13
12
+
13
+ - Resolve canonical project identifiers from nested checkpoint completion metadata so migrated project tasks continue against the correct authoritative backlog.
14
+ - Keep future external-action authorization gates deferred while safe local project work remains actionable.
15
+ - Add regression coverage for checkpoint-only project metadata and premature deferred-gate reconciliation.
16
+
5
17
  ## 0.15.15 - 2026-08-31
6
18
 
7
19
  - Lock a task-level accepted terminal checkpoint monotonically so later broader-project governance cannot reopen or overwrite the completed task.
package/lib/core.mjs CHANGED
@@ -1214,7 +1214,12 @@ function requirementIdsFromCheckpoint(checkpoint, specs) {
1214
1214
  }
1215
1215
 
1216
1216
  function inferProjectSpec(task, checkpoint, specs, requirementIds = []) {
1217
- const explicit = checkpoint?.project_id ?? checkpoint?.projectId ?? task?.project_id ?? task?.projectId;
1217
+ const explicit = checkpoint?.project_id
1218
+ ?? checkpoint?.projectId
1219
+ ?? checkpoint?.project_completion?.project
1220
+ ?? checkpoint?.projectCompletion?.project
1221
+ ?? task?.project_id
1222
+ ?? task?.projectId;
1218
1223
  if (explicit) return specs.find((spec) => spec.project === explicit) ?? null;
1219
1224
  const text = `${task?.title ?? ''}\n${task?.body ?? ''}`.toLowerCase();
1220
1225
  const candidates = specs.map((spec) => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskforce-loop-engineering",
3
- "version": "0.15.15",
3
+ "version": "0.15.17",
4
4
  "private": false,
5
5
  "description": "OpenClaw-native loop engineering CLI, templates, and skill for verifiable agent work loops.",
6
6
  "type": "module",
@@ -70,7 +70,7 @@ async function main() {
70
70
  if (await present(notifier)) {
71
71
  const smoke = await run(process.execPath, [notifier, 'Loop Engineering notification dry-run'], {
72
72
  cwd: args.root,
73
- env: { ...process.env, LOOP_NOTIFICATION_DRY_RUN: '1', LOOP_NOTIFICATION_SOURCE: JSON.stringify({ channel: 'feishu', target: 'user:loop-doctor-dry-run', account: 'doctor', reply_to: 'doctor-message' }) }
73
+ env: { ...process.env, LOOP_NOTIFICATION_DRY_RUN: '1', LOOP_NOTIFICATION_SOURCE: JSON.stringify({ channel: 'feishu', target: 'user:loop-doctor-dry-run', reply_to: 'doctor-message' }) }
74
74
  });
75
75
  checks.push({ id: 'notification_dry_run', ok: smoke.code === 0, detail: (smoke.stdout || smoke.stderr).trim().slice(0, 500) });
76
76
  }
@@ -15,6 +15,10 @@ await writeFile(mockOpenClaw, `#!/usr/bin/env node
15
15
  import { mkdir, writeFile } from 'node:fs/promises';
16
16
  import path from 'node:path';
17
17
  const args = process.argv.slice(2);
18
+ if (args.includes('--account') && args[args.indexOf('--account') + 1] === 'doctor') {
19
+ console.error('Unknown account: doctor');
20
+ process.exit(1);
21
+ }
18
22
  if (args[0] === '--version') console.log('OpenClaw mock 1.0');
19
23
  else if (args[0] === 'agents' && args[1] === 'list') console.log(JSON.stringify([{ id: 'builder' }]));
20
24
  else {
@@ -132,6 +136,7 @@ const doctorResult = await new Promise((resolve) => {
132
136
  if (doctorResult.code !== 0) throw new Error(`doctor failed: ${doctorResult.stderr}`);
133
137
  const doctorReport = JSON.parse(doctorResult.stdout);
134
138
  if (doctorReport.status !== 'ok' || doctorReport.externalWrite !== false || !doctorReport.checks.some((check) => check.id === 'notification_dry_run' && check.ok) || !doctorReport.checks.some((check) => check.id === 'human_gate_bridge_self_test' && check.ok)) throw new Error('doctor did not complete safe notification and Human Gate self-tests');
139
+ if (JSON.parse(await readFile(deliveryCapture, 'utf8')).includes('doctor')) throw new Error('doctor notification forwarded a synthetic account');
135
140
  const smoke = new URL('./openclaw-smoke.mjs', import.meta.url).pathname;
136
141
  const smokeSource = await readFile(smoke, 'utf8');
137
142
  if (!smokeSource.includes('Do not change user or project files, configuration, credentials, or external state.')
@@ -151,6 +156,7 @@ const smokeResult = await new Promise((resolve) => {
151
156
  if (smokeResult.code !== 0) throw new Error(`smoke failed: ${smokeResult.stderr}`);
152
157
  const smokeReport = JSON.parse(smokeResult.stdout);
153
158
  if (smokeReport.status !== 'ok' || smokeReport.externalWrite !== false || !smokeReport.steps.every((step) => step.ok)) throw new Error('end-to-end smoke did not pass safely');
159
+ if (JSON.parse(await readFile(deliveryCapture, 'utf8')).includes('doctor')) throw new Error('smoke notification forwarded a synthetic account');
154
160
  try { await readFile(path.join(root, `configs/loops/queues/${smokeReport.smokeQueue}.json`)); throw new Error('smoke config was not cleaned'); } catch (error) { if (error.code !== 'ENOENT') throw error; }
155
161
  try { await readFile(path.join(root, `runtime/loops/${smokeReport.smokeQueue}/state.json`)); throw new Error('smoke runtime was not cleaned'); } catch (error) { if (error.code !== 'ENOENT') throw error; }
156
162
  for (const generated of ['scripts/loops/openclaw-loop-dispatch.mjs', 'scripts/loops/openclaw-loop.mjs', 'scripts/loops/openclaw-loop-notify.mjs', 'scripts/loops/openclaw-loop-gate.mjs']) {
@@ -65,7 +65,7 @@ async function main() {
65
65
  await writeFile(smokeConfig, `${JSON.stringify(config, null, 2)}\n`);
66
66
  const smokeRuntimeRel = path.relative(args.root, smokeRuntime);
67
67
  const message = `走 loop:Perform a read-only integration smoke. Do not change user or project files, configuration, credentials, or external state. Writing the required Loop checkpoint and verification evidence under ${smokeRuntimeRel}/ is allowed and required; do not write anywhere else. Report SMOKE_OK with verification evidence.`;
68
- const route = await run(args.loopBin, ['route-message', '--root', args.root, '--queue', smokeQueue, '--message', message, '--route', '--confirm-execute', '--source-channel', 'feishu', '--source-target', 'user:loop-smoke-dry-run', '--source-account', 'doctor', '--source-message-id', `smoke-${runToken}`, '--source-reply-to', `smoke-${runToken}`, '--json'], { cwd: args.root });
68
+ const route = await run(args.loopBin, ['route-message', '--root', args.root, '--queue', smokeQueue, '--message', message, '--route', '--confirm-execute', '--source-channel', 'feishu', '--source-target', 'user:loop-smoke-dry-run', '--source-message-id', `smoke-${runToken}`, '--source-reply-to', `smoke-${runToken}`, '--json'], { cwd: args.root });
69
69
  steps.push({ id: 'route', ok: route.code === 0 });
70
70
  if (route.code !== 0) throw new Error(`route failed: ${route.stderr || route.stdout}`);
71
71
  const routed = JSON.parse(route.stdout);
@@ -1,5 +1,5 @@
1
1
  import assert from 'node:assert/strict';
2
- import { mkdtemp, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { access, mkdtemp, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { doctorReport, enqueueTask, notifyHumanInputRequests, projectStatus, queueStatus, reconcileProjectGates, resolveHumanInput, routeLoopMessage, taskRuntimeDirFor, writeTaskContract } from '../lib/core.mjs';
@@ -176,6 +176,46 @@ const futureNotice = await notifyHumanInputRequests(root, { queue, notifyCommand
176
176
  assert.equal(futureNotice.results.some((item) => item.taskId === futureGateTask.task.id), false);
177
177
  assert.equal((await queueStatus(root, queue)).waiting, 0);
178
178
 
179
+ // Older queue carriers can lack project metadata while the worker checkpoint
180
+ // records the canonical project id inside project_completion. Resolve that id
181
+ // before evaluating deferred gates so safe local backlog keeps running.
182
+ const nestedProjectTask = await enqueueTask(root, {
183
+ queue,
184
+ title: 'Continue R1-R7',
185
+ task: 'Continue the next safe local milestone',
186
+ sourceChannel: 'test',
187
+ sourceTarget: 'owner',
188
+ sourceAccount: 'main'
189
+ });
190
+ const nestedProjectDir = path.join(taskRuntimeDirFor(root, queue, nestedProjectTask.task.id), 'checkpoints');
191
+ await mkdir(nestedProjectDir, { recursive: true });
192
+ await writeFile(path.join(nestedProjectDir, 'cp1.json'), `${JSON.stringify({
193
+ version: 1,
194
+ task_id: nestedProjectTask.task.id,
195
+ checkpoint_id: 'cp1',
196
+ status: 'ready_for_acceptance',
197
+ blockers: [],
198
+ project_completion: { project: 'openreel', status: 'in_progress' },
199
+ next_action: 'Implement LOCAL-02 locally.',
200
+ deferred_gates: [{
201
+ id: 'production-later',
202
+ action: 'production_deploy',
203
+ required_authority: 'Owner authorization after local candidate acceptance.',
204
+ authorization_state: 'missing',
205
+ needed_when: 'now',
206
+ materialize: true
207
+ }]
208
+ }, null, 2)}\n`);
209
+ const nestedProjectNotice = await notifyHumanInputRequests(root, { queue, notifyCommand: '/bin/true' });
210
+ assert.equal(nestedProjectNotice.results.some((item) => item.taskId === nestedProjectTask.task.id), false);
211
+ const nestedProjectStatus = await queueStatus(root, queue);
212
+ assert.equal(nestedProjectStatus.waiting, 0);
213
+ await access(path.join(root, nestedProjectTask.file));
214
+ await assert.rejects(
215
+ access(path.join(taskRuntimeDirFor(root, queue, nestedProjectTask.task.id), 'human_input_gate.json')),
216
+ { code: 'ENOENT' }
217
+ );
218
+
179
219
  // Conditional policy boundaries are not current blockers. Plain prose in
180
220
  // deferred_gates must not materialize a gate without a concrete action and
181
221
  // authority requirement.
@@ -1,7 +1,7 @@
1
1
  # npm Package
2
2
 
3
3
  Package name: `taskforce-loop-engineering`
4
- Version: `0.15.15`
4
+ Version: `0.15.17`
5
5
 
6
6
  Install from npm:
7
7