@crouton-kit/crouter 0.3.11 → 0.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/bin/crtrd +2 -0
  2. package/dist/builtin-personas/design/base.md +9 -0
  3. package/dist/builtin-personas/design/orchestrator.md +10 -0
  4. package/dist/builtin-personas/developer/base.md +9 -0
  5. package/dist/builtin-personas/developer/orchestrator.md +12 -0
  6. package/dist/builtin-personas/explore/base.md +9 -0
  7. package/dist/builtin-personas/explore/orchestrator.md +9 -0
  8. package/dist/builtin-personas/general/base.md +5 -0
  9. package/dist/builtin-personas/general/orchestrator.md +7 -0
  10. package/dist/builtin-personas/orchestration-kernel.md +71 -0
  11. package/dist/builtin-personas/plan/base.md +7 -0
  12. package/dist/builtin-personas/plan/orchestrator.md +12 -0
  13. package/dist/builtin-personas/review/base.md +7 -0
  14. package/dist/builtin-personas/review/orchestrator.md +9 -0
  15. package/dist/builtin-personas/runtime-base.md +39 -0
  16. package/dist/builtin-personas/spec/base.md +7 -0
  17. package/dist/builtin-personas/spec/orchestrator.md +10 -0
  18. package/dist/builtin-skills/skills/design/SKILL.md +51 -0
  19. package/dist/builtin-skills/skills/development/SKILL.md +109 -0
  20. package/dist/builtin-skills/skills/planning/SKILL.md +59 -0
  21. package/dist/builtin-skills/skills/spec/SKILL.md +83 -0
  22. package/dist/cli.js +14 -6
  23. package/dist/commands/{mode.d.ts → attention.d.ts} +1 -1
  24. package/dist/commands/attention.js +152 -0
  25. package/dist/commands/canvas.d.ts +2 -0
  26. package/dist/commands/canvas.js +35 -0
  27. package/dist/commands/daemon.d.ts +2 -0
  28. package/dist/commands/daemon.js +111 -0
  29. package/dist/commands/dashboard.d.ts +2 -0
  30. package/dist/commands/dashboard.js +65 -0
  31. package/dist/commands/human/prompts.d.ts +5 -0
  32. package/dist/commands/human/prompts.js +269 -0
  33. package/dist/commands/human/queue.d.ts +3 -0
  34. package/dist/commands/human/queue.js +133 -0
  35. package/dist/commands/human/shared.d.ts +43 -0
  36. package/dist/commands/human/shared.js +107 -0
  37. package/dist/commands/human.js +10 -454
  38. package/dist/commands/node.d.ts +2 -0
  39. package/dist/commands/node.js +407 -0
  40. package/dist/commands/pkg/market-inspect.d.ts +1 -0
  41. package/dist/commands/pkg/market-inspect.js +157 -0
  42. package/dist/commands/pkg/market-manage.d.ts +1 -0
  43. package/dist/commands/pkg/market-manage.js +316 -0
  44. package/dist/commands/pkg/market.d.ts +1 -0
  45. package/dist/commands/pkg/market.js +16 -0
  46. package/dist/commands/pkg/plugin-inspect.d.ts +1 -0
  47. package/dist/commands/pkg/plugin-inspect.js +142 -0
  48. package/dist/commands/pkg/plugin-manage.d.ts +1 -0
  49. package/dist/commands/pkg/plugin-manage.js +294 -0
  50. package/dist/commands/pkg/plugin.d.ts +1 -0
  51. package/dist/commands/pkg/plugin.js +16 -0
  52. package/dist/commands/pkg/shared.d.ts +5 -0
  53. package/dist/commands/pkg/shared.js +61 -0
  54. package/dist/commands/pkg.js +3 -1004
  55. package/dist/commands/push.d.ts +3 -0
  56. package/dist/commands/push.js +159 -0
  57. package/dist/commands/revive.d.ts +2 -0
  58. package/dist/commands/revive.js +64 -0
  59. package/dist/commands/skill/author.d.ts +3 -0
  60. package/dist/commands/skill/author.js +147 -0
  61. package/dist/commands/skill/find.d.ts +4 -0
  62. package/dist/commands/skill/find.js +254 -0
  63. package/dist/commands/skill/read.d.ts +1 -0
  64. package/dist/commands/skill/read.js +89 -0
  65. package/dist/commands/skill/shared.d.ts +19 -0
  66. package/dist/commands/skill/shared.js +207 -0
  67. package/dist/commands/skill/state.d.ts +3 -0
  68. package/dist/commands/skill/state.js +69 -0
  69. package/dist/commands/skill.js +6 -691
  70. package/dist/commands/sys/config.d.ts +1 -0
  71. package/dist/commands/sys/config.js +186 -0
  72. package/dist/commands/sys/doctor.d.ts +1 -0
  73. package/dist/commands/sys/doctor.js +369 -0
  74. package/dist/commands/sys/shared.d.ts +3 -0
  75. package/dist/commands/sys/shared.js +24 -0
  76. package/dist/commands/sys/update.d.ts +2 -0
  77. package/dist/commands/sys/update.js +114 -0
  78. package/dist/commands/sys.js +4 -694
  79. package/dist/core/__tests__/argv-parser.test.js +19 -1
  80. package/dist/core/__tests__/canvas-inbox-watcher.test.js +100 -0
  81. package/dist/core/__tests__/canvas.test.js +154 -0
  82. package/dist/core/__tests__/reset.test.js +105 -0
  83. package/dist/core/canvas/attention.d.ts +24 -0
  84. package/dist/core/canvas/attention.js +94 -0
  85. package/dist/core/canvas/canvas.d.ts +40 -0
  86. package/dist/core/canvas/canvas.js +210 -0
  87. package/dist/core/canvas/db.d.ts +7 -0
  88. package/dist/core/canvas/db.js +61 -0
  89. package/dist/core/canvas/index.d.ts +4 -0
  90. package/dist/core/canvas/index.js +6 -0
  91. package/dist/core/canvas/paths.d.ts +16 -0
  92. package/dist/core/canvas/paths.js +62 -0
  93. package/dist/core/canvas/render.d.ts +30 -0
  94. package/dist/core/canvas/render.js +186 -0
  95. package/dist/core/canvas/types.d.ts +87 -0
  96. package/dist/core/canvas/types.js +8 -0
  97. package/dist/core/command.d.ts +5 -0
  98. package/dist/core/command.js +35 -10
  99. package/dist/core/feed/feed.d.ts +43 -0
  100. package/dist/core/feed/feed.js +116 -0
  101. package/dist/core/feed/inbox.d.ts +50 -0
  102. package/dist/core/feed/inbox.js +124 -0
  103. package/dist/core/help.js +5 -3
  104. package/dist/core/io.d.ts +15 -1
  105. package/dist/core/io.js +56 -6
  106. package/dist/core/personas/index.d.ts +12 -0
  107. package/dist/core/personas/index.js +10 -0
  108. package/dist/core/personas/loader.d.ts +44 -0
  109. package/dist/core/personas/loader.js +157 -0
  110. package/dist/core/personas/resolve.d.ts +36 -0
  111. package/dist/core/personas/resolve.js +110 -0
  112. package/dist/core/render.d.ts +11 -0
  113. package/dist/core/render.js +126 -0
  114. package/dist/core/resolver.d.ts +10 -0
  115. package/dist/core/resolver.js +109 -1
  116. package/dist/core/runtime/front-door.d.ts +10 -0
  117. package/dist/core/runtime/front-door.js +97 -0
  118. package/dist/core/runtime/kickoff.d.ts +23 -0
  119. package/dist/core/runtime/kickoff.js +134 -0
  120. package/dist/core/runtime/launch.d.ts +34 -0
  121. package/dist/core/runtime/launch.js +85 -0
  122. package/dist/core/runtime/nodes.d.ts +38 -0
  123. package/dist/core/runtime/nodes.js +95 -0
  124. package/dist/core/runtime/presence.d.ts +55 -0
  125. package/dist/core/runtime/presence.js +198 -0
  126. package/dist/core/runtime/promote.d.ts +30 -0
  127. package/dist/core/runtime/promote.js +105 -0
  128. package/dist/core/runtime/reset.d.ts +13 -0
  129. package/dist/core/runtime/reset.js +97 -0
  130. package/dist/core/runtime/revive.d.ts +26 -0
  131. package/dist/core/runtime/revive.js +87 -0
  132. package/dist/core/runtime/roadmap.d.ts +12 -0
  133. package/dist/core/runtime/roadmap.js +52 -0
  134. package/dist/core/runtime/spawn.d.ts +31 -0
  135. package/dist/core/runtime/spawn.js +123 -0
  136. package/dist/core/runtime/stop-guard.d.ts +18 -0
  137. package/dist/core/runtime/stop-guard.js +33 -0
  138. package/dist/core/runtime/tmux.d.ts +107 -0
  139. package/dist/core/runtime/tmux.js +244 -0
  140. package/dist/core/spawn.d.ts +17 -197
  141. package/dist/core/spawn.js +16 -539
  142. package/dist/daemon/crtrd-cli.js +4 -0
  143. package/dist/daemon/crtrd.d.ts +20 -0
  144. package/dist/daemon/crtrd.js +200 -0
  145. package/dist/daemon/manage.d.ts +17 -0
  146. package/dist/daemon/manage.js +57 -0
  147. package/dist/pi-extensions/canvas-inbox-watcher.d.ts +16 -0
  148. package/dist/pi-extensions/canvas-inbox-watcher.js +229 -0
  149. package/dist/pi-extensions/canvas-nav.d.ts +32 -0
  150. package/dist/pi-extensions/canvas-nav.js +536 -0
  151. package/dist/pi-extensions/canvas-stophook.d.ts +17 -0
  152. package/dist/pi-extensions/canvas-stophook.js +396 -0
  153. package/package.json +6 -5
  154. package/dist/commands/agent.d.ts +0 -6
  155. package/dist/commands/agent.js +0 -585
  156. package/dist/commands/debug.d.ts +0 -3
  157. package/dist/commands/debug.js +0 -192
  158. package/dist/commands/job.d.ts +0 -11
  159. package/dist/commands/job.js +0 -384
  160. package/dist/commands/mode.js +0 -231
  161. package/dist/commands/plan.d.ts +0 -4
  162. package/dist/commands/plan.js +0 -322
  163. package/dist/commands/spec.d.ts +0 -3
  164. package/dist/commands/spec.js +0 -299
  165. package/dist/core/__tests__/flow-leaves.test.js +0 -248
  166. package/dist/core/__tests__/job.test.js +0 -310
  167. package/dist/core/__tests__/jobs.test.js +0 -98
  168. package/dist/core/__tests__/spawn.test.js +0 -138
  169. package/dist/core/__tests__/subagents.test.d.ts +0 -1
  170. package/dist/core/__tests__/subagents.test.js +0 -75
  171. package/dist/core/jobs.d.ts +0 -107
  172. package/dist/core/jobs.js +0 -565
  173. package/dist/core/subagents.d.ts +0 -18
  174. package/dist/core/subagents.js +0 -163
  175. package/dist/prompts/agent.d.ts +0 -27
  176. package/dist/prompts/agent.js +0 -184
  177. package/dist/prompts/debug.d.ts +0 -8
  178. package/dist/prompts/debug.js +0 -44
  179. /package/dist/core/__tests__/{flow-leaves.test.d.ts → canvas-inbox-watcher.test.d.ts} +0 -0
  180. /package/dist/core/__tests__/{job.test.d.ts → canvas.test.d.ts} +0 -0
  181. /package/dist/core/__tests__/{jobs.test.d.ts → reset.test.d.ts} +0 -0
  182. /package/dist/{core/__tests__/spawn.test.d.ts → daemon/crtrd-cli.d.ts} +0 -0
@@ -0,0 +1 @@
1
+ export declare const configBranch: import("../../core/command.js").BranchDef;
@@ -0,0 +1,186 @@
1
+ import { defineBranch, defineLeaf } from '../../core/command.js';
2
+ import { readConfig, writeConfig, configPath as coreConfigPath } from '../../core/config.js';
3
+ import { usage, notFound } from '../../core/errors.js';
4
+ import { scopeRoot, listScopes } from '../../core/scope.js';
5
+ import { resolveScope } from './shared.js';
6
+ // ---------------------------------------------------------------------------
7
+ // Config helpers (ported from commands/config.ts)
8
+ // ---------------------------------------------------------------------------
9
+ const TOP_LEVEL_KEYS = new Set([
10
+ 'auto_update',
11
+ 'marketplaces',
12
+ 'plugins',
13
+ 'max_panes_per_window',
14
+ ]);
15
+ function getNestedValue(obj, key) {
16
+ const parts = key.split('.');
17
+ let current = obj;
18
+ for (const part of parts) {
19
+ if (current === null || typeof current !== 'object')
20
+ return undefined;
21
+ current = current[part];
22
+ }
23
+ return current;
24
+ }
25
+ function parseConfigValue(raw) {
26
+ if (raw === 'true')
27
+ return true;
28
+ if (raw === 'false')
29
+ return false;
30
+ if (/^-?\d+$/.test(raw))
31
+ return parseInt(raw, 10);
32
+ return raw;
33
+ }
34
+ function setNestedValue(cfg, key, value) {
35
+ const parts = key.split('.');
36
+ const topKey = parts[0];
37
+ if (!TOP_LEVEL_KEYS.has(topKey)) {
38
+ throw usage(`unknown config key: ${topKey} (expected: ${[...TOP_LEVEL_KEYS].join('|')})`);
39
+ }
40
+ if (key === 'auto_update.content') {
41
+ if (value !== 'notify' && value !== 'apply' && value !== false) {
42
+ throw usage(`auto_update.content must be 'notify', 'apply', or false`);
43
+ }
44
+ cfg.auto_update.content = value;
45
+ return;
46
+ }
47
+ if (key === 'auto_update.crtr') {
48
+ const coerced = value === true ? 'notify' : value;
49
+ if (coerced !== 'notify' && coerced !== 'apply' && coerced !== false) {
50
+ throw usage(`auto_update.crtr must be 'notify', 'apply', or false`);
51
+ }
52
+ cfg.auto_update.crtr = coerced;
53
+ return;
54
+ }
55
+ if (key === 'max_panes_per_window') {
56
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 1) {
57
+ throw usage(`max_panes_per_window must be an integer >= 1`);
58
+ }
59
+ cfg.max_panes_per_window = Math.floor(value);
60
+ return;
61
+ }
62
+ if (parts.length === 1) {
63
+ cfg[topKey] = value;
64
+ return;
65
+ }
66
+ if (parts.length === 2 && topKey === 'auto_update') {
67
+ const subKey = parts[1];
68
+ cfg.auto_update[subKey] = value;
69
+ return;
70
+ }
71
+ throw usage(`unsupported key path for set: ${key}`);
72
+ }
73
+ // ---------------------------------------------------------------------------
74
+ // Leaf definitions
75
+ // ---------------------------------------------------------------------------
76
+ const configGet = defineLeaf({
77
+ name: 'get',
78
+ help: {
79
+ name: 'sys config get',
80
+ summary: 'read a config value by dotted key',
81
+ params: [
82
+ { kind: 'positional', name: 'key', type: 'string', required: true, constraint: 'Dotted key path. Top-level keys: auto_update, marketplaces, plugins, max_panes_per_window.' },
83
+ { kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project', 'all'], required: false, constraint: 'Scope to read from. Default: user.' },
84
+ ],
85
+ output: [
86
+ { name: 'key', type: 'string', required: true, constraint: 'Echo of input key.' },
87
+ { name: 'value', type: 'unknown', required: true, constraint: 'The resolved value. Type depends on the key.' },
88
+ { name: 'scope', type: 'string', required: true, constraint: 'Scope the value was read from.' },
89
+ ],
90
+ outputKind: 'object',
91
+ effects: ['None. Read-only.'],
92
+ },
93
+ run: async (input) => {
94
+ const key = input['key'];
95
+ const scope = resolveScope(input['scope']);
96
+ const cfg = readConfig(scope);
97
+ const value = getNestedValue(cfg, key);
98
+ if (value === undefined) {
99
+ throw notFound(`config key not found: ${key}`);
100
+ }
101
+ return { key, value: value, scope };
102
+ },
103
+ });
104
+ const configSet = defineLeaf({
105
+ name: 'set',
106
+ help: {
107
+ name: 'sys config set',
108
+ summary: 'write a config value by dotted key',
109
+ params: [
110
+ { kind: 'positional', name: 'key', type: 'string', required: true, constraint: 'Dotted key path. Supported: auto_update.crtr, auto_update.content, auto_update.interval_hours, max_panes_per_window.' },
111
+ { kind: 'flag', name: 'value', type: 'string', required: true, constraint: 'value VALUE — string, required. Stored as-is if quoted; coerced to number or boolean when unambiguous.' },
112
+ { kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Scope to write to. Default: user.' },
113
+ ],
114
+ output: [
115
+ { name: 'key', type: 'string', required: true, constraint: 'Echo of input key.' },
116
+ { name: 'value', type: 'unknown', required: true, constraint: 'Value as written.' },
117
+ { name: 'scope', type: 'string', required: true, constraint: 'Scope the value was written to.' },
118
+ ],
119
+ outputKind: 'object',
120
+ effects: ['Writes the updated value to config.json in the target scope.'],
121
+ },
122
+ run: async (input) => {
123
+ const key = input['key'];
124
+ const rawValue = input['value'];
125
+ const scope = resolveScope(input['scope']);
126
+ // Flags are stringly-typed; coerce to number or boolean when unambiguous
127
+ const parsed = parseConfigValue(rawValue);
128
+ const cfg = readConfig(scope);
129
+ setNestedValue(cfg, key, parsed);
130
+ writeConfig(scope, cfg);
131
+ // Read back the written value for echo
132
+ const written = getNestedValue(cfg, key);
133
+ return { key, value: written, scope };
134
+ },
135
+ });
136
+ const configPath = defineLeaf({
137
+ name: 'path',
138
+ help: {
139
+ name: 'sys config path',
140
+ summary: 'print absolute path(s) to config.json',
141
+ params: [
142
+ { kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project', 'all'], required: false, constraint: 'Scope to show paths for. Default: all.' },
143
+ ],
144
+ output: [
145
+ { name: 'paths', type: 'object[]', required: true, constraint: 'Each: {scope, path}. Only includes scopes that have a config file.' },
146
+ ],
147
+ outputKind: 'object',
148
+ effects: ['None. Read-only.'],
149
+ },
150
+ run: async (input) => {
151
+ const scopeArg = input['scope'];
152
+ // Resolve 'all' or undefined → all writable scopes
153
+ let scopes;
154
+ if (scopeArg === undefined || scopeArg === 'all') {
155
+ scopes = listScopes(undefined);
156
+ }
157
+ else {
158
+ scopes = listScopes(scopeArg);
159
+ }
160
+ const paths = scopes
161
+ .map((s) => {
162
+ const root = scopeRoot(s);
163
+ if (!root)
164
+ return null;
165
+ const p = coreConfigPath(s);
166
+ if (!p)
167
+ return null;
168
+ return { scope: s, path: p };
169
+ })
170
+ .filter((x) => x !== null);
171
+ return { paths };
172
+ },
173
+ });
174
+ export const configBranch = defineBranch({
175
+ name: 'config',
176
+ help: {
177
+ name: 'sys config',
178
+ summary: 'read and write crtr configuration',
179
+ children: [
180
+ { name: 'get', desc: 'read a config value by key', useWhen: 'inspecting current configuration' },
181
+ { name: 'set', desc: 'write a config value by key', useWhen: 'changing a configuration setting' },
182
+ { name: 'path', desc: 'print path(s) to config.json', useWhen: 'locating the config file for manual inspection' },
183
+ ],
184
+ },
185
+ children: [configGet, configSet, configPath],
186
+ });
@@ -0,0 +1 @@
1
+ export declare const sysDoctorLeaf: import("../../core/command.js").LeafDef;
@@ -0,0 +1,369 @@
1
+ import { join } from 'node:path';
2
+ import { defineLeaf } from '../../core/command.js';
3
+ import { readConfig, updateConfig } from '../../core/config.js';
4
+ import { scopeRoot, listScopes, builtinSkillsRoot, marketplacesDir, pluginsDir } from '../../core/scope.js';
5
+ import { listInstalledPlugins, listSkillsInPlugin } from '../../core/resolver.js';
6
+ import { readMarketplaceManifest, readPluginManifest } from '../../core/manifest.js';
7
+ import { parseFrontmatter } from '../../core/frontmatter.js';
8
+ import { pathExists, listDirs, removePath, readText, writeText } from '../../core/fs-utils.js';
9
+ import { lsRemote } from '../../core/git.js';
10
+ import { SKILL_TYPES, isSkillType } from '../../types.js';
11
+ function pass(scope, name, message) {
12
+ return { scope, name, status: 'pass', message };
13
+ }
14
+ function failCheck(scope, name, message, remediation) {
15
+ return { scope, name, status: 'fail', message, ...(remediation ? { remediation } : {}) };
16
+ }
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
+ /**
53
+ * Apply a remediation. Returns true if applied successfully. Idempotent for
54
+ * the supported kinds (re-applying a config-key removal that's already gone
55
+ * returns true).
56
+ */
57
+ function applyRemediation(rem) {
58
+ try {
59
+ switch (rem.kind) {
60
+ case 'remove_config_key': {
61
+ if (!rem.scope || !rem.configKey)
62
+ return false;
63
+ const segments = rem.configKey.split('.');
64
+ updateConfig(rem.scope, (c) => {
65
+ let cursor = c;
66
+ for (let i = 0; i < segments.length - 1; i++) {
67
+ const next = cursor[segments[i]];
68
+ if (typeof next !== 'object' || next === null)
69
+ return;
70
+ cursor = next;
71
+ }
72
+ delete cursor[segments[segments.length - 1]];
73
+ });
74
+ return true;
75
+ }
76
+ case 'rm_path': {
77
+ if (!rem.path)
78
+ return false;
79
+ removePath(rem.path);
80
+ return true;
81
+ }
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
+ default:
88
+ return false;
89
+ }
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }
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
+ function runChecksForScope(scope, opts) {
133
+ if (scope === 'builtin')
134
+ return runChecksForBuiltin();
135
+ const results = [];
136
+ const root = scopeRoot(scope);
137
+ if (!root)
138
+ return results;
139
+ const cfg = readConfig(scope);
140
+ // Check: every config marketplace entry has a corresponding directory
141
+ const mktDir = marketplacesDir(scope);
142
+ for (const name of Object.keys(cfg.marketplaces)) {
143
+ if (!mktDir) {
144
+ results.push(failCheck(scope, `marketplace:${name}:dir`, `marketplaces directory unavailable`));
145
+ continue;
146
+ }
147
+ const dir = join(mktDir, name);
148
+ if (!pathExists(dir)) {
149
+ const remediation = {
150
+ kind: 'remove_config_key',
151
+ description: `Drop stale config entry config.${scope}.marketplaces.${name}`,
152
+ scope,
153
+ configKey: `marketplaces.${name}`,
154
+ };
155
+ if (opts.fix && applyRemediation(remediation)) {
156
+ results.push({ scope, name: `marketplace:${name}:dir`, status: 'fail', message: `directory missing — removed stale config entry`, fixed: true, remediation });
157
+ }
158
+ else {
159
+ results.push(failCheck(scope, `marketplace:${name}:dir`, `directory missing: ${dir}`, remediation));
160
+ }
161
+ }
162
+ else {
163
+ results.push(pass(scope, `marketplace:${name}:dir`, `directory exists`));
164
+ }
165
+ }
166
+ // Check: every config plugin entry has a corresponding directory
167
+ const plugDir = pluginsDir(scope);
168
+ for (const name of Object.keys(cfg.plugins)) {
169
+ if (!plugDir) {
170
+ results.push(failCheck(scope, `plugin:${name}:dir`, `plugins directory unavailable`));
171
+ continue;
172
+ }
173
+ const dir = join(plugDir, name);
174
+ if (!pathExists(dir)) {
175
+ const remediation = {
176
+ kind: 'remove_config_key',
177
+ description: `Drop stale config entry config.${scope}.plugins.${name}`,
178
+ scope,
179
+ configKey: `plugins.${name}`,
180
+ };
181
+ if (opts.fix && applyRemediation(remediation)) {
182
+ results.push({ scope, name: `plugin:${name}:dir`, status: 'fail', message: `directory missing — removed stale config entry`, fixed: true, remediation });
183
+ }
184
+ else {
185
+ results.push(failCheck(scope, `plugin:${name}:dir`, `directory missing: ${dir}`, remediation));
186
+ }
187
+ }
188
+ else {
189
+ results.push(pass(scope, `plugin:${name}:dir`, `directory exists`));
190
+ }
191
+ }
192
+ // Check: every marketplace directory has a valid manifest
193
+ if (mktDir && pathExists(mktDir)) {
194
+ for (const name of listDirs(mktDir)) {
195
+ const dir = join(mktDir, name);
196
+ const manifest = readMarketplaceManifest(dir);
197
+ if (!manifest) {
198
+ const remediation = {
199
+ kind: 'rm_path',
200
+ description: `Remove dangling marketplace directory (no valid .crouter-marketplace/marketplace.json)`,
201
+ path: dir,
202
+ };
203
+ if (opts.fix && applyRemediation(remediation)) {
204
+ results.push({ scope, name: `marketplace:${name}:manifest`, status: 'fail', message: `no valid marketplace.json — removed dangling directory`, fixed: true, remediation });
205
+ }
206
+ else {
207
+ results.push(failCheck(scope, `marketplace:${name}:manifest`, `no valid marketplace.json in ${dir}`, remediation));
208
+ }
209
+ }
210
+ else {
211
+ results.push(pass(scope, `marketplace:${name}:manifest`, `manifest valid`));
212
+ // Check: marketplace plugins[].source paths resolve (relative paths only)
213
+ for (const entry of manifest.plugins) {
214
+ if (entry.source.startsWith('http://') || entry.source.startsWith('https://') || entry.source.startsWith('git@')) {
215
+ continue;
216
+ }
217
+ const resolved = join(dir, entry.source);
218
+ if (!pathExists(resolved)) {
219
+ results.push(failCheck(scope, `marketplace:${name}:plugin-source:${entry.name}`, `source path does not resolve: ${resolved}`));
220
+ }
221
+ else {
222
+ results.push(pass(scope, `marketplace:${name}:plugin-source:${entry.name}`, `source resolves`));
223
+ }
224
+ }
225
+ }
226
+ }
227
+ }
228
+ // Check: every plugin directory has a valid manifest + no duplicate names
229
+ const seenPluginNames = new Map();
230
+ if (plugDir && pathExists(plugDir)) {
231
+ for (const name of listDirs(plugDir)) {
232
+ const dir = join(plugDir, name);
233
+ const manifest = readPluginManifest(dir);
234
+ if (!manifest) {
235
+ const remediation = {
236
+ kind: 'rm_path',
237
+ description: `Remove dangling plugin directory (no valid .crouter-plugin/plugin.json)`,
238
+ path: dir,
239
+ };
240
+ if (opts.fix && applyRemediation(remediation)) {
241
+ results.push({ scope, name: `plugin:${name}:manifest`, status: 'fail', message: `no valid plugin.json — removed dangling directory`, fixed: true, remediation });
242
+ }
243
+ else {
244
+ results.push(failCheck(scope, `plugin:${name}:manifest`, `no valid plugin.json in ${dir}`, remediation));
245
+ }
246
+ continue;
247
+ }
248
+ results.push(pass(scope, `plugin:${name}:manifest`, `manifest valid`));
249
+ // Duplicate names
250
+ if (seenPluginNames.has(name)) {
251
+ results.push(failCheck(scope, `plugin:${name}:duplicate`, `duplicate plugin name within scope (also at ${seenPluginNames.get(name)})`));
252
+ }
253
+ else {
254
+ seenPluginNames.set(name, dir);
255
+ }
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
+ // Git remote check (slow, opt-in)
313
+ if (opts.remote && manifest.source) {
314
+ const res = lsRemote(manifest.source);
315
+ if (res.status !== 0) {
316
+ results.push(failCheck(scope, `plugin:${name}:remote`, `git remote unreachable: ${manifest.source}`));
317
+ }
318
+ else {
319
+ results.push(pass(scope, `plugin:${name}:remote`, `git remote reachable`));
320
+ }
321
+ }
322
+ }
323
+ }
324
+ return results;
325
+ }
326
+ export const sysDoctorLeaf = defineLeaf({
327
+ name: 'doctor',
328
+ help: {
329
+ name: 'sys doctor',
330
+ summary: 'diagnose missing manifests, broken config entries, and skill frontmatter drift',
331
+ params: [
332
+ { kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Scope to check. Default: all scopes.' },
333
+ { kind: 'flag', name: 'fix', type: 'bool', required: false, constraint: 'Apply each non-pass check\'s remediation and report what was fixed.' },
334
+ { kind: 'flag', name: 'remote', type: 'bool', required: false, constraint: 'Check git remotes with ls-remote (slow — makes network calls).' },
335
+ ],
336
+ output: [
337
+ { name: 'checks', type: 'object[]', required: true, constraint: 'Each: {scope, name, status, message, fixed?, remediation?}. status: pass | fail | warn. remediation (when present) is {kind, description, ...payload} where kind is remove_config_key | rm_path | edit_frontmatter. Sorted by scope then name.' },
338
+ { name: 'ok', type: 'boolean', required: true, constraint: 'True when no unresolved fail checks remain.' },
339
+ ],
340
+ outputKind: 'object',
341
+ effects: [
342
+ 'Read-only unless --fix is passed.',
343
+ 'With --fix: applies each non-pass check\'s `remediation` — removes stale config entries, deletes dangling plugin/marketplace directories, edits frontmatter name fields to the base-name convention.',
344
+ '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.',
345
+ ],
346
+ },
347
+ run: async (input) => {
348
+ const scopeArg = input['scope'];
349
+ const fix = input['fix'];
350
+ const remote = input['remote'];
351
+ const scopes = listScopes(scopeArg);
352
+ const allResults = [];
353
+ for (const scope of scopes) {
354
+ const results = runChecksForScope(scope, { fix, remote });
355
+ allResults.push(...results);
356
+ }
357
+ // Sort by scope then name (mirrors old printResults grouping)
358
+ allResults.sort((a, b) => {
359
+ if (a.scope !== b.scope)
360
+ return a.scope.localeCompare(b.scope);
361
+ return a.name.localeCompare(b.name);
362
+ });
363
+ const ok = !allResults.some((r) => r.status === 'fail' && r.fixed !== true);
364
+ // failed count = unresolved fails
365
+ const failed = allResults.filter((r) => r.status === 'fail' && r.fixed !== true).length;
366
+ // The skeleton schema declared `ok` not `failed`; returning both for completeness
367
+ return { checks: allResults, ok, failed };
368
+ },
369
+ });
@@ -0,0 +1,3 @@
1
+ import type { Scope } from '../../types.js';
2
+ export declare function readPackageVersion(): string;
3
+ export declare function resolveScope(raw: string | undefined): Scope;
@@ -0,0 +1,24 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { join, dirname } from 'node:path';
3
+ import { readFileSync } from 'node:fs';
4
+ import { usage } from '../../core/errors.js';
5
+ // ---------------------------------------------------------------------------
6
+ // Package version
7
+ // ---------------------------------------------------------------------------
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ // shared.ts is one directory deeper than the original sys.ts was
11
+ // (dist/commands/sys/shared.js vs dist/commands/sys.js), so one extra '..'
12
+ const PKG_ROOT = join(__dirname, '..', '..', '..');
13
+ export function readPackageVersion() {
14
+ const raw = readFileSync(join(PKG_ROOT, 'package.json'), 'utf8');
15
+ const parsed = JSON.parse(raw);
16
+ return parsed.version;
17
+ }
18
+ export function resolveScope(raw) {
19
+ if (raw === undefined)
20
+ return 'user';
21
+ if (raw === 'user' || raw === 'project')
22
+ return raw;
23
+ throw usage(`scope must be 'user' or 'project', got: ${raw}`);
24
+ }
@@ -0,0 +1,2 @@
1
+ export declare const sysUpdateLeaf: import("../../core/command.js").LeafDef;
2
+ export declare const sysVersionLeaf: import("../../core/command.js").LeafDef;