chati-dev 4.5.16 → 4.5.27

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.
Files changed (122) hide show
  1. package/README.md +8 -3
  2. package/bin/chati.js +124 -66
  3. package/framework/agents/build/dev.md +5 -5
  4. package/framework/agents/deploy/devops.md +7 -7
  5. package/framework/agents/discover/brief.md +4 -4
  6. package/framework/agents/discover/brownfield-wu.md +3 -3
  7. package/framework/agents/discover/greenfield-wu.md +3 -3
  8. package/framework/agents/plan/architect.md +2 -2
  9. package/framework/agents/plan/detail.md +4 -4
  10. package/framework/agents/plan/phases.md +2 -2
  11. package/framework/agents/plan/tasks.md +2 -2
  12. package/framework/agents/plan/ux.md +2 -2
  13. package/framework/agents/quality/qa-implementation.md +11 -10
  14. package/framework/agents/quality/qa-planning.md +3 -3
  15. package/framework/agents/quality/qa-visual.md +1 -1
  16. package/framework/config.yaml +3 -3
  17. package/framework/constitution.md +18 -18
  18. package/framework/context/protocols.md +1 -1
  19. package/framework/context/quality.md +1 -1
  20. package/framework/context/root.md +4 -4
  21. package/framework/data/entity-registry.yaml +1 -1
  22. package/framework/domains/agents/orchestrator.yaml +2 -2
  23. package/framework/domains/constitution.yaml +1 -1
  24. package/framework/hooks/advance-trigger.js +4 -6
  25. package/framework/hooks/git-push-authority.js +45 -37
  26. package/framework/hooks/mode-governance.js +149 -40
  27. package/framework/hooks/model-governance.js +13 -20
  28. package/framework/hooks/prism-engine.js +74 -92
  29. package/framework/hooks/reasoning-escalator.js +23 -40
  30. package/framework/hooks/session-digest.js +12 -13
  31. package/framework/hooks/session-reader.js +224 -0
  32. package/framework/hooks/session-writer.js +195 -0
  33. package/framework/hooks/team-quality-gate.js +34 -24
  34. package/framework/i18n/en.yaml +2 -2
  35. package/framework/i18n/es.yaml +2 -2
  36. package/framework/i18n/fr.yaml +2 -2
  37. package/framework/i18n/pt.yaml +2 -2
  38. package/framework/intelligence/context-engine.md +4 -4
  39. package/framework/intelligence/memory-layer.md +1 -1
  40. package/framework/manifest.json +129 -119
  41. package/framework/manifest.sig +1 -1
  42. package/framework/orchestrator/chati-router.js +252 -25
  43. package/framework/orchestrator/chati.md +238 -70
  44. package/framework/schemas/session.schema.json +1 -1
  45. package/framework/tasks/orchestrator-deviation.md +1 -1
  46. package/framework/tasks/orchestrator-escalate.md +1 -1
  47. package/framework/tasks/orchestrator-handoff.md +6 -6
  48. package/framework/tasks/orchestrator-health.md +5 -9
  49. package/framework/tasks/orchestrator-mode-switch.md +3 -7
  50. package/framework/tasks/orchestrator-resume.md +10 -14
  51. package/framework/tasks/orchestrator-route.md +3 -3
  52. package/framework/tasks/orchestrator-spawn-terminal.md +1 -1
  53. package/framework/tasks/orchestrator-status.md +9 -9
  54. package/framework/tasks/orchestrator-suggest-mode.md +1 -1
  55. package/framework/tasks/qa-impl-consolidate.md +2 -2
  56. package/framework/tasks/qa-impl-performance-test.md +4 -4
  57. package/framework/tasks/qa-impl-regression-check.md +4 -4
  58. package/framework/tasks/qa-impl-sast-scan.md +1 -1
  59. package/framework/tasks/qa-impl-test-execute.md +1 -1
  60. package/framework/tasks/qa-impl-verdict.md +2 -2
  61. package/framework/tasks/qa-planning-consolidate.md +3 -3
  62. package/framework/tasks/qa-planning-coverage-plan.md +2 -2
  63. package/framework/tasks/qa-planning-gate-define.md +4 -4
  64. package/framework/tasks/qa-planning-risk-matrix.md +4 -4
  65. package/framework/tasks/qa-planning-test-strategy.md +2 -2
  66. package/node_modules/@chati/browser-capability/src/index.js +12 -2
  67. package/node_modules/@chati/planning/src/index.js +24 -4
  68. package/node_modules/@chati/provider-registry/src/index.js +11 -0
  69. package/node_modules/@chati/rail/src/index.js +1967 -83
  70. package/node_modules/@chati/release-lane/README.md +12 -8
  71. package/node_modules/@chati/release-lane/package.json +1 -1
  72. package/node_modules/@chati/release-lane/src/index.js +1426 -58
  73. package/node_modules/@chati/review-council/src/index.js +63 -0
  74. package/node_modules/@chati/tracking-clickup/README.md +13 -0
  75. package/node_modules/@chati/tracking-clickup/src/index.js +690 -30
  76. package/package-artifact-manifest.json +1 -0
  77. package/package-artifact-manifest.sig +1 -0
  78. package/package.json +16 -7
  79. package/scripts/verify-real-harness-e2e.js +1581 -0
  80. package/src/config/framework-adapter.js +4 -4
  81. package/src/installer/core.js +148 -53
  82. package/src/installer/manifest.js +140 -9
  83. package/src/installer/package-artifact.js +249 -0
  84. package/src/installer/templates.js +65 -20
  85. package/src/installer-v2/catalog-client.js +531 -23
  86. package/src/installer-v2/index.js +91 -34
  87. package/src/installer-v2/installation-authority.js +327 -0
  88. package/src/installer-v2/provider-executable.js +235 -0
  89. package/src/installer-v2/wizard-installation.js +1 -1
  90. package/src/orchestrator/browser-runtime.js +44 -13
  91. package/src/orchestrator/cli.js +1545 -174
  92. package/src/orchestrator/clickup-projection.js +43 -1
  93. package/src/orchestrator/clickup-runtime.js +355 -39
  94. package/src/orchestrator/doctor.js +16 -3
  95. package/src/orchestrator/index.js +16 -0
  96. package/src/orchestrator/planning-runtime.js +20 -2
  97. package/src/orchestrator/rail-adjudication-evidence.js +234 -0
  98. package/src/orchestrator/rail-evidence-authority.js +147 -0
  99. package/src/orchestrator/rail-execution-evidence.js +45 -0
  100. package/src/orchestrator/rail-runtime.js +836 -56
  101. package/src/orchestrator/release-runtime.js +65 -6
  102. package/src/orchestrator/review-runtime.js +186 -41
  103. package/src/orchestrator/runtime-installation-v2.js +236 -20
  104. package/src/orchestrator/session-manager.js +1167 -53
  105. package/src/terminal/adapters/claude-adapter.js +3 -1
  106. package/src/terminal/adapters/codex-adapter.js +2 -0
  107. package/src/terminal/adapters/grok-adapter.js +7 -4
  108. package/src/terminal/handoff-parser.js +19 -1
  109. package/src/terminal/prompt-builder.js +9 -1
  110. package/src/terminal/provider-preflight.js +36 -3
  111. package/src/terminal/rail-execution-worktree.js +213 -0
  112. package/src/terminal/rail-prompts.js +169 -0
  113. package/src/terminal/rail-readonly-workspace.js +324 -0
  114. package/src/terminal/run-agent.js +384 -24
  115. package/src/terminal/run-parallel.js +5 -0
  116. package/src/terminal/run-rail-adjudication.js +323 -0
  117. package/src/terminal/run-rail-review.js +291 -0
  118. package/src/terminal/run-rail-rework.js +325 -0
  119. package/src/terminal/run-rail-task.js +380 -0
  120. package/src/terminal/run-team.js +5 -0
  121. package/src/terminal/spawner.js +1225 -77
  122. package/src/wizard/index.js +3 -2
@@ -1,5 +1,11 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
1
+ import {
2
+ closeSync, constants, existsSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync,
3
+ openSync, readSync, readdirSync, renameSync, unlinkSync, writeSync,
4
+ } from 'node:fs';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { hostname } from 'node:os';
7
+ import { basename, dirname, isAbsolute, join, parse, relative, resolve, sep } from 'node:path';
8
+ import { performance } from 'node:perf_hooks';
3
9
  import { ContractError, canonicalize, sha256 } from '@chati/core';
4
10
 
5
11
  const RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
@@ -7,10 +13,46 @@ const DELIVERY_STATES = new Set(['pending', 'sent_unconfirmed', 'confirmed', 're
7
13
  const RECONCILIATION_STATES = new Set(['not_due', 'pending', 'reconciled', 'drift_detected', 'blocked']);
8
14
  const OPERATIONS = new Set(['create', 'update', 'close']);
9
15
  export const HOURLY_WATCHER_INTERVAL_MS = 60 * 60 * 1000;
16
+ const OUTBOX_LOCK_TIMEOUT_MS = 5_000;
17
+ const OUTBOX_LOCK_WAIT = new Int32Array(new SharedArrayBuffer(4));
18
+ const OUTBOX_LOCK_STALE_MS = 30_000;
19
+ const OUTBOX_MAX_BYTES = 64 * 1024 * 1024;
20
+ const CLICKUP_STATE_AUTHORITY_CAPABILITIES = new WeakMap();
21
+ const SIGNED_OUTBOX_KEYS = new Set([
22
+ 'schema_version', 'state_revision', 'previous_state_digest', 'authority_context',
23
+ 'projections', 'authority_id', 'authority_signature',
24
+ ]);
25
+ const UNSIGNED_OUTBOX_KEYS = new Set(['schema_version', 'projections']);
10
26
 
11
27
  function fail(code, message, details = {}) { throw new ContractError(code, message, details); }
12
28
  function string(value, code, label) { if (typeof value !== 'string' || value.trim() === '') fail(code, `${label} must be a non-empty string`); }
13
29
  function object(value, code, label) { if (!value || typeof value !== 'object' || Array.isArray(value)) fail(code, `${label} must be an object`); }
30
+ function exactKeys(value, expected, code, label) {
31
+ object(value, code, label);
32
+ const keys = Object.keys(value);
33
+ if (keys.length !== expected.size || keys.some((key) => !expected.has(key))) {
34
+ fail(code, `${label} has an invalid closed schema`);
35
+ }
36
+ }
37
+ function assertProjectId(value) {
38
+ string(value, 'INVALID_PROJECT_ID', 'project_id');
39
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) {
40
+ fail('INVALID_PROJECT_ID', 'project_id must be one path-inert identifier component');
41
+ }
42
+ return value;
43
+ }
44
+ function projectEvolutionPath(root, id, digest) {
45
+ const safeId = assertProjectId(id);
46
+ const evolutionRoot = resolve(root);
47
+ const projectDirectory = resolve(evolutionRoot, safeId);
48
+ const projectRelative = relative(evolutionRoot, projectDirectory);
49
+ if (!projectRelative || projectRelative.startsWith(`..${sep}`) || projectRelative === '..' || isAbsolute(projectRelative)) {
50
+ fail('INVALID_PROJECT_ID', 'project_id must remain inside the configured Project Evolution directory');
51
+ }
52
+ const path = resolve(projectDirectory, `${digest}.json`);
53
+ if (dirname(path) !== projectDirectory) fail('INVALID_PROJECT_ID', 'Project Evolution path escaped its project directory');
54
+ return path;
55
+ }
14
56
  function timestamp(clock) {
15
57
  if (typeof clock !== 'function') fail('MISSING_CLOCK', 'a deterministic clock function is required');
16
58
  const result = clock();
@@ -18,18 +60,326 @@ function timestamp(clock) {
18
60
  if (typeof value !== 'string' || !RFC3339.test(value) || Number.isNaN(Date.parse(value))) fail('INVALID_CLOCK', 'clock must return a valid RFC3339 timestamp or Date');
19
61
  return value;
20
62
  }
21
- function writeAtomic(path, data) {
22
- mkdirSync(dirname(path), { recursive: true });
23
- const temporary = `${path}.${process.pid}.tmp`;
24
- writeFileSync(temporary, `${JSON.stringify(canonicalize(data), null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
25
- renameSync(temporary, path);
63
+
64
+ function outboxCore(value) {
65
+ return canonicalize({
66
+ schema_version: value.schema_version,
67
+ state_revision: value.state_revision,
68
+ previous_state_digest: value.previous_state_digest,
69
+ authority_context: value.authority_context,
70
+ projections: value.projections,
71
+ });
72
+ }
73
+
74
+ function outboxIsAuthenticated(value) {
75
+ return value && typeof value === 'object'
76
+ && Object.hasOwn(value, 'authority_id')
77
+ && Object.hasOwn(value, 'authority_signature');
78
+ }
79
+
80
+ /** Host-backed capability for authenticating mutable ClickUp outbox snapshots. */
81
+ export class ClickUpStateAuthority {
82
+ constructor({ sign, verify } = {}) {
83
+ if (typeof sign !== 'function' || typeof verify !== 'function') {
84
+ fail('INVALID_CLICKUP_STATE_AUTHORITY', 'ClickUp state authority requires sign and verify callbacks');
85
+ }
86
+ CLICKUP_STATE_AUTHORITY_CAPABILITIES.set(this, Object.freeze({ sign, verify }));
87
+ Object.freeze(this);
88
+ }
89
+ }
90
+
91
+ function assertAuthorityContext(actual, expected) {
92
+ if (!expected) return;
93
+ object(actual, 'CLICKUP_STATE_CONTEXT_MISMATCH', 'ClickUp outbox authority_context');
94
+ if (sha256(actual) !== sha256(expected)) {
95
+ fail('CLICKUP_STATE_CONTEXT_MISMATCH', 'ClickUp outbox belongs to another project or installation');
96
+ }
97
+ }
98
+
99
+ function verifyAuthenticatedOutbox(data, {
100
+ stateAuthority = null, requireAuthenticatedState = false, authorityContext = null,
101
+ } = {}) {
102
+ const authenticated = outboxIsAuthenticated(data);
103
+ if (!authenticated) {
104
+ exactKeys(data, UNSIGNED_OUTBOX_KEYS, 'OUTBOX_CORRUPT', 'unsigned ClickUp outbox');
105
+ if (requireAuthenticatedState) {
106
+ fail('UNAUTHENTICATED_CLICKUP_STATE', 'production ClickUp outbox requires host authentication');
107
+ }
108
+ return { data, digest: 'GENESIS' };
109
+ }
110
+ exactKeys(data, SIGNED_OUTBOX_KEYS, 'OUTBOX_CORRUPT', 'authenticated ClickUp outbox');
111
+ if (!Number.isSafeInteger(data.state_revision) || data.state_revision < 1
112
+ || (data.previous_state_digest !== 'GENESIS' && !/^[a-f0-9]{64}$/.test(data.previous_state_digest))) {
113
+ fail('OUTBOX_CORRUPT', 'authenticated ClickUp outbox revision chain is invalid');
114
+ }
115
+ assertAuthorityContext(data.authority_context, authorityContext);
116
+ const capability = CLICKUP_STATE_AUTHORITY_CAPABILITIES.get(stateAuthority);
117
+ if (!capability) fail('CLICKUP_STATE_AUTHORITY_REQUIRED', 'authenticated ClickUp outbox requires its host authority');
118
+ string(data.authority_id, 'OUTBOX_CORRUPT', 'ClickUp outbox authority_id');
119
+ string(data.authority_signature, 'OUTBOX_CORRUPT', 'ClickUp outbox authority_signature');
120
+ const core = outboxCore(data);
121
+ if (capability.verify(core, {
122
+ authority_id: data.authority_id,
123
+ authority_signature: data.authority_signature,
124
+ }) !== true) {
125
+ fail('INVALID_CLICKUP_STATE_SIGNATURE', 'ClickUp outbox host signature is invalid');
126
+ }
127
+ return { data, digest: sha256(core) };
26
128
  }
27
- function readOutbox(path) {
28
- if (!existsSync(path)) return { schema_version: 1, projections: [] };
129
+ function sameIdentity(left, right) {
130
+ return left?.dev === right?.dev && left?.ino === right?.ino;
131
+ }
132
+
133
+ function sameSnapshot(left, right) {
134
+ return sameIdentity(left, right)
135
+ && left?.mode === right?.mode
136
+ && left?.uid === right?.uid
137
+ && left?.gid === right?.gid
138
+ && left?.nlink === right?.nlink
139
+ && left?.size === right?.size
140
+ && left?.mtimeMs === right?.mtimeMs
141
+ && left?.ctimeMs === right?.ctimeMs;
142
+ }
143
+
144
+ function runSecurityTestHook(name, payload) {
145
+ const hook = globalThis[Symbol.for('chati.tracking-clickup.security-test-hooks')]?.[name];
146
+ if (typeof hook === 'function') hook(payload);
147
+ }
148
+
149
+ function readDescriptorBounded(descriptor, maxBytes) {
150
+ const bytes = Buffer.allocUnsafe(maxBytes + 1);
151
+ let offset = 0;
152
+ while (offset < bytes.length) {
153
+ const count = readSync(descriptor, bytes, offset, bytes.length - offset, null);
154
+ if (count === 0) break;
155
+ offset += count;
156
+ }
157
+ return bytes.subarray(0, offset);
158
+ }
159
+
160
+ function openPinnedTrackingDirectory(path, { create = false } = {}) {
161
+ if (process.platform !== 'linux') fail('UNSUPPORTED_TRACKING_STORAGE_PLATFORM', 'tracking authority storage requires Linux descriptor-relative filesystem operations');
162
+ const absolute = resolve(path);
163
+ const root = parse(absolute).root;
164
+ let descriptor = openSync(root, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
165
+ try {
166
+ for (const segment of absolute.slice(root.length).split(sep).filter(Boolean)) {
167
+ const childPath = join(`/proc/self/fd/${descriptor}`, segment);
168
+ let child;
169
+ try { child = openSync(childPath, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0)); }
170
+ catch (error) {
171
+ if (error?.code !== 'ENOENT' || !create) {
172
+ if (['ELOOP', 'ENOTDIR'].includes(error?.code)) fail('UNSAFE_OUTBOX_DIRECTORY', 'tracking directory chain must contain only real directories');
173
+ throw error;
174
+ }
175
+ try { mkdirSync(childPath, { mode: 0o700 }); }
176
+ catch (mkdirError) { if (mkdirError?.code !== 'EEXIST') throw mkdirError; }
177
+ child = openSync(childPath, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
178
+ }
179
+ const stat = fstatSync(child);
180
+ if (!stat.isDirectory()) {
181
+ closeSync(child);
182
+ fail('UNSAFE_OUTBOX_DIRECTORY', 'tracking directory chain must contain only real directories');
183
+ }
184
+ closeSync(descriptor);
185
+ descriptor = child;
186
+ }
187
+ return Object.freeze({ absolute, descriptor, identity: fstatSync(descriptor), reference: `/proc/self/fd/${descriptor}` });
188
+ } catch (error) {
189
+ closeSync(descriptor);
190
+ throw error;
191
+ }
192
+ }
193
+
194
+ function trackingEntry(directory, name) {
195
+ if (basename(name) !== name || name === '.' || name === '..') fail('UNSAFE_OUTBOX_FILE', 'tracking entry must be one path segment');
196
+ return join(directory.reference, name);
197
+ }
198
+
199
+ function assertPinnedTrackingDirectory(directory) {
200
+ const pinned = fstatSync(directory.descriptor);
201
+ if (!pinned.isDirectory() || !sameIdentity(pinned, directory.identity)) fail('UNSAFE_OUTBOX_DIRECTORY', 'pinned tracking directory changed');
202
+ let current;
203
+ try { current = openPinnedTrackingDirectory(directory.absolute); }
204
+ catch (error) {
205
+ if (error?.code === 'UNSAFE_OUTBOX_DIRECTORY') throw error;
206
+ fail('UNSAFE_OUTBOX_DIRECTORY', 'tracking directory chain changed during storage');
207
+ }
208
+ try {
209
+ if (!sameIdentity(current.identity, directory.identity)) fail('UNSAFE_OUTBOX_DIRECTORY', 'tracking directory chain changed during storage');
210
+ } finally { closeSync(current.descriptor); }
211
+ }
212
+
213
+ function readRegular(path, { allowMissing = false, maxBytes = OUTBOX_MAX_BYTES, directory = null } = {}) {
214
+ let activeDirectory = directory;
215
+ const ownedDirectory = activeDirectory === null;
216
+ if (ownedDirectory) {
217
+ try { activeDirectory = openPinnedTrackingDirectory(dirname(path)); }
218
+ catch (error) { if (allowMissing && error?.code === 'ENOENT') return null; throw error; }
219
+ }
220
+ const entry = trackingEntry(activeDirectory, basename(path));
221
+ let descriptor;
222
+ try {
223
+ descriptor = openSync(entry, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
224
+ } catch (error) {
225
+ if (ownedDirectory) closeSync(activeDirectory.descriptor);
226
+ if (allowMissing && error?.code === 'ENOENT') return null;
227
+ if (['ELOOP', 'ENXIO', 'EISDIR'].includes(error?.code)) fail('UNSAFE_OUTBOX_FILE', 'tracking state must not be a symlink, FIFO or directory');
228
+ throw error;
229
+ }
230
+ try {
231
+ const before = fstatSync(descriptor);
232
+ if (!before.isFile() || before.nlink !== 1) fail('UNSAFE_OUTBOX_FILE', 'tracking state must be a regular single-link file');
233
+ if (before.size > maxBytes) fail('OUTBOX_SIZE_LIMIT', `tracking state exceeds the ${maxBytes}-byte safety limit`);
234
+ runSecurityTestHook('afterTrackingStateStat', { path, descriptor });
235
+ const content = readDescriptorBounded(descriptor, maxBytes);
236
+ if (content.length > maxBytes) fail('OUTBOX_SIZE_LIMIT', `tracking state exceeds the ${maxBytes}-byte safety limit`);
237
+ const after = fstatSync(descriptor);
238
+ if (!after.isFile() || after.nlink !== 1 || !sameSnapshot(before, after)) {
239
+ fail('OUTBOX_STORAGE_CONFLICT', 'tracking state changed while it was being read');
240
+ }
241
+ assertPinnedTrackingDirectory(activeDirectory);
242
+ return { content: content.toString('utf8'), stat: after };
243
+ } finally {
244
+ closeSync(descriptor);
245
+ if (ownedDirectory) closeSync(activeDirectory.descriptor);
246
+ }
247
+ }
248
+
249
+ function writeAtomic(path, data, maxBytes = OUTBOX_MAX_BYTES, { directory = null } = {}) {
250
+ const content = `${JSON.stringify(canonicalize(data), null, 2)}\n`;
251
+ const bytes = Buffer.from(content);
252
+ if (bytes.length > maxBytes) fail('OUTBOX_SIZE_LIMIT', `tracking state exceeds the ${maxBytes}-byte safety limit`);
253
+ const activeDirectory = directory ?? openPinnedTrackingDirectory(dirname(path), { create: true });
254
+ const ownedDirectory = directory === null;
255
+ const temporary = trackingEntry(activeDirectory, `${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
256
+ const destination = trackingEntry(activeDirectory, basename(path));
257
+ let temporaryDescriptor;
258
+ try {
259
+ const existing = readRegular(path, { allowMissing: true, maxBytes, directory: activeDirectory });
260
+ temporaryDescriptor = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
261
+ let offset = 0;
262
+ while (offset < bytes.length) offset += writeSync(temporaryDescriptor, bytes, offset, bytes.length - offset);
263
+ fsyncSync(temporaryDescriptor);
264
+ closeSync(temporaryDescriptor);
265
+ temporaryDescriptor = undefined;
266
+ assertPinnedTrackingDirectory(activeDirectory);
267
+ let current = null;
268
+ try { current = lstatSync(destination); } catch (error) { if (error?.code !== 'ENOENT') throw error; }
269
+ if ((existing === null && current !== null)
270
+ || (existing !== null && (!current || current.isSymbolicLink() || !current.isFile()
271
+ || current.nlink !== 1 || !sameIdentity(current, existing.stat)))) {
272
+ fail('OUTBOX_STORAGE_CONFLICT', 'tracking state changed during atomic persistence');
273
+ }
274
+ renameSync(temporary, destination);
275
+ fsyncSync(activeDirectory.descriptor);
276
+ assertPinnedTrackingDirectory(activeDirectory);
277
+ } finally {
278
+ if (temporaryDescriptor !== undefined) closeSync(temporaryDescriptor);
279
+ try { unlinkSync(temporary); } catch { /* leave an unverified temporary untouched */ }
280
+ if (ownedDirectory) closeSync(activeDirectory.descriptor);
281
+ }
282
+ }
283
+ export function readClickUpOutbox(path, {
284
+ directory = null, state_authority = null, require_authenticated_state = false,
285
+ authority_context = null, allow_missing_authenticated_state = false,
286
+ } = {}) {
287
+ let stored;
288
+ try { stored = readRegular(path, { allowMissing: true, directory }); }
289
+ catch (error) {
290
+ if (error?.code !== 'ENOENT') throw error;
291
+ stored = null;
292
+ }
293
+ if (stored === null) {
294
+ if (require_authenticated_state && !allow_missing_authenticated_state) {
295
+ fail('MISSING_CLICKUP_STATE', 'authenticated ClickUp outbox is missing');
296
+ }
297
+ return authority_context
298
+ ? { schema_version: 1, state_revision: 0, previous_state_digest: 'GENESIS', authority_context: canonicalize(authority_context), projections: [] }
299
+ : { schema_version: 1, projections: [] };
300
+ }
29
301
  let data;
30
- try { data = JSON.parse(readFileSync(path, 'utf8')); } catch { fail('OUTBOX_CORRUPT', 'outbox JSON is malformed'); }
302
+ try { data = JSON.parse(stored.content); } catch { fail('OUTBOX_CORRUPT', 'outbox JSON is malformed'); }
31
303
  if (data?.schema_version !== 1 || !Array.isArray(data.projections)) fail('OUTBOX_CORRUPT', 'outbox schema is invalid');
32
- return data;
304
+ return verifyAuthenticatedOutbox(data, {
305
+ stateAuthority: state_authority,
306
+ requireAuthenticatedState: require_authenticated_state,
307
+ authorityContext: authority_context,
308
+ }).data;
309
+ }
310
+
311
+ export function readClickUpDocument(path, { allowMissing = false, maxBytes = OUTBOX_MAX_BYTES } = {}) {
312
+ const stored = readRegular(path, { allowMissing, maxBytes });
313
+ if (stored === null) return null;
314
+ try { return JSON.parse(stored.content); }
315
+ catch { return fail('OUTBOX_CORRUPT', 'tracking JSON document is malformed'); }
316
+ }
317
+
318
+ export function persistImmutableClickUpDocument(path, data, { maxBytes = OUTBOX_MAX_BYTES } = {}) {
319
+ const content = `${JSON.stringify(canonicalize(data), null, 2)}\n`;
320
+ const bytes = Buffer.from(content);
321
+ if (bytes.length > maxBytes) fail('OUTBOX_SIZE_LIMIT', `tracking document exceeds the ${maxBytes}-byte safety limit`);
322
+ const directory = openPinnedTrackingDirectory(dirname(path), { create: true });
323
+ const temporary = trackingEntry(directory, `${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
324
+ const destination = trackingEntry(directory, basename(path));
325
+ let temporaryDescriptor;
326
+ try {
327
+ const existing = readRegular(path, { allowMissing: true, maxBytes, directory });
328
+ if (existing !== null) {
329
+ if (existing.content !== content) fail('OUTBOX_STORAGE_CONFLICT', 'immutable tracking document already contains different material');
330
+ return Object.freeze({ written: false, path });
331
+ }
332
+ runSecurityTestHook('beforeTrackingDocumentTempOpen', { path, temporary, directory: directory.absolute });
333
+ temporaryDescriptor = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
334
+ runSecurityTestHook('afterTrackingDocumentTempOpen', { path, temporary, directory: directory.absolute });
335
+ let offset = 0;
336
+ while (offset < bytes.length) offset += writeSync(temporaryDescriptor, bytes, offset, bytes.length - offset);
337
+ fsyncSync(temporaryDescriptor);
338
+ closeSync(temporaryDescriptor);
339
+ temporaryDescriptor = undefined;
340
+ assertPinnedTrackingDirectory(directory);
341
+ try { linkSync(temporary, destination); }
342
+ catch (error) {
343
+ if (error?.code !== 'EEXIST') throw error;
344
+ const raced = readRegular(path, { maxBytes, directory });
345
+ if (raced.content !== content) fail('OUTBOX_STORAGE_CONFLICT', 'immutable tracking document raced with different material');
346
+ return Object.freeze({ written: false, path });
347
+ }
348
+ fsyncSync(directory.descriptor);
349
+ assertPinnedTrackingDirectory(directory);
350
+ return Object.freeze({ written: true, path });
351
+ } finally {
352
+ if (temporaryDescriptor !== undefined) closeSync(temporaryDescriptor);
353
+ try { unlinkSync(temporary); } catch { /* leave an unverified temporary untouched */ }
354
+ closeSync(directory.descriptor);
355
+ }
356
+ }
357
+
358
+ export function listClickUpDocumentPaths(directoryPath) {
359
+ const directory = openPinnedTrackingDirectory(directoryPath);
360
+ try {
361
+ const names = readdirSync(directory.reference).filter((name) => /^[A-Za-z0-9._-]+\.json$/.test(name)).sort();
362
+ assertPinnedTrackingDirectory(directory);
363
+ return names.map((name) => join(directory.absolute, name));
364
+ } finally { closeSync(directory.descriptor); }
365
+ }
366
+
367
+ export function removeImmutableClickUpDocument(path, { maxBytes = OUTBOX_MAX_BYTES } = {}) {
368
+ const directory = openPinnedTrackingDirectory(dirname(path));
369
+ const destination = trackingEntry(directory, basename(path));
370
+ try {
371
+ const existing = readRegular(path, { allowMissing: true, maxBytes, directory });
372
+ if (existing === null) return false;
373
+ const current = lstatSync(destination);
374
+ if (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1 || !sameIdentity(current, existing.stat)) {
375
+ fail('OUTBOX_STORAGE_CONFLICT', 'tracking document changed before removal');
376
+ }
377
+ assertPinnedTrackingDirectory(directory);
378
+ unlinkSync(destination);
379
+ fsyncSync(directory.descriptor);
380
+ assertPinnedTrackingDirectory(directory);
381
+ return true;
382
+ } finally { closeSync(directory.descriptor); }
33
383
  }
34
384
  function closureMutation(payload) {
35
385
  const status = typeof payload.status === 'string' ? payload.status.trim().toLowerCase() : '';
@@ -37,6 +387,69 @@ function closureMutation(payload) {
37
387
  || Object.hasOwn(payload, 'end_date') || Object.hasOwn(payload, 'endDate');
38
388
  }
39
389
 
390
+ function processIsAlive(pid) {
391
+ if (!Number.isInteger(pid) || pid <= 0) return false;
392
+ try { process.kill(pid, 0); return true; } catch (error) { return error?.code === 'EPERM'; }
393
+ }
394
+
395
+ function readOutboxLock(path) {
396
+ let descriptor;
397
+ try {
398
+ descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
399
+ const before = fstatSync(descriptor);
400
+ if (!before.isFile() || before.nlink !== 1 || before.size > 16 * 1024) return null;
401
+ const content = readDescriptorBounded(descriptor, 16 * 1024);
402
+ const after = fstatSync(descriptor);
403
+ if (content.length > 16 * 1024 || !after.isFile() || after.nlink !== 1 || !sameSnapshot(before, after)) return null;
404
+ const owner = JSON.parse(content.toString('utf8'));
405
+ return owner && typeof owner === 'object' && typeof owner.token === 'string' ? owner : null;
406
+ } catch { return null; }
407
+ finally { if (descriptor !== undefined) closeSync(descriptor); }
408
+ }
409
+
410
+
411
+ function recoverStaleOutboxLock(path) {
412
+ let stat;
413
+ try { stat = lstatSync(path); } catch (error) { return error?.code === 'ENOENT'; }
414
+ if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) return false;
415
+ const owner = readOutboxLock(path);
416
+ if (owner?.hostname === hostname() && processIsAlive(owner.pid)) return false;
417
+ // A process on another host cannot be proven dead from this host. Keep the
418
+ // lock until an operator explicitly reconciles it.
419
+ if (owner?.hostname && owner.hostname !== hostname()) return false;
420
+ if (!owner && Date.now() - stat.mtimeMs < OUTBOX_LOCK_STALE_MS) return false;
421
+ const recoveryPath = `${path}.recovery`;
422
+ let recoveryFd;
423
+ try {
424
+ recoveryFd = openSync(recoveryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
425
+ const bytes = Buffer.from(JSON.stringify({ pid: process.pid, hostname: hostname(), token: randomUUID() }));
426
+ writeSync(recoveryFd, bytes, 0, bytes.length);
427
+ fsyncSync(recoveryFd);
428
+ } catch {
429
+ if (recoveryFd !== undefined) closeSync(recoveryFd);
430
+ return false;
431
+ }
432
+ const quarantine = `${path}.stale.${process.pid}.${randomUUID()}`;
433
+ try {
434
+ const current = lstatSync(path);
435
+ const currentOwner = readOutboxLock(path);
436
+ if (!sameIdentity(stat, current) || currentOwner?.token !== owner?.token) return false;
437
+ renameSync(path, quarantine);
438
+ const quarantined = lstatSync(quarantine);
439
+ const quarantinedOwner = readOutboxLock(quarantine);
440
+ if (!sameIdentity(stat, quarantined) || quarantinedOwner?.token !== owner?.token) {
441
+ try { if (!existsSync(path)) renameSync(quarantine, path); } catch { /* preserve quarantine fail-closed */ }
442
+ return false;
443
+ }
444
+ unlinkSync(quarantine);
445
+ return true;
446
+ } catch (error) { return error?.code === 'ENOENT'; }
447
+ finally {
448
+ if (recoveryFd !== undefined) closeSync(recoveryFd);
449
+ try { unlinkSync(recoveryPath); } catch { /* fail closed if recovery ownership changed */ }
450
+ }
451
+ }
452
+
40
453
  /**
41
454
  * C-07 local projection authority. It intentionally has no ClickUp SDK,
42
455
  * credentials, network client, timer or knowledge-store integration. A caller
@@ -53,8 +466,19 @@ export class TrackingClickUp {
53
466
  #preflight;
54
467
  #watcherIntervalMs;
55
468
  #state;
469
+ #stateAuthority;
470
+ #requireAuthenticatedState;
471
+ #authorityContext;
472
+ #stateDigest = 'GENESIS';
473
+ #lockDepth = 0;
474
+ #activeDirectory;
56
475
 
57
- constructor({ outbox_path, project_evolution_dir, clock, transport = null, verify_completion, verify_reference, resolve_reference, preflight = null, watcher_interval_ms = HOURLY_WATCHER_INTERVAL_MS } = {}) {
476
+ constructor({
477
+ outbox_path, project_evolution_dir, clock, transport = null,
478
+ verify_completion, verify_reference, resolve_reference, preflight = null,
479
+ watcher_interval_ms = HOURLY_WATCHER_INTERVAL_MS,
480
+ state_authority = null, require_authenticated_state = false, authority_context = null,
481
+ } = {}) {
58
482
  string(outbox_path, 'INVALID_OUTBOX_PATH', 'outbox_path');
59
483
  string(project_evolution_dir, 'INVALID_EVOLUTION_DIR', 'project_evolution_dir');
60
484
  if (transport !== null && (!transport || typeof transport.send !== 'function')) fail('INVALID_TRANSPORT', 'transport.send must be a function');
@@ -72,7 +496,23 @@ export class TrackingClickUp {
72
496
  this.#resolveReference = resolve_reference;
73
497
  this.#preflight = preflight;
74
498
  this.#watcherIntervalMs = watcher_interval_ms;
75
- this.#state = readOutbox(outbox_path);
499
+ if (state_authority !== null && !CLICKUP_STATE_AUTHORITY_CAPABILITIES.has(state_authority)) {
500
+ fail('INVALID_CLICKUP_STATE_AUTHORITY', 'state_authority must be a minted ClickUpStateAuthority');
501
+ }
502
+ if (authority_context !== null) {
503
+ object(authority_context, 'INVALID_CLICKUP_STATE_CONTEXT', 'authority_context');
504
+ for (const field of ['project_id', 'installation_artifact_id']) {
505
+ string(authority_context[field], 'INVALID_CLICKUP_STATE_CONTEXT', `authority_context.${field}`);
506
+ }
507
+ assertProjectId(authority_context.project_id);
508
+ this.#authorityContext = canonicalize(authority_context);
509
+ } else this.#authorityContext = null;
510
+ this.#stateAuthority = state_authority;
511
+ this.#requireAuthenticatedState = require_authenticated_state === true;
512
+ if (this.#requireAuthenticatedState && (!this.#stateAuthority || !this.#authorityContext)) {
513
+ fail('CLICKUP_STATE_AUTHORITY_REQUIRED', 'production ClickUp state requires a host authority and project context');
514
+ }
515
+ this.#loadState({ allowMissing: true });
76
516
  // Project Evolution is a derivative, never the only copy of evidence.
77
517
  // Rebuild it after an interrupted outbox/evolution write sequence.
78
518
  for (const projectId of new Set(this.#state.projections.map((item) => item.project_id))) this.#evolve(projectId);
@@ -82,7 +522,124 @@ export class TrackingClickUp {
82
522
  get outboxPath() { return this.#outboxPath; }
83
523
  get watcherConfig() { return Object.freeze({ interval_ms: this.#watcherIntervalMs, mode: 'external-one-shot-scheduler' }); }
84
524
 
85
- #persist() { writeAtomic(this.#outboxPath, this.#state); }
525
+ #validateLoadedState() {
526
+ const ids = new Set();
527
+ const idempotencyKeys = new Set();
528
+ let priorCreatedAt = Number.NEGATIVE_INFINITY;
529
+ for (const projection of this.#state.projections) {
530
+ validateProjection(projection);
531
+ if (this.#authorityContext && projection.project_id !== this.#authorityContext.project_id) {
532
+ fail('CLICKUP_STATE_CONTEXT_MISMATCH', 'ClickUp outbox contains a projection from another project');
533
+ }
534
+ if (projection.payload_hash !== sha256(projection.payload)) {
535
+ fail('OUTBOX_SEMANTIC_CORRUPTION', 'ClickUp projection payload_hash does not match its payload');
536
+ }
537
+ if (ids.has(projection.projection_id) || idempotencyKeys.has(projection.idempotency_key)) {
538
+ fail('OUTBOX_SEMANTIC_CORRUPTION', 'ClickUp outbox contains duplicate projection identities');
539
+ }
540
+ ids.add(projection.projection_id);
541
+ idempotencyKeys.add(projection.idempotency_key);
542
+ const createdAt = Date.parse(projection.created_at);
543
+ if (!Number.isFinite(createdAt) || createdAt < priorCreatedAt) {
544
+ fail('OUTBOX_SEMANTIC_CORRUPTION', 'ClickUp outbox projection order is not chronological');
545
+ }
546
+ priorCreatedAt = createdAt;
547
+ }
548
+ }
549
+
550
+ #loadState({ directory = null, allowMissing = false } = {}) {
551
+ this.#state = readClickUpOutbox(this.#outboxPath, {
552
+ directory,
553
+ state_authority: this.#stateAuthority,
554
+ require_authenticated_state: this.#requireAuthenticatedState,
555
+ authority_context: this.#authorityContext,
556
+ allow_missing_authenticated_state: allowMissing,
557
+ });
558
+ this.#stateDigest = outboxIsAuthenticated(this.#state) ? sha256(outboxCore(this.#state)) : 'GENESIS';
559
+ this.#validateLoadedState();
560
+ }
561
+
562
+ #exclusive(action) {
563
+ if (this.#lockDepth > 0) return action();
564
+ const directory = openPinnedTrackingDirectory(dirname(this.#outboxPath), { create: true });
565
+ const path = trackingEntry(directory, `${basename(this.#outboxPath)}.lock`);
566
+ let descriptor;
567
+ let token;
568
+ try {
569
+ const lockDeadline = performance.now() + OUTBOX_LOCK_TIMEOUT_MS;
570
+ while (performance.now() < lockDeadline) {
571
+ let candidate;
572
+ try {
573
+ candidate = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
574
+ } catch (error) {
575
+ if (error?.code !== 'EEXIST') throw error;
576
+ if (recoverStaleOutboxLock(path)) continue;
577
+ Atomics.wait(OUTBOX_LOCK_WAIT, 0, 0, 4);
578
+ continue;
579
+ }
580
+ token = randomUUID();
581
+ try {
582
+ const bytes = Buffer.from(JSON.stringify({ schema_version: 1, pid: process.pid, hostname: hostname(), token, acquired_at: new Date().toISOString() }));
583
+ writeSync(candidate, bytes, 0, bytes.length);
584
+ fsyncSync(candidate);
585
+ fsyncSync(directory.descriptor);
586
+ descriptor = candidate;
587
+ break;
588
+ } catch (error) {
589
+ closeSync(candidate);
590
+ try { unlinkSync(path); } catch { /* acquisition failed closed */ }
591
+ throw error;
592
+ }
593
+ }
594
+ if (descriptor === undefined) fail('OUTBOX_LOCK_BUSY', 'outbox mutation lock is already held');
595
+ this.#lockDepth = 1;
596
+ this.#activeDirectory = directory;
597
+ assertPinnedTrackingDirectory(directory);
598
+ this.#loadState({ directory, allowMissing: true });
599
+ return action();
600
+ } finally {
601
+ this.#lockDepth = 0;
602
+ this.#activeDirectory = undefined;
603
+ try {
604
+ if (descriptor !== undefined) {
605
+ closeSync(descriptor);
606
+ assertPinnedTrackingDirectory(directory);
607
+ if (readOutboxLock(path)?.token === token) { unlinkSync(path); fsyncSync(directory.descriptor); }
608
+ }
609
+ } finally {
610
+ closeSync(directory.descriptor);
611
+ }
612
+ }
613
+ }
614
+
615
+ #persist() {
616
+ if (this.#stateAuthority) {
617
+ const capability = CLICKUP_STATE_AUTHORITY_CAPABILITIES.get(this.#stateAuthority);
618
+ const core = canonicalize({
619
+ schema_version: 1,
620
+ state_revision: (this.#state.state_revision ?? 0) + 1,
621
+ previous_state_digest: this.#stateDigest,
622
+ authority_context: this.#authorityContext,
623
+ projections: this.#state.projections,
624
+ });
625
+ const receipt = capability.sign(core);
626
+ object(receipt, 'INVALID_CLICKUP_STATE_AUTHORITY', 'ClickUp state authority receipt');
627
+ string(receipt.authority_id, 'INVALID_CLICKUP_STATE_AUTHORITY', 'ClickUp state authority receipt.authority_id');
628
+ string(receipt.authority_signature, 'INVALID_CLICKUP_STATE_AUTHORITY', 'ClickUp state authority receipt.authority_signature');
629
+ if (capability.verify(core, receipt) !== true) {
630
+ fail('INVALID_CLICKUP_STATE_AUTHORITY', 'ClickUp state authority did not verify its own receipt');
631
+ }
632
+ this.#state = {
633
+ ...core,
634
+ projections: this.#state.projections,
635
+ ...canonicalize(receipt),
636
+ };
637
+ this.#stateDigest = sha256(core);
638
+ } else {
639
+ this.#state = { schema_version: 1, projections: this.#state.projections };
640
+ }
641
+ writeAtomic(this.#outboxPath, this.#state, OUTBOX_MAX_BYTES, { directory: this.#activeDirectory });
642
+ }
86
643
  #find(projectionId) {
87
644
  string(projectionId, 'INVALID_PROJECTION_ID', 'projection_id');
88
645
  const projection = this.#state.projections.find((item) => item.projection_id === projectionId);
@@ -90,13 +647,18 @@ export class TrackingClickUp {
90
647
  return projection;
91
648
  }
92
649
  #evolve(projectId) {
93
- const relevant = this.#state.projections.filter((item) => item.project_id === projectId);
650
+ const safeProjectId = assertProjectId(projectId);
651
+ const relevant = this.#state.projections.filter((item) => item.project_id === safeProjectId);
94
652
  const latestByTask = new Map();
95
653
  for (const item of relevant) latestByTask.set(item.task_id, item);
96
- const sourceProjections = relevant.map(({ project_evolution_ref, ...source }) => source);
654
+ const sourceProjections = relevant.map((item) => {
655
+ const source = { ...item };
656
+ delete source.project_evolution_ref;
657
+ return source;
658
+ });
97
659
  const snapshot = canonicalize({
98
660
  schema_version: 1,
99
- project_id: projectId,
661
+ project_id: safeProjectId,
100
662
  current_phase: [...latestByTask.values()].at(-1)?.handoff_view.phase ?? 'unknown',
101
663
  tasks: [...latestByTask.values()].map((item) => ({ task_id: item.task_id, attempt_id: item.attempt_id, phase: item.handoff_view.phase, canonical_state: item.journal_view.task_state, blockers: item.journal_view.blockers, delivery_state: item.delivery_state, reconciliation_state: item.reconciliation_state })),
102
664
  decisions: [...new Map(relevant.flatMap((item) => item.decisions.map((decision) => [decision.decision_ref, decision]))).values()],
@@ -108,9 +670,9 @@ export class TrackingClickUp {
108
670
  source_outbox_hash: sha256({ schema_version: 1, projections: sourceProjections }),
109
671
  });
110
672
  const digest = sha256(snapshot);
111
- const path = join(this.#evolutionDir, projectId, `${digest}.json`);
673
+ const path = projectEvolutionPath(this.#evolutionDir, safeProjectId, digest);
112
674
  writeAtomic(path, snapshot);
113
- return Object.freeze({ project_evolution_ref: `project-evolution://${projectId}/${digest}`, path, digest });
675
+ return Object.freeze({ project_evolution_ref: `project-evolution://${safeProjectId}/${digest}`, path, digest });
114
676
  }
115
677
  #setEvolution(projection) {
116
678
  const evolution = this.#evolve(projection.project_id);
@@ -125,10 +687,15 @@ export class TrackingClickUp {
125
687
  }
126
688
 
127
689
  enqueue(input = {}) {
690
+ return this.#exclusive(() => {
128
691
  object(input, 'INVALID_PROJECTION', 'projection');
129
692
  const { projection_id, idempotency_key, canonical_event_id, task_id, clickup_ref, operation, payload, project_id, handoff_ref, journal_ref } = input;
130
693
  const attempt_id = input.attempt_id ?? `project:${project_id}:no-attempt`;
131
694
  for (const [name, value] of Object.entries({ projection_id, idempotency_key, canonical_event_id, task_id, clickup_ref, project_id })) string(value, 'INVALID_PROJECTION', name);
695
+ assertProjectId(project_id);
696
+ if (this.#authorityContext && project_id !== this.#authorityContext.project_id) {
697
+ fail('CLICKUP_STATE_CONTEXT_MISMATCH', 'projection belongs to another project than the authenticated outbox');
698
+ }
132
699
  string(attempt_id, 'INVALID_PROJECTION', 'attempt_id');
133
700
  if (!OPERATIONS.has(operation)) fail('INVALID_PROJECTION_OPERATION', 'operation must be create, update, or close');
134
701
  object(payload, 'INVALID_PROJECTION', 'payload');
@@ -170,17 +737,25 @@ export class TrackingClickUp {
170
737
  this.#setEvolution(projection);
171
738
  this.#persist();
172
739
  return Object.freeze(canonicalize(projection));
740
+ });
173
741
  }
174
742
 
175
743
  deliver({ projection_id } = {}) {
744
+ return this.#exclusive(() => {
176
745
  const projection = this.#find(projection_id);
177
746
  if (!this.#transport) fail('TRANSPORT_NOT_CONFIGURED', 'no authorized projection transport is configured');
178
747
  if (projection.delivery_state === 'confirmed') return Object.freeze(canonicalize(projection));
179
748
  if (projection.delivery_state === 'permanent_failure') fail('PERMANENT_FAILURE', 'permanently failed projection cannot be retried');
180
- if (projection.delivery_state === 'sent_unconfirmed') fail('UNCONFIRMED_DELIVERY_REQUIRES_LOOKUP', 'unconfirmed delivery must be resolved by idempotency lookup before resend');
749
+ if (['sent_unconfirmed', 'retryable_failure'].includes(projection.delivery_state)) fail('UNCONFIRMED_DELIVERY_REQUIRES_LOOKUP', 'uncertain delivery must be resolved by idempotency lookup before resend');
181
750
  this.#preflightTransport(projection);
182
751
  projection.attempts += 1;
183
752
  projection.last_attempt_at = timestamp(this.#clock);
753
+ // Persist uncertainty before crossing the external side-effect boundary.
754
+ // A lost response must be reconciled by lookup, never blindly resent.
755
+ projection.delivery_state = 'sent_unconfirmed';
756
+ delete projection.failure_code;
757
+ this.#setEvolution(projection);
758
+ this.#persist();
184
759
  try {
185
760
  const response = this.#transport.send(Object.freeze(canonicalize(projection)));
186
761
  object(response, 'INVALID_TRANSPORT_RECEIPT', 'transport response');
@@ -190,20 +765,52 @@ export class TrackingClickUp {
190
765
  projection.delivery_state = 'confirmed';
191
766
  projection.receipt_ref = response.receipt_ref;
192
767
  projection.reconciliation_state = 'pending';
193
- } else projection.delivery_state = 'sent_unconfirmed';
768
+ }
194
769
  } catch (error) {
195
- projection.delivery_state = error?.permanent === true ? 'permanent_failure' : 'retryable_failure';
770
+ if (error?.permanent === true) projection.delivery_state = 'permanent_failure';
196
771
  projection.failure_code = typeof error?.code === 'string' ? error.code : 'TRANSPORT_FAILURE';
197
772
  }
198
773
  this.#setEvolution(projection);
199
774
  this.#persist();
200
775
  return Object.freeze(canonicalize(projection));
776
+ });
777
+ }
778
+
779
+ /** Persists uncertainty before a separately authorized MCP caller sends. */
780
+ prepareExternalDelivery({ projection_id } = {}) {
781
+ return this.#exclusive(() => {
782
+ const projection = this.#find(projection_id);
783
+ if (projection.delivery_state === 'confirmed') return Object.freeze(canonicalize(projection));
784
+ if (projection.delivery_state === 'permanent_failure') fail('PERMANENT_FAILURE', 'permanently failed projection cannot be retried');
785
+ if (['sent_unconfirmed', 'retryable_failure'].includes(projection.delivery_state)) {
786
+ fail('UNCONFIRMED_DELIVERY_REQUIRES_LOOKUP', 'uncertain delivery must be resolved by idempotency lookup before resend');
787
+ }
788
+ validateProjection(projection);
789
+ projection.attempts += 1;
790
+ projection.last_attempt_at = timestamp(this.#clock);
791
+ projection.delivery_state = 'sent_unconfirmed';
792
+ delete projection.failure_code;
793
+ this.#setEvolution(projection);
794
+ this.#persist();
795
+ return Object.freeze(canonicalize(projection));
796
+ });
201
797
  }
202
798
 
203
799
  /** Records a receipt produced by an independently authorized MCP boundary. */
204
- confirmExternalDelivery({ projection_id, receipt_ref } = {}) {
800
+ confirmExternalDelivery({
801
+ projection_id, receipt_ref, expected_delivery_attempt, expected_delivery_attempted_at,
802
+ } = {}) {
803
+ return this.#exclusive(() => {
205
804
  const projection = this.#find(projection_id);
206
805
  string(receipt_ref, 'INVALID_TRANSPORT_RECEIPT', 'receipt_ref');
806
+ if (!Number.isInteger(expected_delivery_attempt) || expected_delivery_attempt < 1) {
807
+ fail('INVALID_TRANSPORT_RECEIPT', 'expected_delivery_attempt must be a positive integer');
808
+ }
809
+ string(expected_delivery_attempted_at, 'INVALID_TRANSPORT_RECEIPT', 'expected_delivery_attempted_at');
810
+ if (projection.attempts !== expected_delivery_attempt
811
+ || projection.last_attempt_at !== expected_delivery_attempted_at) {
812
+ fail('STALE_EXTERNAL_DELIVERY_RESULT', 'external delivery receipt does not match the current delivery attempt');
813
+ }
207
814
  if (this.#verifyReference(receipt_ref, 'receipt') !== true) {
208
815
  fail('UNVERIFIED_RECEIPT', 'external delivery receipt must be an immutable verified reference');
209
816
  }
@@ -212,17 +819,59 @@ export class TrackingClickUp {
212
819
  if (projection.receipt_ref !== receipt_ref) fail('RECEIPT_CONFLICT', 'projection is already confirmed by a different receipt');
213
820
  return Object.freeze(canonicalize(projection));
214
821
  }
822
+ if (!['sent_unconfirmed', 'retryable_failure'].includes(projection.delivery_state)) {
823
+ fail('DELIVERY_INTENT_REQUIRED', 'external delivery must be marked sent_unconfirmed before it can be confirmed');
824
+ }
215
825
  projection.delivery_state = 'confirmed';
216
826
  projection.receipt_ref = receipt_ref;
217
827
  projection.reconciliation_state = 'pending';
218
- projection.last_attempt_at = timestamp(this.#clock);
219
- projection.attempts += 1;
828
+ projection.last_attempt_at ??= timestamp(this.#clock);
220
829
  this.#setEvolution(projection);
221
830
  this.#persist();
222
831
  return Object.freeze(canonicalize(projection));
832
+ });
833
+ }
834
+
835
+ /** Applies an immutable lookup result supplied by an authorized MCP caller. */
836
+ resolveExternalLookup({
837
+ projection_id, result, evidence_ref, expected_delivery_attempt, expected_delivery_attempted_at,
838
+ } = {}) {
839
+ return this.#exclusive(() => {
840
+ const projection = this.#find(projection_id);
841
+ if (!Number.isInteger(expected_delivery_attempt) || expected_delivery_attempt < 1) {
842
+ fail('INVALID_LOOKUP_RESULT', 'expected_delivery_attempt must be a positive integer');
843
+ }
844
+ string(expected_delivery_attempted_at, 'INVALID_LOOKUP_RESULT', 'expected_delivery_attempted_at');
845
+ if (projection.attempts !== expected_delivery_attempt
846
+ || projection.last_attempt_at !== expected_delivery_attempted_at) {
847
+ fail('STALE_EXTERNAL_DELIVERY_RESULT', 'lookup result does not match the current delivery attempt');
848
+ }
849
+ if (!['sent_unconfirmed', 'retryable_failure'].includes(projection.delivery_state)) {
850
+ fail('NOT_SENT_UNCONFIRMED', 'idempotency lookup only applies to uncertain delivery');
851
+ }
852
+ object(result, 'INVALID_LOOKUP_RESULT', 'idempotency lookup result');
853
+ string(evidence_ref, 'INVALID_LOOKUP_RESULT', 'lookup evidence_ref');
854
+ if (this.#verifyReference(evidence_ref, 'receipt') !== true) fail('UNVERIFIED_RECEIPT', 'lookup evidence must be an immutable verified reference');
855
+ if (result.receipt_ref !== undefined) {
856
+ string(result.receipt_ref, 'INVALID_LOOKUP_RESULT', 'receipt_ref');
857
+ if (this.#verifyReference(result.receipt_ref, 'receipt') !== true) fail('UNVERIFIED_RECEIPT', 'lookup receipt must be an immutable verified reference');
858
+ projection.delivery_state = 'confirmed';
859
+ projection.receipt_ref = result.receipt_ref;
860
+ projection.reconciliation_state = 'pending';
861
+ } else if (result.state === 'not_found') {
862
+ projection.delivery_state = 'pending';
863
+ } else if (result.state !== 'unknown') {
864
+ fail('INVALID_LOOKUP_RESULT', 'lookup requires receipt_ref, not_found, or unknown');
865
+ }
866
+ projection.lookup_evidence_ref = evidence_ref;
867
+ this.#setEvolution(projection);
868
+ this.#persist();
869
+ return Object.freeze(canonicalize(projection));
870
+ });
223
871
  }
224
872
 
225
873
  reconcile({ projection_id } = {}) {
874
+ return this.#exclusive(() => {
226
875
  const projection = this.#find(projection_id);
227
876
  if (!this.#transport || typeof this.#transport.reconcile !== 'function') fail('RECONCILIATION_NOT_CONFIGURED', 'no authorized reconciliation transport is configured');
228
877
  if (projection.delivery_state !== 'confirmed') fail('RECEIPT_REQUIRED', 'reconciliation requires confirmed delivery receipt');
@@ -235,11 +884,13 @@ export class TrackingClickUp {
235
884
  this.#setEvolution(projection);
236
885
  this.#persist();
237
886
  return Object.freeze(canonicalize(projection));
887
+ });
238
888
  }
239
889
 
240
890
  resolveSentUnconfirmed({ projection_id } = {}) {
891
+ return this.#exclusive(() => {
241
892
  const projection = this.#find(projection_id);
242
- if (projection.delivery_state !== 'sent_unconfirmed') fail('NOT_SENT_UNCONFIRMED', 'idempotency lookup only applies to sent_unconfirmed delivery');
893
+ if (!['sent_unconfirmed', 'retryable_failure'].includes(projection.delivery_state)) fail('NOT_SENT_UNCONFIRMED', 'idempotency lookup only applies to uncertain delivery');
243
894
  if (!this.#transport || typeof this.#transport.lookup !== 'function') fail('IDEMPOTENCY_LOOKUP_NOT_CONFIGURED', 'sent_unconfirmed requires an idempotency lookup transport');
244
895
  this.#preflightTransport(projection);
245
896
  const result = this.#transport.lookup(Object.freeze(canonicalize(projection)));
@@ -255,27 +906,33 @@ export class TrackingClickUp {
255
906
  this.#setEvolution(projection);
256
907
  this.#persist();
257
908
  return Object.freeze(canonicalize(projection));
909
+ });
258
910
  }
259
911
 
260
912
  watchOnce() {
913
+ return this.#exclusive(() => {
261
914
  // Intentionally one-shot: a host scheduler may invoke this hourly. No timer is created.
262
915
  const replayed = [];
263
916
  for (const projection of this.#state.projections) {
264
- if (projection.delivery_state === 'sent_unconfirmed') {
917
+ if (['sent_unconfirmed', 'retryable_failure'].includes(projection.delivery_state)) {
265
918
  const resolved = this.resolveSentUnconfirmed({ projection_id: projection.projection_id });
266
919
  if (resolved.delivery_state === 'pending') replayed.push(this.deliver({ projection_id: projection.projection_id }));
267
- } else if (['pending', 'retryable_failure'].includes(projection.delivery_state)) replayed.push(this.deliver({ projection_id: projection.projection_id }));
920
+ } else if (projection.delivery_state === 'pending') replayed.push(this.deliver({ projection_id: projection.projection_id }));
268
921
  }
269
922
  return Object.freeze(replayed);
923
+ });
270
924
  }
271
925
 
272
926
  rederiveProjectEvolution({ project_id } = {}) {
273
- string(project_id, 'INVALID_PROJECT_ID', 'project_id');
927
+ return this.#exclusive(() => {
928
+ assertProjectId(project_id);
274
929
  if (!this.#state.projections.some((item) => item.project_id === project_id)) fail('PROJECT_NOT_FOUND', 'project has no durable projections', { project_id });
275
930
  return this.#evolve(project_id);
931
+ });
276
932
  }
277
933
 
278
934
  recordMetrics({ projection_id, actual_human_effort, actual_ai_processing, rework_cycles, rework_review_refs, completion } = {}) {
935
+ return this.#exclusive(() => {
279
936
  const projection = this.#find(projection_id);
280
937
  object(actual_human_effort, 'INVALID_HUMAN_EFFORT', 'actual_human_effort');
281
938
  object(actual_ai_processing, 'INVALID_AI_PROCESSING', 'actual_ai_processing');
@@ -293,7 +950,8 @@ export class TrackingClickUp {
293
950
  // No wall-clock value exists here. Provenance is durable with the metric.
294
951
  const metricMaterial = canonicalize({ actual_human_effort, actual_ai_processing, rework_cycles, rework_review_refs, completion_id: completion.completion_id });
295
952
  if (projection.metrics) {
296
- const { recorded_at: _recordedAt, ...existingMaterial } = projection.metrics;
953
+ const existingMaterial = { ...projection.metrics };
954
+ delete existingMaterial.recorded_at;
297
955
  if (sha256(existingMaterial) !== sha256(metricMaterial)) fail('METRICS_ALREADY_RECORDED', 'projection metrics are immutable');
298
956
  return Object.freeze(canonicalize(projection));
299
957
  }
@@ -301,12 +959,14 @@ export class TrackingClickUp {
301
959
  this.#setEvolution(projection);
302
960
  this.#persist();
303
961
  return Object.freeze(canonicalize(projection));
962
+ });
304
963
  }
305
964
  }
306
965
 
307
966
  export function validateProjection(projection) {
308
967
  object(projection, 'INVALID_PROJECTION', 'projection');
309
968
  for (const field of ['projection_id', 'idempotency_key', 'canonical_event_id', 'task_id', 'clickup_ref', 'project_id', 'payload_hash']) string(projection[field], 'INVALID_PROJECTION', field);
969
+ assertProjectId(projection.project_id);
310
970
  if (!OPERATIONS.has(projection.operation)) fail('INVALID_PROJECTION_OPERATION', 'operation is invalid');
311
971
  if (!DELIVERY_STATES.has(projection.delivery_state)) fail('INVALID_DELIVERY_STATE', 'delivery_state is invalid');
312
972
  if (!RECONCILIATION_STATES.has(projection.reconciliation_state)) fail('INVALID_RECONCILIATION_STATE', 'reconciliation_state is invalid');