@north-light/crouter 0.3.161 → 0.3.163

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 (130) hide show
  1. package/dist/api/client.d.ts +3 -1
  2. package/dist/api/client.js +6 -0
  3. package/dist/api/dto/modelauth.d.ts +32 -0
  4. package/dist/api/dto/modelauth.js +2 -1
  5. package/dist/api/routes.d.ts +1 -0
  6. package/dist/api/routes.js +1 -0
  7. package/dist/builtin-memory/00-runtime-base.md +1 -0
  8. package/dist/builtin-memory/04-orchestration-kernel.md +1 -0
  9. package/dist/builtin-memory/init.md +38 -0
  10. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +2 -2
  11. package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +0 -1
  12. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +88 -396
  13. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +215 -419
  14. package/dist/clients/attach/__tests__/crtr-output.test.js +14 -9
  15. package/dist/clients/attach/__tests__/edit-diff.test.d.ts +1 -0
  16. package/dist/clients/attach/__tests__/edit-diff.test.js +34 -0
  17. package/dist/clients/attach/__tests__/editor-frame-title.test.d.ts +1 -0
  18. package/dist/clients/attach/__tests__/editor-frame-title.test.js +19 -0
  19. package/dist/clients/attach/render/chat-view.js +8 -3
  20. package/dist/clients/attach/render/crtr-output.js +31 -2
  21. package/dist/clients/attach/render/edit-diff.js +26 -2
  22. package/dist/clients/attach/render/tool-calls.d.ts +7 -2
  23. package/dist/clients/attach/render/tool-calls.js +25 -4
  24. package/dist/clients/attach/session/connection.d.ts +1 -1
  25. package/dist/clients/attach/session/editor-frame.d.ts +5 -1
  26. package/dist/clients/attach/session/editor-frame.js +21 -15
  27. package/dist/clients/attach/session/identity.d.ts +1 -1
  28. package/dist/clients/attach/viewer.js +441 -439
  29. package/dist/commands/__tests__/api-canvas-source.test.d.ts +1 -0
  30. package/dist/commands/__tests__/api-canvas-source.test.js +37 -0
  31. package/dist/commands/__tests__/human.test.js +2 -0
  32. package/dist/commands/__tests__/search-contents.test.d.ts +1 -0
  33. package/dist/commands/__tests__/search-contents.test.js +52 -0
  34. package/dist/commands/api-client.js +1 -0
  35. package/dist/commands/human/prompts.js +2 -2
  36. package/dist/commands/human/shared.d.ts +1 -0
  37. package/dist/commands/human/shared.js +7 -4
  38. package/dist/commands/memory/lint.d.ts +6 -0
  39. package/dist/commands/memory/lint.js +177 -35
  40. package/dist/commands/memory/read.js +22 -1
  41. package/dist/commands/memory/write.js +18 -4
  42. package/dist/commands/memory.js +1 -1
  43. package/dist/commands/search/contents.js +2 -2
  44. package/dist/commands/sys/__tests__/sync-deps.test.js +12 -21
  45. package/dist/commands/sys/__tests__/sync-import.test.js +30 -29
  46. package/dist/commands/sys/setup-wizard.d.ts +8 -1
  47. package/dist/commands/sys/setup-wizard.js +209 -58
  48. package/dist/commands/sys/sync-deps.js +14 -18
  49. package/dist/commands/sys/sync-project-guidance.js +16 -19
  50. package/dist/core/__tests__/broker-launch-candidates.test.js +13 -3
  51. package/dist/core/__tests__/broker-sdk-wiring.test.js +53 -25
  52. package/dist/core/__tests__/full/broker-provider-retry.test.js +6 -122
  53. package/dist/core/__tests__/migration.test.js +8 -3
  54. package/dist/core/__tests__/model-pin-durability.test.js +7 -5
  55. package/dist/core/__tests__/model-routes.test.d.ts +1 -0
  56. package/dist/core/__tests__/model-routes.test.js +41 -0
  57. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +7 -10
  58. package/dist/core/__tests__/on-read-dedup-resume.test.js +13 -25
  59. package/dist/core/__tests__/on-read-identity.test.js +8 -15
  60. package/dist/core/__tests__/review-model-floor.test.js +2 -2
  61. package/dist/core/__tests__/revive.test.js +2 -2
  62. package/dist/core/__tests__/tmux-surface.test.js +10 -4
  63. package/dist/core/__tests__/worktree.test.js +3 -3
  64. package/dist/core/auth-file.d.ts +3 -2
  65. package/dist/core/auth-file.js +5 -3
  66. package/dist/core/canvas/extensions.d.ts +1 -1
  67. package/dist/core/canvas/extensions.js +18 -11
  68. package/dist/core/canvas/labels.d.ts +4 -5
  69. package/dist/core/canvas/labels.js +9 -9
  70. package/dist/core/canvas/types.d.ts +6 -1
  71. package/dist/core/command-manifests/schema.js +18 -5
  72. package/dist/core/command.js +17 -7
  73. package/dist/core/config.d.ts +3 -1
  74. package/dist/core/config.js +88 -2
  75. package/dist/core/configured-clis/invoker.js +2 -0
  76. package/dist/core/help.d.ts +3 -1
  77. package/dist/core/help.js +5 -2
  78. package/dist/core/keybindings/__tests__/resolve.test.js +5 -2
  79. package/dist/core/keybindings/catalog.d.ts +3 -2
  80. package/dist/core/keybindings/catalog.js +34 -25
  81. package/dist/core/keybindings/index.d.ts +1 -1
  82. package/dist/core/keybindings/resolve.js +8 -0
  83. package/dist/core/keybindings/types.d.ts +6 -1
  84. package/dist/core/memory/doc-link-grammar.d.ts +20 -0
  85. package/dist/core/memory/doc-link-grammar.js +110 -0
  86. package/dist/core/memory-resolver.d.ts +5 -0
  87. package/dist/core/memory-resolver.js +12 -1
  88. package/dist/core/model-routes.d.ts +37 -0
  89. package/dist/core/model-routes.js +140 -0
  90. package/dist/core/runtime/bearings.d.ts +8 -8
  91. package/dist/core/runtime/bearings.js +20 -16
  92. package/dist/core/runtime/broker.d.ts +0 -4
  93. package/dist/core/runtime/broker.js +119 -385
  94. package/dist/core/runtime/canvas-extensions.d.ts +1 -0
  95. package/dist/core/runtime/canvas-extensions.js +2 -0
  96. package/dist/core/runtime/launch.d.ts +10 -12
  97. package/dist/core/runtime/launch.js +16 -22
  98. package/dist/core/runtime/managed-provider-cooling.d.ts +0 -14
  99. package/dist/core/runtime/managed-provider-cooling.js +0 -19
  100. package/dist/core/runtime/model-swap.js +7 -1
  101. package/dist/core/runtime/promote.js +1 -0
  102. package/dist/core/runtime/tmux.js +136 -100
  103. package/dist/core/scope.js +4 -5
  104. package/dist/core/subscription-state.d.ts +12 -36
  105. package/dist/core/subscription-state.js +19 -203
  106. package/dist/core/substrate/ceiling.d.ts +3 -6
  107. package/dist/core/substrate/ceiling.js +13 -15
  108. package/dist/core/substrate/index.d.ts +2 -3
  109. package/dist/core/substrate/index.js +2 -2
  110. package/dist/core/substrate/on-read.d.ts +4 -13
  111. package/dist/core/substrate/on-read.js +142 -262
  112. package/dist/core/substrate/render.js +4 -4
  113. package/dist/core/substrate/schema.d.ts +3 -2
  114. package/dist/core/substrate/schema.js +5 -5
  115. package/dist/daemon/api/__tests__/full/api-server.test.js +9 -6
  116. package/dist/daemon/api/__tests__/full/b10-attach-modelauth.test.js +123 -1
  117. package/dist/daemon/api/handlers/modelauth.js +51 -2
  118. package/dist/daemon/api/handlers/nodes.js +2 -0
  119. package/dist/pi-extensions/__tests__/canvas-tool-guide.test.d.ts +1 -0
  120. package/dist/pi-extensions/__tests__/canvas-tool-guide.test.js +96 -0
  121. package/dist/pi-extensions/canvas-doc-substrate.js +7 -8
  122. package/dist/pi-extensions/canvas-tool-guide.d.ts +14 -0
  123. package/dist/pi-extensions/canvas-tool-guide.js +74 -0
  124. package/dist/types.d.ts +17 -0
  125. package/dist/web-client/assets/{index-B76ZKfT_.js → index-NIuSCOHM.js} +1 -1
  126. package/dist/web-client/index.html +1 -1
  127. package/dist/web-client/sw.js +1 -1
  128. package/package.json +1 -1
  129. package/runtime.lock.json +2 -2
  130. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crouter-help.ts +0 -95
@@ -18,10 +18,12 @@ export const writeLeaf = defineLeaf({
18
18
  'Store reusable current truth, not session notes. Useful memories are non-obvious procedures, gotchas, durable preferences, cross-repo conventions, and amendments to plans/specs that future agents must honor. Put plan/spec amendments under path names like `projects/<topic>/...`; add `projects/<topic>/INDEX.md` with `name` visibility so the topic is discoverable without loading the whole body. Do not store chat summaries, implementation history, or facts already recorded in the repo.\n\n' +
19
19
  'Set both visibility rungs explicitly on create. There is no kind default. `none`, `name`, `preview`, and `content` move from least to most loaded: `none` keeps the doc out of auto-load and on-read surfaces, `name` is the bare doc tag only, `preview` is the name + envelope + routing line (`when-and-why-to-read`), and `content` inlines the whole body when the body is short enough to justify it. Each axis is independent; usually one carries a real rung and the other is `none`. When a doc is short enough to state in a single sentence — a one-line preference, or a knowledge fact that fits in a sentence — skip `preview` and use `name` or `content` directly: the routing line would run longer than the doc itself, so a `preview` rung just adds words. Sentence-length `content` docs are not just acceptable, they are correct. Never pad a memory to be more verbose than the rule or fact it carries.\n\n' +
20
20
  'Choose the scope. `project` is for facts any agent in one repo needs. `user` is for person-wide facts and preferences that should follow the user everywhere. `profile` is for the profile’s bundle of dirs: cross-repo conventions, how the pieces relate, or the user’s stance toward that body of work. `node` is the narrowest — the this-node store (`nodes/<CRTR_NODE_ID>/context/memory/`), scratch memory only this running node sees; it rides straight into this node’s knowledge block and dies with the node. When unsure, choose the narrowest scope that will still reach the next agent who needs it.\n\n' +
21
- 'AGENTS.md is the one special doc: a store’s always-loaded operating guide, and every dir a profile manages should carry one (`crtr memory lint` flags the gaps). Name the doc AGENTS, target the exact dir with --dir, and set system-prompt-visibility to `content` so every agent working there boots with it. It is a front door, not a manual — constraints and gotchas, the key commands, a few sentences of architecture orientation, and only the conventions that differ from defaults. Every line is paid by every session in that dir forever: prefer a pointer (“when X, run Y -h / read Z, because W”) over inlined how-to, and cut any line that would not change an agent’s behavior.\n\n' +
22
- 'Choose the hook — boot vs file-read. Behavior and how-to procedure surface at boot. Knowledge about code belongs next to the code: put the file in that directory’s .crouter/memory/ (`--dir <project-dir>` targets it exactly, from anywhere) and it fires positionally when files there are read, costing nothing at boot. A knowledge doc about a person or process has no code directory to anchor to, so on-read triggering is meaningless — route it through boot instead and set the boot rung to `preview` when you want the routing line to surface.\n\n' +
21
+ 'A project root INDEX is the workspace front door. Name the physical doc INDEX, target the exact project with --dir, and use kind knowledge, system-prompt-visibility none, file-read-visibility content, and applies-to ".". The `.` route surfaces the full operating guide in first-message context whenever cwd or a selected profile mounts that project store; its envelope name comes from the owning project directory unless the document already carries an explicit name. It is a front door, not a manual — keep only the project constraints, key commands, architecture orientation, and conventions that differ from defaults. `crtr memory lint` validates this exact contract for every project managed by the selected profile.\n\n' +
22
+ 'Choose the hook — boot vs file context. System-prompt visibility is the boot catalog. Every non-none file-read rung also requires at least one explicit applies-to route: `.` means project workspace/profile mount and is evaluated during first-message assembly; every other value is a glob evaluated only after a matching file is actually read. There is no positional fallback from where the memory file lives. Put code-specific knowledge in the owning project store, give it the narrowest real file glob, and keep it out of boot when the file read is the useful trigger. Knowledge about a person or process usually has no file boundary, so set file-read-visibility none and route it through boot instead.\n\n' +
23
23
  'Write the routing line (--when-and-why-to-read) first, before storing anything: "When <circumstance>, this <kind> should be read because <broader downstream payoff>." WHY is the READER’s payoff — the consequence they secure for the task in front of them by spending the read — never what the document says, its rule, or why it should be obeyed. The trap most authors fall into is the DISGUISED restatement: a because-clause that reads like a benefit but is just the doc’s own thesis reworded as an outcome. It still fails. The test: if you could derive the because-clause by paraphrasing the doc’s advice, it is a restatement, not a payoff — a real payoff names a consequence in the reader’s world that the document itself never asserts. Bad (naked restatement): "because only genuine first principles belong in taste memory." Bad (restatement in benefit’s clothing): "because keeping the test loop fast and free of speculative tests protects the development pace" — that is the doc’s rule in outcome costume, derivable straight from its advice. Good: "because identifying throughlines in user taste lets future decisions be made faster and with less re-litigation." The stranger test: someone mid-task who has NOT read the doc must be able to decide from this one line alone whether the read is worth it. If you cannot name the concrete situation that triggers it, you do not yet understand the memory — ask the user one sharp question instead of improvising.\n\n' +
24
24
  'Gate and read-when share the same predicate language: a field map is AND-ed across fields; dotted fields resolve nested values; field matchers may be scalar, array, or object. Scalar matchers do exact equality, with arrays matching any element. Array matchers do membership or intersection. Object matchers accept `eq`, `ne`, `in`, `nin`, `exists`, `contains`, `containsAll`, `containsAny`, `matches`, `imatches`, `gt`, `gte`, `lt`, and `lte`. Combinators are `all`, `any`, and `not`; sibling field matchers next to them are AND-ed in. An empty condition is inert. An unknown op never matches.\n\n' +
25
+ 'Link related docs with `[[canonical/name]]`. A doc link in any body is a first-class cross-reference to another memory document by its exact canonical name — the same identifier `crtr memory read` takes (a directory INDEX is linked by its bare directory name). Links are pointers, never transclusion: the linked body is loaded only when a reader follows it, so a link costs nothing until needed. `crtr memory lint` fails on a link that resolves to no document, and `crtr memory read` lists a doc’s resolvable links alongside its body. There is no alias or label form.\n\n' +
26
+ 'When a doc grows long or information-rich, nest it into a graph instead of letting it become a scroll. The main doc at the topic’s path keeps the high-level, most load-bearing information, most important first; depth splits into reference docs under the topic’s directory (`area/topic/...`), each pointed at with a `[[link]]` as further reading. The main doc is the entry point a reader can act from alone; a reference leaf is loaded only when the task needs that depth. Save reference leaves at `none` visibility on both axes — the link from the main doc is how they are found, so any higher rung just double-charges every boot or read for depth the graph already routes. This is the same layering the visibility rungs apply to boot cost, applied to the body itself. Keep every doc as short as its job allows; `crtr memory lint` caps body length by rung and its findings carry the split guidance.\n\n' +
25
27
  'Find before write. Prefer slightly expanding an existing document, nesting genuinely separate depth under its topic, and updating the existing `when-and-why-to-read` (plus its INDEX router when present) over creating another similar memory. A new document earns its own identity only when it has a distinct read trigger and a coherent body that would make the existing document harder to route or use. Group related docs with path names (area/topic). Do not store what is already recorded or what only matters to this conversation. Body is for current truth, not history. Provenance is automatic on create and preserved on update. Run `crtr memory lint` — the frontmatter validator — after authoring.\n\n' +
26
28
  '--rationale is the gap this doc exists to close — the observed agent failure that prompted it, captured from user signal (a correction, a mistake you watched happen) rather than inferred from the doc’s own content. The bar: if the rationale is guessable from reading the doc, it is not the real one — a guessable gap is one agents do not actually fall into, so a merely-plausible-sounding reason is not worth recording. It is maintainer-facing only and NEVER ships in a delivered surface (boot render, on-read injection, `memory read` content) — it lives in frontmatter, visible only via `memory read --frontmatter`. Omit it when you have no such gap to record; omitting the flag on an update always preserves whatever rationale already exists.',
27
29
  params: [
@@ -30,9 +32,9 @@ export const writeLeaf = defineLeaf({
30
32
  { kind: 'flag', name: 'when-and-why-to-read', type: 'string', required: false, constraint: 'ONE routing sentence: "When <circumstance>, this <kind> should be read because <broader downstream payoff>." WHY is the reader’s payoff — the consequence they secure for their task by reading — NEVER the doc summary, its rule, or that rule reworded as an outcome (a benefit-shaped restatement still fails). Rendered verbatim as the preview; required when creating.' },
31
33
  { 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`.' },
32
34
  { 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. Required when creating — there is no kind default; pick a rung explicitly.' },
33
- { 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. Required when creating — there is no kind default; pick a rung explicitly.' },
35
+ { kind: 'flag', name: 'file-read-visibility', type: 'enum', choices: [...VISIBILITY_RUNGS], required: false, constraint: 'Rung controlling how much surfaces through file context. Required when creating — there is no kind default. Every value except none also requires at least one applies-to route in the resulting document.' },
34
36
  { kind: 'flag', name: 'gate', type: 'string', required: false, constraint: 'Frontmatter gate — YAML/JSON object predicate over node config using the same field/matcher vocabulary described in the guide.' },
35
- { kind: 'flag', name: 'applies-to', type: 'string', required: false, repeatable: true, constraint: 'Frontmatter applies-to — one glob/path scope per flag occurrence. Repeat --applies-to for multiple globs (e.g. --applies-to "src/**" --applies-to "docs/**").' },
37
+ { kind: 'flag', name: 'applies-to', type: 'string', required: false, repeatable: true, constraint: 'One explicit file-context route per occurrence. In a project store, `.` fires when cwd/profile mounts that workspace during first-message assembly. Any other value is a glob matched after an actual file read against the absolute path, basename, and path relative to the owning project root. Required whenever file-read-visibility is not none.' },
36
38
  { kind: 'flag', name: 'read-when', type: 'string', required: false, constraint: 'Frontmatter read-when — YAML/JSON object predicate over a read file’s own frontmatter using the same field/matcher vocabulary described in the guide.' },
37
39
  { kind: 'flag', name: 'slash', type: 'bool', required: false, default: false, constraint: 'Presence flags this doc invocable as a pi slash command (`/<name>`, `/` in a nested name rendered as `:`) — the doc body becomes the command’s injected prompt. Default false: most docs are consulted, not invoked.' },
38
40
  { kind: 'flag', name: 'rationale', type: 'string', required: false, constraint: 'Frontmatter rationale — the observed agent failure that made this doc necessary. Maintainer-facing only: never ships in any delivered surface (boot render, on-read injection, `memory read` content), visible only via `memory read --frontmatter`. Omitting this flag on an update PRESERVES an existing rationale unchanged.' },
@@ -112,6 +114,18 @@ export const writeLeaf = defineLeaf({
112
114
  if (input['slash'] === true)
113
115
  frontmatter['slash'] = true;
114
116
  setIf('rationale', input['rationale']);
117
+ const applies = frontmatter['applies-to'];
118
+ const routes = typeof applies === 'string'
119
+ ? [applies]
120
+ : Array.isArray(applies) && applies.every((value) => typeof value === 'string')
121
+ ? applies
122
+ : [];
123
+ if (frontmatter['file-read-visibility'] !== 'none' && routes.length === 0) {
124
+ throw usage(`file-read-visibility ${String(frontmatter['file-read-visibility'])} requires --applies-to: use "." for project workspace/profile-open context, a real file glob for matching reads, or choose file-read-visibility none`);
125
+ }
126
+ if (scope !== 'project' && routes.some((route) => route.trim() === '.')) {
127
+ throw usage('applies-to "." is a project-store workspace mount route; use a real file glob or choose file-read-visibility none for user, profile, and node memory');
128
+ }
115
129
  writeText(path, serializeMemoryDoc(frontmatter, body));
116
130
  // Confirm exactly the frontmatter choices made by this invocation. On an
117
131
  // update, inherited fields stay in the file but do not masquerade as newly
@@ -19,7 +19,7 @@ export function registerMemory() {
19
19
  help: {
20
20
  name: 'memory',
21
21
  summary: 'list, read, search, and write memory documents — knowledge and preferences',
22
- model: 'Documents have path-derived identities and resolve across layered scopes in precedence order: node > project stack > profile > user > builtin. Discover a document with `find` when its name is unknown; address it directly with `read`/`write` when known. A directory may carry an `INDEX.md` with the same frontmatter schema as any doc — it 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.',
22
+ model: 'Documents have path-derived identities and resolve across layered scopes in precedence order: node > project stack > profile > user > builtin. Discover a document with `find` when its name is unknown; address it directly with `read`/`write` when known. A directory may carry an `INDEX.md` with the same frontmatter schema as any doc — it renders as one boot-catalog entry whose system-prompt rung caps that subtree. File context is separate and explicit: `applies-to: "."` opens project context with the workspace; other globs fire after matching reads.',
23
23
  },
24
24
  children: [listLeaf, readLeaf, findLeaf, writeLeaf, deleteLeaf, originLeaf, lintLeaf],
25
25
  });
@@ -10,7 +10,7 @@ export const contentsLeaf = defineLeaf({
10
10
  name: 'search contents',
11
11
  summary: 'content extraction via Exa — cleaned highlights or full text for URLs you already have',
12
12
  params: [
13
- { kind: 'positional', name: 'urls', required: true, constraint: 'One or more URLs to extract, separated by commas or whitespace.' },
13
+ { kind: 'positional', name: 'urls', repeatable: true, required: true, constraint: 'One or more URL tokens to extract. Each token may contain URLs separated by commas or whitespace.' },
14
14
  { kind: 'flag', name: 'text', type: 'bool', required: false, constraint: `Return cleaned full page text (capped at ${TEXT_MAX_CHARACTERS} characters per URL) instead of highlight excerpts. Off by default.` },
15
15
  { kind: 'flag', name: 'max-age-hours', type: 'int', required: false, constraint: 'Maximum acceptable age of cached content, in hours; content older than this is freshly crawled. 0 forces a fresh crawl every time. Omit to use cache when available and crawl as fallback.' },
16
16
  ],
@@ -23,7 +23,7 @@ export const contentsLeaf = defineLeaf({
23
23
  effects: ['Sends one contents request to the Exa API (network). URLs Exa fails to fetch are retried through the pure.md markdown service. No local state changes.'],
24
24
  },
25
25
  run: async (input) => {
26
- const urls = parseUrls(input['urls']);
26
+ const urls = input['urls'].flatMap(parseUrls);
27
27
  if (urls.length === 0)
28
28
  throw usage('no URLs provided', { next: 'Pass one or more URLs separated by commas or whitespace.' });
29
29
  const wantText = input['text'];
@@ -9,7 +9,6 @@ import { closeDb } from '../../../core/canvas/db.js';
9
9
  import { resetScopeCache } from '../../../core/scope.js';
10
10
  import { spawnNode } from '../../../core/runtime/nodes.js';
11
11
  import { renderKnowledgeBlock } from '../../../core/substrate/render-node.js';
12
- import { renderOnReadDocs } from '../../../core/substrate/on-read.js';
13
12
  import { clearSessionCache } from '../../../core/substrate/session-cache.js';
14
13
  import { runDepsSync, sysSyncDepsLeaf } from '../sync-deps.js';
15
14
  const projects = [];
@@ -26,7 +25,7 @@ function assertVisibility(path, system, fileRead) {
26
25
  assert.match(doc, new RegExp(`^system-prompt-visibility: ${system}$`, 'm'));
27
26
  assert.match(doc, new RegExp(`^file-read-visibility: ${fileRead}$`, 'm'));
28
27
  }
29
- test('renders the dependency catalog preview and generated bundle names without leaf prose', () => {
28
+ test('renders the dependency boot catalog preview and generated bundle names without leaf prose', () => {
30
29
  const originalHome = process.env.HOME;
31
30
  const originalCrtrHome = process.env.CRTR_HOME;
32
31
  const originalCwd = process.cwd();
@@ -52,21 +51,13 @@ test('renders the dependency catalog preview and generated bundle names without
52
51
  clearSessionCache();
53
52
  const node = spawnNode({ kind: 'general', cwd: project, parent: null }).node_id;
54
53
  const boot = renderKnowledgeBlock(node);
55
- const readFile = join(project, 'src', 'app.ts');
56
- write(readFile, 'export {};\n');
57
- const onRead = renderOnReadDocs(node, readFile, new Set());
58
- for (const rendered of [boot, onRead]) {
59
- assert.match(rendered, /When writing code against this project's dependencies/, 'the top catalog stays preview-visible');
60
- assert.doesNotMatch(rendered, /Dependency-shipped agent docs \(generated\)/, 'the top catalog body stays hidden');
61
- for (const name of ['flat', 'reference', 'nested', 'child', 'notes']) {
62
- assert.ok(rendered.includes(name), `expected generated name ${name} in rendered output`);
63
- }
64
- for (const leaked of ['FLAT_BUNDLE_BODY', 'FLAT_SIDECAR_BODY', 'NESTED_BUNDLE_BODY', 'NESTED_SIDECAR_BODY', 'Flat bundle routing prose.', 'Nested bundle routing prose.']) {
65
- assert.ok(!rendered.includes(leaked), `generated leaf prose must not render: ${leaked}`);
66
- }
54
+ assert.match(boot, /When writing code against this project's dependencies/, 'the top catalog stays preview-visible');
55
+ assert.doesNotMatch(boot, /Dependency-shipped agent docs \(generated\)/, 'the top catalog body stays hidden');
56
+ for (const name of ['flat', 'reference', 'nested', 'child', 'notes']) {
57
+ assert.ok(boot.includes(name), `expected generated name ${name} in boot output`);
67
58
  }
68
- for (const name of ['deps/flat', 'deps/flat/reference', 'deps/nested/child', 'deps/nested/child/notes']) {
69
- assert.match(onRead, new RegExp(`name="${name}"`), `on-read output exposes bare ${name}`);
59
+ for (const leaked of ['FLAT_BUNDLE_BODY', 'FLAT_SIDECAR_BODY', 'NESTED_BUNDLE_BODY', 'NESTED_SIDECAR_BODY', 'Flat bundle routing prose.', 'Nested bundle routing prose.']) {
60
+ assert.ok(!boot.includes(leaked), `generated leaf prose must not render: ${leaked}`);
70
61
  }
71
62
  }
72
63
  finally {
@@ -115,10 +106,10 @@ test('excludes one exact dependency bundle before vendoring while retaining safe
115
106
  assert.equal(existsSync(join(deps, 'safe', 'INDEX.md')), true);
116
107
  assert.equal(existsSync(join(deps, 'unsafe', 'INDEX.md')), false);
117
108
  assert.equal(existsSync(join(deps, 'unsafe', 'reference.md')), false);
118
- assertVisibility(join(deps, 'safe', 'INDEX.md'), 'name', 'name');
119
- assertVisibility(join(deps, 'safe', 'reference.md'), 'name', 'name');
109
+ assertVisibility(join(deps, 'safe', 'INDEX.md'), 'name', 'none');
110
+ assertVisibility(join(deps, 'safe', 'reference.md'), 'name', 'none');
120
111
  const catalog = join(deps, 'INDEX.md');
121
- assertVisibility(catalog, 'preview', 'preview');
112
+ assertVisibility(catalog, 'preview', 'none');
122
113
  const catalogBody = readFileSync(catalog, 'utf8');
123
114
  assert.match(catalogBody, /deps\/safe/);
124
115
  assert.doesNotMatch(catalogBody, /unsafe/);
@@ -147,8 +138,8 @@ test('excludes a parent bundle without deleting its included nested bundle', ()
147
138
  assert.equal(existsSync(join(deps, 'reference.md')), false);
148
139
  assert.equal(existsSync(join(deps, 'child', 'INDEX.md')), true);
149
140
  assert.equal(existsSync(join(deps, 'child', 'reference.md')), true);
150
- assertVisibility(join(deps, 'child', 'INDEX.md'), 'name', 'name');
151
- assertVisibility(join(deps, 'child', 'reference.md'), 'name', 'name');
141
+ assertVisibility(join(deps, 'child', 'INDEX.md'), 'name', 'none');
142
+ assertVisibility(join(deps, 'child', 'reference.md'), 'name', 'none');
152
143
  });
153
144
  test('writes a truthful empty catalog when exclusions remove every bundle', () => {
154
145
  const project = mkdtempSync(join(tmpdir(), 'crtr-sync-deps-empty-'));
@@ -17,7 +17,7 @@ import { closeDb } from '../../../core/canvas/db.js';
17
17
  import { resolveMemoryDoc } from '../../../core/memory-resolver.js';
18
18
  import { parseFrontmatterGeneric } from '../../../core/frontmatter.js';
19
19
  import { gitSync } from '../../../core/git.js';
20
- import { renderKnowledgeBlock } from '../../../core/substrate/render-node.js';
20
+ import { renderWorkspaceOpenDocs } from '../../../core/substrate/on-read.js';
21
21
  import { clearSessionCache } from '../../../core/substrate/session-cache.js';
22
22
  import { sysSyncSkillsLeaf } from '../sync-skills.js';
23
23
  import { sysSyncProjectGuidanceLeaf } from '../sync-project-guidance.js';
@@ -124,24 +124,25 @@ test('migrates CLAUDE.md and a .claude/rules file into that dir\'s own .crouter/
124
124
  writeFileSync(join(source, '.claude', 'rules', 'foo.md'), '---\nname: foo\ndescription: Applies when touching src files.\npaths:\n - "src/**/*.ts"\n---\n\nDo the foo thing.\n', 'utf8');
125
125
  const dry = await sysSyncProjectGuidanceLeaf.run({ source, dryRun: true });
126
126
  const dryRows = dry?.results;
127
- assert.equal(dryRows.length, 2, 'both the AGENTS.md and the rule dry-run as candidates');
127
+ assert.equal(dryRows.length, 2, 'both the project guide and the rule dry-run as candidates');
128
128
  assert.ok(dryRows.every((r) => r.status === 'would-import'));
129
- assert.equal(existsSync(join(source, '.crouter', 'memory', 'AGENTS.md')), false, 'dry-run writes nothing');
129
+ assert.equal(existsSync(join(source, '.crouter', 'memory', 'INDEX.md')), false, 'dry-run writes nothing');
130
130
  await sysSyncProjectGuidanceLeaf.run({ source });
131
- const agentsTarget = join(source, '.crouter', 'memory', 'AGENTS.md');
131
+ const projectIndexTarget = join(source, '.crouter', 'memory', 'INDEX.md');
132
132
  const ruleTarget = join(source, '.crouter', 'memory', 'foo.md');
133
- assert.equal(existsSync(agentsTarget), true);
133
+ assert.equal(existsSync(projectIndexTarget), true);
134
134
  assert.equal(existsSync(ruleTarget), true);
135
- const agentsDoc = parseFrontmatterGeneric(readFileSync(agentsTarget, 'utf8'));
136
- assert.equal(agentsDoc.data?.kind, 'knowledge');
137
- assert.equal(agentsDoc.data?.name, 'demo-project');
138
- assert.equal(agentsDoc.data?.['system-prompt-visibility'], 'content');
139
- assert.equal(agentsDoc.data?.['file-read-visibility'], 'content');
140
- assert.match(agentsDoc.body, /Build then commit\./);
135
+ const projectIndex = parseFrontmatterGeneric(readFileSync(projectIndexTarget, 'utf8'));
136
+ assert.equal(projectIndex.data?.kind, 'knowledge');
137
+ assert.equal(projectIndex.data?.name, 'demo-project');
138
+ assert.equal(projectIndex.data?.['system-prompt-visibility'], 'none');
139
+ assert.equal(projectIndex.data?.['file-read-visibility'], 'content');
140
+ assert.deepEqual(projectIndex.data?.['applies-to'], ['.']);
141
+ assert.match(projectIndex.body, /Build then commit\./);
141
142
  const prevCwd = process.cwd();
142
143
  process.chdir(source);
143
144
  try {
144
- assert.equal(resolveMemoryDoc('demo-project').path, realpathSync(agentsTarget));
145
+ assert.equal(resolveMemoryDoc('demo-project').path, realpathSync(projectIndexTarget));
145
146
  assert.equal(resolveMemoryDoc('demo-project').name, 'demo-project');
146
147
  }
147
148
  finally {
@@ -165,8 +166,8 @@ test('migrates CLAUDE.md and a .claude/rules file into that dir\'s own .crouter/
165
166
  const overwritten = await sysSyncProjectGuidanceLeaf.run({ source, overwrite: true });
166
167
  const overwriteRows = overwritten?.results;
167
168
  assert.ok(overwriteRows.some((r) => r.status === 'imported'));
168
- const agentsDoc2 = parseFrontmatterGeneric(readFileSync(agentsTarget, 'utf8'));
169
- assert.match(agentsDoc2.body, /Build, THEN commit\./);
169
+ const projectIndex2 = parseFrontmatterGeneric(readFileSync(projectIndexTarget, 'utf8'));
170
+ assert.match(projectIndex2.body, /Build, THEN commit\./);
170
171
  rmSync(source, { recursive: true, force: true });
171
172
  });
172
173
  test('routes the walk-root AGENTS.md doc by the real git repo name, not a worktree\'s own basename', async () => {
@@ -189,13 +190,13 @@ test('routes the walk-root AGENTS.md doc by the real git repo name, not a worktr
189
190
  writeFileSync(join(linkedWorktree, 'CLAUDE.md'), '# Fallback Title\n\nRepo guidance.\n', 'utf8');
190
191
  const result = await sysSyncProjectGuidanceLeaf.run({ source: linkedWorktree });
191
192
  assert.equal(result?.imported, 1);
192
- const target = join(linkedWorktree, '.crouter', 'memory', 'AGENTS.md');
193
+ const target = join(linkedWorktree, '.crouter', 'memory', 'INDEX.md');
193
194
  const doc = parseFrontmatterGeneric(readFileSync(target, 'utf8'));
194
195
  assert.equal(doc.data?.['when-and-why-to-read'], 'When working in demo-repo, this knowledge should be read because it is the project\'s operating guide.');
195
196
  git(['worktree', 'remove', '--force', linkedWorktree], mainRepo);
196
197
  rmSync(base, { recursive: true, force: true });
197
198
  });
198
- test('sibling purview roots keep distinct AGENTS names so boot render does not dedupe them away', async () => {
199
+ test('sibling purview roots keep distinct INDEX identities so workspace-open render does not dedupe them away', async () => {
199
200
  const base = mkdtempSync(join(tmpdir(), 'crtr-sys-sync-purview-'));
200
201
  const rootA = join(base, 'alpha-root');
201
202
  const rootB = join(base, 'beta-root');
@@ -218,15 +219,15 @@ test('sibling purview roots keep distinct AGENTS names so boot render does not d
218
219
  process.chdir(runDir);
219
220
  clearSessionCache();
220
221
  const alphaDoc = resolveMemoryDoc('alpha-root');
221
- assert.equal(alphaDoc.path, join(rootA, '.crouter', 'memory', 'AGENTS.md'));
222
+ assert.equal(alphaDoc.path, join(rootA, '.crouter', 'memory', 'INDEX.md'));
222
223
  assert.equal(alphaDoc.name, 'alpha-root');
223
224
  const betaDoc = resolveMemoryDoc('beta-root');
224
- assert.equal(betaDoc.path, join(rootB, '.crouter', 'memory', 'AGENTS.md'));
225
+ assert.equal(betaDoc.path, join(rootB, '.crouter', 'memory', 'INDEX.md'));
225
226
  assert.equal(betaDoc.name, 'beta-root');
226
- createNode(node('bootcheck', runDir));
227
- const block = renderKnowledgeBlock('bootcheck');
228
- assert.match(block, /Alpha body\./, 'first purview root survives the boot render');
229
- assert.match(block, /Beta body\./, 'second purview root survives the boot render');
227
+ createNode({ ...node('bootcheck', runDir), profile_id: profileId });
228
+ const block = renderWorkspaceOpenDocs('bootcheck');
229
+ assert.match(block, /Alpha body\./, 'first purview root survives workspace-open rendering');
230
+ assert.match(block, /Beta body\./, 'second purview root survives workspace-open rendering');
230
231
  }
231
232
  finally {
232
233
  process.chdir(prevCwd);
@@ -237,7 +238,7 @@ test('sibling purview roots keep distinct AGENTS names so boot render does not d
237
238
  }
238
239
  rmSync(base, { recursive: true, force: true });
239
240
  });
240
- test('a nested project-subdir AGENTS/CLAUDE doc migrates to its OWN <nested>/.crouter/memory/AGENTS.md with a deterministic, non-colliding explicit name', async () => {
241
+ test('a nested project-subdir guide migrates to its OWN <nested>/.crouter/memory/INDEX.md with a deterministic, non-colliding explicit name', async () => {
241
242
  const source = mkdtempSync(join(tmpdir(), 'crtr-sys-sync-nested-'));
242
243
  writeFileSync(join(source, 'CLAUDE.md'), '# Nested Demo\n\nRoot guidance.\n', 'utf8');
243
244
  const nestedDir = join(source, 'packages', 'widgets');
@@ -247,10 +248,10 @@ test('a nested project-subdir AGENTS/CLAUDE doc migrates to its OWN <nested>/.cr
247
248
  const projectRows = result?.results;
248
249
  assert.equal(projectRows.length, 2, 'both the root and nested docs are candidates');
249
250
  assert.ok(projectRows.every((r) => r.status === 'imported'));
250
- const rootTarget = join(source, '.crouter', 'memory', 'AGENTS.md');
251
- const nestedTarget = join(nestedDir, '.crouter', 'memory', 'AGENTS.md');
252
- assert.equal(existsSync(rootTarget), true, 'root doc writes to the root\'s OWN .crouter/memory/AGENTS.md');
253
- assert.equal(existsSync(nestedTarget), true, 'nested doc writes to the NESTED dir\'s OWN .crouter/memory/AGENTS.md, not the root store');
251
+ const rootTarget = join(source, '.crouter', 'memory', 'INDEX.md');
252
+ const nestedTarget = join(nestedDir, '.crouter', 'memory', 'INDEX.md');
253
+ assert.equal(existsSync(rootTarget), true, 'root guide writes to the root\'s OWN .crouter/memory/INDEX.md');
254
+ assert.equal(existsSync(nestedTarget), true, 'nested guide writes to the NESTED dir\'s OWN .crouter/memory/INDEX.md, not the root store');
254
255
  const rootDoc = parseFrontmatterGeneric(readFileSync(rootTarget, 'utf8'));
255
256
  const nestedDoc = parseFrontmatterGeneric(readFileSync(nestedTarget, 'utf8'));
256
257
  assert.equal(rootDoc.data?.name, 'nested-demo');
@@ -292,7 +293,7 @@ test('a nested project-subdir doc migrated when --source POINTS AT THE NESTED DI
292
293
  // unmigrated, so the nested doc's name must stay qualified on its own.
293
294
  const nestedResult = await sysSyncProjectGuidanceLeaf.run({ source: nestedDir });
294
295
  assert.equal(nestedResult?.imported, 1);
295
- const nestedTarget = join(nestedDir, '.crouter', 'memory', 'AGENTS.md');
296
+ const nestedTarget = join(nestedDir, '.crouter', 'memory', 'INDEX.md');
296
297
  assert.equal(existsSync(nestedTarget), true);
297
298
  const nestedDoc = parseFrontmatterGeneric(readFileSync(nestedTarget, 'utf8'));
298
299
  assert.equal(nestedDoc.data?.name, 'demo-repo/packages/widgets', 'nested doc name is suffix-qualified even though --source pinned the nested dir directly');
@@ -307,7 +308,7 @@ test('falls back to the doc H1 title when the walk root is not inside a git repo
307
308
  writeFileSync(join(source, 'CLAUDE.md'), '# Standalone Docs Project\n\nDo the thing.\n', 'utf8');
308
309
  const result = await sysSyncProjectGuidanceLeaf.run({ source });
309
310
  assert.equal(result?.imported, 1);
310
- const target = join(source, '.crouter', 'memory', 'AGENTS.md');
311
+ const target = join(source, '.crouter', 'memory', 'INDEX.md');
311
312
  const doc = parseFrontmatterGeneric(readFileSync(target, 'utf8'));
312
313
  assert.equal(doc.data?.['when-and-why-to-read'], 'When working in standalone-docs-project, this knowledge should be read because it is the project\'s operating guide.');
313
314
  rmSync(source, { recursive: true, force: true });
@@ -52,6 +52,7 @@ export declare class SetupWizard implements Component, Focusable {
52
52
  private expandedPluginId;
53
53
  private confirming;
54
54
  private keyIndex;
55
+ private keyFieldIndex;
55
56
  private keyScroll;
56
57
  private keyFilter;
57
58
  private filteringKeys;
@@ -106,8 +107,10 @@ export declare class SetupWizard implements Component, Focusable {
106
107
  private buildSubmission;
107
108
  private finalize;
108
109
  private filteredKeybindings;
110
+ private selectedKeyRow;
109
111
  private selectedKeybinding;
110
112
  private moveKey;
113
+ private moveKeyField;
111
114
  private keyViewportLines;
112
115
  private keyPageSize;
113
116
  private beginKeyEdit;
@@ -129,7 +132,11 @@ export declare class SetupWizard implements Component, Focusable {
129
132
  private renderSystems;
130
133
  private renderExa;
131
134
  private renderConfirm;
132
- private keyRowHasMessage;
135
+ private keyRowMessage;
136
+ /** Number of lines needed to keep the active paired field legible. A narrow
137
+ * row stacks the action and field instead of sacrificing the action name,
138
+ * selected-field identity, or value to fixed columns. */
139
+ private keyRowBodyLineCount;
133
140
  private keyLineCost;
134
141
  private renderKeybindings;
135
142
  private renderModels;