@quolu/lattice 0.12.9 → 0.12.10

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/bin/lattice.mjs CHANGED
@@ -1,9 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { installPipeCloseGuard } from '../src/cli-stdio.mjs';
3
4
  import { runRuntimeCli } from '../src/runtime-cli.mjs';
4
5
  import { renderCliHelp } from '../src/cli-help.mjs';
5
6
  import packageJson from '../package.json' with { type: 'json' };
6
7
 
8
+ installPipeCloseGuard();
9
+
7
10
  const args = process.argv.slice(2);
8
11
  const help = renderCliHelp(args);
9
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.12.9",
3
+ "version": "0.12.10",
4
4
  "description": "Lattice — phase-aware TODO graph compiler and conflict-aware orchestration runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -54,7 +54,7 @@
54
54
  "test": "node scripts/run-product-tests.mjs",
55
55
  "test:sensor": "npm --prefix sensor test",
56
56
  "precheck": "node --check src/bridge-launch-agent.mjs",
57
- "check": "node --check bin/lattice.mjs && node --check bin/lattice-mcp.mjs && node --check bin/lattice-dashboard.mjs && node --check bin/lattice-bridge.mjs && node --check src/bridge-config.mjs && node --check src/bridge-server.mjs && node --check src/bridge-daemon.mjs && node --check src/bridge-cli.mjs && node --check src/project-cli.mjs && node --check src/sensor-cli.mjs && node --check src/sensor-runtime.mjs && node --check src/sensor-adapter.mjs && node --check src/factory-diagnostics.mjs && node --check src/runtime-errors.mjs && node --check src/runtime-contracts.mjs && node --check src/runtime-event-store.mjs && node --check src/hash-chain.mjs && node --check src/dag-chain.mjs && node --check src/todo-contracts.mjs && node --check src/todo-chain.mjs && node --check src/todo-store.mjs && node --check src/todo-migration.mjs && node --check src/todo-revision.mjs && node --check src/todo-status.mjs && node --check src/todo-cli.mjs && node --check src/todo-dashboard-registry.mjs && node --check src/todo-gantt-presentation.mjs && node --check src/todo-gantt-live.mjs && node --check src/bounded-seam.mjs && node --check src/todo-narrative-anchor.mjs && node --check src/todo-markdown-renderer.mjs && node --check src/todo-gantt-svg.mjs && node --check src/todo-gantt-html.mjs && node --check src/runtime-projection.mjs && node --check src/runtime-decision-verifier.mjs && node --check src/runtime-front-end.mjs && node --check src/runtime-cli.mjs && node --check src/rc3-dogfood-scaffold.mjs && node --check src/runtime-engine.mjs && node --check src/runtime-scripted-executor.mjs && node --check src/runtime-diff-observer.mjs && node --check src/runtime-worktree-executor.mjs && node --check src/runtime-hold-recompile.mjs && node --check src/rc3-scripted-campaign.mjs && node --check src/rc3-actual-dogfood.mjs && node --check src/rc4-stage1-dogfood.mjs && node --check research/fixtures/dispatch-record/src/dispatch-record.mjs && node --check test/research-dispatch-record.test.mjs",
57
+ "check": "node --check bin/lattice.mjs && node --check bin/lattice-mcp.mjs && node --check bin/lattice-dashboard.mjs && node --check bin/lattice-bridge.mjs && node --check src/cli-stdio.mjs && node --check src/bridge-config.mjs && node --check src/bridge-server.mjs && node --check src/bridge-daemon.mjs && node --check src/bridge-cli.mjs && node --check src/project-cli.mjs && node --check src/sensor-cli.mjs && node --check src/sensor-runtime.mjs && node --check src/sensor-adapter.mjs && node --check src/factory-diagnostics.mjs && node --check src/runtime-errors.mjs && node --check src/runtime-contracts.mjs && node --check src/runtime-event-store.mjs && node --check src/hash-chain.mjs && node --check src/dag-chain.mjs && node --check src/todo-contracts.mjs && node --check src/todo-chain.mjs && node --check src/todo-store.mjs && node --check src/todo-migration.mjs && node --check src/todo-revision.mjs && node --check src/todo-status.mjs && node --check src/todo-cli.mjs && node --check src/todo-dashboard-registry.mjs && node --check src/todo-gantt-presentation.mjs && node --check src/todo-gantt-live.mjs && node --check src/bounded-seam.mjs && node --check src/todo-narrative-anchor.mjs && node --check src/todo-markdown-renderer.mjs && node --check src/todo-gantt-svg.mjs && node --check src/todo-gantt-html.mjs && node --check src/runtime-projection.mjs && node --check src/runtime-decision-verifier.mjs && node --check src/runtime-front-end.mjs && node --check src/runtime-cli.mjs && node --check src/rc3-dogfood-scaffold.mjs && node --check src/runtime-engine.mjs && node --check src/runtime-scripted-executor.mjs && node --check src/runtime-diff-observer.mjs && node --check src/runtime-worktree-executor.mjs && node --check src/runtime-hold-recompile.mjs && node --check src/rc3-scripted-campaign.mjs && node --check src/rc3-actual-dogfood.mjs && node --check src/rc4-stage1-dogfood.mjs && node --check research/fixtures/dispatch-record/src/dispatch-record.mjs && node --check test/research-dispatch-record.test.mjs",
58
58
  "check:project-identity": "node --check src/project-identity.mjs",
59
59
  "ci": "npm run test && npm run test:sensor && npm run check && npm run check:project-identity"
60
60
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * CLI stdio guards.
3
+ *
4
+ * Reading part of a result is a legitimate use: `lattice todo status --json |
5
+ * head` stops the consumer as soon as it has what it wants. When that consumer
6
+ * exits, the next write to stdout raises EPIPE, and Node's default handling
7
+ * turns it into an unhandled 'error' event — the CLI dies with a stack trace
8
+ * and a non-zero exit, so a working pipeline reports a failure that never
9
+ * happened.
10
+ *
11
+ * EPIPE from a closed consumer is the POSIX contract, not a fault, so the
12
+ * process stops quietly. This is deliberately narrow: only EPIPE, only on the
13
+ * output streams. Every other stream error keeps its previous behaviour and
14
+ * still crashes the CLI, because those are real faults.
15
+ */
16
+
17
+ /**
18
+ * @param {object} [options]
19
+ * @param {Array<NodeJS.WriteStream>} [options.streams] streams to guard.
20
+ * @param {(code: number) => void} [options.exit] process exit, injectable for tests.
21
+ * @returns {() => void} removes the guards again (used by tests).
22
+ */
23
+ export function installPipeCloseGuard({
24
+ streams = [process.stdout, process.stderr],
25
+ exit = (code) => process.exit(code),
26
+ } = {}) {
27
+ const installed = [];
28
+ for (const stream of streams) {
29
+ if (stream === undefined || stream === null || typeof stream.on !== 'function') continue;
30
+ const onError = (error) => {
31
+ if (error?.code !== 'EPIPE') throw error;
32
+ exit(0);
33
+ };
34
+ stream.on('error', onError);
35
+ installed.push(() => stream.off('error', onError));
36
+ }
37
+ return () => {
38
+ for (const remove of installed) remove();
39
+ };
40
+ }
@@ -1082,17 +1082,41 @@ function nextEvent(input, storeMember) {
1082
1082
  return event;
1083
1083
  }
1084
1084
 
1085
+ /**
1086
+ * The digest replay binds a task's completion to.
1087
+ *
1088
+ * A ToDo completed inside this plan version binds to its own `done` event. A
1089
+ * ToDo carried across a revision has no `done` event in the successor journal —
1090
+ * its completion arrives with the `plan_genesis` state migration, and replay
1091
+ * binds it to the genesis digest (see the carry branch in `replay`). Resolving
1092
+ * only the first case is what made `reopen` unusable on carried work.
1093
+ *
1094
+ * Returns null when the task has no completion to bind to at all.
1095
+ */
1096
+ function resolveDoneBindingDigest(storeMember, taskId) {
1097
+ const events = storeMember.journal.events;
1098
+ const authored = [...events].reverse().find((event) => (
1099
+ event.kind === 'done' && event.task_id === taskId
1100
+ ));
1101
+ if (authored !== undefined) return authored.event_digest;
1102
+ const genesis = events[0];
1103
+ if (genesis?.kind !== 'plan_genesis' || !Array.isArray(genesis.state_migration)) return null;
1104
+ const carried = genesis.state_migration.some((migration) => (
1105
+ ['carry', 'carry_reconciled_metadata'].includes(migration.state_policy)
1106
+ && migration.to_task_id === taskId && migration.state?.status === 'done'
1107
+ ));
1108
+ return carried ? genesis.event_digest : null;
1109
+ }
1110
+
1085
1111
  function resolveTargetedEvent(input, storeMember) {
1086
1112
  if (input.kind === 'reopen' && exactRecord(input.payload, [
1087
1113
  'reason', 'override_reason',
1088
1114
  ])) {
1089
- const target = [...storeMember.journal.events].reverse().find((event) => (
1090
- event.kind === 'done' && event.task_id === input.task_id
1091
- ));
1092
- if (target === undefined) fail('STORE_INCONSISTENT', 'invalid_reopen_binding');
1115
+ const targetDigest = resolveDoneBindingDigest(storeMember, input.task_id);
1116
+ if (targetDigest === null) fail('STORE_INCONSISTENT', 'invalid_reopen_binding');
1093
1117
  return {
1094
1118
  ...input,
1095
- payload: { ...input.payload, target_done_digest: target.event_digest },
1119
+ payload: { ...input.payload, target_done_digest: targetDigest },
1096
1120
  };
1097
1121
  }
1098
1122
  if (input.kind === 'done' && exactRecord(input.payload, [