taskforce-loop-engineering 0.15.10 → 0.15.11

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,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.15.11 - 2026-08-28
6
+
7
+ - Add a transactional state kernel with typed effects, receipt chaining, CAS/fencing, exact-effect replay, crash recovery, and completion fencing.
8
+ - Add the public Goal API and five-command primary CLI while preserving advanced commands.
9
+ - Add public CI and seven competitive acceptance fixtures.
10
+
5
11
  ## 0.15.10 - 2026-08-28
6
12
 
7
13
  - Materialize human-input gates only for permissions or external conditions that are explicitly missing and needed now; preserve authorized, consumed, future, and conditional boundaries as audit context instead of repeatedly blocking project progress.
package/MIGRATING.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Migrating to Taskforce Loop Engineering 0.12.0
2
2
 
3
+ ## Goal API and primary CLI
4
+
5
+ The five-command Goal interface is additive. Existing advanced commands and artifact formats are not removed or automatically rewritten. New integrations should prefer `init/run/status/review/doctor --id`; existing integrations may migrate incrementally. See `docs/transactional-kernel-and-goal-api.md`.
6
+
3
7
  ## Operator projection
4
8
 
5
9
  No runtime artifact migration is required. P3 reads P0/P1/P2 and legacy queue/project artifacts in place and emits projection schema `1.0.0`; existing writers remain authoritative. Consumers should use `schema_version`, tolerate additive fields, and treat degraded health as a refresh/investigation signal. `dashboard-serve` is loopback-only unless `--allow-non-loopback` is explicit.
package/README.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Taskforce Loop Engineering
2
2
 
3
+ ## Primary Goal interface
4
+
5
+ The ordinary public surface is `init`, `run`, `status`, `review`, and `doctor`:
6
+
7
+ ```sh
8
+ loop-engineering init --id demo --goal "Deliver the complete verified result"
9
+ loop-engineering run --id demo
10
+ loop-engineering status --id demo
11
+ loop-engineering review --id demo --decision revise --reason "change strategy"
12
+ loop-engineering doctor --id demo
13
+ ```
14
+
15
+ Node.js callers can use `Goal.init/run/status/review/doctor`. Existing queue, project, revision, human-gate, reservation, dashboard, and worktree commands remain supported as advanced commands. See [the transactional kernel and migration guide](docs/transactional-kernel-and-goal-api.md).
16
+
3
17
  [![production trust](https://github.com/ambitioncn/taskforce-loop-engineering/actions/workflows/production-trust.yml/badge.svg)](https://github.com/ambitioncn/taskforce-loop-engineering/actions/workflows/production-trust.yml)
4
18
 
5
19
  ## Platform-neutral adapter SDK
@@ -100,6 +100,7 @@ import {
100
100
  exportDashboard,
101
101
  filterProjection
102
102
  } from '../lib/operator-dashboard.mjs';
103
+ import { doctorGoal, initGoal, reviewGoal, runGoal, statusGoal } from '../lib/goal-api.mjs';
103
104
 
104
105
  function parseArgs(argv) {
105
106
  const args = { _: [], root: process.cwd(), json: false, force: false };
@@ -1605,9 +1606,16 @@ async function writeRevisionDriftAllowTemplateOutput(root, template, output, opt
1605
1606
  return { file: path.relative(root, outputFile), format: 'json' };
1606
1607
  }
1607
1608
 
1608
- const HELP = `loop-engineering - verifiable agent work loops
1609
+ const HELP = `loop-engineering - durable Goal loops
1609
1610
 
1610
1611
  Usage:
1612
+ loop-engineering init --id goal-id --goal "Terminal goal" [--root <workspace>] [--json]
1613
+ loop-engineering run --id goal-id [--root <workspace>] [--json]
1614
+ loop-engineering status --id goal-id [--root <workspace>] [--json]
1615
+ loop-engineering review --id goal-id --decision accept|revise|wait [--reason text] [--root <workspace>] [--json]
1616
+ loop-engineering doctor [--id goal-id] [--root <workspace>] [--json]
1617
+
1618
+ Advanced compatibility commands:
1611
1619
  loop-engineering init [--root <workspace>] [--force]
1612
1620
  loop-engineering run --config configs/loops/name.json [--root <workspace>] [--json]
1613
1621
  loop-engineering verify [--config configs/loops/name.json] [--root <workspace>]
@@ -1694,6 +1702,11 @@ Exit codes:
1694
1702
  1 invalid spec, command error outside a check, or runtime failure`;
1695
1703
 
1696
1704
  async function runCommand(args) {
1705
+ if (args.id && !args.config) {
1706
+ const result = await runGoal(args.root, args.id, { triggerId: args.sourceMessageId ?? 'manual' });
1707
+ console.log(JSON.stringify(result, null, 2));
1708
+ return 0;
1709
+ }
1697
1710
  if (!args.config) throw new Error('run requires --config.');
1698
1711
  const root = args.root;
1699
1712
  const { spec, file: specPath } = await loadSpec(root, args.config);
@@ -1787,6 +1800,10 @@ async function verifyCommand(args) {
1787
1800
  }
1788
1801
 
1789
1802
  async function statusCommand(args) {
1803
+ if (args.id && !args.config) {
1804
+ console.log(JSON.stringify(await statusGoal(args.root, args.id), null, 2));
1805
+ return 0;
1806
+ }
1790
1807
  const files = await configFilesFromArgs(args.root, args.config ? ['--config', args.config] : []);
1791
1808
  if (files.length === 0) throw new Error('No loop configs found.');
1792
1809
  const reports = [];
@@ -1854,6 +1871,11 @@ async function summarizeCommand(args) {
1854
1871
  }
1855
1872
 
1856
1873
  async function doctorCommand(args) {
1874
+ if (args.id) {
1875
+ const report = await doctorGoal(args.root, args.id);
1876
+ console.log(JSON.stringify(report, null, 2));
1877
+ return report.ok ? 0 : 1;
1878
+ }
1857
1879
  const report = await doctorReport(args.root, { limit: args.limit ?? 10 });
1858
1880
  if (args.json) {
1859
1881
  console.log(JSON.stringify(report, null, 2));
@@ -1897,12 +1919,28 @@ async function repairPlanCommand(args) {
1897
1919
  }
1898
1920
 
1899
1921
  async function initCommand(args) {
1922
+ if (args.id || args.goal) {
1923
+ if (!args.id || !args.goal) throw new Error('Goal init requires both --id and --goal.');
1924
+ console.log(JSON.stringify(await initGoal(args.root, { id: args.id, goal: args.goal }), null, 2));
1925
+ return 0;
1926
+ }
1900
1927
  const config = await initWorkspace(args.root, { force: args.force });
1901
1928
  console.log(`initialized loop engineering at ${args.root}`);
1902
1929
  console.log(`config: ${config}`);
1903
1930
  return 0;
1904
1931
  }
1905
1932
 
1933
+ async function reviewCommand(args) {
1934
+ if (!args.id) throw new Error('review requires --id.');
1935
+ const result = await reviewGoal(args.root, args.id, {
1936
+ decision: args.decision,
1937
+ reason: args.reason ?? args.comment ?? '',
1938
+ revision: args.revision ?? 0
1939
+ });
1940
+ console.log(JSON.stringify(result, null, 2));
1941
+ return 0;
1942
+ }
1943
+
1906
1944
  async function queueInitCommand(args) {
1907
1945
  if (!args.queue) throw new Error('queue-init requires --queue.');
1908
1946
  const config = await initQueueConfig(args.root, args.queue, { force: args.force });
@@ -5611,6 +5649,7 @@ async function main() {
5611
5649
  if (command === 'run') return runCommand(args);
5612
5650
  if (command === 'verify') return verifyCommand(args);
5613
5651
  if (command === 'status') return statusCommand(args);
5652
+ if (command === 'review') return reviewCommand(args);
5614
5653
  if (command === 'summarize') return summarizeCommand(args);
5615
5654
  if (command === 'doctor') return doctorCommand(args);
5616
5655
  if (command.startsWith('dashboard-')) return dashboardCommand(command, args);
@@ -0,0 +1,25 @@
1
+ # Transactional kernel and Goal API
2
+
3
+ The public API is `Goal.init`, `Goal.run`, `Goal.status`, `Goal.review`, and `Goal.doctor` from `lib/goal-api.mjs`. The matching primary CLI is:
4
+
5
+ ```sh
6
+ loop-engineering init --id demo --goal "Ship the complete verified result"
7
+ loop-engineering run --id demo
8
+ loop-engineering status --id demo
9
+ loop-engineering review --id demo --decision revise --reason "change strategy"
10
+ loop-engineering doctor --id demo
11
+ ```
12
+
13
+ ## Transaction model
14
+
15
+ Every mutation enters `TransactionalStateKernel.transact` with typed effects. Writers hold an exclusive lease, receive a monotonically increasing fencing token, and may provide `expectedGeneration` for compare-and-swap. Each new effect produces a receipt containing its digest and the previous receipt hash. State records the receipt head and exact effect keys, so replay applies the same logical effect zero or one times.
16
+
17
+ Completion is fenced. Its validator must accept the complete terminal contract; a successful milestone alone cannot set `completed`. Human gates, revisions, action reservations, evidence, and completion are effect types rather than replacement state machines, so existing queue and reservation artifacts remain compatible.
18
+
19
+ External adapters must use the effect key as their upstream idempotency key. For uncertain provider outcomes, reconcile that key before retrying; do not invent a new effect key.
20
+
21
+ ## Compatibility and migration
22
+
23
+ All pre-0.16 advanced commands remain available. Existing `run --config`, `status --config`, queue, project, action-reservation, revision, human-gate, dashboard, and code-worktree commands retain their behavior.
24
+
25
+ The five-command surface is additive. Keep existing automation unchanged, create new goals through `init --id --goal`, use `run/status/review/doctor --id` ordinarily, and retain advanced commands for administration and diagnostics. No automatic migration rewrites legacy state. Adapters may project terminal contracts, revision lineage, human gates, and action reservations as typed effects while keeping authoritative legacy artifacts intact.
@@ -0,0 +1,59 @@
1
+ import path from 'node:path';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { TransactionalStateKernel } from './transactional-state-kernel.mjs';
4
+
5
+ const goalDir = (root, id) => path.join(root, 'runtime', 'loops', 'goals', id);
6
+ const requireId = (id) => {
7
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(id ?? '')) throw new Error('Goal id is invalid.');
8
+ return id;
9
+ };
10
+
11
+ export async function initGoal(root, input) {
12
+ const id = requireId(input.id);
13
+ if (typeof input.goal !== 'string' || input.goal.trim().length < 8) throw new Error('Goal must be a meaningful string.');
14
+ const directory = goalDir(root, id); await mkdir(directory, { recursive: true });
15
+ const contract = { version: 1, id, goal: input.goal, terminal_contract: input.terminalContract ?? null, created_at: new Date().toISOString() };
16
+ const file = path.join(directory, 'goal.json');
17
+ await writeFile(file, `${JSON.stringify(contract, null, 2)}\n`, { flag: 'wx' }).catch((error) => { if (error.code !== 'EEXIST') throw error; });
18
+ return statusGoal(root, id);
19
+ }
20
+
21
+ export async function statusGoal(root, id) {
22
+ requireId(id); const directory = goalDir(root, id);
23
+ const contract = JSON.parse(await readFile(path.join(directory, 'goal.json'), 'utf8'));
24
+ const kernel = new TransactionalStateKernel(path.join(directory, 'kernel'));
25
+ return { contract, runtime: await kernel.inspect(), receipt_chain: await kernel.verifyReceiptChain() };
26
+ }
27
+
28
+ export async function runGoal(root, id, input = {}) {
29
+ requireId(id); const directory = goalDir(root, id);
30
+ await readFile(path.join(directory, 'goal.json'), 'utf8');
31
+ const kernel = new TransactionalStateKernel(path.join(directory, 'kernel'));
32
+ return kernel.transact({
33
+ expectedGeneration: input.expectedGeneration,
34
+ effects: input.effects ?? [{ type: 'state_transition', key: `run:${input.triggerId ?? 'manual'}`, payload: { trigger: input.triggerId ?? 'manual' } }],
35
+ status: input.status ?? 'running',
36
+ reduce: input.reduce ?? ((state, effects) => ({ ...state, last_effects: effects.map((item) => item.key) })),
37
+ complete: input.complete
38
+ });
39
+ }
40
+
41
+ export async function reviewGoal(root, id, review) {
42
+ if (!['accept', 'revise', 'wait'].includes(review.decision)) throw new Error('Review decision must be accept, revise, or wait.');
43
+ return runGoal(root, id, {
44
+ expectedGeneration: review.expectedGeneration,
45
+ effects: [{
46
+ type: review.decision === 'wait' ? 'human_gate' : review.decision === 'revise' ? 'revision' : 'evidence',
47
+ key: review.key ?? `review:${review.decision}:${review.revision ?? 0}`,
48
+ payload: review
49
+ }],
50
+ status: review.decision === 'wait' ? 'waiting_for_human' : review.decision === 'revise' ? 'revision_pending' : 'accepted'
51
+ });
52
+ }
53
+
54
+ export async function doctorGoal(root, id) {
55
+ try { const status = await statusGoal(root, id); return { ok: true, id, generation: status.runtime.generation, receipt_chain: status.receipt_chain }; }
56
+ catch (error) { return { ok: false, id, error: error.message }; }
57
+ }
58
+
59
+ export const Goal = Object.freeze({ init: initGoal, run: runGoal, status: statusGoal, review: reviewGoal, doctor: doctorGoal });
@@ -0,0 +1,158 @@
1
+ import { mkdir, open, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import path from 'node:path';
4
+
5
+ export const EFFECT_TYPES = Object.freeze([
6
+ 'state_transition', 'human_gate', 'revision', 'action_reservation',
7
+ 'external_action', 'evidence', 'completion'
8
+ ]);
9
+
10
+ const allowedTypes = new Set(EFFECT_TYPES);
11
+ const canonical = (value) => JSON.stringify(sort(value));
12
+ const hash = (value) => createHash('sha256').update(value).digest('hex');
13
+ function sort(value) {
14
+ if (Array.isArray(value)) return value.map(sort);
15
+ if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sort(value[key])]));
16
+ return value;
17
+ }
18
+
19
+ async function readJson(file, fallback = null) {
20
+ try { return JSON.parse(await readFile(file, 'utf8')); }
21
+ catch (error) { if (error.code === 'ENOENT') return fallback; throw error; }
22
+ }
23
+
24
+ async function atomicWrite(file, value) {
25
+ await mkdir(path.dirname(file), { recursive: true });
26
+ const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
27
+ await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx' });
28
+ await rename(temporary, file);
29
+ }
30
+
31
+ export function typedEffect(type, payload, options = {}) {
32
+ if (!allowedTypes.has(type)) throw new Error(`Unsupported effect type: ${type}`);
33
+ if (payload === undefined) throw new Error('Effect payload is required.');
34
+ const effect = {
35
+ version: 1,
36
+ type,
37
+ key: options.key ?? hash(canonical({ type, payload })),
38
+ payload: sort(payload)
39
+ };
40
+ effect.digest = hash(canonical(effect));
41
+ return Object.freeze(effect);
42
+ }
43
+
44
+ export class TransactionalStateKernel {
45
+ constructor(directory) {
46
+ this.directory = directory;
47
+ this.stateFile = path.join(directory, 'state.json');
48
+ this.receiptFile = path.join(directory, 'receipts.jsonl');
49
+ this.lockFile = path.join(directory, 'writer.lock');
50
+ }
51
+
52
+ async inspect() {
53
+ return await readJson(this.stateFile, {
54
+ version: 1, generation: 0, fencing_token: 0, status: 'initialized',
55
+ state: {}, applied_effects: {}, receipts: [], last_receipt: null, completion: null
56
+ });
57
+ }
58
+
59
+ async acquire(owner = `pid:${process.pid}`) {
60
+ await mkdir(this.directory, { recursive: true });
61
+ let handle;
62
+ try { handle = await open(this.lockFile, 'wx'); }
63
+ catch (error) {
64
+ if (error.code !== 'EEXIST') throw error;
65
+ const stale = await readJson(this.lockFile, null);
66
+ const pid = Number(String(stale?.owner ?? '').match(/^pid:(\d+)$/)?.[1]);
67
+ let alive = Number.isInteger(pid) && pid > 0;
68
+ if (alive) { try { process.kill(pid, 0); } catch (probe) { if (probe.code === 'ESRCH') alive = false; else throw probe; } }
69
+ if (alive || !pid) throw new Error('Transactional writer lease is active or has an unverifiable owner.');
70
+ await rm(this.lockFile, { force: true });
71
+ handle = await open(this.lockFile, 'wx');
72
+ }
73
+ const current = await this.inspect();
74
+ const lease = { owner, fencingToken: current.fencing_token + 1, generation: current.generation, handle };
75
+ await handle.writeFile(JSON.stringify({ owner, fencing_token: lease.fencingToken }));
76
+ return lease;
77
+ }
78
+
79
+ async release(lease) {
80
+ await lease.handle.close();
81
+ await rm(this.lockFile, { force: true });
82
+ }
83
+
84
+ async transact(input) {
85
+ const lease = input.lease ?? await this.acquire(input.owner);
86
+ const owned = !input.lease;
87
+ try {
88
+ const current = await this.inspect();
89
+ if (lease.fencingToken <= current.fencing_token) throw new Error('Stale fencing token.');
90
+ if (input.expectedGeneration !== undefined && input.expectedGeneration !== current.generation) {
91
+ throw new Error(`CAS generation mismatch: expected ${input.expectedGeneration}, actual ${current.generation}.`);
92
+ }
93
+ const effects = (input.effects ?? []).map((effect) => typedEffect(effect.type, effect.payload, { key: effect.key }));
94
+ const fresh = effects.filter((effect) => !current.applied_effects[effect.key]);
95
+ const nextState = input.reduce ? await input.reduce(structuredClone(current.state), fresh) : current.state;
96
+ const next = {
97
+ ...current,
98
+ generation: current.generation + 1,
99
+ fencing_token: lease.fencingToken,
100
+ status: input.status ?? current.status,
101
+ state: nextState,
102
+ applied_effects: { ...current.applied_effects },
103
+ updated_at: new Date().toISOString()
104
+ };
105
+ const receipts = [];
106
+ let previous = current.last_receipt;
107
+ for (const effect of fresh) {
108
+ const receipt = {
109
+ version: 1, transaction_id: input.transactionId ?? randomUUID(),
110
+ generation: next.generation, fencing_token: lease.fencingToken,
111
+ effect_key: effect.key, effect_digest: effect.digest, effect_type: effect.type,
112
+ previous, created_at: new Date().toISOString()
113
+ };
114
+ receipt.receipt = hash(canonical(receipt));
115
+ previous = receipt.receipt;
116
+ next.applied_effects[effect.key] = { receipt: receipt.receipt, generation: next.generation, effect };
117
+ receipts.push(receipt);
118
+ }
119
+ next.last_receipt = previous;
120
+ next.receipts = [...(current.receipts ?? []), ...receipts];
121
+ if (input.complete) {
122
+ const verdict = await input.complete.validate({ current, next, freshEffects: fresh });
123
+ if (!verdict?.ok) throw new Error(`Completion fence rejected: ${verdict?.reason ?? 'validation failed'}`);
124
+ next.status = 'completed';
125
+ next.completion = { fenced_at_generation: next.generation, evidence: verdict.evidence ?? [], terminal_contract: input.complete.terminalContract ?? null };
126
+ }
127
+ await atomicWrite(this.stateFile, next);
128
+ if (receipts.length) await writeFile(this.receiptFile, receipts.map((item) => JSON.stringify(item)).join('\n') + '\n', { flag: 'a' });
129
+ return { state: next, receipts, replayed: effects.length - fresh.length };
130
+ } finally { if (owned) await this.release(lease); }
131
+ }
132
+
133
+ async replayEffect(effect, execute) {
134
+ const normalized = typedEffect(effect.type, effect.payload, { key: effect.key });
135
+ const current = await this.inspect();
136
+ const existing = current.applied_effects[normalized.key];
137
+ if (existing) return { executed: false, replayed: true, receipt: existing.receipt };
138
+ const outcome = await execute(normalized);
139
+ const committed = await this.transact({
140
+ expectedGeneration: current.generation,
141
+ effects: [normalized],
142
+ reduce: (state) => ({ ...state, effect_outcomes: { ...(state.effect_outcomes ?? {}), [normalized.key]: outcome } })
143
+ });
144
+ return { executed: true, replayed: false, outcome, receipt: committed.receipts[0]?.receipt };
145
+ }
146
+
147
+ async verifyReceiptChain() {
148
+ const state = await this.inspect();
149
+ let previous = null; let count = 0;
150
+ for (const item of state.receipts ?? []) {
151
+ const claimed = item.receipt; const unsigned = { ...item }; delete unsigned.receipt;
152
+ if (item.previous !== previous || hash(canonical(unsigned)) !== claimed) throw new Error(`Receipt chain invalid at ${count + 1}.`);
153
+ previous = claimed; count++;
154
+ }
155
+ if (state.last_receipt !== previous) throw new Error('Receipt head does not match state.');
156
+ return { ok: true, count, head: previous };
157
+ }
158
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskforce-loop-engineering",
3
- "version": "0.15.10",
3
+ "version": "0.15.11",
4
4
  "private": false,
5
5
  "description": "OpenClaw-native loop engineering CLI, templates, and skill for verifiable agent work loops.",
6
6
  "type": "module",
@@ -17,6 +17,8 @@
17
17
  "run-loop-cron.sh": "scripts/run-loop-cron.sh"
18
18
  },
19
19
  "scripts": {
20
+ "test": "npm run check && npm run check:competitive",
21
+ "check:competitive": "node --check lib/transactional-state-kernel.mjs && node --check lib/goal-api.mjs && node scripts/competitive-acceptance.mjs",
20
22
  "check:adapters": "node --check lib/runtime-adapter-sdk.mjs && node scripts/runtime-adapter-conformance.mjs",
21
23
  "demo:adapter": "node examples/adapter-sdk-demo.mjs",
22
24
  "check:production-trust": "node --check lib/runtime-adapter-v1.mjs && node --check lib/durable-journal.mjs && node --check lib/execution-ledger.mjs && node --check lib/production-evidence.mjs && node --check lib/upgrade-planner.mjs && node scripts/production-acceptance.mjs",
@@ -27,6 +29,12 @@
27
29
  "check": "npm run check:config-drift && npm run check:openclaw-install && npm run check:hermes-install && node --check bin/loop-engineering.mjs && node --check lib/core.mjs && node --check lib/action-reservations.mjs && node --check lib/todo-control-plane.mjs && node --check lib/operator-dashboard.mjs && node scripts/action-reservation-self-test.mjs && node scripts/todo-control-plane-self-test.mjs && node scripts/operator-dashboard-self-test.mjs && node scripts/final-judgement-self-test.mjs && node scripts/route-notify-self-test.mjs && node scripts/scheduler-heartbeat-self-test.mjs && node scripts/human-gate-lifecycle-v2-self-test.mjs && node bin/loop-engineering.mjs verify --config templates/workspace-health.json --root . && node bin/loop-engineering.mjs queue-status --queue smoke --root . --json >/dev/null && node bin/loop-engineering.mjs project-intake --root /tmp/loop-engineering-check --name smoke-project --brief \"Build a small website project\" --type auto --check \"npm test\" --json >/dev/null && node bin/loop-engineering.mjs project-plan --root /tmp/loop-engineering-check --project smoke-project --force --json >/dev/null && node bin/loop-engineering.mjs project-status --root /tmp/loop-engineering-check --project smoke-project --json >/dev/null && node bin/loop-engineering.mjs queue-scheduler-tick --queue smoke --root /tmp/loop-engineering-check --plan-only --force-due --json >/dev/null && node bin/loop-engineering.mjs queue-scheduler-tick --queue smoke-progress --root /tmp/loop-engineering-check --plan-only --force-due --progress-report --progress-report-when-not-due --json >/dev/null && node bin/loop-engineering.mjs workflow-metrics --queue smoke --root . --json >/dev/null && node bin/loop-engineering.mjs workflow-tune-plan --queue smoke --root . --json >/dev/null && node bin/loop-engineering.mjs code-queue-init --queue smoke-code --root /tmp/loop-engineering-check --force >/dev/null && node bin/loop-engineering.mjs code-worktree-list --queue smoke-code --root /tmp/loop-engineering-check --json >/dev/null && node bin/loop-engineering.mjs code-task-status --queue smoke-code --root /tmp/loop-engineering-check --json >/dev/null && node bin/loop-engineering.mjs code-task-dashboard --queue smoke-code --root /tmp/loop-engineering-check --json >/dev/null && node bin/loop-engineering.mjs code-worktree-cleanup-plan --queue smoke-code --root /tmp/loop-engineering-check --json >/dev/null && node bin/loop-engineering.mjs code-worktree-cleanup --queue smoke-code --root /tmp/loop-engineering-check --confirm-cleanup --json >/dev/null && node bin/loop-engineering.mjs code-patch-verify --patch README.md --root . --json >/dev/null && node bin/loop-engineering.mjs code-patch-apply-plan --patch README.md --json >/dev/null && node bin/loop-engineering.mjs queue-revision-ci-self-test --queue smoke-ci --root /tmp/loop-engineering-check --json >/dev/null && node bin/loop-engineering.mjs summarize --root . --json >/dev/null && node bin/loop-engineering.mjs dashboard-health --root . --max-age-seconds 999999999 --json >/dev/null && node bin/loop-engineering.mjs doctor --root . --json >/dev/null",
28
30
  "pack:dry": "npm pack --dry-run"
29
31
  },
32
+ "exports": {
33
+ ".": "./lib/goal-api.mjs",
34
+ "./goal": "./lib/goal-api.mjs",
35
+ "./transactional-kernel": "./lib/transactional-state-kernel.mjs",
36
+ "./runtime-adapter-sdk": "./lib/runtime-adapter-sdk.mjs"
37
+ },
30
38
  "engines": {
31
39
  "node": ">=22"
32
40
  },
@@ -0,0 +1,65 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { Goal, initGoal, reviewGoal, runGoal, statusGoal } from '../lib/goal-api.mjs';
6
+ import { TransactionalStateKernel } from '../lib/transactional-state-kernel.mjs';
7
+
8
+ const root = await mkdtemp(path.join(tmpdir(), 'loop-competitive-'));
9
+ const results = [];
10
+ const fixture = async (id, run) => { try { await run(); results.push({ id, ok: true }); } catch (error) { results.push({ id, ok: false, error: error.message }); } };
11
+
12
+ await fixture('crash_recovery', async () => {
13
+ await initGoal(root, { id: 'crash', goal: 'Recover durable progress after process interruption.' });
14
+ await runGoal(root, 'crash', { triggerId: 'before-crash' });
15
+ const kernelDir = path.join(root, 'runtime', 'loops', 'goals', 'crash', 'kernel');
16
+ await rm(path.join(kernelDir, 'receipts.jsonl'), { force: true });
17
+ await mkdir(kernelDir, { recursive: true });
18
+ await writeFile(path.join(kernelDir, 'writer.lock'), JSON.stringify({ owner: 'pid:2147483647' }));
19
+ await runGoal(root, 'crash', { triggerId: 'after-crash' });
20
+ const reopened = await statusGoal(root, 'crash');
21
+ assert.equal(reopened.runtime.generation, 2); assert.equal(reopened.receipt_chain.ok, true);
22
+ });
23
+ await fixture('duplicate_trigger', async () => {
24
+ await initGoal(root, { id: 'duplicate', goal: 'Apply each duplicate trigger at most once.' });
25
+ await runGoal(root, 'duplicate', { triggerId: 'same' });
26
+ const duplicate = await runGoal(root, 'duplicate', { triggerId: 'same' });
27
+ assert.equal(duplicate.replayed, 1); assert.equal(duplicate.receipts.length, 0);
28
+ });
29
+ await fixture('human_wait_resume', async () => {
30
+ await initGoal(root, { id: 'human', goal: 'Wait for and resume from a human decision.' });
31
+ await reviewGoal(root, 'human', { decision: 'wait', key: 'gate:1', reason: 'choose' });
32
+ assert.equal((await statusGoal(root, 'human')).runtime.status, 'waiting_for_human');
33
+ await reviewGoal(root, 'human', { decision: 'accept', key: 'gate:1:resume' });
34
+ assert.equal((await statusGoal(root, 'human')).runtime.status, 'accepted');
35
+ });
36
+ await fixture('standing_authorization', async () => {
37
+ await initGoal(root, { id: 'standing', goal: 'Respect bounded standing authorization scopes.' });
38
+ const result = await runGoal(root, 'standing', { effects: [{ type: 'action_reservation', key: 'auth:deploy:1', payload: { authorization: { kind: 'standing', scope: 'staging', limit: 1 }, action: 'deploy' } }] });
39
+ assert.equal(result.receipts[0].effect_type, 'action_reservation');
40
+ assert.equal(result.state.applied_effects['auth:deploy:1'].effect.payload.authorization.scope, 'staging');
41
+ });
42
+ await fixture('idempotent_external_action', async () => {
43
+ const kernel = new TransactionalStateKernel(path.join(root, 'external-kernel')); let calls = 0;
44
+ const effect = { type: 'external_action', key: 'provider:request-1', payload: { idempotency_key: 'request-1' } };
45
+ const execute = ({ key }) => { calls++; return { accepted: true, upstream_key: key }; };
46
+ await kernel.replayEffect(effect, execute); const replay = await kernel.replayEffect(effect, execute);
47
+ assert.equal(calls, 1); assert.equal(replay.replayed, true);
48
+ });
49
+ await fixture('false_milestone_completion', async () => {
50
+ const kernel = new TransactionalStateKernel(path.join(root, 'completion-kernel'));
51
+ await assert.rejects(kernel.transact({ effects: [{ type: 'completion', key: 'milestone:1', payload: { milestone: true } }], complete: { terminalContract: { required: ['m1', 'm2'] }, validate: async () => ({ ok: false, reason: 'required backlog remains' }) } }), /Completion fence rejected/);
52
+ assert.notEqual((await kernel.inspect()).status, 'completed');
53
+ });
54
+ await fixture('repeated_revision', async () => {
55
+ await initGoal(root, { id: 'revision', goal: 'Preserve repeated revision lineage without collision.' });
56
+ await reviewGoal(root, 'revision', { decision: 'revise', revision: 1, key: 'revision:1', parent: null });
57
+ await reviewGoal(root, 'revision', { decision: 'revise', revision: 2, key: 'revision:2', parent: 'revision:1' });
58
+ const status = await statusGoal(root, 'revision');
59
+ assert.equal(status.runtime.applied_effects['revision:2'].effect.payload.parent, 'revision:1'); assert.equal(status.receipt_chain.count, 2);
60
+ });
61
+
62
+ assert.deepEqual(Object.keys(Goal), ['init', 'run', 'status', 'review', 'doctor']);
63
+ await rm(root, { recursive: true, force: true });
64
+ console.log(JSON.stringify({ ok: results.every((item) => item.ok), fixtures: results }, null, 2));
65
+ if (results.some((item) => !item.ok)) process.exitCode = 1;
@@ -0,0 +1,27 @@
1
+ import { access, readFile } from 'node:fs/promises';
2
+ import { spawn } from 'node:child_process';
3
+ import path from 'node:path';
4
+
5
+ const root = path.resolve(import.meta.dirname, '..');
6
+ const checks = [];
7
+ const record = (id, ok, evidence) => checks.push({ id, ok, evidence });
8
+ const requiredFiles = ['lib/transactional-state-kernel.mjs', 'lib/goal-api.mjs', '.github/workflows/ci.yml', 'scripts/competitive-acceptance.mjs', 'docs/transactional-kernel-and-goal-api.md'];
9
+ for (const file of requiredFiles) {
10
+ try { await access(path.join(root, file)); record(`file:${file}`, true, file); }
11
+ catch { record(`file:${file}`, false, 'missing'); }
12
+ }
13
+ const source = await readFile(path.join(root, 'lib/transactional-state-kernel.mjs'), 'utf8');
14
+ for (const token of ['state_transition', 'human_gate', 'revision', 'action_reservation', 'external_action', 'completion', 'fencingToken', 'expectedGeneration', 'verifyReceiptChain', 'replayEffect']) {
15
+ record(`kernel:${token}`, source.includes(token), token);
16
+ }
17
+ const cli = await readFile(path.join(root, 'bin/loop-engineering.mjs'), 'utf8');
18
+ for (const command of ['init', 'run', 'status', 'review', 'doctor']) record(`cli:${command}`, cli.includes(`command === '${command}'`), command);
19
+ const fixture = await new Promise((resolve) => {
20
+ const child = spawn(process.execPath, ['scripts/competitive-acceptance.mjs'], { cwd: root, stdio: ['ignore', 'pipe', 'pipe'] });
21
+ let stdout = ''; let stderr = ''; child.stdout.on('data', (chunk) => { stdout += chunk; }); child.stderr.on('data', (chunk) => { stderr += chunk; });
22
+ child.on('close', (code) => resolve({ code, stdout, stderr }));
23
+ });
24
+ record('competitive_fixtures', fixture.code === 0, fixture.code === 0 ? '7/7 passed' : fixture.stderr);
25
+ const outcome = checks.every((item) => item.ok) ? 'accept' : 'reject';
26
+ console.log(JSON.stringify({ version: 1, scope: 'complete_project_terminal_contract', independent_from_runtime_implementation: true, outcome, checks, residual_risks: ['Filesystem durability depends on the host filesystem honoring atomic rename and fsync semantics.', 'External exactly-once behavior requires providers to honor the supplied idempotency key.'] }, null, 2));
27
+ if (outcome !== 'accept') process.exitCode = 1;