@north-light/crouter 0.3.175 → 0.3.177
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/dist/api/client.d.ts +4 -4
- package/dist/api/client.js +2 -2
- package/dist/api/dto/lifecycle.d.ts +1 -1
- package/dist/api/dto/nodes.d.ts +7 -1
- package/dist/api/dto/profiles.d.ts +3 -0
- package/dist/api/dto/reports.d.ts +1 -1
- package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
- package/dist/clients/attach/config.js +5 -5
- package/dist/clients/attach/input/clipboard-image.js +2 -2
- package/dist/clients/attach/input/ref-autocomplete.js +1 -1
- package/dist/clients/attach/input/titled-editor.d.ts +12 -13
- package/dist/clients/attach/input/titled-editor.js +14 -15
- package/dist/clients/attach/overlays/dialogs.d.ts +1 -1
- package/dist/clients/attach/overlays/dialogs.js +2 -2
- package/dist/clients/attach/overlays/mcp.js +5 -12
- package/dist/clients/attach/render/chat-view.d.ts +1 -1
- package/dist/clients/attach/render/chat-view.js +2 -2
- package/dist/clients/attach/render/crtr-output.d.ts +3 -3
- package/dist/clients/attach/render/crtr-output.js +25 -13
- package/dist/clients/attach/render/group-recap.js +7 -0
- package/dist/clients/attach/session/context.js +3 -3
- package/dist/clients/attach/session/editor-inventory.js +6 -6
- package/dist/clients/attach/session/input-wiring.js +1 -1
- package/dist/clients/attach/session/teardown.js +7 -9
- package/dist/clients/attach/slash/dispatch.d.ts +1 -2
- package/dist/clients/attach/slash/dispatch.js +4 -8
- package/dist/clients/attach/viewer.js +536 -566
- package/dist/clients/inbox/controller.js +3 -2
- package/dist/clients/inbox/deck-adapter.js +3 -2
- package/dist/clients/surfaces/host.d.ts +1 -1
- package/dist/commands/api-client.d.ts +12 -13
- package/dist/commands/api-client.js +15 -16
- package/dist/commands/chord.js +1 -2
- package/dist/commands/human/queue.js +6 -5
- package/dist/commands/memory/find.js +3 -3
- package/dist/commands/node/create.js +7 -3
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node-lifecycle-revive.d.ts +1 -1
- package/dist/commands/node-lifecycle-revive.js +3 -3
- package/dist/commands/profile/list.js +3 -1
- package/dist/commands/profile/new.js +4 -1
- package/dist/commands/profile/project.js +23 -9
- package/dist/commands/profile/show.js +3 -1
- package/dist/commands/profile.js +2 -2
- package/dist/commands/push.js +1 -1
- package/dist/commands/surface/node/placement.d.ts +1 -1
- package/dist/commands/surface/node/placement.js +1 -1
- package/dist/commands/surface-edit.js +1 -4
- package/dist/commands/sys/logs.js +4 -2
- package/dist/commands/sys/panels/panel.d.ts +3 -3
- package/dist/commands/sys/panels/panel.js +3 -3
- package/dist/commands/sys/setup-core.js +1 -7
- package/dist/commands/sys/sync-deps.js +1 -3
- package/dist/commands/sys/sync-project-guidance.js +2 -11
- package/dist/commands/sys/sysprompt.js +1 -3
- package/dist/core/__tests__/branded-host-concurrency.test.js +1 -1
- package/dist/core/__tests__/fixtures/fake-engine.js +1 -1
- package/dist/core/__tests__/helpers/harness.d.ts +1 -1
- package/dist/core/__tests__/helpers/harness.js +1 -1
- package/dist/core/__tests__/on-read-crouter-home-fence.test.js +1 -1
- package/dist/core/__tests__/on-read-dedup-resume.test.js +1 -1
- package/dist/core/canvas/boot.js +1 -1
- package/dist/core/canvas/browse/app.js +2 -2
- package/dist/core/canvas/canvas.d.ts +3 -3
- package/dist/core/canvas/canvas.js +3 -3
- package/dist/core/canvas/daemon-owner.js +1 -2
- package/dist/core/canvas/db.js +7 -8
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +7 -1
- package/dist/core/canvas/index.js +2 -2
- package/dist/core/canvas/pid.d.ts +12 -14
- package/dist/core/canvas/pid.js +16 -20
- package/dist/core/canvas/remote-transport.js +2 -2
- package/dist/core/canvas/status-glyph.d.ts +1 -1
- package/dist/core/canvas/status-glyph.js +1 -1
- package/dist/core/canvas/types.d.ts +1 -1
- package/dist/core/command-manifests/manifest.js +4 -6
- package/dist/core/command-manifests/schema.js +10 -12
- package/dist/core/command-plugins/transport/exec-invoke.js +5 -7
- package/dist/core/command-plugins/transport/http-invoke.js +14 -8
- package/dist/core/command.js +3 -1
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.js +6 -29
- package/dist/core/events/read.js +1 -3
- package/dist/core/exclusive-lock.d.ts +14 -0
- package/dist/core/exclusive-lock.js +124 -0
- package/dist/core/fault-classifier.js +1 -3
- package/dist/core/fs-utils.d.ts +7 -0
- package/dist/core/fs-utils.js +29 -2
- package/dist/core/host-exports/export.d.ts +4 -5
- package/dist/core/host-exports/export.js +4 -5
- package/dist/core/human/claim.js +4 -2
- package/dist/core/human/convention.d.ts +0 -11
- package/dist/core/human/convention.js +1 -131
- package/dist/core/human/scan.js +5 -4
- package/dist/core/io.d.ts +23 -0
- package/dist/core/io.js +60 -0
- package/dist/core/preview-registry.d.ts +10 -5
- package/dist/core/preview-registry.js +136 -2537
- package/dist/core/profiles/manifest.d.ts +13 -2
- package/dist/core/profiles/manifest.js +58 -49
- package/dist/core/review/store.d.ts +4 -1
- package/dist/core/review/store.js +10 -0
- package/dist/core/review/types.d.ts +1 -1
- package/dist/core/review/types.js +1 -1
- package/dist/core/runtime/branded-host.js +6 -84
- package/dist/core/runtime/broker/frame-dispatch.js +4 -4
- package/dist/core/runtime/broker-cli.js +1 -1
- package/dist/core/runtime/broker-extension-render.js +1 -1
- package/dist/core/runtime/broker-protocol.d.ts +9 -10
- package/dist/core/runtime/broker-protocol.js +9 -10
- package/dist/core/runtime/broker-sdk.d.ts +1 -1
- package/dist/core/runtime/broker.js +6 -6
- package/dist/core/runtime/canvas-extensions.d.ts +1 -0
- package/dist/core/runtime/canvas-extensions.js +2 -0
- package/dist/core/runtime/host.js +14 -14
- package/dist/core/runtime/launch.d.ts +1 -1
- package/dist/core/runtime/launch.js +1 -1
- package/dist/core/runtime/lifecycle.js +16 -21
- package/dist/core/runtime/node-read.js +2 -3
- package/dist/core/runtime/package-health.js +1 -8
- package/dist/core/runtime/persona.js +2 -2
- package/dist/core/runtime/pi-vendored.d.ts +4 -2
- package/dist/core/runtime/pi-vendored.js +5 -16
- package/dist/core/runtime/promote.js +1 -1
- package/dist/core/runtime/reopen.js +2 -2
- package/dist/core/runtime/revive-all.d.ts +3 -3
- package/dist/core/runtime/revive-all.js +6 -6
- package/dist/core/runtime/revive.js +1 -1
- package/dist/core/runtime/spawn.d.ts +1 -2
- package/dist/core/runtime/spawn.js +2 -3
- package/dist/core/runtime/structured-output.js +4 -6
- package/dist/core/runtime/tmux-bindings.js +1 -1
- package/dist/core/runtime/tmux-driver.d.ts +2 -2
- package/dist/core/runtime/tmux-driver.js +3 -5
- package/dist/core/runtime/tool-group-summary.js +5 -4
- package/dist/core/runtime/warm-pool.d.ts +9 -8
- package/dist/core/runtime/warm-pool.js +58 -62
- package/dist/core/scope.js +2 -9
- package/dist/core/self-update.js +3 -3
- package/dist/core/spawn.d.ts +0 -1
- package/dist/core/spawn.js +1 -4
- package/dist/core/substrate/index.d.ts +2 -1
- package/dist/core/substrate/index.js +2 -1
- package/dist/core/substrate/on-read-node.d.ts +7 -0
- package/dist/core/substrate/on-read-node.js +37 -0
- package/dist/core/substrate/on-read.d.ts +7 -5
- package/dist/core/substrate/on-read.js +16 -32
- package/dist/core/termrender/termrender.js +16 -96
- package/dist/core/wake.js +2 -2
- package/dist/core/worktree.d.ts +3 -3
- package/dist/core/worktree.js +16 -107
- package/dist/daemon/api/bridge.js +1 -1
- package/dist/daemon/api/handlers/broker-ops.js +1 -2
- package/dist/daemon/api/handlers/messages.js +2 -2
- package/dist/daemon/api/handlers/nodes.js +33 -10
- package/dist/daemon/api/handlers/reports.js +1 -1
- package/dist/daemon/api/handlers/review-comments.js +5 -44
- package/dist/daemon/api/handlers/reviews.js +5 -53
- package/dist/daemon/api/handlers/validate.d.ts +23 -0
- package/dist/daemon/api/handlers/validate.js +58 -0
- package/dist/daemon/api/map.js +1 -0
- package/dist/daemon/cron-run.js +7 -7
- package/dist/daemon/human/finish.js +3 -2
- package/dist/daemon/manage.d.ts +6 -6
- package/dist/daemon/manage.js +15 -16
- package/dist/daemon/review/finish.js +1 -10
- package/dist/pi-extensions/canvas-context-intro.js +1 -14
- package/dist/pi-extensions/canvas-doc-substrate.js +2 -2
- package/dist/pi-extensions/canvas-preview-result.d.ts +22 -0
- package/dist/pi-extensions/canvas-preview-result.js +60 -0
- package/dist/pi-extensions/canvas-review-boundary.js +1 -14
- package/dist/pi-extensions/truncate.d.ts +5 -0
- package/dist/pi-extensions/truncate.js +14 -0
- package/dist/shared/predicates.d.ts +2 -0
- package/dist/shared/predicates.js +4 -0
- package/dist/shared/shell-quote.d.ts +3 -0
- package/dist/shared/shell-quote.js +5 -0
- package/dist/shared/tool-groups.d.ts +4 -2
- package/dist/shared/tool-groups.js +3 -3
- package/dist/types.d.ts +8 -5
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/core/substrate/on-read-pure.d.ts +0 -7
- package/dist/core/substrate/on-read-pure.js +0 -272
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Unified command-manifest schema. Transport is the sole leaf-dialect discriminator.
|
|
2
|
+
import { isRecord } from '../../shared/predicates.js';
|
|
2
3
|
// ---------------------------------------------------------------------------
|
|
3
4
|
// Validation helpers
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
@@ -41,9 +42,6 @@ const OUTPUT_KEYS = new Set(['name', 'type', 'required', 'constraint']);
|
|
|
41
42
|
* may name, so a typo'd lowercase key fails at authoring time rather than
|
|
42
43
|
* silently never resolving. */
|
|
43
44
|
const ENV_VAR_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
44
|
-
function isObject(v) {
|
|
45
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
46
|
-
}
|
|
47
45
|
function typeName(v) {
|
|
48
46
|
if (v === null)
|
|
49
47
|
return 'null';
|
|
@@ -60,7 +58,7 @@ function checkKeys(raw, allowed, path, issue) {
|
|
|
60
58
|
return true;
|
|
61
59
|
}
|
|
62
60
|
function validateRootEntry(raw, path, issue) {
|
|
63
|
-
if (!
|
|
61
|
+
if (!isRecord(raw)) {
|
|
64
62
|
issue('command_node_invalid', `rootEntry must be an object`, typeName(raw), 'a { concept, description, whenToUse } object', 'Fix rootEntry.', path);
|
|
65
63
|
return null;
|
|
66
64
|
}
|
|
@@ -91,7 +89,7 @@ function validateParams(raw, path, issue) {
|
|
|
91
89
|
for (let i = 0; i < raw.length; i++) {
|
|
92
90
|
const p = raw[i];
|
|
93
91
|
const pp = `${path}[${i}]`;
|
|
94
|
-
if (!
|
|
92
|
+
if (!isRecord(p)) {
|
|
95
93
|
issue('command_node_invalid', `param must be an object`, typeName(p), 'a param object', 'Fix the param.', pp);
|
|
96
94
|
return null;
|
|
97
95
|
}
|
|
@@ -289,7 +287,7 @@ function validateOutput(raw, path, issue) {
|
|
|
289
287
|
for (let i = 0; i < raw.length; i++) {
|
|
290
288
|
const f = raw[i];
|
|
291
289
|
const fp = `${path}[${i}]`;
|
|
292
|
-
if (!
|
|
290
|
+
if (!isRecord(f)) {
|
|
293
291
|
issue('command_node_invalid', `output field must be an object`, typeName(f), 'a { name, type, required, constraint } object', 'Fix the output field.', fp);
|
|
294
292
|
return null;
|
|
295
293
|
}
|
|
@@ -337,7 +335,7 @@ function checkCommon(raw, path, issue) {
|
|
|
337
335
|
}
|
|
338
336
|
export function validateCommandNode(raw, path, topLevel, transport, issue) {
|
|
339
337
|
const pathString = path.join('.');
|
|
340
|
-
if (!
|
|
338
|
+
if (!isRecord(raw)) {
|
|
341
339
|
issue('command_node_invalid', 'node must be an object', typeName(raw), 'a branch or leaf node object', 'Fix the node.', pathString);
|
|
342
340
|
return null;
|
|
343
341
|
}
|
|
@@ -424,7 +422,7 @@ function validateBranch(raw, path, topLevel, transport, issue) {
|
|
|
424
422
|
}
|
|
425
423
|
/** `passthrough: { bin, installHint }` — both required non-empty strings. */
|
|
426
424
|
function validatePassthrough(raw, path, issue) {
|
|
427
|
-
if (!
|
|
425
|
+
if (!isRecord(raw)) {
|
|
428
426
|
issue('command_node_invalid', 'passthrough must be an object', typeName(raw), 'an object { bin, installHint }', 'Fix or remove passthrough.', path);
|
|
429
427
|
return null;
|
|
430
428
|
}
|
|
@@ -482,7 +480,7 @@ const HTTP_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
|
482
480
|
const REST_PARAM_PLACEMENT_SET = new Set(['path', 'query', 'body', 'header']);
|
|
483
481
|
function validateRestMapping(raw, basePath, params, issue) {
|
|
484
482
|
const path = `${basePath}.rest`;
|
|
485
|
-
if (!
|
|
483
|
+
if (!isRecord(raw)) {
|
|
486
484
|
issue('command_rest_invalid', `rest must be an object`, typeName(raw), '{ method, path, streaming?, params }', 'Provide a valid rest mapping.', path);
|
|
487
485
|
return null;
|
|
488
486
|
}
|
|
@@ -523,7 +521,7 @@ function validateRestMapping(raw, basePath, params, issue) {
|
|
|
523
521
|
}
|
|
524
522
|
// Validate params mapping (required object)
|
|
525
523
|
const restParams = raw['params'];
|
|
526
|
-
if (!
|
|
524
|
+
if (!isRecord(restParams)) {
|
|
527
525
|
issue('command_rest_invalid', `params must be an object`, typeName(restParams), 'a mapping of param names to placement specs', 'Provide params.', `${path}.params`);
|
|
528
526
|
return null;
|
|
529
527
|
}
|
|
@@ -542,7 +540,7 @@ function validateRestMapping(raw, basePath, params, issue) {
|
|
|
542
540
|
return null;
|
|
543
541
|
}
|
|
544
542
|
declaredNames.add(paramName);
|
|
545
|
-
if (!
|
|
543
|
+
if (!isRecord(mapping)) {
|
|
546
544
|
issue('command_rest_invalid', `param mapping must be an object`, typeName(mapping), '{ in, as? }', 'Fix the param mapping.', `${path}.params.${paramName}`);
|
|
547
545
|
return null;
|
|
548
546
|
}
|
|
@@ -644,7 +642,7 @@ function validateRestMapping(raw, basePath, params, issue) {
|
|
|
644
642
|
return null;
|
|
645
643
|
}
|
|
646
644
|
const rawBody = raw['body'];
|
|
647
|
-
if (!
|
|
645
|
+
if (!isRecord(rawBody)) {
|
|
648
646
|
issue('command_rest_invalid', `body must be an object`, typeName(rawBody), 'a map of constant literal body fields (string | number | boolean)', 'Fix body.', `${path}.body`);
|
|
649
647
|
return null;
|
|
650
648
|
}
|
|
@@ -11,6 +11,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
11
11
|
import { CrtrError } from '../../errors.js';
|
|
12
12
|
import { diag } from '../../io.js';
|
|
13
13
|
import { ExitCode } from '../../../types.js';
|
|
14
|
+
import { isRecord } from '../../../shared/predicates.js';
|
|
14
15
|
const MAX_STDOUT = 10 * 1024 * 1024; // 10 MiB hard cap on plugin stdout.
|
|
15
16
|
const RESERVED_CODES = new Set(['internal', 'unknown_path', 'command_collision', 'plugin_protocol_error']);
|
|
16
17
|
// Strict snake_case: no leading/trailing underscore, no consecutive underscores.
|
|
@@ -77,7 +78,7 @@ export function executeExternalLeaf(spec, input) {
|
|
|
77
78
|
catch {
|
|
78
79
|
throw protocolError(spec, `stdout was not a single JSON envelope`, 'exactly one JSON envelope on stdout');
|
|
79
80
|
}
|
|
80
|
-
if (!
|
|
81
|
+
if (!isRecord(envelope)) {
|
|
81
82
|
throw protocolError(spec, `stdout was not a JSON object`, 'a { protocolVersion: 1, ok: boolean, ... } envelope');
|
|
82
83
|
}
|
|
83
84
|
if (envelope['protocolVersion'] !== 1) {
|
|
@@ -96,7 +97,7 @@ export function executeExternalLeaf(spec, input) {
|
|
|
96
97
|
}
|
|
97
98
|
if (ok) {
|
|
98
99
|
const result = envelope['result'];
|
|
99
|
-
if (!
|
|
100
|
+
if (!isRecord(result)) {
|
|
100
101
|
throw protocolError(spec, `success envelope has no result object`, 'result: an object matching the declared output');
|
|
101
102
|
}
|
|
102
103
|
validateResult(spec, result);
|
|
@@ -104,7 +105,7 @@ export function executeExternalLeaf(spec, input) {
|
|
|
104
105
|
}
|
|
105
106
|
// Recoverable command failure → structured CrtrError with the plugin's code.
|
|
106
107
|
const error = envelope['error'];
|
|
107
|
-
if (!
|
|
108
|
+
if (!isRecord(error) || typeof error['code'] !== 'string' || typeof error['message'] !== 'string') {
|
|
108
109
|
throw protocolError(spec, `error envelope is missing a { code, message }`, 'error: { code, message, ... }');
|
|
109
110
|
}
|
|
110
111
|
const code = error['code'];
|
|
@@ -146,7 +147,7 @@ function typeMatches(type, v) {
|
|
|
146
147
|
if (t === 'array' || t.endsWith('[]'))
|
|
147
148
|
return Array.isArray(v);
|
|
148
149
|
if (t === 'object')
|
|
149
|
-
return
|
|
150
|
+
return isRecord(v);
|
|
150
151
|
return true;
|
|
151
152
|
}
|
|
152
153
|
function protocolError(spec, received, expected) {
|
|
@@ -155,6 +156,3 @@ function protocolError(spec, received, expected) {
|
|
|
155
156
|
function label(spec) {
|
|
156
157
|
return `plugin "${spec.plugin.name}" command \`${spec.commandPath.join(' ')}\``;
|
|
157
158
|
}
|
|
158
|
-
function isObject(v) {
|
|
159
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
160
|
-
}
|
|
@@ -17,8 +17,9 @@ import { request as httpRequest } from 'node:http';
|
|
|
17
17
|
import { request as httpsRequest } from 'node:https';
|
|
18
18
|
import { validateDeclaredResult } from './exec-invoke.js';
|
|
19
19
|
import { CrtrError } from '../../errors.js';
|
|
20
|
-
import { diag, writeStdout } from '../../io.js';
|
|
20
|
+
import { diag, recordPreviewError, recordPreviewJsonLine, writeStdout } from '../../io.js';
|
|
21
21
|
import { ExitCode } from '../../../types.js';
|
|
22
|
+
import { isRecord } from '../../../shared/predicates.js';
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
// Constants
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
@@ -320,7 +321,7 @@ function mapUnary2xx(spec, body) {
|
|
|
320
321
|
catch {
|
|
321
322
|
throw protocolError(spec, 'response body was not valid JSON', 'a JSON object matching the declared output');
|
|
322
323
|
}
|
|
323
|
-
if (!
|
|
324
|
+
if (!isRecord(parsed)) {
|
|
324
325
|
throw protocolError(spec, `response body was not a JSON object (got ${typeName(parsed)})`, 'a JSON object matching the declared output');
|
|
325
326
|
}
|
|
326
327
|
const issue = validateDeclaredResult(spec.output, parsed);
|
|
@@ -420,6 +421,7 @@ async function requestStreaming(spec, plan) {
|
|
|
420
421
|
if (line.trim() === '')
|
|
421
422
|
continue; // drop blank records
|
|
422
423
|
framesEmitted = true;
|
|
424
|
+
recordPreviewJsonLine(line);
|
|
423
425
|
const ok = await writeStdout(line + '\n');
|
|
424
426
|
if (!ok) {
|
|
425
427
|
// Consumer closed the pipe (EPIPE) — stop cleanly, nothing to report.
|
|
@@ -434,6 +436,7 @@ async function requestStreaming(spec, plan) {
|
|
|
434
436
|
// backend sent no trailing delimiter, so none is added).
|
|
435
437
|
if (partial.trim() !== '') {
|
|
436
438
|
framesEmitted = true;
|
|
439
|
+
recordPreviewJsonLine(partial);
|
|
437
440
|
await writeStdout(partial);
|
|
438
441
|
}
|
|
439
442
|
return; // clean EOF → SUCCESS
|
|
@@ -444,7 +447,13 @@ async function requestStreaming(spec, plan) {
|
|
|
444
447
|
if (framesEmitted) {
|
|
445
448
|
// After a frame has been emitted: one concise stderr diagnostic + NETWORK
|
|
446
449
|
// exit, and DO NOT throw (that would append an <error> to stdout).
|
|
447
|
-
|
|
450
|
+
const message = `${label(spec)}: stream interrupted (${detail})`;
|
|
451
|
+
diag(`crtr: ${message}`);
|
|
452
|
+
recordPreviewError({
|
|
453
|
+
error: 'stream_interrupted',
|
|
454
|
+
message,
|
|
455
|
+
next: 'Retry the command to begin a fresh stream.',
|
|
456
|
+
});
|
|
448
457
|
process.exitCode = ExitCode.NETWORK;
|
|
449
458
|
return;
|
|
450
459
|
}
|
|
@@ -474,9 +483,9 @@ function throwNon2xx(spec, status, body) {
|
|
|
474
483
|
catch {
|
|
475
484
|
parsed = undefined;
|
|
476
485
|
}
|
|
477
|
-
if (
|
|
486
|
+
if (isRecord(parsed)) {
|
|
478
487
|
const err = parsed['error'];
|
|
479
|
-
if (
|
|
488
|
+
if (isRecord(err) && typeof err['code'] === 'string' && typeof err['message'] === 'string') {
|
|
480
489
|
const code = err['code'];
|
|
481
490
|
const message = err['message'];
|
|
482
491
|
const field = typeof err['field'] === 'string' ? err['field'] : undefined;
|
|
@@ -535,9 +544,6 @@ function readAll(res) {
|
|
|
535
544
|
res.on('error', () => resolve(Buffer.concat(chunks).toString('utf8'))); // best-effort
|
|
536
545
|
});
|
|
537
546
|
}
|
|
538
|
-
function isObject(v) {
|
|
539
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
540
|
-
}
|
|
541
547
|
function typeName(v) {
|
|
542
548
|
if (v === null)
|
|
543
549
|
return 'null';
|
package/dist/core/command.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// A plain array walk + a small flag parser is ~120 lines and has no surprising
|
|
6
6
|
// edge cases.
|
|
7
7
|
import { renderRoot, renderBranch, renderLeafArgv } from './help.js';
|
|
8
|
-
import { readStdinRaw, peekStdinRaw, emit, handle, setJsonOutput, isJsonOutput } from './io.js';
|
|
8
|
+
import { beginPreview, publishPreviewResult, readStdinRaw, peekStdinRaw, emit, handle, setJsonOutput, isJsonOutput } from './io.js';
|
|
9
9
|
import { renderResult } from './render.js';
|
|
10
10
|
import { CrtrError } from './errors.js';
|
|
11
11
|
import { operationIdContext } from './events/operation-id.js';
|
|
@@ -614,6 +614,7 @@ export async function runCli(root, argv) {
|
|
|
614
614
|
// below: `node` is now a reassignable `let` (the §5.1 retry re-walks it),
|
|
615
615
|
// which TS re-widens to the node union across an `await`.
|
|
616
616
|
const leaf = node;
|
|
617
|
+
beginPreview(path.join(' '));
|
|
617
618
|
const params = leaf.help.params !== undefined ? leaf.help.params : [];
|
|
618
619
|
let providedParams = new Set();
|
|
619
620
|
let contextFilePaths = new Map();
|
|
@@ -628,6 +629,7 @@ export async function runCli(root, argv) {
|
|
|
628
629
|
});
|
|
629
630
|
const context = { providedParams, contextFilePaths };
|
|
630
631
|
const result = await operationIdContext.fresh(() => leaf.run(input, context));
|
|
632
|
+
publishPreviewResult(result);
|
|
631
633
|
if (result !== undefined && result !== null) {
|
|
632
634
|
if (isJsonOutput()) {
|
|
633
635
|
emit(result);
|
package/dist/core/config.d.ts
CHANGED
|
@@ -81,7 +81,7 @@ export declare function readMergedLaunchConfig(targetCwd?: string, targetProfile
|
|
|
81
81
|
* `developer`, or sub-kind, e.g. `plan/reviewers/security`), across
|
|
82
82
|
* project > user > builtin precedence. Returns `undefined` for a kind no
|
|
83
83
|
* scope registers — existence is deliberately NOT validated here (kind
|
|
84
|
-
* existence/launch-menu enumeration is a
|
|
84
|
+
* existence/launch-menu enumeration is a caller concern); this only
|
|
85
85
|
* resolves the config for a kind the caller already knows about. */
|
|
86
86
|
export declare function resolveKindConfig(kind: string): KindConfig | undefined;
|
|
87
87
|
/** The sub-kinds available to spawn FROM a given top-level kind — every
|
package/dist/core/config.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { mkdirSync, rmSync } from 'node:fs';
|
|
2
1
|
import { dirname, join } from 'node:path';
|
|
3
2
|
import { isDeepStrictEqual } from 'node:util';
|
|
4
3
|
import { CONFIG_FILE, STATE_FILE, CONDENSED_HISTORY_MODES, MOUSE_MODE_DEFAULTS, DEFAULT_WHIP_MESSAGES, defaultScopeConfig, defaultScopeState, defaultModelLaddersConfig, defaultKindsConfig, defaultRemoteCanvasConfig } from '../types.js';
|
|
@@ -7,6 +6,7 @@ import { emitEvent } from './events/emit.js';
|
|
|
7
6
|
import { atomicWriteJson, readJsonIfExists, writeJson, ensureDir } from './fs-utils.js';
|
|
8
7
|
import { scopeRoot, requireScopeRoot, findProjectScopeRoots } from './scope.js';
|
|
9
8
|
import { profileRoot } from './profiles/manifest.js';
|
|
9
|
+
import { withExclusiveDirectoryLock } from './exclusive-lock.js';
|
|
10
10
|
import { SCOPE_CONFIG_KEYS } from './user-settings.js';
|
|
11
11
|
import { normalizeWorkingGerunds } from '../shared/working-activity.js';
|
|
12
12
|
function configPathFor(root) {
|
|
@@ -54,34 +54,11 @@ export function readState(scope) {
|
|
|
54
54
|
activeCanvas: existing.activeCanvas ?? null,
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
-
const CONFIG_LOCK_TIMEOUT_MS = 5000;
|
|
58
|
-
const CONFIG_LOCK_POLL_MS = 25;
|
|
59
|
-
function sleepSync(ms) {
|
|
60
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
61
|
-
}
|
|
62
57
|
function withConfigPathLock(path, run) {
|
|
63
58
|
ensureDir(dirname(path));
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
mkdirSync(lockPath);
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
if (error.code !== 'EEXIST')
|
|
73
|
-
throw error;
|
|
74
|
-
if (Date.now() > deadline)
|
|
75
|
-
throw new Error(`timed out waiting for config lock: ${path}`);
|
|
76
|
-
sleepSync(CONFIG_LOCK_POLL_MS);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
try {
|
|
80
|
-
return run(path);
|
|
81
|
-
}
|
|
82
|
-
finally {
|
|
83
|
-
rmSync(lockPath, { recursive: true, force: true });
|
|
84
|
-
}
|
|
59
|
+
return withExclusiveDirectoryLock(`${path}.lock`, () => run(path), {
|
|
60
|
+
timeoutError: () => new Error(`timed out waiting for config lock: ${path}`),
|
|
61
|
+
});
|
|
85
62
|
}
|
|
86
63
|
function withConfigLock(scope, run) {
|
|
87
64
|
const root = requireScopeRoot(scope);
|
|
@@ -583,7 +560,7 @@ export function readMergedLaunchConfig(targetCwd = process.cwd(), targetProfileI
|
|
|
583
560
|
* `developer`, or sub-kind, e.g. `plan/reviewers/security`), across
|
|
584
561
|
* project > user > builtin precedence. Returns `undefined` for a kind no
|
|
585
562
|
* scope registers — existence is deliberately NOT validated here (kind
|
|
586
|
-
* existence/launch-menu enumeration is a
|
|
563
|
+
* existence/launch-menu enumeration is a caller concern); this only
|
|
587
564
|
* resolves the config for a kind the caller already knows about. */
|
|
588
565
|
export function resolveKindConfig(kind) {
|
|
589
566
|
return readMergedLaunchConfig().kinds[kind];
|
|
@@ -645,7 +622,7 @@ function sparseModelLaddersToPersist(rawLadders, baseline, mutated) {
|
|
|
645
622
|
* sparse override stays sparse), and only keys the mutation actually changed
|
|
646
623
|
* are written. `modelLadders` gets sub-field-granular sparse handling via
|
|
647
624
|
* `sparseModelLaddersToPersist`. Result: a fresh bootstrap or any unrelated
|
|
648
|
-
* `sys config set`
|
|
625
|
+
* `sys config set` never freezes a full concrete `modelLadders` to disk,
|
|
649
626
|
* and a direct ladder-rung set still persists only the touched rung -- all
|
|
650
627
|
* through one lock-protected atomic update path. */
|
|
651
628
|
function mutateConfigAtPath(path, mutate) {
|
package/dist/core/events/read.js
CHANGED
|
@@ -4,6 +4,7 @@ import { crtrHome, isSafeNodeId, nodesRoot } from '../canvas/paths.js';
|
|
|
4
4
|
import { validateOperationId } from './operation-id.js';
|
|
5
5
|
import { MAX_EVENT_RECORD_BYTES } from './serialize.js';
|
|
6
6
|
import { errorClassCodes, } from './types.js';
|
|
7
|
+
import { isRecord } from '../../shared/predicates.js';
|
|
7
8
|
const SNAPSHOT_ITEMS = 200;
|
|
8
9
|
const MAX_CANONICAL_SOURCE_BYTES = 6 * 1024 * 1024;
|
|
9
10
|
const MAX_RAW_SOURCE_BYTES = 256 * 1024;
|
|
@@ -41,9 +42,6 @@ const RETRY_DISPOSITIONS = new Set(['auto', 'manual', 'fatal']);
|
|
|
41
42
|
const ERROR_CLASSES = new Set(errorClassCodes);
|
|
42
43
|
const LEVEL_RANK = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
43
44
|
const GENERATION_RANK = { 2: 0, 1: 1, 0: 2 };
|
|
44
|
-
function isRecord(value) {
|
|
45
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
46
|
-
}
|
|
47
45
|
function hasOwn(value, key) {
|
|
48
46
|
return Object.prototype.hasOwnProperty.call(value, key);
|
|
49
47
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ExclusiveLockOptions {
|
|
2
|
+
/** Grace before a lock that names no owner at all is cleared. */
|
|
3
|
+
staleMs?: number;
|
|
4
|
+
timeoutMs?: number;
|
|
5
|
+
/** The error thrown when the wait runs out, so callers keep their own remediation. */
|
|
6
|
+
timeoutError?: () => Error;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Run a short filesystem transition under a token-checked, crash-reclaimable
|
|
10
|
+
* directory lock. A holder that dies releases immediately — the next contender
|
|
11
|
+
* sees a dead pid in the marker name — while a live holder is never stolen from,
|
|
12
|
+
* however long its operation runs.
|
|
13
|
+
*/
|
|
14
|
+
export declare function withExclusiveDirectoryLock<T>(path: string, operation: () => T, options?: ExclusiveLockOptions): T;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
const MARKER_PREFIX = 'owner.';
|
|
4
|
+
const POLL_MS = 10;
|
|
5
|
+
const DEFAULT_STALE_MS = 30_000;
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
7
|
+
function pause(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
|
|
8
|
+
function markerPath(path, token) { return `${path}/${MARKER_PREFIX}${token}`; }
|
|
9
|
+
function observedMarkerToken(path) {
|
|
10
|
+
try {
|
|
11
|
+
return readdirSync(path).find((entry) => entry.startsWith(MARKER_PREFIX))?.slice(MARKER_PREFIX.length) ?? null;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** The token opens with the owner's pid, so liveness reads off the name. */
|
|
18
|
+
function ownerIsAlive(token) {
|
|
19
|
+
const pid = Number(token.split('.', 1)[0]);
|
|
20
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
21
|
+
return false;
|
|
22
|
+
try {
|
|
23
|
+
process.kill(pid, 0);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return error.code === 'EPERM';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function isEmptyDirectory(path) {
|
|
31
|
+
try {
|
|
32
|
+
return readdirSync(path).length === 0;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function ageMs(path) {
|
|
39
|
+
try {
|
|
40
|
+
return Date.now() - statSync(path).mtimeMs;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Reclamation is bound to the exact instance it observed: it unlinks ONLY that
|
|
47
|
+
// token's marker, then removes the now-empty directory with an empty-guarded
|
|
48
|
+
// rmdir. A successor holds a different random token — a different marker name —
|
|
49
|
+
// so a lagging reclaimer that saw the old dead lock can never strip a live
|
|
50
|
+
// successor: its unlink targets a name that no longer exists.
|
|
51
|
+
function reclaimIfOwnerGone(path, staleMs) {
|
|
52
|
+
const token = observedMarkerToken(path);
|
|
53
|
+
if (token !== null) {
|
|
54
|
+
if (ownerIsAlive(token))
|
|
55
|
+
return;
|
|
56
|
+
try {
|
|
57
|
+
unlinkSync(markerPath(path, token));
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error.code === 'ENOENT')
|
|
61
|
+
return;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// Nothing names an owner. A live holder always has its marker, so this is a
|
|
67
|
+
// crash between mkdir and the marker write, or a lock left in some older
|
|
68
|
+
// shape; age is the only evidence, and an empty-guarded rmdir still cannot
|
|
69
|
+
// take a successor that has since claimed the name.
|
|
70
|
+
if (ageMs(path) <= staleMs)
|
|
71
|
+
return;
|
|
72
|
+
if (!isEmptyDirectory(path)) {
|
|
73
|
+
rmSync(path, { recursive: true, force: true });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
rmdirSync(path);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
const code = error.code;
|
|
82
|
+
if (code !== 'ENOENT' && code !== 'ENOTEMPTY' && code !== 'EEXIST')
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function tryAcquire(path, staleMs) {
|
|
87
|
+
const token = `${process.pid}.${randomUUID()}`;
|
|
88
|
+
try {
|
|
89
|
+
mkdirSync(path, { mode: 0o700 });
|
|
90
|
+
writeFileSync(markerPath(path, token), '', { flag: 'wx', mode: 0o600 });
|
|
91
|
+
return { path, token };
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error.code !== 'EEXIST')
|
|
95
|
+
throw error;
|
|
96
|
+
reclaimIfOwnerGone(path, staleMs);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Run a short filesystem transition under a token-checked, crash-reclaimable
|
|
102
|
+
* directory lock. A holder that dies releases immediately — the next contender
|
|
103
|
+
* sees a dead pid in the marker name — while a live holder is never stolen from,
|
|
104
|
+
* however long its operation runs.
|
|
105
|
+
*/
|
|
106
|
+
export function withExclusiveDirectoryLock(path, operation, options = {}) {
|
|
107
|
+
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
108
|
+
const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
109
|
+
let lock = tryAcquire(path, staleMs);
|
|
110
|
+
while (lock === null) {
|
|
111
|
+
if (Date.now() >= deadline) {
|
|
112
|
+
throw options.timeoutError?.() ?? new Error(`timed out waiting for the exclusive lock: ${path}`);
|
|
113
|
+
}
|
|
114
|
+
pause(POLL_MS);
|
|
115
|
+
lock = tryAcquire(path, staleMs);
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
return operation();
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
if (existsSync(markerPath(lock.path, lock.token)))
|
|
122
|
+
rmSync(lock.path, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hasManagedProviderCoolingDiagnostic } from './runtime/managed-provider-cooling.js';
|
|
2
|
+
import { isRecord } from '../shared/predicates.js';
|
|
2
3
|
const errorClassByFaultKind = {
|
|
3
4
|
'rate-limit': 'rate_limit',
|
|
4
5
|
overloaded: 'overloaded',
|
|
@@ -16,9 +17,6 @@ export function errorClassFromFault(fault) {
|
|
|
16
17
|
retry_disposition: fault.retry.disposition,
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
|
-
function isRecord(value) {
|
|
20
|
-
return typeof value === 'object' && value !== null;
|
|
21
|
-
}
|
|
22
20
|
function text(value) {
|
|
23
21
|
return typeof value === 'string' ? value : '';
|
|
24
22
|
}
|
package/dist/core/fs-utils.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
/** `realpathSync`, tolerant of a path that doesn't exist or can't be resolved. */
|
|
2
|
+
export declare function realpathOrSelf(p: string): string;
|
|
3
|
+
/** Expand a leading `~` against `homeDir` (pi's own convention). Other forms
|
|
4
|
+
* — absolute, relative, `~user` — pass through as pi leaves them. */
|
|
5
|
+
export declare function expandTilde(p: string, homeDir?: string): string;
|
|
1
6
|
export declare function ensureDir(dir: string): void;
|
|
2
7
|
export declare function writeJson(path: string, data: unknown): void;
|
|
3
8
|
export declare function atomicWriteJson(path: string, data: unknown): void;
|
|
4
9
|
export declare function atomicWriteText(path: string, content: string | Uint8Array): void;
|
|
5
10
|
export declare function readJson<T = unknown>(path: string): T;
|
|
6
11
|
export declare function readJsonIfExists<T = unknown>(path: string): T | null;
|
|
12
|
+
/** Missing or corrupt reads as absent, so a crash artifact never throws. */
|
|
13
|
+
export declare function readJsonOrNull<T = unknown>(path: string): T | null;
|
|
7
14
|
export declare function readTextIfExists(path: string): string | null;
|
|
8
15
|
export declare function readText(path: string): string;
|
|
9
16
|
export declare function writeText(path: string, content: string): void;
|
package/dist/core/fs-utils.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync, cpSync, readlinkSync, renameSync, chmodSync, } from 'node:fs';
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync, cpSync, readlinkSync, renameSync, chmodSync, realpathSync, } from 'node:fs';
|
|
2
2
|
import { dirname, join, relative } from 'node:path';
|
|
3
|
-
import { platform } from 'node:os';
|
|
3
|
+
import { homedir, platform } from 'node:os';
|
|
4
|
+
/** `realpathSync`, tolerant of a path that doesn't exist or can't be resolved. */
|
|
5
|
+
export function realpathOrSelf(p) {
|
|
6
|
+
try {
|
|
7
|
+
return realpathSync(p);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return p;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Expand a leading `~` against `homeDir` (pi's own convention). Other forms
|
|
14
|
+
* — absolute, relative, `~user` — pass through as pi leaves them. */
|
|
15
|
+
export function expandTilde(p, homeDir = homedir()) {
|
|
16
|
+
if (p === '~')
|
|
17
|
+
return homeDir;
|
|
18
|
+
if (p.startsWith('~/') || (platform() === 'win32' && p.startsWith('~\\')))
|
|
19
|
+
return join(homeDir, p.slice(2));
|
|
20
|
+
return p;
|
|
21
|
+
}
|
|
4
22
|
export function ensureDir(dir) {
|
|
5
23
|
mkdirSync(dir, { recursive: true });
|
|
6
24
|
}
|
|
@@ -44,6 +62,15 @@ export function readJsonIfExists(path) {
|
|
|
44
62
|
return null;
|
|
45
63
|
return readJson(path);
|
|
46
64
|
}
|
|
65
|
+
/** Missing or corrupt reads as absent, so a crash artifact never throws. */
|
|
66
|
+
export function readJsonOrNull(path) {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
47
74
|
export function readTextIfExists(path) {
|
|
48
75
|
if (!existsSync(path))
|
|
49
76
|
return null;
|
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
* generated slash-command templates to `~/.claude/commands` and
|
|
4
4
|
* `~/.pi/agent/prompts`.
|
|
5
5
|
*
|
|
6
|
-
* crouter no
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* user-owned and never removed.
|
|
6
|
+
* crouter exports no Agent Skills / SKILL.md bundles. The export pass prunes
|
|
7
|
+
* marker-bearing `crtr-skills` bundles so hosts stop surfacing pi/Claude native
|
|
8
|
+
* skills. Markerless files are user-owned and never removed.
|
|
10
9
|
*
|
|
11
|
-
* Triggered per-run by cli.ts (argv-gated
|
|
10
|
+
* Triggered per-run by cli.ts (argv-gated) so
|
|
12
11
|
* first-run provisioning is preserved. Best-effort: a provisioning fault must
|
|
13
12
|
* never break an unrelated `crtr` command (the deliberate, pre-existing
|
|
14
13
|
* behavior of the deleted writers).
|
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
* generated slash-command templates to `~/.claude/commands` and
|
|
4
4
|
* `~/.pi/agent/prompts`.
|
|
5
5
|
*
|
|
6
|
-
* crouter no
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* user-owned and never removed.
|
|
6
|
+
* crouter exports no Agent Skills / SKILL.md bundles. The export pass prunes
|
|
7
|
+
* marker-bearing `crtr-skills` bundles so hosts stop surfacing pi/Claude native
|
|
8
|
+
* skills. Markerless files are user-owned and never removed.
|
|
10
9
|
*
|
|
11
|
-
* Triggered per-run by cli.ts (argv-gated
|
|
10
|
+
* Triggered per-run by cli.ts (argv-gated) so
|
|
12
11
|
* first-run provisioning is preserved. Best-effort: a provisioning fault must
|
|
13
12
|
* never break an unrelated `crtr` command (the deliberate, pre-existing
|
|
14
13
|
* behavior of the deleted writers).
|
package/dist/core/human/claim.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { existsSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { hostname } from 'node:os';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { claimPath, deckPath,
|
|
4
|
+
import { claimPath, deckPath, responsePath, reviewPath } from './convention.js';
|
|
5
|
+
import { readJsonOrNull } from '../fs-utils.js';
|
|
6
|
+
import { withExclusiveDirectoryLock } from '../exclusive-lock.js';
|
|
5
7
|
const REMOTE_STALE_MS = 30_000;
|
|
6
8
|
function parseClaim(raw) {
|
|
7
9
|
if (typeof raw !== 'object' || raw === null)
|
|
@@ -11,7 +13,7 @@ function parseClaim(raw) {
|
|
|
11
13
|
return null;
|
|
12
14
|
return { token: claim.token, host: claim.host, pid: claim.pid, claimedAt: claim.claimedAt, heartbeatAt: claim.heartbeatAt, ...(typeof claim.tmuxClient === 'string' ? { tmuxClient: claim.tmuxClient } : {}) };
|
|
13
15
|
}
|
|
14
|
-
export function readTicketClaim(dir) { return parseClaim(
|
|
16
|
+
export function readTicketClaim(dir) { return parseClaim(readJsonOrNull(claimPath(dir))); }
|
|
15
17
|
function isLiveLocalPid(pid) { try {
|
|
16
18
|
process.kill(pid, 0);
|
|
17
19
|
return true;
|
|
@@ -14,17 +14,6 @@ export declare function stampCanvasNode(deck: Deck): void;
|
|
|
14
14
|
export declare function atomicWriteJson(path: string, value: unknown): void;
|
|
15
15
|
/** Publish one immutable JSON record without replacing an existing winner. */
|
|
16
16
|
export declare function publishJsonExclusive(path: string, value: unknown): boolean;
|
|
17
|
-
export declare function readJson<T>(path: string): T | null;
|
|
18
|
-
/** Runs a short filesystem transition under a token-checked, crash-reclaimable directory lock. */
|
|
19
|
-
export declare function withExclusiveDirectoryLock<T>(path: string, operation: () => T, options?: {
|
|
20
|
-
staleMs?: number;
|
|
21
|
-
timeoutMs?: number;
|
|
22
|
-
}): T;
|
|
23
|
-
/** Async counterpart heartbeats while its operation runs, so a valid long handler is never stolen. */
|
|
24
|
-
export declare function withExclusiveDirectoryLockAsync<T>(path: string, operation: () => Promise<T>, options?: {
|
|
25
|
-
staleMs?: number;
|
|
26
|
-
timeoutMs?: number;
|
|
27
|
-
}): Promise<T>;
|
|
28
17
|
export declare function writeResponse(dir: string, responses: InteractionResponse[], completedAt: string, deck?: Deck): string;
|
|
29
18
|
export declare function writeProgress(dir: string, responses: InteractionResponse[]): void;
|
|
30
19
|
export declare function clearProgress(dir: string): void;
|