@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.
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/commands/memory/write.js +8 -0
- package/dist/commands/memory.js +1 -1
- package/dist/core/runtime/naming.d.ts +1 -5
- package/dist/core/runtime/naming.js +33 -49
- package/dist/core/runtime/spawn.js +7 -10
- package/dist/pi-extensions/canvas-goal-capture.js +38 -22
- package/package.json +1 -1
- package/dist/builtin-memory/memory-authoring.md +0 -100
|
@@ -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 (
|
|
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.' },
|
package/dist/commands/memory.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
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
|
-
//
|
|
20
|
-
//
|
|
21
|
-
import {
|
|
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
|
-
/**
|
|
101
|
-
*
|
|
102
|
-
* output)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
timeout: NAME_TIMEOUT_MS,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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
|
-
//
|
|
45
|
-
//
|
|
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
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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(
|
|
72
|
+
pi.setSessionName?.(editorLabel(named));
|
|
57
73
|
}
|
|
58
74
|
catch { /* best-effort */ }
|
|
59
75
|
});
|
package/package.json
CHANGED
|
@@ -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`.
|