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
- if (!Array.isArray(spec.backlog) || spec.backlog.length === 0) throw new Error('Project spec backlog must be non-empty.');
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 registryItems = Array.isArray(spec.backlog) ? spec.backlog : [];
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
- for (const id of new Set([...registryById.keys(), ...sourceById.keys()])) {
1810
- const registry = registryById.get(id);
1811
- const authoritative = sourceById.get(id);
1812
- if (!registry || !authoritative) drift.push({ id, kind: registry ? 'missing_from_authoritative_backlog' : 'missing_from_registry' });
1813
- else if (registry.status !== authoritative.status || Boolean(registry.required) !== Boolean(authoritative.required)) {
1814
- drift.push({ id, kind: 'status_or_scope_mismatch', registry: { status: registry.status, required: registry.required }, authoritative: { status: authoritative.status, required: authoritative.required } });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskforce-loop-engineering",
3
- "version": "0.15.5",
3
+ "version": "0.15.6",
4
4
  "private": false,
5
5
  "description": "OpenClaw-native loop engineering CLI, templates, and skill for verifiable agent work loops.",
6
6
  "type": "module",
@@ -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' });