@north-light/crouter 0.3.219 → 0.3.221

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 (155) hide show
  1. package/dist/api/client.d.ts +9 -0
  2. package/dist/api/client.js +10 -0
  3. package/dist/api/dto/chat-inventory.d.ts +43 -0
  4. package/dist/api/dto/chat-inventory.js +11 -0
  5. package/dist/api/dto/profiles.d.ts +19 -5
  6. package/dist/api/dto/profiles.js +2 -1
  7. package/dist/api/index.d.ts +1 -0
  8. package/dist/api/index.js +1 -0
  9. package/dist/api/routes.d.ts +1 -0
  10. package/dist/api/routes.js +1 -0
  11. package/dist/build-root.d.ts +2 -6
  12. package/dist/build-root.js +51 -4
  13. package/dist/builtin-memory/internal/agent-shaping.md +3 -1
  14. package/dist/builtin-memory/internal/memory-loading.md +4 -0
  15. package/dist/builtin-memory/plan/roadmap.md +7 -1
  16. package/dist/builtin-memory/spec/guide.md +7 -1
  17. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +1 -1
  18. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -1
  19. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +7 -2
  20. package/dist/clients/attach/__tests__/ref-autocomplete.test.js +1 -1
  21. package/dist/clients/attach/__tests__/titled-editor-preview.test.js +1 -1
  22. package/dist/clients/attach/overlays/file-review.js +2 -2
  23. package/dist/clients/attach/session/keys.d.ts +1 -1
  24. package/dist/clients/attach/session/profile-files.js +1 -1
  25. package/dist/clients/attach/viewer.js +690 -690
  26. package/dist/clients/inbox/review/launch.d.ts +8 -4
  27. package/dist/clients/inbox/review/launch.js +55 -5
  28. package/dist/clients/inbox/review/review-client.d.ts +1 -0
  29. package/dist/clients/inbox/review/review-client.js +4 -0
  30. package/dist/clients/inbox/review-adapter.d.ts +1 -8
  31. package/dist/clients/inbox/review-adapter.js +4 -52
  32. package/dist/commands/memory/lint.js +2 -1
  33. package/dist/commands/memory/read.js +1 -0
  34. package/dist/commands/memory.js +1 -1
  35. package/dist/commands/pkg/market-manage.js +165 -75
  36. package/dist/commands/pkg/plugin-inspect.js +19 -2
  37. package/dist/commands/pkg/plugin-manage.d.ts +8 -3
  38. package/dist/commands/pkg/plugin-manage.js +72 -24
  39. package/dist/commands/profile/default.js +6 -10
  40. package/dist/commands/profile/list.js +5 -3
  41. package/dist/commands/profile/new.js +21 -8
  42. package/dist/commands/profile/project.js +25 -19
  43. package/dist/commands/profile/show.js +3 -3
  44. package/dist/commands/surface-inbox.js +1 -0
  45. package/dist/commands/sys/__tests__/migrate.test.js +16 -5
  46. package/dist/commands/sys/doctor.js +35 -5
  47. package/dist/commands/sys/migrate.js +38 -19
  48. package/dist/commands/sys/panels/broker-limits-panel.js +3 -3
  49. package/dist/commands/sys/setup-core.js +1 -1
  50. package/dist/commands/sys/sync-project-guidance.js +1 -1
  51. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +1 -1
  52. package/dist/core/__tests__/fixtures/c5-command-boundary-ext.js +24 -0
  53. package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -18
  54. package/dist/core/__tests__/fixtures/fake-engine.js +8 -1
  55. package/dist/core/__tests__/helpers/broker-clients.d.ts +1 -0
  56. package/dist/core/__tests__/helpers/broker-clients.js +1 -0
  57. package/dist/core/__tests__/inline-memory-refs.test.js +36 -2
  58. package/dist/core/__tests__/profile-project-memory-delivery.test.js +217 -0
  59. package/dist/core/__tests__/seam/dormancy-release.test.js +37 -1
  60. package/dist/core/__tests__/serial/broker-sdk-wiring.test.js +102 -2
  61. package/dist/core/bootstrap.js +6 -0
  62. package/dist/core/canvas/browse/app.js +5 -2
  63. package/dist/core/canvas/browse/model.d.ts +25 -15
  64. package/dist/core/canvas/browse/model.js +86 -65
  65. package/dist/core/canvas/render-source.d.ts +6 -0
  66. package/dist/core/canvas/render-source.js +7 -1
  67. package/dist/core/canvas/render.js +10 -2
  68. package/dist/core/command-hooks/artifact.d.ts +10 -0
  69. package/dist/core/command-hooks/artifact.js +129 -0
  70. package/dist/core/command-hooks/catalog.d.ts +14 -0
  71. package/dist/core/command-hooks/catalog.js +38 -0
  72. package/dist/core/command-hooks/compose.d.ts +15 -0
  73. package/dist/core/command-hooks/compose.js +99 -0
  74. package/dist/core/command-hooks/discovery.d.ts +87 -0
  75. package/dist/core/command-hooks/discovery.js +174 -0
  76. package/dist/core/command-hooks/help.d.ts +5 -0
  77. package/dist/core/command-hooks/help.js +18 -0
  78. package/dist/core/command-hooks/index.d.ts +6 -0
  79. package/dist/core/command-hooks/index.js +6 -0
  80. package/dist/core/command-hooks/report.d.ts +23 -0
  81. package/dist/core/command-hooks/report.js +19 -0
  82. package/dist/core/command-hooks/schema.d.ts +27 -0
  83. package/dist/core/command-hooks/schema.js +68 -0
  84. package/dist/core/command-hooks/transport/exec-invoke.d.ts +22 -0
  85. package/dist/core/command-hooks/transport/exec-invoke.js +274 -0
  86. package/dist/core/command-plugins/presence.d.ts +2 -0
  87. package/dist/core/command-plugins/presence.js +17 -0
  88. package/dist/core/command-plugins/transport/exec-invoke.d.ts +5 -0
  89. package/dist/core/command-plugins/transport/exec-invoke.js +58 -5
  90. package/dist/core/command.d.ts +8 -1
  91. package/dist/core/command.js +12 -10
  92. package/dist/core/help.d.ts +7 -1
  93. package/dist/core/io.d.ts +9 -1
  94. package/dist/core/io.js +44 -2
  95. package/dist/core/memory/inline-ref-inventory.d.ts +2 -1
  96. package/dist/core/memory/inline-ref-inventory.js +15 -8
  97. package/dist/core/memory-resolver.d.ts +13 -1
  98. package/dist/core/memory-resolver.js +25 -19
  99. package/dist/core/profiles/manifest.d.ts +13 -2
  100. package/dist/core/profiles/manifest.js +84 -18
  101. package/dist/core/profiles/select.js +9 -9
  102. package/dist/core/render.js +11 -0
  103. package/dist/core/runtime/advertised-command-invocation.d.ts +20 -0
  104. package/dist/core/runtime/advertised-command-invocation.js +233 -0
  105. package/dist/core/runtime/bearings.js +1 -1
  106. package/dist/core/runtime/broker/client-registry.d.ts +6 -3
  107. package/dist/core/runtime/broker/client-registry.js +6 -4
  108. package/dist/core/runtime/broker/event-projection.js +7 -0
  109. package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
  110. package/dist/core/runtime/broker/frame-dispatch.js +16 -10
  111. package/dist/core/runtime/broker/read-ops.d.ts +4 -0
  112. package/dist/core/runtime/broker/read-ops.js +6 -2
  113. package/dist/core/runtime/broker-extension-render.js +1 -1
  114. package/dist/core/runtime/broker-inventory.d.ts +5 -0
  115. package/dist/core/runtime/broker-inventory.js +191 -0
  116. package/dist/core/runtime/broker-protocol.d.ts +13 -2
  117. package/dist/core/runtime/broker.js +10 -1
  118. package/dist/core/runtime/command-surface.d.ts +33 -0
  119. package/dist/core/runtime/command-surface.js +81 -0
  120. package/dist/core/runtime/node-read.js +5 -0
  121. package/dist/core/scope.d.ts +26 -1
  122. package/dist/core/scope.js +52 -12
  123. package/dist/core/substrate/on-read.d.ts +7 -1
  124. package/dist/core/substrate/on-read.js +13 -4
  125. package/dist/core/substrate/render.js +14 -5
  126. package/dist/core/substrate/schema.d.ts +11 -1
  127. package/dist/core/substrate/schema.js +11 -2
  128. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +4 -4
  129. package/dist/daemon/api/handlers/chat-inventory.d.ts +2 -0
  130. package/dist/daemon/api/handlers/chat-inventory.js +25 -0
  131. package/dist/daemon/api/handlers/profiles.js +7 -1
  132. package/dist/daemon/api/map.d.ts +2 -1
  133. package/dist/daemon/api/map.js +3 -2
  134. package/dist/daemon/api/server.js +2 -0
  135. package/dist/hook-authoring.d.ts +75 -0
  136. package/dist/hook-authoring.js +358 -0
  137. package/dist/hook-process.d.ts +7 -0
  138. package/dist/hook-process.js +34 -0
  139. package/dist/index.d.ts +2 -0
  140. package/dist/index.js +2 -0
  141. package/dist/migrations/002-profile-project-memory.d.ts +2 -0
  142. package/dist/migrations/002-profile-project-memory.js +71 -0
  143. package/dist/migrations/profile-manifests.d.ts +30 -0
  144. package/dist/migrations/profile-manifests.js +70 -0
  145. package/dist/migrations/registry.js +10 -5
  146. package/dist/migrations/types.d.ts +28 -1
  147. package/dist/migrations/types.js +15 -9
  148. package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +21 -4
  149. package/dist/pi-extensions/canvas-structured-output.js +85 -2
  150. package/dist/types.d.ts +15 -6
  151. package/dist/types.js +5 -1
  152. package/package.json +1 -1
  153. package/runtime.lock.json +2 -2
  154. package/dist/clients/attach/__tests__/file-review-focus.test.js +0 -49
  155. /package/dist/{clients/attach/__tests__/file-review-focus.test.d.ts → core/__tests__/profile-project-memory-delivery.test.d.ts} +0 -0
@@ -0,0 +1,70 @@
1
+ // The profile-manifest lane runner: fold every registered profile-manifest
2
+ // migration over each stored `profile.json`, one profile at a time, under the
3
+ // caller-supplied per-profile lock.
4
+ //
5
+ // This lane reads RAW BYTES. The current manifest reader treats a manifest
6
+ // whose projects are not the current shape exactly like corrupt JSON, so a
7
+ // pre-migration home has no readable profiles at all — every profile surface,
8
+ // `listProfiles()` included, is blind until this lane has run. That is why the
9
+ // runner enumerates the profiles root itself, why `crtr sys migrate` runs it
10
+ // before any other profile read, and why its snapshots (not `listProfiles()`)
11
+ // are what the rest of that command's discovery works from.
12
+ //
13
+ // The lock arrives as a callback rather than an import: a migration file may
14
+ // not reach into the profile module, and a lane runner that did would be one
15
+ // import away from the same blind reader.
16
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { atomicWriteJson } from '../core/fs-utils.js';
19
+ import { STATE_MIGRATIONS } from './registry.js';
20
+ /** Frozen copy of the generated-profile-id shape (core/profiles/manifest.ts),
21
+ * so enumeration matches `listProfiles()`: a hand-made directory crtr can
22
+ * never resolve is skipped here too, rather than failing the whole run. */
23
+ const ID_SHAPE = /^[a-z0-9]+(?:-[a-z0-9]+)*-[0-9a-f]{8}$/;
24
+ export function runProfileManifestMigrations(profilesRoot, opts) {
25
+ const migrations = (opts.migrations ?? STATE_MIGRATIONS).filter((m) => m.lane === 'profile-manifest');
26
+ const result = { profiles: 0, applied: [], snapshots: [], skipped: [] };
27
+ if (!existsSync(profilesRoot))
28
+ return result;
29
+ for (const dirent of readdirSync(profilesRoot, { withFileTypes: true })) {
30
+ if (!dirent.isDirectory())
31
+ continue;
32
+ const profileId = dirent.name;
33
+ if (!ID_SHAPE.test(profileId))
34
+ continue;
35
+ const path = join(profilesRoot, profileId, 'profile.json');
36
+ if (!existsSync(path))
37
+ continue;
38
+ result.profiles += 1;
39
+ opts.withLock(profileId, () => {
40
+ let raw;
41
+ try {
42
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
43
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
44
+ throw new Error('manifest is not a JSON object');
45
+ }
46
+ raw = parsed;
47
+ }
48
+ catch (e) {
49
+ result.skipped.push({ profileId, path, error: (e instanceof Error ? e.message : String(e)).split('\n')[0] });
50
+ return;
51
+ }
52
+ let current = raw;
53
+ let changed = false;
54
+ for (const migration of migrations) {
55
+ const next = migration.apply({ profileId, path, raw: current });
56
+ if (next === null)
57
+ continue;
58
+ current = next;
59
+ changed = true;
60
+ result.applied.push({ profileId, path, migration: migration.description });
61
+ }
62
+ if (changed && opts.dryRun !== true)
63
+ atomicWriteJson(path, current);
64
+ // Every migration guarantees a current manifest or throws, so the folded
65
+ // record is the post-migration contract by construction.
66
+ result.snapshots.push({ profileId, path, manifest: current });
67
+ });
68
+ }
69
+ return result;
70
+ }
@@ -3,10 +3,11 @@
3
3
  //
4
4
  // Authoring contract (each migration is one file, `NNN-<desc>.ts`, appended
5
5
  // here in order):
6
- // • Idempotent. A convergent migration re-runs on every `crtr sys migrate`,
7
- // so applying it to an already-migrated store must plan zero changes. A
8
- // journaled migration runs once per database via user_version, but its
9
- // body must still tolerate a crash-resume re-run of any filesystem work.
6
+ // • Idempotent. A convergent or profile-manifest migration re-runs on every
7
+ // `crtr sys migrate`, so applying it to already-migrated state must plan
8
+ // zero changes. A journaled migration runs once per database via
9
+ // user_version, but its body must still tolerate a crash-resume re-run of
10
+ // any filesystem work.
10
11
  // • Journaled filesystem steps are crash-resumable: fs writes happen outside
11
12
  // the db transaction, so a step interrupted mid-fs-work must complete
12
13
  // (never corrupt) when the chain re-runs it.
@@ -16,4 +17,8 @@
16
17
  // This keeps the registry legal in every CLI leaf's import graph (the
17
18
  // build's V-1 gate).
18
19
  import { surfacesFrontmatterMigration } from './001-surfaces-frontmatter.js';
19
- export const STATE_MIGRATIONS = [surfacesFrontmatterMigration];
20
+ import { profileProjectMemoryMigration } from './002-profile-project-memory.js';
21
+ export const STATE_MIGRATIONS = [
22
+ surfacesFrontmatterMigration,
23
+ profileProjectMemoryMigration,
24
+ ];
@@ -1,4 +1,5 @@
1
1
  import type { DatabaseSync } from 'node:sqlite';
2
+ import type { ProfileManifest } from '../types.js';
2
3
  export interface JournaledMigration {
3
4
  lane: 'journaled';
4
5
  description: string;
@@ -19,7 +20,33 @@ export interface ConvergentMigration {
19
20
  * runner filters no-op changes and performs the writes. */
20
21
  apply(store: StoreSnapshot, context?: ConvergentMigrationContext): DocChange[];
21
22
  }
22
- export type StateMigration = JournaledMigration | ConvergentMigration;
23
+ /** One stored `profile.json` handed to the profile-manifest lane. */
24
+ export interface ProfileManifestInput {
25
+ /** The immediate directory name under the profiles root. The only root
26
+ * classifier a migration may key on — a manifest `name` is mutable. */
27
+ profileId: string;
28
+ /** Absolute path to the `profile.json`. */
29
+ path: string;
30
+ /** Parsed JSON exactly as stored, historical shapes included. */
31
+ raw: Record<string, unknown>;
32
+ }
33
+ export interface ProfileManifestMigration {
34
+ lane: 'profile-manifest';
35
+ description: string;
36
+ /** Pure planning: the full replacement record, or null when this manifest is
37
+ * already current. A structure the migration cannot convert THROWS — the
38
+ * run fails rather than persisting a guessed value. */
39
+ apply(input: ProfileManifestInput): Record<string, unknown> | null;
40
+ }
41
+ export type StateMigration = JournaledMigration | ConvergentMigration | ProfileManifestMigration;
42
+ /** One profile's manifest after every profile-manifest migration folded —
43
+ * the bytes on disk after a write run, and the bytes a write run WOULD have
44
+ * produced under `--dry-run`. */
45
+ export interface ProfileManifestSnapshot {
46
+ profileId: string;
47
+ path: string;
48
+ manifest: ProfileManifest;
49
+ }
23
50
  /** One parsed markdown doc in a store snapshot. */
24
51
  export interface StoreDoc {
25
52
  /** Absolute file path. */
@@ -1,11 +1,17 @@
1
- // State-migration shapes. Two lanes, split by which store owns the state:
1
+ // State-migration shapes. Three lanes, split by which store owns the state:
2
2
  //
3
- // journaled — canvas-db state. Runs inside the db's ordered migration chain
4
- // (core/canvas/db.ts appends these to MIGRATIONS), so each step
5
- // gets the chain's BEGIN IMMEDIATE/COMMIT + user_version gating
6
- // and runs exactly once per database.
7
- // convergent — on-disk memory documents. No journal exists over a user's
8
- // stores (files appear, sync in, get hand-edited), so these
9
- // CONVERGE instead: every run re-scans and rewrites whatever
10
- // still matches the old shape. `crtr sys migrate` is the runner.
3
+ // journaled — canvas-db state. Runs inside the db's ordered migration
4
+ // chain (core/canvas/db.ts appends these to MIGRATIONS), so
5
+ // each step gets the chain's BEGIN IMMEDIATE/COMMIT +
6
+ // user_version gating and runs exactly once per database.
7
+ // convergent — on-disk memory documents. No journal exists over a user's
8
+ // stores (files appear, sync in, get hand-edited), so these
9
+ // CONVERGE instead: every run re-scans and rewrites whatever
10
+ // still matches the old shape.
11
+ // profile-manifest — `~/.crouter/profiles/<id>/profile.json`. Converges like
12
+ // the document lane, but each manifest folds under the same
13
+ // per-profile lock add/remove take, and a structure the
14
+ // migration cannot convert throws instead of being guessed.
15
+ //
16
+ // `crtr sys migrate` runs both on-disk lanes, profile manifests first.
11
17
  export {};
@@ -26,7 +26,7 @@ after(() => {
26
26
  else
27
27
  process.env['CRTR_NODE_ID'] = previous.nodeId;
28
28
  });
29
- test('optional submit fields remain optional through a strict provider tool schema', () => {
29
+ test('strict provider schema stays portable while original constraints validate submit', async () => {
30
30
  home = mkdtempSync(join(tmpdir(), 'crtr-structured-output-'));
31
31
  process.env['CRTR_HOME'] = home;
32
32
  process.env['CRTR_NODE_ID'] = 'structured-output-test';
@@ -44,8 +44,14 @@ test('optional submit fields remain optional through a strict provider tool sche
44
44
  },
45
45
  required: ['summary'],
46
46
  },
47
+ tags: {
48
+ type: 'array',
49
+ items: { type: 'string' },
50
+ minItems: 2,
51
+ maxItems: 5,
52
+ },
47
53
  },
48
- required: ['status', 'verification', 'details'],
54
+ required: ['status', 'verification', 'details', 'tags'],
49
55
  };
50
56
  writeOutputSchema('structured-output-test', 'oneoff', schema);
51
57
  let definition;
@@ -57,15 +63,26 @@ test('optional submit fields remain optional through a strict provider tool sche
57
63
  });
58
64
  assert.ok(definition, 'the pending request registers submit');
59
65
  const parameters = definition['parameters'];
60
- assert.deepEqual(parameters.required, ['status', 'commit', 'verification', 'blocker', 'details']);
66
+ assert.deepEqual(parameters.required, ['status', 'commit', 'verification', 'blocker', 'details', 'tags']);
61
67
  assert.deepEqual(parameters.properties.commit, { anyOf: [{ type: 'string' }, { type: 'null' }] });
62
68
  assert.deepEqual(parameters.properties.blocker, { anyOf: [{ type: 'string' }, { type: 'null' }] });
63
69
  assert.equal(parameters.additionalProperties, false);
64
70
  assert.equal(parameters.properties.details.additionalProperties, false);
65
- assert.deepEqual(schema.required, ['status', 'verification', 'details'], 'the stored request keeps its original optionality');
71
+ assert.equal(parameters.properties.tags['minItems'], undefined);
72
+ assert.equal(parameters.properties.tags['maxItems'], undefined);
73
+ assert.match(parameters.properties.tags['description'], /minItems: 2/);
74
+ assert.match(parameters.properties.tags['description'], /maxItems: 5/);
75
+ assert.deepEqual(schema.required, ['status', 'verification', 'details', 'tags'], 'the stored request keeps its original constraints');
66
76
  const [providerTool] = convertResponsesTools([definition], {
67
77
  strict: false,
68
78
  supportsStrictMode: true,
69
79
  });
70
80
  assert.equal(providerTool['strict'], true);
81
+ const execute = definition['execute'];
82
+ await assert.rejects(execute('call', {
83
+ status: 'done',
84
+ verification: 'verified',
85
+ details: { summary: 'ok' },
86
+ tags: ['only-one'],
87
+ }, new AbortController().signal), /must not have fewer than 2 items/);
71
88
  });
@@ -5,6 +5,7 @@
5
5
  // stop-guard will reprompt the node with an explicit error about the invalid file.
6
6
  import { mkdirSync, writeFileSync } from 'node:fs';
7
7
  import { dirname } from 'node:path';
8
+ import { validateToolArguments } from '@earendil-works/pi-ai';
8
9
  import { emitEvent } from '../core/events/emit.js';
9
10
  import { publishBrokerReport } from '../core/runtime/broker/daemon-ops.js';
10
11
  import { outputResultPath, readOutputRequest, removeOutputSchema, } from '../core/runtime/structured-output.js';
@@ -30,6 +31,85 @@ function retireTool(pi) {
30
31
  /* best-effort; the execute handler is inert when no schema file exists */
31
32
  }
32
33
  }
34
+ const STRICT_STRING_FORMATS = new Set([
35
+ 'date-time', 'time', 'date', 'duration', 'email', 'hostname', 'uri', 'ipv4', 'ipv6', 'uuid',
36
+ ]);
37
+ // Provider strict modes accept a structural subset; omitted constraints stay visible in descriptions.
38
+ function providerStrictSchema(schema) {
39
+ const transform = (value) => {
40
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
41
+ return value;
42
+ const source = { ...value };
43
+ const take = (key) => {
44
+ const child = source[key];
45
+ delete source[key];
46
+ return child;
47
+ };
48
+ const transformed = {};
49
+ const ref = take('$ref');
50
+ if (ref !== undefined)
51
+ return { $ref: ref };
52
+ const defs = take('$defs');
53
+ if (defs !== null && typeof defs === 'object' && !Array.isArray(defs)) {
54
+ transformed['$defs'] = Object.fromEntries(Object.entries(defs).map(([name, definition]) => [name, transform(definition)]));
55
+ }
56
+ const type = take('type');
57
+ const anyOf = take('anyOf');
58
+ const oneOf = take('oneOf');
59
+ const allOf = take('allOf');
60
+ if (Array.isArray(anyOf))
61
+ transformed['anyOf'] = anyOf.map(transform);
62
+ else if (Array.isArray(oneOf))
63
+ transformed['anyOf'] = oneOf.map(transform);
64
+ else if (Array.isArray(allOf))
65
+ transformed['allOf'] = allOf.map(transform);
66
+ else if (type !== undefined)
67
+ transformed['type'] = type;
68
+ else
69
+ return value;
70
+ const description = take('description');
71
+ if (description !== undefined)
72
+ transformed['description'] = description;
73
+ const title = take('title');
74
+ if (title !== undefined)
75
+ transformed['title'] = title;
76
+ if (type === 'object') {
77
+ const properties = take('properties');
78
+ transformed['properties'] = properties !== null && typeof properties === 'object' && !Array.isArray(properties)
79
+ ? Object.fromEntries(Object.entries(properties).map(([name, property]) => [name, transform(property)]))
80
+ : {};
81
+ take('additionalProperties');
82
+ transformed['additionalProperties'] = false;
83
+ const required = take('required');
84
+ if (required !== undefined)
85
+ transformed['required'] = required;
86
+ }
87
+ else if (type === 'string') {
88
+ const format = take('format');
89
+ if (typeof format === 'string' && STRICT_STRING_FORMATS.has(format))
90
+ transformed['format'] = format;
91
+ else if (format !== undefined)
92
+ source['format'] = format;
93
+ }
94
+ else if (type === 'array') {
95
+ const items = take('items');
96
+ if (items !== undefined)
97
+ transformed['items'] = transform(items);
98
+ const minItems = take('minItems');
99
+ if (minItems === 0 || minItems === 1)
100
+ transformed['minItems'] = minItems;
101
+ else if (minItems !== undefined)
102
+ source['minItems'] = minItems;
103
+ }
104
+ if (Object.keys(source).length > 0) {
105
+ const existing = typeof transformed['description'] === 'string' ? transformed['description'] : '';
106
+ const constraints = `{${Object.entries(source).map(([key, child]) => `${key}: ${JSON.stringify(child)}`).join(', ')}}`;
107
+ transformed['description'] = existing === '' ? constraints : `${existing}\n\n${constraints}`;
108
+ }
109
+ return transformed;
110
+ };
111
+ return transform(schema);
112
+ }
33
113
  function strictToolSchema(schema) {
34
114
  const transform = (value) => {
35
115
  if (Array.isArray(value))
@@ -61,7 +141,10 @@ function strictToolSchema(schema) {
61
141
  transformed['required'] = Object.keys(properties);
62
142
  return transformed;
63
143
  };
64
- return transform(schema);
144
+ return providerStrictSchema(transform(schema));
145
+ }
146
+ function validateResult(value, schema) {
147
+ return validateToolArguments({ name: 'submit', description: '', parameters: schema }, { type: 'toolCall', id: 'structured-output', name: 'submit', arguments: value });
65
148
  }
66
149
  function omitStrictNulls(value, schema) {
67
150
  if (value === null || typeof value !== 'object' || Array.isArray(value) || schema === null || typeof schema !== 'object' || Array.isArray(schema))
@@ -156,7 +239,7 @@ export function registerCanvasStructuredOutput(pi) {
156
239
  details: { submitted: false, reason: 'schema_replaced' },
157
240
  };
158
241
  }
159
- const result = omitStrictNulls(params, current.request.schema);
242
+ const result = validateResult(omitStrictNulls(params, current.request.schema), current.request.schema);
160
243
  const resultPath = outputResultPath(nodeId);
161
244
  const json = JSON.stringify(result, null, 2);
162
245
  mkdirSync(dirname(resultPath), { recursive: true });
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { BindingId } from './core/keybindings/catalog.js';
2
+ import type { ProfileProject } from './api/dto/profiles.js';
2
3
  export type Scope = 'user' | 'project' | 'builtin';
3
4
  export declare const ExitCode: {
4
5
  readonly SUCCESS: 0;
@@ -94,6 +95,13 @@ export interface PluginManifest {
94
95
  * crtr owns parse/help/render/errors and direct-spawns the plugin's one
95
96
  * executable per leaf invocation. See `src/core/command-plugins/`. */
96
97
  commands?: string;
98
+ /** Plugin-root-relative path to a declarative hook manifest (`hooks.json`).
99
+ * Must be declared together with `hookExecutable`; hooks are trusted local
100
+ * code and are unavailable to endpoint archive plugins. */
101
+ hooks?: string;
102
+ /** Plugin-root-relative executable that dispatches declarations in `hooks`.
103
+ * Must be a regular file with a POSIX executable permission bit. */
104
+ hookExecutable?: string;
97
105
  /** Bare-callable executables this plugin ships, keyed by the bare command
98
106
  * name a node's bash sees. Each value is a PLUGIN-ROOT-relative path to a
99
107
  * regular executable file when the effective set resolves. These are trusted
@@ -411,9 +419,9 @@ export interface InstalledMarketplace {
411
419
  }
412
420
  /** The on-disk manifest at `~/.crouter/profiles/<slug>-<id>/profile.json`
413
421
  * (spec §2.1). A profile is an agent identity defined at the user root: it
414
- * names N project directories and owns its own `memory/` store. `projects`
415
- * are absolute, real-path-resolved directories in manifest order — the
416
- * pointer set `findProjectScopeRoots` walks, after the node's own
422
+ * names N project directories and owns its own `memory/` store. Project
423
+ * `path`s are absolute, real-path-resolved directories in manifest order —
424
+ * the pointer set `findProjectScopeRoots` walks, after the node's own
417
425
  * cwd. `name` is the mutable display name (and the gateable `profile` subject
418
426
  * field); `<slug>-<id>` in the directory name is the stable id and
419
427
  * never changes on rename. See `src/core/profiles/manifest.ts` for every
@@ -422,10 +430,11 @@ export interface InstalledMarketplace {
422
430
  export interface ProfileManifest {
423
431
  schema_version: number;
424
432
  name: string;
425
- projects: string[];
433
+ projects: ProfileProject[];
426
434
  /** The directory nodes under this profile are pinned to (see
427
- * `profileHome`). Normalized on read to the first project when the manifest
428
- * carries none, so a profile written before homes existed still has one. */
435
+ * `profileHome`). Normalized on read to the first project's path when the
436
+ * manifest carries none, so a profile written before homes existed still
437
+ * has one. */
429
438
  home: string | null;
430
439
  /** ISO timestamp while the profile is inert; null (or absent on older manifests) means active. */
431
440
  paused_at: string | null;
package/dist/types.js CHANGED
@@ -77,7 +77,11 @@ export function defaultScopeConfig() {
77
77
  auto_update: { crtr: 'notify', content: 'notify', interval_hours: 24 },
78
78
  max_panes_per_window: DEFAULT_MAX_PANES_PER_WINDOW,
79
79
  auto_open_child_viewers: true,
80
- brokerThresholds: { warning: 128, automaticReviveCap: 256 },
80
+ // Every live broker is a whole pi engine. 256 of them is not a bound any
81
+ // real host survives — an 8 GB guest dies around a few dozen — so these are
82
+ // sized as a backstop that actually engages before the machine does.
83
+ // Override per host with CRTR_MAX_LIVE_BROKERS / CRTR_WARN_LIVE_BROKERS.
84
+ brokerThresholds: { warning: 16, automaticReviveCap: 32 },
81
85
  completion_bell: true,
82
86
  working_gerunds: [...DEFAULT_WORKING_GERUNDS],
83
87
  whip_mode: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.219",
3
+ "version": "0.3.221",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.219",
3
+ "version": "0.3.221",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.219",
9
+ "version": "0.3.221",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
@@ -1,49 +0,0 @@
1
- import { fileURLToPath } from 'node:url';
2
- import { test } from 'node:test';
3
- import assert from 'node:assert/strict';
4
- import { initTheme } from '@earendil-works/pi-coding-agent';
5
- import { InputController } from '../input/controller.js';
6
- import { createViewerCapabilities } from '../input/capabilities.js';
7
- initTheme(undefined, false);
8
- function fakeEditor() {
9
- const obj = {
10
- onSubmit: undefined,
11
- onEscape: undefined,
12
- onPasteImage: undefined,
13
- onAction: () => { },
14
- getText: () => '',
15
- setText: () => { },
16
- addToHistory: () => { },
17
- insertTextAtCursor: () => { },
18
- };
19
- return obj;
20
- }
21
- test('choosing a review file closes the inline picker before the review surface takes focus', () => {
22
- const editor = fakeEditor();
23
- let focused = null;
24
- let mountedPicker;
25
- const tui = {
26
- requestRender: () => { },
27
- setFocus: (component) => { focused = component; },
28
- showOverlay: () => ({ hide: () => { } }),
29
- start: () => { },
30
- stop: () => { },
31
- };
32
- const reviewSurface = {
33
- render: () => [],
34
- invalidate: () => { },
35
- handleInput: () => { },
36
- };
37
- const controller = new InputController(tui, editor, {}, {
38
- onCommand: () => { },
39
- onDialogResponse: () => { },
40
- onMountPicker: (component) => { mountedPicker = component; },
41
- onUnmountPicker: () => { },
42
- }, createViewerCapabilities({ role: 'controller', remote: false, nodeId: 'test', profileId: undefined }));
43
- controller.openFileReviewPicker([fileURLToPath(import.meta.url)], process.cwd(), () => {
44
- tui.setFocus(reviewSurface);
45
- });
46
- assert.ok(mountedPicker?.handleInput);
47
- mountedPicker.handleInput('\n');
48
- assert.equal(focused, reviewSurface, 'picker close must not restore editor focus over the review surface');
49
- });