taskforce-loop-engineering 0.15.14 → 0.15.16
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 +80 -11
- package/package.json +1 -1
- package/scripts/final-judgement-self-test.mjs +25 -0
- package/scripts/project-gate-reconciliation-self-test.mjs +68 -13
- package/scripts/route-notify-self-test.mjs +4 -4
- package/skills/taskforce-loop-engineering/references/npm-package.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.15.16 - 2026-09-13
|
|
6
|
+
|
|
7
|
+
- Resolve canonical project identifiers from nested checkpoint completion metadata so migrated project tasks continue against the correct authoritative backlog.
|
|
8
|
+
- Keep future external-action authorization gates deferred while safe local project work remains actionable.
|
|
9
|
+
- Add regression coverage for checkpoint-only project metadata and premature deferred-gate reconciliation.
|
|
10
|
+
|
|
11
|
+
## 0.15.15 - 2026-08-31
|
|
12
|
+
|
|
13
|
+
- Lock a task-level accepted terminal checkpoint monotonically so later broader-project governance cannot reopen or overwrite the completed task.
|
|
14
|
+
- Require fully materialized, source/actor/generation-bound Human Gate artifacts before moving tasks to waiting or sending actionable notifications.
|
|
15
|
+
- Fail closed for incomplete or non-authoritative Gate artifacts, and keep dry-run previews free of actionable reply commands.
|
|
16
|
+
- Add regressions for broader-project scope creep after task acceptance, empty/unbound gates, authoritative waiting placement, and notification consistency.
|
|
17
|
+
|
|
5
18
|
## 0.15.14 - 2026-08-30
|
|
6
19
|
|
|
7
20
|
- 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
|
@@ -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
|
|
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) => ({
|
|
@@ -2453,6 +2458,33 @@ function humanInputMessage(queue, task, checkpoint, gateId, language = 'en', blo
|
|
|
2453
2458
|
].join('\n');
|
|
2454
2459
|
}
|
|
2455
2460
|
|
|
2461
|
+
function authoritativeGateBindings(task) {
|
|
2462
|
+
const source = task?.source;
|
|
2463
|
+
const generation = Number(task?.runtimeSessionGeneration ?? 0);
|
|
2464
|
+
if (!source?.channel || !source?.target || !source?.account || !Number.isInteger(generation) || generation < 0) return null;
|
|
2465
|
+
return {
|
|
2466
|
+
source_binding: {
|
|
2467
|
+
channel: source.channel,
|
|
2468
|
+
target: source.target,
|
|
2469
|
+
account: source.account,
|
|
2470
|
+
message_id: source.messageId ?? source.message_id ?? null
|
|
2471
|
+
},
|
|
2472
|
+
actor_binding: { kind: 'source_target', actor_id: source.target, account: source.account },
|
|
2473
|
+
generation
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
function authoritativeGateReady(gate, task) {
|
|
2478
|
+
const expected = authoritativeGateBindings(task);
|
|
2479
|
+
if (!expected || !gate || gate.status !== 'waiting_for_human') return false;
|
|
2480
|
+
return gate.source_binding?.channel === expected.source_binding.channel
|
|
2481
|
+
&& gate.source_binding?.target === expected.source_binding.target
|
|
2482
|
+
&& gate.source_binding?.account === expected.source_binding.account
|
|
2483
|
+
&& gate.actor_binding?.actor_id === expected.actor_binding.actor_id
|
|
2484
|
+
&& gate.actor_binding?.account === expected.actor_binding.account
|
|
2485
|
+
&& gate.generation === expected.generation;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2456
2488
|
function blockerCoveredByContractAuthorization(blocker, contract) {
|
|
2457
2489
|
if (!blocker || typeof blocker !== 'object' || Array.isArray(blocker)) return false;
|
|
2458
2490
|
const state = String(blocker.authorization_state ?? blocker.state ?? '')
|
|
@@ -2660,8 +2692,17 @@ export async function notifyHumanInputRequests(root, options = {}) {
|
|
|
2660
2692
|
const checkpointId = checkpoint.checkpoint_id ?? path.basename(file, '.json');
|
|
2661
2693
|
const gateId = `${taskId}:${checkpointId}`;
|
|
2662
2694
|
const ledgerFile = path.join(gatesDir, `${safeTaskId(taskId)}.${normalizeLoopId(checkpointId)}.json`);
|
|
2695
|
+
const bindings = authoritativeGateBindings(entry.task);
|
|
2696
|
+
if (!bindings) {
|
|
2697
|
+
results.push({ taskId, checkpointId, gateId, outcome: 'fail_closed', error: 'incomplete_source_actor_generation_binding' });
|
|
2698
|
+
continue;
|
|
2699
|
+
}
|
|
2663
2700
|
if (await exists(ledgerFile)) {
|
|
2664
2701
|
const gate = await readJson(ledgerFile);
|
|
2702
|
+
if (!authoritativeGateReady(gate, entry.task)) {
|
|
2703
|
+
results.push({ taskId, checkpointId, gateId, outcome: 'fail_closed', error: 'non_authoritative_gate_artifact', ledger: path.relative(root, ledgerFile) });
|
|
2704
|
+
continue;
|
|
2705
|
+
}
|
|
2665
2706
|
if (gate.status === 'waiting_for_human' && ['inbox', 'done', 'failed'].includes(entry.subdir)) {
|
|
2666
2707
|
const sourceFile = path.join(queueSubdirFor(root, queue, entry.subdir), `${safeTaskId(taskId)}.json`);
|
|
2667
2708
|
if (await exists(sourceFile)) {
|
|
@@ -2693,7 +2734,7 @@ export async function notifyHumanInputRequests(root, options = {}) {
|
|
|
2693
2734
|
id: gate?.id ?? `deferred-${index + 1}`, action: gate?.action ?? gate?.kind ?? null,
|
|
2694
2735
|
required_authority: gate?.required_authority ?? gate?.human_action_required ?? gate?.reason ?? gate?.description ?? String(gate),
|
|
2695
2736
|
scope: gate?.scope ?? null
|
|
2696
|
-
})), source: entry.task.source, requested_at: now,
|
|
2737
|
+
})), source: entry.task.source, ...bindings, requested_at: now,
|
|
2697
2738
|
notification_record: { status: 'pending', attempts: 0, idempotency_key: gateId }
|
|
2698
2739
|
});
|
|
2699
2740
|
if (['inbox', 'done', 'failed'].includes(entry.subdir)) {
|
|
@@ -2711,11 +2752,16 @@ export async function notifyHumanInputRequests(root, options = {}) {
|
|
|
2711
2752
|
}
|
|
2712
2753
|
}
|
|
2713
2754
|
}
|
|
2714
|
-
const message = humanInputMessage(queue, entry.task, checkpoint, gateId, language, blockers);
|
|
2715
2755
|
if (options.dryRun) {
|
|
2716
|
-
results.push({ taskId, checkpointId, gateId, outcome: 'dry_run',
|
|
2756
|
+
results.push({ taskId, checkpointId, gateId, outcome: 'dry_run', preview: 'authoritative_gate_would_be_materialized', source: entry.task.source });
|
|
2757
|
+
continue;
|
|
2758
|
+
}
|
|
2759
|
+
const materializedGate = await readJson(ledgerFile);
|
|
2760
|
+
if (!authoritativeGateReady(materializedGate, entry.task)) {
|
|
2761
|
+
results.push({ taskId, checkpointId, gateId, outcome: 'fail_closed', error: 'gate_materialization_validation_failed' });
|
|
2717
2762
|
continue;
|
|
2718
2763
|
}
|
|
2764
|
+
const message = humanInputMessage(queue, entry.task, checkpoint, gateId, language, blockers);
|
|
2719
2765
|
const result = await runCommand(`${options.notifyCommand} ${shellQuote(message)}`, {
|
|
2720
2766
|
cwd: root,
|
|
2721
2767
|
timeoutMs: options.timeoutMs ?? 60_000,
|
|
@@ -2873,7 +2919,19 @@ async function prepareHumanInputContext(root, queue, taskId) {
|
|
|
2873
2919
|
for (const file of await listJson(gatesDir)) {
|
|
2874
2920
|
const full = path.join(gatesDir, file);
|
|
2875
2921
|
const gate = await readJson(full);
|
|
2876
|
-
if (gate.task_id !== taskId || !['resolved', 'consumed'].includes(gate.status)) continue;
|
|
2922
|
+
if (gate.task_id !== taskId || !['resolved', 'consumed', 'waiting_for_human'].includes(gate.status)) continue;
|
|
2923
|
+
if (gate.status === 'waiting_for_human') {
|
|
2924
|
+
gates.push({
|
|
2925
|
+
gate_id: gate.gate_id,
|
|
2926
|
+
checkpoint_id: gate.checkpoint_id,
|
|
2927
|
+
status: gate.status,
|
|
2928
|
+
source_binding: gate.source_binding ?? null,
|
|
2929
|
+
actor_binding: gate.actor_binding ?? null,
|
|
2930
|
+
generation: gate.generation ?? null,
|
|
2931
|
+
authoritative: true
|
|
2932
|
+
});
|
|
2933
|
+
continue;
|
|
2934
|
+
}
|
|
2877
2935
|
const consumedAt = gate.consumed_at ?? new Date().toISOString();
|
|
2878
2936
|
const consumed = gate.status === 'resolved' ? { ...gate, status: 'consumed', consumed_at: consumedAt } : gate;
|
|
2879
2937
|
if (gate.status === 'resolved') await writeJson(full, consumed);
|
|
@@ -3698,9 +3756,8 @@ function aggregateCriticStatus(baseStatus, criticReviews) {
|
|
|
3698
3756
|
}
|
|
3699
3757
|
|
|
3700
3758
|
function projectCompletionStatus(value) {
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
return null;
|
|
3759
|
+
const raw = typeof value === 'string' ? value : value && typeof value === 'object' ? value.status : null;
|
|
3760
|
+
return ['accepted', 'complete', 'completed'].includes(raw) ? 'accepted' : raw;
|
|
3704
3761
|
}
|
|
3705
3762
|
|
|
3706
3763
|
function continuationNextAction(value) {
|
|
@@ -3880,11 +3937,22 @@ function compareReviewRecency(a, b) {
|
|
|
3880
3937
|
return compareReviewSequence(a, b);
|
|
3881
3938
|
}
|
|
3882
3939
|
|
|
3883
|
-
|
|
3940
|
+
function terminalTaskReview(review, contract = {}) {
|
|
3941
|
+
if (review?.status !== 'accepted') return false;
|
|
3942
|
+
return projectCompletionStatus(review.projectCompletion) === 'accepted'
|
|
3943
|
+
|| review.taskTerminal === true;
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
export function selectEffectiveAcceptanceReviews(devPlan, acceptanceReviews, contract = {}) {
|
|
3884
3947
|
const allReviews = acceptanceReviews?.reviews ?? [];
|
|
3885
3948
|
const planned = Array.isArray(devPlan?.checkpoints) ? devPlan.checkpoints : [];
|
|
3886
3949
|
if (planned.length <= 1) {
|
|
3887
|
-
|
|
3950
|
+
const ordered = [...allReviews].sort(compareReviewRecency);
|
|
3951
|
+
// A task-level terminal acceptance is monotonic. Later checkpoints may
|
|
3952
|
+
// report broader project governance, but they cannot reopen or overwrite
|
|
3953
|
+
// the completed task. Broader work belongs to the project ledger/gates.
|
|
3954
|
+
const terminal = ordered.find((review) => terminalTaskReview(review, contract));
|
|
3955
|
+
return terminal ? [terminal] : ordered.slice(-1);
|
|
3888
3956
|
}
|
|
3889
3957
|
const selected = [];
|
|
3890
3958
|
for (const checkpoint of planned) {
|
|
@@ -3907,7 +3975,7 @@ export function buildFinalJudgement(contract, acceptancePlan, devPlan, checkpoin
|
|
|
3907
3975
|
// Checkpoints produced after the planned set are revision/progress snapshots,
|
|
3908
3976
|
// not additional required milestones. Judge the latest complete set so a
|
|
3909
3977
|
// resolved historical blocker does not permanently poison the task.
|
|
3910
|
-
const effectiveReviews = selectEffectiveAcceptanceReviews(devPlan, acceptanceReviews);
|
|
3978
|
+
const effectiveReviews = selectEffectiveAcceptanceReviews(devPlan, acceptanceReviews, contract);
|
|
3911
3979
|
const reviewCount = effectiveReviews.length;
|
|
3912
3980
|
const acceptedCount = effectiveReviews.filter((item) => item.status === 'accepted').length;
|
|
3913
3981
|
const reviseCount = effectiveReviews.filter((item) => item.status === 'revise').length;
|
|
@@ -4011,6 +4079,7 @@ export function buildFinalJudgement(contract, acceptancePlan, devPlan, checkpoin
|
|
|
4011
4079
|
reviews: reviewCount,
|
|
4012
4080
|
historical_reviews: allReviews.length,
|
|
4013
4081
|
effective_review_ids: effectiveReviews.map((review) => review.checkpointId),
|
|
4082
|
+
terminal_lock: effectiveReviews.some((review) => terminalTaskReview(review, contract)),
|
|
4014
4083
|
accepted: acceptedCount,
|
|
4015
4084
|
revise: reviseCount,
|
|
4016
4085
|
blocked: blockedCount,
|
package/package.json
CHANGED
|
@@ -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');
|
|
@@ -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';
|
|
@@ -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({
|
|
@@ -176,11 +176,51 @@ 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.
|
|
182
222
|
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' });
|
|
223
|
+
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
224
|
const conditionalDir = path.join(taskRuntimeDirFor(root, queue, conditional.task.id), 'checkpoints');
|
|
185
225
|
await mkdir(conditionalDir, { recursive: true });
|
|
186
226
|
await writeFile(path.join(conditionalDir, 'cp1.json'), `${JSON.stringify({
|
|
@@ -194,7 +234,7 @@ assert.equal(conditionalNotice.results.some((item) => item.taskId === conditiona
|
|
|
194
234
|
// A conditional formal blocker becomes current when the producer explicitly
|
|
195
235
|
// marks it needed now and materialize=true. It must stop once, rather than be
|
|
196
236
|
// 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' });
|
|
237
|
+
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
238
|
const conditionalNowDir = path.join(taskRuntimeDirFor(root, queue, conditionalNow.task.id), 'checkpoints');
|
|
199
239
|
await mkdir(conditionalNowDir, { recursive: true });
|
|
200
240
|
await writeFile(path.join(conditionalNowDir, 'cp1.json'), `${JSON.stringify({
|
|
@@ -213,7 +253,7 @@ await reconcileProjectGates(root, { queue });
|
|
|
213
253
|
// Authorization already granted or already consumed is audit context, not a
|
|
214
254
|
// new human-input request. A future boundary is likewise dormant.
|
|
215
255
|
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' });
|
|
256
|
+
const stateTask = await enqueueTask(root, { queue, title: `OpenReel ${authorizationState} authority`, task: 'Continue within recorded authority', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner', sourceAccount: 'main' });
|
|
217
257
|
const stateDir = path.join(taskRuntimeDirFor(root, queue, stateTask.task.id), 'checkpoints');
|
|
218
258
|
await mkdir(stateDir, { recursive: true });
|
|
219
259
|
await writeFile(path.join(stateDir, 'cp1.json'), `${JSON.stringify({
|
|
@@ -239,7 +279,7 @@ await writeFile(subprojectBacklog, `${JSON.stringify({
|
|
|
239
279
|
{ id: 'CDQI2-11', status: 'in_progress', dependsOn: ['CDQI2-10'] }
|
|
240
280
|
]
|
|
241
281
|
}, 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' });
|
|
282
|
+
const subprojectTask = await enqueueTask(root, { queue, title: 'OpenReel CDQI2 actionable backlog', task: 'Continue CDQI2-11', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner', sourceAccount: 'main' });
|
|
243
283
|
const subprojectDir = path.join(taskRuntimeDirFor(root, queue, subprojectTask.task.id), 'checkpoints');
|
|
244
284
|
await mkdir(subprojectDir, { recursive: true });
|
|
245
285
|
await writeFile(path.join(subprojectDir, 'cp1.json'), `${JSON.stringify({
|
|
@@ -254,7 +294,7 @@ assert.equal(subprojectNotice.results.some((item) => item.taskId === subprojectT
|
|
|
254
294
|
// A genuinely missing current authorization becomes a waiting gate once no
|
|
255
295
|
// safe project work remains.
|
|
256
296
|
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' });
|
|
297
|
+
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
298
|
const missingDir = path.join(taskRuntimeDirFor(root, queue, missing.task.id), 'checkpoints');
|
|
259
299
|
await mkdir(missingDir, { recursive: true });
|
|
260
300
|
await writeFile(path.join(missingDir, 'cp1.json'), `${JSON.stringify({
|
|
@@ -279,7 +319,7 @@ await writeFile(projectFile, `${JSON.stringify({
|
|
|
279
319
|
backupRestoreRollbackRehearsal: 'standing_authorization_openreel_2026-08-19'
|
|
280
320
|
}
|
|
281
321
|
}, 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' });
|
|
322
|
+
const coveredBlocker = await enqueueTask(root, { queue, title: 'OpenReel covered production blocker', task: 'Deploy accepted candidate', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner', sourceAccount: 'main' });
|
|
283
323
|
const coveredContract = await writeTaskContract(root, queue, coveredBlocker.task);
|
|
284
324
|
assert.equal(coveredContract.contract.constraints.project_authorization.production_authorized, true);
|
|
285
325
|
const coveredDir = path.join(taskRuntimeDirFor(root, queue, coveredBlocker.task.id), 'checkpoints');
|
|
@@ -294,7 +334,7 @@ assert.equal(coveredNotice.results.some((item) => item.taskId === coveredBlocker
|
|
|
294
334
|
|
|
295
335
|
// Explicitly authorized blocker metadata is also non-materializable, while a
|
|
296
336
|
// 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' });
|
|
337
|
+
const publication = await enqueueTask(root, { queue, title: 'OpenReel publication blocker', task: 'Publish candidate', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner', sourceAccount: 'main' });
|
|
298
338
|
await writeTaskContract(root, queue, publication.task);
|
|
299
339
|
const publicationDir = path.join(taskRuntimeDirFor(root, queue, publication.task.id), 'checkpoints');
|
|
300
340
|
await mkdir(publicationDir, { recursive: true });
|
|
@@ -332,7 +372,7 @@ await rename(legacyInbox, `${legacyInbox}.moved`);
|
|
|
332
372
|
await reconcileProjectGates(root, { queue });
|
|
333
373
|
assert.equal((await queueStatus(root, queue)).done >= 1, true, 'accepted project task must close in done, not canceled');
|
|
334
374
|
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' });
|
|
375
|
+
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
376
|
const acceptedOptionalDir = path.join(taskRuntimeDirFor(root, queue, acceptedOptional.task.id), 'checkpoints');
|
|
337
377
|
await mkdir(acceptedOptionalDir, { recursive: true });
|
|
338
378
|
await writeFile(path.join(acceptedOptionalDir, 'cp1.json'), `${JSON.stringify({
|
|
@@ -344,4 +384,19 @@ const acceptedNotice = await notifyHumanInputRequests(root, { queue, notifyComma
|
|
|
344
384
|
assert.equal(acceptedNotice.results.some((item) => item.taskId === acceptedOptional.task.id), false);
|
|
345
385
|
assert.equal((await queueStatus(root, queue)).waiting, 0);
|
|
346
386
|
|
|
347
|
-
|
|
387
|
+
// Missing source/actor/generation binding must fail closed: no waiting move,
|
|
388
|
+
// no gate command text, and no notification invocation.
|
|
389
|
+
const unbound = await enqueueTask(root, { queue, title: 'Unbound human request', task: 'Project openreel B-01', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner' });
|
|
390
|
+
const unboundDir = path.join(taskRuntimeDirFor(root, queue, unbound.task.id), 'checkpoints');
|
|
391
|
+
await mkdir(unboundDir, { recursive: true });
|
|
392
|
+
await writeFile(path.join(unboundDir, 'cp1.json'), `${JSON.stringify({
|
|
393
|
+
version: 1, task_id: unbound.task.id, checkpoint_id: 'cp1', milestone_id: 'B-01',
|
|
394
|
+
status: 'needs_human_input', blockers: [{ action: 'approve', required_authority: 'owner', authorization_state: 'missing', needed_when: 'now' }]
|
|
395
|
+
}, null, 2)}\n`);
|
|
396
|
+
const unboundNotice = await notifyHumanInputRequests(root, { queue, notifyCommand: '/bin/true' });
|
|
397
|
+
const unboundResult = unboundNotice.results.find((item) => item.taskId === unbound.task.id);
|
|
398
|
+
assert.equal(unboundResult.outcome, 'fail_closed');
|
|
399
|
+
assert.equal(unboundResult.message, undefined);
|
|
400
|
+
assert.equal((await queueStatus(root, queue)).waiting, 0);
|
|
401
|
+
|
|
402
|
+
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.
|
|
427
|
-
assert.
|
|
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,
|