@crouton-kit/crouter 0.3.20 → 0.3.21

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 (43) 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/human/queue.js +2 -2
  12. package/dist/commands/memory/__tests__/lint-schema.test.d.ts +1 -0
  13. package/dist/commands/memory/__tests__/lint-schema.test.js +29 -0
  14. package/dist/commands/memory/find.js +10 -15
  15. package/dist/commands/memory/lint.d.ts +7 -0
  16. package/dist/commands/memory/lint.js +10 -1
  17. package/dist/commands/memory/shared.js +1 -2
  18. package/dist/commands/memory/write.js +6 -9
  19. package/dist/commands/node.js +47 -25
  20. package/dist/commands/push.js +10 -15
  21. package/dist/commands/tmux-spread.js +16 -1
  22. package/dist/commands/view-cycle.js +9 -5
  23. package/dist/commands/view-run.js +3 -2
  24. package/dist/core/__tests__/cascade-close.test.js +3 -2
  25. package/dist/core/__tests__/context-intro.test.js +23 -27
  26. package/dist/core/__tests__/detach-focus.test.js +2 -2
  27. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  28. package/dist/core/__tests__/memory.test.js +4 -4
  29. package/dist/core/__tests__/subscription-delivery.test.js +3 -3
  30. package/dist/core/bootstrap.js +18 -14
  31. package/dist/core/help.d.ts +2 -2
  32. package/dist/core/help.js +1 -1
  33. package/dist/core/render.d.ts +4 -3
  34. package/dist/core/render.js +38 -41
  35. package/dist/core/resolver.js +1 -1
  36. package/dist/core/runtime/bearings.d.ts +2 -2
  37. package/dist/core/runtime/bearings.js +2 -2
  38. package/dist/core/substrate/render.d.ts +1 -2
  39. package/dist/core/substrate/render.js +9 -12
  40. package/dist/core/substrate/schema.d.ts +18 -13
  41. package/dist/core/substrate/schema.js +12 -11
  42. package/dist/pi-extensions/canvas-context-intro.js +5 -3
  43. package/package.json +1 -1
@@ -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`).
@@ -177,8 +177,8 @@ export const humanCancel = defineLeaf({
177
177
  return { canceled: true, job_id: jobId };
178
178
  },
179
179
  render: (r) => r['canceled'] === true
180
- ? `<canceled job_id="${r['job_id']}"/>`
181
- : `<cancel-noop job_id="${r['job_id']}">${r['reason'] ?? 'nothing to cancel'}</cancel-noop>`,
180
+ ? `Canceled human interaction ${r['job_id']} — its TUI pane is closed and subscribers were notified no answer is coming.`
181
+ : `Nothing to cancel for ${r['job_id']} — ${r['reason'] ?? 'nothing to cancel'}.`,
182
182
  });
183
183
  // ---------------------------------------------------------------------------
184
184
  // _run (hidden worker; not listed in branch help)
@@ -0,0 +1,29 @@
1
+ // Bug-regression: guards the hard cut that merged the substrate's `when`/`why`
2
+ // frontmatter pair into the single `when-and-why-to-read` read-routing field.
3
+ // The CTO ruling (taste/why-field-means-why-to-read) requires the migration be
4
+ // a hard cut: an old-shape doc must FAIL at `crtr memory lint`, never be read at
5
+ // runtime. `lintSubstrateSchema` is that enforcement seam — these lock it.
6
+ //
7
+ // Run: node --import tsx/esm --test src/commands/memory/__tests__/lint-schema.test.ts
8
+ import { test } from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import { lintSubstrateSchema } from '../lint.js';
11
+ const ROUTING = 'When you are X, this reference should be read because Y';
12
+ test('lint rejects a doc still carrying the retired `when` key', () => {
13
+ const err = lintSubstrateSchema({ kind: 'reference', when: 'when X' });
14
+ assert.ok(err !== null, 'old-shape `when` must produce a finding');
15
+ assert.match(err, /when-and-why-to-read/, 'the message points at the new field');
16
+ });
17
+ test('lint rejects a doc still carrying the retired `why` key', () => {
18
+ const err = lintSubstrateSchema({ kind: 'reference', why: 'because Y' });
19
+ assert.ok(err !== null, 'old-shape `why` must produce a finding');
20
+ assert.match(err, /when-and-why-to-read/, 'the message points at the new field');
21
+ });
22
+ test('lint rejects a substrate doc missing the merged routing field', () => {
23
+ const err = lintSubstrateSchema({ kind: 'reference' });
24
+ assert.ok(err !== null, 'a substrate doc must carry when-and-why-to-read');
25
+ assert.match(err, /when-and-why-to-read/);
26
+ });
27
+ test('lint accepts the merged new-shape frontmatter', () => {
28
+ assert.equal(lintSubstrateSchema({ kind: 'reference', 'when-and-why-to-read': ROUTING }), null);
29
+ });
@@ -12,8 +12,7 @@ function substrateUnit(d) {
12
12
  kind: d.kind,
13
13
  scope: d.scope,
14
14
  path: d.path,
15
- when: d.when,
16
- why: d.why,
15
+ routing: d.whenAndWhyToRead,
17
16
  shortForm: d.shortForm,
18
17
  loadBody: () => d.body,
19
18
  };
@@ -26,10 +25,9 @@ function skillUnit(s) {
26
25
  kind: 'skill',
27
26
  scope: s.scope,
28
27
  path: s.path,
29
- // A skill's description plays the when+why role; keywords ride along so the
30
- // ranker weighs them too. short_form surfaces the description as the hook.
31
- when: [desc, keywords].filter((x) => x).join(' '),
32
- why: '',
28
+ // A skill's description plays the read-routing role; keywords ride along so
29
+ // the ranker weighs them too. short_form surfaces the description as the hook.
30
+ routing: [desc, keywords].filter((x) => x).join(' '),
33
31
  shortForm: desc,
34
32
  loadBody: () => parseFrontmatter(readText(s.path)).body,
35
33
  };
@@ -80,12 +78,12 @@ function candidates(kindFilter) {
80
78
  export const findLeaf = defineLeaf({
81
79
  name: 'find',
82
80
  description: 'relevance search across memory documents',
83
- whenToUse: 'you do not yet know which document applies and need to discover what is stored — ranks documents by relevance, weighted over name, when, why, and short-form (add --body to also weigh body text). Searches the full scope set regardless of any visibility gate. Use --grep instead when you need an exact regex or literal-string match across document bodies rather than a ranked topic match.',
81
+ whenToUse: 'you do not yet know which document applies and need to discover what is stored — ranks documents by relevance, weighted over name, the read-routing line, and short-form (add --body to also weigh body text). Searches the full scope set regardless of any visibility gate. Use --grep instead when you need an exact regex or literal-string match across document bodies rather than a ranked topic match.',
84
82
  help: {
85
83
  name: 'memory find',
86
- summary: 'relevance search across memory documents, weighted over name/when/why/short-form (and body with --body)',
84
+ summary: 'relevance search across memory documents, weighted over name/routing-line/short-form (and body with --body)',
87
85
  params: [
88
- { kind: 'positional', name: 'query', required: true, constraint: 'With ranked search (default): whitespace-separated terms, matched case-insensitively and weighted over name, when, why, and short-form (plus body with --body); documents matching more/stronger fields rank higher. With --grep: an ECMAScript regex applied to each document body line.' },
86
+ { kind: 'positional', name: 'query', required: true, constraint: 'With ranked search (default): whitespace-separated terms, matched case-insensitively and weighted over name, the read-routing line, and short-form (plus body with --body); documents matching more/stronger fields rank higher. With --grep: an ECMAScript regex applied to each document body line.' },
89
87
  { kind: 'flag', name: 'kind', type: 'enum', choices: [...MEMORY_KINDS], required: false, constraint: 'Filter to a single kind. Default: all kinds.' },
90
88
  { kind: 'flag', name: 'grep', type: 'bool', required: false, constraint: 'Treat the query as an ECMAScript regex and match it against document bodies, instead of weighted relevance ranking. Mutually exclusive with --body.' },
91
89
  { kind: 'flag', name: 'body', type: 'bool', required: false, constraint: 'Also weigh document body text in the relevance ranking (in addition to name/when/why/short-form). Ignored under --grep, which always scans bodies.' },
@@ -134,7 +132,7 @@ export const findLeaf = defineLeaf({
134
132
  follow_up: 'Read a document in full with `crtr memory read <name>`. Drop --grep for a ranked topic search.',
135
133
  };
136
134
  }
137
- // --- ranked mode: weighted relevance over name/when/why/short-form(+body) ---
135
+ // --- ranked mode: weighted relevance over name/routing-line/short-form(+body) ---
138
136
  const terms = query
139
137
  .toLowerCase()
140
138
  .split(/\s+/)
@@ -144,18 +142,15 @@ export const findLeaf = defineLeaf({
144
142
  const hits = [];
145
143
  for (const u of units) {
146
144
  const nameLc = u.name.toLowerCase();
147
- const whenLc = u.when.toLowerCase();
148
- const whyLc = u.why.toLowerCase();
145
+ const routingLc = u.routing.toLowerCase();
149
146
  const shortLc = u.shortForm.toLowerCase();
150
147
  const bodyLc = weighBody ? u.loadBody().toLowerCase() : null;
151
148
  let score = 0;
152
149
  for (const term of terms) {
153
150
  if (nameLc.includes(term))
154
151
  score += 10;
155
- if (whenLc.includes(term))
152
+ if (routingLc.includes(term))
156
153
  score += 5;
157
- if (whyLc.includes(term))
158
- score += 4;
159
154
  if (shortLc.includes(term))
160
155
  score += 3;
161
156
  if (bodyLc !== null && bodyLc.includes(term))
@@ -1 +1,8 @@
1
+ /** Schema checks for a doc living in a substrate memory dir: a memory store
2
+ * holds ONLY substrate docs, so a missing/invalid `kind` is an authoring
3
+ * error here (elsewhere it just means "not a substrate doc"). Rung and gate
4
+ * values are checked RAW — the runtime parser silently falls back to kind
5
+ * defaults / inert gates, which is exactly the silent tolerance this lint
6
+ * exists to catch at authoring time. */
7
+ export declare function lintSubstrateSchema(fm: Record<string, unknown> | null): string | null;
1
8
  export declare const lintLeaf: import("../../core/command.js").LeafDef;
@@ -24,12 +24,21 @@ const RUNG_FIELDS = ['system-prompt-visibility', 'file-read-visibility'];
24
24
  * values are checked RAW — the runtime parser silently falls back to kind
25
25
  * defaults / inert gates, which is exactly the silent tolerance this lint
26
26
  * exists to catch at authoring time. */
27
- function lintSubstrateSchema(fm) {
27
+ export function lintSubstrateSchema(fm) {
28
28
  if (fm === null)
29
29
  return 'missing frontmatter: a memory store doc requires `kind: skill|reference|preference`';
30
30
  if (!isDocKind(fm.kind)) {
31
31
  return `invalid kind: ${JSON.stringify(fm.kind)} (expected skill|reference|preference)`;
32
32
  }
33
+ // The retired `when`/`why` pair was merged into one read-routing field. The
34
+ // hard cut is enforced HERE: an old-shape doc must fail, never be silently
35
+ // read at runtime.
36
+ if ('when' in fm || 'why' in fm) {
37
+ return 'retired `when`/`why` keys: merge them into one `when-and-why-to-read` line — "When <circumstance>, this <kind> should be read <because <payoff>>."';
38
+ }
39
+ if (typeof fm['when-and-why-to-read'] !== 'string' || fm['when-and-why-to-read'].trim() === '') {
40
+ return 'missing `when-and-why-to-read`: one read-routing line — "When <circumstance>, this <kind> should be read <because <payoff>>."';
41
+ }
33
42
  for (const field of RUNG_FIELDS) {
34
43
  const v = fm[field];
35
44
  if (v !== undefined && (typeof v !== 'string' || !VALID_RUNGS.includes(v))) {
@@ -91,8 +91,7 @@ export function coerceAppliesTo(raw) {
91
91
  // first in this order; any preserved-on-update extras append after.
92
92
  const FRONTMATTER_ORDER = [
93
93
  'kind',
94
- 'when',
95
- 'why',
94
+ 'when-and-why-to-read',
96
95
  'short-form',
97
96
  'system-prompt-visibility',
98
97
  'file-read-visibility',
@@ -13,8 +13,7 @@ export const writeLeaf = defineLeaf({
13
13
  params: [
14
14
  { kind: 'positional', name: 'name', required: true, constraint: 'Path-derived identity (e.g. `topic` or `area/topic`) → memory/<name>.md at the resolved scope. Updated in place if it already exists, otherwise created.' },
15
15
  { kind: 'flag', name: 'kind', type: 'enum', choices: [...MEMORY_KINDS], required: true, constraint: 'Document kind.' },
16
- { kind: 'flag', name: 'when', type: 'string', required: false, constraint: 'Frontmatter whenText a short string describing when this document should be read.' },
17
- { kind: 'flag', name: 'why', type: 'string', required: false, constraint: 'Frontmatter whyText — a short string describing why this document matters.' },
16
+ { kind: 'flag', name: 'when-and-why-to-read', type: 'string', required: false, constraint: 'The read-routing line, authored as ONE sentence: "When <circumstance>, this <kind> should be read <because <payoff>>." It states WHEN to read this doc and WHY the read is worth it — read-routing, NEVER a justification of why the content should be obeyed. Rendered verbatim as the preview. Required when creating.' },
18
17
  { kind: 'flag', name: 'short-form', type: 'string', required: false, constraint: 'Frontmatter short-form — a very abbreviated version of the content, the hook shown in `crtr memory list`.' },
19
18
  { kind: 'flag', name: 'system-prompt-visibility', type: 'enum', choices: [...VISIBILITY_RUNGS], required: false, constraint: 'Rung controlling how much of this document auto-loads into the system prompt / CLI help.' },
20
19
  { kind: 'flag', name: 'file-read-visibility', type: 'enum', choices: [...VISIBILITY_RUNGS], required: false, constraint: 'Rung controlling how much of this document surfaces when it is read off disk.' },
@@ -44,11 +43,10 @@ export const writeLeaf = defineLeaf({
44
43
  const { scope, memoryDir } = resolveWriteTarget(scopeArg);
45
44
  const path = memoryFilePath(memoryDir, name);
46
45
  const created = !pathExists(path);
47
- // CREATE requires the two prose fields that compose the generated preview
48
- // routing line without them every preview renders the degenerate
49
- // '", read this <kind>. ."'. UPDATE inherits them from the existing doc.
50
- if (created && (input['when'] === undefined || input['why'] === undefined)) {
51
- throw usage(`creating ${name} requires --when and --why (they compose the preview routing line "{when}, read this ${kind}. {why}.")`);
46
+ // CREATE requires the read-routing line that becomes the preview — without
47
+ // it every preview renders empty. UPDATE inherits it from the existing doc.
48
+ if (created && input['whenAndWhyToRead'] === undefined) {
49
+ throw usage(`creating ${name} requires --when-and-why-to-read: one read-routing sentence "When <circumstance>, this ${kind} should be read <because <payoff>>." (rendered verbatim as the preview).`);
52
50
  }
53
51
  // In-place update: start from the existing frontmatter (preserving fields
54
52
  // not passed this time), then overlay the provided ones. Create: start clean.
@@ -62,8 +60,7 @@ export const writeLeaf = defineLeaf({
62
60
  if (value !== undefined)
63
61
  frontmatter[key] = value;
64
62
  };
65
- setIf('when', input['when']);
66
- setIf('why', input['why']);
63
+ setIf('when-and-why-to-read', input['whenAndWhyToRead']);
67
64
  setIf('short-form', input['shortForm']);
68
65
  setIf('system-prompt-visibility', input['systemPromptVisibility']);
69
66
  setIf('file-read-visibility', input['fileReadVisibility']);
@@ -164,7 +164,14 @@ const nodeNew = defineLeaf({
164
164
  : childFollowUp(parent ?? process.env['CRTR_NODE_ID']),
165
165
  };
166
166
  },
167
- render: (r) => `<spawned name="${r['name']}" id="${r['node_id']}" status="${r['status']}">\n${r['follow_up']}\n</spawned>`,
167
+ render: (r) => {
168
+ const win = r['window'] ? ` in window ${r['window']}` : '';
169
+ const lines = [`Spawned "${r['name']}" (${r['node_id']}) — ${r['status']}${win}.`];
170
+ if (r['session'])
171
+ lines.push(`- session: ${r['session']}`);
172
+ lines.push('', String(r['follow_up']));
173
+ return lines.join('\n');
174
+ },
168
175
  });
169
176
  // ---------------------------------------------------------------------------
170
177
  // node list — the active canvas (or a status slice)
@@ -352,9 +359,16 @@ const nodeRecycle = defineLeaf({
352
359
  const res = await recycleNode(id, pane);
353
360
  return { recycled: res.recycled, node_id: id, finalized: res.finalized, delivered: res.delivered.length, new_root: res.newRoot ?? undefined };
354
361
  },
355
- render: (r) => r['recycled'] === true
356
- ? `<recycled id="${r['node_id']}" finalized="${r['finalized']}" delivered="${r['delivered']}" new_root="${r['new_root'] ?? ''}"/>`
357
- : `<recycle-failed id="${r['node_id'] ?? ''}">not in tmux, or no agent in this pane</recycle-failed>`,
362
+ render: (r) => {
363
+ if (r['recycled'] !== true)
364
+ return `Recycle failed for ${r['node_id'] ?? '?'}not in tmux, or no agent in this pane.`;
365
+ const lines = [`Recycled the pane — finished ${r['node_id']}.`, `- finalized: ${r['finalized']}`];
366
+ if (r['finalized'] === true)
367
+ lines.push(`- delivered: ${r['delivered']}`);
368
+ if (r['new_root'])
369
+ lines.push(`- new root: ${r['new_root']}`);
370
+ return lines.join('\n');
371
+ },
358
372
  });
359
373
  // ---------------------------------------------------------------------------
360
374
  // node close — close a node + cascade-cancel its exclusive subtree (Alt+C → x)
@@ -398,9 +412,12 @@ const nodeClose = defineLeaf({
398
412
  const res = closeNode(id);
399
413
  return { closed: true, node_id: res.root, count: res.closed.length, closed_ids: res.closed, spared: res.spared };
400
414
  },
401
- render: (r) => r['closed'] === true
402
- ? `<closed id="${r['node_id']}" count="${r['count']}" spared="${r['spared']?.length ?? 0}"/>`
403
- : `<close-failed/>`,
415
+ render: (r) => {
416
+ if (r['closed'] !== true)
417
+ return 'Close failed — no node found in this pane to close.';
418
+ const spared = r['spared']?.length ?? 0;
419
+ return `Closed ${r['node_id']} and its exclusive subtree — ${r['count']} node(s) closed${spared > 0 ? `, ${spared} spared (still managed from outside the subtree)` : ''}.`;
420
+ },
404
421
  });
405
422
  // ---------------------------------------------------------------------------
406
423
  // node cycle — DFS-walk the canvas one window at a time (Alt+] / Alt+[)
@@ -486,8 +503,8 @@ const nodeCycle = defineLeaf({
486
503
  return { focused: res.focused, node_id: targetId, name: target.name, from: fromId };
487
504
  },
488
505
  render: (r) => r['focused'] === true
489
- ? `<cycled to="${r['node_id']}" name="${r['name'] ?? ''}" from="${r['from'] ?? ''}"/>`
490
- : `<cycle-noop>no other live node to focus</cycle-noop>`,
506
+ ? `Cycled to "${r['name'] ?? ''}" (${r['node_id']}) from ${r['from'] ?? '?'}.`
507
+ : 'No other live node to focus.',
491
508
  });
492
509
  // ---------------------------------------------------------------------------
493
510
  // node msg — direct-address any node at a wake tier (wakes a dormant target)
@@ -582,7 +599,7 @@ const nodeSubscribe = defineLeaf({
582
599
  subscribe(subscriber, publisher, !passive);
583
600
  return { subscribed: true, subscriber, publisher, mode: passive ? 'passive' : 'active' };
584
601
  },
585
- render: (r) => `<subscribed subscriber="${r['subscriber']}" publisher="${r['publisher']}" mode="${r['mode']}"/>`,
602
+ render: (r) => `Subscribed ${r['subscriber']} ${r['publisher']} (${r['mode']}).`,
586
603
  });
587
604
  const nodeUnsubscribe = defineLeaf({
588
605
  name: 'unsubscribe',
@@ -609,7 +626,7 @@ const nodeUnsubscribe = defineLeaf({
609
626
  unsubscribe(subscriber, publisher);
610
627
  return { unsubscribed: true, subscriber, publisher };
611
628
  },
612
- render: (r) => `<unsubscribed subscriber="${r['subscriber']}" publisher="${r['publisher']}"/>`,
629
+ render: (r) => `Unsubscribed ${r['subscriber']} from ${r['publisher']}.`,
613
630
  });
614
631
  // ---------------------------------------------------------------------------
615
632
  // node promote — become an orchestrator (the worker→orchestrator polymorph)
@@ -728,7 +745,7 @@ const nodeDemote = defineLeaf({
728
745
  const res = setLifecycle(id, 'terminal', { pane, detach: input['detach'] === true });
729
746
  return { node_id: res.node_id, lifecycle: res.lifecycle, detached: res.detached };
730
747
  },
731
- render: (r) => `<demoted node="${r['node_id']}" lifecycle="${r['lifecycle']}"${r['detached'] === true ? ' detached="true"' : ''}/>`,
748
+ render: (r) => `Demoted ${r['node_id']} lifecycle now ${r['lifecycle']} (in place)${r['detached'] === true ? ', relocated to the background crtr session' : ''}.`,
732
749
  });
733
750
  // ---------------------------------------------------------------------------
734
751
  // node lifecycle — flip the lifecycle axis (terminal ↔ resident), independent
@@ -765,7 +782,7 @@ const nodeLifecycle = defineLeaf({
765
782
  const res = setLifecycle(id, value, { pane, detach: input['detach'] === true });
766
783
  return { node_id: res.node_id, lifecycle: res.lifecycle, detached: res.detached };
767
784
  },
768
- render: (r) => `<lifecycle node="${r['node_id']}" set="${r['lifecycle']}"${r['detached'] === true ? ' detached="true"' : ''}/>`,
785
+ render: (r) => `Set ${r['node_id']} lifecycle → ${r['lifecycle']}${r['detached'] === true ? ', relocated to the background crtr session' : ''}.`,
769
786
  });
770
787
  // ---------------------------------------------------------------------------
771
788
  // node yield — refresh: discard context, revive fresh against the roadmap
@@ -908,10 +925,6 @@ function etaHint(fireAt, now) {
908
925
  return ` (~${hrs}h)`;
909
926
  return ` (~${Math.round(ms / 86_400_000)}d)`;
910
927
  }
911
- /** Escape a value for a rendered XML attribute (list rows carry free-text notes). */
912
- function xmlAttr(s) {
913
- return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
914
- }
915
928
  // node wake at ---------------------------------------------------------------
916
929
  const nodeWakeAt = defineLeaf({
917
930
  name: 'at',
@@ -1011,7 +1024,10 @@ const nodeWakeAt = defineLeaf({
1011
1024
  : `Bare self-alarm armed. You wake in a fresh window at ${fireAt}${eta}. End your turn now to go dormant; do not push final. The wake re-reads your roadmap.`;
1012
1025
  return { id, kind, fires_at: fireAt, recur: cadenceDisplay(recur), target, guidance };
1013
1026
  },
1014
- render: (r) => `<wake-armed id="${r['id']}" kind="${r['kind']}" fires-at="${r['fires_at']}" recur="${r['recur']}" target="${r['target']}">\n${r['guidance']}\n</wake-armed>`,
1027
+ render: (r) => {
1028
+ const recur = r['recur'] !== 'none' ? `, recurs ${r['recur']}` : '';
1029
+ return `Armed ${r['kind']} wake ${r['id']} — fires ${r['fires_at']}${recur}, target ${r['target']}.\n\n${r['guidance']}`;
1030
+ },
1015
1031
  });
1016
1032
  // node wake until ------------------------------------------------------------
1017
1033
  const nodeWakeUntil = defineLeaf({
@@ -1063,7 +1079,7 @@ const nodeWakeUntil = defineLeaf({
1063
1079
  const guidance = `Deadline armed. You wake on the first inbox message, or at ${w.fireAt}${eta} if none arrives. Now delegate / end your turn to go dormant — whichever fires first cancels the other.`;
1064
1080
  return { id, fires_at: w.fireAt, target: 'self', guidance };
1065
1081
  },
1066
- render: (r) => `<deadline-armed id="${r['id']}" fires-at="${r['fires_at']}" target="${r['target']}">\n${r['guidance']}\n</deadline-armed>`,
1082
+ render: (r) => `Armed deadline ${r['id']} fires ${r['fires_at']}, target ${r['target']}.\n\n${r['guidance']}`,
1067
1083
  });
1068
1084
  // node wake spawn -----------------------------------------------------------
1069
1085
  const nodeWakeSpawn = defineLeaf({
@@ -1164,7 +1180,10 @@ const nodeWakeSpawn = defineLeaf({
1164
1180
  : `Deferred spawn armed: a fresh ${kind} node is born at ${fireAt}${eta}. No node exists yet — inspect/cancel via \`crtr node wake list\` / \`crtr node wake cancel ${id}\`. Pick up other work or end your turn.`;
1165
1181
  return { id, kind, fires_at: fireAt, recur: cadenceDisplay(recur), guidance };
1166
1182
  },
1167
- render: (r) => `<spawn-deferred id="${r['id']}" kind="${r['kind']}" fires-at="${r['fires_at']}" recur="${r['recur']}">\n${r['guidance']}\n</spawn-deferred>`,
1183
+ render: (r) => {
1184
+ const recur = r['recur'] !== 'none' ? `, recurs ${r['recur']}` : '';
1185
+ return `Armed deferred spawn ${r['id']} — a ${r['kind']} node, fires ${r['fires_at']}${recur}.\n\n${r['guidance']}`;
1186
+ },
1168
1187
  });
1169
1188
  // node wake list -------------------------------------------------------------
1170
1189
  const nodeWakeList = defineLeaf({
@@ -1246,10 +1265,13 @@ const nodeWakeList = defineLeaf({
1246
1265
  },
1247
1266
  render: (r) => {
1248
1267
  const wakes = r['wakes'];
1249
- const rows = wakes.map((w) => ` <wake id="${w['id']}" kind="${w['kind']}" next="${w['next']}" recur="${xmlAttr(String(w['recur']))}" target="${xmlAttr(String(w['target']))}" owner="${xmlAttr(String(w['owner']))}" note="${xmlAttr(String(w['note']))}"/>`);
1250
- const body = rows.length > 0 ? '\n' + rows.join('\n') + '\n' : '\n';
1251
- return (`<wakes scope="${xmlAttr(String(r['scope']))}" count="${wakes.length}">${body}</wakes>\n` +
1252
- '<follow_up>Cancel one with `crtr node wake cancel <id>`. Fired one-shots are gone; a recurrence shows its NEXT fire, not past slots.</follow_up>');
1268
+ const follow = 'Cancel one with `crtr node wake cancel <id>`. Fired one-shots are gone; a recurrence shows its NEXT fire, not past slots.';
1269
+ if (wakes.length === 0)
1270
+ return `No pending wakes in scope ${r['scope']}.\n\n${follow}`;
1271
+ const cell = (v) => String(v).replace(/\|/g, '\\|').replace(/\n/g, ' ');
1272
+ const head = '| id | kind | next | recur | target | owner | note |\n| --- | --- | --- | --- | --- | --- | --- |';
1273
+ const rows = wakes.map((w) => `| ${cell(w['id'])} | ${cell(w['kind'])} | ${cell(w['next'])} | ${cell(w['recur'])} | ${cell(w['target'])} | ${cell(w['owner'])} | ${cell(w['note'])} |`);
1274
+ return `${wakes.length} pending wake(s) in scope ${r['scope']}.\n\n${head}\n${rows.join('\n')}\n\n${follow}`;
1253
1275
  },
1254
1276
  });
1255
1277
  // node wake cancel -----------------------------------------------------------
@@ -1276,7 +1298,7 @@ const nodeWakeCancel = defineLeaf({
1276
1298
  cancelWake(id);
1277
1299
  return { id, was_pending: wasPending };
1278
1300
  },
1279
- render: (r) => `<wake-canceled id="${r['id']}" was-pending="${r['was_pending']}"/>`,
1301
+ render: (r) => `Canceled wake ${r['id']}${r['was_pending'] === true ? '' : ' (already gone — no-op)'}.`,
1280
1302
  });
1281
1303
  // node wake (branch) ---------------------------------------------------------
1282
1304
  const nodeWake = defineBranch({
@@ -112,7 +112,7 @@ function makeTierLeaf(tier) {
112
112
  : tier === 'urgent'
113
113
  ? `Urgent report fanned to ${n} subscriber(s) — they are force-woken.`
114
114
  : `Progress report fanned to ${n} subscriber(s).`;
115
- return `<pushed tier="${tier}" status="${r['status']}" delivered="${n}">\n${line}\nreport: ${r['report_path']}\n</pushed>`;
115
+ return `${line}\n\n- report: ${r['report_path']}`;
116
116
  },
117
117
  });
118
118
  }
@@ -161,16 +161,16 @@ const feedReadLeaf = defineLeaf({
161
161
  const n = Array.isArray(r['entries']) ? r['entries'].length : 0;
162
162
  const rawDigest = typeof r['digest'] === 'string' ? r['digest'] : '';
163
163
  if (n > 0 && rawDigest.trim() !== '') {
164
- return `<feed node="${r['node_id']}" unread="${n}">\n${rawDigest}\n</feed>`;
164
+ return `${n} unread pointer${n === 1 ? '' : 's'} drained from ${r['node_id']}'s inbox.\n\n${rawDigest.trim()}`;
165
165
  }
166
166
  // Empty drain has two distinct causes — say which, honestly. An inbox that
167
167
  // already holds entries was drained when the watcher woke you (the wake
168
168
  // message WAS that digest); a never-used inbox simply has nothing yet.
169
169
  const total = typeof r['inbox_total'] === 'number' ? r['inbox_total'] : 0;
170
- const digest = total > 0
171
- ? 'Nothing unread — but your inbox is not empty. The watcher drains your inbox to wake you, so the entries you already saw in your wake message are the same ones you would read here; that is why this is empty. Re-read the whole history (full message bodies included) with `crtr feed read --all`, or dereference the refs from the wake digest you already have.'
172
- : 'Inbox empty — nothing has arrived from your subscriptions yet. Expected while workers run: a worker that has not pushed yet leaves no pointer, and it will wake you the moment it does. The wake is automatic — just continue your own work or end your turn. (Reach for `crtr feed peek` only if you suspect a worker died, not to confirm a live one.)';
173
- return `<feed node="${r['node_id']}" unread="${n}">\n${digest}\n</feed>`;
170
+ const explanation = total > 0
171
+ ? 'Your inbox is not empty, though. The watcher drains your inbox to wake you, so the entries you already saw in your wake message are the same ones you would read here; that is why this is empty. Re-read the whole history (full message bodies included) with `crtr feed read --all`, or dereference the refs from the wake digest you already have.'
172
+ : 'Your inbox is empty — nothing has arrived from your subscriptions yet. Expected while workers run: a worker that has not pushed yet leaves no pointer, and it will wake you the moment it does. The wake is automatic — just continue your own work or end your turn. (Reach for `crtr feed peek` only if you suspect a worker died, not to confirm a live one.)';
173
+ return `No unread pointers for ${r['node_id']} (unread=0).\n\n${explanation}`;
174
174
  },
175
175
  });
176
176
  // ---------------------------------------------------------------------------
@@ -249,28 +249,23 @@ const feedPeekLeaf = defineLeaf({
249
249
  const tail = unread > 0
250
250
  ? `${unread} unread report${unread === 1 ? '' : 's'} sit in your inbox — run \`crtr feed read\` to absorb ${unread === 1 ? 'it' : 'them'}.`
251
251
  : 'If you spawned workers, they have finished and detached, or you never subscribed. Nothing will wake you.';
252
- return `<peek node="${id}" subscriptions="0" unread="${unread}" verdict="empty">\nNo nodes below you. ${tail}\n</peek>`;
252
+ return `No nodes below you (0 subscriptions, ${unread} unread). ${tail}`;
253
253
  }
254
254
  const working = kids.filter((k) => k.status === 'active' || k.status === 'idle');
255
255
  const liveWaking = working.filter((k) => k.active); // active sub to a live node = it will wake me
256
256
  const done = kids.filter((k) => k.status === 'done' || k.status === 'canceled');
257
257
  const dead = kids.filter((k) => k.status === 'dead');
258
- let verdict;
259
258
  let line;
260
259
  if (dead.length > 0) {
261
- verdict = 'attention';
262
260
  line = `\u26a0 ${dead.length} below you ${dead.length === 1 ? 'is' : 'are'} dead and will NOT wake you. Inspect with \`crtr node inspect show <id>\`, then re-delegate or proceed without ${dead.length === 1 ? 'it' : 'them'}.`;
263
261
  }
264
262
  else if (liveWaking.length > 0) {
265
- verdict = 'working';
266
263
  line = `Safe to yield \u2014 ${liveWaking.length} worker${liveWaking.length === 1 ? '' : 's'} running async will wake you on the next push. Nothing to do now; end your turn and chill.`;
267
264
  }
268
265
  else if (unread > 0) {
269
- verdict = 'ready';
270
266
  line = `Nothing still running, but ${unread} unread report${unread === 1 ? '' : 's'} \u2014 run \`crtr feed read\` to absorb ${unread === 1 ? 'it' : 'them'}, then continue or finish.`;
271
267
  }
272
268
  else {
273
- verdict = 'idle';
274
269
  line = 'Everything below you has finished and been drained \u2014 nothing is running. Continue your own work, or `crtr push final` to finish.';
275
270
  }
276
271
  const rows = kids.map((k) => {
@@ -279,10 +274,10 @@ const feedPeekLeaf = defineLeaf({
279
274
  ? `pushed ${fmtAge(k.last_push.ts)} [${k.last_push.kind}]${k.last_push.ref !== null ? ` ref:${k.last_push.ref}` : ''}`
280
275
  : 'no push yet';
281
276
  const glyph = STATUS_GLYPH[k.status] ?? '?';
282
- return ` ${glyph} ${k.node_id} ${k.kind} ${k.name}${sub} \u00b7 ${k.status} \u00b7 spawned ${fmtAge(k.spawned)} \u00b7 cyc ${k.cycles} \u00b7 ${push}`;
277
+ return `- ${glyph} ${k.name}${sub} (${k.node_id}) \u00b7 ${k.kind} \u00b7 ${k.status} \u00b7 spawned ${fmtAge(k.spawned)} \u00b7 cyc ${k.cycles} \u00b7 ${push}`;
283
278
  }).join('\n');
284
- const attrs = `node="${id}" subscriptions="${kids.length}" working="${working.length}" done="${done.length}" dead="${dead.length}" unread="${unread}" verdict="${verdict}"`;
285
- return `<peek ${attrs}>\n${line}\n\n${rows}\n</peek>`;
279
+ const summary = `${kids.length} node${kids.length === 1 ? '' : 's'} below you — ${working.length} working, ${done.length} done, ${dead.length} dead, ${unread} unread:`;
280
+ return `${line}\n\n${summary}\n${rows}`;
286
281
  },
287
282
  });
288
283
  // ---------------------------------------------------------------------------