taskforce-loop-engineering 0.15.5 → 0.15.7
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,21 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.15.7 - 2026-08-24
|
|
6
|
+
|
|
7
|
+
- Prevent accepted phase checkpoints from producing `ready_to_apply` when they explicitly mark project completion as `in_progress`, including both string and object completion forms.
|
|
8
|
+
- Preserve unresolved checkpoint risks in the final judgement and treat a concrete continuation action as project work that must continue instead of terminal completion.
|
|
9
|
+
- Use an accepted terminal contract as the authoritative project-completion source when no separate acceptance ledger is configured.
|
|
10
|
+
- Reconcile stale optional post-completion authorization gates into `done` rather than `canceled`, while retaining fail-closed behavior for real blockers.
|
|
11
|
+
- Add regression coverage for premature terminal judgement, terminal-contract status projection, and historical stale-gate reconciliation.
|
|
12
|
+
|
|
13
|
+
## 0.15.6 - 2026-08-24
|
|
14
|
+
|
|
15
|
+
- Allow project configurations to use an external authoritative `backlogSource` without duplicating an embedded backlog registry.
|
|
16
|
+
- Keep project status fail-closed when neither an embedded backlog nor an external backlog source is configured.
|
|
17
|
+
- Suppress false registry-drift findings when no embedded projection is intentionally present, while retaining drift detection when both representations exist.
|
|
18
|
+
- Add regression coverage for external-only authoritative project backlogs.
|
|
19
|
+
|
|
5
20
|
## 0.15.5 - 2026-08-24
|
|
6
21
|
|
|
7
22
|
- Treat embedded-runtime `incomplete_turn` / abandoned-liveness trailers as recoverable interruptions even when the dispatcher wrapper exits zero, preventing partial work from being accepted or stale human gates from being replayed.
|
package/lib/core.mjs
CHANGED
|
@@ -1173,20 +1173,34 @@ async function projectSpecs(root) {
|
|
|
1173
1173
|
return specs;
|
|
1174
1174
|
}
|
|
1175
1175
|
|
|
1176
|
-
|
|
1176
|
+
function acceptedTerminalRecord(record) {
|
|
1177
|
+
if (!record || typeof record !== 'object') return false;
|
|
1178
|
+
const accepted = record.status === 'accepted' || record.terminalState?.accepted === true || record.terminalAccepted === true;
|
|
1179
|
+
const unmet = Array.isArray(record.unmet) ? record.unmet : [];
|
|
1180
|
+
const blockers = Array.isArray(record.blockers) ? record.blockers : [];
|
|
1181
|
+
return accepted && unmet.length === 0 && blockers.length === 0;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
async function projectAcceptanceRecord(root, spec) {
|
|
1177
1185
|
const configuredAcceptanceLedger = spec?.acceptanceLedger ?? spec?.ledger;
|
|
1178
|
-
|
|
1186
|
+
const configuredTerminalContract = spec?.terminalContract;
|
|
1187
|
+
const configured = configuredAcceptanceLedger ?? configuredTerminalContract;
|
|
1188
|
+
if (!configured) return null;
|
|
1179
1189
|
try {
|
|
1180
|
-
const
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
&& Array.isArray(ledger.blockers) && ledger.blockers.length === 0;
|
|
1190
|
+
const kind = configuredAcceptanceLedger ? 'project acceptance ledger' : 'project terminal contract';
|
|
1191
|
+
const file = path.resolve(root, safeRelativePath(configured, kind));
|
|
1192
|
+
const record = await readJson(file);
|
|
1193
|
+
return { file, record, source: configuredAcceptanceLedger ? 'acceptance_ledger' : 'terminal_contract' };
|
|
1185
1194
|
} catch {
|
|
1186
|
-
return
|
|
1195
|
+
return null;
|
|
1187
1196
|
}
|
|
1188
1197
|
}
|
|
1189
1198
|
|
|
1199
|
+
async function projectTerminalAccepted(root, spec) {
|
|
1200
|
+
const acceptance = await projectAcceptanceRecord(root, spec);
|
|
1201
|
+
return acceptedTerminalRecord(acceptance?.record);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1190
1204
|
function requirementIdsFromCheckpoint(checkpoint, specs) {
|
|
1191
1205
|
const explicit = checkpoint?.requirement_ids ?? checkpoint?.requirementIds;
|
|
1192
1206
|
if (Array.isArray(explicit)) return [...new Set(explicit.map(String))];
|
|
@@ -1284,12 +1298,19 @@ export async function reconcileProjectGates(root, options = {}) {
|
|
|
1284
1298
|
await writeJson(full, reconciled);
|
|
1285
1299
|
if (found?.subdir === 'waiting' && task?.waitingGateId === gate.gate_id) {
|
|
1286
1300
|
const resumeSafeBacklog = invalid.code === 'safe_backlog_actionable';
|
|
1287
|
-
const
|
|
1288
|
-
const
|
|
1301
|
+
const projectAccepted = invalid.code === 'project_accepted_optional_deferred';
|
|
1302
|
+
const destination = resumeSafeBacklog ? 'inbox' : projectAccepted ? 'done' : 'canceled';
|
|
1303
|
+
const destinationFile = path.join(queueSubdirFor(root, queue, destination), path.basename(found.file));
|
|
1304
|
+
const restored = {
|
|
1305
|
+
...task,
|
|
1306
|
+
status: resumeSafeBacklog ? 'queued' : projectAccepted ? 'completed' : invalid.status,
|
|
1307
|
+
gateReconciliation: reconciled.reconciliation
|
|
1308
|
+
};
|
|
1289
1309
|
delete restored.waitingGateId;
|
|
1290
1310
|
delete restored.waitKind;
|
|
1291
1311
|
delete restored.waitingSince;
|
|
1292
|
-
if (
|
|
1312
|
+
if (projectAccepted) restored.completedAt ??= now;
|
|
1313
|
+
else if (!resumeSafeBacklog) restored.canceledAt = now;
|
|
1293
1314
|
else restored.requeuedAt = now;
|
|
1294
1315
|
await writeJson(destinationFile, restored);
|
|
1295
1316
|
await rm(found.file, { force: true });
|
|
@@ -1669,7 +1690,12 @@ function validateProjectSpec(spec) {
|
|
|
1669
1690
|
normalizeLoopId(queue.queue);
|
|
1670
1691
|
if (!['code', 'standard'].includes(queue.kind)) throw new Error(`Unsupported project queue kind: ${queue.kind}`);
|
|
1671
1692
|
}
|
|
1672
|
-
|
|
1693
|
+
const hasEmbeddedBacklog = Array.isArray(spec.backlog) && spec.backlog.length > 0;
|
|
1694
|
+
const configuredBacklogSource = spec.backlogSource ?? spec.authoritativeBacklog;
|
|
1695
|
+
const hasConfiguredBacklog = typeof configuredBacklogSource === 'string' && configuredBacklogSource.trim().length > 0;
|
|
1696
|
+
if (!hasEmbeddedBacklog && !hasConfiguredBacklog) {
|
|
1697
|
+
throw new Error('Project spec must provide a non-empty backlog or backlogSource.');
|
|
1698
|
+
}
|
|
1673
1699
|
}
|
|
1674
1700
|
|
|
1675
1701
|
export async function projectPlan(root, options = {}) {
|
|
@@ -1801,20 +1827,43 @@ export async function projectStatus(root, options = {}) {
|
|
|
1801
1827
|
acceptanceLedger = { file: path.relative(root, ledgerFile), readable: false, error: error.message };
|
|
1802
1828
|
}
|
|
1803
1829
|
}
|
|
1804
|
-
|
|
1830
|
+
let terminalContract = null;
|
|
1831
|
+
if (!acceptanceLedger && spec.terminalContract) {
|
|
1832
|
+
const terminalFile = path.resolve(root, safeRelativePath(spec.terminalContract, 'project terminal contract'));
|
|
1833
|
+
try {
|
|
1834
|
+
const contract = await readJson(terminalFile);
|
|
1835
|
+
terminalContract = {
|
|
1836
|
+
file: path.relative(root, terminalFile),
|
|
1837
|
+
status: contract.status ?? null,
|
|
1838
|
+
terminalAccepted: contract.terminalState?.accepted === true,
|
|
1839
|
+
updatedAt: contract.updatedAt ?? contract.terminalState?.acceptedAt ?? null,
|
|
1840
|
+
unmet: Array.isArray(contract.unmet) ? contract.unmet : [],
|
|
1841
|
+
blockers: Array.isArray(contract.blockers) ? contract.blockers : []
|
|
1842
|
+
};
|
|
1843
|
+
} catch (error) {
|
|
1844
|
+
terminalContract = { file: path.relative(root, terminalFile), readable: false, error: error.message };
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
const hasEmbeddedBacklog = Array.isArray(spec.backlog) && spec.backlog.length > 0;
|
|
1848
|
+
const registryItems = hasEmbeddedBacklog ? spec.backlog : [];
|
|
1805
1849
|
const sourceItems = backlog ? await readJson(backlogFile).then((loaded) => loaded.items ?? loaded.tasks ?? []) : [];
|
|
1806
1850
|
const registryById = new Map(registryItems.map((item) => [item.id, item]));
|
|
1807
1851
|
const sourceById = new Map(sourceItems.map((item) => [item.id, item]));
|
|
1808
1852
|
const drift = [];
|
|
1809
|
-
|
|
1810
|
-
const
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1853
|
+
if (hasEmbeddedBacklog) {
|
|
1854
|
+
for (const id of new Set([...registryById.keys(), ...sourceById.keys()])) {
|
|
1855
|
+
const registry = registryById.get(id);
|
|
1856
|
+
const authoritative = sourceById.get(id);
|
|
1857
|
+
if (!registry || !authoritative) drift.push({ id, kind: registry ? 'missing_from_authoritative_backlog' : 'missing_from_registry' });
|
|
1858
|
+
else if (registry.status !== authoritative.status || Boolean(registry.required) !== Boolean(authoritative.required)) {
|
|
1859
|
+
drift.push({ id, kind: 'status_or_scope_mismatch', registry: { status: registry.status, required: registry.required }, authoritative: { status: authoritative.status, required: authoritative.required } });
|
|
1860
|
+
}
|
|
1815
1861
|
}
|
|
1816
1862
|
}
|
|
1817
|
-
const
|
|
1863
|
+
const completionRecord = acceptanceLedger?.readable !== false && acceptanceLedger
|
|
1864
|
+
? acceptanceLedger
|
|
1865
|
+
: terminalContract?.readable !== false ? terminalContract : null;
|
|
1866
|
+
const projectCompletion = acceptedTerminalRecord(completionRecord)
|
|
1818
1867
|
? 'accepted'
|
|
1819
1868
|
: 'in_progress';
|
|
1820
1869
|
const needsAttention = [];
|
|
@@ -1824,6 +1873,7 @@ export async function projectStatus(root, options = {}) {
|
|
|
1824
1873
|
if (queues.some((queue) => queue.status.locked)) needsAttention.push('queue_locked');
|
|
1825
1874
|
if (drift.length > 0) needsAttention.push('authoritative_source_drift');
|
|
1826
1875
|
if (acceptanceLedger?.readable === false) needsAttention.push('acceptance_ledger_unreadable');
|
|
1876
|
+
if (!acceptanceLedger && terminalContract?.readable === false) needsAttention.push('terminal_contract_unreadable');
|
|
1827
1877
|
return {
|
|
1828
1878
|
version: 1,
|
|
1829
1879
|
project,
|
|
@@ -1835,13 +1885,15 @@ export async function projectStatus(root, options = {}) {
|
|
|
1835
1885
|
totals,
|
|
1836
1886
|
backlog,
|
|
1837
1887
|
acceptanceLedger,
|
|
1888
|
+
terminalContract,
|
|
1838
1889
|
authority: {
|
|
1839
|
-
terminalContract: spec.terminalContract ?? null,
|
|
1890
|
+
terminalContract: terminalContract?.file ?? spec.terminalContract ?? null,
|
|
1840
1891
|
backlog: backlog?.file ?? null,
|
|
1841
1892
|
acceptanceLedger: acceptanceLedger?.file ?? null,
|
|
1842
|
-
|
|
1893
|
+
completionSource: acceptanceLedger ? 'acceptance_ledger' : terminalContract ? 'terminal_contract' : null,
|
|
1894
|
+
rule: 'acceptance ledger determines project completion when configured; otherwise the terminal contract is authoritative; backlog source determines remaining work; registry is a projection validated for drift'
|
|
1843
1895
|
},
|
|
1844
|
-
consistency: { ok: drift.length === 0 && acceptanceLedger?.readable !== false, drift },
|
|
1896
|
+
consistency: { ok: drift.length === 0 && acceptanceLedger?.readable !== false && terminalContract?.readable !== false, drift },
|
|
1845
1897
|
projectCompletion,
|
|
1846
1898
|
latestIntake,
|
|
1847
1899
|
needsAttention,
|
|
@@ -3579,6 +3631,26 @@ function aggregateCriticStatus(baseStatus, criticReviews) {
|
|
|
3579
3631
|
return 'accepted';
|
|
3580
3632
|
}
|
|
3581
3633
|
|
|
3634
|
+
function projectCompletionStatus(value) {
|
|
3635
|
+
if (typeof value === 'string') return value;
|
|
3636
|
+
if (value && typeof value === 'object') return value.status ?? null;
|
|
3637
|
+
return null;
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
function continuationNextAction(value) {
|
|
3641
|
+
const action = String(value ?? '').trim().toLowerCase();
|
|
3642
|
+
if (!action) return false;
|
|
3643
|
+
return ![
|
|
3644
|
+
'acceptance_review',
|
|
3645
|
+
'final_judge',
|
|
3646
|
+
'ready_to_apply',
|
|
3647
|
+
'apply',
|
|
3648
|
+
'report',
|
|
3649
|
+
'complete',
|
|
3650
|
+
'completed'
|
|
3651
|
+
].includes(action);
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3582
3654
|
function buildCheckpointReview(contract, acceptancePlan, checkpoint) {
|
|
3583
3655
|
const baseStatus = checkpointReviewStatus(checkpoint);
|
|
3584
3656
|
const failed = [];
|
|
@@ -3701,6 +3773,8 @@ export async function writeAcceptanceReviews(root, queue, task, taskContract, ac
|
|
|
3701
3773
|
checkpointCreatedAt: checkpoint.created_at ?? null,
|
|
3702
3774
|
createdAt: checkpoint.created_at ?? review.created_at,
|
|
3703
3775
|
projectCompletion: checkpoint.project_completion ?? null,
|
|
3776
|
+
checkpointRisks: Array.isArray(checkpoint.risks) ? checkpoint.risks : [],
|
|
3777
|
+
checkpointNextAction: checkpoint.next_action ?? null,
|
|
3704
3778
|
deferredGates: materializableDeferredGates(checkpoint),
|
|
3705
3779
|
status: review.status,
|
|
3706
3780
|
file: path.relative(root, reviewFile)
|
|
@@ -3773,8 +3847,11 @@ export function buildFinalJudgement(contract, acceptancePlan, devPlan, checkpoin
|
|
|
3773
3847
|
const blockedCount = effectiveReviews.filter((item) => item.status === 'blocked').length;
|
|
3774
3848
|
const dispatchStatus = context.dispatchStatus ?? 'unknown';
|
|
3775
3849
|
const dispatchFailure = context.dispatchFailureClassification ?? null;
|
|
3776
|
-
const projectCompletionAccepted = effectiveReviews.some((review) => review.projectCompletion
|
|
3777
|
-
const projectCompletionInProgress = effectiveReviews.some((review) => review.projectCompletion
|
|
3850
|
+
const projectCompletionAccepted = effectiveReviews.some((review) => projectCompletionStatus(review.projectCompletion) === 'accepted');
|
|
3851
|
+
const projectCompletionInProgress = effectiveReviews.some((review) => projectCompletionStatus(review.projectCompletion) === 'in_progress');
|
|
3852
|
+
const explicitContinuationReviews = effectiveReviews.filter((review) =>
|
|
3853
|
+
(review.checkpointRisks?.length ?? 0) > 0 && continuationNextAction(review.checkpointNextAction)
|
|
3854
|
+
);
|
|
3778
3855
|
const deferredGateCount = effectiveReviews.reduce((count, review) => count + (review.deferredGates?.length ?? 0), 0);
|
|
3779
3856
|
let outcome = 'needs_revision';
|
|
3780
3857
|
|
|
@@ -3826,9 +3903,11 @@ export function buildFinalJudgement(contract, acceptancePlan, devPlan, checkpoin
|
|
|
3826
3903
|
outcome = 'needs_revision';
|
|
3827
3904
|
reasons.push(`Accepted checkpoints (${acceptedCount}) do not cover required checkpoints (${Math.max(requiredCheckpoints, 1)}).`);
|
|
3828
3905
|
nextActions.push('Complete and review the remaining planned checkpoints.');
|
|
3829
|
-
} else if ((contract.task_scope === 'project' || projectCompletionInProgress || deferredGateCount > 0) && !projectCompletionAccepted) {
|
|
3906
|
+
} else if ((contract.task_scope === 'project' || projectCompletionInProgress || explicitContinuationReviews.length > 0 || deferredGateCount > 0) && !projectCompletionAccepted) {
|
|
3830
3907
|
outcome = 'project_in_progress';
|
|
3831
3908
|
reasons.push('The latest milestone is accepted, but the project terminal contract is not accepted.');
|
|
3909
|
+
if (projectCompletionInProgress) reasons.push('The latest effective checkpoint explicitly marks project completion as in_progress.');
|
|
3910
|
+
if (explicitContinuationReviews.length > 0) reasons.push(`${explicitContinuationReviews.length} accepted checkpoint(s) record unresolved risks and an explicit continuation action.`);
|
|
3832
3911
|
if (deferredGateCount > 0) reasons.push(`${deferredGateCount} deferred authorization gate(s) remain and must be materialized as waiting gates when no unrelated safe work is actionable.`);
|
|
3833
3912
|
nextActions.push('Reread the authoritative project ledger and continue with the next safe actionable backlog item, or wait on a structured authorization gate.');
|
|
3834
3913
|
} else if (contract.requires_human_gate) {
|
|
@@ -3868,12 +3947,14 @@ export function buildFinalJudgement(contract, acceptancePlan, devPlan, checkpoin
|
|
|
3868
3947
|
revise: reviseCount,
|
|
3869
3948
|
blocked: blockedCount,
|
|
3870
3949
|
deferred_gates: deferredGateCount,
|
|
3950
|
+
explicit_continuations: explicitContinuationReviews.length,
|
|
3871
3951
|
rubric_items: Array.isArray(acceptancePlan?.rubric) ? acceptancePlan.rubric.length : 0,
|
|
3872
3952
|
automation_suggestions: Array.isArray(acceptancePlan?.automation) ? acceptancePlan.automation.length : 0
|
|
3873
3953
|
},
|
|
3874
3954
|
residual_risks: [
|
|
3875
3955
|
...(contract.requires_human_gate ? ['Human gate still required before external or high-risk action.'] : []),
|
|
3876
|
-
...(context.verificationFailed ? ['Configured verification has failing commands.'] : [])
|
|
3956
|
+
...(context.verificationFailed ? ['Configured verification has failing commands.'] : []),
|
|
3957
|
+
...explicitContinuationReviews.flatMap((review) => review.checkpointRisks ?? [])
|
|
3877
3958
|
],
|
|
3878
3959
|
created_at: new Date().toISOString()
|
|
3879
3960
|
};
|
package/package.json
CHANGED
|
@@ -132,6 +132,34 @@ assert.equal(inferTaskScope({ body: 'Continue the overall project with a product
|
|
|
132
132
|
assert.equal(judgement.outcome, 'project_in_progress');
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
{
|
|
136
|
+
const devPlan = { checkpoints: [{ id: 'cp1' }] };
|
|
137
|
+
const reviews = { reviews: [{
|
|
138
|
+
checkpointId: 'cp2', milestoneId: 'cp1', sequence: 2, status: 'accepted',
|
|
139
|
+
projectCompletion: 'in_progress',
|
|
140
|
+
checkpointRisks: ['Creative/perceptual director scoring is not wired into the production inspector.'],
|
|
141
|
+
checkpointNextAction: 'wire_director_perceptual_quality_and_quote_cost_without_paid_calls'
|
|
142
|
+
}] };
|
|
143
|
+
const judgement = buildFinalJudgement(baseContract, basePlan, devPlan, { count: 2 }, reviews, { dispatchStatus: 'completed' });
|
|
144
|
+
assert.equal(judgement.outcome, 'project_in_progress');
|
|
145
|
+
assert.equal(judgement.coverage.explicit_continuations, 1);
|
|
146
|
+
assert.deepEqual(judgement.residual_risks, ['Creative/perceptual director scoring is not wired into the production inspector.']);
|
|
147
|
+
assert.match(judgement.reasons.join(' '), /explicitly marks project completion as in_progress/);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
{
|
|
151
|
+
const devPlan = { checkpoints: [{ id: 'cp1' }] };
|
|
152
|
+
const reviews = { reviews: [{
|
|
153
|
+
checkpointId: 'cp2', milestoneId: 'cp1', sequence: 2, status: 'accepted',
|
|
154
|
+
projectCompletion: null,
|
|
155
|
+
checkpointRisks: ['A required production integration remains incomplete.'],
|
|
156
|
+
checkpointNextAction: 'implement_remaining_production_integration'
|
|
157
|
+
}] };
|
|
158
|
+
const judgement = buildFinalJudgement(baseContract, basePlan, devPlan, { count: 2 }, reviews, { dispatchStatus: 'completed' });
|
|
159
|
+
assert.equal(judgement.outcome, 'project_in_progress');
|
|
160
|
+
assert.equal(judgement.coverage.explicit_continuations, 1);
|
|
161
|
+
}
|
|
162
|
+
|
|
135
163
|
{
|
|
136
164
|
const devPlan = { checkpoints: [{ id: 'cp1' }] };
|
|
137
165
|
const reviews = { reviews: [{
|
|
@@ -91,6 +91,45 @@ assert.equal(drifted.authority.acceptanceLedger, 'project/acceptance-ledger.json
|
|
|
91
91
|
assert.equal(drifted.consistency.ok, false);
|
|
92
92
|
assert(drifted.needsAttention.includes('authoritative_source_drift'));
|
|
93
93
|
|
|
94
|
+
// A project may keep its authoritative backlog exclusively in backlogSource.
|
|
95
|
+
// Requiring a duplicate embedded backlog makes the two copies drift and used
|
|
96
|
+
// to prevent project-status from reading an otherwise valid project ledger.
|
|
97
|
+
await writeFile(projectFile, `${JSON.stringify({
|
|
98
|
+
...spec,
|
|
99
|
+
backlog: undefined,
|
|
100
|
+
backlogSource: 'project/backlog.json',
|
|
101
|
+
acceptanceLedger: 'project/acceptance-ledger.json',
|
|
102
|
+
terminalContract: 'project/terminal.md'
|
|
103
|
+
}, null, 2)}\n`);
|
|
104
|
+
const externalOnly = await projectStatus(root, { project: 'openreel' });
|
|
105
|
+
assert.equal(externalOnly.backlog.file, 'project/backlog.json');
|
|
106
|
+
assert.equal(externalOnly.backlog.count, 1);
|
|
107
|
+
assert.equal(externalOnly.consistency.ok, true);
|
|
108
|
+
|
|
109
|
+
// When a project deliberately uses its terminal contract as the completion
|
|
110
|
+
// ledger, accepted terminal bytes must project accepted status and suppress
|
|
111
|
+
// optional post-completion authorization gates.
|
|
112
|
+
const terminalContractFile = path.join(root, 'project', 'terminal-contract.json');
|
|
113
|
+
await writeFile(terminalContractFile, `${JSON.stringify({
|
|
114
|
+
status: 'accepted', terminalState: { accepted: true }, unmet: [], blockers: []
|
|
115
|
+
}, null, 2)}\n`);
|
|
116
|
+
await writeFile(projectFile, `${JSON.stringify({
|
|
117
|
+
...spec,
|
|
118
|
+
backlog: undefined,
|
|
119
|
+
backlogSource: 'project/backlog.json',
|
|
120
|
+
acceptanceLedger: undefined,
|
|
121
|
+
terminalContract: 'project/terminal-contract.json'
|
|
122
|
+
}, null, 2)}\n`);
|
|
123
|
+
const terminalOnly = await projectStatus(root, { project: 'openreel' });
|
|
124
|
+
assert.equal(terminalOnly.projectCompletion, 'accepted');
|
|
125
|
+
assert.equal(terminalOnly.authority.completionSource, 'terminal_contract');
|
|
126
|
+
await writeFile(projectFile, `${JSON.stringify({
|
|
127
|
+
...spec,
|
|
128
|
+
backlogSource: 'project/backlog.json',
|
|
129
|
+
acceptanceLedger: 'project/acceptance-ledger.json',
|
|
130
|
+
terminalContract: 'project/terminal.md'
|
|
131
|
+
}, null, 2)}\n`);
|
|
132
|
+
|
|
94
133
|
// A ready milestone with project in progress and a deferred authorization is
|
|
95
134
|
// converted into a structured waiting gate, not left as prose on a done task.
|
|
96
135
|
const deferred = await enqueueTask(root, { queue, title: 'OpenReel deferred S-01', task: 'Project openreel S-01', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner' });
|
|
@@ -155,6 +194,23 @@ await writeFile(authoritativeLedger, `${JSON.stringify({ status: 'accepted', unm
|
|
|
155
194
|
await reconcileProjectGates(root, { queue });
|
|
156
195
|
assert.equal((await queueStatus(root, queue)).waiting, 0);
|
|
157
196
|
assert.equal(JSON.parse(await readFile(path.join(root, deferredResult.ledger), 'utf8')).status, 'superseded');
|
|
197
|
+
const legacyTerminal = await enqueueTask(root, { queue, title: 'Legacy accepted-project gate', task: 'Project openreel terminal bookkeeping', projectId: 'openreel' });
|
|
198
|
+
const legacyCheckpointDir = path.join(taskRuntimeDirFor(root, queue, legacyTerminal.task.id), 'checkpoints');
|
|
199
|
+
await mkdir(legacyCheckpointDir, { recursive: true });
|
|
200
|
+
await writeFile(path.join(legacyCheckpointDir, 'cp-terminal.json'), `${JSON.stringify({ checkpoint_id: 'cp-terminal', status: 'ready_for_acceptance', blockers: [] }, null, 2)}\n`);
|
|
201
|
+
const legacyGateId = `${legacyTerminal.task.id}:cp-terminal`;
|
|
202
|
+
const legacyGateFile = path.join(root, 'runtime', 'loops', queue, 'human-input', 'gates', `${legacyTerminal.task.id}.cp-terminal.json`);
|
|
203
|
+
await writeFile(legacyGateFile, `${JSON.stringify({
|
|
204
|
+
gate_id: legacyGateId, task_id: legacyTerminal.task.id, checkpoint_id: 'cp-terminal', project_id: 'openreel',
|
|
205
|
+
requirement_ids: [], contract_hash: 'legacy', gate_kind: 'deferred_authorization', status: 'waiting_for_human'
|
|
206
|
+
}, null, 2)}\n`);
|
|
207
|
+
const legacyInbox = path.join(root, legacyTerminal.file);
|
|
208
|
+
const legacyWaiting = path.join(root, 'runtime', 'loops', queue, 'waiting', path.basename(legacyTerminal.file));
|
|
209
|
+
await writeFile(legacyWaiting, `${JSON.stringify({ ...legacyTerminal.task, status: 'waiting_for_human', waitingGateId: legacyGateId }, null, 2)}\n`);
|
|
210
|
+
await rename(legacyInbox, `${legacyInbox}.moved`);
|
|
211
|
+
await reconcileProjectGates(root, { queue });
|
|
212
|
+
assert.equal((await queueStatus(root, queue)).done >= 1, true, 'accepted project task must close in done, not canceled');
|
|
213
|
+
assert.equal(JSON.parse(await readFile(legacyGateFile, 'utf8')).status, 'superseded');
|
|
158
214
|
const acceptedOptional = await enqueueTask(root, { queue, title: 'OpenReel optional operations transfer', task: 'Project openreel optional post-completion transfer', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner' });
|
|
159
215
|
const acceptedOptionalDir = path.join(taskRuntimeDirFor(root, queue, acceptedOptional.task.id), 'checkpoints');
|
|
160
216
|
await mkdir(acceptedOptionalDir, { recursive: true });
|