@crouton-kit/crouter 0.3.21 → 0.3.23
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/commands/canvas-history/read.d.ts +1 -0
- package/dist/commands/canvas-history/read.js +61 -0
- package/dist/commands/canvas-history/search.d.ts +1 -0
- package/dist/commands/canvas-history/search.js +270 -0
- package/dist/commands/canvas-history/show.d.ts +1 -0
- package/dist/commands/canvas-history/show.js +79 -0
- package/dist/commands/canvas-history.d.ts +2 -0
- package/dist/commands/canvas-history.js +33 -0
- package/dist/commands/canvas.js +3 -2
- package/dist/commands/memory/find.js +7 -40
- package/dist/commands/memory/lint.js +19 -34
- package/dist/commands/memory/list.js +8 -18
- package/dist/commands/memory/read.js +4 -37
- package/dist/commands/pkg/bridge.d.ts +1 -0
- package/dist/commands/pkg/bridge.js +137 -0
- package/dist/commands/pkg/plugin-inspect.js +7 -6
- package/dist/commands/pkg.js +2 -1
- package/dist/commands/skill/author.js +35 -44
- package/dist/commands/skill/shared.d.ts +1 -5
- package/dist/commands/skill/shared.js +9 -63
- package/dist/commands/skill.js +5 -6
- package/dist/commands/sys/doctor.js +6 -144
- package/dist/core/__tests__/broker-dormant-wake.test.js +71 -34
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +21 -5
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +21 -5
- package/dist/core/__tests__/child-death-wake.test.js +133 -185
- package/dist/core/__tests__/daemon-liveness.test.js +28 -258
- package/dist/core/__tests__/flagship-lifecycle.test.js +156 -135
- package/dist/core/__tests__/frame-decoder-perf.test.js +51 -25
- package/dist/core/__tests__/{broker-attach-limits.test.js → full/broker-attach-limits.test.js} +62 -35
- package/dist/core/__tests__/{broker-attach-stream.test.js → full/broker-attach-stream.test.js} +54 -31
- package/dist/core/__tests__/full/broker-crash-teardown.test.js +143 -0
- package/dist/core/__tests__/full/broker-dialogs.test.js +153 -0
- package/dist/core/__tests__/full/broker-lifecycle.test.js +116 -0
- package/dist/core/__tests__/{cascade-close.test.js → full/cascade-close.test.js} +1 -1
- package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +292 -0
- package/dist/core/__tests__/{dead-pane-regression.test.js → full/dead-pane-regression.test.js} +2 -2
- package/dist/core/__tests__/{detach-focus.test.js → full/detach-focus.test.js} +5 -5
- package/dist/core/__tests__/{human-new-window-regression.test.js → full/human-new-window-regression.test.js} +2 -2
- package/dist/core/__tests__/{placement-focus.test.js → full/placement-focus.test.js} +5 -5
- package/dist/core/__tests__/{placement-reconcile.test.js → full/placement-reconcile.test.js} +3 -3
- package/dist/core/__tests__/{placement-revive.test.js → full/placement-revive.test.js} +5 -5
- package/dist/core/__tests__/{placement-teardown.test.js → full/placement-teardown.test.js} +4 -4
- package/dist/core/__tests__/{review-render-pane-regression.test.js → full/review-render-pane-regression.test.js} +2 -2
- package/dist/core/__tests__/full/spike-harness.test.js +117 -0
- package/dist/core/__tests__/grace-clock.test.js +72 -75
- package/dist/core/__tests__/helpers/harness.d.ts +35 -1
- package/dist/core/__tests__/helpers/harness.js +70 -12
- package/dist/core/__tests__/live-mutation-verbs.test.js +99 -104
- package/dist/core/__tests__/live-mutation.test.js +111 -134
- package/dist/core/__tests__/revive.test.js +96 -86
- package/dist/core/__tests__/subscription-delivery.test.js +116 -138
- package/dist/core/__tests__/wake-origin.test.js +54 -50
- package/dist/core/bridge-map.d.ts +19 -0
- package/dist/core/bridge-map.js +73 -0
- package/dist/core/canvas/history.d.ts +91 -0
- package/dist/core/canvas/history.js +389 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/config.js +3 -22
- package/dist/core/memory-resolver.d.ts +9 -2
- package/dist/core/memory-resolver.js +47 -7
- package/dist/core/resolver.d.ts +1 -30
- package/dist/core/resolver.js +6 -546
- package/dist/core/runtime/persona.js +5 -7
- package/dist/core/scope.d.ts +7 -5
- package/dist/core/scope.js +16 -17
- package/dist/core/substrate/ceiling.d.ts +28 -0
- package/dist/core/substrate/ceiling.js +87 -0
- package/dist/core/substrate/index.d.ts +2 -0
- package/dist/core/substrate/index.js +1 -0
- package/dist/core/substrate/on-read.js +18 -2
- package/dist/core/substrate/render.d.ts +2 -3
- package/dist/core/substrate/render.js +63 -51
- package/dist/pi-extensions/canvas-inbox-watcher.js +19 -2
- package/dist/types.d.ts +3 -18
- package/dist/types.js +0 -8
- package/package.json +4 -3
- package/dist/builtin-skills/.crouter-plugin/plugin.json +0 -5
- package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +0 -157
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +0 -106
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +0 -49
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +0 -49
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +0 -156
- package/dist/builtin-skills/skills/design/SKILL.md +0 -51
- package/dist/builtin-skills/skills/development/SKILL.md +0 -109
- package/dist/builtin-skills/skills/planning/SKILL.md +0 -59
- package/dist/builtin-skills/skills/spec/SKILL.md +0 -83
- package/dist/commands/skill/state.d.ts +0 -3
- package/dist/commands/skill/state.js +0 -71
- package/dist/core/__tests__/broker-crash-teardown.test.js +0 -116
- package/dist/core/__tests__/broker-dialogs.test.js +0 -126
- package/dist/core/__tests__/broker-lifecycle.test.js +0 -87
- package/dist/core/__tests__/resolver.test.js +0 -181
- package/dist/core/__tests__/spike-harness.test.js +0 -242
- /package/dist/core/__tests__/{broker-attach-limits.test.d.ts → full/broker-attach-limits.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-attach-stream.test.d.ts → full/broker-attach-stream.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-crash-teardown.test.d.ts → full/broker-crash-teardown.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-dialogs.test.d.ts → full/broker-dialogs.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-lifecycle.test.d.ts → full/broker-lifecycle.test.d.ts} +0 -0
- /package/dist/core/__tests__/{cascade-close.test.d.ts → full/cascade-close.test.d.ts} +0 -0
- /package/dist/core/__tests__/{dead-pane-regression.test.d.ts → full/daemon-liveness-pane.full.test.d.ts} +0 -0
- /package/dist/core/__tests__/{detach-focus.test.d.ts → full/dead-pane-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{human-new-window-regression.test.d.ts → full/detach-focus.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-focus.test.d.ts → full/human-new-window-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-reconcile.test.d.ts → full/placement-focus.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-revive.test.d.ts → full/placement-reconcile.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-teardown.test.d.ts → full/placement-revive.test.d.ts} +0 -0
- /package/dist/core/__tests__/{resolver.test.d.ts → full/placement-teardown.test.d.ts} +0 -0
- /package/dist/core/__tests__/{review-render-pane-regression.test.d.ts → full/review-render-pane-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{spike-harness.test.d.ts → full/spike-harness.test.d.ts} +0 -0
package/dist/core/scope.js
CHANGED
|
@@ -5,16 +5,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { CRTR_DIR_NAME } from '../types.js';
|
|
6
6
|
import { usage } from './errors.js';
|
|
7
7
|
let cachedProjectRoot;
|
|
8
|
-
export function builtinSkillsRoot() {
|
|
9
|
-
// Resolve relative to this file: src/core/scope.ts → src/builtin-skills/ OR dist/core/scope.js → dist/builtin-skills/
|
|
10
|
-
const thisFile = fileURLToPath(import.meta.url);
|
|
11
|
-
const coreDir = dirname(thisFile);
|
|
12
|
-
const pkgDir = dirname(coreDir); // src/ or dist/
|
|
13
|
-
return join(pkgDir, 'builtin-skills');
|
|
14
|
-
}
|
|
15
8
|
export function builtinViewsRoot() {
|
|
16
|
-
// Resolved relative to this file
|
|
17
|
-
//
|
|
9
|
+
// Resolved relative to this file: dist/core/scope.js → dist/builtin-views/
|
|
10
|
+
// (src/ at dev time).
|
|
18
11
|
const thisFile = fileURLToPath(import.meta.url);
|
|
19
12
|
const coreDir = dirname(thisFile);
|
|
20
13
|
const pkgDir = dirname(coreDir); // src/ or dist/
|
|
@@ -22,11 +15,10 @@ export function builtinViewsRoot() {
|
|
|
22
15
|
}
|
|
23
16
|
export function builtinMemoryRoot() {
|
|
24
17
|
// The substrate's shipped built-in documents live in their OWN package dir,
|
|
25
|
-
//
|
|
26
|
-
// builtinSkillsRoot/builtinViewsRoot: dist/core/scope.js → dist/builtin-memory/
|
|
18
|
+
// resolved relative to this file: dist/core/scope.js → dist/builtin-memory/
|
|
27
19
|
// (src/ at dev time). This is a HARD special case (design verdict m2): builtin
|
|
28
|
-
// memory is
|
|
29
|
-
//
|
|
20
|
+
// memory is the only content the builtin scope carries — the scope has no
|
|
21
|
+
// scopeRoot (scopeRoot('builtin') is null).
|
|
30
22
|
const thisFile = fileURLToPath(import.meta.url);
|
|
31
23
|
const coreDir = dirname(thisFile);
|
|
32
24
|
const pkgDir = dirname(coreDir); // src/ or dist/
|
|
@@ -65,8 +57,10 @@ export function projectScopeRoot(startDir) {
|
|
|
65
57
|
return findProjectScopeRoot(startDir);
|
|
66
58
|
}
|
|
67
59
|
export function scopeRoot(scope) {
|
|
60
|
+
// The builtin scope has no scope-root dir: its only content is memory
|
|
61
|
+
// (builtinMemoryRoot) and views (builtinViewsRoot), both hard special cases.
|
|
68
62
|
if (scope === 'builtin')
|
|
69
|
-
return
|
|
63
|
+
return null;
|
|
70
64
|
return scope === 'user' ? userScopeRoot() : projectScopeRoot();
|
|
71
65
|
}
|
|
72
66
|
export function requireScopeRoot(scope) {
|
|
@@ -98,15 +92,20 @@ export function scopeSkillsDir(scope) {
|
|
|
98
92
|
return root ? join(root, 'skills') : null;
|
|
99
93
|
}
|
|
100
94
|
/** Where substrate memory documents live per scope. Builtin memory is the
|
|
101
|
-
* special case — its own package dir (builtinMemoryRoot,
|
|
102
|
-
*
|
|
103
|
-
*
|
|
95
|
+
* special case — its own package dir (builtinMemoryRoot), since the builtin
|
|
96
|
+
* scope has no scopeRoot — mirroring viewsDir's builtin handling. User and
|
|
97
|
+
* project memory live at scopeRoot(scope)/memory. */
|
|
104
98
|
export function scopeMemoryDir(scope) {
|
|
105
99
|
if (scope === 'builtin')
|
|
106
100
|
return builtinMemoryRoot();
|
|
107
101
|
const root = scope === 'user' ? userScopeRoot() : projectScopeRoot();
|
|
108
102
|
return root ? join(root, 'memory') : null;
|
|
109
103
|
}
|
|
104
|
+
/** Where a plugin's substrate memory documents live: `<plugin.root>/memory`.
|
|
105
|
+
* The resolver mounts these under the virtual `<pluginName>/` namespace. */
|
|
106
|
+
export function pluginMemoryDir(plugin) {
|
|
107
|
+
return join(plugin.root, 'memory');
|
|
108
|
+
}
|
|
110
109
|
/** Where view definition dirs live per scope. Builtin views sit directly under
|
|
111
110
|
* builtinViewsRoot() (no `views/` segment — they ARE the builtin views dir),
|
|
112
111
|
* matching the loader's `<root>/<name>/view.mjs` resolution. */
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Rung, type SubstrateDoc } from './schema.js';
|
|
2
|
+
/** The reserved basename (no extension) that marks a directory's index doc. */
|
|
3
|
+
export declare const INDEX_NAME = "INDEX";
|
|
4
|
+
/** Does this path-derived name denote a directory INDEX (`taste/INDEX`, or a
|
|
5
|
+
* top-level `INDEX`)? */
|
|
6
|
+
export declare function isIndexName(name: string): boolean;
|
|
7
|
+
/** The directory an INDEX doc represents: `taste/INDEX` → `taste`, `INDEX` → ''. */
|
|
8
|
+
export declare function indexDirOf(name: string): string;
|
|
9
|
+
/** The name a doc renders under in agent-facing surfaces: an INDEX doc renders
|
|
10
|
+
* under its directory name (the dir entry); every other doc keeps its name. */
|
|
11
|
+
export declare function displayName(name: string): string;
|
|
12
|
+
/** The two visibility surfaces an INDEX can cap. */
|
|
13
|
+
export type Surface = 'systemPromptVisibility' | 'fileReadVisibility';
|
|
14
|
+
/** Build the dir → governing-INDEX map. Precedence is first-occurrence-wins, so
|
|
15
|
+
* callers MUST pass docs precedence-ordered (project > user > builtin) — the
|
|
16
|
+
* highest-precedence INDEX governs a dir, mirroring the memory resolver. */
|
|
17
|
+
export declare function buildCeilingIndex(docs: SubstrateDoc[]): Map<string, SubstrateDoc>;
|
|
18
|
+
/** A doc's effective rung on `surface` after applying every ancestor INDEX
|
|
19
|
+
* ceiling: min(own rung, each ancestor INDEX's rung). A `none` ancestor INDEX
|
|
20
|
+
* hides the whole subtree (min with none = none). */
|
|
21
|
+
export declare function effectiveRung(doc: SubstrateDoc, ceil: Map<string, SubstrateDoc>, surface: Surface): Rung;
|
|
22
|
+
/** Apply ceilings to a doc list for a surface: each doc gets its effective rung
|
|
23
|
+
* on `surface` written back into that field, and INDEX docs are renamed to
|
|
24
|
+
* their dir entry. Returns NEW objects only where something changed (cheap,
|
|
25
|
+
* preserves identity for untouched docs). Does NOT drop `none`-rung docs — the
|
|
26
|
+
* caller filters per surface (boot drops them; node-local intentionally keeps
|
|
27
|
+
* them). */
|
|
28
|
+
export declare function applyCeilings(docs: SubstrateDoc[], surface: Surface): SubstrateDoc[];
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// ceiling.ts — INDEX.md dir-level entries and the subtree rung ceiling.
|
|
2
|
+
//
|
|
3
|
+
// A directory under a memory store may carry an `INDEX.md` with the SAME
|
|
4
|
+
// substrate frontmatter as any doc. That file makes the directory a first-class
|
|
5
|
+
// substrate entry: it renders as ONE entry at dir level (headed by the dir
|
|
6
|
+
// name, not `…/INDEX`), and its rung is a CEILING for everything in its subtree.
|
|
7
|
+
//
|
|
8
|
+
// effective rung of a descendant = min(own rung, every ancestor INDEX's rung)
|
|
9
|
+
//
|
|
10
|
+
// So a dir's INDEX at `preview` lets its files surface at most at `preview`; an
|
|
11
|
+
// INDEX at `none` hides the whole subtree. This is the single, explicit
|
|
12
|
+
// mechanism for collapsing a directory in agent-facing surfaces (boot render +
|
|
13
|
+
// on-read) — there is no parallel auto-hiding heuristic. See design taste:
|
|
14
|
+
// `crtr memory read taste/document-substrate`.
|
|
15
|
+
import { rungRank } from './schema.js';
|
|
16
|
+
/** The reserved basename (no extension) that marks a directory's index doc. */
|
|
17
|
+
export const INDEX_NAME = 'INDEX';
|
|
18
|
+
/** Does this path-derived name denote a directory INDEX (`taste/INDEX`, or a
|
|
19
|
+
* top-level `INDEX`)? */
|
|
20
|
+
export function isIndexName(name) {
|
|
21
|
+
return name === INDEX_NAME || name.endsWith('/' + INDEX_NAME);
|
|
22
|
+
}
|
|
23
|
+
/** The directory an INDEX doc represents: `taste/INDEX` → `taste`, `INDEX` → ''. */
|
|
24
|
+
export function indexDirOf(name) {
|
|
25
|
+
return name === INDEX_NAME ? '' : name.slice(0, name.length - INDEX_NAME.length - 1);
|
|
26
|
+
}
|
|
27
|
+
/** The name a doc renders under in agent-facing surfaces: an INDEX doc renders
|
|
28
|
+
* under its directory name (the dir entry); every other doc keeps its name. */
|
|
29
|
+
export function displayName(name) {
|
|
30
|
+
return isIndexName(name) ? indexDirOf(name) : name;
|
|
31
|
+
}
|
|
32
|
+
/** Build the dir → governing-INDEX map. Precedence is first-occurrence-wins, so
|
|
33
|
+
* callers MUST pass docs precedence-ordered (project > user > builtin) — the
|
|
34
|
+
* highest-precedence INDEX governs a dir, mirroring the memory resolver. */
|
|
35
|
+
export function buildCeilingIndex(docs) {
|
|
36
|
+
const m = new Map();
|
|
37
|
+
for (const d of docs) {
|
|
38
|
+
if (!isIndexName(d.name))
|
|
39
|
+
continue;
|
|
40
|
+
const dir = indexDirOf(d.name);
|
|
41
|
+
if (!m.has(dir))
|
|
42
|
+
m.set(dir, d);
|
|
43
|
+
}
|
|
44
|
+
return m;
|
|
45
|
+
}
|
|
46
|
+
function minRung(a, b) {
|
|
47
|
+
return rungRank(a) <= rungRank(b) ? a : b;
|
|
48
|
+
}
|
|
49
|
+
/** Ancestor directory names of a doc, nearest-last: `a/b/c` → [`a/b`, `a`];
|
|
50
|
+
* `a` → []. An INDEX doc's OWN dir is included (a self-cap is a no-op), so a
|
|
51
|
+
* nested INDEX still inherits its parents' ceilings. */
|
|
52
|
+
function ancestorDirs(name) {
|
|
53
|
+
const parts = name.split('/');
|
|
54
|
+
parts.pop(); // drop the leaf
|
|
55
|
+
const out = [];
|
|
56
|
+
for (let i = parts.length; i > 0; i--)
|
|
57
|
+
out.push(parts.slice(0, i).join('/'));
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/** A doc's effective rung on `surface` after applying every ancestor INDEX
|
|
61
|
+
* ceiling: min(own rung, each ancestor INDEX's rung). A `none` ancestor INDEX
|
|
62
|
+
* hides the whole subtree (min with none = none). */
|
|
63
|
+
export function effectiveRung(doc, ceil, surface) {
|
|
64
|
+
let rung = doc[surface];
|
|
65
|
+
for (const dir of ancestorDirs(doc.name)) {
|
|
66
|
+
const idx = ceil.get(dir);
|
|
67
|
+
if (idx)
|
|
68
|
+
rung = minRung(rung, idx[surface]);
|
|
69
|
+
}
|
|
70
|
+
return rung;
|
|
71
|
+
}
|
|
72
|
+
/** Apply ceilings to a doc list for a surface: each doc gets its effective rung
|
|
73
|
+
* on `surface` written back into that field, and INDEX docs are renamed to
|
|
74
|
+
* their dir entry. Returns NEW objects only where something changed (cheap,
|
|
75
|
+
* preserves identity for untouched docs). Does NOT drop `none`-rung docs — the
|
|
76
|
+
* caller filters per surface (boot drops them; node-local intentionally keeps
|
|
77
|
+
* them). */
|
|
78
|
+
export function applyCeilings(docs, surface) {
|
|
79
|
+
const ceil = buildCeilingIndex(docs);
|
|
80
|
+
return docs.map((d) => {
|
|
81
|
+
const rung = effectiveRung(d, ceil, surface);
|
|
82
|
+
const name = displayName(d.name);
|
|
83
|
+
if (rung === d[surface] && name === d.name)
|
|
84
|
+
return d;
|
|
85
|
+
return { ...d, [surface]: rung, name };
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -3,5 +3,7 @@ export type { DocKind, Rung, GatePredicate, SubstrateSchema, SubstrateDoc } from
|
|
|
3
3
|
export { scopeForCwd, spineDepth, assembleNodeSubject } from './subject.js';
|
|
4
4
|
export type { NodeConfigSubject } from './subject.js';
|
|
5
5
|
export { gatePasses } from './gate.js';
|
|
6
|
+
export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveRung, applyCeilings, } from './ceiling.js';
|
|
7
|
+
export type { Surface } from './ceiling.js';
|
|
6
8
|
export { renderSkillsSection, renderPreferencesSection, renderReferencesBlock } from './render.js';
|
|
7
9
|
export { renderOnReadDocs } from './on-read.js';
|
|
@@ -14,5 +14,6 @@ KIND_DEFAULT_RUNGS,
|
|
|
14
14
|
parseSubstrateFrontmatter, parseSubstrateDoc, previewLine, } from './schema.js';
|
|
15
15
|
export { scopeForCwd, spineDepth, assembleNodeSubject } from './subject.js';
|
|
16
16
|
export { gatePasses } from './gate.js';
|
|
17
|
+
export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveRung, applyCeilings, } from './ceiling.js';
|
|
17
18
|
export { renderSkillsSection, renderPreferencesSection, renderReferencesBlock } from './render.js';
|
|
18
19
|
export { renderOnReadDocs } from './on-read.js';
|
|
@@ -36,7 +36,7 @@ import { CRTR_DIR_NAME } from '../../types.js';
|
|
|
36
36
|
import { pathExists, readText, walkFiles } from '../fs-utils.js';
|
|
37
37
|
import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
38
38
|
import { listAllMemoryDocs } from '../memory-resolver.js';
|
|
39
|
-
import { assembleNodeSubject, gatePasses, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, } from './index.js';
|
|
39
|
+
import { assembleNodeSubject, buildCeilingIndex, displayName, effectiveRung, gatePasses, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, } from './index.js';
|
|
40
40
|
import { cachedSubstrateDocs } from './session-cache.js';
|
|
41
41
|
// Ancestor dirs we never look inside for a `.crouter/memory/` store (the read
|
|
42
42
|
// file may live under a build/dependency tree; `.crouter` is NOT junk here — it
|
|
@@ -264,6 +264,18 @@ export function renderOnReadDocs(nodeId, readFilePath, seen = new Set()) {
|
|
|
264
264
|
// Outermost-first: the nearest/most-specific doc reads last — closest to the
|
|
265
265
|
// file content that follows it (the applies-to set, order -1, trails).
|
|
266
266
|
candidates.sort((a, b) => b.order - a.order);
|
|
267
|
+
// INDEX ceiling: build the dir → governing-INDEX map over the whole resolved
|
|
268
|
+
// corpus PLUS the candidate docs, so a dir's INDEX caps (or `none`-hides) its
|
|
269
|
+
// subtree on-read exactly as it does at boot. The candidate's own
|
|
270
|
+
// fileReadVisibility and name are overridden by the effective rung / dir entry.
|
|
271
|
+
let ceil;
|
|
272
|
+
try {
|
|
273
|
+
const resolved = cachedSubstrateDocs(listAllMemoryDocs, parseSubstrateDoc);
|
|
274
|
+
ceil = buildCeilingIndex([...resolved, ...candidates.map((c) => c.doc)]);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
ceil = buildCeilingIndex(candidates.map((c) => c.doc));
|
|
278
|
+
}
|
|
267
279
|
const rendered = [];
|
|
268
280
|
for (const c of candidates) {
|
|
269
281
|
// Never re-surface the doc the agent is literally reading.
|
|
@@ -276,7 +288,11 @@ export function renderOnReadDocs(nodeId, readFilePath, seen = new Set()) {
|
|
|
276
288
|
try {
|
|
277
289
|
if (!gatePasses(c.doc, subject))
|
|
278
290
|
continue; // gated out for this node
|
|
279
|
-
|
|
291
|
+
const rung = effectiveRung(c.doc, ceil, 'fileReadVisibility');
|
|
292
|
+
const doc = rung === c.doc.fileReadVisibility && displayName(c.doc.name) === c.doc.name
|
|
293
|
+
? c.doc
|
|
294
|
+
: { ...c.doc, fileReadVisibility: rung, name: displayName(c.doc.name) };
|
|
295
|
+
block = renderDocEnvelope(doc, absReadFile);
|
|
280
296
|
}
|
|
281
297
|
catch {
|
|
282
298
|
continue; // a single bad doc never breaks the read
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/** The `## Skills` system-prompt section: every eligible `kind: skill` doc,
|
|
2
2
|
* rendered at its `system-prompt-visibility`. `name`-rung skills collapse into
|
|
3
|
-
* one compact catalog
|
|
4
|
-
*
|
|
5
|
-
* Returns '' when nothing is eligible. */
|
|
3
|
+
* one compact, plugin-grouped catalog; `preview`/`content`-rung skills each get
|
|
4
|
+
* a `###` sub-section. Returns '' when nothing is eligible. */
|
|
6
5
|
export declare function renderSkillsSection(nodeId: string): string;
|
|
7
6
|
/** The `## Preferences` system-prompt section: every eligible `kind: preference`
|
|
8
7
|
* doc as its own `###` sub-section, at its `system-prompt-visibility` (the
|
|
@@ -18,14 +18,13 @@
|
|
|
18
18
|
// null for a non-substrate doc and is `.filter()`ed out; per-file loads are
|
|
19
19
|
// wrapped so one bad file is skipped, not fatal.
|
|
20
20
|
import { relative, sep } from 'node:path';
|
|
21
|
-
import {
|
|
22
|
-
import { listAllPlugins, listAllSkills } from '../resolver.js';
|
|
21
|
+
import { listAllPlugins } from '../resolver.js';
|
|
23
22
|
import { renderCatalogSection } from '../../commands/skill/shared.js';
|
|
24
23
|
import { listAllMemoryDocs } from '../memory-resolver.js';
|
|
25
24
|
import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
26
25
|
import { pathExists, readText, walkFiles } from '../fs-utils.js';
|
|
27
26
|
import { memoryDir } from '../runtime/memory.js';
|
|
28
|
-
import { assembleNodeSubject, gatePasses, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, } from './index.js';
|
|
27
|
+
import { applyCeilings, assembleNodeSubject, gatePasses, isIndexName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, } from './index.js';
|
|
29
28
|
import { cachedAllPlugins, cachedNodeSubject, cachedSubstrateDocs, } from './session-cache.js';
|
|
30
29
|
// ---------------------------------------------------------------------------
|
|
31
30
|
// The shared per-doc pipeline.
|
|
@@ -35,7 +34,12 @@ import { cachedAllPlugins, cachedNodeSubject, cachedSubstrateDocs, } from './ses
|
|
|
35
34
|
* system-prompt rung above `none`. Resolver = user + project + builtin scopes
|
|
36
35
|
* (precedence-ordered); node-local is loaded separately (see nodeLocalDocs).
|
|
37
36
|
* Uses the per-session cache so the full corpus is scanned+parsed at most once
|
|
38
|
-
* per session across the three boot-render calls.
|
|
37
|
+
* per session across the three boot-render calls.
|
|
38
|
+
*
|
|
39
|
+
* Ceilings are applied over the WHOLE corpus (cross-kind) BEFORE the kind
|
|
40
|
+
* filter: an INDEX.md renders as its dir entry (`taste`, not `taste/INDEX`) at
|
|
41
|
+
* its own rung, and every descendant's rung is capped by its ancestor INDEX
|
|
42
|
+
* rungs — a `none` dir hides its whole subtree. */
|
|
39
43
|
function resolverDocs(subject, kind) {
|
|
40
44
|
let docs;
|
|
41
45
|
try {
|
|
@@ -44,10 +48,17 @@ function resolverDocs(subject, kind) {
|
|
|
44
48
|
catch {
|
|
45
49
|
return [];
|
|
46
50
|
}
|
|
47
|
-
return docs
|
|
51
|
+
return applyCeilings(docs, 'systemPromptVisibility')
|
|
48
52
|
.filter((d) => d.kind === kind)
|
|
49
53
|
.filter((d) => gatePasses(d, subject))
|
|
50
|
-
.filter((d) => d.systemPromptVisibility !== 'none')
|
|
54
|
+
.filter((d) => d.systemPromptVisibility !== 'none')
|
|
55
|
+
// Re-sort after the INDEX rename (taste/INDEX → taste) so a dir entry sorts
|
|
56
|
+
// immediately BEFORE its children, keeping scope precedence (project > user
|
|
57
|
+
// > builtin) intact.
|
|
58
|
+
.sort((a, b) => scopeRank(a.scope) - scopeRank(b.scope) || a.name.localeCompare(b.name));
|
|
59
|
+
}
|
|
60
|
+
function scopeRank(scope) {
|
|
61
|
+
return scope === 'project' ? 0 : scope === 'user' ? 1 : 2;
|
|
51
62
|
}
|
|
52
63
|
/** The node-local memory docs eligible at boot. Node-local memory lives in the
|
|
53
64
|
* canvas home (`nodes/<id>/context/memory/`) and is OUTSIDE the scope resolver,
|
|
@@ -114,50 +125,49 @@ function renderSubSection(d) {
|
|
|
114
125
|
// ---------------------------------------------------------------------------
|
|
115
126
|
// 1. Skills section — `## Skills` (system prompt).
|
|
116
127
|
// ---------------------------------------------------------------------------
|
|
117
|
-
/** The compact, group-collapsed `name`-rung catalog
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* is
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
pluginSkills = listAllSkills().filter((s) => s.enabled && s.plugin !== SCOPE_SKILL_PLUGIN && s.scope !== 'builtin');
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
pluginSkills = [];
|
|
132
|
-
}
|
|
133
|
-
const entries = [
|
|
134
|
-
...pluginSkills.map((s) => ({ scope: s.scope, plugin: s.plugin, name: s.name })),
|
|
135
|
-
...nameRungSkillDocs.map((d) => ({ scope: d.scope, plugin: SCOPE_SKILL_PLUGIN, name: d.name })),
|
|
136
|
-
];
|
|
137
|
-
if (entries.length === 0)
|
|
138
|
-
return '';
|
|
139
|
-
// Group by scope+plugin → forest-root names (drop nested children) so each
|
|
140
|
-
// source contributes only its top-level skills.
|
|
128
|
+
/** The compact, group-collapsed `name`-rung catalog of substrate `skill` docs.
|
|
129
|
+
* Every leaf is a migrated/generated substrate doc (native, builtin, or a
|
|
130
|
+
* plugin's `<pluginName>/` subtree) — there is no second, resolver-provided
|
|
131
|
+
* skill corpus. Each doc self-groups by its top-dir segment: a name with a
|
|
132
|
+
* slash sources to its top segment (the plugin name); a bare name sources to
|
|
133
|
+
* '' (a scope-local native/builtin skill). A plugin whose INDEX is elevated to
|
|
134
|
+
* preview/content renders as its own `### <plugin>` subsection, so its catalog
|
|
135
|
+
* group is dropped here (via `elevatedSources`) to avoid a double render.
|
|
136
|
+
* Reuses skill/shared.ts's renderCatalogSection group-collapse. Returns ''
|
|
137
|
+
* when nothing is in the catalog. */
|
|
138
|
+
function renderSkillCatalog(nameRungSkillDocs, elevatedSources) {
|
|
141
139
|
const bySource = new Map();
|
|
142
|
-
for (const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
|
|
140
|
+
for (const d of nameRungSkillDocs) {
|
|
141
|
+
// INDEX docs are structural ceilings (already renamed to their dir entry by
|
|
142
|
+
// applyCeilings), never catalog leaves — drop defensively.
|
|
143
|
+
if (isIndexName(d.name))
|
|
144
|
+
continue;
|
|
145
|
+
const slash = d.name.indexOf('/');
|
|
146
|
+
const plugin = slash === -1 ? '' : d.name.slice(0, slash);
|
|
147
|
+
const leaf = slash === -1 ? d.name : d.name.slice(slash + 1);
|
|
148
|
+
// A plugin represented by its own elevated ### subsection is dropped from the
|
|
149
|
+
// catalog so it is not rendered twice.
|
|
150
|
+
if (plugin !== '' && elevatedSources.has(plugin))
|
|
151
|
+
continue;
|
|
152
|
+
const key = `${d.scope}\t${plugin}`;
|
|
153
|
+
const src = bySource.get(key);
|
|
154
|
+
if (src)
|
|
155
|
+
src.roots.push(leaf);
|
|
147
156
|
else
|
|
148
|
-
bySource.set(key, [
|
|
157
|
+
bySource.set(key, { scope: d.scope, plugin, roots: [leaf] });
|
|
149
158
|
}
|
|
159
|
+
if (bySource.size === 0)
|
|
160
|
+
return '';
|
|
150
161
|
const projectSources = [];
|
|
151
162
|
const userSources = [];
|
|
152
|
-
for (const
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
.filter((n) => !names.some((m) => m !== n && n.startsWith(m + '/')))
|
|
163
|
+
for (const { scope, plugin, roots } of bySource.values()) {
|
|
164
|
+
// Drop nested children so each source contributes only its top-level skills.
|
|
165
|
+
const top = roots
|
|
166
|
+
.filter((n) => !roots.some((m) => m !== n && n.startsWith(m + '/')))
|
|
157
167
|
.sort();
|
|
158
|
-
if (
|
|
168
|
+
if (top.length === 0)
|
|
159
169
|
continue;
|
|
160
|
-
(scope === 'project' ? projectSources : userSources).push({ plugin, roots });
|
|
170
|
+
(scope === 'project' ? projectSources : userSources).push({ plugin, roots: top });
|
|
161
171
|
}
|
|
162
172
|
const descriptions = new Map();
|
|
163
173
|
try {
|
|
@@ -180,19 +190,21 @@ function renderSkillCatalog(nameRungSkillDocs) {
|
|
|
180
190
|
}
|
|
181
191
|
/** The `## Skills` system-prompt section: every eligible `kind: skill` doc,
|
|
182
192
|
* rendered at its `system-prompt-visibility`. `name`-rung skills collapse into
|
|
183
|
-
* one compact catalog
|
|
184
|
-
*
|
|
185
|
-
* Returns '' when nothing is eligible. */
|
|
193
|
+
* one compact, plugin-grouped catalog; `preview`/`content`-rung skills each get
|
|
194
|
+
* a `###` sub-section. Returns '' when nothing is eligible. */
|
|
186
195
|
export function renderSkillsSection(nodeId) {
|
|
187
196
|
const subject = cachedNodeSubject(nodeId, assembleNodeSubject);
|
|
188
197
|
if (subject === null)
|
|
189
198
|
return '';
|
|
190
199
|
const skills = resolverDocs(subject, 'skill');
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
200
|
+
const elevated = skills.filter((d) => d.systemPromptVisibility !== 'name');
|
|
201
|
+
// A plugin whose INDEX is elevated surfaces as its own `### <plugin>`
|
|
202
|
+
// subsection (its display name is the plugin/dir name after ceiling rename);
|
|
203
|
+
// the catalog drops that plugin's group so it is not rendered twice.
|
|
204
|
+
const elevatedSources = new Set(elevated.map((d) => d.name));
|
|
205
|
+
const catalog = renderSkillCatalog(skills.filter((d) => d.systemPromptVisibility === 'name'), elevatedSources);
|
|
206
|
+
const elevatedBlocks = elevated.map(renderSubSection);
|
|
207
|
+
const blocks = [catalog, ...elevatedBlocks].filter((s) => s !== '');
|
|
196
208
|
if (blocks.length === 0)
|
|
197
209
|
return '';
|
|
198
210
|
return `## Skills\n\n${blocks.join('\n\n')}`;
|
|
@@ -43,8 +43,24 @@ import { getNode } from '../core/canvas/index.js';
|
|
|
43
43
|
// one watcher is live. Pattern copied verbatim from agent-inbox-watcher.ts.
|
|
44
44
|
// ---------------------------------------------------------------------------
|
|
45
45
|
let liveTimer;
|
|
46
|
-
const
|
|
47
|
-
const
|
|
46
|
+
const DEFAULT_TICK_MS = 800; // polling cadence
|
|
47
|
+
const DEFAULT_DEBOUNCE_MS = 1000; // flush once the burst has been quiet for this long
|
|
48
|
+
// Testability seam: a test can inject a much shorter cadence so it doesn't have
|
|
49
|
+
// to sleep against the real ~800ms/1000ms wall-clock timing. Read per-registration
|
|
50
|
+
// (not as an import-time module const) so each registerCanvasInboxWatcher() call
|
|
51
|
+
// picks up the env current at that moment. Unset → production defaults.
|
|
52
|
+
function resolveCadence() {
|
|
53
|
+
const parse = (raw, fallback) => {
|
|
54
|
+
if (raw === undefined)
|
|
55
|
+
return fallback;
|
|
56
|
+
const n = Number(raw);
|
|
57
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
tickMs: parse(process.env['CRTR_WATCHER_TICK_MS'], DEFAULT_TICK_MS),
|
|
61
|
+
debounceMs: parse(process.env['CRTR_WATCHER_DEBOUNCE_MS'], DEFAULT_DEBOUNCE_MS),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
48
64
|
// ---------------------------------------------------------------------------
|
|
49
65
|
// Extension
|
|
50
66
|
// ---------------------------------------------------------------------------
|
|
@@ -56,6 +72,7 @@ const DEBOUNCE_MS = 1000; // flush once the burst has been quiet for this long
|
|
|
56
72
|
* the module-level liveTimer guard is the actual stacking prevention.
|
|
57
73
|
*/
|
|
58
74
|
export function registerCanvasInboxWatcher(pi) {
|
|
75
|
+
const { tickMs: TICK_MS, debounceMs: DEBOUNCE_MS } = resolveCadence();
|
|
59
76
|
// Capture the latest event context so isIdle() is readable inside the timer
|
|
60
77
|
// callback, which has no ctx of its own.
|
|
61
78
|
let lastCtx;
|
package/dist/types.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface PluginManifest {
|
|
|
20
20
|
source?: string;
|
|
21
21
|
owner?: OwnerRef;
|
|
22
22
|
kinds?: string[];
|
|
23
|
+
generator?: 'claude-bridge';
|
|
23
24
|
}
|
|
24
25
|
export interface MarketplacePluginEntry {
|
|
25
26
|
name: string;
|
|
@@ -44,9 +45,6 @@ export interface ConfigPluginEntry {
|
|
|
44
45
|
source_marketplace?: string;
|
|
45
46
|
version?: string;
|
|
46
47
|
}
|
|
47
|
-
export interface ConfigSkillEntry {
|
|
48
|
-
enabled: boolean;
|
|
49
|
-
}
|
|
50
48
|
export type AutoUpdateMode = 'notify' | 'apply' | false;
|
|
51
49
|
export interface AutoUpdateConfig {
|
|
52
50
|
crtr: AutoUpdateMode;
|
|
@@ -79,7 +77,6 @@ export interface ScopeConfig {
|
|
|
79
77
|
schema_version: number;
|
|
80
78
|
marketplaces: Record<string, ConfigMarketplaceEntry>;
|
|
81
79
|
plugins: Record<string, ConfigPluginEntry>;
|
|
82
|
-
skills: Record<string, ConfigSkillEntry>;
|
|
83
80
|
auto_update: AutoUpdateConfig;
|
|
84
81
|
max_panes_per_window: number;
|
|
85
82
|
canvasNav: CanvasNavConfig;
|
|
@@ -107,16 +104,6 @@ export interface SkillFrontmatter {
|
|
|
107
104
|
keywords?: string[];
|
|
108
105
|
type?: SkillType;
|
|
109
106
|
}
|
|
110
|
-
export interface Skill {
|
|
111
|
-
name: string;
|
|
112
|
-
plugin: string;
|
|
113
|
-
scope: Scope;
|
|
114
|
-
path: string;
|
|
115
|
-
pluginRoot: string;
|
|
116
|
-
frontmatter: SkillFrontmatter;
|
|
117
|
-
enabled: boolean;
|
|
118
|
-
disabledIn?: Scope;
|
|
119
|
-
}
|
|
120
107
|
export interface SubagentFrontmatter {
|
|
121
108
|
name: string;
|
|
122
109
|
description?: string;
|
|
@@ -127,8 +114,8 @@ export interface SubagentFrontmatter {
|
|
|
127
114
|
}
|
|
128
115
|
export interface Subagent {
|
|
129
116
|
name: string;
|
|
130
|
-
/** Plugin the subagent belongs to, or
|
|
131
|
-
*
|
|
117
|
+
/** Plugin the subagent belongs to, or the sentinel '_' for a scope-root
|
|
118
|
+
* agent stored at `<scope-root>/agents/<name>.md`. */
|
|
132
119
|
plugin: string;
|
|
133
120
|
scope: Scope;
|
|
134
121
|
/** Absolute path to the agent's .md file. */
|
|
@@ -165,7 +152,6 @@ export declare const STATE_FILE = "state.json";
|
|
|
165
152
|
export declare const SKILL_ENTRY_FILE = "SKILL.md";
|
|
166
153
|
export declare const SKILLS_DIR = "skills";
|
|
167
154
|
export declare const AGENTS_DIR = "agents";
|
|
168
|
-
export declare const SCOPE_SKILL_PLUGIN = "_";
|
|
169
155
|
export declare const DEFAULT_MAX_PANES_PER_WINDOW = 3;
|
|
170
156
|
export declare function defaultScopeConfig(): ScopeConfig;
|
|
171
157
|
/** Built-in canvas-nav binds so an absent config still gives the intended UX.
|
|
@@ -173,5 +159,4 @@ export declare function defaultScopeConfig(): ScopeConfig;
|
|
|
173
159
|
* 1..9 (focus report N) are generated by the menu layer, not literal entries.
|
|
174
160
|
* graphBinds: empty — built-in GRAPH keys are reserved, custom keys add on. */
|
|
175
161
|
export declare function defaultCanvasNavConfig(): CanvasNavConfig;
|
|
176
|
-
export declare function skillConfigKey(plugin: string, name: string): string;
|
|
177
162
|
export declare function defaultScopeState(): ScopeState;
|
package/dist/types.js
CHANGED
|
@@ -23,17 +23,12 @@ export const SKILLS_DIR = 'skills';
|
|
|
23
23
|
// Subagent definitions live as flat `<name>.md` files under `<root>/agents/`,
|
|
24
24
|
// for both scope roots and plugins. Mirrors SKILLS_DIR.
|
|
25
25
|
export const AGENTS_DIR = 'agents';
|
|
26
|
-
// Sentinel plugin name for skills that live at a scope root (no plugin wrapper).
|
|
27
|
-
// Stored as `<scope-root>/skills/<name>/SKILL.md`. Shown in listings without the
|
|
28
|
-
// `_/` prefix.
|
|
29
|
-
export const SCOPE_SKILL_PLUGIN = '_';
|
|
30
26
|
export const DEFAULT_MAX_PANES_PER_WINDOW = 3;
|
|
31
27
|
export function defaultScopeConfig() {
|
|
32
28
|
return {
|
|
33
29
|
schema_version: SCHEMA_VERSION,
|
|
34
30
|
marketplaces: {},
|
|
35
31
|
plugins: {},
|
|
36
|
-
skills: {},
|
|
37
32
|
auto_update: { crtr: 'notify', content: 'notify', interval_hours: 24 },
|
|
38
33
|
max_panes_per_window: DEFAULT_MAX_PANES_PER_WINDOW,
|
|
39
34
|
canvasNav: defaultCanvasNavConfig(),
|
|
@@ -55,9 +50,6 @@ export function defaultCanvasNavConfig() {
|
|
|
55
50
|
graphBinds: {},
|
|
56
51
|
};
|
|
57
52
|
}
|
|
58
|
-
export function skillConfigKey(plugin, name) {
|
|
59
|
-
return `${plugin}/${name}`;
|
|
60
|
-
}
|
|
61
53
|
export function defaultScopeState() {
|
|
62
54
|
return { marketplaces: {}, plugins: {} };
|
|
63
55
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crouton-kit/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.23",
|
|
4
4
|
"description": "crtr — fast access to skills, plugins, and marketplaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,11 +24,12 @@
|
|
|
24
24
|
"bin"
|
|
25
25
|
],
|
|
26
26
|
"scripts": {
|
|
27
|
-
"build": "rm -rf dist && tsc && cp -R src/builtin-
|
|
27
|
+
"build": "rm -rf dist && tsc && cp -R src/builtin-personas dist/builtin-personas && cp -R src/builtin-views dist/builtin-views && cp -R src/builtin-memory dist/builtin-memory",
|
|
28
28
|
"dev": "tsx src/cli.ts",
|
|
29
29
|
"link": "npm link",
|
|
30
30
|
"prepublishOnly": "npm run build",
|
|
31
|
-
"test": "node --import tsx/esm --test 'src/**/__tests__
|
|
31
|
+
"test": "node --import tsx/esm --test 'src/**/__tests__/*.test.ts'",
|
|
32
|
+
"test:full": "node --import tsx/esm --test 'src/**/__tests__/**/*.test.ts'"
|
|
32
33
|
},
|
|
33
34
|
"repository": {
|
|
34
35
|
"type": "git",
|