@colin4k1024/tsp 2.5.2 → 2.5.4
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/bin/lib/install-surface.js +5 -0
- package/commands/goal.md +7 -1
- package/commands/heartbeat.md +34 -1
- package/commands/loop-start.md +34 -6
- package/commands/triage.md +8 -1
- package/hooks/README.md +2 -2
- package/hooks/harness-statusline.js +24 -22
- package/hooks/strategic-compact/README.md +11 -12
- package/manifests/install-components.json +40 -0
- package/manifests/install-modules.json +141 -31
- package/manifests/install-profiles.json +2 -0
- package/package.json +2 -1
- package/schemas/loop-spec.schema.json +124 -0
- package/scripts/hooks/pre-compact.js +39 -8
- package/scripts/hooks/session-start-goal-resume.js +3 -20
- package/scripts/hooks/suggest-compact.js +9 -74
- package/scripts/lib/completion-oracle.js +4 -27
- package/scripts/lib/context-window-state.js +129 -0
- package/scripts/lib/context-window.js +294 -0
- package/scripts/lib/heartbeat-scheduler.js +40 -25
- package/scripts/lib/install/request.js +1 -1
- package/scripts/lib/install-manifests.js +9 -1
- package/scripts/lib/install-targets/cangming-home.js +143 -0
- package/scripts/lib/install-targets/codewhale-home.js +187 -0
- package/scripts/lib/install-targets/registry.js +4 -0
- package/scripts/lib/loop-oracle.js +5 -0
- package/scripts/lib/loop-spec.js +168 -0
- package/scripts/lib/loop-state-store.js +221 -0
- package/scripts/lib/transcript-usage.js +193 -0
- package/scripts/test-cangming-install.js +105 -0
- package/skills/strategic-compact/SKILL.md +11 -2
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const { execSync } = require('child_process');
|
|
17
|
-
const yaml = require
|
|
17
|
+
const yaml = require('js-yaml');
|
|
18
18
|
|
|
19
19
|
const { createGoal, saveGoal } = require('./completion-oracle');
|
|
20
|
+
const loopStateStore = require('./loop-state-store');
|
|
21
|
+
const { parseLoopSpecContent } = require('./loop-spec');
|
|
20
22
|
|
|
21
23
|
const DEFAULT_CONFIG = {
|
|
22
24
|
interval: '30m',
|
|
@@ -75,12 +77,40 @@ function parseSimpleYaml(content) {
|
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
function loadConfig(projectRoot) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
+
const root = projectRoot || process.cwd();
|
|
81
|
+
const loopSpecPath = path.join(root, '.tsp', 'loop.yaml');
|
|
82
|
+
if (fs.existsSync(loopSpecPath)) {
|
|
83
|
+
try {
|
|
84
|
+
const loopSpec = parseLoopSpecContent(fs.readFileSync(loopSpecPath, 'utf-8'), loopSpecPath);
|
|
85
|
+
return {
|
|
86
|
+
...DEFAULT_CONFIG,
|
|
87
|
+
interval: loopSpec.cadence,
|
|
88
|
+
scans: loopSpec.gates.map(gate => ({
|
|
89
|
+
name: gate.name,
|
|
90
|
+
command: `${gate.command} 2>&1; echo EXIT:$?`,
|
|
91
|
+
onFailure: SCAN_ACTIONS.autoGoal,
|
|
92
|
+
description: gate.description || gate.name,
|
|
93
|
+
})),
|
|
94
|
+
budget: {
|
|
95
|
+
...DEFAULT_CONFIG.budget,
|
|
96
|
+
maxDollarsPerHour: loopSpec.budget.maxDollars,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
} catch {
|
|
100
|
+
return DEFAULT_CONFIG;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const configPath = [
|
|
105
|
+
path.join(root, '.tsp', 'heartbeat.yaml'),
|
|
106
|
+
path.join(root, '.claude', 'heartbeat.yaml'),
|
|
107
|
+
].find(candidate => fs.existsSync(candidate));
|
|
108
|
+
|
|
109
|
+
if (!configPath) return DEFAULT_CONFIG;
|
|
80
110
|
|
|
81
111
|
try {
|
|
82
112
|
const content = fs.readFileSync(configPath, 'utf-8');
|
|
83
|
-
const parsed = parseSimpleYaml(content);
|
|
113
|
+
const parsed = yaml.load(content, { schema: yaml.JSON_SCHEMA }) || parseSimpleYaml(content);
|
|
84
114
|
return { ...DEFAULT_CONFIG, ...parsed.heartbeat };
|
|
85
115
|
} catch {
|
|
86
116
|
return DEFAULT_CONFIG;
|
|
@@ -147,14 +177,7 @@ function createTriageItem(scan, result) {
|
|
|
147
177
|
}
|
|
148
178
|
|
|
149
179
|
function appendToTriageInbox(item) {
|
|
150
|
-
|
|
151
|
-
const inboxDir = path.join(home, '.claude', 'triage');
|
|
152
|
-
if (!fs.existsSync(inboxDir)) {
|
|
153
|
-
fs.mkdirSync(inboxDir, { recursive: true });
|
|
154
|
-
}
|
|
155
|
-
const inboxPath = path.join(inboxDir, 'inbox.jsonl');
|
|
156
|
-
fs.appendFileSync(inboxPath, JSON.stringify(item) + '\n', 'utf-8');
|
|
157
|
-
return inboxPath;
|
|
180
|
+
return loopStateStore.appendTriageItem(item);
|
|
158
181
|
}
|
|
159
182
|
|
|
160
183
|
function createGoalFromScanFailure(scan, result) {
|
|
@@ -181,7 +204,7 @@ function runHeartbeat(projectRoot) {
|
|
|
181
204
|
if (config.scans.length === 0) {
|
|
182
205
|
return {
|
|
183
206
|
status: 'no_scans',
|
|
184
|
-
message: 'No scans configured in .claude/heartbeat.yaml',
|
|
207
|
+
message: 'No scans configured in .tsp/loop.yaml, .tsp/heartbeat.yaml, or .claude/heartbeat.yaml',
|
|
185
208
|
results: [],
|
|
186
209
|
};
|
|
187
210
|
}
|
|
@@ -216,14 +239,10 @@ function runHeartbeat(projectRoot) {
|
|
|
216
239
|
function getHeartbeatStatus(projectRoot) {
|
|
217
240
|
const config = loadConfig(projectRoot);
|
|
218
241
|
|
|
219
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
220
|
-
const lastRunPath = path.join(home, '.claude', 'heartbeat-last-run.json');
|
|
221
242
|
let lastRun = null;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
} catch { /* ignore */ }
|
|
226
|
-
}
|
|
243
|
+
try {
|
|
244
|
+
lastRun = loopStateStore.loadHeartbeat('last-run');
|
|
245
|
+
} catch { /* ignore */ }
|
|
227
246
|
|
|
228
247
|
return {
|
|
229
248
|
configured: config.scans.length > 0,
|
|
@@ -236,11 +255,7 @@ function getHeartbeatStatus(projectRoot) {
|
|
|
236
255
|
}
|
|
237
256
|
|
|
238
257
|
function saveLastRun(result) {
|
|
239
|
-
|
|
240
|
-
const lastRunPath = path.join(home, '.claude', 'heartbeat-last-run.json');
|
|
241
|
-
const dir = path.dirname(lastRunPath);
|
|
242
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
243
|
-
fs.writeFileSync(lastRunPath, JSON.stringify(result, null, 2), 'utf-8');
|
|
258
|
+
return loopStateStore.saveHeartbeat('last-run', result);
|
|
244
259
|
}
|
|
245
260
|
|
|
246
261
|
function parseInterval(interval) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const { validateInstallModuleIds } = require('../install-manifests');
|
|
4
4
|
const { normalizeInstallTarget } = require('../install-targets/registry');
|
|
5
5
|
|
|
6
|
-
const LEGACY_INSTALL_TARGETS = ['claude', 'cursor', 'antigravity', 'codex', 'opencode'];
|
|
6
|
+
const LEGACY_INSTALL_TARGETS = ['claude', 'cursor', 'antigravity', 'codex', 'opencode', 'cangming'];
|
|
7
7
|
|
|
8
8
|
function dedupeStrings(values) {
|
|
9
9
|
return [...new Set((Array.isArray(values) ? values : []).map(value => String(value).trim()).filter(Boolean))];
|
|
@@ -8,7 +8,7 @@ const {
|
|
|
8
8
|
} = require('./install-targets/registry');
|
|
9
9
|
|
|
10
10
|
const DEFAULT_REPO_ROOT = path.join(__dirname, '../..');
|
|
11
|
-
const SUPPORTED_INSTALL_TARGETS = ['claude', 'cursor', 'antigravity', 'codex', 'gemini', 'opencode', 'codebuddy', 'copilot', 'windsurf', 'augment'];
|
|
11
|
+
const SUPPORTED_INSTALL_TARGETS = ['claude', 'cursor', 'antigravity', 'codex', 'gemini', 'opencode', 'cangming', 'codewhale', 'codebuddy', 'copilot', 'windsurf', 'augment'];
|
|
12
12
|
const COMPONENT_FAMILY_PREFIXES = {
|
|
13
13
|
baseline: 'baseline:',
|
|
14
14
|
language: 'lang:',
|
|
@@ -54,6 +54,14 @@ const LEGACY_COMPAT_BASE_MODULE_IDS_BY_TARGET = Object.freeze({
|
|
|
54
54
|
'platform-configs',
|
|
55
55
|
'workflow-quality',
|
|
56
56
|
],
|
|
57
|
+
codewhale: [
|
|
58
|
+
'rules-core',
|
|
59
|
+
'agents-core',
|
|
60
|
+
'commands-core',
|
|
61
|
+
'hooks-runtime',
|
|
62
|
+
'platform-configs',
|
|
63
|
+
'workflow-quality',
|
|
64
|
+
],
|
|
57
65
|
});
|
|
58
66
|
const LEGACY_LANGUAGE_ALIAS_TO_CANONICAL = Object.freeze({
|
|
59
67
|
cpp: 'cpp',
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
createInstallTargetAdapter,
|
|
6
|
+
createManagedOperation,
|
|
7
|
+
normalizeRelativePath,
|
|
8
|
+
} = require('./helpers');
|
|
9
|
+
|
|
10
|
+
const PLUGIN_NAME = require('../team-skills-data.json').plugin.name;
|
|
11
|
+
|
|
12
|
+
function addOperation(operations, seen, operation) {
|
|
13
|
+
const key = `${operation.sourceRelativePath}=>${operation.destinationPath}`;
|
|
14
|
+
if (seen.has(key)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
seen.add(key);
|
|
18
|
+
operations.push(operation);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function addCopyOperation(operations, seen, moduleId, sourceRelativePath, destinationPath, strategy = 'preserve-relative-path') {
|
|
22
|
+
addOperation(operations, seen, createManagedOperation({
|
|
23
|
+
moduleId,
|
|
24
|
+
sourceRelativePath,
|
|
25
|
+
destinationPath,
|
|
26
|
+
strategy,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sourceDir(input, sourceRelativePath) {
|
|
31
|
+
return path.join(input.repoRoot || '', normalizeRelativePath(sourceRelativePath));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function rootAgentFileName(sourcePath, fileName) {
|
|
35
|
+
const normalizedSourcePath = normalizeRelativePath(sourcePath);
|
|
36
|
+
if (normalizedSourcePath === 'agents/specialists' || normalizedSourcePath.startsWith('agents/specialists/')) {
|
|
37
|
+
return `specialist-${fileName}`;
|
|
38
|
+
}
|
|
39
|
+
return fileName;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function addRootAgentOperations(operations, seen, moduleId, sourceRelativePath, input, targetRoot) {
|
|
43
|
+
const normalizedSourcePath = normalizeRelativePath(sourceRelativePath);
|
|
44
|
+
if (normalizedSourcePath !== 'agents' && !normalizedSourcePath.startsWith('agents/')) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const agentDirs = normalizedSourcePath === 'agents'
|
|
49
|
+
? ['roles', 'specialists']
|
|
50
|
+
: [''];
|
|
51
|
+
|
|
52
|
+
for (const agentDir of agentDirs) {
|
|
53
|
+
const sourcePath = agentDir ? path.join(normalizedSourcePath, agentDir) : normalizedSourcePath;
|
|
54
|
+
const absoluteSourceDir = sourceDir(input, sourcePath);
|
|
55
|
+
if (!input.repoRoot || !fs.existsSync(absoluteSourceDir)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const stat = fs.statSync(absoluteSourceDir);
|
|
60
|
+
if (stat.isFile() && path.extname(absoluteSourceDir) === '.md') {
|
|
61
|
+
const fileName = rootAgentFileName(path.dirname(sourcePath), path.basename(sourcePath));
|
|
62
|
+
addCopyOperation(
|
|
63
|
+
operations,
|
|
64
|
+
seen,
|
|
65
|
+
moduleId,
|
|
66
|
+
sourcePath,
|
|
67
|
+
path.join(targetRoot, 'agents', fileName),
|
|
68
|
+
'flatten-agent-copy'
|
|
69
|
+
);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!stat.isDirectory()) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const entry of fs.readdirSync(absoluteSourceDir, { withFileTypes: true }).sort((left, right) => (
|
|
78
|
+
left.name.localeCompare(right.name)
|
|
79
|
+
))) {
|
|
80
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
81
|
+
const fileName = rootAgentFileName(sourcePath, entry.name);
|
|
82
|
+
addCopyOperation(
|
|
83
|
+
operations,
|
|
84
|
+
seen,
|
|
85
|
+
moduleId,
|
|
86
|
+
path.join(sourcePath, entry.name),
|
|
87
|
+
path.join(targetRoot, 'agents', fileName),
|
|
88
|
+
'flatten-agent-copy'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function planCangmingOperations(input, adapter) {
|
|
96
|
+
const targetRoot = adapter.resolveRoot(input);
|
|
97
|
+
const pluginRoot = path.join(targetRoot, 'plugins', PLUGIN_NAME);
|
|
98
|
+
const operations = [];
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
|
|
101
|
+
for (const module of Array.isArray(input.modules) ? input.modules : []) {
|
|
102
|
+
for (const rawSourcePath of Array.isArray(module.paths) ? module.paths : []) {
|
|
103
|
+
const sourceRelativePath = normalizeRelativePath(rawSourcePath);
|
|
104
|
+
|
|
105
|
+
addCopyOperation(
|
|
106
|
+
operations,
|
|
107
|
+
seen,
|
|
108
|
+
module.id,
|
|
109
|
+
sourceRelativePath,
|
|
110
|
+
path.join(pluginRoot, sourceRelativePath),
|
|
111
|
+
'plugin-copy'
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (sourceRelativePath === 'commands' || sourceRelativePath.startsWith('commands/')) {
|
|
115
|
+
const commandSuffix = sourceRelativePath === 'commands'
|
|
116
|
+
? ''
|
|
117
|
+
: sourceRelativePath.slice('commands/'.length);
|
|
118
|
+
addCopyOperation(
|
|
119
|
+
operations,
|
|
120
|
+
seen,
|
|
121
|
+
module.id,
|
|
122
|
+
sourceRelativePath,
|
|
123
|
+
path.join(targetRoot, 'command', commandSuffix),
|
|
124
|
+
'opencode-command-copy'
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
addRootAgentOperations(operations, seen, module.id, sourceRelativePath, input, targetRoot);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return operations;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = createInstallTargetAdapter({
|
|
136
|
+
id: 'cangming-home',
|
|
137
|
+
target: 'cangming',
|
|
138
|
+
kind: 'home',
|
|
139
|
+
rootSegments: ['.config', 'cangming'],
|
|
140
|
+
installStatePathSegments: ['ecc-install-state.json'],
|
|
141
|
+
nativeRootRelativePath: '.cangming',
|
|
142
|
+
planOperations: planCangmingOperations,
|
|
143
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
createInstallTargetAdapter,
|
|
6
|
+
createManagedOperation,
|
|
7
|
+
normalizeRelativePath,
|
|
8
|
+
} = require('./helpers');
|
|
9
|
+
|
|
10
|
+
const PLUGIN_NAME = require('../team-skills-data.json').plugin.name;
|
|
11
|
+
|
|
12
|
+
function addOperation(operations, seen, operation) {
|
|
13
|
+
const key = `${operation.sourceRelativePath}=>${operation.destinationPath}`;
|
|
14
|
+
if (seen.has(key)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
seen.add(key);
|
|
18
|
+
operations.push(operation);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function addCopyOperation(operations, seen, moduleId, sourceRelativePath, destinationPath, strategy = 'preserve-relative-path') {
|
|
22
|
+
addOperation(operations, seen, createManagedOperation({
|
|
23
|
+
moduleId,
|
|
24
|
+
sourceRelativePath,
|
|
25
|
+
destinationPath,
|
|
26
|
+
strategy,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sourceDir(input, sourceRelativePath) {
|
|
31
|
+
return path.join(input.repoRoot || '', normalizeRelativePath(sourceRelativePath));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function addAgentOperations(operations, seen, moduleId, sourceRelativePath, input, targetRoot) {
|
|
35
|
+
const normalizedSourcePath = normalizeRelativePath(sourceRelativePath);
|
|
36
|
+
if (normalizedSourcePath !== 'agents' && !normalizedSourcePath.startsWith('agents/')) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const agentDirs = normalizedSourcePath === 'agents'
|
|
41
|
+
? ['roles', 'specialists']
|
|
42
|
+
: [''];
|
|
43
|
+
|
|
44
|
+
for (const agentDir of agentDirs) {
|
|
45
|
+
const sourcePath = agentDir ? path.join(normalizedSourcePath, agentDir) : normalizedSourcePath;
|
|
46
|
+
const absoluteSourceDir = sourceDir(input, sourcePath);
|
|
47
|
+
if (!input.repoRoot || !fs.existsSync(absoluteSourceDir)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const stat = fs.statSync(absoluteSourceDir);
|
|
52
|
+
if (stat.isFile() && path.extname(absoluteSourceDir) === '.md') {
|
|
53
|
+
const fileName = path.basename(sourcePath);
|
|
54
|
+
addCopyOperation(
|
|
55
|
+
operations, seen, moduleId, sourcePath,
|
|
56
|
+
path.join(targetRoot, 'agents', fileName),
|
|
57
|
+
'flatten-agent-copy'
|
|
58
|
+
);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!stat.isDirectory()) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const entry of fs.readdirSync(absoluteSourceDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
67
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
68
|
+
const prefix = agentDir === 'specialists' ? 'specialist-' : '';
|
|
69
|
+
addCopyOperation(
|
|
70
|
+
operations, seen, moduleId,
|
|
71
|
+
path.join(sourcePath, entry.name),
|
|
72
|
+
path.join(targetRoot, 'agents', `${prefix}${entry.name}`),
|
|
73
|
+
'flatten-agent-copy'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function addRuleOperations(operations, seen, moduleId, sourceRelativePath, input, targetRoot) {
|
|
81
|
+
const normalizedSourcePath = normalizeRelativePath(sourceRelativePath);
|
|
82
|
+
if (normalizedSourcePath !== 'rules' && !normalizedSourcePath.startsWith('rules/')) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const absoluteSourceDir = sourceDir(input, normalizedSourcePath);
|
|
87
|
+
if (!input.repoRoot || !fs.existsSync(absoluteSourceDir) || !fs.statSync(absoluteSourceDir).isDirectory()) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const entries = fs.readdirSync(absoluteSourceDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
const entryPath = path.join(absoluteSourceDir, entry.name);
|
|
94
|
+
if (entry.isDirectory()) {
|
|
95
|
+
const subEntries = fs.readdirSync(entryPath, { withFileTypes: true });
|
|
96
|
+
for (const sub of subEntries) {
|
|
97
|
+
if (sub.isFile() && sub.name.endsWith('.md')) {
|
|
98
|
+
addCopyOperation(
|
|
99
|
+
operations, seen, moduleId,
|
|
100
|
+
path.join(normalizedSourcePath, entry.name, sub.name),
|
|
101
|
+
path.join(targetRoot, 'rules', entry.name, sub.name),
|
|
102
|
+
'rule-copy'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
107
|
+
addCopyOperation(
|
|
108
|
+
operations, seen, moduleId,
|
|
109
|
+
path.join(normalizedSourcePath, entry.name),
|
|
110
|
+
path.join(targetRoot, 'rules', entry.name),
|
|
111
|
+
'rule-copy'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function planCodeWhaleOperations(input, adapter) {
|
|
118
|
+
const targetRoot = adapter.resolveRoot(input);
|
|
119
|
+
const operations = [];
|
|
120
|
+
const seen = new Set();
|
|
121
|
+
|
|
122
|
+
for (const module of Array.isArray(input.modules) ? input.modules : []) {
|
|
123
|
+
for (const rawSourcePath of Array.isArray(module.paths) ? module.paths : []) {
|
|
124
|
+
const sourceRelativePath = normalizeRelativePath(rawSourcePath);
|
|
125
|
+
|
|
126
|
+
// Skills: preserve directory structure
|
|
127
|
+
if (sourceRelativePath === 'skills' || sourceRelativePath.startsWith('skills/')) {
|
|
128
|
+
addCopyOperation(
|
|
129
|
+
operations, seen, module.id, sourceRelativePath,
|
|
130
|
+
path.join(targetRoot, 'skills', sourceRelativePath.replace(/^skills\/?/, '')),
|
|
131
|
+
'skill-copy'
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Commands: flatten to commands/
|
|
136
|
+
if (sourceRelativePath === 'commands' || sourceRelativePath.startsWith('commands/')) {
|
|
137
|
+
const commandSuffix = sourceRelativePath === 'commands'
|
|
138
|
+
? ''
|
|
139
|
+
: sourceRelativePath.slice('commands/'.length);
|
|
140
|
+
addCopyOperation(
|
|
141
|
+
operations, seen, module.id, sourceRelativePath,
|
|
142
|
+
path.join(targetRoot, 'commands', commandSuffix),
|
|
143
|
+
'command-copy'
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Agents: flatten roles + specialists
|
|
148
|
+
addAgentOperations(operations, seen, module.id, sourceRelativePath, input, targetRoot);
|
|
149
|
+
|
|
150
|
+
// Rules: preserve namespace/file structure
|
|
151
|
+
addRuleOperations(operations, seen, module.id, sourceRelativePath, input, targetRoot);
|
|
152
|
+
|
|
153
|
+
// Contexts: direct copy
|
|
154
|
+
if (sourceRelativePath === 'contexts' || sourceRelativePath.startsWith('contexts/')) {
|
|
155
|
+
const contextSuffix = sourceRelativePath === 'contexts'
|
|
156
|
+
? ''
|
|
157
|
+
: sourceRelativePath.slice('contexts/'.length);
|
|
158
|
+
addCopyOperation(
|
|
159
|
+
operations, seen, module.id, sourceRelativePath,
|
|
160
|
+
path.join(targetRoot, 'contexts', contextSuffix),
|
|
161
|
+
'context-copy'
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Hooks: copy hooks directory for reference, config.toml injection handled by post-install
|
|
166
|
+
if (sourceRelativePath === 'hooks' || sourceRelativePath.startsWith('hooks/')) {
|
|
167
|
+
addCopyOperation(
|
|
168
|
+
operations, seen, module.id, sourceRelativePath,
|
|
169
|
+
path.join(targetRoot, 'hooks', sourceRelativePath.replace(/^hooks\/?/, '')),
|
|
170
|
+
'hook-copy'
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return operations;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = createInstallTargetAdapter({
|
|
180
|
+
id: 'codewhale-home',
|
|
181
|
+
target: 'codewhale',
|
|
182
|
+
kind: 'home',
|
|
183
|
+
rootSegments: ['.codewhale'],
|
|
184
|
+
installStatePathSegments: ['ecc-install-state.json'],
|
|
185
|
+
nativeRootRelativePath: '.codewhale',
|
|
186
|
+
planOperations: planCodeWhaleOperations,
|
|
187
|
+
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
const antigravityProject = require('./antigravity-project');
|
|
2
2
|
const augmentProject = require('./augment-project');
|
|
3
|
+
const cangmingHome = require('./cangming-home');
|
|
3
4
|
const claudeHome = require('./claude-home');
|
|
4
5
|
const codebuddyProject = require('./codebuddy-project');
|
|
5
6
|
const codexHome = require('./codex-home');
|
|
7
|
+
const codewhaleHome = require('./codewhale-home');
|
|
6
8
|
const copilotHome = require('./copilot-home');
|
|
7
9
|
const cursorProject = require('./cursor-project');
|
|
8
10
|
const geminiProject = require('./gemini-project');
|
|
@@ -22,6 +24,8 @@ const ADAPTERS = Object.freeze([
|
|
|
22
24
|
codexHome,
|
|
23
25
|
geminiProject,
|
|
24
26
|
opencodeHome,
|
|
27
|
+
cangmingHome,
|
|
28
|
+
codewhaleHome,
|
|
25
29
|
codebuddyProject,
|
|
26
30
|
copilotHome,
|
|
27
31
|
windsurfProject,
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const yaml = require('js-yaml');
|
|
6
|
+
|
|
7
|
+
class LoopSpecError extends Error {
|
|
8
|
+
constructor(message, details = {}) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'LoopSpecError';
|
|
11
|
+
this.details = details;
|
|
12
|
+
this.code = details.code || 'loop_spec_error';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function ensureObject(value, label, filePath) {
|
|
17
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
18
|
+
throw new LoopSpecError(`${label} must be an object`, {
|
|
19
|
+
filePath,
|
|
20
|
+
code: 'invalid_payload',
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ensureNonEmptyString(value, label, filePath) {
|
|
26
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
27
|
+
throw new LoopSpecError(`${label} must be a non-empty string`, {
|
|
28
|
+
filePath,
|
|
29
|
+
code: 'invalid_field',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ensurePositiveNumber(value, label, filePath) {
|
|
35
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
36
|
+
throw new LoopSpecError(`${label} must be a positive number`, {
|
|
37
|
+
filePath,
|
|
38
|
+
code: 'invalid_budget',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeGate(gate, index, filePath) {
|
|
44
|
+
ensureObject(gate, `loop.gates[${index}]`, filePath);
|
|
45
|
+
ensureNonEmptyString(gate.name, `loop.gates[${index}].name`, filePath);
|
|
46
|
+
ensureNonEmptyString(gate.command, `loop.gates[${index}].command`, filePath);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
name: gate.name.trim(),
|
|
50
|
+
command: gate.command.trim(),
|
|
51
|
+
description: typeof gate.description === 'string' && gate.description.trim()
|
|
52
|
+
? gate.description.trim()
|
|
53
|
+
: null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeBudget(budget, filePath) {
|
|
58
|
+
ensureObject(budget, 'loop.budget', filePath);
|
|
59
|
+
ensurePositiveNumber(budget.maxIterations, 'loop.budget.maxIterations', filePath);
|
|
60
|
+
ensurePositiveNumber(budget.maxDollars, 'loop.budget.maxDollars', filePath);
|
|
61
|
+
ensureNonEmptyString(budget.maxDuration, 'loop.budget.maxDuration', filePath);
|
|
62
|
+
|
|
63
|
+
if (!/^(\d+)(m|h)$/.test(budget.maxDuration)) {
|
|
64
|
+
throw new LoopSpecError('loop.budget.maxDuration must use Nm or Nh format', {
|
|
65
|
+
filePath,
|
|
66
|
+
code: 'invalid_budget_duration',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
maxIterations: budget.maxIterations,
|
|
72
|
+
maxDuration: budget.maxDuration,
|
|
73
|
+
maxDollars: budget.maxDollars,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeActor(actor, label, filePath) {
|
|
78
|
+
ensureObject(actor, label, filePath);
|
|
79
|
+
ensureNonEmptyString(actor.role, `${label}.role`, filePath);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
role: actor.role.trim(),
|
|
83
|
+
writeAccess: actor.writeAccess === true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeLoopSpec(payload, filePath = '<inline>') {
|
|
88
|
+
ensureObject(payload, 'loop spec', filePath);
|
|
89
|
+
ensureObject(payload.loop, 'loop', filePath);
|
|
90
|
+
|
|
91
|
+
const loop = payload.loop;
|
|
92
|
+
ensureNonEmptyString(loop.id, 'loop.id', filePath);
|
|
93
|
+
ensureNonEmptyString(loop.description, 'loop.description', filePath);
|
|
94
|
+
ensureNonEmptyString(loop.cadence, 'loop.cadence', filePath);
|
|
95
|
+
ensureNonEmptyString(loop.skill, 'loop.skill', filePath);
|
|
96
|
+
ensureNonEmptyString(loop.stateFile, 'loop.stateFile', filePath);
|
|
97
|
+
|
|
98
|
+
if (!/^(\d+)(m|h|d)$/.test(loop.cadence)) {
|
|
99
|
+
throw new LoopSpecError('loop.cadence must use Nm, Nh, or Nd format', {
|
|
100
|
+
filePath,
|
|
101
|
+
code: 'invalid_cadence',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!Array.isArray(loop.gates) || loop.gates.length === 0) {
|
|
106
|
+
throw new LoopSpecError('loop.gates must include at least one hard verification gate', {
|
|
107
|
+
filePath,
|
|
108
|
+
code: 'missing_gates',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
id: loop.id.trim(),
|
|
114
|
+
description: loop.description.trim(),
|
|
115
|
+
cadence: loop.cadence.trim(),
|
|
116
|
+
skill: loop.skill.trim(),
|
|
117
|
+
stateFile: loop.stateFile.trim(),
|
|
118
|
+
gates: loop.gates.map((gate, index) => normalizeGate(gate, index, filePath)),
|
|
119
|
+
maker: normalizeActor(loop.maker, 'loop.maker', filePath),
|
|
120
|
+
checker: normalizeActor(loop.checker, 'loop.checker', filePath),
|
|
121
|
+
budget: normalizeBudget(loop.budget, filePath),
|
|
122
|
+
escalation: {
|
|
123
|
+
onBudgetExhausted: loop.escalation?.onBudgetExhausted || 'triage',
|
|
124
|
+
onSecurityFinding: loop.escalation?.onSecurityFinding || 'human',
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseLoopSpecContent(content, filePath = '<inline>') {
|
|
130
|
+
let payload;
|
|
131
|
+
try {
|
|
132
|
+
payload = yaml.load(content, {
|
|
133
|
+
filename: path.basename(filePath),
|
|
134
|
+
schema: yaml.JSON_SCHEMA,
|
|
135
|
+
});
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw new LoopSpecError(`Failed to parse loop spec YAML: ${error.message}`, {
|
|
138
|
+
filePath,
|
|
139
|
+
code: 'yaml_parse_error',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return normalizeLoopSpec(payload, filePath);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function loadLoopSpecFile(filePath) {
|
|
147
|
+
let content;
|
|
148
|
+
try {
|
|
149
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
150
|
+
} catch (error) {
|
|
151
|
+
throw new LoopSpecError(`Failed to read loop spec file: ${error.message}`, {
|
|
152
|
+
filePath,
|
|
153
|
+
code: 'read_error',
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
path: filePath,
|
|
159
|
+
loop: parseLoopSpecContent(content, filePath),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
LoopSpecError,
|
|
165
|
+
normalizeLoopSpec,
|
|
166
|
+
parseLoopSpecContent,
|
|
167
|
+
loadLoopSpecFile,
|
|
168
|
+
};
|