@crouton-kit/crouter 0.3.11 → 0.3.13

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 (182) hide show
  1. package/bin/crtrd +2 -0
  2. package/dist/builtin-personas/design/base.md +9 -0
  3. package/dist/builtin-personas/design/orchestrator.md +10 -0
  4. package/dist/builtin-personas/developer/base.md +9 -0
  5. package/dist/builtin-personas/developer/orchestrator.md +12 -0
  6. package/dist/builtin-personas/explore/base.md +9 -0
  7. package/dist/builtin-personas/explore/orchestrator.md +9 -0
  8. package/dist/builtin-personas/general/base.md +5 -0
  9. package/dist/builtin-personas/general/orchestrator.md +7 -0
  10. package/dist/builtin-personas/orchestration-kernel.md +71 -0
  11. package/dist/builtin-personas/plan/base.md +7 -0
  12. package/dist/builtin-personas/plan/orchestrator.md +12 -0
  13. package/dist/builtin-personas/review/base.md +7 -0
  14. package/dist/builtin-personas/review/orchestrator.md +9 -0
  15. package/dist/builtin-personas/runtime-base.md +39 -0
  16. package/dist/builtin-personas/spec/base.md +7 -0
  17. package/dist/builtin-personas/spec/orchestrator.md +10 -0
  18. package/dist/builtin-skills/skills/design/SKILL.md +51 -0
  19. package/dist/builtin-skills/skills/development/SKILL.md +109 -0
  20. package/dist/builtin-skills/skills/planning/SKILL.md +59 -0
  21. package/dist/builtin-skills/skills/spec/SKILL.md +83 -0
  22. package/dist/cli.js +14 -6
  23. package/dist/commands/{mode.d.ts → attention.d.ts} +1 -1
  24. package/dist/commands/attention.js +152 -0
  25. package/dist/commands/canvas.d.ts +2 -0
  26. package/dist/commands/canvas.js +35 -0
  27. package/dist/commands/daemon.d.ts +2 -0
  28. package/dist/commands/daemon.js +111 -0
  29. package/dist/commands/dashboard.d.ts +2 -0
  30. package/dist/commands/dashboard.js +65 -0
  31. package/dist/commands/human/prompts.d.ts +5 -0
  32. package/dist/commands/human/prompts.js +269 -0
  33. package/dist/commands/human/queue.d.ts +3 -0
  34. package/dist/commands/human/queue.js +133 -0
  35. package/dist/commands/human/shared.d.ts +43 -0
  36. package/dist/commands/human/shared.js +107 -0
  37. package/dist/commands/human.js +10 -454
  38. package/dist/commands/node.d.ts +2 -0
  39. package/dist/commands/node.js +407 -0
  40. package/dist/commands/pkg/market-inspect.d.ts +1 -0
  41. package/dist/commands/pkg/market-inspect.js +157 -0
  42. package/dist/commands/pkg/market-manage.d.ts +1 -0
  43. package/dist/commands/pkg/market-manage.js +316 -0
  44. package/dist/commands/pkg/market.d.ts +1 -0
  45. package/dist/commands/pkg/market.js +16 -0
  46. package/dist/commands/pkg/plugin-inspect.d.ts +1 -0
  47. package/dist/commands/pkg/plugin-inspect.js +142 -0
  48. package/dist/commands/pkg/plugin-manage.d.ts +1 -0
  49. package/dist/commands/pkg/plugin-manage.js +294 -0
  50. package/dist/commands/pkg/plugin.d.ts +1 -0
  51. package/dist/commands/pkg/plugin.js +16 -0
  52. package/dist/commands/pkg/shared.d.ts +5 -0
  53. package/dist/commands/pkg/shared.js +61 -0
  54. package/dist/commands/pkg.js +3 -1004
  55. package/dist/commands/push.d.ts +3 -0
  56. package/dist/commands/push.js +159 -0
  57. package/dist/commands/revive.d.ts +2 -0
  58. package/dist/commands/revive.js +64 -0
  59. package/dist/commands/skill/author.d.ts +3 -0
  60. package/dist/commands/skill/author.js +147 -0
  61. package/dist/commands/skill/find.d.ts +4 -0
  62. package/dist/commands/skill/find.js +254 -0
  63. package/dist/commands/skill/read.d.ts +1 -0
  64. package/dist/commands/skill/read.js +89 -0
  65. package/dist/commands/skill/shared.d.ts +19 -0
  66. package/dist/commands/skill/shared.js +207 -0
  67. package/dist/commands/skill/state.d.ts +3 -0
  68. package/dist/commands/skill/state.js +69 -0
  69. package/dist/commands/skill.js +6 -691
  70. package/dist/commands/sys/config.d.ts +1 -0
  71. package/dist/commands/sys/config.js +186 -0
  72. package/dist/commands/sys/doctor.d.ts +1 -0
  73. package/dist/commands/sys/doctor.js +369 -0
  74. package/dist/commands/sys/shared.d.ts +3 -0
  75. package/dist/commands/sys/shared.js +24 -0
  76. package/dist/commands/sys/update.d.ts +2 -0
  77. package/dist/commands/sys/update.js +114 -0
  78. package/dist/commands/sys.js +4 -694
  79. package/dist/core/__tests__/argv-parser.test.js +19 -1
  80. package/dist/core/__tests__/canvas-inbox-watcher.test.js +100 -0
  81. package/dist/core/__tests__/canvas.test.js +154 -0
  82. package/dist/core/__tests__/reset.test.js +105 -0
  83. package/dist/core/canvas/attention.d.ts +24 -0
  84. package/dist/core/canvas/attention.js +94 -0
  85. package/dist/core/canvas/canvas.d.ts +40 -0
  86. package/dist/core/canvas/canvas.js +210 -0
  87. package/dist/core/canvas/db.d.ts +7 -0
  88. package/dist/core/canvas/db.js +61 -0
  89. package/dist/core/canvas/index.d.ts +4 -0
  90. package/dist/core/canvas/index.js +6 -0
  91. package/dist/core/canvas/paths.d.ts +16 -0
  92. package/dist/core/canvas/paths.js +62 -0
  93. package/dist/core/canvas/render.d.ts +30 -0
  94. package/dist/core/canvas/render.js +186 -0
  95. package/dist/core/canvas/types.d.ts +87 -0
  96. package/dist/core/canvas/types.js +8 -0
  97. package/dist/core/command.d.ts +5 -0
  98. package/dist/core/command.js +35 -10
  99. package/dist/core/feed/feed.d.ts +43 -0
  100. package/dist/core/feed/feed.js +116 -0
  101. package/dist/core/feed/inbox.d.ts +50 -0
  102. package/dist/core/feed/inbox.js +124 -0
  103. package/dist/core/help.js +5 -3
  104. package/dist/core/io.d.ts +15 -1
  105. package/dist/core/io.js +56 -6
  106. package/dist/core/personas/index.d.ts +12 -0
  107. package/dist/core/personas/index.js +10 -0
  108. package/dist/core/personas/loader.d.ts +44 -0
  109. package/dist/core/personas/loader.js +157 -0
  110. package/dist/core/personas/resolve.d.ts +36 -0
  111. package/dist/core/personas/resolve.js +110 -0
  112. package/dist/core/render.d.ts +11 -0
  113. package/dist/core/render.js +126 -0
  114. package/dist/core/resolver.d.ts +10 -0
  115. package/dist/core/resolver.js +109 -1
  116. package/dist/core/runtime/front-door.d.ts +10 -0
  117. package/dist/core/runtime/front-door.js +97 -0
  118. package/dist/core/runtime/kickoff.d.ts +23 -0
  119. package/dist/core/runtime/kickoff.js +134 -0
  120. package/dist/core/runtime/launch.d.ts +34 -0
  121. package/dist/core/runtime/launch.js +85 -0
  122. package/dist/core/runtime/nodes.d.ts +38 -0
  123. package/dist/core/runtime/nodes.js +95 -0
  124. package/dist/core/runtime/presence.d.ts +55 -0
  125. package/dist/core/runtime/presence.js +198 -0
  126. package/dist/core/runtime/promote.d.ts +30 -0
  127. package/dist/core/runtime/promote.js +105 -0
  128. package/dist/core/runtime/reset.d.ts +13 -0
  129. package/dist/core/runtime/reset.js +97 -0
  130. package/dist/core/runtime/revive.d.ts +26 -0
  131. package/dist/core/runtime/revive.js +87 -0
  132. package/dist/core/runtime/roadmap.d.ts +12 -0
  133. package/dist/core/runtime/roadmap.js +52 -0
  134. package/dist/core/runtime/spawn.d.ts +31 -0
  135. package/dist/core/runtime/spawn.js +123 -0
  136. package/dist/core/runtime/stop-guard.d.ts +18 -0
  137. package/dist/core/runtime/stop-guard.js +33 -0
  138. package/dist/core/runtime/tmux.d.ts +107 -0
  139. package/dist/core/runtime/tmux.js +244 -0
  140. package/dist/core/spawn.d.ts +17 -197
  141. package/dist/core/spawn.js +16 -539
  142. package/dist/daemon/crtrd-cli.js +4 -0
  143. package/dist/daemon/crtrd.d.ts +20 -0
  144. package/dist/daemon/crtrd.js +200 -0
  145. package/dist/daemon/manage.d.ts +17 -0
  146. package/dist/daemon/manage.js +57 -0
  147. package/dist/pi-extensions/canvas-inbox-watcher.d.ts +16 -0
  148. package/dist/pi-extensions/canvas-inbox-watcher.js +229 -0
  149. package/dist/pi-extensions/canvas-nav.d.ts +32 -0
  150. package/dist/pi-extensions/canvas-nav.js +536 -0
  151. package/dist/pi-extensions/canvas-stophook.d.ts +17 -0
  152. package/dist/pi-extensions/canvas-stophook.js +396 -0
  153. package/package.json +6 -5
  154. package/dist/commands/agent.d.ts +0 -6
  155. package/dist/commands/agent.js +0 -585
  156. package/dist/commands/debug.d.ts +0 -3
  157. package/dist/commands/debug.js +0 -192
  158. package/dist/commands/job.d.ts +0 -11
  159. package/dist/commands/job.js +0 -384
  160. package/dist/commands/mode.js +0 -231
  161. package/dist/commands/plan.d.ts +0 -4
  162. package/dist/commands/plan.js +0 -322
  163. package/dist/commands/spec.d.ts +0 -3
  164. package/dist/commands/spec.js +0 -299
  165. package/dist/core/__tests__/flow-leaves.test.js +0 -248
  166. package/dist/core/__tests__/job.test.js +0 -310
  167. package/dist/core/__tests__/jobs.test.js +0 -98
  168. package/dist/core/__tests__/spawn.test.js +0 -138
  169. package/dist/core/__tests__/subagents.test.d.ts +0 -1
  170. package/dist/core/__tests__/subagents.test.js +0 -75
  171. package/dist/core/jobs.d.ts +0 -107
  172. package/dist/core/jobs.js +0 -565
  173. package/dist/core/subagents.d.ts +0 -18
  174. package/dist/core/subagents.js +0 -163
  175. package/dist/prompts/agent.d.ts +0 -27
  176. package/dist/prompts/agent.js +0 -184
  177. package/dist/prompts/debug.d.ts +0 -8
  178. package/dist/prompts/debug.js +0 -44
  179. /package/dist/core/__tests__/{flow-leaves.test.d.ts → canvas-inbox-watcher.test.d.ts} +0 -0
  180. /package/dist/core/__tests__/{job.test.d.ts → canvas.test.d.ts} +0 -0
  181. /package/dist/core/__tests__/{jobs.test.d.ts → reset.test.d.ts} +0 -0
  182. /package/dist/{core/__tests__/spawn.test.d.ts → daemon/crtrd-cli.d.ts} +0 -0
@@ -0,0 +1,8 @@
1
+ // The canvas vocabulary — the node + edge model the whole runtime hangs on.
2
+ //
3
+ // One global canvas (`~/.crtr/canvas.db`) holds the topology (nodes + edges);
4
+ // each node's flesh lives on disk under `~/.crtr/nodes/<id>/`. A node's
5
+ // `meta.json` is the source of truth for its own row; the db is a queryable
6
+ // index over those metas, plus the authoritative store for the mutable
7
+ // `subscribes_to` edges (which no single meta owns).
8
+ export {};
@@ -23,6 +23,10 @@ export interface LeafDef {
23
23
  /** Opt into editor slash-command exposure (see SlashSpec). */
24
24
  slash?: SlashSpec;
25
25
  run: (input: Record<string, unknown>) => Promise<Record<string, unknown> | void>;
26
+ /** Optional bespoke renderer: turn the result into instruction-shaped
27
+ * XML+markdown the agent acts on. Omit to fall back to the schema-driven
28
+ * generic renderer. Ignored when `--json` is set. */
29
+ render?: (result: Record<string, unknown>) => string;
26
30
  }
27
31
  export interface BranchDef {
28
32
  kind: 'branch';
@@ -46,6 +50,7 @@ export declare function defineLeaf(opts: {
46
50
  help: LeafHelp;
47
51
  slash?: SlashSpec;
48
52
  run: (input: Record<string, unknown>) => Promise<Record<string, unknown> | void>;
53
+ render?: (result: Record<string, unknown>) => string;
49
54
  }): LeafDef;
50
55
  export declare function defineBranch(opts: {
51
56
  name: string;
@@ -5,7 +5,8 @@
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, emit, handle } from './io.js';
8
+ import { readStdinRaw, emit, handle, setJsonOutput, isJsonOutput } from './io.js';
9
+ import { renderResult } from './render.js';
9
10
  import { CrtrError } from './errors.js';
10
11
  import { ExitCode } from '../types.js';
11
12
  import { readFileSync } from 'node:fs';
@@ -19,6 +20,7 @@ export function defineLeaf(opts) {
19
20
  help: opts.help,
20
21
  slash: opts.slash,
21
22
  run: opts.run,
23
+ render: opts.render,
22
24
  };
23
25
  }
24
26
  export function defineBranch(opts) {
@@ -263,7 +265,10 @@ export async function parseArgv(params, tokens) {
263
265
  if (positionalValue !== undefined) {
264
266
  throw parseArgvError('bad_invocation', `unexpected extra positional argument: ${token}`, tokens.join(' '), undefined, 'Use --flag for parameters; only one positional allowed.');
265
267
  }
266
- if (positionalParam === undefined) {
268
+ // A bare positional is accepted when the leaf declares a positional param,
269
+ // OR when it declares a stdin param (the positional supplies the stdin
270
+ // value as an ergonomic alternative to piping). Otherwise it's an error.
271
+ if (positionalParam === undefined && stdinParam === undefined) {
267
272
  throw parseArgvError('bad_invocation', `this leaf takes no positional arguments: ${token}`, token, undefined, 'Use --flag for parameters. Run -h for the schema.');
268
273
  }
269
274
  positionalValue = token;
@@ -273,13 +278,20 @@ export async function parseArgv(params, tokens) {
273
278
  if (positionalValue !== undefined && positionalParam !== undefined) {
274
279
  result[flagNameToKey(positionalParam.name)] = positionalValue;
275
280
  }
276
- // Read stdin if declared
281
+ // Resolve stdin if declared. A positional token (when there's no dedicated
282
+ // positional param to claim it) satisfies the stdin param directly, so
283
+ // `crtr node new "Say hi"` works as well as piping on stdin.
277
284
  if (stdinParam !== undefined) {
278
- const raw = await readStdinRaw();
279
- if (raw.trim() === '' && stdinParam.required) {
280
- throw parseArgvError('missing_parameter', `stdin is required for this leaf`, '', stdinParam.name, 'Pipe the required content on stdin.');
285
+ if (positionalValue !== undefined && positionalParam === undefined) {
286
+ result[flagNameToKey(stdinParam.name)] = positionalValue;
287
+ }
288
+ else {
289
+ const raw = await readStdinRaw();
290
+ if (raw.trim() === '' && stdinParam.required) {
291
+ throw parseArgvError('missing_parameter', `stdin is required for this leaf`, '', stdinParam.name, 'Pipe the required content on stdin, or pass it as a positional argument.');
292
+ }
293
+ result[flagNameToKey(stdinParam.name)] = raw;
281
294
  }
282
- result[flagNameToKey(stdinParam.name)] = raw;
283
295
  }
284
296
  // Validate required params
285
297
  for (const p of params) {
@@ -298,8 +310,15 @@ export async function parseArgv(params, tokens) {
298
310
  return result;
299
311
  }
300
312
  export async function runCli(root, argv) {
301
- // argv is process.argv — strip node binary + script path
302
- const tokens = argv.slice(2);
313
+ // argv is process.argv — strip node binary + script path. `--json` is a
314
+ // global: pull it out anywhere it appears so the rest of argv parses against
315
+ // the leaf schema unchanged, and switch stdout from rendered prose to raw
316
+ // JSON. It is intentionally undocumented — the default prose output is the
317
+ // agent contract; --json exists only for programmatic/tooling consumers.
318
+ const rawTokens = argv.slice(2);
319
+ const tokens = rawTokens.filter((t) => t !== '--json');
320
+ if (tokens.length !== rawTokens.length)
321
+ setJsonOutput(true);
303
322
  // Bare root invocation or -h at root
304
323
  if (tokens.length === 0 || (tokens.length === 1 && (tokens[0] === '-h' || tokens[0] === '--help'))) {
305
324
  process.stdout.write(renderRoot(root.help) + '\n');
@@ -325,7 +344,13 @@ export async function runCli(root, argv) {
325
344
  const input = await parseArgv(params, remaining);
326
345
  const result = await node.run(input);
327
346
  if (result !== undefined && result !== null) {
328
- emit(result);
347
+ if (isJsonOutput()) {
348
+ emit(result);
349
+ }
350
+ else {
351
+ const text = node.render !== undefined ? node.render(result) : renderResult(result, node.help);
352
+ process.stdout.write(text + '\n');
353
+ }
329
354
  }
330
355
  // JSONL leaves call emitLine themselves and return void
331
356
  }
@@ -0,0 +1,43 @@
1
+ export type PushKind = 'update' | 'urgent' | 'final';
2
+ export interface PushOpts {
3
+ /** Semantic kind of this push. `final` also finalises the node. */
4
+ kind: PushKind;
5
+ /** Report body (markdown). Written verbatim after the YAML frontmatter. */
6
+ body: string;
7
+ /**
8
+ * Node id of the sender — recorded as `from` on each inbox entry.
9
+ * Defaults to `nodeId` (the publisher) when omitted.
10
+ */
11
+ from?: string;
12
+ }
13
+ export interface PushResult {
14
+ /** Absolute path of the written report file. */
15
+ reportPath: string;
16
+ /** Node ids that received an inbox pointer. */
17
+ deliveredTo: string[];
18
+ }
19
+ /**
20
+ * Push a report from `nodeId` and fan it out as inbox pointers to all
21
+ * current subscribers.
22
+ *
23
+ * Steps:
24
+ * (a) Write nodes/<nodeId>/reports/<ts>-<kind>.md (YAML front + body).
25
+ * (b) For each active/passive subscriber, append a pointer to their inbox.
26
+ * (c) If kind === 'final', mark the node done.
27
+ */
28
+ export declare function push(nodeId: string, opts: PushOpts): Promise<PushResult>;
29
+ /** Emit a routine progress update from `nodeId`. */
30
+ export declare function pushUpdate(nodeId: string, body: string, opts?: {
31
+ from?: string;
32
+ }): Promise<PushResult>;
33
+ /** Emit an urgent alert from `nodeId` (inbox tier: urgent). */
34
+ export declare function pushUrgent(nodeId: string, body: string, opts?: {
35
+ from?: string;
36
+ }): Promise<PushResult>;
37
+ /**
38
+ * Emit the final report from `nodeId` (inbox tier: normal, kind: final).
39
+ * Also transitions the node to status=done / intent=done.
40
+ */
41
+ export declare function pushFinal(nodeId: string, body: string, opts?: {
42
+ from?: string;
43
+ }): Promise<PushResult>;
@@ -0,0 +1,116 @@
1
+ // Push engine for the pi-native canvas runtime.
2
+ //
3
+ // `push(nodeId, opts)` writes a report to the node's reports/ directory, then
4
+ // fans out a lightweight inbox pointer to every subscriber. The inbox entry
5
+ // carries the report path (ref), not the body — subscribers dereference on
6
+ // demand.
7
+ //
8
+ // Compact timestamp format: 20260602T184512 (UTC, no separators) chosen for
9
+ // file-system friendliness and lexicographic sort alignment.
10
+ import { writeFileSync, renameSync, mkdirSync, existsSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { reportsDir, subscribersOf, setStatus, updateNode, } from '../canvas/index.js';
13
+ import { appendInbox } from './inbox.js';
14
+ // ---------------------------------------------------------------------------
15
+ // Internal helpers
16
+ // ---------------------------------------------------------------------------
17
+ /** Format a Date as `YYYYMMDDTHHmmss` (UTC, no separators). */
18
+ function compactTs(d = new Date()) {
19
+ const pad = (n, w = 2) => String(n).padStart(w, '0');
20
+ return (String(d.getUTCFullYear()) +
21
+ pad(d.getUTCMonth() + 1) +
22
+ pad(d.getUTCDate()) +
23
+ 'T' +
24
+ pad(d.getUTCHours()) +
25
+ pad(d.getUTCMinutes()) +
26
+ pad(d.getUTCSeconds()));
27
+ }
28
+ /**
29
+ * Write a report file atomically (tmp + rename).
30
+ * Returns the final absolute path.
31
+ */
32
+ function writeReport(nodeId, kind, ts, body) {
33
+ const dir = reportsDir(nodeId);
34
+ if (!existsSync(dir))
35
+ mkdirSync(dir, { recursive: true });
36
+ const fileName = `${ts}-${kind}.md`;
37
+ const finalPath = join(dir, fileName);
38
+ const tmpPath = `${finalPath}.tmp`;
39
+ const isoTs = new Date().toISOString();
40
+ // YAML frontmatter: minimal, machine-readable, no freeform content in it.
41
+ const frontmatter = `---\nnode: ${nodeId}\nkind: ${kind}\nts: ${isoTs}\n---\n`;
42
+ writeFileSync(tmpPath, frontmatter + body, 'utf8');
43
+ renameSync(tmpPath, finalPath);
44
+ return finalPath;
45
+ }
46
+ /**
47
+ * Extract the first line of a string and truncate to `maxLen` chars.
48
+ * Used to populate the inbox entry's `label` field (~80 chars).
49
+ */
50
+ function firstLine(text, maxLen = 80) {
51
+ const line = text.split('\n')[0] ?? '';
52
+ return line.length > maxLen ? line.slice(0, maxLen - 1) + '…' : line;
53
+ }
54
+ /** Map a PushKind to the appropriate inbox delivery tier. */
55
+ function tierFor(kind) {
56
+ return kind === 'urgent' ? 'urgent' : 'normal';
57
+ }
58
+ // ---------------------------------------------------------------------------
59
+ // Core push
60
+ // ---------------------------------------------------------------------------
61
+ /**
62
+ * Push a report from `nodeId` and fan it out as inbox pointers to all
63
+ * current subscribers.
64
+ *
65
+ * Steps:
66
+ * (a) Write nodes/<nodeId>/reports/<ts>-<kind>.md (YAML front + body).
67
+ * (b) For each active/passive subscriber, append a pointer to their inbox.
68
+ * (c) If kind === 'final', mark the node done.
69
+ */
70
+ export async function push(nodeId, opts) {
71
+ const { kind, body } = opts;
72
+ const from = opts.from ?? nodeId;
73
+ const now = new Date();
74
+ const ts = compactTs(now);
75
+ // (a) Write the report.
76
+ const reportPath = writeReport(nodeId, kind, ts, body);
77
+ // (b) Fan out inbox pointers to every subscriber (active and passive both
78
+ // receive the pointer; the daemon decides whether to wake active ones).
79
+ const subscribers = subscribersOf(nodeId);
80
+ const deliveredTo = [];
81
+ const label = firstLine(body);
82
+ for (const sub of subscribers) {
83
+ appendInbox(sub.node_id, {
84
+ from,
85
+ tier: tierFor(kind),
86
+ kind,
87
+ ref: reportPath,
88
+ label,
89
+ });
90
+ deliveredTo.push(sub.node_id);
91
+ }
92
+ // (c) Finalise node when kind === 'final'.
93
+ if (kind === 'final') {
94
+ setStatus(nodeId, 'done');
95
+ updateNode(nodeId, { intent: 'done' });
96
+ }
97
+ return { reportPath, deliveredTo };
98
+ }
99
+ // ---------------------------------------------------------------------------
100
+ // Convenience wrappers
101
+ // ---------------------------------------------------------------------------
102
+ /** Emit a routine progress update from `nodeId`. */
103
+ export async function pushUpdate(nodeId, body, opts) {
104
+ return push(nodeId, { kind: 'update', body, ...opts });
105
+ }
106
+ /** Emit an urgent alert from `nodeId` (inbox tier: urgent). */
107
+ export async function pushUrgent(nodeId, body, opts) {
108
+ return push(nodeId, { kind: 'urgent', body, ...opts });
109
+ }
110
+ /**
111
+ * Emit the final report from `nodeId` (inbox tier: normal, kind: final).
112
+ * Also transitions the node to status=done / intent=done.
113
+ */
114
+ export async function pushFinal(nodeId, body, opts) {
115
+ return push(nodeId, { kind: 'final', body, ...opts });
116
+ }
@@ -0,0 +1,50 @@
1
+ export type InboxTier = 'critical' | 'urgent' | 'normal' | 'deferred';
2
+ export type InboxKind = 'update' | 'urgent' | 'final' | 'message' | 'completed';
3
+ /** A single inbox entry — a pointer, not a copy of the content. */
4
+ export interface InboxEntry {
5
+ /** ISO 8601 timestamp of delivery. */
6
+ ts: string;
7
+ /** Node id of the sender, or null for system-generated entries. */
8
+ from: string | null;
9
+ /** Priority band for the receiver's attention. */
10
+ tier: InboxTier;
11
+ /** Semantic kind of the push event. */
12
+ kind: InboxKind;
13
+ /** Absolute path to the report file, when this entry is a push pointer. */
14
+ ref?: string;
15
+ /** First ~80 chars of the body's first line — enough to decide if it matters. */
16
+ label: string;
17
+ /** Arbitrary structured payload for non-push message entries. */
18
+ data?: Record<string, unknown>;
19
+ }
20
+ /**
21
+ * Atomically append one inbox entry to `nodes/<nodeId>/inbox.jsonl`.
22
+ * Fills `ts` (current ISO time). Returns the completed entry.
23
+ */
24
+ export declare function appendInbox(nodeId: string, entry: Omit<InboxEntry, 'ts'>): InboxEntry;
25
+ /**
26
+ * Return all inbox entries strictly after `cursorIso`.
27
+ * When `cursorIso` is undefined, returns every entry in the file.
28
+ */
29
+ export declare function readInboxSince(nodeId: string, cursorIso?: string): InboxEntry[];
30
+ /**
31
+ * Read the persisted cursor ISO for a node's inbox.
32
+ * Returns undefined if no cursor file exists yet.
33
+ */
34
+ export declare function readCursor(nodeId: string): string | undefined;
35
+ /**
36
+ * Persist a new cursor ISO for a node's inbox (atomic tmp+rename).
37
+ */
38
+ export declare function writeCursor(nodeId: string, iso: string): void;
39
+ /**
40
+ * Render many unread inbox pointers into one compact digest string.
41
+ *
42
+ * Format (per sender group):
43
+ * From <sender> — <N> update(s):
44
+ * [<kind>] <label> (ref: <path>)
45
+ * …
46
+ *
47
+ * A header line announces the total count and instructs the receiver to
48
+ * dereference only what matters.
49
+ */
50
+ export declare function coalesce(entries: InboxEntry[]): string;
@@ -0,0 +1,124 @@
1
+ // Per-node inbox.jsonl primitive for the pi-native canvas runtime.
2
+ //
3
+ // An inbox entry is a lightweight POINTER (~30 tokens), never content.
4
+ // The report body lives in nodes/<id>/reports/; the inbox line carries only
5
+ // enough to find it and decide whether to dereference.
6
+ //
7
+ // Layout:
8
+ // nodes/<id>/inbox.jsonl — one JSON line per entry, append-only
9
+ // nodes/<id>/inbox.jsonl.cursor — ISO 8601 of last-read entry (sidecar)
10
+ import { appendFileSync, existsSync, readFileSync, writeFileSync, renameSync, mkdirSync, } from 'node:fs';
11
+ import { dirname } from 'node:path';
12
+ import { inboxPath } from '../canvas/index.js';
13
+ // ---------------------------------------------------------------------------
14
+ // Cursor sidecar path
15
+ // ---------------------------------------------------------------------------
16
+ function cursorPath(nodeId) {
17
+ return `${inboxPath(nodeId)}.cursor`;
18
+ }
19
+ // ---------------------------------------------------------------------------
20
+ // Append
21
+ // ---------------------------------------------------------------------------
22
+ /**
23
+ * Atomically append one inbox entry to `nodes/<nodeId>/inbox.jsonl`.
24
+ * Fills `ts` (current ISO time). Returns the completed entry.
25
+ */
26
+ export function appendInbox(nodeId, entry) {
27
+ const full = { ts: new Date().toISOString(), ...entry };
28
+ const line = JSON.stringify(full) + '\n';
29
+ // Ensure the parent directory exists (inbox.jsonl lives directly under the
30
+ // node dir, which ensureNodeDirs creates — but guard anyway for callers that
31
+ // haven't yet scaffolded the node).
32
+ const dir = dirname(inboxPath(nodeId));
33
+ if (!existsSync(dir))
34
+ mkdirSync(dir, { recursive: true });
35
+ // appendFileSync is atomic within a single process (a single write(2) call for
36
+ // a short line is atomic on POSIX). For multi-process safety we rely on the
37
+ // OS-level append guarantee (O_APPEND) which Node honours via 'a' flag.
38
+ appendFileSync(inboxPath(nodeId), line, { encoding: 'utf8', flag: 'a' });
39
+ return full;
40
+ }
41
+ // ---------------------------------------------------------------------------
42
+ // Read
43
+ // ---------------------------------------------------------------------------
44
+ /**
45
+ * Return all inbox entries strictly after `cursorIso`.
46
+ * When `cursorIso` is undefined, returns every entry in the file.
47
+ */
48
+ export function readInboxSince(nodeId, cursorIso) {
49
+ const p = inboxPath(nodeId);
50
+ if (!existsSync(p))
51
+ return [];
52
+ const raw = readFileSync(p, 'utf8');
53
+ const entries = raw
54
+ .split('\n')
55
+ .filter((l) => l.trim() !== '')
56
+ .map((l) => JSON.parse(l));
57
+ if (cursorIso === undefined)
58
+ return entries;
59
+ // Entries are appended in chronological order; filter to those strictly
60
+ // after the cursor. We compare ISO strings lexicographically (valid for UTC).
61
+ return entries.filter((e) => e.ts > cursorIso);
62
+ }
63
+ // ---------------------------------------------------------------------------
64
+ // Cursor persistence
65
+ // ---------------------------------------------------------------------------
66
+ /**
67
+ * Read the persisted cursor ISO for a node's inbox.
68
+ * Returns undefined if no cursor file exists yet.
69
+ */
70
+ export function readCursor(nodeId) {
71
+ const p = cursorPath(nodeId);
72
+ if (!existsSync(p))
73
+ return undefined;
74
+ const val = readFileSync(p, 'utf8').trim();
75
+ return val !== '' ? val : undefined;
76
+ }
77
+ /**
78
+ * Persist a new cursor ISO for a node's inbox (atomic tmp+rename).
79
+ */
80
+ export function writeCursor(nodeId, iso) {
81
+ const p = cursorPath(nodeId);
82
+ const tmp = `${p}.tmp`;
83
+ const dir = dirname(p);
84
+ if (!existsSync(dir))
85
+ mkdirSync(dir, { recursive: true });
86
+ writeFileSync(tmp, iso, 'utf8');
87
+ renameSync(tmp, p);
88
+ }
89
+ // ---------------------------------------------------------------------------
90
+ // Coalesce
91
+ // ---------------------------------------------------------------------------
92
+ /**
93
+ * Render many unread inbox pointers into one compact digest string.
94
+ *
95
+ * Format (per sender group):
96
+ * From <sender> — <N> update(s):
97
+ * [<kind>] <label> (ref: <path>)
98
+ * …
99
+ *
100
+ * A header line announces the total count and instructs the receiver to
101
+ * dereference only what matters.
102
+ */
103
+ export function coalesce(entries) {
104
+ if (entries.length === 0)
105
+ return '(inbox empty)';
106
+ const header = `${entries.length} update${entries.length === 1 ? '' : 's'} since last read — dereference what matters.\n`;
107
+ // Group by `from` (null → 'system').
108
+ const groups = new Map();
109
+ for (const e of entries) {
110
+ const key = e.from ?? 'system';
111
+ if (!groups.has(key))
112
+ groups.set(key, []);
113
+ groups.get(key).push(e);
114
+ }
115
+ const sections = [];
116
+ for (const [sender, items] of groups) {
117
+ const lines = items.map((e) => {
118
+ const refPart = e.ref !== undefined ? ` (ref: ${e.ref})` : '';
119
+ return ` [${e.kind}] ${e.label}${refPart}`;
120
+ });
121
+ sections.push(`From ${sender} — ${items.length} update${items.length === 1 ? '' : 's'}:\n${lines.join('\n')}`);
122
+ }
123
+ return header + sections.join('\n\n');
124
+ }
package/dist/core/help.js CHANGED
@@ -45,7 +45,8 @@ function pad(s, width) {
45
45
  // ---------------------------------------------------------------------------
46
46
  // renderRoot
47
47
  // ---------------------------------------------------------------------------
48
- const IO_CONTRACT = 'I/O contract: flags and positional args on input, JSON on stdout (JSONL for streams).\n' +
48
+ const IO_CONTRACT = 'I/O contract: flags and positional args on input; stdout is agent-ready markdown/XML you\n' +
49
+ 'act on directly — read it as a continuation of your prompt, don\'t parse it as data.\n' +
49
50
  'Exit 0 on success, non-zero on failure. Schemas appear at leaf -h.';
50
51
  // Behavioral instruction (not a schema) — engrained in the appended system
51
52
  // prompt so the model treats unfamiliar capabilities as a cue to discover the
@@ -180,8 +181,9 @@ export function renderLeafArgv(h) {
180
181
  lines.push(h.inputNote !== undefined ? h.inputNote : 'No input parameters.');
181
182
  }
182
183
  lines.push('');
183
- const outputLabel = h.outputKind === 'jsonl' ? 'Output (stdout, JSONL)' : 'Output (stdout, JSON)';
184
- lines.push(outputLabel);
184
+ // The result is rendered as instruction-shaped XML+markdown; these fields are
185
+ // the information it carries, in order, not a literal JSON shape.
186
+ lines.push('Output (fields carried in the rendered result)');
185
187
  const outNameW = maxLen(h.output.map((f) => f.name));
186
188
  for (const f of h.output) {
187
189
  lines.push(` ${pad(f.name, outNameW)} ${f.type}. ${f.constraint}`);
package/dist/core/io.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import { CrtrError } from './errors.js';
2
2
  import { type ExitCodeValue } from '../types.js';
3
+ /** Set by the dispatcher when `--json` is present anywhere in argv. */
4
+ export declare function setJsonOutput(v: boolean): void;
5
+ /** True when the caller asked for raw JSON instead of rendered prose. */
6
+ export declare function isJsonOutput(): boolean;
3
7
  /** Structured error payload. `error` is a stable code the agent branches on;
4
8
  * `next` is the recovery road sign. */
5
9
  export interface ErrorPayload {
@@ -17,10 +21,20 @@ export declare class InputError extends CrtrError {
17
21
  /** Read raw stdin to EOF. Returns empty string when stdin is a TTY (no pipe).
18
22
  * Called by the argv parser for leaves declaring a `stdin` parameter. */
19
23
  export declare function readStdinRaw(): Promise<string>;
20
- /** Single-shot response: one JSON object. The whole response is one value. */
24
+ /** Raw-JSON mirror of a single-shot response (the `--json` escape hatch). The
25
+ * default path renders the result as prose instead — see render.ts. */
21
26
  export declare function emit(obj: Record<string, unknown>): void;
22
27
  /** One JSONL record. Call per event in a stream; partial reads stay parseable. */
23
28
  export declare function emitLine(obj: Record<string, unknown>): void;
29
+ /**
30
+ * Write to stdout and resolve true ONLY once the bytes are confirmed flushed to
31
+ * a connected reader. Resolves false if the consumer is gone (EPIPE) or the
32
+ * write fails. This is the reliable "the caller actually received it" signal:
33
+ * use it to gate side effects that must only happen on genuine delivery (e.g.
34
+ * acking a collected result). A killed process never resolves at all — also
35
+ * safe, since the gated side effect then never runs.
36
+ */
37
+ export declare function writeStdout(s: string): Promise<boolean>;
24
38
  export declare function diag(message: string): void;
25
39
  /** Terminal error handler. Command-level failures (bad input, not-found,
26
40
  * ambiguous) surface as the JSON response on stdout so the caller parses one
package/dist/core/io.js CHANGED
@@ -1,9 +1,23 @@
1
- // The agent-facing I/O contract. Flags and positional args on input; one JSON
2
- // object on stdout (JSONL for streams); structured errors; stderr is
3
- // diagnostics only and never carries the result. The stdout value is the next
4
- // caller's stdin. See cli-design SKILL.md / reference.md.
1
+ // The agent-facing I/O contract. Flags and positional args on input; stdout is
2
+ // agent-ready markdown/XML the caller acts on directly (the result rendered FOR
3
+ // the model, not data it parses); structured errors; stderr is diagnostics only
4
+ // and never carries the result. The raw JSON object is available behind the
5
+ // `--json` global for tooling. See cli-design SKILL.md / reference.md.
5
6
  import { CrtrError } from './errors.js';
6
7
  import { ExitCode } from '../types.js';
8
+ import { renderError } from './render.js';
9
+ // ---------------------------------------------------------------------------
10
+ // output mode — prose (default) vs raw JSON (--json global, for tooling)
11
+ // ---------------------------------------------------------------------------
12
+ let jsonOutput = false;
13
+ /** Set by the dispatcher when `--json` is present anywhere in argv. */
14
+ export function setJsonOutput(v) {
15
+ jsonOutput = v;
16
+ }
17
+ /** True when the caller asked for raw JSON instead of rendered prose. */
18
+ export function isJsonOutput() {
19
+ return jsonOutput;
20
+ }
7
21
  /** A command-level failure: surfaces as the JSON response on stdout. */
8
22
  export class InputError extends CrtrError {
9
23
  payload;
@@ -29,7 +43,8 @@ export async function readStdinRaw() {
29
43
  // ---------------------------------------------------------------------------
30
44
  // stdout — the result, nothing else
31
45
  // ---------------------------------------------------------------------------
32
- /** Single-shot response: one JSON object. The whole response is one value. */
46
+ /** Raw-JSON mirror of a single-shot response (the `--json` escape hatch). The
47
+ * default path renders the result as prose instead — see render.ts. */
33
48
  export function emit(obj) {
34
49
  process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
35
50
  }
@@ -37,6 +52,37 @@ export function emit(obj) {
37
52
  export function emitLine(obj) {
38
53
  process.stdout.write(JSON.stringify(obj) + '\n');
39
54
  }
55
+ /**
56
+ * Write to stdout and resolve true ONLY once the bytes are confirmed flushed to
57
+ * a connected reader. Resolves false if the consumer is gone (EPIPE) or the
58
+ * write fails. This is the reliable "the caller actually received it" signal:
59
+ * use it to gate side effects that must only happen on genuine delivery (e.g.
60
+ * acking a collected result). A killed process never resolves at all — also
61
+ * safe, since the gated side effect then never runs.
62
+ */
63
+ export function writeStdout(s) {
64
+ return new Promise((resolve) => {
65
+ let settled = false;
66
+ const onErr = (e) => {
67
+ if (e.code === 'EPIPE')
68
+ finish(false);
69
+ };
70
+ const finish = (ok) => {
71
+ if (settled)
72
+ return;
73
+ settled = true;
74
+ process.stdout.off('error', onErr);
75
+ resolve(ok);
76
+ };
77
+ process.stdout.on('error', onErr);
78
+ try {
79
+ process.stdout.write(s, (err) => finish(err === null || err === undefined));
80
+ }
81
+ catch {
82
+ finish(false);
83
+ }
84
+ });
85
+ }
40
86
  // ---------------------------------------------------------------------------
41
87
  // stderr — diagnostics the agent MAY capture, never the result
42
88
  // ---------------------------------------------------------------------------
@@ -67,7 +113,11 @@ function payloadOf(e) {
67
113
  * raw traces never reach the agent. Exits non-zero either way. */
68
114
  export function handle(e) {
69
115
  if (e instanceof CrtrError) {
70
- process.stdout.write(JSON.stringify(payloadOf(e), null, 2) + '\n');
116
+ const payload = payloadOf(e);
117
+ const out = jsonOutput
118
+ ? JSON.stringify(payload, null, 2)
119
+ : renderError(payload);
120
+ process.stdout.write(out + '\n');
71
121
  process.exit(e.exitCode);
72
122
  }
73
123
  const err = e;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Persona composer public surface.
3
+ *
4
+ * Re-exports:
5
+ * - loadPersona / loadKernel / availableKinds (loader — raw file access)
6
+ * - resolve (high-level composer)
7
+ * - ResolvedPersona (return type of resolve)
8
+ */
9
+ export { loadPersona, loadKernel, availableKinds } from './loader.js';
10
+ export type { LoadedPersona } from './loader.js';
11
+ export { resolve } from './resolve.js';
12
+ export type { ResolvedPersona } from './resolve.js';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Persona composer public surface.
3
+ *
4
+ * Re-exports:
5
+ * - loadPersona / loadKernel / availableKinds (loader — raw file access)
6
+ * - resolve (high-level composer)
7
+ * - ResolvedPersona (return type of resolve)
8
+ */
9
+ export { loadPersona, loadKernel, availableKinds } from './loader.js';
10
+ export { resolve } from './resolve.js';