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 +7 -0
- package/lib/core.mjs +74 -10
- package/package.json +1 -1
- package/scripts/final-judgement-self-test.mjs +25 -0
- package/scripts/project-gate-reconciliation-self-test.mjs +27 -12
- 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,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',
|
|
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
|
-
|
|
3702
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
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.
|
|
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,
|