taskforce-loop-engineering 0.15.5 → 0.15.6
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.6 - 2026-08-24
|
|
6
|
+
|
|
7
|
+
- Allow project configurations to use an external authoritative `backlogSource` without duplicating an embedded backlog registry.
|
|
8
|
+
- Keep project status fail-closed when neither an embedded backlog nor an external backlog source is configured.
|
|
9
|
+
- Suppress false registry-drift findings when no embedded projection is intentionally present, while retaining drift detection when both representations exist.
|
|
10
|
+
- Add regression coverage for external-only authoritative project backlogs.
|
|
11
|
+
|
|
5
12
|
## 0.15.5 - 2026-08-24
|
|
6
13
|
|
|
7
14
|
- 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
|
@@ -1669,7 +1669,12 @@ function validateProjectSpec(spec) {
|
|
|
1669
1669
|
normalizeLoopId(queue.queue);
|
|
1670
1670
|
if (!['code', 'standard'].includes(queue.kind)) throw new Error(`Unsupported project queue kind: ${queue.kind}`);
|
|
1671
1671
|
}
|
|
1672
|
-
|
|
1672
|
+
const hasEmbeddedBacklog = Array.isArray(spec.backlog) && spec.backlog.length > 0;
|
|
1673
|
+
const configuredBacklogSource = spec.backlogSource ?? spec.authoritativeBacklog;
|
|
1674
|
+
const hasConfiguredBacklog = typeof configuredBacklogSource === 'string' && configuredBacklogSource.trim().length > 0;
|
|
1675
|
+
if (!hasEmbeddedBacklog && !hasConfiguredBacklog) {
|
|
1676
|
+
throw new Error('Project spec must provide a non-empty backlog or backlogSource.');
|
|
1677
|
+
}
|
|
1673
1678
|
}
|
|
1674
1679
|
|
|
1675
1680
|
export async function projectPlan(root, options = {}) {
|
|
@@ -1801,17 +1806,20 @@ export async function projectStatus(root, options = {}) {
|
|
|
1801
1806
|
acceptanceLedger = { file: path.relative(root, ledgerFile), readable: false, error: error.message };
|
|
1802
1807
|
}
|
|
1803
1808
|
}
|
|
1804
|
-
const
|
|
1809
|
+
const hasEmbeddedBacklog = Array.isArray(spec.backlog) && spec.backlog.length > 0;
|
|
1810
|
+
const registryItems = hasEmbeddedBacklog ? spec.backlog : [];
|
|
1805
1811
|
const sourceItems = backlog ? await readJson(backlogFile).then((loaded) => loaded.items ?? loaded.tasks ?? []) : [];
|
|
1806
1812
|
const registryById = new Map(registryItems.map((item) => [item.id, item]));
|
|
1807
1813
|
const sourceById = new Map(sourceItems.map((item) => [item.id, item]));
|
|
1808
1814
|
const drift = [];
|
|
1809
|
-
|
|
1810
|
-
const
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
+
if (hasEmbeddedBacklog) {
|
|
1816
|
+
for (const id of new Set([...registryById.keys(), ...sourceById.keys()])) {
|
|
1817
|
+
const registry = registryById.get(id);
|
|
1818
|
+
const authoritative = sourceById.get(id);
|
|
1819
|
+
if (!registry || !authoritative) drift.push({ id, kind: registry ? 'missing_from_authoritative_backlog' : 'missing_from_registry' });
|
|
1820
|
+
else if (registry.status !== authoritative.status || Boolean(registry.required) !== Boolean(authoritative.required)) {
|
|
1821
|
+
drift.push({ id, kind: 'status_or_scope_mismatch', registry: { status: registry.status, required: registry.required }, authoritative: { status: authoritative.status, required: authoritative.required } });
|
|
1822
|
+
}
|
|
1815
1823
|
}
|
|
1816
1824
|
}
|
|
1817
1825
|
const projectCompletion = acceptanceLedger?.status === 'accepted' && acceptanceLedger.unmet.length === 0 && acceptanceLedger.blockers.length === 0
|
package/package.json
CHANGED
|
@@ -91,6 +91,27 @@ 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
|
+
await writeFile(projectFile, `${JSON.stringify({
|
|
109
|
+
...spec,
|
|
110
|
+
backlogSource: 'project/backlog.json',
|
|
111
|
+
acceptanceLedger: 'project/acceptance-ledger.json',
|
|
112
|
+
terminalContract: 'project/terminal.md'
|
|
113
|
+
}, null, 2)}\n`);
|
|
114
|
+
|
|
94
115
|
// A ready milestone with project in progress and a deferred authorization is
|
|
95
116
|
// converted into a structured waiting gate, not left as prose on a done task.
|
|
96
117
|
const deferred = await enqueueTask(root, { queue, title: 'OpenReel deferred S-01', task: 'Project openreel S-01', projectId: 'openreel', sourceChannel: 'test', sourceTarget: 'owner' });
|