@north-light/crouter 0.3.175 → 0.3.176

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 (63) hide show
  1. package/dist/api/dto/nodes.d.ts +7 -1
  2. package/dist/api/dto/profiles.d.ts +3 -0
  3. package/dist/clients/attach/render/crtr-output.d.ts +3 -3
  4. package/dist/clients/attach/render/crtr-output.js +25 -13
  5. package/dist/clients/attach/viewer.js +531 -561
  6. package/dist/commands/node/create.js +7 -3
  7. package/dist/commands/profile/list.js +3 -1
  8. package/dist/commands/profile/new.js +4 -1
  9. package/dist/commands/profile/project.js +23 -9
  10. package/dist/commands/profile/show.js +3 -1
  11. package/dist/commands/profile.js +2 -2
  12. package/dist/commands/sys/logs.js +4 -2
  13. package/dist/core/__tests__/branded-host-concurrency.test.js +1 -1
  14. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +1 -1
  15. package/dist/core/__tests__/on-read-dedup-resume.test.js +1 -1
  16. package/dist/core/canvas/extensions.d.ts +1 -1
  17. package/dist/core/canvas/extensions.js +7 -1
  18. package/dist/core/command-plugins/transport/http-invoke.js +10 -2
  19. package/dist/core/command.js +3 -1
  20. package/dist/core/config.js +4 -27
  21. package/dist/core/exclusive-lock.d.ts +14 -0
  22. package/dist/core/exclusive-lock.js +124 -0
  23. package/dist/core/human/claim.js +2 -1
  24. package/dist/core/human/convention.d.ts +0 -10
  25. package/dist/core/human/convention.js +1 -123
  26. package/dist/core/io.d.ts +23 -0
  27. package/dist/core/io.js +60 -0
  28. package/dist/core/preview-registry.d.ts +10 -5
  29. package/dist/core/preview-registry.js +136 -2537
  30. package/dist/core/profiles/manifest.d.ts +12 -1
  31. package/dist/core/profiles/manifest.js +57 -48
  32. package/dist/core/review/store.d.ts +4 -1
  33. package/dist/core/review/store.js +10 -0
  34. package/dist/core/runtime/branded-host.js +6 -84
  35. package/dist/core/runtime/broker-extension-render.js +1 -1
  36. package/dist/core/runtime/canvas-extensions.d.ts +1 -0
  37. package/dist/core/runtime/canvas-extensions.js +2 -0
  38. package/dist/core/runtime/tmux-bindings.js +1 -1
  39. package/dist/core/runtime/warm-pool.d.ts +9 -8
  40. package/dist/core/runtime/warm-pool.js +58 -62
  41. package/dist/core/substrate/index.d.ts +2 -1
  42. package/dist/core/substrate/index.js +2 -1
  43. package/dist/core/substrate/on-read-node.d.ts +7 -0
  44. package/dist/core/substrate/on-read-node.js +37 -0
  45. package/dist/core/substrate/on-read.d.ts +7 -5
  46. package/dist/core/substrate/on-read.js +15 -23
  47. package/dist/core/termrender/termrender.js +16 -96
  48. package/dist/core/worktree.js +11 -100
  49. package/dist/daemon/api/handlers/nodes.js +32 -9
  50. package/dist/daemon/api/handlers/review-comments.js +5 -44
  51. package/dist/daemon/api/handlers/reviews.js +5 -53
  52. package/dist/daemon/api/handlers/validate.d.ts +22 -0
  53. package/dist/daemon/api/handlers/validate.js +59 -0
  54. package/dist/daemon/api/map.js +1 -0
  55. package/dist/daemon/review/finish.js +1 -10
  56. package/dist/pi-extensions/canvas-doc-substrate.js +1 -1
  57. package/dist/pi-extensions/canvas-preview-result.d.ts +22 -0
  58. package/dist/pi-extensions/canvas-preview-result.js +60 -0
  59. package/dist/types.d.ts +4 -0
  60. package/package.json +1 -1
  61. package/runtime.lock.json +2 -2
  62. package/dist/core/substrate/on-read-pure.d.ts +0 -7
  63. package/dist/core/substrate/on-read-pure.js +0 -272
@@ -1,6 +1,5 @@
1
- import { existsSync, linkSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, linkSync, mkdirSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
- import { randomUUID } from 'node:crypto';
4
3
  import { buildSummary } from './summary.js';
5
4
  export function deckPath(dir) { return `${dir}/deck.json`; }
6
5
  export function reviewPath(dir) { return `${dir}/review.json`; }
@@ -62,127 +61,6 @@ export function readJson(path) {
62
61
  return null;
63
62
  }
64
63
  }
65
- function pause(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
66
- // The holder's identity lives in the NAME of its single marker file. The marker's
67
- // mtime is the heartbeat and its presence is ownership, so the observed token, the
68
- // staleness clock, and the reclaim gate are one atomic fact.
69
- const MARKER_PREFIX = 'owner.';
70
- function markerPath(path, token) { return `${path}/${MARKER_PREFIX}${token}`; }
71
- function observedMarkerToken(path) {
72
- try {
73
- return readdirSync(path).find((entry) => entry.startsWith(MARKER_PREFIX))?.slice(MARKER_PREFIX.length) ?? null;
74
- }
75
- catch {
76
- return null;
77
- }
78
- }
79
- function ownsLock(lock) { return existsSync(markerPath(lock.path, lock.token)); }
80
- function releaseDirectoryLock(lock) { if (ownsLock(lock))
81
- rmSync(lock.path, { recursive: true, force: true }); }
82
- function lockAge(path, token) {
83
- const target = token !== null ? markerPath(path, token) : path;
84
- try {
85
- return Date.now() - statSync(existsSync(target) ? target : path).mtimeMs;
86
- }
87
- catch {
88
- return 0;
89
- }
90
- }
91
- // Reclamation is bound to the exact instance it observed: it unlinks ONLY that
92
- // token's marker, then removes the now-empty directory with an empty-guarded
93
- // rmdir. A successor lock holds a different random token (a different marker
94
- // name), so a lagging reclaimer that saw the old stale lock can never strip a
95
- // live successor — its unlink targets a name that no longer exists.
96
- function reclaimIfStale(path, staleMs) {
97
- const token = observedMarkerToken(path);
98
- if (lockAge(path, token) <= staleMs)
99
- return;
100
- if (token !== null) {
101
- try {
102
- unlinkSync(markerPath(path, token));
103
- }
104
- catch (error) {
105
- if (error.code === 'ENOENT')
106
- return;
107
- throw error;
108
- }
109
- }
110
- try {
111
- rmdirSync(path);
112
- }
113
- catch (error) {
114
- const code = error.code;
115
- if (code !== 'ENOENT' && code !== 'ENOTEMPTY' && code !== 'EEXIST')
116
- throw error;
117
- }
118
- }
119
- function tryAcquireDirectoryLock(path, staleMs) {
120
- const token = randomUUID();
121
- try {
122
- mkdirSync(path, { mode: 0o700 });
123
- writeFileSync(markerPath(path, token), '', { flag: 'wx', mode: 0o600 });
124
- return { path, token };
125
- }
126
- catch (error) {
127
- if (error.code !== 'EEXIST')
128
- throw error;
129
- reclaimIfStale(path, staleMs);
130
- return null;
131
- }
132
- }
133
- function acquireDirectoryLock(path, staleMs, timeoutMs) {
134
- const startedAt = Date.now();
135
- while (true) {
136
- const lock = tryAcquireDirectoryLock(path, staleMs);
137
- if (lock !== null)
138
- return lock;
139
- if (Date.now() - startedAt >= timeoutMs)
140
- throw new Error('exclusive operation lock acquisition timed out');
141
- pause(5);
142
- }
143
- }
144
- async function acquireDirectoryLockAsync(path, staleMs, timeoutMs) {
145
- const startedAt = Date.now();
146
- while (true) {
147
- const lock = tryAcquireDirectoryLock(path, staleMs);
148
- if (lock !== null)
149
- return lock;
150
- if (Date.now() - startedAt >= timeoutMs)
151
- throw new Error('exclusive operation lock acquisition timed out');
152
- await new Promise((resolvePromise) => setTimeout(resolvePromise, 10));
153
- }
154
- }
155
- /** Runs a short filesystem transition under a token-checked, crash-reclaimable directory lock. */
156
- export function withExclusiveDirectoryLock(path, operation, options = {}) {
157
- const lock = acquireDirectoryLock(path, options.staleMs ?? 30_000, options.timeoutMs ?? 5_000);
158
- try {
159
- return operation();
160
- }
161
- finally {
162
- releaseDirectoryLock(lock);
163
- }
164
- }
165
- /** Async counterpart heartbeats while its operation runs, so a valid long handler is never stolen. */
166
- export async function withExclusiveDirectoryLockAsync(path, operation, options = {}) {
167
- const staleMs = options.staleMs ?? 35_000;
168
- const lock = await acquireDirectoryLockAsync(path, staleMs, options.timeoutMs ?? staleMs + 5_000);
169
- const heartbeat = setInterval(() => {
170
- if (ownsLock(lock)) {
171
- try {
172
- utimesSync(markerPath(lock.path, lock.token), new Date(), new Date());
173
- }
174
- catch { /* a reclaimed lock is no longer ours */ }
175
- }
176
- }, 1_000);
177
- try {
178
- return await operation();
179
- }
180
- finally {
181
- clearInterval(heartbeat);
182
- releaseDirectoryLock(lock);
183
- }
184
- }
185
- // Kept for the existing panel until H2 routes all finalization through tickets.ts.
186
64
  export function writeResponse(dir, responses, completedAt, deck) {
187
65
  const summary = deck === undefined ? '' : buildSummary(deck, responses);
188
66
  atomicWriteJson(responsePath(dir), { schema: 'humanloop.response/v2', kind: 'deck', responses, summary, completedAt });
package/dist/core/io.d.ts CHANGED
@@ -1,6 +1,29 @@
1
1
  import { CrtrError } from './errors.js';
2
2
  import { type ExitCodeValue } from '../types.js';
3
3
  import { ApiError } from '../api/index.js';
4
+ /** The private result mirror for one crtr CLI invocation launched by the
5
+ * canvas bash-preview extension. This deliberately never enters stdout: bash
6
+ * text is model context, while Pi tool-result details are viewer-only. */
7
+ export interface CrtrPreviewRecord {
8
+ path: string;
9
+ result?: Record<string, unknown>;
10
+ jsonl?: Record<string, unknown>[];
11
+ error?: ErrorPayload;
12
+ }
13
+ /** Begin collecting the current leaf's structured response, if its caller
14
+ * supplied the private per-tool-call result path. */
15
+ export declare function beginPreview(path: string): void;
16
+ /** Publish the single-object or collected JSONL result after leaf dispatch. */
17
+ export declare function publishPreviewResult(result: Record<string, unknown> | void): void;
18
+ /** Mirror a structured record whose caller renders it itself instead of using
19
+ * the generic dispatcher (JSONL and event-stream leaves). */
20
+ export declare function recordPreviewLine(obj: Record<string, unknown>): void;
21
+ /** Preserve a verbatim JSONL stream's structured frames without changing the
22
+ * bytes written to stdout. Invalid third-party lines remain raw-only. */
23
+ export declare function recordPreviewJsonLine(line: string): void;
24
+ /** Record a terminal streaming failure that correctly remains off stdout
25
+ * because prior frames have already been delivered. */
26
+ export declare function recordPreviewError(payload: ErrorPayload): void;
4
27
  /** Set by the dispatcher when `--json` is present anywhere in argv. */
5
28
  export declare function setJsonOutput(v: boolean): void;
6
29
  /** True when the caller asked for raw JSON instead of rendered prose. */
package/dist/core/io.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // the model, not data it parses); structured errors; stderr is diagnostics only
4
4
  // and never carries the result. The raw JSON object is available behind the
5
5
  // `--json` global for tooling. See the cli-design reference.
6
+ import { renameSync, writeFileSync } from 'node:fs';
6
7
  import { CrtrError } from './errors.js';
7
8
  import { ExitCode } from '../types.js';
8
9
  import { renderError } from './render.js';
@@ -11,6 +12,62 @@ import { ApiError } from '../api/index.js';
11
12
  // output mode — prose (default) vs raw JSON (--json global, for tooling)
12
13
  // ---------------------------------------------------------------------------
13
14
  let jsonOutput = false;
15
+ let preview;
16
+ /** Begin collecting the current leaf's structured response, if its caller
17
+ * supplied the private per-tool-call result path. */
18
+ export function beginPreview(path) {
19
+ const resultPath = process.env['CRTR_PREVIEW_RESULT_PATH'];
20
+ preview = resultPath === undefined || resultPath === '' ? undefined : { path, jsonl: [] };
21
+ }
22
+ function publishPreview(record) {
23
+ const resultPath = process.env['CRTR_PREVIEW_RESULT_PATH'];
24
+ if (resultPath === undefined || resultPath === '')
25
+ return;
26
+ const tempPath = `${resultPath}.${process.pid}.tmp`;
27
+ try {
28
+ writeFileSync(tempPath, `${JSON.stringify({ path: preview?.path ?? '', ...record })}\n`, 'utf8');
29
+ renameSync(tempPath, resultPath);
30
+ }
31
+ catch {
32
+ // Preview transport is optional UI metadata. Its failure must never alter
33
+ // the CLI's stdout/error contract.
34
+ try {
35
+ writeFileSync(tempPath, '', 'utf8');
36
+ }
37
+ catch { /* nothing to clean */ }
38
+ }
39
+ }
40
+ /** Publish the single-object or collected JSONL result after leaf dispatch. */
41
+ export function publishPreviewResult(result) {
42
+ if (preview === undefined)
43
+ return;
44
+ if (result !== undefined && result !== null)
45
+ publishPreview({ result });
46
+ else if (preview.jsonl.length > 0 || preview.error !== undefined) {
47
+ publishPreview({ ...(preview.jsonl.length > 0 ? { jsonl: preview.jsonl } : {}), ...(preview.error === undefined ? {} : { error: preview.error }) });
48
+ }
49
+ }
50
+ /** Mirror a structured record whose caller renders it itself instead of using
51
+ * the generic dispatcher (JSONL and event-stream leaves). */
52
+ export function recordPreviewLine(obj) {
53
+ preview?.jsonl.push(obj);
54
+ }
55
+ /** Preserve a verbatim JSONL stream's structured frames without changing the
56
+ * bytes written to stdout. Invalid third-party lines remain raw-only. */
57
+ export function recordPreviewJsonLine(line) {
58
+ try {
59
+ const value = JSON.parse(line);
60
+ if (value !== null && typeof value === 'object' && !Array.isArray(value))
61
+ recordPreviewLine(value);
62
+ }
63
+ catch { /* a malformed remote frame remains the command's raw truth */ }
64
+ }
65
+ /** Record a terminal streaming failure that correctly remains off stdout
66
+ * because prior frames have already been delivered. */
67
+ export function recordPreviewError(payload) {
68
+ if (preview !== undefined)
69
+ preview.error = payload;
70
+ }
14
71
  /** Set by the dispatcher when `--json` is present anywhere in argv. */
15
72
  export function setJsonOutput(v) {
16
73
  jsonOutput = v;
@@ -88,6 +145,7 @@ export function emit(obj) {
88
145
  }
89
146
  /** One JSONL record. Call per event in a stream; partial reads stay parseable. */
90
147
  export function emitLine(obj) {
148
+ recordPreviewLine(obj);
91
149
  process.stdout.write(JSON.stringify(obj) + '\n');
92
150
  }
93
151
  /**
@@ -234,6 +292,8 @@ export function handle(e) {
234
292
  }
235
293
  if (e instanceof CrtrError) {
236
294
  const payload = payloadOf(e);
295
+ if (preview !== undefined)
296
+ publishPreview({ error: payload });
237
297
  const out = jsonOutput
238
298
  ? JSON.stringify(payload, null, 2)
239
299
  : renderError(payload);
@@ -23,8 +23,13 @@ export interface SummaryLine {
23
23
  text: string;
24
24
  tone?: SummaryTone;
25
25
  }
26
- /** Dispatch a command's argv tokens + its raw result text to the summarizer
27
- * that owns that leaf path (longest-registered-prefix match, same shape as
28
- * suppressOutputForPath), or null when no command owns a summary — the
29
- * caller falls through to generic truncation in that case. */
30
- export declare function summarizePath(tokens: readonly string[], rawText: string): SummaryLine[] | null;
26
+ export interface CrtrPreview {
27
+ path: string;
28
+ result?: Record<string, unknown>;
29
+ jsonl?: Record<string, unknown>[];
30
+ error?: Record<string, unknown>;
31
+ }
32
+ /** Summarize an exact CLI result record. The recorded path is authoritative:
33
+ * shell tokenization is used only for an invocation's static icon/help chrome,
34
+ * never to identify or decode the returned leaf data. */
35
+ export declare function summarizePath(preview: CrtrPreview, tokens?: readonly string[]): SummaryLine[] | null;