taskforce-loop-engineering 0.15.14 → 0.15.15

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,13 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.15.15 - 2026-08-31
6
+
7
+ - Lock a task-level accepted terminal checkpoint monotonically so later broader-project governance cannot reopen or overwrite the completed task.
8
+ - Require fully materialized, source/actor/generation-bound Human Gate artifacts before moving tasks to waiting or sending actionable notifications.
9
+ - Fail closed for incomplete or non-authoritative Gate artifacts, and keep dry-run previews free of actionable reply commands.
10
+ - Add regressions for broader-project scope creep after task acceptance, empty/unbound gates, authoritative waiting placement, and notification consistency.
11
+
5
12
  ## 0.15.14 - 2026-08-30
6
13
 
7
14
  - Add one authoritative cross-interface Human Gate command core for Dashboard and trusted chat adapters, with strict source binding, generation fencing, idempotency receipts, confirmation escalation, and fail-closed handling of ordinary chat.
package/lib/core.mjs CHANGED
@@ -2453,6 +2453,33 @@ function humanInputMessage(queue, task, checkpoint, gateId, language = 'en', blo
2453
2453
  ].join('\n');
2454
2454
  }
2455
2455
 
2456
+ function authoritativeGateBindings(task) {
2457
+ const source = task?.source;
2458
+ const generation = Number(task?.runtimeSessionGeneration ?? 0);
2459
+ if (!source?.channel || !source?.target || !source?.account || !Number.isInteger(generation) || generation < 0) return null;
2460
+ return {
2461
+ source_binding: {
2462
+ channel: source.channel,
2463
+ target: source.target,
2464
+ account: source.account,
2465
+ message_id: source.messageId ?? source.message_id ?? null
2466
+ },
2467
+ actor_binding: { kind: 'source_target', actor_id: source.target, account: source.account },
2468
+ generation
2469
+ };
2470
+ }
2471
+
2472
+ function authoritativeGateReady(gate, task) {
2473
+ const expected = authoritativeGateBindings(task);
2474
+ if (!expected || !gate || gate.status !== 'waiting_for_human') return false;
2475
+ return gate.source_binding?.channel === expected.source_binding.channel
2476
+ && gate.source_binding?.target === expected.source_binding.target
2477
+ && gate.source_binding?.account === expected.source_binding.account
2478
+ && gate.actor_binding?.actor_id === expected.actor_binding.actor_id
2479
+ && gate.actor_binding?.account === expected.actor_binding.account
2480
+ && gate.generation === expected.generation;
2481
+ }
2482
+
2456
2483
  function blockerCoveredByContractAuthorization(blocker, contract) {
2457
2484
  if (!blocker || typeof blocker !== 'object' || Array.isArray(blocker)) return false;
2458
2485
  const state = String(blocker.authorization_state ?? blocker.state ?? '')
@@ -2660,8 +2687,17 @@ export async function notifyHumanInputRequests(root, options = {}) {
2660
2687
  const checkpointId = checkpoint.checkpoint_id ?? path.basename(file, '.json');
2661
2688
  const gateId = `${taskId}:${checkpointId}`;
2662
2689
  const ledgerFile = path.join(gatesDir, `${safeTaskId(taskId)}.${normalizeLoopId(checkpointId)}.json`);
2690
+ const bindings = authoritativeGateBindings(entry.task);
2691
+ if (!bindings) {
2692
+ results.push({ taskId, checkpointId, gateId, outcome: 'fail_closed', error: 'incomplete_source_actor_generation_binding' });
2693
+ continue;
2694
+ }
2663
2695
  if (await exists(ledgerFile)) {
2664
2696
  const gate = await readJson(ledgerFile);
2697
+ if (!authoritativeGateReady(gate, entry.task)) {
2698
+ results.push({ taskId, checkpointId, gateId, outcome: 'fail_closed', error: 'non_authoritative_gate_artifact', ledger: path.relative(root, ledgerFile) });
2699
+ continue;
2700
+ }
2665
2701
  if (gate.status === 'waiting_for_human' && ['inbox', 'done', 'failed'].includes(entry.subdir)) {
2666
2702
  const sourceFile = path.join(queueSubdirFor(root, queue, entry.subdir), `${safeTaskId(taskId)}.json`);
2667
2703
  if (await exists(sourceFile)) {
@@ -2693,7 +2729,7 @@ export async function notifyHumanInputRequests(root, options = {}) {
2693
2729
  id: gate?.id ?? `deferred-${index + 1}`, action: gate?.action ?? gate?.kind ?? null,
2694
2730
  required_authority: gate?.required_authority ?? gate?.human_action_required ?? gate?.reason ?? gate?.description ?? String(gate),
2695
2731
  scope: gate?.scope ?? null
2696
- })), source: entry.task.source, requested_at: now,
2732
+ })), source: entry.task.source, ...bindings, requested_at: now,
2697
2733
  notification_record: { status: 'pending', attempts: 0, idempotency_key: gateId }
2698
2734
  });
2699
2735
  if (['inbox', 'done', 'failed'].includes(entry.subdir)) {
@@ -2711,11 +2747,16 @@ export async function notifyHumanInputRequests(root, options = {}) {
2711
2747
  }
2712
2748
  }
2713
2749
  }
2714
- const message = humanInputMessage(queue, entry.task, checkpoint, gateId, language, blockers);
2715
2750
  if (options.dryRun) {
2716
- results.push({ taskId, checkpointId, gateId, outcome: 'dry_run', message, source: entry.task.source });
2751
+ results.push({ taskId, checkpointId, gateId, outcome: 'dry_run', preview: 'authoritative_gate_would_be_materialized', source: entry.task.source });
2752
+ continue;
2753
+ }
2754
+ const materializedGate = await readJson(ledgerFile);
2755
+ if (!authoritativeGateReady(materializedGate, entry.task)) {
2756
+ results.push({ taskId, checkpointId, gateId, outcome: 'fail_closed', error: 'gate_materialization_validation_failed' });
2717
2757
  continue;
2718
2758
  }
2759
+ const message = humanInputMessage(queue, entry.task, checkpoint, gateId, language, blockers);
2719
2760
  const result = await runCommand(`${options.notifyCommand} ${shellQuote(message)}`, {
2720
2761
  cwd: root,
2721
2762
  timeoutMs: options.timeoutMs ?? 60_000,
@@ -2873,7 +2914,19 @@ async function prepareHumanInputContext(root, queue, taskId) {
2873
2914
  for (const file of await listJson(gatesDir)) {
2874
2915
  const full = path.join(gatesDir, file);
2875
2916
  const gate = await readJson(full);
2876
- if (gate.task_id !== taskId || !['resolved', 'consumed'].includes(gate.status)) continue;
2917
+ if (gate.task_id !== taskId || !['resolved', 'consumed', 'waiting_for_human'].includes(gate.status)) continue;
2918
+ if (gate.status === 'waiting_for_human') {
2919
+ gates.push({
2920
+ gate_id: gate.gate_id,
2921
+ checkpoint_id: gate.checkpoint_id,
2922
+ status: gate.status,
2923
+ source_binding: gate.source_binding ?? null,
2924
+ actor_binding: gate.actor_binding ?? null,
2925
+ generation: gate.generation ?? null,
2926
+ authoritative: true
2927
+ });
2928
+ continue;
2929
+ }
2877
2930
  const consumedAt = gate.consumed_at ?? new Date().toISOString();
2878
2931
  const consumed = gate.status === 'resolved' ? { ...gate, status: 'consumed', consumed_at: consumedAt } : gate;
2879
2932
  if (gate.status === 'resolved') await writeJson(full, consumed);
@@ -3698,9 +3751,8 @@ function aggregateCriticStatus(baseStatus, criticReviews) {
3698
3751
  }
3699
3752
 
3700
3753
  function projectCompletionStatus(value) {
3701
- if (typeof value === 'string') return value;
3702
- if (value && typeof value === 'object') return value.status ?? null;
3703
- return null;
3754
+ const raw = typeof value === 'string' ? value : value && typeof value === 'object' ? value.status : null;
3755
+ return ['accepted', 'complete', 'completed'].includes(raw) ? 'accepted' : raw;
3704
3756
  }
3705
3757
 
3706
3758
  function continuationNextAction(value) {
@@ -3880,11 +3932,22 @@ function compareReviewRecency(a, b) {
3880
3932
  return compareReviewSequence(a, b);
3881
3933
  }
3882
3934
 
3883
- export function selectEffectiveAcceptanceReviews(devPlan, acceptanceReviews) {
3935
+ function terminalTaskReview(review, contract = {}) {
3936
+ if (review?.status !== 'accepted') return false;
3937
+ return projectCompletionStatus(review.projectCompletion) === 'accepted'
3938
+ || review.taskTerminal === true;
3939
+ }
3940
+
3941
+ export function selectEffectiveAcceptanceReviews(devPlan, acceptanceReviews, contract = {}) {
3884
3942
  const allReviews = acceptanceReviews?.reviews ?? [];
3885
3943
  const planned = Array.isArray(devPlan?.checkpoints) ? devPlan.checkpoints : [];
3886
3944
  if (planned.length <= 1) {
3887
- return [...allReviews].sort(compareReviewRecency).slice(-1);
3945
+ const ordered = [...allReviews].sort(compareReviewRecency);
3946
+ // A task-level terminal acceptance is monotonic. Later checkpoints may
3947
+ // report broader project governance, but they cannot reopen or overwrite
3948
+ // the completed task. Broader work belongs to the project ledger/gates.
3949
+ const terminal = ordered.find((review) => terminalTaskReview(review, contract));
3950
+ return terminal ? [terminal] : ordered.slice(-1);
3888
3951
  }
3889
3952
  const selected = [];
3890
3953
  for (const checkpoint of planned) {
@@ -3907,7 +3970,7 @@ export function buildFinalJudgement(contract, acceptancePlan, devPlan, checkpoin
3907
3970
  // Checkpoints produced after the planned set are revision/progress snapshots,
3908
3971
  // not additional required milestones. Judge the latest complete set so a
3909
3972
  // resolved historical blocker does not permanently poison the task.
3910
- const effectiveReviews = selectEffectiveAcceptanceReviews(devPlan, acceptanceReviews);
3973
+ const effectiveReviews = selectEffectiveAcceptanceReviews(devPlan, acceptanceReviews, contract);
3911
3974
  const reviewCount = effectiveReviews.length;
3912
3975
  const acceptedCount = effectiveReviews.filter((item) => item.status === 'accepted').length;
3913
3976
  const reviseCount = effectiveReviews.filter((item) => item.status === 'revise').length;
@@ -4011,6 +4074,7 @@ export function buildFinalJudgement(contract, acceptancePlan, devPlan, checkpoin
4011
4074
  reviews: reviewCount,
4012
4075
  historical_reviews: allReviews.length,
4013
4076
  effective_review_ids: effectiveReviews.map((review) => review.checkpointId),
4077
+ terminal_lock: effectiveReviews.some((review) => terminalTaskReview(review, contract)),
4014
4078
  accepted: acceptedCount,
4015
4079
  revise: reviseCount,
4016
4080
  blocked: blockedCount,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskforce-loop-engineering",
3
- "version": "0.15.14",
3
+ "version": "0.15.15",
4
4
  "private": false,
5
5
  "description": "OpenClaw-native loop engineering CLI, templates, and skill for verifiable agent work loops.",
6
6
  "type": "module",
@@ -194,4 +194,29 @@ assert.equal(inferTaskScope({ body: 'Continue the overall project with a product
194
194
  assert.deepEqual(judgement.residual_risks, ['A non-blocking operational risk remains after acceptance.']);
195
195
  }
196
196
 
197
+ // Regression: once cp19 accepted the task's own terminal contract, a later
198
+ // checkpoint carrying broader-project human acceptance cannot reopen it.
199
+ {
200
+ const devPlan = { checkpoints: [{ id: 'cp1' }] };
201
+ const reviews = { reviews: [
202
+ {
203
+ checkpointId: 'cp19', sequence: 19, status: 'accepted',
204
+ projectCompletion: { status: 'complete' }, checkpointRisks: []
205
+ },
206
+ {
207
+ checkpointId: 'cp21', sequence: 21, status: 'blocked',
208
+ projectCompletion: { status: 'needs_human_acceptance' },
209
+ checkpointNextAction: 'owner accepts broader OpenReel v4 project'
210
+ }
211
+ ] };
212
+ const judgement = buildFinalJudgement(
213
+ { ...baseContract, task_scope: 'project', requires_human_gate: false },
214
+ basePlan, devPlan, { count: 21 }, reviews, { dispatchStatus: 'completed' }
215
+ );
216
+ assert.equal(judgement.outcome, 'ready_to_apply');
217
+ assert.deepEqual(judgement.coverage.effective_review_ids, ['cp19']);
218
+ assert.equal(judgement.coverage.terminal_lock, true);
219
+ assert.equal(judgement.requires_human_gate, false);
220
+ }
221
+
197
222
  console.log('final judgement self-test passed');
@@ -40,7 +40,7 @@ assert(authorizedContract.constraints.allowed_actions.includes('paid_provider_ac
40
40
  assert(authorizedContract.constraints.blocked_actions.includes('credential_change_without_explicit_confirmation'));
41
41
  await writeFile(projectFile, `${JSON.stringify(spec, null, 2)}\n`);
42
42
 
43
- const b = await enqueueTask(root, { queue, title: 'OpenReel B-01', task: 'Project openreel requirement B-01', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner' });
43
+ const b = await enqueueTask(root, { queue, title: 'OpenReel B-01', task: 'Project openreel requirement B-01', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner', sourceAccount: 'main' });
44
44
  const checkpointDir = path.join(taskRuntimeDirFor(root, queue, b.task.id), 'checkpoints');
45
45
  await mkdir(checkpointDir, { recursive: true });
46
46
  await writeFile(path.join(checkpointDir, 'cp1.json'), `${JSON.stringify({ version: 1, task_id: b.task.id, checkpoint_id: 'cp1', milestone_id: 'B-01', requirement_ids: ['B-01'], status: 'needs_human_input', blockers: ['Authorize B-01'] }, null, 2)}\n`);
@@ -132,7 +132,7 @@ await writeFile(projectFile, `${JSON.stringify({
132
132
 
133
133
  // A ready milestone with project in progress and a deferred authorization is
134
134
  // converted into a structured waiting gate, not left as prose on a done task.
135
- const deferred = await enqueueTask(root, { queue, title: 'OpenReel deferred S-01', task: 'Project openreel S-01', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner' });
135
+ const deferred = await enqueueTask(root, { queue, title: 'OpenReel deferred S-01', task: 'Project openreel S-01', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner', sourceAccount: 'main' });
136
136
  const deferredDir = path.join(taskRuntimeDirFor(root, queue, deferred.task.id), 'checkpoints');
137
137
  await mkdir(deferredDir, { recursive: true });
138
138
  await writeFile(path.join(deferredDir, 'cp-ready.json'), `${JSON.stringify({
@@ -164,7 +164,7 @@ await writeFile(authoritativeBacklog, `${JSON.stringify({
164
164
  { id: 'LOCAL-02', status: 'pending', dependsOn: ['LOCAL-01'] }
165
165
  ]
166
166
  }, null, 2)}\n`);
167
- const futureGateTask = await enqueueTask(root, { queue, title: 'OpenReel future production gate', task: 'Continue safe local backlog', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner' });
167
+ const futureGateTask = await enqueueTask(root, { queue, title: 'OpenReel future production gate', task: 'Continue safe local backlog', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner', sourceAccount: 'main' });
168
168
  const futureGateDir = path.join(taskRuntimeDirFor(root, queue, futureGateTask.task.id), 'checkpoints');
169
169
  await mkdir(futureGateDir, { recursive: true });
170
170
  await writeFile(path.join(futureGateDir, 'cp1.json'), `${JSON.stringify({
@@ -180,7 +180,7 @@ assert.equal((await queueStatus(root, queue)).waiting, 0);
180
180
  // deferred_gates must not materialize a gate without a concrete action and
181
181
  // authority requirement.
182
182
  await reconcileProjectGates(root, { queue });
183
- const conditional = await enqueueTask(root, { queue, title: 'OpenReel conditional policy boundary', task: 'Continue safe OpenReel backlog', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner' });
183
+ const conditional = await enqueueTask(root, { queue, title: 'OpenReel conditional policy boundary', task: 'Continue safe OpenReel backlog', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner', sourceAccount: 'main' });
184
184
  const conditionalDir = path.join(taskRuntimeDirFor(root, queue, conditional.task.id), 'checkpoints');
185
185
  await mkdir(conditionalDir, { recursive: true });
186
186
  await writeFile(path.join(conditionalDir, 'cp1.json'), `${JSON.stringify({
@@ -194,7 +194,7 @@ assert.equal(conditionalNotice.results.some((item) => item.taskId === conditiona
194
194
  // A conditional formal blocker becomes current when the producer explicitly
195
195
  // marks it needed now and materialize=true. It must stop once, rather than be
196
196
  // filtered into a needs_revision/project_in_progress polling loop.
197
- const conditionalNow = await enqueueTask(root, { queue, title: 'OpenReel conditional blocker now', task: 'Wait for a real external precondition', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner' });
197
+ const conditionalNow = await enqueueTask(root, { queue, title: 'OpenReel conditional blocker now', task: 'Wait for a real external precondition', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner', sourceAccount: 'main' });
198
198
  const conditionalNowDir = path.join(taskRuntimeDirFor(root, queue, conditionalNow.task.id), 'checkpoints');
199
199
  await mkdir(conditionalNowDir, { recursive: true });
200
200
  await writeFile(path.join(conditionalNowDir, 'cp1.json'), `${JSON.stringify({
@@ -213,7 +213,7 @@ await reconcileProjectGates(root, { queue });
213
213
  // Authorization already granted or already consumed is audit context, not a
214
214
  // new human-input request. A future boundary is likewise dormant.
215
215
  for (const authorizationState of ['authorized', 'consumed', 'future']) {
216
- const stateTask = await enqueueTask(root, { queue, title: `OpenReel ${authorizationState} authority`, task: 'Continue within recorded authority', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner' });
216
+ const stateTask = await enqueueTask(root, { queue, title: `OpenReel ${authorizationState} authority`, task: 'Continue within recorded authority', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner', sourceAccount: 'main' });
217
217
  const stateDir = path.join(taskRuntimeDirFor(root, queue, stateTask.task.id), 'checkpoints');
218
218
  await mkdir(stateDir, { recursive: true });
219
219
  await writeFile(path.join(stateDir, 'cp1.json'), `${JSON.stringify({
@@ -239,7 +239,7 @@ await writeFile(subprojectBacklog, `${JSON.stringify({
239
239
  { id: 'CDQI2-11', status: 'in_progress', dependsOn: ['CDQI2-10'] }
240
240
  ]
241
241
  }, null, 2)}\n`);
242
- const subprojectTask = await enqueueTask(root, { queue, title: 'OpenReel CDQI2 actionable backlog', task: 'Continue CDQI2-11', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner' });
242
+ const subprojectTask = await enqueueTask(root, { queue, title: 'OpenReel CDQI2 actionable backlog', task: 'Continue CDQI2-11', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner', sourceAccount: 'main' });
243
243
  const subprojectDir = path.join(taskRuntimeDirFor(root, queue, subprojectTask.task.id), 'checkpoints');
244
244
  await mkdir(subprojectDir, { recursive: true });
245
245
  await writeFile(path.join(subprojectDir, 'cp1.json'), `${JSON.stringify({
@@ -254,7 +254,7 @@ assert.equal(subprojectNotice.results.some((item) => item.taskId === subprojectT
254
254
  // A genuinely missing current authorization becomes a waiting gate once no
255
255
  // safe project work remains.
256
256
  await writeFile(authoritativeBacklog, `${JSON.stringify({ status: 'ongoing', items: [{ id: 'GLOBAL-01', status: 'accepted', dependsOn: [] }] }, null, 2)}\n`);
257
- const missing = await enqueueTask(root, { queue, title: 'OpenReel missing current authority', task: 'Perform currently gated action', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner' });
257
+ const missing = await enqueueTask(root, { queue, title: 'OpenReel missing current authority', task: 'Perform currently gated action', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner', sourceAccount: 'main' });
258
258
  const missingDir = path.join(taskRuntimeDirFor(root, queue, missing.task.id), 'checkpoints');
259
259
  await mkdir(missingDir, { recursive: true });
260
260
  await writeFile(path.join(missingDir, 'cp1.json'), `${JSON.stringify({
@@ -279,7 +279,7 @@ await writeFile(projectFile, `${JSON.stringify({
279
279
  backupRestoreRollbackRehearsal: 'standing_authorization_openreel_2026-08-19'
280
280
  }
281
281
  }, null, 2)}\n`);
282
- const coveredBlocker = await enqueueTask(root, { queue, title: 'OpenReel covered production blocker', task: 'Deploy accepted candidate', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner' });
282
+ const coveredBlocker = await enqueueTask(root, { queue, title: 'OpenReel covered production blocker', task: 'Deploy accepted candidate', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner', sourceAccount: 'main' });
283
283
  const coveredContract = await writeTaskContract(root, queue, coveredBlocker.task);
284
284
  assert.equal(coveredContract.contract.constraints.project_authorization.production_authorized, true);
285
285
  const coveredDir = path.join(taskRuntimeDirFor(root, queue, coveredBlocker.task.id), 'checkpoints');
@@ -294,7 +294,7 @@ assert.equal(coveredNotice.results.some((item) => item.taskId === coveredBlocker
294
294
 
295
295
  // Explicitly authorized blocker metadata is also non-materializable, while a
296
296
  // genuinely missing publication permission remains a human gate.
297
- const publication = await enqueueTask(root, { queue, title: 'OpenReel publication blocker', task: 'Publish candidate', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner' });
297
+ const publication = await enqueueTask(root, { queue, title: 'OpenReel publication blocker', task: 'Publish candidate', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner', sourceAccount: 'main' });
298
298
  await writeTaskContract(root, queue, publication.task);
299
299
  const publicationDir = path.join(taskRuntimeDirFor(root, queue, publication.task.id), 'checkpoints');
300
300
  await mkdir(publicationDir, { recursive: true });
@@ -332,7 +332,7 @@ await rename(legacyInbox, `${legacyInbox}.moved`);
332
332
  await reconcileProjectGates(root, { queue });
333
333
  assert.equal((await queueStatus(root, queue)).done >= 1, true, 'accepted project task must close in done, not canceled');
334
334
  assert.equal(JSON.parse(await readFile(legacyGateFile, 'utf8')).status, 'superseded');
335
- const acceptedOptional = await enqueueTask(root, { queue, title: 'OpenReel optional operations transfer', task: 'Project openreel optional post-completion transfer', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner' });
335
+ const acceptedOptional = await enqueueTask(root, { queue, title: 'OpenReel optional operations transfer', task: 'Project openreel optional post-completion transfer', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner', sourceAccount: 'main' });
336
336
  const acceptedOptionalDir = path.join(taskRuntimeDirFor(root, queue, acceptedOptional.task.id), 'checkpoints');
337
337
  await mkdir(acceptedOptionalDir, { recursive: true });
338
338
  await writeFile(path.join(acceptedOptionalDir, 'cp1.json'), `${JSON.stringify({
@@ -344,4 +344,19 @@ const acceptedNotice = await notifyHumanInputRequests(root, { queue, notifyComma
344
344
  assert.equal(acceptedNotice.results.some((item) => item.taskId === acceptedOptional.task.id), false);
345
345
  assert.equal((await queueStatus(root, queue)).waiting, 0);
346
346
 
347
- console.log(JSON.stringify({ status: 'ok', assertions: ['standing project authorization reaches task contract', 'structured gate context', 'B-01 waiting to zero', 'project-isolated supersede', 'doctor strong validation', 'authoritative ledger drift', 'ready milestone deferred gate', 'future gate does not stop safe actionable backlog', 'conditional policy prose does not create a gate', 'authorized and consumed authority do not create gates', 'checkpoint-bound subproject backlog remains actionable', 'missing current authority creates a gate', 'standing-authorized production blocker does not create a gate', 'missing publication blocker creates a gate', 'accepted project optional deferred gate stays out of queue'] }));
347
+ // Missing source/actor/generation binding must fail closed: no waiting move,
348
+ // no gate command text, and no notification invocation.
349
+ const unbound = await enqueueTask(root, { queue, title: 'Unbound human request', task: 'Project openreel B-01', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner' });
350
+ const unboundDir = path.join(taskRuntimeDirFor(root, queue, unbound.task.id), 'checkpoints');
351
+ await mkdir(unboundDir, { recursive: true });
352
+ await writeFile(path.join(unboundDir, 'cp1.json'), `${JSON.stringify({
353
+ version: 1, task_id: unbound.task.id, checkpoint_id: 'cp1', milestone_id: 'B-01',
354
+ status: 'needs_human_input', blockers: [{ action: 'approve', required_authority: 'owner', authorization_state: 'missing', needed_when: 'now' }]
355
+ }, null, 2)}\n`);
356
+ const unboundNotice = await notifyHumanInputRequests(root, { queue, notifyCommand: '/bin/true' });
357
+ const unboundResult = unboundNotice.results.find((item) => item.taskId === unbound.task.id);
358
+ assert.equal(unboundResult.outcome, 'fail_closed');
359
+ assert.equal(unboundResult.message, undefined);
360
+ assert.equal((await queueStatus(root, queue)).waiting, 0);
361
+
362
+ console.log(JSON.stringify({ status: 'ok', assertions: ['standing project authorization reaches task contract', 'structured gate context', 'B-01 waiting to zero', 'project-isolated supersede', 'doctor strong validation', 'authoritative ledger drift', 'ready milestone deferred gate', 'future gate does not stop safe actionable backlog', 'conditional policy prose does not create a gate', 'authorized and consumed authority do not create gates', 'checkpoint-bound subproject backlog remains actionable', 'missing current authority creates a gate', 'standing-authorized production blocker does not create a gate', 'missing publication blocker creates a gate', 'accepted project optional deferred gate stays out of queue', 'unbound gate fails closed without actionable text'] }));
@@ -423,8 +423,8 @@ await writeJson(path.join(root, 'configs', 'loops', 'queues', `${queue}.json`),
423
423
 
424
424
  const gateDryRun = await notifyHumanInputRequests(root, { queue, dryRun: true });
425
425
  assert.equal(gateDryRun.results[0].outcome, 'dry_run');
426
- assert.match(gateDryRun.results[0].message, /Provide the SMS code/);
427
- assert.match(gateDryRun.results[0].message, /正在等待你的输入/);
426
+ assert.equal(gateDryRun.results[0].preview, 'authoritative_gate_would_be_materialized');
427
+ assert.equal(gateDryRun.results[0].message, undefined, 'dry-run must not emit an actionable reply command before gate materialization');
428
428
  const gateSent = await notifyHumanInputRequests(root, { queue, notifyCommand: '/bin/true' });
429
429
  assert.equal(gateSent.sent, 1);
430
430
  const gateId = gateSent.results[0].gateId;
@@ -544,7 +544,7 @@ await writeJson(path.join(regressionRoot, 'configs', 'loops', 'projects', 'demo.
544
544
  for (const [id, enqueuedAt] of [['history-done', '2026-01-01T00:00:00Z'], ['latest-done', '2026-01-02T00:00:00Z']]) {
545
545
  await writeJson(path.join(queueSubdirFor(regressionRoot, regressionQueue, 'done'), `${id}.json`), {
546
546
  id, title: `demo ${id}`, body: 'demo R-1', projectId: 'demo', status: 'completed', enqueuedAt,
547
- source: { channel: 'test', target: 'owner' }
547
+ source: { channel: 'test', target: 'owner', account: 'main' }
548
548
  });
549
549
  await writeJson(path.join(taskRuntimeDirFor(regressionRoot, regressionQueue, id), 'checkpoints', 'cp1.json'), {
550
550
  version: 1, task_id: id, checkpoint_id: 'cp1', milestone_id: 'R-1', sequence: 1,
@@ -577,7 +577,7 @@ await writeJson(path.join(queueSubdirFor(regressionRoot, regressionQueue, 'faile
577
577
  projectId: 'demo',
578
578
  status: 'blocked',
579
579
  enqueuedAt: '2026-01-03T00:00:00Z',
580
- source: { channel: 'feishu', target: 'owner' }
580
+ source: { channel: 'feishu', target: 'owner', account: 'main' }
581
581
  });
582
582
  await writeJson(path.join(taskRuntimeDirFor(regressionRoot, regressionQueue, blockedProjectTask), 'checkpoints', 'cp2.json'), {
583
583
  version: 1,
@@ -1,7 +1,7 @@
1
1
  # npm Package
2
2
 
3
3
  Package name: `taskforce-loop-engineering`
4
- Version: `0.9.0`
4
+ Version: `0.15.15`
5
5
 
6
6
  Install from npm:
7
7