@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
|
@@ -1,56 +1,7 @@
|
|
|
1
1
|
import { usage } from '../../core/errors.js';
|
|
2
|
-
import { SCOPE_SKILL_PLUGIN } from '../../types.js';
|
|
3
|
-
import { listSkillSiblings, listSkillChildren, } from '../../core/resolver.js';
|
|
4
2
|
import { resolveScopeArg, projectScopeRoot } from '../../core/scope.js';
|
|
5
3
|
// ---------------------------------------------------------------------------
|
|
6
|
-
//
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
export function formatNeighborQualifier(s) {
|
|
9
|
-
return s.plugin === SCOPE_SKILL_PLUGIN
|
|
10
|
-
? `${s.scope}/${s.name}`
|
|
11
|
-
: `${s.plugin}/${s.name}`;
|
|
12
|
-
}
|
|
13
|
-
export function formatNeighborKeywords(s) {
|
|
14
|
-
const kw = s.frontmatter.keywords;
|
|
15
|
-
if (!kw || kw.length === 0)
|
|
16
|
-
return '';
|
|
17
|
-
return ` — [${kw.join(', ')}]`;
|
|
18
|
-
}
|
|
19
|
-
export function buildNeighborsSection(skill) {
|
|
20
|
-
const siblings = listSkillSiblings(skill);
|
|
21
|
-
const children = listSkillChildren(skill);
|
|
22
|
-
if (siblings.length === 0 && children.length === 0)
|
|
23
|
-
return null;
|
|
24
|
-
const lines = [
|
|
25
|
-
'## Neighbors',
|
|
26
|
-
'*Auto-discovered from filesystem. Run `crtr memory read <name>` for full description + body.*',
|
|
27
|
-
'',
|
|
28
|
-
];
|
|
29
|
-
if (siblings.length > 0) {
|
|
30
|
-
lines.push('**Siblings:**');
|
|
31
|
-
for (const s of siblings) {
|
|
32
|
-
lines.push(`- \`${formatNeighborQualifier(s)}\`${formatNeighborKeywords(s)}`);
|
|
33
|
-
}
|
|
34
|
-
if (children.length > 0)
|
|
35
|
-
lines.push('');
|
|
36
|
-
}
|
|
37
|
-
if (children.length > 0) {
|
|
38
|
-
lines.push('**Nested:**');
|
|
39
|
-
for (const s of children) {
|
|
40
|
-
lines.push(`- \`${formatNeighborQualifier(s)}\`${formatNeighborKeywords(s)}`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return lines.join('\n');
|
|
44
|
-
}
|
|
45
|
-
export function appendNeighbors(skill, body) {
|
|
46
|
-
const section = buildNeighborsSection(skill);
|
|
47
|
-
if (section === null)
|
|
48
|
-
return body;
|
|
49
|
-
const sep = body.endsWith('\n') ? '\n' : '\n\n';
|
|
50
|
-
return body + sep + `<neighbors>\n${section}\n</neighbors>\n`;
|
|
51
|
-
}
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
// Resolve scope for enable/disable/scaffold
|
|
4
|
+
// Resolve scope for scaffold
|
|
54
5
|
// ---------------------------------------------------------------------------
|
|
55
6
|
export function resolveWriteScope(scopeStr) {
|
|
56
7
|
if (scopeStr !== undefined) {
|
|
@@ -73,19 +24,7 @@ export function renderCatalogSection(label, sources, descriptions, out) {
|
|
|
73
24
|
const count = sources.reduce((n, s) => n + s.roots.length, 0);
|
|
74
25
|
out.push('');
|
|
75
26
|
out.push(`${label} (${count})`);
|
|
76
|
-
const
|
|
77
|
-
const named = sources
|
|
78
|
-
.filter((s) => s.plugin !== SCOPE_SKILL_PLUGIN)
|
|
79
|
-
.sort((a, b) => a.plugin.localeCompare(b.plugin));
|
|
80
|
-
for (const s of sentinel) {
|
|
81
|
-
if (s.roots.length > CATALOG_T) {
|
|
82
|
-
out.push(` (scope skills) ${s.roots.length} skills`);
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
for (const n of s.roots)
|
|
86
|
-
out.push(` ${n}`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
27
|
+
const named = [...sources].sort((a, b) => a.plugin.localeCompare(b.plugin));
|
|
89
28
|
if (named.length === 0)
|
|
90
29
|
return;
|
|
91
30
|
const classified = named.map((s) => {
|
|
@@ -123,6 +62,13 @@ export function renderCatalogSection(label, sources, descriptions, out) {
|
|
|
123
62
|
})
|
|
124
63
|
.reduce((m, p) => Math.max(m, p.plugin.length + 1), 0);
|
|
125
64
|
for (const p of classified) {
|
|
65
|
+
// Bare native/builtin skills (no plugin namespace) carry sourceKey ''.
|
|
66
|
+
// Render them as flat lines, never under a stray '/' header.
|
|
67
|
+
if (p.plugin === '') {
|
|
68
|
+
for (const r of [...p.roots].sort())
|
|
69
|
+
out.push(` ${r}`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
126
72
|
const direct = p.subcats.size + p.bare.length;
|
|
127
73
|
if (direct > CATALOG_T) {
|
|
128
74
|
out.push(` ${(p.plugin + '/').padEnd(inlineW)} ${p.roots.length} skills${descSuffix(p.plugin)}`);
|
package/dist/commands/skill.js
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
// `crtr skill` subtree handlers.
|
|
2
|
-
// Sub-
|
|
2
|
+
// Sub-branch: author {guide, scaffold}.
|
|
3
3
|
// Discovery and reading are now `crtr memory` verbs.
|
|
4
4
|
import { defineBranch } from '../core/command.js';
|
|
5
5
|
import { authorBranch } from './skill/author.js';
|
|
6
|
-
import { stateBranch } from './skill/state.js';
|
|
7
6
|
export function registerSkill() {
|
|
8
7
|
return defineBranch({
|
|
9
8
|
name: 'skill',
|
|
10
9
|
rootEntry: {
|
|
11
10
|
concept: 'a SKILL.md you author and manage',
|
|
12
11
|
desc: 'author and manage skills',
|
|
13
|
-
useWhen: 'authoring a new skill (`crtr skill author`)
|
|
12
|
+
useWhen: 'authoring a new skill (`crtr skill author`). To discover and read skills use `crtr memory` — `crtr memory read <name>`, `crtr memory find`, `crtr memory list`.',
|
|
14
13
|
},
|
|
15
14
|
help: {
|
|
16
15
|
name: 'skill',
|
|
17
|
-
summary: 'author
|
|
18
|
-
model: '`author` when you are writing a new skill — it carries the template workflow and the scaffolder.
|
|
16
|
+
summary: 'author skills — use `crtr memory` to discover and read them',
|
|
17
|
+
model: '`author` when you are writing a new skill — it carries the template workflow and the scaffolder. Discovery and reading are `crtr memory` verbs: `crtr memory list`, `crtr memory find`, `crtr memory read`. Visibility is governed by the substrate (INDEX/rung/gate), not a per-skill on/off. Append `-h` at any branch or leaf for its full schema.',
|
|
19
18
|
},
|
|
20
|
-
children: [authorBranch
|
|
19
|
+
children: [authorBranch],
|
|
21
20
|
});
|
|
22
21
|
}
|
|
@@ -1,54 +1,16 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { defineLeaf } from '../../core/command.js';
|
|
3
3
|
import { readConfig, updateConfig } from '../../core/config.js';
|
|
4
|
-
import { scopeRoot, listScopes,
|
|
5
|
-
import { listInstalledPlugins, listSkillsInPlugin } from '../../core/resolver.js';
|
|
4
|
+
import { scopeRoot, listScopes, marketplacesDir, pluginsDir } from '../../core/scope.js';
|
|
6
5
|
import { readMarketplaceManifest, readPluginManifest } from '../../core/manifest.js';
|
|
7
|
-
import {
|
|
8
|
-
import { pathExists, listDirs, removePath, readText, writeText } from '../../core/fs-utils.js';
|
|
6
|
+
import { pathExists, listDirs, removePath } from '../../core/fs-utils.js';
|
|
9
7
|
import { lsRemote } from '../../core/git.js';
|
|
10
|
-
import { SKILL_TYPES, isSkillType } from '../../types.js';
|
|
11
8
|
function pass(scope, name, message) {
|
|
12
9
|
return { scope, name, status: 'pass', message };
|
|
13
10
|
}
|
|
14
11
|
function failCheck(scope, name, message, remediation) {
|
|
15
12
|
return { scope, name, status: 'fail', message, ...(remediation ? { remediation } : {}) };
|
|
16
13
|
}
|
|
17
|
-
function warnCheck(scope, name, message, remediation) {
|
|
18
|
-
return { scope, name, status: 'warn', message, ...(remediation ? { remediation } : {}) };
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Surgically replace a single frontmatter scalar field's value in a SKILL.md
|
|
22
|
-
* file. Preserves the rest of the file (key order, comments, extra fields,
|
|
23
|
-
* body) exactly. Returns true on success, false if no frontmatter or no such
|
|
24
|
-
* field was found.
|
|
25
|
-
*/
|
|
26
|
-
function editFrontmatterField(filePath, field, newValue) {
|
|
27
|
-
let src;
|
|
28
|
-
try {
|
|
29
|
-
src = readText(filePath);
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
const fmMatch = src.match(/^(---\s*\r?\n)([\s\S]*?)(\r?\n---\s*\r?\n?)/);
|
|
35
|
-
if (!fmMatch)
|
|
36
|
-
return false;
|
|
37
|
-
const [, head, body, tail] = fmMatch;
|
|
38
|
-
const fieldRe = new RegExp(`^(\\s*${field}\\s*:\\s*)(.*)$`, 'm');
|
|
39
|
-
if (!fieldRe.test(body))
|
|
40
|
-
return false;
|
|
41
|
-
const quoted = /[:#\-\[\]{},&*?|<>=!%@`]/.test(newValue) || /^\s/.test(newValue) || /\s$/.test(newValue);
|
|
42
|
-
const formatted = quoted ? `"${newValue.replace(/"/g, '\\"')}"` : newValue;
|
|
43
|
-
const newBody = body.replace(fieldRe, `$1${formatted}`);
|
|
44
|
-
try {
|
|
45
|
-
writeText(filePath, head + newBody + tail + src.slice(fmMatch[0].length));
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
14
|
/**
|
|
53
15
|
* Apply a remediation. Returns true if applied successfully. Idempotent for
|
|
54
16
|
* the supported kinds (re-applying a config-key removal that's already gone
|
|
@@ -79,11 +41,6 @@ function applyRemediation(rem) {
|
|
|
79
41
|
removePath(rem.path);
|
|
80
42
|
return true;
|
|
81
43
|
}
|
|
82
|
-
case 'edit_frontmatter': {
|
|
83
|
-
if (!rem.filePath || !rem.field || rem.value === undefined)
|
|
84
|
-
return false;
|
|
85
|
-
return editFrontmatterField(rem.filePath, rem.field, rem.value);
|
|
86
|
-
}
|
|
87
44
|
default:
|
|
88
45
|
return false;
|
|
89
46
|
}
|
|
@@ -92,46 +49,7 @@ function applyRemediation(rem) {
|
|
|
92
49
|
return false;
|
|
93
50
|
}
|
|
94
51
|
}
|
|
95
|
-
function readRawTypeField(skillPath) {
|
|
96
|
-
const content = readText(skillPath);
|
|
97
|
-
const { raw } = parseFrontmatter(content);
|
|
98
|
-
if (!raw)
|
|
99
|
-
return undefined;
|
|
100
|
-
const m = raw.match(/^type:\s*(.+?)\s*$/m);
|
|
101
|
-
if (!m)
|
|
102
|
-
return undefined;
|
|
103
|
-
let v = m[1].trim();
|
|
104
|
-
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
105
|
-
v = v.slice(1, -1);
|
|
106
|
-
}
|
|
107
|
-
return v;
|
|
108
|
-
}
|
|
109
|
-
function runChecksForBuiltin() {
|
|
110
|
-
const root = builtinSkillsRoot();
|
|
111
|
-
const plugins = listInstalledPlugins('builtin');
|
|
112
|
-
if (plugins.length === 0) {
|
|
113
|
-
return [failCheck('builtin', 'builtin:crtr:root', `builtin-skills root missing or has no valid plugin.json: ${root}`)];
|
|
114
|
-
}
|
|
115
|
-
const results = [
|
|
116
|
-
pass('builtin', 'builtin:crtr:root', `builtin-skills root present: ${root}`),
|
|
117
|
-
];
|
|
118
|
-
for (const plugin of plugins) {
|
|
119
|
-
results.push(pass('builtin', `builtin:${plugin.name}:manifest`, `manifest valid`));
|
|
120
|
-
const skills = listSkillsInPlugin(plugin);
|
|
121
|
-
for (const skill of skills) {
|
|
122
|
-
if (!skill.frontmatter.name) {
|
|
123
|
-
results.push(failCheck('builtin', `builtin:${plugin.name}:skill:${skill.name}:frontmatter`, `frontmatter missing or name field empty`));
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
results.push(pass('builtin', `builtin:${plugin.name}:skill:${skill.name}:frontmatter`, `frontmatter valid`));
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return results;
|
|
131
|
-
}
|
|
132
52
|
function runChecksForScope(scope, opts) {
|
|
133
|
-
if (scope === 'builtin')
|
|
134
|
-
return runChecksForBuiltin();
|
|
135
53
|
const results = [];
|
|
136
54
|
const root = scopeRoot(scope);
|
|
137
55
|
if (!root)
|
|
@@ -253,62 +171,6 @@ function runChecksForScope(scope, opts) {
|
|
|
253
171
|
else {
|
|
254
172
|
seenPluginNames.set(name, dir);
|
|
255
173
|
}
|
|
256
|
-
// Check: skills frontmatter name. Convention: frontmatter `name:` holds
|
|
257
|
-
// the leaf segment only (e.g. "cli-design"); the full discovered name
|
|
258
|
-
// ("interface/cli-design") is derived from the path automatically.
|
|
259
|
-
const plugin = listInstalledPlugins(scope).find((p) => p.name === name);
|
|
260
|
-
if (plugin) {
|
|
261
|
-
const skills = listSkillsInPlugin(plugin);
|
|
262
|
-
for (const skill of skills) {
|
|
263
|
-
const checkName = `plugin:${name}:skill:${skill.name}:frontmatter`;
|
|
264
|
-
const segments = skill.name.split('/');
|
|
265
|
-
const baseName = segments[segments.length - 1];
|
|
266
|
-
if (skill.frontmatter.name === baseName) {
|
|
267
|
-
results.push(pass(scope, checkName, `frontmatter valid`));
|
|
268
|
-
}
|
|
269
|
-
else if (skill.frontmatter.name === '') {
|
|
270
|
-
const remediation = {
|
|
271
|
-
kind: 'edit_frontmatter',
|
|
272
|
-
description: `Set frontmatter "name: ${baseName}" (discovered name "${skill.name}" is auto-derived from the directory path; frontmatter holds the base segment only)`,
|
|
273
|
-
filePath: skill.path,
|
|
274
|
-
field: 'name',
|
|
275
|
-
value: baseName,
|
|
276
|
-
};
|
|
277
|
-
if (opts.fix && applyRemediation(remediation)) {
|
|
278
|
-
results.push({ scope, name: checkName, status: 'fail', message: `frontmatter name was missing — set to "${baseName}"`, fixed: true, remediation });
|
|
279
|
-
}
|
|
280
|
-
else {
|
|
281
|
-
results.push(failCheck(scope, checkName, `frontmatter missing or name field empty`, remediation));
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
else {
|
|
285
|
-
const remediation = {
|
|
286
|
-
kind: 'edit_frontmatter',
|
|
287
|
-
description: `Replace frontmatter "name: ${skill.frontmatter.name}" with "name: ${baseName}" (discovered name "${skill.name}" is auto-derived from the directory path; frontmatter holds the base segment only)`,
|
|
288
|
-
filePath: skill.path,
|
|
289
|
-
field: 'name',
|
|
290
|
-
value: baseName,
|
|
291
|
-
};
|
|
292
|
-
if (opts.fix && applyRemediation(remediation)) {
|
|
293
|
-
results.push({ scope, name: checkName, status: 'warn', message: `frontmatter name updated from "${skill.frontmatter.name}" to "${baseName}"`, fixed: true, remediation });
|
|
294
|
-
}
|
|
295
|
-
else {
|
|
296
|
-
results.push(warnCheck(scope, checkName, `name mismatch: frontmatter says "${skill.frontmatter.name}", expected base name "${baseName}" (discovered as "${skill.name}")`, remediation));
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
const typeCheckName = `plugin:${name}:skill:${skill.name}:type`;
|
|
300
|
-
const rawType = readRawTypeField(skill.path);
|
|
301
|
-
if (rawType === undefined) {
|
|
302
|
-
results.push(warnCheck(scope, typeCheckName, `missing type field — add one of: ${SKILL_TYPES.join(' | ')}`));
|
|
303
|
-
}
|
|
304
|
-
else if (!isSkillType(rawType)) {
|
|
305
|
-
results.push(failCheck(scope, typeCheckName, `invalid type "${rawType}" — valid: ${SKILL_TYPES.join(' | ')}`));
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
results.push(pass(scope, typeCheckName, `type: ${rawType}`));
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
174
|
// Git remote check (slow, opt-in)
|
|
313
175
|
if (opts.remote && manifest.source) {
|
|
314
176
|
const res = lsRemote(manifest.source);
|
|
@@ -326,23 +188,23 @@ function runChecksForScope(scope, opts) {
|
|
|
326
188
|
export const sysDoctorLeaf = defineLeaf({
|
|
327
189
|
name: 'doctor',
|
|
328
190
|
description: 'diagnose installation health',
|
|
329
|
-
whenToUse: 'something in your crtr install looks off and you want it diagnosed — a plugin or marketplace manifest is missing, a config entry points at a directory that no longer exists
|
|
191
|
+
whenToUse: 'something in your crtr install looks off and you want it diagnosed — a plugin or marketplace manifest is missing, or a config entry points at a directory that no longer exists. Reports each problem with a structured remediation, and can apply the repairs for you.',
|
|
330
192
|
help: {
|
|
331
193
|
name: 'sys doctor',
|
|
332
|
-
summary: 'diagnose missing manifests
|
|
194
|
+
summary: 'diagnose missing manifests and broken config entries',
|
|
333
195
|
params: [
|
|
334
196
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Scope to check. Default: all scopes.' },
|
|
335
197
|
{ kind: 'flag', name: 'fix', type: 'bool', required: false, constraint: 'Apply each non-pass check\'s remediation and report what was fixed.' },
|
|
336
198
|
{ kind: 'flag', name: 'remote', type: 'bool', required: false, constraint: 'Check git remotes with ls-remote (slow — makes network calls).' },
|
|
337
199
|
],
|
|
338
200
|
output: [
|
|
339
|
-
{ name: 'checks', type: 'object[]', required: true, constraint: 'Each: {scope, name, status, message, fixed?, remediation?}. status: pass | fail
|
|
201
|
+
{ name: 'checks', type: 'object[]', required: true, constraint: 'Each: {scope, name, status, message, fixed?, remediation?}. status: pass | fail. remediation (when present) is {kind, description, ...payload} where kind is remove_config_key | rm_path. Sorted by scope then name.' },
|
|
340
202
|
{ name: 'ok', type: 'boolean', required: true, constraint: 'True when no unresolved fail checks remain.' },
|
|
341
203
|
],
|
|
342
204
|
outputKind: 'object',
|
|
343
205
|
effects: [
|
|
344
206
|
'Read-only unless --fix is passed.',
|
|
345
|
-
'With --fix: applies each non-pass check\'s `remediation` — removes stale config entries
|
|
207
|
+
'With --fix: applies each non-pass check\'s `remediation` — removes stale config entries and deletes dangling plugin/marketplace directories.',
|
|
346
208
|
'Each non-pass result carries a structured `remediation` describing the fix action (absolute paths, exact config keys) so callers can apply it directly without --fix.',
|
|
347
209
|
],
|
|
348
210
|
},
|
|
@@ -1,51 +1,88 @@
|
|
|
1
1
|
// Run with: node --import tsx/esm --test src/core/__tests__/broker-dormant-wake.test.ts
|
|
2
2
|
//
|
|
3
|
-
// Acceptance item 3 (DORMANT) of
|
|
4
|
-
// broker
|
|
5
|
-
//
|
|
3
|
+
// HEADLESS RETARGET (foundation-spec §C.5 + §E). Acceptance item 3 (DORMANT) of
|
|
4
|
+
// the headless-broker migration, fabricated DIRECTLY in the canvas — no real
|
|
5
|
+
// tmux session, no pane chrome, and NO real broker boot. The wake decision is
|
|
6
|
+
// pure daemon logic, so the test never pays the ~5s SDK-boot cost.
|
|
7
|
+
//
|
|
8
|
+
// (1) BUG LOCKED — Invariant D (idle-release dormancy). A node that naturally
|
|
9
|
+
// stops while awaiting a LIVE subscription idle-RELEASES its broker (the
|
|
10
|
+
// broker process exits); the daemon's SECOND PASS must revive it (RESUME)
|
|
11
|
+
// the moment an UNSEEN inbox entry appears. Without the second pass a dormant
|
|
12
|
+
// orchestrator never wakes on a worker's message and the graph deadlocks.
|
|
13
|
+
//
|
|
14
|
+
// (2) WHY MODEL-LEVEL, NOT TMUX CHROME — every step is canvas/daemon state: the
|
|
15
|
+
// dormant state is status=idle / intent=idle-release / pi_pid dead (a
|
|
16
|
+
// released broker is a dead pid, the headless analog of a closed pane), and
|
|
17
|
+
// superviseTick's second pass gates on status===idle && intent===idle-release
|
|
18
|
+
// && !isPidAlive(pi_pid) && readInboxSince>cursor → reviveNode(resume). No
|
|
19
|
+
// pane is read anywhere; a broker is supervised purely by pid, so a
|
|
20
|
+
// fabricated row reproduces the exact dormant state with no process at all.
|
|
21
|
+
//
|
|
22
|
+
// (3) HOW THE FABRICATED DRIVE STILL FAILS IF THE BUG REGRESSES — pass 1: a tick
|
|
23
|
+
// with NO unseen inbox must NOT revive (cycles unchanged) — the idle-release
|
|
24
|
+
// early-return. Then we appendInbox an unseen entry and tick again; pass 2's
|
|
25
|
+
// reviveNode bumps cycles + transition('revive') BEFORE the (detached,
|
|
26
|
+
// unawaited) broker spawn, so the wake is observable instantly via cycles +
|
|
27
|
+
// status. If the second-pass reviveNode is removed, the post-inbox tick never
|
|
28
|
+
// bumps cycles → the "revived" asserts go RED. Verified by commenting the
|
|
29
|
+
// second-pass reviveNode(resume) call (see bug-injection report).
|
|
6
30
|
import { test, before, after } from 'node:test';
|
|
7
31
|
import assert from 'node:assert/strict';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
32
|
+
import { spawnSync } from 'node:child_process';
|
|
33
|
+
import { createHarness } from './helpers/harness.js';
|
|
10
34
|
import { appendInbox } from '../feed/inbox.js';
|
|
11
|
-
import { isPidAlive } from '../canvas/pid.js';
|
|
12
35
|
let h;
|
|
13
|
-
let root;
|
|
14
36
|
before(async () => {
|
|
15
|
-
|
|
16
|
-
return;
|
|
17
|
-
h = await createHarness({ sessionPrefix: 'crtr-brkdorm' });
|
|
18
|
-
// An active resident root: the spawn parent AND the live node a child can hold
|
|
19
|
-
// an active subscription to (so its natural stop classifies 'awaiting').
|
|
20
|
-
root = h.spawnRoot('broker-dormant suite root');
|
|
37
|
+
h = await createHarness({ headless: true, sessionPrefix: 'crtr-brkdorm' });
|
|
21
38
|
});
|
|
22
39
|
after(async () => {
|
|
23
40
|
if (h !== undefined)
|
|
24
41
|
await h.dispose();
|
|
25
42
|
});
|
|
43
|
+
// A pid that is reaped (dead) by the time spawnSync returns — a released
|
|
44
|
+
// broker's exited process.
|
|
45
|
+
function deadPid() {
|
|
46
|
+
const r = spawnSync('true', [], { stdio: 'ignore' });
|
|
47
|
+
return r.pid ?? 0x7ffffffe;
|
|
48
|
+
}
|
|
26
49
|
// ===========================================================================
|
|
27
|
-
// Item 3 (DORMANT) — a
|
|
28
|
-
//
|
|
29
|
-
//
|
|
50
|
+
// Item 3 (DORMANT) — a node idle-RELEASED (its broker exited) is NOT revived by
|
|
51
|
+
// a bare tick, but the daemon's second pass revives it (RESUME) on the next
|
|
52
|
+
// unseen inbox entry.
|
|
30
53
|
// ===========================================================================
|
|
31
|
-
test('DORMANT wake — idle-release
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
54
|
+
test('DORMANT wake — idle-release broker (pi dead) → daemon pass-2 reviveNode(resume) on unseen inbox', async () => {
|
|
55
|
+
// Fabricate the post-release dormant state directly: status=idle,
|
|
56
|
+
// intent=idle-release, a DEAD supervised pid, pi_session_id set (so the revive
|
|
57
|
+
// RESUMES the saved session). No boot, no stop, no pane.
|
|
58
|
+
const id = h.fabricateBrokerNode({
|
|
59
|
+
status: 'idle',
|
|
60
|
+
intent: 'idle-release',
|
|
61
|
+
pi_pid: deadPid(),
|
|
62
|
+
pi_session_id: 'sess-dorm',
|
|
63
|
+
});
|
|
64
|
+
{
|
|
65
|
+
const n = h.node(id);
|
|
66
|
+
assert.equal(n.host_kind, 'broker', 'broker-hosted (paneless)');
|
|
67
|
+
assert.equal(n.status, 'idle', 'idle (released)');
|
|
68
|
+
assert.equal(n.intent, 'idle-release', 'intent=idle-release');
|
|
69
|
+
assert.equal(n.cycles ?? 0, 0, 'no revive yet');
|
|
70
|
+
}
|
|
71
|
+
// PASS 1 — a tick with NO unseen inbox must NOT revive: idle-release is
|
|
72
|
+
// dormancy by choice; the daemon leaves it until an inbox entry arrives.
|
|
73
|
+
await h.tick();
|
|
74
|
+
assert.equal(h.node(id).cycles ?? 0, 0, 'no unseen inbox → NOT revived (idle-release left dormant)');
|
|
75
|
+
assert.equal(h.node(id).status, 'idle', 'still idle after the empty tick');
|
|
76
|
+
// PASS 2 — an unseen inbox entry + a tick → the daemon owns wake-on-message for
|
|
77
|
+
// a dormant (pi-dead) broker: second pass → reviveNode(resume:true).
|
|
44
78
|
appendInbox(id, { from: 'tester', tier: 'normal', kind: 'message', label: 'resume-me', data: { body: 'work after release' } });
|
|
45
79
|
await h.tick();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
80
|
+
// reviveNode bumps cycles + transition('revive') BEFORE the (detached,
|
|
81
|
+
// unawaited) broker spawn, so the wake is observable instantly.
|
|
82
|
+
{
|
|
83
|
+
const n = h.node(id);
|
|
84
|
+
assert.equal(n.cycles ?? 0, 1, 'unseen inbox + tick → daemon pass-2 REVIVED (cycles bumped to 1)');
|
|
85
|
+
assert.equal(n.status, 'active', 'revive → active (transition(revive))');
|
|
86
|
+
assert.equal(n.intent ?? null, null, 'intent cleared by the revive');
|
|
87
|
+
}
|
|
51
88
|
});
|
|
@@ -13,11 +13,13 @@ import registerCanvasInboxWatcher from '../../pi-extensions/canvas-inbox-watcher
|
|
|
13
13
|
import { appendInbox } from '../feed/inbox.js';
|
|
14
14
|
import { createNode, setIntent } from '../canvas/canvas.js';
|
|
15
15
|
import { closeDb } from '../canvas/db.js';
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
16
|
+
// Drive the watcher's injectable cadence seam (CRTR_WATCHER_TICK_MS /
|
|
17
|
+
// CRTR_WATCHER_DEBOUNCE_MS) at a fast tempo so the test sleeps milliseconds, not
|
|
18
|
+
// seconds. SETTLE_MS still allows a resolve+seed tick, a read tick, and the
|
|
19
|
+
// debounce window before asserting, exactly as against the real 800/1000 cadence.
|
|
20
|
+
const TICK_MS = 20;
|
|
21
|
+
const DEBOUNCE_MS = 25;
|
|
22
|
+
const SETTLE_MS = TICK_MS * 2 + DEBOUNCE_MS + 30;
|
|
21
23
|
let origHome;
|
|
22
24
|
let origNode;
|
|
23
25
|
const homes = [];
|
|
@@ -40,9 +42,15 @@ function makeFakePi() {
|
|
|
40
42
|
};
|
|
41
43
|
}
|
|
42
44
|
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
45
|
+
let origTick;
|
|
46
|
+
let origDebounce;
|
|
43
47
|
before(() => {
|
|
44
48
|
origHome = process.env['CRTR_HOME'];
|
|
45
49
|
origNode = process.env['CRTR_NODE_ID'];
|
|
50
|
+
origTick = process.env['CRTR_WATCHER_TICK_MS'];
|
|
51
|
+
origDebounce = process.env['CRTR_WATCHER_DEBOUNCE_MS'];
|
|
52
|
+
process.env['CRTR_WATCHER_TICK_MS'] = String(TICK_MS);
|
|
53
|
+
process.env['CRTR_WATCHER_DEBOUNCE_MS'] = String(DEBOUNCE_MS);
|
|
46
54
|
});
|
|
47
55
|
afterEach(() => {
|
|
48
56
|
while (disposers.length > 0)
|
|
@@ -57,6 +65,14 @@ after(() => {
|
|
|
57
65
|
delete process.env['CRTR_NODE_ID'];
|
|
58
66
|
else
|
|
59
67
|
process.env['CRTR_NODE_ID'] = origNode;
|
|
68
|
+
if (origTick === undefined)
|
|
69
|
+
delete process.env['CRTR_WATCHER_TICK_MS'];
|
|
70
|
+
else
|
|
71
|
+
process.env['CRTR_WATCHER_TICK_MS'] = origTick;
|
|
72
|
+
if (origDebounce === undefined)
|
|
73
|
+
delete process.env['CRTR_WATCHER_DEBOUNCE_MS'];
|
|
74
|
+
else
|
|
75
|
+
process.env['CRTR_WATCHER_DEBOUNCE_MS'] = origDebounce;
|
|
60
76
|
for (const h of homes) {
|
|
61
77
|
try {
|
|
62
78
|
rmSync(h, { recursive: true, force: true });
|
|
@@ -13,11 +13,13 @@ import { tmpdir } from 'node:os';
|
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import registerCanvasInboxWatcher from '../../pi-extensions/canvas-inbox-watcher.js';
|
|
15
15
|
import { appendInbox } from '../feed/inbox.js';
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
16
|
+
// Drive the watcher's injectable cadence seam (CRTR_WATCHER_TICK_MS /
|
|
17
|
+
// CRTR_WATCHER_DEBOUNCE_MS) at a fast tempo so the test sleeps milliseconds, not
|
|
18
|
+
// seconds. SETTLE_MS still allows a resolve+seed tick, a read tick, and the
|
|
19
|
+
// debounce window before asserting, exactly as against the real 800/1000 cadence.
|
|
20
|
+
const TICK_MS = 20;
|
|
21
|
+
const DEBOUNCE_MS = 25;
|
|
22
|
+
const SETTLE_MS = TICK_MS * 2 + DEBOUNCE_MS + 30;
|
|
21
23
|
let origHome;
|
|
22
24
|
let origNode;
|
|
23
25
|
const homes = [];
|
|
@@ -40,9 +42,15 @@ function makeFakePi() {
|
|
|
40
42
|
};
|
|
41
43
|
}
|
|
42
44
|
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
45
|
+
let origTick;
|
|
46
|
+
let origDebounce;
|
|
43
47
|
before(() => {
|
|
44
48
|
origHome = process.env['CRTR_HOME'];
|
|
45
49
|
origNode = process.env['CRTR_NODE_ID'];
|
|
50
|
+
origTick = process.env['CRTR_WATCHER_TICK_MS'];
|
|
51
|
+
origDebounce = process.env['CRTR_WATCHER_DEBOUNCE_MS'];
|
|
52
|
+
process.env['CRTR_WATCHER_TICK_MS'] = String(TICK_MS);
|
|
53
|
+
process.env['CRTR_WATCHER_DEBOUNCE_MS'] = String(DEBOUNCE_MS);
|
|
46
54
|
});
|
|
47
55
|
afterEach(() => {
|
|
48
56
|
while (disposers.length > 0)
|
|
@@ -57,6 +65,14 @@ after(() => {
|
|
|
57
65
|
delete process.env['CRTR_NODE_ID'];
|
|
58
66
|
else
|
|
59
67
|
process.env['CRTR_NODE_ID'] = origNode;
|
|
68
|
+
if (origTick === undefined)
|
|
69
|
+
delete process.env['CRTR_WATCHER_TICK_MS'];
|
|
70
|
+
else
|
|
71
|
+
process.env['CRTR_WATCHER_TICK_MS'] = origTick;
|
|
72
|
+
if (origDebounce === undefined)
|
|
73
|
+
delete process.env['CRTR_WATCHER_DEBOUNCE_MS'];
|
|
74
|
+
else
|
|
75
|
+
process.env['CRTR_WATCHER_DEBOUNCE_MS'] = origDebounce;
|
|
60
76
|
for (const h of homes) {
|
|
61
77
|
try {
|
|
62
78
|
rmSync(h, { recursive: true, force: true });
|