@north-light/crouter 0.3.217 → 0.3.219

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 (74) hide show
  1. package/dist/api/client.d.ts +5 -2
  2. package/dist/api/client.js +7 -1
  3. package/dist/api/dto/canvas.d.ts +9 -0
  4. package/dist/api/dto/nodes.d.ts +27 -1
  5. package/dist/builtin-memory/04-base-worker.md +7 -1
  6. package/dist/builtin-memory/internal/plugins.md +67 -0
  7. package/dist/builtin-memory/memory-read-orientation.md +13 -0
  8. package/dist/clients/attach/chrome/bash-jobs.js +1 -1
  9. package/dist/clients/attach/photon_rs_bg.wasm +0 -0
  10. package/dist/clients/attach/viewer.js +478 -478
  11. package/dist/commands/human/shared.js +1 -1
  12. package/dist/commands/memory/edit.js +6 -1
  13. package/dist/commands/memory/lint.d.ts +1 -6
  14. package/dist/commands/memory/lint.js +18 -126
  15. package/dist/commands/memory/list.d.ts +1 -0
  16. package/dist/commands/memory/list.js +17 -2
  17. package/dist/commands/memory/read.js +15 -6
  18. package/dist/commands/memory/shared.d.ts +32 -2
  19. package/dist/commands/memory/shared.js +179 -0
  20. package/dist/commands/memory/write.js +6 -1
  21. package/dist/commands/pkg/browse/catalog.js +2 -0
  22. package/dist/commands/pkg/browse/model.d.ts +4 -1
  23. package/dist/commands/pkg/plugin-manage.js +209 -148
  24. package/dist/core/bash-jobs.d.ts +2 -5
  25. package/dist/core/bash-jobs.js +4 -8
  26. package/dist/core/command-plugins/bundle.d.ts +4 -1
  27. package/dist/core/command-plugins/bundle.js +16 -2
  28. package/dist/core/human/component-docs.js +1 -0
  29. package/dist/core/human/scan.d.ts +5 -4
  30. package/dist/core/human/scan.js +7 -4
  31. package/dist/core/io.js +3 -2
  32. package/dist/core/manifest.d.ts +2 -0
  33. package/dist/core/manifest.js +5 -0
  34. package/dist/core/memory/extensions.d.ts +30 -0
  35. package/dist/core/memory/extensions.js +219 -0
  36. package/dist/core/preview-result-path.d.ts +4 -0
  37. package/dist/core/preview-result-path.js +25 -0
  38. package/dist/core/runtime/launch-config-worker.d.ts +1 -0
  39. package/dist/core/runtime/launch-config-worker.js +12 -0
  40. package/dist/core/runtime/launch.d.ts +16 -2
  41. package/dist/core/runtime/launch.js +55 -21
  42. package/dist/core/runtime/promote.d.ts +20 -2
  43. package/dist/core/runtime/promote.js +63 -59
  44. package/dist/core/runtime/recycle.js +2 -2
  45. package/dist/core/runtime/reset.js +2 -2
  46. package/dist/core/runtime/spawn.js +3 -3
  47. package/dist/core/runtime/warm-pool.js +9 -7
  48. package/dist/core/substrate/frontmatter-validation.d.ts +13 -0
  49. package/dist/core/substrate/frontmatter-validation.js +101 -0
  50. package/dist/core/substrate/index.d.ts +1 -0
  51. package/dist/core/substrate/index.js +1 -0
  52. package/dist/daemon/api/__tests__/bridge-heartbeat.test.d.ts +1 -0
  53. package/dist/daemon/api/__tests__/bridge-heartbeat.test.js +30 -0
  54. package/dist/daemon/api/__tests__/nodes-activity-query.test.d.ts +1 -0
  55. package/dist/daemon/api/__tests__/nodes-activity-query.test.js +101 -0
  56. package/dist/daemon/api/__tests__/seam/api-server.test.js +30 -0
  57. package/dist/daemon/api/bridge.d.ts +6 -0
  58. package/dist/daemon/api/bridge.js +39 -2
  59. package/dist/daemon/api/handlers/canvas.js +3 -0
  60. package/dist/daemon/api/handlers/inbox.js +4 -3
  61. package/dist/daemon/api/handlers/nodes.d.ts +1 -0
  62. package/dist/daemon/api/handlers/nodes.js +117 -36
  63. package/dist/daemon/api/handlers/reports.d.ts +4 -0
  64. package/dist/daemon/api/handlers/reports.js +14 -8
  65. package/dist/daemon/crtrd.js +7 -5
  66. package/dist/daemon/manage.d.ts +3 -0
  67. package/dist/daemon/manage.js +14 -0
  68. package/dist/pi-extensions/canvas-bash-valve.d.ts +4 -3
  69. package/dist/pi-extensions/canvas-bash-valve.js +19 -6
  70. package/dist/pi-extensions/canvas-preview-result.d.ts +0 -8
  71. package/dist/pi-extensions/canvas-preview-result.js +9 -23
  72. package/dist/types.d.ts +31 -0
  73. package/package.json +1 -1
  74. package/runtime.lock.json +2 -2
@@ -2,4 +2,6 @@ import type { MarketplaceManifest, PluginManifest } from '../types.js';
2
2
  export declare function pluginManifestPath(pluginRoot: string): string;
3
3
  export declare function marketplaceManifestPath(mktRoot: string): string;
4
4
  export declare function readPluginManifest(pluginRoot: string): PluginManifest | null;
5
+ /** Validate an optional memory-extension declaration at package preflight. */
6
+ export declare function validatePluginManifestMemoryExtensions(manifest: PluginManifest): void;
5
7
  export declare function readMarketplaceManifest(mktRoot: string): MarketplaceManifest | null;
@@ -1,5 +1,6 @@
1
1
  import { join } from 'node:path';
2
2
  import { MARKETPLACE_MANIFEST_DIR, MARKETPLACE_MANIFEST_FILE, PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE, } from '../types.js';
3
+ import { validatePluginMemoryExtensions } from './memory/extensions.js';
3
4
  import { readJsonIfExists } from './fs-utils.js';
4
5
  export function pluginManifestPath(pluginRoot) {
5
6
  return join(pluginRoot, PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE);
@@ -10,6 +11,10 @@ export function marketplaceManifestPath(mktRoot) {
10
11
  export function readPluginManifest(pluginRoot) {
11
12
  return readJsonIfExists(pluginManifestPath(pluginRoot));
12
13
  }
14
+ /** Validate an optional memory-extension declaration at package preflight. */
15
+ export function validatePluginManifestMemoryExtensions(manifest) {
16
+ validatePluginMemoryExtensions(manifest);
17
+ }
13
18
  export function readMarketplaceManifest(mktRoot) {
14
19
  return readJsonIfExists(marketplaceManifestPath(mktRoot));
15
20
  }
@@ -0,0 +1,30 @@
1
+ import type { MemoryExtensionDeclarations, MemoryExtensionScalar, PluginManifest } from '../../types.js';
2
+ import type { MemoryDoc } from '../memory-resolver.js';
3
+ export type MemoryExtensionCatalog = Record<string, MemoryExtensionDeclarations>;
4
+ export interface MemoryExtensionValidationIssue {
5
+ path: string;
6
+ message: string;
7
+ }
8
+ export interface MemoryExtensionCatalogOptions {
9
+ /** The manifest currently being staged. Its namespace shadows an installed copy only for that candidate validation. */
10
+ candidate?: Pick<PluginManifest, 'name' | 'memory_extensions'>;
11
+ }
12
+ export declare function isMemoryExtensionScalar(value: unknown): value is MemoryExtensionScalar;
13
+ /** Parse the closed declaration addendum. Callers that accept a plugin manifest
14
+ * invoke this at their install/preflight boundary; normal runtime catalogs only
15
+ * admit declarations which have already cleared that boundary. */
16
+ export declare function validateMemoryExtensionDeclarations(raw: unknown): MemoryExtensionDeclarations;
17
+ /** Throw when a manifest carries an invalid addendum; absent is valid. */
18
+ export declare function validatePluginMemoryExtensions(manifest: Pick<PluginManifest, 'memory_extensions'>): void;
19
+ /** Installed declarations used for validation and authoring. Disabled plugins
20
+ * remain present so existing values can still be checked and edited. */
21
+ export declare function memoryExtensionValidationCatalog(doc: Pick<MemoryDoc, 'scope' | 'path'>, options?: MemoryExtensionCatalogOptions): MemoryExtensionCatalog;
22
+ /** Winning enabled declarations used only to interpret structured output. */
23
+ export declare function memoryExtensionEffectiveCatalog(doc: Pick<MemoryDoc, 'scope' | 'path'>): MemoryExtensionCatalog;
24
+ /** Validate the raw `extensions` frontmatter value. Unresolved namespaces are
25
+ * intentionally reported rather than discarded: removal leaves raw metadata
26
+ * intact but lint-invalid until its plugin returns. */
27
+ export declare function validateMemoryExtensionValues(raw: unknown, catalog: MemoryExtensionCatalog): MemoryExtensionValidationIssue[];
28
+ /** Effective plugin metadata for structured consumers. It overlays only valid
29
+ * explicit scalars onto defaults and never mutates or serializes frontmatter. */
30
+ export declare function projectEffectiveMemoryExtensions(raw: unknown, catalog: MemoryExtensionCatalog): Record<string, Record<string, MemoryExtensionScalar>>;
@@ -0,0 +1,219 @@
1
+ import { sep } from 'node:path';
2
+ import { listInstalledPlugins, listInstalledPluginsInRoot } from '../resolver.js';
3
+ const LOCAL_FIELD_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
4
+ const DECLARATION_KEYS = new Set(['type', 'values', 'default', 'write_help', 'edit_help']);
5
+ function isRecord(value) {
6
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
7
+ }
8
+ export function isMemoryExtensionScalar(value) {
9
+ return typeof value === 'string' || typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value));
10
+ }
11
+ function declarationError(path, message) {
12
+ return new Error(`invalid memory_extensions declaration at ${path}: ${message}`);
13
+ }
14
+ function validHelp(value) {
15
+ return typeof value === 'string' && value.trim() !== '' && !/[\r\n]/.test(value);
16
+ }
17
+ function validDefault(declaration, value) {
18
+ if (!isMemoryExtensionScalar(value))
19
+ return false;
20
+ if (declaration.type === 'enum')
21
+ return typeof value === 'string' && declaration.values.includes(value);
22
+ return typeof value === declaration.type;
23
+ }
24
+ /** Parse the closed declaration addendum. Callers that accept a plugin manifest
25
+ * invoke this at their install/preflight boundary; normal runtime catalogs only
26
+ * admit declarations which have already cleared that boundary. */
27
+ export function validateMemoryExtensionDeclarations(raw) {
28
+ if (!isRecord(raw))
29
+ throw declarationError('memory_extensions', 'expected a non-empty object');
30
+ const declarations = {};
31
+ const fields = Object.entries(raw);
32
+ if (fields.length === 0)
33
+ throw declarationError('memory_extensions', 'must not be empty');
34
+ for (const [field, value] of fields) {
35
+ const path = `memory_extensions.${field}`;
36
+ if (!LOCAL_FIELD_NAME.test(field)) {
37
+ throw declarationError(path, 'field names must be lowercase kebab-case');
38
+ }
39
+ if (!isRecord(value))
40
+ throw declarationError(path, 'expected a declaration object');
41
+ for (const key of Object.keys(value)) {
42
+ if (!DECLARATION_KEYS.has(key))
43
+ throw declarationError(path, `unknown key \`${key}\``);
44
+ }
45
+ const type = value['type'];
46
+ if (type !== 'boolean' && type !== 'string' && type !== 'number' && type !== 'enum') {
47
+ throw declarationError(`${path}.type`, 'expected boolean, string, number, or enum');
48
+ }
49
+ if (!validHelp(value['write_help']))
50
+ throw declarationError(`${path}.write_help`, 'expected a nonblank single-line string');
51
+ if (!validHelp(value['edit_help']))
52
+ throw declarationError(`${path}.edit_help`, 'expected a nonblank single-line string');
53
+ if (type === 'enum') {
54
+ const values = value['values'];
55
+ if (!Array.isArray(values) || values.length === 0 || !values.every((entry) => typeof entry === 'string' && entry !== '')) {
56
+ throw declarationError(`${path}.values`, 'expected a non-empty string list');
57
+ }
58
+ if (new Set(values).size !== values.length)
59
+ throw declarationError(`${path}.values`, 'values must be unique');
60
+ const declaration = {
61
+ type,
62
+ values: [...values],
63
+ write_help: value['write_help'],
64
+ edit_help: value['edit_help'],
65
+ };
66
+ if ('default' in value) {
67
+ if (!validDefault(declaration, value['default'])) {
68
+ throw declarationError(`${path}.default`, 'must be one of the declared enum values');
69
+ }
70
+ declaration.default = value['default'];
71
+ }
72
+ declarations[field] = declaration;
73
+ continue;
74
+ }
75
+ if ('values' in value)
76
+ throw declarationError(`${path}.values`, 'is allowed only for enum declarations');
77
+ const declaration = {
78
+ type,
79
+ write_help: value['write_help'],
80
+ edit_help: value['edit_help'],
81
+ };
82
+ if ('default' in value) {
83
+ if (!validDefault(declaration, value['default'])) {
84
+ throw declarationError(`${path}.default`, `must be a ${type}`);
85
+ }
86
+ declaration.default = value['default'];
87
+ }
88
+ declarations[field] = declaration;
89
+ }
90
+ return declarations;
91
+ }
92
+ /** Throw when a manifest carries an invalid addendum; absent is valid. */
93
+ export function validatePluginMemoryExtensions(manifest) {
94
+ if (manifest.memory_extensions !== undefined)
95
+ validateMemoryExtensionDeclarations(manifest.memory_extensions);
96
+ }
97
+ function declarationsFromManifest(manifest, strict) {
98
+ if (manifest.memory_extensions === undefined)
99
+ return undefined;
100
+ try {
101
+ return validateMemoryExtensionDeclarations(manifest.memory_extensions);
102
+ }
103
+ catch (error) {
104
+ if (strict)
105
+ throw error;
106
+ return undefined;
107
+ }
108
+ }
109
+ function catalogForPlugins(groups, enabledOnly, candidate) {
110
+ const catalog = {};
111
+ const shadowed = new Set();
112
+ if (candidate !== undefined) {
113
+ // A candidate that removes its addendum must still hide an installed copy
114
+ // while that package's own documents are being validated.
115
+ shadowed.add(candidate.name);
116
+ const declarations = declarationsFromManifest(candidate, true);
117
+ if (declarations !== undefined)
118
+ catalog[candidate.name] = declarations;
119
+ }
120
+ for (const plugins of groups) {
121
+ for (const plugin of plugins) {
122
+ if (enabledOnly && !plugin.enabled)
123
+ continue;
124
+ const namespace = plugin.manifest.name;
125
+ if (shadowed.has(namespace) || catalog[namespace] !== undefined)
126
+ continue;
127
+ const declarations = declarationsFromManifest(plugin.manifest, false);
128
+ if (declarations !== undefined)
129
+ catalog[namespace] = declarations;
130
+ }
131
+ }
132
+ return catalog;
133
+ }
134
+ /** Declarations for a native document. Project documents use their owning
135
+ * project scope only, then user scope; profile/node/user docs use user scope. */
136
+ function pluginGroupsForDocument(doc) {
137
+ if (doc.scope === 'project') {
138
+ const marker = `${sep}.crouter${sep}`;
139
+ const markerIndex = doc.path.lastIndexOf(marker);
140
+ if (markerIndex >= 0) {
141
+ const root = doc.path.slice(0, markerIndex + `${sep}.crouter`.length);
142
+ return [listInstalledPluginsInRoot('project', root), listInstalledPlugins('user')];
143
+ }
144
+ }
145
+ return [listInstalledPlugins('user')];
146
+ }
147
+ /** Installed declarations used for validation and authoring. Disabled plugins
148
+ * remain present so existing values can still be checked and edited. */
149
+ export function memoryExtensionValidationCatalog(doc, options = {}) {
150
+ return catalogForPlugins(pluginGroupsForDocument(doc), false, options.candidate);
151
+ }
152
+ /** Winning enabled declarations used only to interpret structured output. */
153
+ export function memoryExtensionEffectiveCatalog(doc) {
154
+ return catalogForPlugins(pluginGroupsForDocument(doc), true);
155
+ }
156
+ /** Validate the raw `extensions` frontmatter value. Unresolved namespaces are
157
+ * intentionally reported rather than discarded: removal leaves raw metadata
158
+ * intact but lint-invalid until its plugin returns. */
159
+ export function validateMemoryExtensionValues(raw, catalog) {
160
+ if (raw === undefined)
161
+ return [];
162
+ if (!isRecord(raw))
163
+ return [{ path: 'extensions', message: 'expected a mapping' }];
164
+ const issues = [];
165
+ for (const [namespace, fields] of Object.entries(raw)) {
166
+ const namespacePath = `extensions.${namespace}`;
167
+ if (!isRecord(fields)) {
168
+ issues.push({ path: namespacePath, message: 'expected a mapping' });
169
+ continue;
170
+ }
171
+ const declarations = catalog[namespace];
172
+ if (declarations === undefined) {
173
+ issues.push({ path: namespacePath, message: 'unresolved extension namespace' });
174
+ for (const [field, value] of Object.entries(fields)) {
175
+ if (!isMemoryExtensionScalar(value)) {
176
+ issues.push({ path: `${namespacePath}.${field}`, message: 'expected a string, boolean, or finite number' });
177
+ }
178
+ }
179
+ continue;
180
+ }
181
+ for (const [field, value] of Object.entries(fields)) {
182
+ const path = `${namespacePath}.${field}`;
183
+ if (!isMemoryExtensionScalar(value)) {
184
+ issues.push({ path, message: 'expected a string, boolean, or finite number' });
185
+ continue;
186
+ }
187
+ const declaration = declarations[field];
188
+ if (declaration === undefined) {
189
+ issues.push({ path, message: 'undeclared extension field' });
190
+ continue;
191
+ }
192
+ if (!validDefault(declaration, value)) {
193
+ const expected = declaration.type === 'enum' ? `one of ${declaration.values.join('|')}` : declaration.type;
194
+ issues.push({ path, message: `expected ${expected}` });
195
+ }
196
+ }
197
+ }
198
+ return issues;
199
+ }
200
+ /** Effective plugin metadata for structured consumers. It overlays only valid
201
+ * explicit scalars onto defaults and never mutates or serializes frontmatter. */
202
+ export function projectEffectiveMemoryExtensions(raw, catalog) {
203
+ const source = isRecord(raw) ? raw : {};
204
+ const effective = {};
205
+ for (const [namespace, declarations] of Object.entries(catalog)) {
206
+ const values = {};
207
+ const explicit = isRecord(source[namespace]) ? source[namespace] : {};
208
+ for (const [field, declaration] of Object.entries(declarations)) {
209
+ if (declaration.default !== undefined)
210
+ values[field] = declaration.default;
211
+ const value = explicit[field];
212
+ if (validDefault(declaration, value))
213
+ values[field] = value;
214
+ }
215
+ if (Object.keys(values).length > 0)
216
+ effective[namespace] = values;
217
+ }
218
+ return effective;
219
+ }
@@ -0,0 +1,4 @@
1
+ export declare const PREVIEW_RESULT_PATH_ENV = "CRTR_PREVIEW_RESULT_PATH";
2
+ /** Deterministic from the tool call id, so the spawning side and the result
3
+ * side derive the same path without sharing state. */
4
+ export declare function previewResultPath(toolCallId: string): string;
@@ -0,0 +1,25 @@
1
+ // The private structured-result side channel between a crtr CLI invocation and
2
+ // the canvas broker that spawned it: the broker names one file per bash tool
3
+ // call, the leaf mirrors its record there, and the broker attaches the record
4
+ // to Pi's ToolResult.details.
5
+ //
6
+ // The path travels in the child process environment. It must never be spliced
7
+ // into the command text — the executed script has to stay exactly what the
8
+ // agent wrote, because `cmd.sh`, job inspection, and the doc substrate's
9
+ // `command` surfaces all read that text as the agent's own words.
10
+ import { tmpdir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ export const PREVIEW_RESULT_PATH_ENV = 'CRTR_PREVIEW_RESULT_PATH';
13
+ /** A tool call id is unique only within one broker: some provider adapters
14
+ * synthesize a missing id from a process-local counter and a millisecond
15
+ * clock, so two brokers can mint the same one and collide on a shared file.
16
+ * Node id and pid are equal across both sides of the transport (they run in
17
+ * the same broker process) without either side sharing state. */
18
+ function brokerToken() {
19
+ return `${encodeURIComponent(process.env['CRTR_NODE_ID'] ?? 'no-node')}-${process.pid}`;
20
+ }
21
+ /** Deterministic from the tool call id, so the spawning side and the result
22
+ * side derive the same path without sharing state. */
23
+ export function previewResultPath(toolCallId) {
24
+ return join(tmpdir(), 'crouter-preview-results', `${brokerToken()}-${encodeURIComponent(toolCallId)}.json`);
25
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ // Runs the synchronous configuration resolver outside crtrd's process. A
2
+ // network filesystem can block a native read indefinitely; unlike a worker
3
+ // thread, this subprocess can be killed at the launch-config timeout.
4
+ import { readMergedLaunchConfig } from '../config.js';
5
+ process.once('message', (message) => {
6
+ try {
7
+ process.send?.({ config: readMergedLaunchConfig(message.cwd, message.profileId) });
8
+ }
9
+ catch (error) {
10
+ process.send?.({ error: error instanceof Error ? error.message : String(error) });
11
+ }
12
+ });
@@ -1,3 +1,4 @@
1
+ import { type MergedLaunchConfig } from '../config.js';
1
2
  import type { PiInvocation } from './invocation.js';
2
3
  export { CANVAS_EXTENSIONS, CANVAS_STOPHOOK_PATH, CANVAS_INBOX_WATCHER_PATH, CANVAS_RECAP_PATH, CANVAS_GOAL_CAPTURE_PATH, CANVAS_PASSIVE_CONTEXT_PATH, CANVAS_CONTEXT_INTRO_PATH, CANVAS_TOOL_GUIDE_PATH, CANVAS_DOC_SUBSTRATE_PATH, CANVAS_STRUCTURED_OUTPUT_PATH, CANVAS_BASH_VALVE_PATH, } from './canvas-extensions.js';
3
4
  import type { NodeMeta, LaunchSpec, Mode, Lifecycle } from '../canvas/index.js';
@@ -59,7 +60,7 @@ export declare function nextLadderModel(currentSpec: string, direction?: 1 | -1,
59
60
  * a polymorph/flip rebuilds the recipe faithfully; `lifecycle` is returned
60
61
  * as-given, never derived from persona frontmatter. The two canvas
61
62
  * extensions are always first; kind-declared extensions follow. */
62
- export declare function buildLaunchSpec(kind: string, mode: Mode, opts: {
63
+ export interface LaunchBuildOptions {
63
64
  lifecycle: Lifecycle;
64
65
  hasManager: boolean;
65
66
  extraEnv?: Record<string, string>;
@@ -101,10 +102,23 @@ export declare function buildLaunchSpec(kind: string, mode: Mode, opts: {
101
102
  * broker's launch-time/turn-time resolution paths must bypass every
102
103
  * fallback for and fail closed on unavailability. */
103
104
  modelExact?: boolean;
104
- }): {
105
+ }
106
+ /** Build a launch recipe for callers that are not on crtrd's event loop. */
107
+ export declare function buildLaunchSpec(kind: string, mode: Mode, opts: LaunchBuildOptions): {
105
108
  launch: LaunchSpec;
106
109
  lifecycle: 'terminal' | 'resident';
107
110
  };
111
+ /**
112
+ * Resolve launch config in a bounded subprocess before building a recipe.
113
+ * crtrd is single-threaded: project/profile roots can live on a network
114
+ * filesystem, so even an ordinary synchronous config read must never run on
115
+ * its event loop.
116
+ */
117
+ export declare function buildLaunchSpecAsync(kind: string, mode: Mode, opts: LaunchBuildOptions): Promise<{
118
+ launch: LaunchSpec;
119
+ lifecycle: 'terminal' | 'resident';
120
+ }>;
121
+ export declare function readMergedLaunchConfigAsync(cwd: string, profileId: string | null): Promise<MergedLaunchConfig>;
108
122
  export type { PiInvocation } from './invocation.js';
109
123
  /** Construct the pi invocation for a node.
110
124
  * - fresh start: pass `prompt` (the node's first user message), no resume.
@@ -11,6 +11,8 @@
11
11
  import { mkdirSync, writeFileSync } from 'node:fs';
12
12
  import { join } from 'node:path';
13
13
  import { readMergedLaunchConfig } from '../config.js';
14
+ import { fork } from 'node:child_process';
15
+ import { fileURLToPath } from 'node:url';
14
16
  import { LAUNCH_SYSTEM_PROMPT } from './launch-prompt.js';
15
17
  import { nodeEnv } from './nodes.js';
16
18
  import { editorLabel } from '../canvas/index.js';
@@ -232,27 +234,7 @@ export function nextLadderModel(currentSpec, direction = 1, providers) {
232
234
  return order[0];
233
235
  return order[(idx + direction + order.length) % order.length];
234
236
  }
235
- // ---------------------------------------------------------------------------
236
- // Build the launch spec from {kind, mode}
237
- // ---------------------------------------------------------------------------
238
- /** Compose a node's full pi launch recipe from the kind registry. The system
239
- * prompt is now a minimal static scaffold (`LAUNCH_SYSTEM_PROMPT`) — runtime/
240
- * spine/lifecycle/kind prose is spliced per-turn from gated builtin memory docs,
241
- * not baked in here. Launch KNOBS (model/tools/extensions) still come from the
242
- * kind registry (`resolveKindConfig`), keyed on the FULL kind string (top-level
243
- * or sub-kind). Model precedence, strongest to weakest:
244
- * 1. Caller-supplied `opts.model` (durable on `meta.model_override`, re-passed
245
- * on every polymorph).
246
- * 2. The kind's registered model — mode-aware: orchestrator mode prefers
247
- * `orchestratorModel`, falling back to `model`; base mode always uses
248
- * `model`.
249
- * 3. Unset → pi default.
250
- * Callers pass the authoritative lifecycle + hasManager (`parent !== null`) so
251
- * a polymorph/flip rebuilds the recipe faithfully; `lifecycle` is returned
252
- * as-given, never derived from persona frontmatter. The two canvas
253
- * extensions are always first; kind-declared extensions follow. */
254
- export function buildLaunchSpec(kind, mode, opts) {
255
- const merged = readMergedLaunchConfig(opts.cwd ?? process.cwd(), opts.profileId !== undefined ? opts.profileId : (process.env['CRTR_PROFILE_ID'] || null));
237
+ function buildLaunchSpecFromMerged(kind, mode, opts, merged) {
256
238
  const kindConfig = merged.kinds[kind];
257
239
  const ladders = merged.modelLadders;
258
240
  const kindModel = mode === 'orchestrator' ? kindConfig?.orchestratorModel ?? kindConfig?.model : kindConfig?.model;
@@ -274,6 +256,58 @@ export function buildLaunchSpec(kind, mode, opts) {
274
256
  };
275
257
  return { launch, lifecycle: opts.lifecycle };
276
258
  }
259
+ /** Build a launch recipe for callers that are not on crtrd's event loop. */
260
+ export function buildLaunchSpec(kind, mode, opts) {
261
+ return buildLaunchSpecFromMerged(kind, mode, opts, readMergedLaunchConfig(opts.cwd ?? process.cwd(), opts.profileId !== undefined ? opts.profileId : (process.env['CRTR_PROFILE_ID'] || null)));
262
+ }
263
+ /**
264
+ * Resolve launch config in a bounded subprocess before building a recipe.
265
+ * crtrd is single-threaded: project/profile roots can live on a network
266
+ * filesystem, so even an ordinary synchronous config read must never run on
267
+ * its event loop.
268
+ */
269
+ export async function buildLaunchSpecAsync(kind, mode, opts) {
270
+ const cwd = opts.cwd ?? process.cwd();
271
+ const profileId = opts.profileId !== undefined ? opts.profileId : (process.env['CRTR_PROFILE_ID'] || null);
272
+ const merged = await readMergedLaunchConfigAsync(cwd, profileId);
273
+ return buildLaunchSpecFromMerged(kind, mode, opts, merged);
274
+ }
275
+ const LAUNCH_CONFIG_TIMEOUT_MS = 15_000;
276
+ export function readMergedLaunchConfigAsync(cwd, profileId) {
277
+ return new Promise((resolve, reject) => {
278
+ const child = fork(fileURLToPath(new URL('./launch-config-worker.js', import.meta.url)), [], {
279
+ stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
280
+ serialization: 'advanced',
281
+ });
282
+ child.send({ cwd, profileId });
283
+ let settled = false;
284
+ const finish = (fn) => {
285
+ if (settled)
286
+ return;
287
+ settled = true;
288
+ clearTimeout(timeout);
289
+ if (child.connected)
290
+ child.disconnect();
291
+ if (!child.killed)
292
+ child.kill('SIGKILL');
293
+ fn();
294
+ };
295
+ const timeout = setTimeout(() => finish(() => reject(new Error(`timed out after ${LAUNCH_CONFIG_TIMEOUT_MS}ms reading launch config for ${cwd}`))), LAUNCH_CONFIG_TIMEOUT_MS);
296
+ child.once('message', (message) => {
297
+ if (message.error !== undefined)
298
+ finish(() => reject(new Error(message.error)));
299
+ else if (message.config !== undefined)
300
+ finish(() => resolve(message.config));
301
+ else
302
+ finish(() => reject(new Error('launch config subprocess returned no result')));
303
+ });
304
+ child.once('error', (error) => finish(() => reject(error)));
305
+ child.once('exit', (code, signal) => {
306
+ if (code !== 0 && signal !== 'SIGKILL')
307
+ finish(() => reject(new Error(`launch config subprocess exited with code ${code}`)));
308
+ });
309
+ });
310
+ }
277
311
  // ---------------------------------------------------------------------------
278
312
  // Build the pi argv to launch / revive a node
279
313
  // ---------------------------------------------------------------------------
@@ -14,13 +14,17 @@ export interface PromoteResult {
14
14
  * is what a plain `crtr node yield --kind/--model` reshape rides, with no
15
15
  * mode flip and no roadmap seeding. `promote()` below is a thin wrapper that
16
16
  * also forces mode→orchestrator and seeds the roadmap. */
17
- export declare function reshapeNode(nodeId: string, opts?: {
17
+ type ReshapeOptions = {
18
18
  kind?: string;
19
19
  model?: string;
20
20
  mode?: Mode;
21
21
  lifecycle?: Lifecycle;
22
22
  profileId?: string | null;
23
- }, beforePersist?: () => void): NodeMeta;
23
+ };
24
+ /** Synchronous reshape for CLI callers outside the daemon. */
25
+ export declare function reshapeNode(nodeId: string, opts?: ReshapeOptions, beforePersist?: () => void): NodeMeta;
26
+ /** Daemon-safe reshape: network-backed config resolution happens off its event loop. */
27
+ export declare function reshapeNodeAsync(nodeId: string, opts?: ReshapeOptions, beforePersist?: () => void): Promise<NodeMeta>;
24
28
  /** Promote a node to an orchestrator (mode→orchestrator), optionally
25
29
  * specializing its kind (e.g. a `general` worker becoming a
26
30
  * `developer.orchestrator`) and optionally also making it resident. Idempotent:
@@ -33,6 +37,12 @@ export declare function promote(nodeId: string, opts?: {
33
37
  resident?: boolean;
34
38
  model?: string;
35
39
  }): PromoteResult;
40
+ /** Daemon-safe promotion variant; preserves the same validation/seed ordering. */
41
+ export declare function promoteAsync(nodeId: string, opts?: {
42
+ kind?: string;
43
+ resident?: boolean;
44
+ model?: string;
45
+ }): Promise<PromoteResult>;
36
46
  export interface YieldResult {
37
47
  meta: NodeMeta;
38
48
  /** True iff `promote:true` was passed — a plain yield never flips mode. */
@@ -49,6 +59,13 @@ export interface YieldResult {
49
59
  * same path `crtr node promote` would — mode flips to orchestrator and a
50
60
  * roadmap scaffold is seeded. Sets intent='refresh'; the stophook shuts the
51
61
  * process down on the next stop and the daemon revives it fresh. */
62
+ export declare function requestYieldAsync(nodeId: string, opts?: {
63
+ kind?: string;
64
+ model?: string;
65
+ promote?: boolean;
66
+ }, deps?: {
67
+ ensure?: () => void;
68
+ }): Promise<YieldResult>;
52
69
  export declare function requestYield(nodeId: string, opts?: {
53
70
  kind?: string;
54
71
  model?: string;
@@ -56,3 +73,4 @@ export declare function requestYield(nodeId: string, opts?: {
56
73
  }, deps?: {
57
74
  ensure?: () => void;
58
75
  }): YieldResult;
76
+ export {};