chati-dev 4.5.28 → 4.5.29
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/chati.js +151 -123
- package/framework/agents/build/dev.md +4 -4
- package/framework/agents/deploy/devops.md +3 -3
- package/framework/agents/discover/brief.md +18 -6
- package/framework/agents/discover/brownfield-wu.md +3 -3
- package/framework/agents/discover/greenfield-wu.md +4 -4
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +4 -4
- package/framework/agents/plan/phases.md +3 -3
- package/framework/agents/plan/tasks.md +47 -19
- package/framework/agents/plan/ux-brand-architect.md +1 -1
- package/framework/agents/plan/ux.md +26 -2
- package/framework/agents/quality/qa-implementation.md +8 -8
- package/framework/agents/quality/qa-planning.md +58 -4
- package/framework/agents/quality/qa-visual.md +4 -4
- package/framework/config.yaml +2 -2
- package/framework/constitution.md +12 -1
- package/framework/context/governance.md +2 -1
- package/framework/context/root.md +3 -1
- package/framework/domains/constitution.yaml +1 -1
- package/framework/domains/global.yaml +1 -1
- package/framework/domains/workflows/standard-flow.yaml +7 -8
- package/framework/hooks/advance-trigger.js +3 -2
- package/framework/hooks/brief-validator.js +1 -1
- package/framework/hooks/post-dev.js +1 -1
- package/framework/hooks/team-quality-gate.js +9 -2
- package/framework/i18n/en.yaml +1 -0
- package/framework/i18n/es.yaml +1 -0
- package/framework/i18n/fr.yaml +1 -0
- package/framework/i18n/pt.yaml +1 -0
- package/framework/manifest.json +83 -83
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati-router.js +14 -6
- package/framework/orchestrator/chati.md +61 -23
- package/framework/rules/human-writing-style.md +7 -0
- package/framework/schemas/session.schema.json +25 -2
- package/framework/schemas/task.schema.json +26 -0
- package/framework/tasks/orchestrator-handoff.md +10 -10
- package/framework/tasks/orchestrator-resume.md +3 -3
- package/framework/workflows/brownfield-fullstack.yaml +6 -3
- package/framework/workflows/brownfield-service.yaml +5 -3
- package/framework/workflows/brownfield-ui.yaml +5 -3
- package/framework/workflows/greenfield-fullstack.yaml +6 -3
- package/framework/workflows/standard-flow.yaml +8 -3
- package/node_modules/@chati/core/native/browser-proof-wire.h +29 -0
- package/node_modules/@chati/core/native/darwin-browser-listener.c +36 -0
- package/node_modules/@chati/core/native/darwin-browser-listener.h +32 -0
- package/node_modules/@chati/core/native/darwin-browser-proof.h +86 -0
- package/node_modules/@chati/core/native/darwin-code-identity.h +35 -0
- package/node_modules/@chati/core/native/darwin-launch-messages.h +37 -0
- package/node_modules/@chati/core/native/darwin-launcher.c +312 -0
- package/node_modules/@chati/core/native/descriptor-fs.c +407 -0
- package/node_modules/@chati/core/native/execution-admission.h +59 -0
- package/node_modules/@chati/core/native/execution-gate.c +41 -0
- package/node_modules/@chati/core/native/linux-process.c +41 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/darwin-launcher +0 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/descriptor-fs.node +0 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/execution-gate +0 -0
- package/node_modules/@chati/core/native/prebuilds/linux-x64/descriptor-fs.node +0 -0
- package/node_modules/@chati/core/native/prebuilds/linux-x64/execution-gate +0 -0
- package/node_modules/@chati/core/native/process-info-pipe.c +91 -0
- package/node_modules/@chati/core/package.json +2 -1
- package/node_modules/@chati/core/scripts/build-native.js +36 -0
- package/node_modules/@chati/core/src/darwin-browser-proof.js +87 -0
- package/node_modules/@chati/core/src/darwin-host-boot.js +58 -0
- package/node_modules/@chati/core/src/darwin-process.js +79 -0
- package/node_modules/@chati/core/src/darwin-supervisor-channels.js +82 -0
- package/node_modules/@chati/core/src/darwin-terminal-receipt.js +111 -0
- package/node_modules/@chati/core/src/descriptor-fs.js +222 -0
- package/node_modules/@chati/core/src/execution-gate.js +8 -0
- package/node_modules/@chati/core/src/host-boot-marker.js +45 -0
- package/node_modules/@chati/core/src/index.js +18 -0
- package/node_modules/@chati/core/src/linux-host-boot.js +218 -0
- package/node_modules/@chati/core/src/linux-process.js +133 -0
- package/node_modules/@chati/planning/src/browser-verification-plan.js +71 -0
- package/node_modules/@chati/planning/src/index.js +33 -0
- package/node_modules/@chati/planning/src/planning-material.js +64 -0
- package/node_modules/@chati/planning/src/verification-plan.js +57 -0
- package/node_modules/@chati/rail/package.json +1 -0
- package/node_modules/@chati/rail/src/execution-restoration.js +92 -0
- package/node_modules/@chati/rail/src/index.js +812 -167
- package/node_modules/@chati/rail/src/provider-processing.js +118 -0
- package/node_modules/@chati/rail/src/terminal-containment.js +23 -0
- package/node_modules/@chati/release-lane/src/index.js +40 -43
- package/node_modules/@chati/review-council/src/index.js +223 -0
- package/node_modules/@chati/tracking-clickup/src/index.js +67 -58
- package/package-artifact-manifest.json +1 -1
- package/package-artifact-manifest.sig +1 -1
- package/package.json +3 -2
- package/scripts/verify-real-harness-e2e.js +20 -8
- package/src/installer/core.js +147 -107
- package/src/installer/package-artifact.js +7 -0
- package/src/installer/preflight.js +13 -0
- package/src/installer/provider-overlay.js +1 -2
- package/src/installer/templates.js +58 -0
- package/src/installer-v2/catalog-client.js +31 -1
- package/src/installer-v2/clickup-connection-storage.js +92 -0
- package/src/installer-v2/clickup-mcp-client.js +155 -0
- package/src/installer-v2/clickup-oauth-loopback.js +96 -0
- package/src/installer-v2/clickup-oauth.js +233 -0
- package/src/installer-v2/index.js +141 -15
- package/src/installer-v2/installation-authority.js +98 -4
- package/src/installer-v2/provider-executable.js +117 -12
- package/src/installer-v2/wizard-installation.js +11 -0
- package/src/orchestrator/agent-selector.js +39 -1
- package/src/orchestrator/browser-artifact-store.js +131 -0
- package/src/orchestrator/cli.js +762 -205
- package/src/orchestrator/clickup-mcp-observer.js +226 -0
- package/src/orchestrator/clickup-runtime.js +45 -24
- package/src/orchestrator/council-gate-runtime.js +903 -0
- package/src/orchestrator/evidence-authority-enrollment.js +75 -0
- package/src/orchestrator/evidence-authority-request.js +46 -0
- package/src/orchestrator/handoff-engine.js +4 -11
- package/src/orchestrator/pipeline-manager.js +42 -4
- package/src/orchestrator/planning-contract-revision.js +711 -0
- package/src/orchestrator/planning-reconciliation.js +302 -0
- package/src/orchestrator/planning-runtime.js +476 -29
- package/src/orchestrator/rail-adjudication-evidence.js +15 -24
- package/src/orchestrator/rail-human-effort-authority.js +46 -0
- package/src/orchestrator/rail-human-effort.js +149 -0
- package/src/orchestrator/rail-runtime.js +291 -47
- package/src/orchestrator/rail-verification.js +278 -0
- package/src/orchestrator/review-runtime.js +37 -2
- package/src/orchestrator/runtime-installation-v2.js +143 -24
- package/src/orchestrator/session-manager.js +1457 -29
- package/src/tasks/handoff.js +57 -33
- package/src/terminal/adapters/claude-adapter.js +3 -0
- package/src/terminal/adapters/codex-adapter.js +13 -0
- package/src/terminal/adapters/grok-adapter.js +1 -0
- package/src/terminal/agent-attempt-budget.js +35 -0
- package/src/terminal/browser-endpoint-proxy.js +174 -0
- package/src/terminal/browser-observer-driver.js +94 -0
- package/src/terminal/browser-observer-supervisor.js +73 -0
- package/src/terminal/browser-observer.js +95 -0
- package/src/terminal/browser-process-facts.js +75 -0
- package/src/terminal/browser-runtime.js +131 -0
- package/src/terminal/darwin-authority-policy.js +213 -0
- package/src/terminal/darwin-browser-application.js +116 -0
- package/src/terminal/darwin-browser-observer.js +57 -0
- package/src/terminal/darwin-execution.js +185 -0
- package/src/terminal/executable-snapshot.js +202 -0
- package/src/terminal/handoff-parser.js +48 -2
- package/src/terminal/isolation.js +14 -13
- package/src/terminal/prompt-builder.js +176 -15
- package/src/terminal/provider-preflight.js +47 -27
- package/src/terminal/rail-execution-worktree.js +22 -6
- package/src/terminal/rail-process-observer.js +10 -0
- package/src/terminal/rail-prompts.js +21 -3
- package/src/terminal/rail-readonly-workspace.js +54 -5
- package/src/terminal/rail-worktree-checkpoint.js +315 -0
- package/src/terminal/rail-worktree-restoration.js +485 -0
- package/src/terminal/run-agent.js +164 -42
- package/src/terminal/run-council-gate.js +338 -0
- package/src/terminal/run-planning-reconciliation.js +49 -0
- package/src/terminal/run-rail-adjudication.js +29 -7
- package/src/terminal/run-rail-recovery.js +97 -0
- package/src/terminal/run-rail-review.js +36 -15
- package/src/terminal/run-rail-rework.js +26 -15
- package/src/terminal/run-rail-task.js +22 -9
- package/src/terminal/runtime-native-helper.js +85 -0
- package/src/terminal/spawner.js +899 -142
- package/src/terminal/verification-process-facts.js +49 -0
- package/src/upgrade/entrypoints.js +68 -0
- package/src/upgrade/generation.js +194 -0
- package/src/upgrade/maintenance.js +79 -0
- package/src/upgrade/paths.js +81 -0
- package/src/upgrade/upgrade.js +267 -0
- package/src/utils/execution-role.js +15 -0
- package/src/utils/handoff-paths.js +59 -0
- package/src/wizard/i18n.js +1 -0
- package/src/wizard/index.js +17 -5
- package/src/wizard/questions.js +24 -12
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
-
closeSync, constants,
|
|
3
|
-
openSync, readSync, readdirSync, renameSync, unlinkSync, writeSync,
|
|
2
|
+
closeSync, constants, fstatSync, fsyncSync, openSync, readSync, writeSync,
|
|
4
3
|
} from 'node:fs';
|
|
5
4
|
import { randomUUID } from 'node:crypto';
|
|
6
5
|
import { hostname } from 'node:os';
|
|
7
6
|
import { basename, dirname, isAbsolute, join, parse, relative, resolve, sep } from 'node:path';
|
|
8
7
|
import { performance } from 'node:perf_hooks';
|
|
9
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
captureHostBootMarker, ContractError, canonicalize, directoryPathSync, hostBootMarkerChanged,
|
|
10
|
+
linkAtSync, lstatAtSync, mkdirAtSync, openAtSync, readDirectorySync, renameAtSync, sha256, unlinkAtSync,
|
|
11
|
+
} from '@chati/core';
|
|
10
12
|
|
|
11
13
|
const RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
12
14
|
const DELIVERY_STATES = new Set(['pending', 'sent_unconfirmed', 'confirmed', 'retryable_failure', 'permanent_failure']);
|
|
@@ -158,23 +160,22 @@ function readDescriptorBounded(descriptor, maxBytes) {
|
|
|
158
160
|
}
|
|
159
161
|
|
|
160
162
|
function openPinnedTrackingDirectory(path, { create = false } = {}) {
|
|
161
|
-
if (process.platform
|
|
163
|
+
if (!['linux', 'darwin'].includes(process.platform)) fail('UNSUPPORTED_TRACKING_STORAGE_PLATFORM', `tracking authority storage requires descriptor-relative operations; unsupported platform: ${process.platform}`);
|
|
162
164
|
const absolute = resolve(path);
|
|
163
165
|
const root = parse(absolute).root;
|
|
164
166
|
let descriptor = openSync(root, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
|
|
165
167
|
try {
|
|
166
168
|
for (const segment of absolute.slice(root.length).split(sep).filter(Boolean)) {
|
|
167
|
-
const childPath = join(`/proc/self/fd/${descriptor}`, segment);
|
|
168
169
|
let child;
|
|
169
|
-
try { child =
|
|
170
|
+
try { child = openAtSync(descriptor, segment, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0)); }
|
|
170
171
|
catch (error) {
|
|
171
172
|
if (error?.code !== 'ENOENT' || !create) {
|
|
172
173
|
if (['ELOOP', 'ENOTDIR'].includes(error?.code)) fail('UNSAFE_OUTBOX_DIRECTORY', 'tracking directory chain must contain only real directories');
|
|
173
174
|
throw error;
|
|
174
175
|
}
|
|
175
|
-
try {
|
|
176
|
+
try { mkdirAtSync(descriptor, segment, { mode: 0o700 }); }
|
|
176
177
|
catch (mkdirError) { if (mkdirError?.code !== 'EEXIST') throw mkdirError; }
|
|
177
|
-
child =
|
|
178
|
+
child = openAtSync(descriptor, segment, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
|
|
178
179
|
}
|
|
179
180
|
const stat = fstatSync(child);
|
|
180
181
|
if (!stat.isDirectory()) {
|
|
@@ -184,18 +185,13 @@ function openPinnedTrackingDirectory(path, { create = false } = {}) {
|
|
|
184
185
|
closeSync(descriptor);
|
|
185
186
|
descriptor = child;
|
|
186
187
|
}
|
|
187
|
-
return Object.freeze({ absolute, descriptor, identity: fstatSync(descriptor)
|
|
188
|
+
return Object.freeze({ absolute, descriptor, identity: fstatSync(descriptor) });
|
|
188
189
|
} catch (error) {
|
|
189
190
|
closeSync(descriptor);
|
|
190
191
|
throw error;
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
194
|
|
|
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
195
|
function assertPinnedTrackingDirectory(directory) {
|
|
200
196
|
const pinned = fstatSync(directory.descriptor);
|
|
201
197
|
if (!pinned.isDirectory() || !sameIdentity(pinned, directory.identity)) fail('UNSAFE_OUTBOX_DIRECTORY', 'pinned tracking directory changed');
|
|
@@ -217,10 +213,9 @@ function readRegular(path, { allowMissing = false, maxBytes = OUTBOX_MAX_BYTES,
|
|
|
217
213
|
try { activeDirectory = openPinnedTrackingDirectory(dirname(path)); }
|
|
218
214
|
catch (error) { if (allowMissing && error?.code === 'ENOENT') return null; throw error; }
|
|
219
215
|
}
|
|
220
|
-
const entry = trackingEntry(activeDirectory, basename(path));
|
|
221
216
|
let descriptor;
|
|
222
217
|
try {
|
|
223
|
-
descriptor =
|
|
218
|
+
descriptor = openAtSync(activeDirectory.descriptor, basename(path), constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
|
|
224
219
|
} catch (error) {
|
|
225
220
|
if (ownedDirectory) closeSync(activeDirectory.descriptor);
|
|
226
221
|
if (allowMissing && error?.code === 'ENOENT') return null;
|
|
@@ -252,12 +247,12 @@ function writeAtomic(path, data, maxBytes = OUTBOX_MAX_BYTES, { directory = null
|
|
|
252
247
|
if (bytes.length > maxBytes) fail('OUTBOX_SIZE_LIMIT', `tracking state exceeds the ${maxBytes}-byte safety limit`);
|
|
253
248
|
const activeDirectory = directory ?? openPinnedTrackingDirectory(dirname(path), { create: true });
|
|
254
249
|
const ownedDirectory = directory === null;
|
|
255
|
-
const
|
|
256
|
-
const
|
|
250
|
+
const temporaryName = `${basename(path)}.${process.pid}.${randomUUID()}.tmp`;
|
|
251
|
+
const name = basename(path);
|
|
257
252
|
let temporaryDescriptor;
|
|
258
253
|
try {
|
|
259
254
|
const existing = readRegular(path, { allowMissing: true, maxBytes, directory: activeDirectory });
|
|
260
|
-
temporaryDescriptor =
|
|
255
|
+
temporaryDescriptor = openAtSync(activeDirectory.descriptor, temporaryName, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
|
|
261
256
|
let offset = 0;
|
|
262
257
|
while (offset < bytes.length) offset += writeSync(temporaryDescriptor, bytes, offset, bytes.length - offset);
|
|
263
258
|
fsyncSync(temporaryDescriptor);
|
|
@@ -265,18 +260,18 @@ function writeAtomic(path, data, maxBytes = OUTBOX_MAX_BYTES, { directory = null
|
|
|
265
260
|
temporaryDescriptor = undefined;
|
|
266
261
|
assertPinnedTrackingDirectory(activeDirectory);
|
|
267
262
|
let current = null;
|
|
268
|
-
try { current =
|
|
263
|
+
try { current = lstatAtSync(activeDirectory.descriptor, name); } catch (error) { if (error?.code !== 'ENOENT') throw error; }
|
|
269
264
|
if ((existing === null && current !== null)
|
|
270
265
|
|| (existing !== null && (!current || current.isSymbolicLink() || !current.isFile()
|
|
271
266
|
|| current.nlink !== 1 || !sameIdentity(current, existing.stat)))) {
|
|
272
267
|
fail('OUTBOX_STORAGE_CONFLICT', 'tracking state changed during atomic persistence');
|
|
273
268
|
}
|
|
274
|
-
|
|
269
|
+
renameAtSync(activeDirectory.descriptor, temporaryName, activeDirectory.descriptor, name);
|
|
275
270
|
fsyncSync(activeDirectory.descriptor);
|
|
276
271
|
assertPinnedTrackingDirectory(activeDirectory);
|
|
277
272
|
} finally {
|
|
278
273
|
if (temporaryDescriptor !== undefined) closeSync(temporaryDescriptor);
|
|
279
|
-
try {
|
|
274
|
+
try { unlinkAtSync(activeDirectory.descriptor, temporaryName); } catch { /* leave an unverified temporary untouched */ }
|
|
280
275
|
if (ownedDirectory) closeSync(activeDirectory.descriptor);
|
|
281
276
|
}
|
|
282
277
|
}
|
|
@@ -320,8 +315,10 @@ export function persistImmutableClickUpDocument(path, data, { maxBytes = OUTBOX_
|
|
|
320
315
|
const bytes = Buffer.from(content);
|
|
321
316
|
if (bytes.length > maxBytes) fail('OUTBOX_SIZE_LIMIT', `tracking document exceeds the ${maxBytes}-byte safety limit`);
|
|
322
317
|
const directory = openPinnedTrackingDirectory(dirname(path), { create: true });
|
|
323
|
-
const
|
|
324
|
-
const
|
|
318
|
+
const name = basename(path);
|
|
319
|
+
const temporaryName = `${name}.${process.pid}.${randomUUID()}.tmp`;
|
|
320
|
+
// Diagnostic/test-hook path only; mutations use the pinned descriptor.
|
|
321
|
+
const temporary = join(directoryPathSync(directory.descriptor), temporaryName);
|
|
325
322
|
let temporaryDescriptor;
|
|
326
323
|
try {
|
|
327
324
|
const existing = readRegular(path, { allowMissing: true, maxBytes, directory });
|
|
@@ -330,7 +327,7 @@ export function persistImmutableClickUpDocument(path, data, { maxBytes = OUTBOX_
|
|
|
330
327
|
return Object.freeze({ written: false, path });
|
|
331
328
|
}
|
|
332
329
|
runSecurityTestHook('beforeTrackingDocumentTempOpen', { path, temporary, directory: directory.absolute });
|
|
333
|
-
temporaryDescriptor =
|
|
330
|
+
temporaryDescriptor = openAtSync(directory.descriptor, temporaryName, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
|
|
334
331
|
runSecurityTestHook('afterTrackingDocumentTempOpen', { path, temporary, directory: directory.absolute });
|
|
335
332
|
let offset = 0;
|
|
336
333
|
while (offset < bytes.length) offset += writeSync(temporaryDescriptor, bytes, offset, bytes.length - offset);
|
|
@@ -338,7 +335,7 @@ export function persistImmutableClickUpDocument(path, data, { maxBytes = OUTBOX_
|
|
|
338
335
|
closeSync(temporaryDescriptor);
|
|
339
336
|
temporaryDescriptor = undefined;
|
|
340
337
|
assertPinnedTrackingDirectory(directory);
|
|
341
|
-
try {
|
|
338
|
+
try { linkAtSync(directory.descriptor, temporaryName, directory.descriptor, name); }
|
|
342
339
|
catch (error) {
|
|
343
340
|
if (error?.code !== 'EEXIST') throw error;
|
|
344
341
|
const raced = readRegular(path, { maxBytes, directory });
|
|
@@ -350,7 +347,7 @@ export function persistImmutableClickUpDocument(path, data, { maxBytes = OUTBOX_
|
|
|
350
347
|
return Object.freeze({ written: true, path });
|
|
351
348
|
} finally {
|
|
352
349
|
if (temporaryDescriptor !== undefined) closeSync(temporaryDescriptor);
|
|
353
|
-
try {
|
|
350
|
+
try { unlinkAtSync(directory.descriptor, temporaryName); } catch { /* leave an unverified temporary untouched */ }
|
|
354
351
|
closeSync(directory.descriptor);
|
|
355
352
|
}
|
|
356
353
|
}
|
|
@@ -358,7 +355,7 @@ export function persistImmutableClickUpDocument(path, data, { maxBytes = OUTBOX_
|
|
|
358
355
|
export function listClickUpDocumentPaths(directoryPath) {
|
|
359
356
|
const directory = openPinnedTrackingDirectory(directoryPath);
|
|
360
357
|
try {
|
|
361
|
-
const names =
|
|
358
|
+
const names = readDirectorySync(directory.descriptor).filter((name) => /^[A-Za-z0-9._-]+\.json$/.test(name)).sort();
|
|
362
359
|
assertPinnedTrackingDirectory(directory);
|
|
363
360
|
return names.map((name) => join(directory.absolute, name));
|
|
364
361
|
} finally { closeSync(directory.descriptor); }
|
|
@@ -366,16 +363,16 @@ export function listClickUpDocumentPaths(directoryPath) {
|
|
|
366
363
|
|
|
367
364
|
export function removeImmutableClickUpDocument(path, { maxBytes = OUTBOX_MAX_BYTES } = {}) {
|
|
368
365
|
const directory = openPinnedTrackingDirectory(dirname(path));
|
|
369
|
-
const
|
|
366
|
+
const name = basename(path);
|
|
370
367
|
try {
|
|
371
368
|
const existing = readRegular(path, { allowMissing: true, maxBytes, directory });
|
|
372
369
|
if (existing === null) return false;
|
|
373
|
-
const current =
|
|
370
|
+
const current = lstatAtSync(directory.descriptor, name);
|
|
374
371
|
if (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1 || !sameIdentity(current, existing.stat)) {
|
|
375
372
|
fail('OUTBOX_STORAGE_CONFLICT', 'tracking document changed before removal');
|
|
376
373
|
}
|
|
377
374
|
assertPinnedTrackingDirectory(directory);
|
|
378
|
-
|
|
375
|
+
unlinkAtSync(directory.descriptor, name);
|
|
379
376
|
fsyncSync(directory.descriptor);
|
|
380
377
|
assertPinnedTrackingDirectory(directory);
|
|
381
378
|
return true;
|
|
@@ -392,10 +389,10 @@ function processIsAlive(pid) {
|
|
|
392
389
|
try { process.kill(pid, 0); return true; } catch (error) { return error?.code === 'EPERM'; }
|
|
393
390
|
}
|
|
394
391
|
|
|
395
|
-
function readOutboxLock(
|
|
392
|
+
function readOutboxLock(directory, name) {
|
|
396
393
|
let descriptor;
|
|
397
394
|
try {
|
|
398
|
-
descriptor =
|
|
395
|
+
descriptor = openAtSync(directory.descriptor, name, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
|
|
399
396
|
const before = fstatSync(descriptor);
|
|
400
397
|
if (!before.isFile() || before.nlink !== 1 || before.size > 16 * 1024) return null;
|
|
401
398
|
const content = readDescriptorBounded(descriptor, 16 * 1024);
|
|
@@ -408,45 +405,54 @@ function readOutboxLock(path) {
|
|
|
408
405
|
}
|
|
409
406
|
|
|
410
407
|
|
|
411
|
-
function recoverStaleOutboxLock(
|
|
408
|
+
function recoverStaleOutboxLock(directory, name) {
|
|
412
409
|
let stat;
|
|
413
|
-
try { stat =
|
|
410
|
+
try { stat = lstatAtSync(directory.descriptor, name); } catch (error) { return error?.code === 'ENOENT'; }
|
|
414
411
|
if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) return false;
|
|
415
|
-
const owner = readOutboxLock(
|
|
416
|
-
|
|
412
|
+
const owner = readOutboxLock(directory, name);
|
|
413
|
+
// A rebooted host may recycle the owner's pid onto an unrelated live process.
|
|
414
|
+
// Prove the reboot from the boot marker before ever trusting PID liveness.
|
|
415
|
+
if (owner?.hostname === hostname() && !hostBootMarkerChanged(owner.host_boot_marker, captureHostBootMarker())
|
|
416
|
+
&& processIsAlive(owner.pid)) return false;
|
|
417
417
|
// A process on another host cannot be proven dead from this host. Keep the
|
|
418
418
|
// lock until an operator explicitly reconciles it.
|
|
419
419
|
if (owner?.hostname && owner.hostname !== hostname()) return false;
|
|
420
420
|
if (!owner && Date.now() - stat.mtimeMs < OUTBOX_LOCK_STALE_MS) return false;
|
|
421
|
-
const
|
|
421
|
+
const recoveryName = `${name}.recovery`;
|
|
422
422
|
let recoveryFd;
|
|
423
423
|
try {
|
|
424
|
-
recoveryFd =
|
|
425
|
-
const
|
|
424
|
+
recoveryFd = openAtSync(directory.descriptor, recoveryName, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
|
|
425
|
+
const recoveryMarker = captureHostBootMarker();
|
|
426
|
+
const bytes = Buffer.from(JSON.stringify({ pid: process.pid, hostname: hostname(), token: randomUUID(), ...(recoveryMarker ? { host_boot_marker: recoveryMarker } : {}) }));
|
|
426
427
|
writeSync(recoveryFd, bytes, 0, bytes.length);
|
|
427
428
|
fsyncSync(recoveryFd);
|
|
428
429
|
} catch {
|
|
429
430
|
if (recoveryFd !== undefined) closeSync(recoveryFd);
|
|
430
431
|
return false;
|
|
431
432
|
}
|
|
432
|
-
const quarantine = `${
|
|
433
|
+
const quarantine = `${name}.stale.${process.pid}.${randomUUID()}`;
|
|
433
434
|
try {
|
|
434
|
-
const current =
|
|
435
|
-
const currentOwner = readOutboxLock(
|
|
435
|
+
const current = lstatAtSync(directory.descriptor, name);
|
|
436
|
+
const currentOwner = readOutboxLock(directory, name);
|
|
436
437
|
if (!sameIdentity(stat, current) || currentOwner?.token !== owner?.token) return false;
|
|
437
|
-
|
|
438
|
-
const quarantined =
|
|
439
|
-
const quarantinedOwner = readOutboxLock(quarantine);
|
|
438
|
+
renameAtSync(directory.descriptor, name, directory.descriptor, quarantine);
|
|
439
|
+
const quarantined = lstatAtSync(directory.descriptor, quarantine);
|
|
440
|
+
const quarantinedOwner = readOutboxLock(directory, quarantine);
|
|
440
441
|
if (!sameIdentity(stat, quarantined) || quarantinedOwner?.token !== owner?.token) {
|
|
441
|
-
try {
|
|
442
|
+
try {
|
|
443
|
+
let missing = false;
|
|
444
|
+
try { lstatAtSync(directory.descriptor, name); }
|
|
445
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; missing = true; }
|
|
446
|
+
if (missing) renameAtSync(directory.descriptor, quarantine, directory.descriptor, name);
|
|
447
|
+
} catch { /* preserve quarantine fail-closed */ }
|
|
442
448
|
return false;
|
|
443
449
|
}
|
|
444
|
-
|
|
450
|
+
unlinkAtSync(directory.descriptor, quarantine);
|
|
445
451
|
return true;
|
|
446
452
|
} catch (error) { return error?.code === 'ENOENT'; }
|
|
447
453
|
finally {
|
|
448
454
|
if (recoveryFd !== undefined) closeSync(recoveryFd);
|
|
449
|
-
try {
|
|
455
|
+
try { unlinkAtSync(directory.descriptor, recoveryName); } catch { /* fail closed if recovery ownership changed */ }
|
|
450
456
|
}
|
|
451
457
|
}
|
|
452
458
|
|
|
@@ -512,10 +518,12 @@ export class TrackingClickUp {
|
|
|
512
518
|
if (this.#requireAuthenticatedState && (!this.#stateAuthority || !this.#authorityContext)) {
|
|
513
519
|
fail('CLICKUP_STATE_AUTHORITY_REQUIRED', 'production ClickUp state requires a host authority and project context');
|
|
514
520
|
}
|
|
515
|
-
this.#loadState({ allowMissing: true });
|
|
516
521
|
// Project Evolution is a derivative, never the only copy of evidence.
|
|
517
|
-
// Rebuild
|
|
518
|
-
|
|
522
|
+
// Rebuild under the same lock as enqueue: another constructor or writer may
|
|
523
|
+
// otherwise replace a snapshot during this read/atomic persistence sequence.
|
|
524
|
+
this.#exclusive(() => {
|
|
525
|
+
for (const projectId of new Set(this.#state.projections.map((item) => item.project_id))) this.#evolve(projectId);
|
|
526
|
+
});
|
|
519
527
|
}
|
|
520
528
|
|
|
521
529
|
get projections() { return Object.freeze(this.#state.projections.map((item) => Object.freeze(canonicalize(item)))); }
|
|
@@ -562,7 +570,7 @@ export class TrackingClickUp {
|
|
|
562
570
|
#exclusive(action) {
|
|
563
571
|
if (this.#lockDepth > 0) return action();
|
|
564
572
|
const directory = openPinnedTrackingDirectory(dirname(this.#outboxPath), { create: true });
|
|
565
|
-
const
|
|
573
|
+
const name = `${basename(this.#outboxPath)}.lock`;
|
|
566
574
|
let descriptor;
|
|
567
575
|
let token;
|
|
568
576
|
try {
|
|
@@ -570,16 +578,17 @@ export class TrackingClickUp {
|
|
|
570
578
|
while (performance.now() < lockDeadline) {
|
|
571
579
|
let candidate;
|
|
572
580
|
try {
|
|
573
|
-
candidate =
|
|
581
|
+
candidate = openAtSync(directory.descriptor, name, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
|
|
574
582
|
} catch (error) {
|
|
575
583
|
if (error?.code !== 'EEXIST') throw error;
|
|
576
|
-
if (recoverStaleOutboxLock(
|
|
584
|
+
if (recoverStaleOutboxLock(directory, name)) continue;
|
|
577
585
|
Atomics.wait(OUTBOX_LOCK_WAIT, 0, 0, 4);
|
|
578
586
|
continue;
|
|
579
587
|
}
|
|
580
588
|
token = randomUUID();
|
|
581
589
|
try {
|
|
582
|
-
const
|
|
590
|
+
const ownerMarker = captureHostBootMarker();
|
|
591
|
+
const bytes = Buffer.from(JSON.stringify({ schema_version: 1, pid: process.pid, hostname: hostname(), token, acquired_at: new Date().toISOString(), ...(ownerMarker ? { host_boot_marker: ownerMarker } : {}) }));
|
|
583
592
|
writeSync(candidate, bytes, 0, bytes.length);
|
|
584
593
|
fsyncSync(candidate);
|
|
585
594
|
fsyncSync(directory.descriptor);
|
|
@@ -587,7 +596,7 @@ export class TrackingClickUp {
|
|
|
587
596
|
break;
|
|
588
597
|
} catch (error) {
|
|
589
598
|
closeSync(candidate);
|
|
590
|
-
try {
|
|
599
|
+
try { unlinkAtSync(directory.descriptor, name); } catch { /* acquisition failed closed */ }
|
|
591
600
|
throw error;
|
|
592
601
|
}
|
|
593
602
|
}
|
|
@@ -604,7 +613,7 @@ export class TrackingClickUp {
|
|
|
604
613
|
if (descriptor !== undefined) {
|
|
605
614
|
closeSync(descriptor);
|
|
606
615
|
assertPinnedTrackingDirectory(directory);
|
|
607
|
-
if (readOutboxLock(
|
|
616
|
+
if (readOutboxLock(directory, name)?.token === token) { unlinkAtSync(directory.descriptor, name); fsyncSync(directory.descriptor); }
|
|
608
617
|
}
|
|
609
618
|
} finally {
|
|
610
619
|
closeSync(directory.descriptor);
|