@crouton-kit/crouter 0.3.26 → 0.3.27

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.
@@ -16,6 +16,6 @@ Open this dir whenever a task turns on understanding the runtime itself. Content
16
16
  - **storage-tiers** — where every kind of state lives: the three tiers (scope root, per-cwd crouter root, canvas home) and their durability/ownership contracts.
17
17
  - **examples/** — worked compositions of the primitives into complete systems (the analogue of pi's `examples/` dir), e.g. the iMessage assistant node.
18
18
 
19
- Adjacent, outside this dir: authoring memory documents (frontmatter, rungs, gates, the asked-to-remember workflow) is owned by the builtin **memory-authoring** skill; making a persona (a custom `--kind`) is owned by the builtin **crouter-development/personas** skills.
19
+ Adjacent, outside this dir: authoring memory documents (kind, rungs, gates, routing line, the asked-to-remember workflow) is owned by `crtr memory write -h` — the authoring guide lives on the help-gate so it surfaces exactly when you write; making a persona (a custom `--kind`) is owned by the builtin **crouter-development/personas** skills.
20
20
 
21
21
  The individual files surface at `name` (their titles route; open the one the situation calls for); this index surfaces at `preview` so the dir announces when to come looking.
@@ -10,6 +10,14 @@ export const writeLeaf = defineLeaf({
10
10
  help: {
11
11
  name: 'memory write',
12
12
  summary: 'create or update memory/<name>.md at the resolved scope from frontmatter flags + a stdin body',
13
+ guide: 'The body is the easy part; the craft is ROUTING — every frontmatter flag decides who sees this doc, when, and at what context cost. Each rung up is paid by every future agent at every boot or read, forever, so default each rung DOWN.\n\n' +
14
+ 'Pick the kind. skill = how to DO something (a repeatable procedure/playbook); reference = what is TRUE or how something WORKS (a fact about the user, a system\u2019s behavior, code docs); preference = how to BEHAVE (a directive, a standing correction). The test that splits the close pair: does it DIRECT behavior ("always lint after authoring" \u2192 preference) or INFORM the world-model ("Silas likes chicken", "the daemon never reloads dist/" \u2192 reference)? A correction yields a preference, a learned fact a reference, a repeatable procedure a skill.\n\n' +
15
+ 'Set the rungs (none < name < preview < content). Kind sets sensible defaults, so the common case needs no visibility flags at all: skill \u2192 name at boot; preference \u2192 preview at boot; reference \u2192 preview on-read, nothing at boot. Reserve `content` (full body injected) for guidance that is BOTH always-relevant AND ~one bullet long \u2014 fail either test and it is `preview`. Situational guidance is `preview` no matter how short; long guidance is `preview` no matter how universal it feels.\n\n' +
16
+ 'Choose the hook \u2014 boot vs file-read. There are exactly two moments a doc can surface. Behavior and procedure (preferences, skills) are relevant whatever file is open \u2192 surface at boot. Knowledge about code (references) belongs NEXT TO the code: put the file in that directory\u2019s .crouter/memory/ and it fires positionally when files there are read, costing nothing at boot. The exception that matters: a reference about a PERSON or PROCESS has no code directory to anchor to, so on-read triggering is meaningless \u2014 set --system-prompt-visibility preview so its routing line surfaces at boot instead.\n\n' +
17
+ 'Write the routing line (--when-and-why-to-read) FIRST, before storing anything: "When <circumstance the agent is in>, this <kind> should be read because <what the read buys>." The test: can a stranger mid-task decide from that one line alone whether to spend the read? If you cannot name the concrete situation that triggers it, you do not yet understand the memory \u2014 ask the user ONE sharp question instead of improvising. ("Remember I like chicken" routes cleanly \u2192 food/meal decisions; "be careful with the API" does not \u2192 which API, careful how, against what failure?) And NEVER paraphrase the advice in the routing line \u2014 a preview that gives away the gist makes every future agent skip the real read. BAD: "\u2026because it carries the placement policy \u2014 -h first, memory only for dev-mode material." GOOD: "\u2026because it carries the standing placement policy."\n\n' +
18
+ 'Find before write. `crtr memory find <topic>` first; grow ONE doc per recurring circumstance rather than minting near-duplicates \u2014 extend `food-preferences`, do not create `likes-chicken`. Group related docs with path names (area/topic). Do not store what is already recorded (code structure, git history, CLAUDE.md) or what only matters to this conversation.\n\n' +
19
+ 'Body: write for a STRANGER \u2014 a future session that shares none of this conversation. State current truth, not the history of getting there (no "as discussed"). Keep the reasoning behind a rule and cut everything else; dense beats complete, since every line costs a mid-task reader.\n\n' +
20
+ 'Gate (--gate, optional): a predicate making the doc eligible only for nodes whose own config matches (e.g. `{ mode: orchestrator }` or `{ orchestration.depth: { gte: 2 } }`); a failing gate hides it from both hooks but it stays findable by search. Default is no gate \u2014 most docs want exactly that. After authoring, validate with `crtr memory lint`.',
13
21
  params: [
14
22
  { 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
23
  { kind: 'flag', name: 'kind', type: 'enum', choices: [...MEMORY_KINDS], required: true, constraint: 'Document kind.' },
@@ -20,7 +20,7 @@ export function registerMemory() {
20
20
  help: {
21
21
  name: 'memory',
22
22
  summary: 'list, read, search, and write memory documents — skills, references, preferences',
23
- model: '`list` for a human inventory of what is stored — one line per document, the only surface that shows short-form. `read` (leaf) loads one document body by name, resolved project > user > builtin with leaf-name fallback; --frontmatter keeps the YAML header. `find` when you do not yet know which document applies — it ranks by relevance over name/when/why/short-form, --body to also weigh bodies, --grep for an exact regex over bodies. `write` creates or updates memory/<name>.md at a scope from frontmatter flags + a body piped on stdin. `lint` strict-parses the whole bounded corpus and fails on any invalid frontmatter — run it after authoring. Append `-h` at any leaf for its full schema.',
23
+ model: '`list` for a human inventory of what is stored — one line per document, the only surface that shows short-form. `read` (leaf) loads one document body by name, resolved project > user > builtin with leaf-name fallback; --frontmatter keeps the YAML header. `find` when you do not yet know which document applies — it ranks by relevance over name/when/why/short-form, --body to also weigh bodies, --grep for an exact regex over bodies. `write` creates or updates memory/<name>.md at a scope from frontmatter flags + a body piped on stdin. `lint` strict-parses the whole bounded corpus and fails on any invalid frontmatter — run it after authoring. A directory may carry an `INDEX.md` with the same frontmatter schema as any doc; the dir then renders as one entry at the INDEX\'s rung, and that rung is a ceiling for its whole subtree (`none` hides the dir) — when a doc mysteriously is not surfacing, check its ancestors\' INDEX rungs and its gate. Append `-h` at any leaf for its full schema, and `crtr memory write -h` for the authoring guide.',
24
24
  },
25
25
  children: [listLeaf, readLeaf, findLeaf, writeLeaf, lintLeaf],
26
26
  });
@@ -6,17 +6,13 @@ export declare function sanitizeSessionName(raw: string): string;
6
6
  /** Local fallback: derive a name straight from the prompt (no pi call). Drops
7
7
  * stop-words, takes the first few content words. */
8
8
  export declare function slugFromPrompt(prompt: string): string;
9
- /** Synchronously ask pi for a 3-8 word kebab name for `prompt`. Blocks up to
10
- * NAME_TIMEOUT_MS; on any failure (non-zero exit, timeout, empty/garbled
11
- * output) falls back to a local slug. Returns '' only for an empty prompt. */
12
- export declare function generateSessionName(prompt: string): string;
13
9
  /** Asynchronously generate a name for `prompt` and persist it to the node's
14
10
  * meta as `description` — only if the node has none yet (so a later message
15
11
  * never clobbers it). Non-blocking: safe to call from inside a live pi event
16
12
  * loop. Best-effort; swallows all errors.
17
13
  *
18
14
  * `onNamed` (optional) fires with the freshly-persisted meta the moment the
19
- * name lands — the bare-root path passes a callback that calls
15
+ * name lands — the canvas-goal-capture naming hook passes a callback that calls
20
16
  * pi.setSessionName(editorLabel(meta)) so the LIVE editor label updates in the
21
17
  * same session, instead of waiting for the next revive/cycle. */
22
18
  export declare function generateAndPersistName(nodeId: string, prompt: string, onNamed?: (meta: NodeMeta) => void): void;
@@ -6,19 +6,16 @@
6
6
  // the first prompt by asking pi headlessly (`pi -p`), persisted on the node's
7
7
  // meta so it survives revives and shows in every cycle.
8
8
  //
9
- // Two entry points:
10
- // generateSessionName — synchronous (spawnSync). For the CLI spawn paths
11
- // (spawnChild / bootRoot) that run outside any pi event loop, where a brief
12
- // block before launching the worker is fine and lets the FIRST pi session
13
- // already carry the name.
14
- // generateAndPersistName async (execFile, non-blocking). For the bare-root
15
- // case where the prompt only arrives as the first interactive message inside
16
- // a live pi process; it must never block the event loop. Persists the name
17
- // to meta so the label picks it up on the next cycle.
9
+ // One entry point: generateAndPersistName — async (execFile, non-blocking).
10
+ // Naming happens INSIDE the named node's own pi process, off the first real
11
+ // message (the kickoff task or a human's first line), never on the spawn path:
12
+ // blocking spawn on an LLM round-trip used to freeze the caller's terminal for
13
+ // 2-3s on every `crtr node new`. The headless namer runs with --no-extensions,
14
+ // so it loads no canvas hooks and can never recurse into another spawn/name.
18
15
  //
19
- // Both are best-effort: a failed/slow/garbled pi call falls back to a local slug
20
- // of the prompt, so a node always gets a sane name.
21
- import { spawnSync, execFile } from 'node:child_process';
16
+ // Best-effort: a failed/slow/garbled pi call falls back to a local slug of the
17
+ // prompt, so a node always gets a sane name.
18
+ import { execFile } from 'node:child_process';
22
19
  import { getNode, updateNode } from '../canvas/index.js';
23
20
  /** Cap on prompt text fed to the namer — a name needs only the gist. */
24
21
  const PROMPT_CAP = 2000;
@@ -97,30 +94,27 @@ function nameArgs(prompt) {
97
94
  argv.push(nameUserPrompt(prompt));
98
95
  return argv;
99
96
  }
100
- /** Synchronously ask pi for a 3-8 word kebab name for `prompt`. Blocks up to
101
- * NAME_TIMEOUT_MS; on any failure (non-zero exit, timeout, empty/garbled
102
- * output) falls back to a local slug. Returns '' only for an empty prompt. */
103
- export function generateSessionName(prompt) {
104
- const body = (prompt ?? '').trim();
105
- if (body === '')
106
- return '';
107
- try {
108
- const r = spawnSync('pi', nameArgs(body), {
109
- encoding: 'utf8',
110
- timeout: NAME_TIMEOUT_MS,
111
- // Don't inherit a TUI; capture stdout only.
112
- stdio: ['ignore', 'pipe', 'ignore'],
113
- });
114
- if (r.status === 0 && typeof r.stdout === 'string') {
115
- const name = sanitizeSessionName(r.stdout);
116
- if (name !== '')
117
- return name;
97
+ /** Ask pi headlessly for a kebab-case name for `body`, async. Resolves to the
98
+ * sanitized name, or '' on any failure (non-zero exit, timeout, empty/garbled
99
+ * output) so the caller can fall back to a local slug. Owns the subprocess
100
+ * mechanics crucially it hands pi an immediate stdin EOF: `pi -p` reads
101
+ * stdin, and execFile's default stdin is an OPEN pipe that never closes, so
102
+ * without this pi blocks waiting for EOF and the call exits non-zero (the
103
+ * regression that silently lost every LLM name to the slug fallback). */
104
+ function headlessName(body) {
105
+ return new Promise((resolve) => {
106
+ try {
107
+ const child = execFile('pi', nameArgs(body), { encoding: 'utf8', timeout: NAME_TIMEOUT_MS }, (err, stdout) => {
108
+ if (err || typeof stdout !== 'string')
109
+ return resolve('');
110
+ resolve(sanitizeSessionName(stdout));
111
+ });
112
+ child.stdin?.end(); // immediate EOF — see the doc above
118
113
  }
119
- }
120
- catch {
121
- // fall through to slug
122
- }
123
- return slugFromPrompt(body);
114
+ catch {
115
+ resolve('');
116
+ }
117
+ });
124
118
  }
125
119
  /** Asynchronously generate a name for `prompt` and persist it to the node's
126
120
  * meta as `description` — only if the node has none yet (so a later message
@@ -128,7 +122,7 @@ export function generateSessionName(prompt) {
128
122
  * loop. Best-effort; swallows all errors.
129
123
  *
130
124
  * `onNamed` (optional) fires with the freshly-persisted meta the moment the
131
- * name lands — the bare-root path passes a callback that calls
125
+ * name lands — the canvas-goal-capture naming hook passes a callback that calls
132
126
  * pi.setSessionName(editorLabel(meta)) so the LIVE editor label updates in the
133
127
  * same session, instead of waiting for the next revive/cycle. */
134
128
  export function generateAndPersistName(nodeId, prompt, onNamed) {
@@ -150,17 +144,7 @@ export function generateAndPersistName(nodeId, prompt, onNamed) {
150
144
  // best-effort
151
145
  }
152
146
  };
153
- try {
154
- execFile('pi', nameArgs(body), { encoding: 'utf8', timeout: NAME_TIMEOUT_MS }, (err, stdout) => {
155
- if (err || typeof stdout !== 'string') {
156
- persist(slugFromPrompt(body));
157
- return;
158
- }
159
- const name = sanitizeSessionName(stdout);
160
- persist(name !== '' ? name : slugFromPrompt(body));
161
- });
162
- }
163
- catch {
164
- persist(slugFromPrompt(body));
165
- }
147
+ void headlessName(body).then((name) => {
148
+ persist(name !== '' ? name : slugFromPrompt(body));
149
+ });
166
150
  }
@@ -14,7 +14,6 @@ import { spawnNode, currentNodeContext, resolveBirthSession, nodeSession, rootOf
14
14
  import { buildLaunchSpec, buildPiArgv } from './launch.js';
15
15
  import { writeGoal } from './kickoff.js';
16
16
  import { hasRoadmap, seedRoadmap } from './roadmap.js';
17
- import { generateSessionName } from './naming.js';
18
17
  import { buildIdentityAssertion, buildWakeBearings } from './bearings.js';
19
18
  import { installMenuBinding, installNavBindings, installViewNavBindings } from './tmux-chrome.js';
20
19
  import { setPresence, updateNode, getNode, fullName } from '../canvas/index.js';
@@ -35,19 +34,16 @@ export function bootRoot(opts) {
35
34
  // A born-resident root starts in base mode; it earns the orchestrator persona
36
35
  // the first time it delegates (or on promotion). Resident lifecycle either way.
37
36
  const { launch } = buildLaunchSpec(kind, 'base', { lifecycle: 'resident', hasManager: false });
38
- // A root opened WITH a prompt gets its editor name now (so the first pi
39
- // session already carries it). A bare root has no prompt yet the
40
- // goal-capture extension names it from the first message (async, next cycle).
41
- const description = opts.prompt !== undefined && opts.prompt.trim() !== ''
42
- ? generateSessionName(opts.prompt)
43
- : undefined;
37
+ // Born WITHOUT a name. Naming is async + event-driven: the canvas-goal-capture
38
+ // extension names the node from its FIRST real message (the kickoff prompt or
39
+ // a human's first line) inside its own pi process, via a headless `pi -p`.
40
+ // Never block the front door on an LLM round-trip.
44
41
  const meta = spawnNode({
45
42
  kind,
46
43
  mode: 'base',
47
44
  lifecycle: 'resident',
48
45
  cwd: opts.cwd,
49
46
  name: opts.name ?? kind,
50
- description,
51
47
  parent: null,
52
48
  launch,
53
49
  });
@@ -162,14 +158,15 @@ export function spawnChild(opts) {
162
158
  // independent root sits top-of-spine with nobody to push to. Mirrors the
163
159
  // `parent` set below (root ? null : spawner), so hasManager === parent!==null.
164
160
  const { launch } = buildLaunchSpec(opts.kind, mode, { lifecycle, hasManager: !root, model: opts.model });
165
- // Name the worker from its task now, so its first editor label carries it.
161
+ // Born WITHOUT a name the canvas-goal-capture extension names it async from
162
+ // its first message (the kickoff task) inside its own pi process, so spawn
163
+ // never blocks on the LLM naming round-trip (the 2-3s freeze it used to cost).
166
164
  const meta = spawnNode({
167
165
  kind: opts.kind,
168
166
  mode,
169
167
  lifecycle,
170
168
  cwd: opts.cwd,
171
169
  name: opts.name ?? opts.kind,
172
- description: generateSessionName(opts.prompt),
173
170
  // A root has no spine parent (top-level, nobody subscribes); it still
174
171
  // records spawned_by=spawner when a node (not a human shell) spawned it.
175
172
  // A child's parent IS its manager.
@@ -3,13 +3,23 @@
3
3
  // Loaded into every canvas node's pi process via the node's launch.extensions
4
4
  // list. INERT when CRTR_NODE_ID is absent (plain pi session or legacy job agent).
5
5
  //
6
- // A node spawned with a prompt has its goal persisted at birth (writeGoal in
7
- // spawn.ts). A bare root (`crtr` with no prompt) starts goal-less — its mandate
8
- // only arrives when the human types their first message. This extension closes
9
- // that gap: on the FIRST interactive user message, if the node has no goal yet,
10
- // it persists that message as context/initial-prompt.md. Subsequent messages
11
- // never clobber it (captureGoalIfAbsent is guarded), and a fresh-revive kickoff
12
- // prompt is skipped via its sentinel so it can never be mistaken for a mandate.
6
+ // Two first-message jobs, both keyed off the node's first real `input` event:
7
+ //
8
+ // 1. Goal capture (bare roots). A node spawned with a prompt has its goal
9
+ // persisted at birth (writeGoal in spawn.ts). A bare root (`crtr` with no
10
+ // prompt) starts goal-less its mandate only arrives when the human types
11
+ // their first message; this persists that as context/initial-prompt.md.
12
+ // Guarded so later messages never clobber it.
13
+ //
14
+ // 2. Naming (every node). Naming is async + event-driven — it does NOT run on
15
+ // the spawn path (that blocking LLM call froze the caller's terminal for
16
+ // 2-3s on every spawn). On the first real message, if the node has no name
17
+ // yet, ask pi headlessly (async, non-blocking) for a kebab-case name and
18
+ // live-update the editor label. The first message may be a human's line OR
19
+ // a delegated child's kickoff task — naming off the agent prompt is fine.
20
+ //
21
+ // Both skip extension-injected messages (inbox wakes, steering) and the
22
+ // fresh-revive kickoff (its sentinel), so neither is mistaken for a first input.
13
23
  //
14
24
  // Pure observation — it writes the goal file as a side effect and always lets
15
25
  // the message through unchanged (returns nothing ⇒ continue). Registered before
@@ -20,7 +30,7 @@
20
30
  // crouter's own tsc build without a dep on the pi packages.
21
31
  import { captureGoalIfAbsent, REVIVE_KICKOFF_SENTINEL } from '../core/runtime/kickoff.js';
22
32
  import { generateAndPersistName } from '../core/runtime/naming.js';
23
- import { editorLabel } from '../core/canvas/index.js';
33
+ import { editorLabel, getNode } from '../core/canvas/index.js';
24
34
  /**
25
35
  * Register the goal-capture handler on `pi`.
26
36
  *
@@ -34,26 +44,32 @@ export function registerCanvasGoalCapture(pi) {
34
44
  const nodeId = process.env['CRTR_NODE_ID'];
35
45
  if (nodeId === undefined || nodeId.trim() === '')
36
46
  return; // not a canvas node
37
- // Only a genuine human-typed prompt seeds the mandate — never an RPC or an
38
- // extension-injected message (inbox wakes, steering nudges, kickoffs).
39
- if (event.source !== 'interactive')
40
- return;
41
47
  const text = (event.text ?? '').trim();
42
48
  if (text === '')
43
49
  return;
44
- // A fresh-revive kickoff is delivered as the launch prompt; never let it
45
- // masquerade as the user's first mandate.
50
+ // Never seed a mandate or a name from an extension-injected message (inbox
51
+ // wakes, steering nudges) or a fresh-revive kickoff (the node is already
52
+ // named). Both would otherwise masquerade as the node's first real input.
53
+ if (event.source === 'extension')
54
+ return;
46
55
  if (text.startsWith(REVIVE_KICKOFF_SENTINEL))
47
56
  return;
48
- // First mandate for a bare root: persist it as the goal, and ask pi
49
- // (async, non-blocking) to name the session from it. The name lands on
50
- // meta.description; the onNamed callback pushes the new editor label into
51
- // THIS live session via setSessionName, so it updates immediately instead
52
- // of only on the next cycle.
53
- if (captureGoalIfAbsent(nodeId, text)) {
54
- generateAndPersistName(nodeId, text, (meta) => {
57
+ // Goal capture is bare-root only: a delegated child already had its goal
58
+ // persisted at birth (writeGoal), so only a genuine human-typed prompt
59
+ // seeds a mandate here.
60
+ if (event.source === 'interactive')
61
+ captureGoalIfAbsent(nodeId, text);
62
+ // Naming: name the node from its FIRST real message — a human's first line
63
+ // OR a delegated child's kickoff task (naming off the agent prompt is
64
+ // fine) — whenever it has no name yet. Async headless `pi -p` with no
65
+ // canvas extensions, so it never recurses into another spawn/name. The
66
+ // onNamed callback live-updates THIS session's label instead of waiting
67
+ // for the next cycle. The unnamed-guard keeps it to one call per node.
68
+ const meta = getNode(nodeId);
69
+ if (meta !== null && (meta.description ?? '').trim() === '') {
70
+ generateAndPersistName(nodeId, text, (named) => {
55
71
  try {
56
- pi.setSessionName?.(editorLabel(meta));
72
+ pi.setSessionName?.(editorLabel(named));
57
73
  }
58
74
  catch { /* best-effort */ }
59
75
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crouton-kit/crouter",
3
- "version": "0.3.26",
3
+ "version": "0.3.27",
4
4
  "description": "crtr — fast access to skills, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,100 +0,0 @@
1
- ---
2
- kind: skill
3
- when-and-why-to-read: When you are about to write or update a memory document — including any time the user asks you to remember something — or need to debug why a document is (not) surfacing, this skill should be read because it tells you how to choose kind and scope, set the disclosure rungs and gate, size the body, and when to first ask the user a clarifying question.
4
- short-form: Author excellent memories — kind, scope, visibility rungs, gates, body sizing, and the asked-to-remember workflow.
5
- system-prompt-visibility: preview
6
- file-read-visibility: none
7
- ---
8
-
9
- # Writing memories
10
-
11
- A memory is a markdown document whose frontmatter decides **when, where, and how much** of it loads into future agents. The body is the easy part; the craft is routing — deciding who sees it, at what moment, at what context cost. Every frontmatter choice below is a routing choice.
12
-
13
- ## When asked to remember something
14
-
15
- 1. **Write the routing line first.** Before storing anything, try to complete: *"When <circumstance>, this <kind> should be read because <payoff>."* If you cannot name the concrete situation a future agent will be in when this matters, you do not understand the memory yet — ask the user **one sharp question** instead of improvising. "Remember I like chicken" routes cleanly (food/meal decisions); "remember to be careful with the API" does not (which API? careful how? against what failure?) — that one needs clarifying before it becomes a memory. The routing line is a comprehension test.
16
- 2. **Find before write.** `crtr memory find <topic>` for an existing doc; update it rather than minting a near-duplicate. Prefer growing `food-preferences` over creating `likes-chicken` — one document per recurring *circumstance*, not one per fact. Group related docs with path names (`area/topic`). Delete memories that turn out wrong.
17
- 3. **Capture the why, not just the what.** Especially for corrections: record what was rejected and the reasoning. The why is what lets a future agent apply the rule to cases you never saw.
18
- 4. **Don't store what's already recorded** — code structure, git history, CLAUDE.md content — or what only matters to this conversation. If asked to remember something the repo already records, ask what was non-obvious about it and store that instead.
19
-
20
- ## Choosing kind
21
-
22
- - **skill** — how to *do* something (a playbook, a procedure you'd repeat).
23
- - **reference** — what is *true* or how something *works* (a fact about the user, a system's behavior, code docs).
24
- - **preference** — how to *behave* (a directive, a standing correction).
25
-
26
- The reference-vs-preference test: does it direct behavior ("always run lint after authoring") or inform the world-model ("Silas likes chicken", "the daemon never reloads dist/")? A correction usually yields a preference; a learned fact yields a reference; a repeatable procedure yields a skill.
27
-
28
- Kind sets sensible default rungs — the common case needs no visibility fields at all:
29
-
30
- | kind | at boot | on file-read |
31
- |---|---|---|
32
- | skill | name | none |
33
- | preference | preview | none |
34
- | reference | none | preview |
35
-
36
- ## The two hooks — boot vs file-read
37
-
38
- There are exactly two moments a doc can surface. `system-prompt-visibility` governs **boot** (the system prompt); `file-read-visibility` governs **on-read** (when a related file is opened).
39
-
40
- - Behavior and procedure (preferences, skills) are relevant regardless of which file is open → surface at boot, stay out of file-read. That is what the defaults do.
41
- - Knowledge about code (references) belongs **next to the code**: put the doc in that directory's `.crouter/memory/` and it fires positionally when files under that directory are read — costing nothing at boot.
42
- - The exception that matters: a reference about a *person or a process* ("Silas's food preferences") has no code directory to anchor to, so positional triggering is meaningless — set `system-prompt-visibility: preview` so its routing line surfaces at boot instead.
43
- - `applies-to: <glob or list>` extends the on-read trigger beyond position for cross-cutting docs (e.g. a testing reference that should fire for `**/*.test.ts` anywhere).
44
-
45
- ## Choosing the rung
46
-
47
- `none → name → preview → content`. Each rung up costs more of **every** future agent's context, paid at every boot or read, forever. Default down, not up.
48
-
49
- - **content** (full body injected): reserved for guidance that is BOTH always relevant AND ~one bullet's worth of text. Fail either test → preview. Situational guidance is preview *no matter how short*; long guidance is preview no matter how universal it feels.
50
- - **preview** (the one-sentence routing line): the workhorse. Costs one line; the body is read on demand.
51
- - **name** (title only): catalog-style docs whose name already routes (`api-reference`).
52
- - **none**: invisible except to `crtr memory find` — archival or narrowly specialized material.
53
-
54
- `short-form` is NOT a rung and never enters an agent's context (an agent handed a summary satisfices and skips the real read). It is the gist a human sees in `crtr memory list` — write it for them, and don't make it do routing work.
55
-
56
- ## The routing line
57
-
58
- `when-and-why-to-read` is **read-routing, never content**: it answers when to open the doc and why the read pays — never why the content should be obeyed, and never a paraphrase of the content. A preview that gives away the gist defeats the ladder: the agent feels informed and skips the body. Shape: *"When <circumstance the agent is in>, this <kind> should be read because <what the read buys>."* The test: can a stranger mid-task decide from this single line alone whether to spend the read?
59
-
60
- ## Gates — conditioning on who the agent is
61
-
62
- An optional `gate:` predicate makes the doc eligible only for nodes whose own config matches. Subject fields: `kind` (node role, free-form), `mode` (base|orchestrator), `lifecycle` (terminal|resident), `hasManager` (bool), `cwd`, `scope` (user|project), `orchestration.depth` (hops to the root orchestrator; root = 0). Matchers: scalar equality, list membership, or operator objects (`{gte: 2}`, `{in: […]}`, `{matches: "…"}`, `exists`, `contains`…), with `all`/`any`/`not` combinators.
63
-
64
- ```yaml
65
- gate: { mode: orchestrator } # only delegating managers
66
- gate: { orchestration.depth: { gte: 2 } } # only substantial, scaled-up efforts
67
- ```
68
-
69
- Default is no gate — always eligible; most docs want exactly that. An empty `gate: {}` is inert (never matches) — don't write it. A failing gate hides the doc from both hooks, but it stays findable by search.
70
-
71
- ## Scope — where the file lives
72
-
73
- - **user** (`~/.crouter/memory/`): facts about the user, cross-project behavior. The chicken memory goes here.
74
- - **project** (`<dir>/.crouter/memory/`): anything about a codebase or workspace — and place it in the *specific directory* it describes (any directory can hold a `.crouter/`), so positional on-read fires precisely.
75
- - **builtin**: ships with crtr itself (contributed via `src/builtin-memory/` in the crouter repo) — docs every crtr agent should have.
76
-
77
- Resolution is project > user > builtin with leaf-name fallback (`read <leaf>` finds `area/<leaf>` when unambiguous), so a project doc can shadow a same-named user or builtin doc.
78
-
79
- ## Mechanics worth knowing
80
-
81
- - **Directory entries**: a directory may carry an `INDEX.md` with the same frontmatter schema as any doc; the dir then renders as one entry at the INDEX's rung, and that rung is a **ceiling for its subtree** (`none` hides the whole dir). When a doc mysteriously isn't surfacing, check its ancestors' INDEX rungs and its gate.
82
- - **CLI**: `crtr memory list` (human inventory) · `read <name>` (names are path-derived, never file paths) · `find <query>` (search ignores gates and rungs) · `write` (author) · `lint` (validate after authoring). Run `-h` on a leaf before first use.
83
-
84
- ## Body content
85
-
86
- Write for a stranger: a future session that shares none of this conversation. No "as discussed", no narration of how you learned it — state current truth, not the history of getting there. Keep the reasoning behind rules; cut everything else. Don't pad: a preference can legitimately be two sentences, and a skill should lead with decisions, not mechanism. A body behind `preview` may be long when it earns it, but every line still costs a reader who is mid-task — dense beats complete.
87
-
88
- ## Worked example
89
-
90
- User says: *"remember that I like chicken."*
91
-
92
- ```bash
93
- echo "Silas likes chicken." | crtr memory write food-preferences \
94
- --kind reference --scope user \
95
- --when-and-why-to-read "When you are choosing or recommending food, meals, or recipes for Silas, this reference should be read because it records his food preferences." \
96
- --short-form "Silas's food likes/dislikes" \
97
- --system-prompt-visibility preview
98
- ```
99
-
100
- Reference (a fact, not a directive) · user scope (about the person, not a repo) · `preview` at boot (no code directory to anchor on-read) · named for the recurring circumstance, so future food facts append to the same doc. After authoring, validate: `crtr memory lint`.