@crouton-kit/crouter 0.3.20 → 0.3.22

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 (144) hide show
  1. package/dist/builtin-memory/crouter-development/marketplaces.md +2 -4
  2. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +3 -5
  3. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +3 -6
  4. package/dist/builtin-memory/crouter-development/personas.md +2 -6
  5. package/dist/builtin-memory/crouter-development/plugins.md +2 -4
  6. package/dist/builtin-memory/design.md +2 -4
  7. package/dist/builtin-memory/development.md +3 -3
  8. package/dist/builtin-memory/planning.md +3 -4
  9. package/dist/builtin-memory/spec.md +3 -10
  10. package/dist/builtin-personas/orchestration-kernel.md +1 -1
  11. package/dist/commands/canvas-history/read.d.ts +1 -0
  12. package/dist/commands/canvas-history/read.js +61 -0
  13. package/dist/commands/canvas-history/search.d.ts +1 -0
  14. package/dist/commands/canvas-history/search.js +270 -0
  15. package/dist/commands/canvas-history/show.d.ts +1 -0
  16. package/dist/commands/canvas-history/show.js +79 -0
  17. package/dist/commands/canvas-history.d.ts +2 -0
  18. package/dist/commands/canvas-history.js +33 -0
  19. package/dist/commands/canvas.js +3 -2
  20. package/dist/commands/human/queue.js +2 -2
  21. package/dist/commands/memory/__tests__/lint-schema.test.js +29 -0
  22. package/dist/commands/memory/find.js +14 -52
  23. package/dist/commands/memory/lint.d.ts +7 -0
  24. package/dist/commands/memory/lint.js +29 -35
  25. package/dist/commands/memory/list.js +8 -18
  26. package/dist/commands/memory/read.js +4 -37
  27. package/dist/commands/memory/shared.js +1 -2
  28. package/dist/commands/memory/write.js +6 -9
  29. package/dist/commands/node.js +47 -25
  30. package/dist/commands/pkg/bridge.d.ts +1 -0
  31. package/dist/commands/pkg/bridge.js +137 -0
  32. package/dist/commands/pkg/plugin-inspect.js +7 -6
  33. package/dist/commands/pkg.js +2 -1
  34. package/dist/commands/push.js +10 -15
  35. package/dist/commands/skill/author.js +35 -44
  36. package/dist/commands/skill/shared.d.ts +1 -5
  37. package/dist/commands/skill/shared.js +9 -63
  38. package/dist/commands/skill.js +5 -6
  39. package/dist/commands/sys/doctor.js +6 -144
  40. package/dist/commands/tmux-spread.js +16 -1
  41. package/dist/commands/view-cycle.js +9 -5
  42. package/dist/commands/view-run.js +3 -2
  43. package/dist/core/__tests__/broker-dormant-wake.test.js +71 -34
  44. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +21 -5
  45. package/dist/core/__tests__/canvas-inbox-watcher.test.js +21 -5
  46. package/dist/core/__tests__/child-death-wake.test.js +133 -185
  47. package/dist/core/__tests__/context-intro.test.js +23 -27
  48. package/dist/core/__tests__/daemon-liveness.test.js +28 -258
  49. package/dist/core/__tests__/flagship-lifecycle.test.js +156 -135
  50. package/dist/core/__tests__/frame-decoder-perf.test.js +51 -25
  51. package/dist/core/__tests__/{broker-attach-limits.test.js → full/broker-attach-limits.test.js} +62 -35
  52. package/dist/core/__tests__/{broker-attach-stream.test.js → full/broker-attach-stream.test.js} +54 -31
  53. package/dist/core/__tests__/full/broker-crash-teardown.test.js +143 -0
  54. package/dist/core/__tests__/full/broker-dialogs.test.js +153 -0
  55. package/dist/core/__tests__/full/broker-lifecycle.test.js +116 -0
  56. package/dist/core/__tests__/{cascade-close.test.js → full/cascade-close.test.js} +4 -3
  57. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +292 -0
  58. package/dist/core/__tests__/{dead-pane-regression.test.js → full/dead-pane-regression.test.js} +2 -2
  59. package/dist/core/__tests__/{detach-focus.test.js → full/detach-focus.test.js} +7 -7
  60. package/dist/core/__tests__/{human-new-window-regression.test.js → full/human-new-window-regression.test.js} +2 -2
  61. package/dist/core/__tests__/{placement-focus.test.js → full/placement-focus.test.js} +5 -5
  62. package/dist/core/__tests__/{placement-reconcile.test.js → full/placement-reconcile.test.js} +3 -3
  63. package/dist/core/__tests__/{placement-revive.test.js → full/placement-revive.test.js} +5 -5
  64. package/dist/core/__tests__/{placement-teardown.test.js → full/placement-teardown.test.js} +4 -4
  65. package/dist/core/__tests__/{review-render-pane-regression.test.js → full/review-render-pane-regression.test.js} +2 -2
  66. package/dist/core/__tests__/full/spike-harness.test.d.ts +1 -0
  67. package/dist/core/__tests__/full/spike-harness.test.js +117 -0
  68. package/dist/core/__tests__/grace-clock.test.js +72 -75
  69. package/dist/core/__tests__/helpers/harness.d.ts +35 -1
  70. package/dist/core/__tests__/helpers/harness.js +70 -12
  71. package/dist/core/__tests__/live-mutation-verbs.test.js +100 -105
  72. package/dist/core/__tests__/live-mutation.test.js +111 -134
  73. package/dist/core/__tests__/memory.test.js +4 -4
  74. package/dist/core/__tests__/revive.test.js +96 -86
  75. package/dist/core/__tests__/subscription-delivery.test.js +116 -138
  76. package/dist/core/__tests__/wake-origin.test.js +54 -50
  77. package/dist/core/bootstrap.js +18 -14
  78. package/dist/core/bridge-map.d.ts +19 -0
  79. package/dist/core/bridge-map.js +73 -0
  80. package/dist/core/canvas/history.d.ts +91 -0
  81. package/dist/core/canvas/history.js +389 -0
  82. package/dist/core/canvas/index.d.ts +1 -0
  83. package/dist/core/canvas/index.js +1 -0
  84. package/dist/core/config.js +3 -22
  85. package/dist/core/help.d.ts +2 -2
  86. package/dist/core/help.js +1 -1
  87. package/dist/core/memory-resolver.d.ts +9 -2
  88. package/dist/core/memory-resolver.js +47 -7
  89. package/dist/core/render.d.ts +4 -3
  90. package/dist/core/render.js +38 -41
  91. package/dist/core/resolver.d.ts +1 -30
  92. package/dist/core/resolver.js +6 -546
  93. package/dist/core/runtime/bearings.d.ts +2 -2
  94. package/dist/core/runtime/bearings.js +2 -2
  95. package/dist/core/runtime/persona.js +5 -7
  96. package/dist/core/scope.d.ts +7 -5
  97. package/dist/core/scope.js +16 -17
  98. package/dist/core/substrate/ceiling.d.ts +28 -0
  99. package/dist/core/substrate/ceiling.js +87 -0
  100. package/dist/core/substrate/index.d.ts +2 -0
  101. package/dist/core/substrate/index.js +1 -0
  102. package/dist/core/substrate/on-read.js +18 -2
  103. package/dist/core/substrate/render.d.ts +3 -5
  104. package/dist/core/substrate/render.js +68 -59
  105. package/dist/core/substrate/schema.d.ts +18 -13
  106. package/dist/core/substrate/schema.js +12 -11
  107. package/dist/pi-extensions/canvas-context-intro.js +5 -3
  108. package/dist/pi-extensions/canvas-inbox-watcher.js +19 -2
  109. package/dist/types.d.ts +3 -18
  110. package/dist/types.js +0 -8
  111. package/package.json +4 -3
  112. package/dist/builtin-skills/.crouter-plugin/plugin.json +0 -5
  113. package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +0 -157
  114. package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +0 -106
  115. package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +0 -49
  116. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +0 -49
  117. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +0 -156
  118. package/dist/builtin-skills/skills/design/SKILL.md +0 -51
  119. package/dist/builtin-skills/skills/development/SKILL.md +0 -109
  120. package/dist/builtin-skills/skills/planning/SKILL.md +0 -59
  121. package/dist/builtin-skills/skills/spec/SKILL.md +0 -83
  122. package/dist/commands/skill/state.d.ts +0 -3
  123. package/dist/commands/skill/state.js +0 -71
  124. package/dist/core/__tests__/broker-crash-teardown.test.js +0 -116
  125. package/dist/core/__tests__/broker-dialogs.test.js +0 -126
  126. package/dist/core/__tests__/broker-lifecycle.test.js +0 -87
  127. package/dist/core/__tests__/resolver.test.js +0 -181
  128. package/dist/core/__tests__/spike-harness.test.js +0 -242
  129. /package/dist/{core/__tests__/broker-attach-limits.test.d.ts → commands/memory/__tests__/lint-schema.test.d.ts} +0 -0
  130. /package/dist/core/__tests__/{broker-attach-stream.test.d.ts → full/broker-attach-limits.test.d.ts} +0 -0
  131. /package/dist/core/__tests__/{broker-crash-teardown.test.d.ts → full/broker-attach-stream.test.d.ts} +0 -0
  132. /package/dist/core/__tests__/{broker-dialogs.test.d.ts → full/broker-crash-teardown.test.d.ts} +0 -0
  133. /package/dist/core/__tests__/{broker-lifecycle.test.d.ts → full/broker-dialogs.test.d.ts} +0 -0
  134. /package/dist/core/__tests__/{cascade-close.test.d.ts → full/broker-lifecycle.test.d.ts} +0 -0
  135. /package/dist/core/__tests__/{dead-pane-regression.test.d.ts → full/cascade-close.test.d.ts} +0 -0
  136. /package/dist/core/__tests__/{detach-focus.test.d.ts → full/daemon-liveness-pane.full.test.d.ts} +0 -0
  137. /package/dist/core/__tests__/{human-new-window-regression.test.d.ts → full/dead-pane-regression.test.d.ts} +0 -0
  138. /package/dist/core/__tests__/{placement-focus.test.d.ts → full/detach-focus.test.d.ts} +0 -0
  139. /package/dist/core/__tests__/{placement-reconcile.test.d.ts → full/human-new-window-regression.test.d.ts} +0 -0
  140. /package/dist/core/__tests__/{placement-revive.test.d.ts → full/placement-focus.test.d.ts} +0 -0
  141. /package/dist/core/__tests__/{placement-teardown.test.d.ts → full/placement-reconcile.test.d.ts} +0 -0
  142. /package/dist/core/__tests__/{resolver.test.d.ts → full/placement-revive.test.d.ts} +0 -0
  143. /package/dist/core/__tests__/{review-render-pane-regression.test.d.ts → full/placement-teardown.test.d.ts} +0 -0
  144. /package/dist/core/__tests__/{spike-harness.test.d.ts → full/review-render-pane-regression.test.d.ts} +0 -0
@@ -1,9 +1,7 @@
1
1
  ---
2
2
  kind: skill
3
- when: When a task relates to marketplaces
4
- why: How to author a crtr marketplace marketplace.json index, plugin entries,
5
- symlink-based install, auto-bump CI, dual-publishing. Use when creating a
6
- marketplace or contributing plugins to one.
3
+ when-and-why-to-read: When creating a crtr marketplace or contributing plugins
4
+ to one, this skill should be read.
7
5
  short-form: How to author a crtr marketplace — marketplace.json index, plugin
8
6
  entries, symlink-based install, auto-bump CI, dual-publishing. Use when
9
7
  creating a marketplace or contributing plugins to one.
@@ -1,10 +1,8 @@
1
1
  ---
2
2
  kind: skill
3
- when: When a task relates to base prompt
4
- why: How to write a base persona prompt (the mode=base PERSONA.md) — the system
5
- prompt for a single-window worker node. Covers what a base persona is for,
6
- what to put in it, the identity/deliverable/boundary/report shape, and the
7
- voice to use. Use when writing or revising a <kind>/PERSONA.md.
3
+ when-and-why-to-read: When writing or revising a base persona prompt (a
4
+ <kind>/PERSONA.md, the system prompt for a single-window worker node), this
5
+ skill should be read.
8
6
  short-form: How to write a base persona prompt (the mode=base PERSONA.md) — the
9
7
  system prompt for a single-window worker node. Covers what a base persona is
10
8
  for, what to put in it, the identity/deliverable/boundary/report shape, and
@@ -1,11 +1,8 @@
1
1
  ---
2
2
  kind: skill
3
- when: When a task relates to orchestrator prompt
4
- why: How to write an orchestrator persona prompt (orchestrator.md) — the system
5
- prompt for a resident coordinator node. Covers the kernel-vs-kind split, what
6
- belongs in the per-kind body, naming the child pipeline, the roadmapSkill
7
- pointer, and the @include rule. Use when writing or revising a
8
- <kind>/orchestrator.md.
3
+ when-and-why-to-read: When writing or revising an orchestrator persona prompt (a
4
+ <kind>/orchestrator.md, the system prompt for a resident coordinator node),
5
+ this skill should be read.
9
6
  short-form: How to write an orchestrator persona prompt (orchestrator.md) — the
10
7
  system prompt for a resident coordinator node. Covers the kernel-vs-kind
11
8
  split, what belongs in the per-kind body, naming the child pipeline, the
@@ -1,11 +1,7 @@
1
1
  ---
2
2
  kind: skill
3
- when: When a task relates to personas
4
- why: How to define a custom node kind (persona) for crtr — the
5
- PERSONA.md/orchestrator files, the frontmatter contract (incl. whenToUse),
6
- nested sub-personas, scope resolution and overrides, and how to write the
7
- prose. Use when adding a new `--kind`, overriding a builtin agent, or
8
- debugging persona resolution.
3
+ when-and-why-to-read: When adding a new `--kind`, overriding a builtin agent, or
4
+ debugging persona resolution, this skill should be read.
9
5
  short-form: How to define a custom node kind (persona) for crtr — the
10
6
  PERSONA.md/orchestrator files, the frontmatter contract (incl. whenToUse),
11
7
  nested sub-personas, scope resolution and overrides, and how to write the
@@ -1,9 +1,7 @@
1
1
  ---
2
2
  kind: skill
3
- when: When a task relates to plugins
4
- why: How to author a crtr plugin plugin.json manifest, directory layout,
5
- scopes, install mechanics, versioning. Use when creating a new plugin,
6
- packaging skills for distribution, or debugging install/resolution.
3
+ when-and-why-to-read: When creating a crtr plugin, packaging skills for
4
+ distribution, or debugging install/resolution, this skill should be read.
7
5
  short-form: How to author a crtr plugin — plugin.json manifest, directory
8
6
  layout, scopes, install mechanics, versioning. Use when creating a new plugin,
9
7
  packaging skills for distribution, or debugging install/resolution.
@@ -1,9 +1,7 @@
1
1
  ---
2
2
  kind: skill
3
- when: When shaping a design roadmap or producing an architecture/interface design
4
- why: covers what a design deliverable is, the design-artifact shape, when to go
5
- top-down vs bottom-up, and how to decompose a large design into composable
6
- sub-designs.
3
+ when-and-why-to-read: When shaping a design roadmap or producing an
4
+ architecture/interface design, this skill should be read.
7
5
  short-form: Use when shaping a design roadmap or producing an
8
6
  architecture/interface design — covers what a design deliverable is, the
9
7
  design-artifact shape, when to go top-down vs bottom-up, and how to decompose
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  kind: skill
3
- when: When shaping or reshaping a build roadmap
4
- why: choosing a development style, selecting a phase skeleton, or setting exit
5
- criteria for a software goal.
3
+ when-and-why-to-read: When shaping or reshaping a build roadmap — choosing a
4
+ development style, selecting a phase skeleton, or setting exit criteria for a
5
+ software goal this skill should be read.
6
6
  short-form: Use when shaping or reshaping a build roadmap — choosing a
7
7
  development style, selecting a phase skeleton, or setting exit criteria for a
8
8
  software goal.
@@ -1,9 +1,8 @@
1
1
  ---
2
2
  kind: skill
3
- when: When shaping a planning roadmap, deciding plan structure, or fanning out
4
- plan-review specialists before declaring a plan ready
5
- why: Use when shaping a planning roadmap, deciding plan structure, or fanning
6
- out plan-review specialists before declaring a plan ready.
3
+ when-and-why-to-read: When shaping a planning roadmap, deciding plan structure,
4
+ or fanning out plan-review specialists before declaring a plan ready, this
5
+ skill should be read.
7
6
  short-form: Use when shaping a planning roadmap, deciding plan structure, or
8
7
  fanning out plan-review specialists before declaring a plan ready.
9
8
  system-prompt-visibility: name
@@ -1,15 +1,8 @@
1
1
  ---
2
2
  kind: skill
3
- when: When running a specification effort, shaping a spec roadmap, or deciding
4
- how to stage design and requirements work. Covers the three-stage
5
- shape→design→requirements methodology, when to delegate design to a child
6
- node, the isolation principle behind the design/requirements split, and what a
7
- finished spec contains
8
- why: Use when running a specification effort, shaping a spec roadmap, or
9
- deciding how to stage design and requirements work. Covers the three-stage
10
- shape→design→requirements methodology, when to delegate design to a child
11
- node, the isolation principle behind the design/requirements split, and what a
12
- finished spec contains.
3
+ when-and-why-to-read: When running a specification effort, shaping a spec
4
+ roadmap, or deciding how to stage design and requirements work, this skill
5
+ should be read.
13
6
  short-form: Use when running a specification effort, shaping a spec roadmap, or
14
7
  deciding how to stage design and requirements work. Covers the three-stage
15
8
  shape→design→requirements methodology, when to delegate design to a child
@@ -52,7 +52,7 @@ Separate from the roadmap (your live plan and state) you have a persistent docum
52
52
 
53
53
  **Reading.** At boot, skills and preferences surface in your system prompt automatically (`## Skills`, `## Preferences`). References surface in your `<crtr-context>` block (`## References`). To browse the full inventory: `crtr memory list`. To search by topic: `crtr memory find <query>`. To load a document by name: `crtr memory read <name>`.
54
54
 
55
- **Writing.** Use `crtr memory write` to create or update a document. Every document carries `kind`, `when`, and `why` in its frontmatter, plus a body. The `kind` governs which section it surfaces in at boot and how it loads:
55
+ **Writing.** Use `crtr memory write` to create or update a document. Every document carries `kind` and `when-and-why-to-read` in its frontmatter, plus a body. `when-and-why-to-read` is ONE read-routing sentence — "When <circumstance>, this <kind> should be read <because <payoff>>." — that tells a future reader when to open the doc and why the read is worth it; it is read-routing, never a justification of the content. It becomes the preview line verbatim. The `kind` governs which section it surfaces in at boot and how it loads:
56
56
 
57
57
  - `skill` — a workflow or methodology to adopt. Surfaces by name in `## Skills`; load with `crtr memory read`.
58
58
  - `preference` — how you should work. Surfaces as a `###` routing line in `## Preferences` at boot (default `system-prompt-visibility: preview`).
@@ -0,0 +1 @@
1
+ export declare const readLeaf: import("../../core/command.js").LeafDef;
@@ -0,0 +1,61 @@
1
+ import { defineLeaf } from '../../core/command.js';
2
+ import { notFound, usage } from '../../core/errors.js';
3
+ import { resolveRef } from '../../core/canvas/index.js';
4
+ export const readLeaf = defineLeaf({
5
+ name: 'read',
6
+ description: 'read one search hit\'s full body by its ref',
7
+ whenToUse: 'you picked a hit from `canvas history search` (or `canvas history show`) and want its full content — pass the `<node-id>:<relpath>` ref verbatim. Strips frontmatter by default; add --frontmatter to keep the YAML header.',
8
+ help: {
9
+ name: 'canvas history read',
10
+ summary: 'resolve a <node-id>:<relpath> ref to its full artifact body',
11
+ params: [
12
+ { kind: 'positional', name: 'ref', required: true, constraint: 'The <node-id>:<relpath> handle from `canvas history search` (e.g. mq186ky0-c754531c:reports/20260607T075536-final.md, or <node-id>:meta for a node\'s identity).' },
13
+ { kind: 'flag', name: 'frontmatter', type: 'bool', required: false, constraint: 'Include the artifact\'s YAML frontmatter. Stripped by default.' },
14
+ ],
15
+ output: [
16
+ { name: 'ref', type: 'string', required: true, constraint: 'Echo of the resolved ref.' },
17
+ { name: 'node', type: 'string', required: true, constraint: 'Node name + id.' },
18
+ { name: 'source', type: 'string', required: true, constraint: 'report | doc | roadmap | meta (+ report-kind for reports).' },
19
+ { name: 'ts', type: 'string', required: true, constraint: 'Artifact timestamp.' },
20
+ { name: 'content', type: 'string', required: true, constraint: 'Full body. Frontmatter stripped unless --frontmatter.' },
21
+ ],
22
+ outputKind: 'object',
23
+ effects: ['None. Read-only.'],
24
+ },
25
+ run: async (input) => {
26
+ const ref = input['ref'].trim();
27
+ const includeFrontmatter = input['frontmatter'] === true;
28
+ if (!ref.includes(':')) {
29
+ throw usage(`ref must be <node-id>:<relpath>; received: ${ref}`, {
30
+ received: ref,
31
+ next: 'Get a valid ref from `canvas history search` or `canvas history show <node-id>`.',
32
+ });
33
+ }
34
+ let resolved;
35
+ try {
36
+ resolved = resolveRef(ref);
37
+ }
38
+ catch (e) {
39
+ throw usage(`ref rejected: ${e.message}`, { received: ref, next: 'Refs come verbatim from `canvas history search`; do not hand-construct relpaths.' });
40
+ }
41
+ if (resolved === null) {
42
+ throw notFound(`no artifact for ref: ${ref}`, {
43
+ ref,
44
+ next: 'The node or file may not exist. Re-run `canvas history search` for a current ref, or `canvas history show <node-id>` to list a node\'s artifacts.',
45
+ });
46
+ }
47
+ const source = resolved.source === 'report' && resolved.reportKind ? `report:${resolved.reportKind}` : resolved.source;
48
+ const content = includeFrontmatter && resolved.raw !== null ? resolved.raw : resolved.body;
49
+ return {
50
+ ref,
51
+ node: `${resolved.nodeName} (${resolved.nodeId})`,
52
+ source,
53
+ ts: resolved.ts,
54
+ content,
55
+ };
56
+ },
57
+ render: (r) => {
58
+ const head = `${r['node']} | source: ${r['source']} | ts: ${r['ts']} | ref: ${r['ref']}`;
59
+ return `${head}\n\n${String(r['content']).trim()}`;
60
+ },
61
+ });
@@ -0,0 +1 @@
1
+ export declare const searchLeaf: import("../../core/command.js").LeafDef;
@@ -0,0 +1,270 @@
1
+ import { defineLeaf } from '../../core/command.js';
2
+ import { usage } from '../../core/errors.js';
3
+ import { buildCorpus, } from '../../core/canvas/index.js';
4
+ const TYPES = ['report', 'doc', 'roadmap', 'meta'];
5
+ const NODE_KINDS = ['developer', 'spec', 'design', 'review', 'explore', 'plan', 'general'];
6
+ const STATUSES = ['active', 'idle', 'done', 'dead', 'canceled'];
7
+ const SORTS = ['relevance', 'recency', 'oldest'];
8
+ const REL_UNITS = {
9
+ s: 1000,
10
+ m: 60_000,
11
+ h: 3_600_000,
12
+ d: 86_400_000,
13
+ w: 604_800_000,
14
+ mo: 2_592_000_000,
15
+ y: 31_536_000_000,
16
+ };
17
+ /** ISO 8601 (`2026-06-01`) → epoch ms (absolute); relative (`7d`, `2w`, `1mo`)
18
+ * → now minus the duration. Throws usage on an unparseable value. */
19
+ function parseWhen(field, raw) {
20
+ const rel = raw.match(/^(\d+)(mo|[smhdwy])$/);
21
+ if (rel !== null)
22
+ return Date.now() - Number(rel[1]) * REL_UNITS[rel[2]];
23
+ const abs = Date.parse(raw);
24
+ if (!Number.isNaN(abs))
25
+ return abs;
26
+ throw usage(`--${field} must be ISO 8601 (2026-06-01) or relative (7d, 2w, 1mo); received: ${raw}`);
27
+ }
28
+ function splitCsv(v) {
29
+ if (v === undefined || v === '')
30
+ return undefined;
31
+ return v.split(',').map((s) => s.trim()).filter((s) => s !== '');
32
+ }
33
+ function encodeCursor(offset) {
34
+ return Buffer.from(String(offset), 'utf8').toString('base64');
35
+ }
36
+ function decodeCursor(c) {
37
+ const n = Number(Buffer.from(c, 'base64').toString('utf8'));
38
+ if (!Number.isInteger(n) || n < 0)
39
+ throw usage(`invalid --cursor: ${c}`);
40
+ return n;
41
+ }
42
+ /** One snippet block: the matching line(s), `lines` deep. Falls back to the
43
+ * title or the first body lines when nothing matched on the body. */
44
+ function snippet(a, terms, lines) {
45
+ const body = a.loadBody();
46
+ const rows = body.split('\n');
47
+ if (terms.length > 0) {
48
+ const idx = rows.findIndex((r) => {
49
+ const lc = r.toLowerCase();
50
+ return terms.some((t) => lc.includes(t));
51
+ });
52
+ if (idx !== -1) {
53
+ return rows
54
+ .slice(idx, idx + lines)
55
+ .map((r) => r.trim())
56
+ .filter((r) => r !== '')
57
+ .join(' ⏎ ');
58
+ }
59
+ }
60
+ const lead = rows.map((r) => r.trim()).filter((r) => r !== '').slice(0, lines).join(' ⏎ ');
61
+ return lead !== '' ? lead : a.title;
62
+ }
63
+ function sortArtifacts(arts, mode, scored) {
64
+ const out = [...arts];
65
+ if (mode === 'relevance' && scored !== undefined) {
66
+ out.sort((x, y) => (scored.get(y.ref) ?? 0) - (scored.get(x.ref) ?? 0) || y.tsMs - x.tsMs || x.ref.localeCompare(y.ref));
67
+ }
68
+ else if (mode === 'oldest') {
69
+ out.sort((x, y) => x.tsMs - y.tsMs || x.ref.localeCompare(y.ref));
70
+ }
71
+ else {
72
+ out.sort((x, y) => y.tsMs - x.tsMs || x.ref.localeCompare(y.ref));
73
+ }
74
+ return out;
75
+ }
76
+ export const searchLeaf = defineLeaf({
77
+ name: 'search',
78
+ description: 'ranked content search across the cwd\'s node history',
79
+ whenToUse: 'you want to find past work by what it SAYS — a design, a final report, a roadmap, a finding — across every node that ran in this cwd, ranked by relevance. Omit the query to browse the record by recency ("what happened here lately"). Narrow with --type/--kind/--status/--since, scope elsewhere with --cwd/--all-cwds/--under/--node, or switch to --grep for an exact regex over bodies. Use `canvas history read <ref>` to read a hit in full, `canvas history show <node-id>` to list one node\'s artifacts, and `canvas revive <id>` to reopen a node you found.',
80
+ help: {
81
+ name: 'canvas history search',
82
+ summary: 'ranked/filtered/sorted content search over the per-cwd episodic record (reports + context docs + meta)',
83
+ params: [
84
+ { kind: 'positional', name: 'query', required: false, constraint: 'Whitespace-separated terms, matched case-insensitively and weighted across node name/description, report titles, and doc headings (and full bodies with --body); artifacts matching more/stronger fields rank higher. Omit to browse by recency (no relevance ranking; --sort still applies). Under --grep: an ECMAScript regex matched against bodies (line hits).' },
85
+ { kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Search another project\'s record. Default: the calling node\'s cwd (process cwd when not a node). Mutually exclusive with --all-cwds.' },
86
+ { kind: 'flag', name: 'all-cwds', type: 'bool', required: false, constraint: 'Search every cwd on the canvas. Mutually exclusive with --cwd.' },
87
+ { kind: 'flag', name: 'under', type: 'string', required: false, constraint: 'Restrict to a node and its subscription descendants (one initiative / sub-DAG).' },
88
+ { kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Restrict to specific node ids. Comma-separated for several.' },
89
+ { kind: 'flag', name: 'type', type: 'string', required: false, constraint: `Corpus to search; comma-separated for several. One of: ${TYPES.join(', ')}. report = push history; doc = context artifacts; roadmap = roadmap.md specifically; meta = node name/description/kind. Default: all.` },
90
+ { kind: 'flag', name: 'report-kind', type: 'enum', choices: ['final', 'update'], required: false, constraint: 'Narrow reports to outcome summaries (final) or progress (update). Implies --type report.' },
91
+ { kind: 'flag', name: 'kind', type: 'string', required: false, constraint: `Node kind; comma-separated for several. One of: ${NODE_KINDS.join(', ')}.` },
92
+ { kind: 'flag', name: 'status', type: 'string', required: false, constraint: `Node lifecycle status; comma-separated for several. One of: ${STATUSES.join(', ')} (e.g. done = completed work only).` },
93
+ { kind: 'flag', name: 'since', type: 'string', required: false, constraint: 'Lower bound on artifact timestamp. ISO 8601 (2026-06-01) or relative (7d, 2w, 1mo).' },
94
+ { kind: 'flag', name: 'until', type: 'string', required: false, constraint: 'Upper bound on artifact timestamp. ISO 8601 or relative.' },
95
+ { kind: 'flag', name: 'grep', type: 'bool', required: false, constraint: 'Treat the query as an ECMAScript regex matched against bodies (one hit per matching line) instead of weighted ranking. Requires a query. Mutually exclusive with --body.' },
96
+ { kind: 'flag', name: 'body', type: 'bool', required: false, constraint: 'Also weigh full body text in the ranking (default weighs name/title/heading only). Ignored under --grep.' },
97
+ { kind: 'flag', name: 'sort', type: 'enum', choices: SORTS, required: false, constraint: 'relevance (default with a query) ranks by score; recency (default without a query) / oldest order by artifact timestamp.' },
98
+ { kind: 'flag', name: 'snippet-lines', type: 'int', required: false, default: 1, constraint: 'Context lines per hit. Default 1.' },
99
+ { kind: 'flag', name: 'full', type: 'bool', required: false, constraint: 'Inline each hit\'s full body. Off by default; pair with a small --limit. Ignored under --grep.' },
100
+ { kind: 'flag', name: 'limit', type: 'int', required: false, default: 20, constraint: 'Page size. Default 20, hard max 100.' },
101
+ { kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: 'Opaque token from a prior next_cursor; omit on the first call.' },
102
+ ],
103
+ output: [
104
+ { name: 'hits', type: 'object[]', required: true, constraint: 'Ranked/sorted hits, each: {ref, node, kind, source, ts, score?, snippet}. Under --grep: {ref, node, ts, line, text} per matching body line, sorted by ref then line. ref is the <node-id>:<relpath> handle passed verbatim to `canvas history read`.' },
105
+ { name: 'next_cursor', type: 'string|null', required: true, constraint: 'Token for the next page; null is the only end-of-list signal.' },
106
+ { name: 'total', type: 'int', required: true, constraint: 'Total matching artifacts (exact).' },
107
+ { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands.' },
108
+ ],
109
+ outputKind: 'object',
110
+ effects: ['None. Read-only.'],
111
+ },
112
+ run: async (input) => {
113
+ const query = (input['query'] ?? '').trim();
114
+ const cwd = input['cwd'];
115
+ const allCwds = input['allCwds'] === true;
116
+ const under = input['under'];
117
+ const nodes = splitCsv(input['node']);
118
+ const grep = input['grep'] === true;
119
+ const weighBody = input['body'] === true;
120
+ const full = input['full'] === true;
121
+ const snippetLines = Math.max(1, input['snippetLines'] ?? 1);
122
+ const limit = Math.min(100, Math.max(1, input['limit'] ?? 20));
123
+ const cursor = input['cursor'];
124
+ if (cwd !== undefined && allCwds)
125
+ throw usage('--cwd and --all-cwds are mutually exclusive.');
126
+ if (grep && weighBody)
127
+ throw usage('--grep and --body are mutually exclusive (--grep always scans bodies).');
128
+ if (grep && query === '')
129
+ throw usage('--grep requires a query (the regex). Omit --grep to browse by recency.');
130
+ const reportKind = input['reportKind'];
131
+ const types = splitCsv(input['type']);
132
+ if (types !== undefined) {
133
+ const bad = types.find((t) => !TYPES.includes(t));
134
+ if (bad !== undefined)
135
+ throw usage(`--type must be one of: ${TYPES.join(', ')}; received: ${bad}`);
136
+ }
137
+ const scope = { cwd, allCwds, under, nodes };
138
+ const corpus = {
139
+ types,
140
+ reportKind,
141
+ kinds: splitCsv(input['kind']),
142
+ statuses: splitCsv(input['status']),
143
+ sinceMs: input['since'] !== undefined ? parseWhen('since', input['since']) : undefined,
144
+ untilMs: input['until'] !== undefined ? parseWhen('until', input['until']) : undefined,
145
+ };
146
+ const arts = buildCorpus(scope, corpus);
147
+ const offset = cursor !== undefined ? decodeCursor(cursor) : 0;
148
+ // --- grep mode: regex over bodies, one row per matching line ---
149
+ if (grep) {
150
+ let re;
151
+ try {
152
+ re = new RegExp(query, 'i');
153
+ }
154
+ catch {
155
+ throw usage(`invalid regex pattern: ${query}`);
156
+ }
157
+ const lineHits = [];
158
+ for (const a of arts) {
159
+ const rows = a.loadBody().split('\n');
160
+ rows.forEach((text, i) => {
161
+ if (re.test(text))
162
+ lineHits.push({ ref: a.ref, node: `${a.nodeName} (${a.nodeId})`, ts: a.ts, line: i + 1, text: text.trim() });
163
+ });
164
+ }
165
+ lineHits.sort((x, y) => x.ref.localeCompare(y.ref) || x.line - y.line);
166
+ const page = lineHits.slice(offset, offset + limit);
167
+ const nextCursor = offset + limit < lineHits.length ? encodeCursor(offset + limit) : null;
168
+ return {
169
+ hits: page,
170
+ next_cursor: nextCursor,
171
+ total: lineHits.length,
172
+ follow_up: 'Read a hit with `canvas history read <ref>`. Drop --grep for a ranked topic search; narrow with `canvas history search -h`.',
173
+ };
174
+ }
175
+ // --- ranked / browse mode ---
176
+ const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
177
+ let pool = arts;
178
+ let scored;
179
+ const sortMode = input['sort'] ?? (terms.length > 0 ? 'relevance' : 'recency');
180
+ if (terms.length > 0 && sortMode === 'relevance') {
181
+ scored = new Map();
182
+ const matched = [];
183
+ for (const a of arts) {
184
+ const titleLc = a.title.toLowerCase();
185
+ const nameLc = a.nodeName.toLowerCase();
186
+ const descLc = a.nodeDesc.toLowerCase();
187
+ const bodyLc = weighBody ? a.loadBody().toLowerCase() : null;
188
+ let score = 0;
189
+ for (const t of terms) {
190
+ if (titleLc.includes(t))
191
+ score += 10;
192
+ if (nameLc.includes(t))
193
+ score += 5;
194
+ if (descLc.includes(t))
195
+ score += 3;
196
+ if (bodyLc !== null && bodyLc.includes(t))
197
+ score += 1;
198
+ }
199
+ if (score > 0) {
200
+ scored.set(a.ref, score);
201
+ matched.push(a);
202
+ }
203
+ }
204
+ pool = matched;
205
+ }
206
+ else if (terms.length > 0) {
207
+ // Query present but a timeline sort requested: filter by match, order by ts.
208
+ pool = arts.filter((a) => {
209
+ const hay = `${a.title}\n${a.nodeName}\n${a.nodeDesc}${weighBody ? '\n' + a.loadBody() : ''}`.toLowerCase();
210
+ return terms.some((t) => hay.includes(t));
211
+ });
212
+ }
213
+ const sorted = sortArtifacts(pool, sortMode, scored);
214
+ const page = sorted.slice(offset, offset + limit);
215
+ const nextCursor = offset + limit < sorted.length ? encodeCursor(offset + limit) : null;
216
+ const hits = page.map((a) => {
217
+ const base = {
218
+ ref: a.ref,
219
+ node: `${a.nodeName} (${a.nodeId})`,
220
+ kind: a.nodeKind,
221
+ source: a.source === 'report' && a.reportKind ? `report:${a.reportKind}` : a.source,
222
+ ts: a.ts,
223
+ };
224
+ if (sortMode === 'relevance' && scored !== undefined)
225
+ base['score'] = scored.get(a.ref) ?? 0;
226
+ if (full)
227
+ base['body'] = a.loadBody();
228
+ else
229
+ base['snippet'] = snippet(a, terms, snippetLines);
230
+ return base;
231
+ });
232
+ return {
233
+ hits,
234
+ next_cursor: nextCursor,
235
+ total: sorted.length,
236
+ follow_up: 'Read a hit with `canvas history read <ref>`; narrow with `canvas history search -h`.',
237
+ };
238
+ },
239
+ render: (r) => renderSearch(r),
240
+ });
241
+ function renderSearch(r) {
242
+ const hits = r['hits'];
243
+ const total = r['total'];
244
+ const nextCursor = r['next_cursor'];
245
+ const full = hits.length > 0 && Object.prototype.hasOwnProperty.call(hits[0], 'body');
246
+ const parts = [];
247
+ if (hits.length === 0) {
248
+ parts.push('0 hits.');
249
+ }
250
+ else if (full) {
251
+ parts.push(`${hits.length} of ${total} hits:`);
252
+ for (const h of hits) {
253
+ const head = `### ${h['ref']}\n- node: ${h['node']} | kind: ${h['kind']} | source: ${h['source']} | ts: ${h['ts']}`;
254
+ parts.push(`${head}\n\n${String(h['body'] ?? '').trim()}`);
255
+ }
256
+ }
257
+ else {
258
+ parts.push(`${hits.length} of ${total} hits:`);
259
+ const cols = Object.keys(hits[0]).filter((c) => c !== 'body');
260
+ const head = `| ${cols.join(' | ')} |`;
261
+ const sep = `| ${cols.map(() => '---').join(' | ')} |`;
262
+ const body = hits
263
+ .map((h) => `| ${cols.map((c) => String(h[c] ?? '').replace(/\|/g, '\\|').replace(/\n/g, ' ')).join(' | ')} |`)
264
+ .join('\n');
265
+ parts.push(`${head}\n${sep}\n${body}`);
266
+ }
267
+ parts.push(`- next_cursor: ${nextCursor ?? 'null'}\n- total: ${total}`);
268
+ parts.push(String(r['follow_up']));
269
+ return parts.join('\n\n');
270
+ }
@@ -0,0 +1 @@
1
+ export declare const showLeaf: import("../../core/command.js").LeafDef;
@@ -0,0 +1,79 @@
1
+ import { defineLeaf } from '../../core/command.js';
2
+ import { notFound } from '../../core/errors.js';
3
+ import { getNode, nodeArtifacts } from '../../core/canvas/index.js';
4
+ import { statSync } from 'node:fs';
5
+ const TYPES = ['report', 'doc', 'roadmap'];
6
+ export const showLeaf = defineLeaf({
7
+ name: 'show',
8
+ description: 'enumerate one node\'s artifacts as refs',
9
+ whenToUse: 'you have a node id and want to see everything it left behind — its reports and context docs as refs ready for `canvas history read`. The bridge from "found the node" to "read its artifacts". Use `node inspect show` instead for the node\'s topology/neighbors, not its content.',
10
+ help: {
11
+ name: 'canvas history show',
12
+ summary: 'list one node\'s artifacts (reports + context docs) as refs ready for `canvas history read`',
13
+ params: [
14
+ { kind: 'positional', name: 'node-id', required: true, constraint: 'The node whose artifacts to list.' },
15
+ { kind: 'flag', name: 'type', type: 'enum', choices: [...TYPES], required: false, constraint: 'Narrow to one corpus: report | doc | roadmap. Default: all.' },
16
+ { kind: 'flag', name: 'sort', type: 'enum', choices: ['recency', 'oldest'], required: false, constraint: 'Order by artifact timestamp. Default recency.' },
17
+ ],
18
+ output: [
19
+ { name: 'node', type: 'string', required: true, constraint: 'Node name + id.' },
20
+ { name: 'artifacts', type: 'object[]', required: true, constraint: 'Each: {ref, source, ts, detail}. source = report:<kind> | doc | roadmap. detail = report title / doc heading + size. ref passes verbatim to `canvas history read`.' },
21
+ { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands.' },
22
+ ],
23
+ outputKind: 'object',
24
+ effects: ['None. Read-only.'],
25
+ },
26
+ run: async (input) => {
27
+ const nodeId = input['nodeId'].trim();
28
+ const type = input['type'];
29
+ const sort = input['sort'] ?? 'recency';
30
+ if (getNode(nodeId) === null) {
31
+ throw notFound(`unknown node: ${nodeId}`, {
32
+ received: nodeId,
33
+ next: 'List nodes with `crtr node inspect list`, or find one with `canvas history search`.',
34
+ });
35
+ }
36
+ const arts = nodeArtifacts(nodeId, type !== undefined ? [type] : undefined);
37
+ arts.sort((a, b) => (sort === 'oldest' ? a.tsMs - b.tsMs : b.tsMs - a.tsMs) || a.ref.localeCompare(b.ref));
38
+ const artifacts = arts.map((a) => {
39
+ let size = 0;
40
+ try {
41
+ if (a.path !== null)
42
+ size = statSync(a.path).size;
43
+ }
44
+ catch {
45
+ /* ignore */
46
+ }
47
+ return {
48
+ ref: a.ref,
49
+ source: a.source === 'report' && a.reportKind ? `report:${a.reportKind}` : a.source,
50
+ ts: a.ts,
51
+ detail: a.title + (size > 0 ? ` (${size}B)` : ''),
52
+ };
53
+ });
54
+ const node = getNode(nodeId);
55
+ return {
56
+ node: `${node?.name} (${nodeId})`,
57
+ artifacts,
58
+ follow_up: 'Read one with `canvas history read <ref>`; reopen the node with `canvas revive ' + nodeId + '`.',
59
+ };
60
+ },
61
+ render: (r) => {
62
+ const arts = r['artifacts'];
63
+ const parts = [`node: ${r['node']}`];
64
+ if (arts.length === 0) {
65
+ parts.push('0 artifacts.');
66
+ }
67
+ else {
68
+ const cols = Object.keys(arts[0]);
69
+ const head = `| ${cols.join(' | ')} |`;
70
+ const sep = `| ${cols.map(() => '---').join(' | ')} |`;
71
+ const body = arts
72
+ .map((a) => `| ${cols.map((c) => String(a[c] ?? '').replace(/\|/g, '\\|').replace(/\n/g, ' ')).join(' | ')} |`)
73
+ .join('\n');
74
+ parts.push(`${arts.length} artifacts:\n\n${head}\n${sep}\n${body}`);
75
+ }
76
+ parts.push(String(r['follow_up']));
77
+ return parts.join('\n\n');
78
+ },
79
+ });
@@ -0,0 +1,2 @@
1
+ import type { BranchDef } from '../core/command.js';
2
+ export declare const historyBranch: BranchDef;
@@ -0,0 +1,33 @@
1
+ // `canvas history` — search and recall the canvas's record of past work in a
2
+ // cwd. The accumulated episodic record of every node that ran here and the
3
+ // artifacts it left: its reports (final/update/urgent outcome summaries) and
4
+ // context docs (specs, designs, roadmaps, findings). Distinct from `crtr
5
+ // memory`, which is curated semantic knowledge held independent of any episode.
6
+ // This branch owns CONTENT; it never duplicates the graph (use `canvas revive`
7
+ // to reopen a found node, `node inspect show` for its topology).
8
+ import { defineBranch } from '../core/command.js';
9
+ import { searchLeaf } from './canvas-history/search.js';
10
+ import { readLeaf } from './canvas-history/read.js';
11
+ import { showLeaf } from './canvas-history/show.js';
12
+ import { callerCwd, corpusStats } from '../core/canvas/index.js';
13
+ /** The bounded per-cwd `<corpus>` aggregate for the branch -h — pre-empts a
14
+ * `search` call just to gauge the corpus (cli-design §15: aggregate, never
15
+ * enumerate). Soft-fails to omission if the db is unreachable. */
16
+ function corpusBlock() {
17
+ const cwd = callerCwd();
18
+ const s = corpusStats(cwd);
19
+ const span = s.span !== null ? `${s.span.from} → ${s.span.to}` : 'n/a';
20
+ return `<corpus cwd="${cwd}" nodes="${s.nodes}" reports="${s.reports}" docs="${s.docs}" span="${span}"/>`;
21
+ }
22
+ export const historyBranch = defineBranch({
23
+ name: 'history',
24
+ description: 'search and recall the canvas\'s record of past work in this cwd',
25
+ whenToUse: 'you want to find or re-read what was DONE in a cwd — a past design, a final report, a roadmap, a finding — across every node that ran there. Use it when picking up prior work ("that caching work from last week"), recovering an artifact, or surveying a project\'s history. Distinct from `crtr memory` (curated semantic knowledge, not episodes) and from `canvas dashboard`/`node inspect` (graph topology, not content).',
26
+ help: {
27
+ name: 'canvas history',
28
+ summary: 'search and recall the canvas\'s record of past work in this cwd',
29
+ model: 'The accumulated record of every node that ran in a cwd and the artifacts it left — its reports (final/update outcome summaries) and context docs (specs, designs, roadmaps, findings). `search` finds by content (ranked, filtered, sorted; omit the query to browse by recency); `read` pulls one hit\'s full body by its <node-id>:<relpath> ref; `show` lists everything one node left. This is the episodic record of what was DONE — distinct from `crtr memory`, which is curated semantic knowledge. To reopen a node you found, use `canvas revive`; for its place in the graph, `node inspect show`.',
30
+ dynamicState: corpusBlock,
31
+ },
32
+ children: [searchLeaf, readLeaf, showLeaf],
33
+ });
@@ -15,6 +15,7 @@ import { daemonBranch } from './daemon.js';
15
15
  import { tmuxSpreadLeaf } from './tmux-spread.js';
16
16
  import { chordLeaf } from './chord.js';
17
17
  import { canvasPruneLeaf } from './canvas-prune.js';
18
+ import { historyBranch } from './canvas-history.js';
18
19
  export function registerCanvas() {
19
20
  return defineBranch({
20
21
  name: 'canvas',
@@ -26,8 +27,8 @@ export function registerCanvas() {
26
27
  help: {
27
28
  name: 'canvas',
28
29
  summary: 'observe and supervise the whole agent graph',
29
- model: 'Canvas-wide operations, distinct from per-node work (`node`) and a node\'s own spine I/O (`push`/`feed`). `dashboard` renders the subscription forest as a tree; `browse` opens an interactive full-screen navigator (tabs/tree/search) over the whole canvas and resumes the chosen node; `attention` aggregates pending human asks across the graph; `revive` reopens a window for a done/idle/dead/canceled node; `daemon` manages the thin crtrd supervisor that auto-revives nodes on window exit; `prune` bounds growth by deleting terminal nodes past a TTL.',
30
+ model: 'Canvas-wide operations, distinct from per-node work (`node`) and a node\'s own spine I/O (`push`/`feed`). `dashboard` renders the subscription forest as a tree; `browse` opens an interactive full-screen navigator (tabs/tree/search) over the whole canvas and resumes the chosen node; `attention` aggregates pending human asks across the graph; `revive` reopens a window for a done/idle/dead/canceled node; `history` searches and recalls the content record (reports + context docs) of past work in a cwd; `daemon` manages the thin crtrd supervisor that auto-revives nodes on window exit; `prune` bounds growth by deleting terminal nodes past a TTL.',
30
31
  },
31
- children: [dashboardLeaf, browseLeaf, attentionBranch, reviveLeaf, tmuxSpreadLeaf, daemonBranch, chordLeaf, canvasPruneLeaf],
32
+ children: [dashboardLeaf, browseLeaf, attentionBranch, reviveLeaf, historyBranch, tmuxSpreadLeaf, daemonBranch, chordLeaf, canvasPruneLeaf],
32
33
  });
33
34
  }