@crouton-kit/crouter 0.3.20 → 0.3.22
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/crouter-development/marketplaces.md +2 -4
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +3 -5
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +3 -6
- package/dist/builtin-memory/crouter-development/personas.md +2 -6
- package/dist/builtin-memory/crouter-development/plugins.md +2 -4
- package/dist/builtin-memory/design.md +2 -4
- package/dist/builtin-memory/development.md +3 -3
- package/dist/builtin-memory/planning.md +3 -4
- package/dist/builtin-memory/spec.md +3 -10
- package/dist/builtin-personas/orchestration-kernel.md +1 -1
- 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/human/queue.js +2 -2
- package/dist/commands/memory/__tests__/lint-schema.test.js +29 -0
- package/dist/commands/memory/find.js +14 -52
- package/dist/commands/memory/lint.d.ts +7 -0
- package/dist/commands/memory/lint.js +29 -35
- package/dist/commands/memory/list.js +8 -18
- package/dist/commands/memory/read.js +4 -37
- package/dist/commands/memory/shared.js +1 -2
- package/dist/commands/memory/write.js +6 -9
- package/dist/commands/node.js +47 -25
- 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/push.js +10 -15
- 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/commands/tmux-spread.js +16 -1
- package/dist/commands/view-cycle.js +9 -5
- package/dist/commands/view-run.js +3 -2
- 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__/context-intro.test.js +23 -27
- 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} +4 -3
- 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} +7 -7
- 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.d.ts +1 -0
- 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 +100 -105
- package/dist/core/__tests__/live-mutation.test.js +111 -134
- package/dist/core/__tests__/memory.test.js +4 -4
- 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/bootstrap.js +18 -14
- 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/help.d.ts +2 -2
- package/dist/core/help.js +1 -1
- package/dist/core/memory-resolver.d.ts +9 -2
- package/dist/core/memory-resolver.js +47 -7
- package/dist/core/render.d.ts +4 -3
- package/dist/core/render.js +38 -41
- package/dist/core/resolver.d.ts +1 -30
- package/dist/core/resolver.js +6 -546
- package/dist/core/runtime/bearings.d.ts +2 -2
- package/dist/core/runtime/bearings.js +2 -2
- 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 +3 -5
- package/dist/core/substrate/render.js +68 -59
- package/dist/core/substrate/schema.d.ts +18 -13
- package/dist/core/substrate/schema.js +12 -11
- package/dist/pi-extensions/canvas-context-intro.js +5 -3
- 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 → commands/memory/__tests__/lint-schema.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-attach-stream.test.d.ts → full/broker-attach-limits.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-crash-teardown.test.d.ts → full/broker-attach-stream.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-dialogs.test.d.ts → full/broker-crash-teardown.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-lifecycle.test.d.ts → full/broker-dialogs.test.d.ts} +0 -0
- /package/dist/core/__tests__/{cascade-close.test.d.ts → full/broker-lifecycle.test.d.ts} +0 -0
- /package/dist/core/__tests__/{dead-pane-regression.test.d.ts → full/cascade-close.test.d.ts} +0 -0
- /package/dist/core/__tests__/{detach-focus.test.d.ts → full/daemon-liveness-pane.full.test.d.ts} +0 -0
- /package/dist/core/__tests__/{human-new-window-regression.test.d.ts → full/dead-pane-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-focus.test.d.ts → full/detach-focus.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-reconcile.test.d.ts → full/human-new-window-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-revive.test.d.ts → full/placement-focus.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-teardown.test.d.ts → full/placement-reconcile.test.d.ts} +0 -0
- /package/dist/core/__tests__/{resolver.test.d.ts → full/placement-revive.test.d.ts} +0 -0
- /package/dist/core/__tests__/{review-render-pane-regression.test.d.ts → full/placement-teardown.test.d.ts} +0 -0
- /package/dist/core/__tests__/{spike-harness.test.d.ts → full/review-render-pane-regression.test.d.ts} +0 -0
|
@@ -177,8 +177,8 @@ export const humanCancel = defineLeaf({
|
|
|
177
177
|
return { canceled: true, job_id: jobId };
|
|
178
178
|
},
|
|
179
179
|
render: (r) => r['canceled'] === true
|
|
180
|
-
?
|
|
181
|
-
:
|
|
180
|
+
? `Canceled human interaction ${r['job_id']} — its TUI pane is closed and subscribers were notified no answer is coming.`
|
|
181
|
+
: `Nothing to cancel for ${r['job_id']} — ${r['reason'] ?? 'nothing to cancel'}.`,
|
|
182
182
|
});
|
|
183
183
|
// ---------------------------------------------------------------------------
|
|
184
184
|
// _run (hidden worker; not listed in branch help)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Bug-regression: guards the hard cut that merged the substrate's `when`/`why`
|
|
2
|
+
// frontmatter pair into the single `when-and-why-to-read` read-routing field.
|
|
3
|
+
// The CTO ruling (taste/why-field-means-why-to-read) requires the migration be
|
|
4
|
+
// a hard cut: an old-shape doc must FAIL at `crtr memory lint`, never be read at
|
|
5
|
+
// runtime. `lintSubstrateSchema` is that enforcement seam — these lock it.
|
|
6
|
+
//
|
|
7
|
+
// Run: node --import tsx/esm --test src/commands/memory/__tests__/lint-schema.test.ts
|
|
8
|
+
import { test } from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { lintSubstrateSchema } from '../lint.js';
|
|
11
|
+
const ROUTING = 'When you are X, this reference should be read because Y';
|
|
12
|
+
test('lint rejects a doc still carrying the retired `when` key', () => {
|
|
13
|
+
const err = lintSubstrateSchema({ kind: 'reference', when: 'when X' });
|
|
14
|
+
assert.ok(err !== null, 'old-shape `when` must produce a finding');
|
|
15
|
+
assert.match(err, /when-and-why-to-read/, 'the message points at the new field');
|
|
16
|
+
});
|
|
17
|
+
test('lint rejects a doc still carrying the retired `why` key', () => {
|
|
18
|
+
const err = lintSubstrateSchema({ kind: 'reference', why: 'because Y' });
|
|
19
|
+
assert.ok(err !== null, 'old-shape `why` must produce a finding');
|
|
20
|
+
assert.match(err, /when-and-why-to-read/, 'the message points at the new field');
|
|
21
|
+
});
|
|
22
|
+
test('lint rejects a substrate doc missing the merged routing field', () => {
|
|
23
|
+
const err = lintSubstrateSchema({ kind: 'reference' });
|
|
24
|
+
assert.ok(err !== null, 'a substrate doc must carry when-and-why-to-read');
|
|
25
|
+
assert.match(err, /when-and-why-to-read/);
|
|
26
|
+
});
|
|
27
|
+
test('lint accepts the merged new-shape frontmatter', () => {
|
|
28
|
+
assert.equal(lintSubstrateSchema({ kind: 'reference', 'when-and-why-to-read': ROUTING }), null);
|
|
29
|
+
});
|
|
@@ -2,9 +2,6 @@ import { defineLeaf } from '../../core/command.js';
|
|
|
2
2
|
import { usage } from '../../core/errors.js';
|
|
3
3
|
import { listAllMemoryDocs } from '../../core/memory-resolver.js';
|
|
4
4
|
import { parseSubstrateDoc } from '../../core/substrate/index.js';
|
|
5
|
-
import { listAllSkills } from '../../core/resolver.js';
|
|
6
|
-
import { parseFrontmatter } from '../../core/frontmatter.js';
|
|
7
|
-
import { readText } from '../../core/fs-utils.js';
|
|
8
5
|
import { MEMORY_KINDS } from './shared.js';
|
|
9
6
|
function substrateUnit(d) {
|
|
10
7
|
return {
|
|
@@ -12,42 +9,19 @@ function substrateUnit(d) {
|
|
|
12
9
|
kind: d.kind,
|
|
13
10
|
scope: d.scope,
|
|
14
11
|
path: d.path,
|
|
15
|
-
|
|
16
|
-
why: d.why,
|
|
12
|
+
routing: d.whenAndWhyToRead,
|
|
17
13
|
shortForm: d.shortForm,
|
|
18
14
|
loadBody: () => d.body,
|
|
19
15
|
};
|
|
20
16
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
name: s.name,
|
|
26
|
-
kind: 'skill',
|
|
27
|
-
scope: s.scope,
|
|
28
|
-
path: s.path,
|
|
29
|
-
// A skill's description plays the when+why role; keywords ride along so the
|
|
30
|
-
// ranker weighs them too. short_form surfaces the description as the hook.
|
|
31
|
-
when: [desc, keywords].filter((x) => x).join(' '),
|
|
32
|
-
why: '',
|
|
33
|
-
shortForm: desc,
|
|
34
|
-
loadBody: () => parseFrontmatter(readText(s.path)).body,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
/** The unioned candidate set: every substrate memory document + every skill,
|
|
38
|
-
* optionally narrowed to one kind. find searches EVERYTHING — it never applies
|
|
39
|
-
* gate or visibility-rung filtering (design §11#3). The skill half is wrapped
|
|
40
|
-
* defensively: the skill corpus is parsed by the resolver, which currently
|
|
41
|
-
* hard-throws on at least one shipped skill's frontmatter (a pre-existing
|
|
42
|
-
* regression in the yaml-parser swap, owned by the frontmatter/skill track). A
|
|
43
|
-
* broken skill corpus must degrade the union to substrate-docs-only, never
|
|
44
|
-
* crash `memory find`.
|
|
17
|
+
/** The candidate set: every substrate memory document (native + plugin, supplied
|
|
18
|
+
* by listAllMemoryDocs), optionally narrowed to one kind. find searches
|
|
19
|
+
* EVERYTHING — it never applies gate or visibility-rung filtering (design
|
|
20
|
+
* §11#3).
|
|
45
21
|
*
|
|
46
|
-
* Dedup by (scope, name):
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* unit encountered for a given identity wins — substrate docs before skill-plugin
|
|
50
|
-
* docs, which mirrors the memory resolver's own precedence. */
|
|
22
|
+
* Dedup by (scope, name): native and plugin docs of the same scope can collide
|
|
23
|
+
* on identity. The FIRST unit encountered for a given identity wins — native
|
|
24
|
+
* before plugin, which mirrors the memory resolver's own precedence. */
|
|
51
25
|
function candidates(kindFilter) {
|
|
52
26
|
const seen = new Set(); // "scope/name" → first wins
|
|
53
27
|
const add = (u) => {
|
|
@@ -66,26 +40,17 @@ function candidates(kindFilter) {
|
|
|
66
40
|
continue;
|
|
67
41
|
add(substrateUnit(sub));
|
|
68
42
|
}
|
|
69
|
-
if (kindFilter === undefined || kindFilter === 'skill') {
|
|
70
|
-
try {
|
|
71
|
-
for (const skill of listAllSkills())
|
|
72
|
-
add(skillUnit(skill));
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
/* skill corpus unavailable — search substrate documents alone */
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
43
|
return units;
|
|
79
44
|
}
|
|
80
45
|
export const findLeaf = defineLeaf({
|
|
81
46
|
name: 'find',
|
|
82
47
|
description: 'relevance search across memory documents',
|
|
83
|
-
whenToUse: 'you do not yet know which document applies and need to discover what is stored — ranks documents by relevance, weighted over name,
|
|
48
|
+
whenToUse: 'you do not yet know which document applies and need to discover what is stored — ranks documents by relevance, weighted over name, the read-routing line, and short-form (add --body to also weigh body text). Searches the full scope set regardless of any visibility gate. Use --grep instead when you need an exact regex or literal-string match across document bodies rather than a ranked topic match.',
|
|
84
49
|
help: {
|
|
85
50
|
name: 'memory find',
|
|
86
|
-
summary: 'relevance search across memory documents, weighted over name/
|
|
51
|
+
summary: 'relevance search across memory documents, weighted over name/routing-line/short-form (and body with --body)',
|
|
87
52
|
params: [
|
|
88
|
-
{ kind: 'positional', name: 'query', required: true, constraint: 'With ranked search (default): whitespace-separated terms, matched case-insensitively and weighted over name,
|
|
53
|
+
{ kind: 'positional', name: 'query', required: true, constraint: 'With ranked search (default): whitespace-separated terms, matched case-insensitively and weighted over name, the read-routing line, and short-form (plus body with --body); documents matching more/stronger fields rank higher. With --grep: an ECMAScript regex applied to each document body line.' },
|
|
89
54
|
{ kind: 'flag', name: 'kind', type: 'enum', choices: [...MEMORY_KINDS], required: false, constraint: 'Filter to a single kind. Default: all kinds.' },
|
|
90
55
|
{ kind: 'flag', name: 'grep', type: 'bool', required: false, constraint: 'Treat the query as an ECMAScript regex and match it against document bodies, instead of weighted relevance ranking. Mutually exclusive with --body.' },
|
|
91
56
|
{ kind: 'flag', name: 'body', type: 'bool', required: false, constraint: 'Also weigh document body text in the relevance ranking (in addition to name/when/why/short-form). Ignored under --grep, which always scans bodies.' },
|
|
@@ -134,7 +99,7 @@ export const findLeaf = defineLeaf({
|
|
|
134
99
|
follow_up: 'Read a document in full with `crtr memory read <name>`. Drop --grep for a ranked topic search.',
|
|
135
100
|
};
|
|
136
101
|
}
|
|
137
|
-
// --- ranked mode: weighted relevance over name/
|
|
102
|
+
// --- ranked mode: weighted relevance over name/routing-line/short-form(+body) ---
|
|
138
103
|
const terms = query
|
|
139
104
|
.toLowerCase()
|
|
140
105
|
.split(/\s+/)
|
|
@@ -144,18 +109,15 @@ export const findLeaf = defineLeaf({
|
|
|
144
109
|
const hits = [];
|
|
145
110
|
for (const u of units) {
|
|
146
111
|
const nameLc = u.name.toLowerCase();
|
|
147
|
-
const
|
|
148
|
-
const whyLc = u.why.toLowerCase();
|
|
112
|
+
const routingLc = u.routing.toLowerCase();
|
|
149
113
|
const shortLc = u.shortForm.toLowerCase();
|
|
150
114
|
const bodyLc = weighBody ? u.loadBody().toLowerCase() : null;
|
|
151
115
|
let score = 0;
|
|
152
116
|
for (const term of terms) {
|
|
153
117
|
if (nameLc.includes(term))
|
|
154
118
|
score += 10;
|
|
155
|
-
if (
|
|
119
|
+
if (routingLc.includes(term))
|
|
156
120
|
score += 5;
|
|
157
|
-
if (whyLc.includes(term))
|
|
158
|
-
score += 4;
|
|
159
121
|
if (shortLc.includes(term))
|
|
160
122
|
score += 3;
|
|
161
123
|
if (bodyLc !== null && bodyLc.includes(term))
|
|
@@ -1 +1,8 @@
|
|
|
1
|
+
/** Schema checks for a doc living in a substrate memory dir: a memory store
|
|
2
|
+
* holds ONLY substrate docs, so a missing/invalid `kind` is an authoring
|
|
3
|
+
* error here (elsewhere it just means "not a substrate doc"). Rung and gate
|
|
4
|
+
* values are checked RAW — the runtime parser silently falls back to kind
|
|
5
|
+
* defaults / inert gates, which is exactly the silent tolerance this lint
|
|
6
|
+
* exists to catch at authoring time. */
|
|
7
|
+
export declare function lintSubstrateSchema(fm: Record<string, unknown> | null): string | null;
|
|
1
8
|
export declare const lintLeaf: import("../../core/command.js").LeafDef;
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
// `crtr memory lint` — the permanent valid-YAML gate over the bounded corpus
|
|
2
2
|
// (the CTO green-checkpoint: zero frontmatter parse errors at authoring time,
|
|
3
3
|
// so an invalid doc fails HERE instead of being silently isolated at runtime).
|
|
4
|
-
// Bounded corpus = the substrate memory dirs (project/user/builtin)
|
|
5
|
-
// every installed plugin's skills tree + ~/.claude/skills. Never a
|
|
4
|
+
// Bounded corpus = the substrate memory dirs (project/user/builtin). Never a
|
|
6
5
|
// filesystem-wide scan.
|
|
7
|
-
import {
|
|
8
|
-
import { basename, join, relative, sep } from 'node:path';
|
|
6
|
+
import { basename, relative, sep } from 'node:path';
|
|
9
7
|
import { defineLeaf } from '../../core/command.js';
|
|
10
8
|
import { general } from '../../core/errors.js';
|
|
11
9
|
import { warn } from '../../core/output.js';
|
|
12
10
|
import { pathExists, readText, walkFiles } from '../../core/fs-utils.js';
|
|
13
11
|
import { parseFrontmatterGeneric } from '../../core/frontmatter.js';
|
|
14
|
-
import {
|
|
15
|
-
import { scopeMemoryDir } from '../../core/scope.js';
|
|
12
|
+
import { listInstalledPlugins } from '../../core/resolver.js';
|
|
13
|
+
import { pluginMemoryDir, scopeMemoryDir } from '../../core/scope.js';
|
|
16
14
|
import { isDocKind, RUNGS } from '../../core/substrate/index.js';
|
|
17
|
-
import { SKILL_ENTRY_FILE, SKILLS_DIR } from '../../types.js';
|
|
18
15
|
/** The parser normalizes the `always` alias to `content`, so it lints valid. */
|
|
19
16
|
const VALID_RUNGS = [...RUNGS, 'always'];
|
|
20
17
|
const RUNG_FIELDS = ['system-prompt-visibility', 'file-read-visibility'];
|
|
@@ -24,12 +21,21 @@ const RUNG_FIELDS = ['system-prompt-visibility', 'file-read-visibility'];
|
|
|
24
21
|
* values are checked RAW — the runtime parser silently falls back to kind
|
|
25
22
|
* defaults / inert gates, which is exactly the silent tolerance this lint
|
|
26
23
|
* exists to catch at authoring time. */
|
|
27
|
-
function lintSubstrateSchema(fm) {
|
|
24
|
+
export function lintSubstrateSchema(fm) {
|
|
28
25
|
if (fm === null)
|
|
29
26
|
return 'missing frontmatter: a memory store doc requires `kind: skill|reference|preference`';
|
|
30
27
|
if (!isDocKind(fm.kind)) {
|
|
31
28
|
return `invalid kind: ${JSON.stringify(fm.kind)} (expected skill|reference|preference)`;
|
|
32
29
|
}
|
|
30
|
+
// The retired `when`/`why` pair was merged into one read-routing field. The
|
|
31
|
+
// hard cut is enforced HERE: an old-shape doc must fail, never be silently
|
|
32
|
+
// read at runtime.
|
|
33
|
+
if ('when' in fm || 'why' in fm) {
|
|
34
|
+
return 'retired `when`/`why` keys: merge them into one `when-and-why-to-read` line — "When <circumstance>, this <kind> should be read <because <payoff>>."';
|
|
35
|
+
}
|
|
36
|
+
if (typeof fm['when-and-why-to-read'] !== 'string' || fm['when-and-why-to-read'].trim() === '') {
|
|
37
|
+
return 'missing `when-and-why-to-read`: one read-routing line — "When <circumstance>, this <kind> should be read <because <payoff>>."';
|
|
38
|
+
}
|
|
33
39
|
for (const field of RUNG_FIELDS) {
|
|
34
40
|
const v = fm[field];
|
|
35
41
|
if (v !== undefined && (typeof v !== 'string' || !VALID_RUNGS.includes(v))) {
|
|
@@ -69,14 +75,14 @@ function lintFile(file, substrateStore, findings) {
|
|
|
69
75
|
export const lintLeaf = defineLeaf({
|
|
70
76
|
name: 'lint',
|
|
71
77
|
description: 'validate frontmatter across the whole bounded document corpus',
|
|
72
|
-
whenToUse: 'you authored or migrated documents and want the authoring-time gate: strict-parse every doc in the bounded corpus (memory stores
|
|
78
|
+
whenToUse: 'you authored or migrated documents and want the authoring-time gate: strict-parse every doc in the bounded corpus (the substrate memory stores) and fail loudly on any invalid YAML or substrate schema violation. Run it before shipping doc changes; CI-friendly (non-zero exit on any finding).',
|
|
73
79
|
help: {
|
|
74
80
|
name: 'memory lint',
|
|
75
81
|
summary: 'strict-parse frontmatter across the bounded corpus; non-zero exit on any finding',
|
|
76
82
|
params: [],
|
|
77
83
|
output: [
|
|
78
84
|
{ name: 'checked', type: 'number', required: true, constraint: 'Files linted across all corpora.' },
|
|
79
|
-
{ name: 'corpora', type: 'object', required: true, constraint: 'Per-corpus file counts: {memory_stores
|
|
85
|
+
{ name: 'corpora', type: 'object', required: true, constraint: 'Per-corpus file counts: {memory_stores}.' },
|
|
80
86
|
{ name: 'findings', type: 'object[]', required: true, constraint: 'One row per failure: {path, error}. Empty when green.' },
|
|
81
87
|
],
|
|
82
88
|
outputKind: 'object',
|
|
@@ -85,40 +91,28 @@ export const lintLeaf = defineLeaf({
|
|
|
85
91
|
run: async () => {
|
|
86
92
|
const findings = [];
|
|
87
93
|
let memoryCount = 0;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
//
|
|
94
|
+
// Substrate memory stores (project/user/builtin), schema-aware. A scope's
|
|
95
|
+
// corpus is its NATIVE memory dir plus each enabled plugin's memory dir —
|
|
96
|
+
// plugin docs are substrate docs and lint through the same schema gate.
|
|
91
97
|
// MEMORY.md index files are not substrate docs — YAML-parse only.
|
|
92
|
-
|
|
93
|
-
const dir = scopeMemoryDir(scope);
|
|
98
|
+
const lintDir = (dir) => {
|
|
94
99
|
if (!dir || !pathExists(dir))
|
|
95
|
-
|
|
100
|
+
return;
|
|
96
101
|
for (const file of walkFiles(dir, (n) => n.endsWith('.md'))) {
|
|
97
102
|
if (!relative(dir, file).split(sep).join('/'))
|
|
98
103
|
continue;
|
|
99
104
|
memoryCount += 1;
|
|
100
105
|
lintFile(file, basename(file) !== 'MEMORY.md', findings);
|
|
101
106
|
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
for (const file of walkFiles(skillsRoot, (n) => n === SKILL_ENTRY_FILE)) {
|
|
109
|
-
pluginCount += 1;
|
|
110
|
-
lintFile(file, false, findings);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
// 3. ~/.claude/skills — read by the runtime, so it lints too.
|
|
114
|
-
const claudeSkills = join(homedir(), '.claude', SKILLS_DIR);
|
|
115
|
-
if (pathExists(claudeSkills)) {
|
|
116
|
-
for (const file of walkFiles(claudeSkills, (n) => n === SKILL_ENTRY_FILE)) {
|
|
117
|
-
claudeCount += 1;
|
|
118
|
-
lintFile(file, false, findings);
|
|
107
|
+
};
|
|
108
|
+
for (const scope of ['project', 'user', 'builtin']) {
|
|
109
|
+
lintDir(scopeMemoryDir(scope));
|
|
110
|
+
for (const plugin of listInstalledPlugins(scope)) {
|
|
111
|
+
if (plugin.enabled)
|
|
112
|
+
lintDir(pluginMemoryDir(plugin));
|
|
119
113
|
}
|
|
120
114
|
}
|
|
121
|
-
const checked = memoryCount
|
|
115
|
+
const checked = memoryCount;
|
|
122
116
|
if (findings.length > 0) {
|
|
123
117
|
// Human/agent path renders only the message — surface every offender as
|
|
124
118
|
// a scoped stderr notice (the --json path carries them in details too).
|
|
@@ -132,7 +126,7 @@ export const lintLeaf = defineLeaf({
|
|
|
132
126
|
}
|
|
133
127
|
return {
|
|
134
128
|
checked,
|
|
135
|
-
corpora: { memory_stores: memoryCount
|
|
129
|
+
corpora: { memory_stores: memoryCount },
|
|
136
130
|
findings: [],
|
|
137
131
|
follow_up: 'Corpus green — zero invalid frontmatter docs.',
|
|
138
132
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { defineLeaf } from '../../core/command.js';
|
|
2
2
|
import { listAllMemoryDocs } from '../../core/memory-resolver.js';
|
|
3
|
-
import { parseSubstrateDoc } from '../../core/substrate/index.js';
|
|
4
|
-
import { listAllSkills } from '../../core/resolver.js';
|
|
3
|
+
import { parseSubstrateDoc, isIndexName, indexDirOf } from '../../core/substrate/index.js';
|
|
5
4
|
import { MEMORY_KINDS, MEMORY_SCOPES, scopeRank } from './shared.js';
|
|
6
5
|
export const listLeaf = defineLeaf({
|
|
7
6
|
name: 'list',
|
|
@@ -15,7 +14,7 @@ export const listLeaf = defineLeaf({
|
|
|
15
14
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: [...MEMORY_SCOPES], required: false, constraint: 'Filter to a single scope. Default: all resolved scopes.' },
|
|
16
15
|
],
|
|
17
16
|
output: [
|
|
18
|
-
{ name: 'items', type: 'object[]', required: true, constraint: 'One row per document. Each: {name, title, short_form, kind, scope}. short_form is the abbreviated hook — shown here and nowhere else. Sorted by scope then kind then name ascending.' },
|
|
17
|
+
{ name: 'items', type: 'object[]', required: true, constraint: 'One row per document. Each: {name, title, short_form, kind, scope, is_dir}. A directory INDEX.md surfaces as a dir entry (is_dir true, name = the directory path with a trailing slash); is_dir is false for an ordinary doc. short_form is the abbreviated hook — shown here and nowhere else. Sorted by scope then kind then name ascending.' },
|
|
19
18
|
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands — read a document in full or narrow the inventory.' },
|
|
20
19
|
],
|
|
21
20
|
outputKind: 'object',
|
|
@@ -40,21 +39,11 @@ export const listLeaf = defineLeaf({
|
|
|
40
39
|
continue;
|
|
41
40
|
if (kindFilter !== undefined && sub.kind !== kindFilter)
|
|
42
41
|
continue;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
for (const skill of listAllSkills(scopeFilter)) {
|
|
50
|
-
const raw = skill.frontmatter.description;
|
|
51
|
-
const desc = typeof raw === 'string' ? raw : '';
|
|
52
|
-
addItem({ name: skill.name, kind: 'skill', scope: skill.scope, shortForm: desc });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
/* skill corpus unavailable — list substrate documents alone */
|
|
57
|
-
}
|
|
42
|
+
// A directory INDEX surfaces as a dir entry: its dir path with a trailing
|
|
43
|
+
// slash, flagged is_dir, so the inventory distinguishes it from a doc.
|
|
44
|
+
const isDir = isIndexName(sub.name);
|
|
45
|
+
const name = isDir ? indexDirOf(sub.name) + '/' : sub.name;
|
|
46
|
+
addItem({ name, kind: sub.kind, scope: sub.scope, shortForm: sub.shortForm, isDir });
|
|
58
47
|
}
|
|
59
48
|
items.sort((a, b) => {
|
|
60
49
|
const sr = scopeRank(a.scope) - scopeRank(b.scope);
|
|
@@ -72,6 +61,7 @@ export const listLeaf = defineLeaf({
|
|
|
72
61
|
short_form: d.shortForm,
|
|
73
62
|
kind: d.kind,
|
|
74
63
|
scope: d.scope,
|
|
64
|
+
is_dir: d.isDir,
|
|
75
65
|
})),
|
|
76
66
|
follow_up: 'Read one in full with `crtr memory read <name>`. Narrow with --kind / --scope, or search a topic with `crtr memory find <query>`.',
|
|
77
67
|
};
|
|
@@ -2,8 +2,6 @@ import { defineLeaf } from '../../core/command.js';
|
|
|
2
2
|
import { CrtrError, notFound } from '../../core/errors.js';
|
|
3
3
|
import { resolveMemoryDoc } from '../../core/memory-resolver.js';
|
|
4
4
|
import { parseSubstrateDoc } from '../../core/substrate/index.js';
|
|
5
|
-
import { resolveSkill } from '../../core/resolver.js';
|
|
6
|
-
import { parseFrontmatter } from '../../core/frontmatter.js';
|
|
7
5
|
import { readText } from '../../core/fs-utils.js';
|
|
8
6
|
import { MEMORY_KINDS } from './shared.js';
|
|
9
7
|
export const readLeaf = defineLeaf({
|
|
@@ -33,9 +31,9 @@ export const readLeaf = defineLeaf({
|
|
|
33
31
|
const nameRaw = input['name'];
|
|
34
32
|
const kindFilter = input['kind'];
|
|
35
33
|
const includeFrontmatter = input['frontmatter'];
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
34
|
+
// Resolve a substrate/memory document across scopes (project>user>builtin,
|
|
35
|
+
// with leaf-name fallback). The substrate corpus now includes plugin docs
|
|
36
|
+
// (mounted under <pluginName>/), so this resolves every name.
|
|
39
37
|
let doc;
|
|
40
38
|
try {
|
|
41
39
|
doc = resolveMemoryDoc(nameRaw);
|
|
@@ -51,8 +49,7 @@ export const readLeaf = defineLeaf({
|
|
|
51
49
|
: typeof doc.frontmatter?.['kind'] === 'string'
|
|
52
50
|
? doc.frontmatter['kind']
|
|
53
51
|
: 'reference';
|
|
54
|
-
// --kind asserts the resolved kind; a mismatch falls through to
|
|
55
|
-
// (which only satisfy --kind=skill).
|
|
52
|
+
// --kind asserts the resolved kind; a mismatch falls through to not-found.
|
|
56
53
|
if (kindFilter === undefined || kind === kindFilter) {
|
|
57
54
|
const content = includeFrontmatter ? readText(doc.path) : doc.body;
|
|
58
55
|
return {
|
|
@@ -65,36 +62,6 @@ export const readLeaf = defineLeaf({
|
|
|
65
62
|
};
|
|
66
63
|
}
|
|
67
64
|
}
|
|
68
|
-
// 2) D4 union fallthrough: the name is not a memory document (or its kind
|
|
69
|
-
// did not match --kind) — resolve it as a plugin/marketplace/scope skill
|
|
70
|
-
// so skills are readable through `crtr memory read` too. Skills are kind
|
|
71
|
-
// "skill", so honor --kind by skipping this path for other kinds.
|
|
72
|
-
if (kindFilter === undefined || kindFilter === 'skill') {
|
|
73
|
-
let skill;
|
|
74
|
-
try {
|
|
75
|
-
skill = resolveSkill(nameRaw);
|
|
76
|
-
}
|
|
77
|
-
catch (e) {
|
|
78
|
-
// not_found → fall through to the memory-not-found below. A genuine
|
|
79
|
-
// CrtrError (e.g. ambiguous) propagates. Any other throw means the skill
|
|
80
|
-
// corpus is currently unparseable (the yaml-parser regression owned by
|
|
81
|
-
// the frontmatter/skill track) — degrade to not-found rather than 500.
|
|
82
|
-
if (e instanceof CrtrError && e.code !== 'not_found')
|
|
83
|
-
throw e;
|
|
84
|
-
}
|
|
85
|
-
if (skill !== undefined) {
|
|
86
|
-
const raw = readText(skill.path);
|
|
87
|
-
const content = includeFrontmatter ? raw : parseFrontmatter(raw).body;
|
|
88
|
-
return {
|
|
89
|
-
name: skill.name,
|
|
90
|
-
kind: 'skill',
|
|
91
|
-
scope: skill.scope,
|
|
92
|
-
path: skill.path,
|
|
93
|
-
content,
|
|
94
|
-
follow_up: 'Add --frontmatter to include the YAML header. Browse the inventory with `crtr memory list`.',
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
65
|
throw notFound(`memory document not found: ${nameRaw}`, {
|
|
99
66
|
memory: nameRaw,
|
|
100
67
|
next: 'Run `crtr memory find <query>` to discover documents, or `crtr memory list` to browse the inventory.',
|
|
@@ -91,8 +91,7 @@ export function coerceAppliesTo(raw) {
|
|
|
91
91
|
// first in this order; any preserved-on-update extras append after.
|
|
92
92
|
const FRONTMATTER_ORDER = [
|
|
93
93
|
'kind',
|
|
94
|
-
'when',
|
|
95
|
-
'why',
|
|
94
|
+
'when-and-why-to-read',
|
|
96
95
|
'short-form',
|
|
97
96
|
'system-prompt-visibility',
|
|
98
97
|
'file-read-visibility',
|
|
@@ -13,8 +13,7 @@ export const writeLeaf = defineLeaf({
|
|
|
13
13
|
params: [
|
|
14
14
|
{ kind: 'positional', name: 'name', required: true, constraint: 'Path-derived identity (e.g. `topic` or `area/topic`) → memory/<name>.md at the resolved scope. Updated in place if it already exists, otherwise created.' },
|
|
15
15
|
{ kind: 'flag', name: 'kind', type: 'enum', choices: [...MEMORY_KINDS], required: true, constraint: 'Document kind.' },
|
|
16
|
-
{ kind: 'flag', name: 'when', type: 'string', required: false, constraint: '
|
|
17
|
-
{ kind: 'flag', name: 'why', type: 'string', required: false, constraint: 'Frontmatter whyText — a short string describing why this document matters.' },
|
|
16
|
+
{ kind: 'flag', name: 'when-and-why-to-read', type: 'string', required: false, constraint: 'The read-routing line, authored as ONE sentence: "When <circumstance>, this <kind> should be read <because <payoff>>." It states WHEN to read this doc and WHY the read is worth it — read-routing, NEVER a justification of why the content should be obeyed. Rendered verbatim as the preview. Required when creating.' },
|
|
18
17
|
{ kind: 'flag', name: 'short-form', type: 'string', required: false, constraint: 'Frontmatter short-form — a very abbreviated version of the content, the hook shown in `crtr memory list`.' },
|
|
19
18
|
{ kind: 'flag', name: 'system-prompt-visibility', type: 'enum', choices: [...VISIBILITY_RUNGS], required: false, constraint: 'Rung controlling how much of this document auto-loads into the system prompt / CLI help.' },
|
|
20
19
|
{ kind: 'flag', name: 'file-read-visibility', type: 'enum', choices: [...VISIBILITY_RUNGS], required: false, constraint: 'Rung controlling how much of this document surfaces when it is read off disk.' },
|
|
@@ -44,11 +43,10 @@ export const writeLeaf = defineLeaf({
|
|
|
44
43
|
const { scope, memoryDir } = resolveWriteTarget(scopeArg);
|
|
45
44
|
const path = memoryFilePath(memoryDir, name);
|
|
46
45
|
const created = !pathExists(path);
|
|
47
|
-
// CREATE requires the
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
throw usage(`creating ${name} requires --when and --why (they compose the preview routing line "{when}, read this ${kind}. {why}.")`);
|
|
46
|
+
// CREATE requires the read-routing line that becomes the preview — without
|
|
47
|
+
// it every preview renders empty. UPDATE inherits it from the existing doc.
|
|
48
|
+
if (created && input['whenAndWhyToRead'] === undefined) {
|
|
49
|
+
throw usage(`creating ${name} requires --when-and-why-to-read: one read-routing sentence "When <circumstance>, this ${kind} should be read <because <payoff>>." (rendered verbatim as the preview).`);
|
|
52
50
|
}
|
|
53
51
|
// In-place update: start from the existing frontmatter (preserving fields
|
|
54
52
|
// not passed this time), then overlay the provided ones. Create: start clean.
|
|
@@ -62,8 +60,7 @@ export const writeLeaf = defineLeaf({
|
|
|
62
60
|
if (value !== undefined)
|
|
63
61
|
frontmatter[key] = value;
|
|
64
62
|
};
|
|
65
|
-
setIf('when', input['
|
|
66
|
-
setIf('why', input['why']);
|
|
63
|
+
setIf('when-and-why-to-read', input['whenAndWhyToRead']);
|
|
67
64
|
setIf('short-form', input['shortForm']);
|
|
68
65
|
setIf('system-prompt-visibility', input['systemPromptVisibility']);
|
|
69
66
|
setIf('file-read-visibility', input['fileReadVisibility']);
|