@colin4k1024/tsp 2.4.9 → 2.5.1
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/README.md +71 -1
- package/hooks/README.md +1 -1
- package/hooks/hooks.json +5 -4
- package/hooks/strategic-compact/README.md +7 -5
- package/hooks/strategic-compact/suggest-compact.js +9 -174
- package/package.json +1 -1
- package/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- package/scripts/__pycache__/build_platform_artifacts.cpython-311.pyc +0 -0
- package/scripts/__pycache__/install_platform.cpython-311.pyc +0 -0
- package/scripts/__pycache__/langfuse_trace.cpython-311.pyc +0 -0
- package/scripts/__pycache__/query_audit_logs.cpython-311.pyc +0 -0
- package/scripts/__pycache__/scan_leaked_keys.cpython-311.pyc +0 -0
- package/scripts/__pycache__/team_skills_platform.cpython-311.pyc +0 -0
- package/scripts/__pycache__/team_skills_platform.cpython-313.pyc +0 -0
- package/scripts/__pycache__/validate_library.cpython-311.pyc +0 -0
- package/scripts/__pycache__/validate_workflow_state.cpython-311.pyc +0 -0
- package/scripts/evolution/__pycache__/__init__.cpython-311.pyc +0 -0
- package/scripts/evolution/__pycache__/store.cpython-311.pyc +0 -0
- package/scripts/harness-audit.js +7 -4
- package/scripts/hooks/__pycache__/__init__.cpython-311.pyc +0 -0
- package/scripts/hooks/__pycache__/mcp_health_check.cpython-311.pyc +0 -0
- package/scripts/hooks/__pycache__/observe.cpython-311.pyc +0 -0
- package/scripts/hooks/__pycache__/session_end.cpython-311.pyc +0 -0
- package/scripts/hooks/__pycache__/session_start.cpython-311.pyc +0 -0
- package/scripts/hooks/suggest-compact.js +331 -63
- package/scripts/install-platform.js +68 -85
- package/scripts/lib/__pycache__/audit_logger.cpython-311.pyc +0 -0
- package/scripts/lib/__pycache__/audit_query.cpython-311.pyc +0 -0
- package/scripts/lib/__pycache__/hook_contract.cpython-311.pyc +0 -0
- package/scripts/lib/__pycache__/memory_store.cpython-311.pyc +0 -0
- package/scripts/lib/__pycache__/utils.cpython-311.pyc +0 -0
- package/scripts/lib/opencode/convert-agents.js +273 -0
- package/scripts/lib/opencode/convert-hooks.js +286 -0
- package/scripts/lib/opencode/generate-agents-md.js +361 -0
- package/scripts/test-opencode-install.js +151 -0
- package/skills/goframe-v2/examples/practices/quick-demo/manifest/config/config.yaml +14 -14
- package/skills/repo-scan/SKILL.md +63 -63
|
@@ -2,79 +2,347 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Strategic Compact Suggester
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Runs on PreToolUse or periodically to suggest manual compaction at logical intervals
|
|
8
|
-
*
|
|
9
|
-
* Why manual over auto-compact:
|
|
10
|
-
* - Auto-compact happens at arbitrary points, often mid-task
|
|
11
|
-
* - Strategic compacting preserves context through logical phases
|
|
12
|
-
* - Compact after exploration, before execution
|
|
13
|
-
* - Compact after completing a milestone, before starting next
|
|
5
|
+
* Suggests `/compact` from real context pressure instead of tool-call count.
|
|
6
|
+
* Supports direct hook execution and run-with-flags.js require() mode.
|
|
14
7
|
*/
|
|
15
8
|
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
16
11
|
const fs = require('fs');
|
|
17
12
|
const path = require('path');
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
|
|
16
|
+
const AUTO_COMPACT_BUFFER_PCT = 16.5;
|
|
17
|
+
const DEFAULT_CONTEXT_LIMIT = 200000;
|
|
18
|
+
const DEFAULT_DEBOUNCE_CALLS = 8;
|
|
19
|
+
const STALE_BRIDGE_SECONDS = 60;
|
|
20
|
+
|
|
21
|
+
const URGENCY = [
|
|
22
|
+
[95, 'critical'],
|
|
23
|
+
[85, 'high'],
|
|
24
|
+
[70, 'medium'],
|
|
25
|
+
[0, 'low'],
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function toNumber(value) {
|
|
29
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
30
|
+
if (typeof value === 'string' && value.trim()) {
|
|
31
|
+
const parsed = Number(value);
|
|
32
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function clampPct(value) {
|
|
38
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeRemainingToUsed(remainingPct) {
|
|
42
|
+
const usableRemaining = Math.max(
|
|
43
|
+
0,
|
|
44
|
+
((remainingPct - AUTO_COMPACT_BUFFER_PCT) / (100 - AUTO_COMPACT_BUFFER_PCT)) * 100
|
|
45
|
+
);
|
|
46
|
+
return clampPct(100 - usableRemaining);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getUrgency(usagePct) {
|
|
50
|
+
for (const [threshold, label] of URGENCY) {
|
|
51
|
+
if (usagePct >= threshold) return label;
|
|
52
|
+
}
|
|
53
|
+
return 'low';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getHookEventName(data) {
|
|
57
|
+
const event = data.hook_event_name || data.hookEventName || data.event;
|
|
58
|
+
return typeof event === 'string' && event.trim() ? event.trim() : 'PreToolUse';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sessionKey(data) {
|
|
62
|
+
const raw = data.session_id || process.env.CLAUDE_SESSION_ID;
|
|
63
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
64
|
+
return raw.replace(/[^a-zA-Z0-9_-]/g, '').slice(-64) || 'default';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const cwd = data.cwd || process.cwd();
|
|
68
|
+
return crypto.createHash('sha256').update(String(cwd)).digest('hex').slice(0, 16);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readBridgeMetrics(sessionId) {
|
|
72
|
+
if (!sessionId) return null;
|
|
73
|
+
|
|
74
|
+
const bridgePath = path.join(os.tmpdir(), `harness-ctx-${sessionId}.json`);
|
|
75
|
+
if (!fs.existsSync(bridgePath)) return null;
|
|
76
|
+
|
|
40
77
|
try {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
const bridge = JSON.parse(fs.readFileSync(bridgePath, 'utf8'));
|
|
79
|
+
const now = Math.floor(Date.now() / 1000);
|
|
80
|
+
if (bridge.timestamp && now - bridge.timestamp > STALE_BRIDGE_SECONDS) return null;
|
|
81
|
+
|
|
82
|
+
const usagePct = toNumber(bridge.used_pct);
|
|
83
|
+
const remainingPct = toNumber(bridge.remaining_percentage);
|
|
84
|
+
if (usagePct == null && remainingPct == null) return null;
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
usagePct: usagePct != null ? clampPct(usagePct) : normalizeRemainingToUsed(remainingPct),
|
|
88
|
+
remainingPct,
|
|
89
|
+
source: 'bridge',
|
|
90
|
+
};
|
|
91
|
+
} catch (_) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveContextMetrics(data) {
|
|
97
|
+
const contextLimit = toNumber(process.env.CLAUDE_CONTEXT_LIMIT) || DEFAULT_CONTEXT_LIMIT;
|
|
98
|
+
const cw = data.context_window && typeof data.context_window === 'object' ? data.context_window : {};
|
|
99
|
+
|
|
100
|
+
const stdinUsed = toNumber(cw.used_percentage);
|
|
101
|
+
if (stdinUsed != null) {
|
|
102
|
+
const usagePct = clampPct(stdinUsed);
|
|
103
|
+
return {
|
|
104
|
+
usagePct,
|
|
105
|
+
remainingPct: toNumber(cw.remaining_percentage),
|
|
106
|
+
contextLimit,
|
|
107
|
+
contextSize: Math.round((usagePct / 100) * contextLimit),
|
|
108
|
+
source: 'stdin.used_percentage',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const stdinRemaining = toNumber(cw.remaining_percentage);
|
|
113
|
+
if (stdinRemaining != null) {
|
|
114
|
+
const usagePct = normalizeRemainingToUsed(stdinRemaining);
|
|
115
|
+
return {
|
|
116
|
+
usagePct,
|
|
117
|
+
remainingPct: clampPct(stdinRemaining),
|
|
118
|
+
contextLimit,
|
|
119
|
+
contextSize: Math.round((usagePct / 100) * contextLimit),
|
|
120
|
+
source: 'stdin.remaining_percentage',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const envSize = toNumber(process.env.CLAUDE_CONTEXT_SIZE);
|
|
125
|
+
if (envSize != null && envSize > 0) {
|
|
126
|
+
const usagePct = clampPct((envSize / contextLimit) * 100);
|
|
127
|
+
return {
|
|
128
|
+
usagePct,
|
|
129
|
+
remainingPct: null,
|
|
130
|
+
contextLimit,
|
|
131
|
+
contextSize: envSize,
|
|
132
|
+
source: 'env',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const bridge = readBridgeMetrics(sessionKey(data));
|
|
137
|
+
if (bridge) {
|
|
138
|
+
return {
|
|
139
|
+
...bridge,
|
|
140
|
+
contextLimit,
|
|
141
|
+
contextSize: Math.round((bridge.usagePct / 100) * contextLimit),
|
|
142
|
+
};
|
|
62
143
|
}
|
|
63
144
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function buildSuggestions(usagePct, contextSize) {
|
|
149
|
+
const suggestions = [];
|
|
150
|
+
let savings = 0;
|
|
151
|
+
|
|
152
|
+
if (usagePct >= 85) {
|
|
153
|
+
const saved = Math.round(contextSize * 0.15);
|
|
154
|
+
suggestions.push({
|
|
155
|
+
action: 'summarize',
|
|
156
|
+
target: 'early conversation history',
|
|
157
|
+
reason: 'Preserve decisions and pending work, compress exploration traces',
|
|
158
|
+
estimated_tokens_saved: saved,
|
|
159
|
+
});
|
|
160
|
+
savings += saved;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (usagePct >= 70) {
|
|
164
|
+
const saved = Math.round(contextSize * 0.10);
|
|
165
|
+
suggestions.push({
|
|
166
|
+
action: 'discard',
|
|
167
|
+
target: 'large tool outputs and search traces',
|
|
168
|
+
reason: 'Keep file paths and conclusions, drop bulky intermediate output',
|
|
169
|
+
estimated_tokens_saved: saved,
|
|
170
|
+
});
|
|
171
|
+
savings += saved;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (usagePct >= 70) {
|
|
175
|
+
const saved = Math.round(contextSize * 0.08);
|
|
176
|
+
suggestions.push({
|
|
177
|
+
action: 'reorganize',
|
|
178
|
+
target: 'role and specialist outputs',
|
|
179
|
+
reason: 'Move decisions, handoff state, and validation results into the compact summary',
|
|
180
|
+
estimated_tokens_saved: saved,
|
|
181
|
+
});
|
|
182
|
+
savings += saved;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { suggestions, savings };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function buildReorganizationPlan(urgency) {
|
|
189
|
+
return {
|
|
190
|
+
phase_1: {
|
|
191
|
+
action: 'capture_decisions',
|
|
192
|
+
description: 'Keep decisions, constraints, current branch, changed files, and validation results.',
|
|
193
|
+
},
|
|
194
|
+
phase_2: {
|
|
195
|
+
action: 'capture_next_steps',
|
|
196
|
+
description: 'Keep active task, pending todos, blockers, and the next command to run.',
|
|
197
|
+
},
|
|
198
|
+
phase_3: {
|
|
199
|
+
action: 'compress_history',
|
|
200
|
+
description: 'Summarize early exploration and long tool outputs to conclusions plus file paths.',
|
|
201
|
+
},
|
|
202
|
+
phase_4: {
|
|
203
|
+
action: urgency === 'critical' ? 'compact_now' : 'compact_at_logical_break',
|
|
204
|
+
description: urgency === 'critical'
|
|
205
|
+
? 'Stop broad work and ask the user to run /compact now.'
|
|
206
|
+
: 'Finish the current small step, then run /compact before more exploration.',
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function shouldEmit(sessionId, urgency, usagePct) {
|
|
212
|
+
if (process.env.STRATEGIC_COMPACT_DISABLE_DEBOUNCE === '1') return true;
|
|
213
|
+
if (!sessionId) return true;
|
|
214
|
+
|
|
215
|
+
const debounceCalls =
|
|
216
|
+
toNumber(process.env.STRATEGIC_COMPACT_DEBOUNCE_CALLS) || DEFAULT_DEBOUNCE_CALLS;
|
|
217
|
+
const statePath = path.join(os.tmpdir(), `harness-strategic-compact-${sessionId}.json`);
|
|
218
|
+
const nextState = {
|
|
219
|
+
lastUrgency: urgency,
|
|
220
|
+
lastUsagePct: usagePct,
|
|
221
|
+
callsSinceEmit: 0,
|
|
222
|
+
updatedAt: new Date().toISOString(),
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
let previous = null;
|
|
227
|
+
if (fs.existsSync(statePath)) {
|
|
228
|
+
previous = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const callsSinceEmit = (previous?.callsSinceEmit || 0) + 1;
|
|
232
|
+
const severityOrder = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
233
|
+
const escalated = severityOrder[urgency] > severityOrder[previous?.lastUrgency || 'low'];
|
|
234
|
+
const usageJumped = usagePct - (previous?.lastUsagePct || 0) >= 8;
|
|
235
|
+
const repeatDue = callsSinceEmit >= debounceCalls;
|
|
236
|
+
|
|
237
|
+
if (!previous || escalated || usageJumped || repeatDue) {
|
|
238
|
+
fs.writeFileSync(statePath, JSON.stringify(nextState));
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
fs.writeFileSync(statePath, JSON.stringify({
|
|
243
|
+
...nextState,
|
|
244
|
+
callsSinceEmit,
|
|
245
|
+
lastUrgency: previous.lastUrgency || urgency,
|
|
246
|
+
lastUsagePct: previous.lastUsagePct || usagePct,
|
|
247
|
+
}));
|
|
248
|
+
return false;
|
|
249
|
+
} catch (_) {
|
|
250
|
+
return true;
|
|
67
251
|
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function buildContextMessage({ usagePct, remainingPct, urgency, savings, suggestions, source }) {
|
|
255
|
+
const remainingPart = remainingPct == null ? '' : ` | remaining: ${clampPct(remainingPct)}%`;
|
|
256
|
+
const actionByUrgency = {
|
|
257
|
+
medium: 'Finish the current small step, then run `/compact` before more broad reading or implementation.',
|
|
258
|
+
high: 'Stop new exploration, preserve decisions/todos/validation results, and ask the user to run `/compact`.',
|
|
259
|
+
critical: 'Context is nearly exhausted. Do not start new tool chains; ask the user to run `/compact` now.',
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
return [
|
|
263
|
+
'## Strategic Compact Triggered',
|
|
264
|
+
`- Context usage: ${usagePct}%${remainingPart} | urgency: ${urgency} | source: ${source}`,
|
|
265
|
+
`- Action: ${actionByUrgency[urgency] || actionByUrgency.medium}`,
|
|
266
|
+
`- Estimated savings: ~${savings} tokens`,
|
|
267
|
+
...suggestions.map(item => `- ${item.action}: ${item.target} - ${item.reason}`),
|
|
268
|
+
].join('\n');
|
|
269
|
+
}
|
|
68
270
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
271
|
+
function buildHookOutput(rawInput) {
|
|
272
|
+
let data = {};
|
|
273
|
+
try {
|
|
274
|
+
data = JSON.parse(rawInput || '{}');
|
|
275
|
+
} catch (_) {
|
|
276
|
+
return null;
|
|
72
277
|
}
|
|
73
278
|
|
|
74
|
-
|
|
279
|
+
const metrics = resolveContextMetrics(data);
|
|
280
|
+
if (!metrics) return null;
|
|
281
|
+
|
|
282
|
+
const urgency = getUrgency(metrics.usagePct);
|
|
283
|
+
if (urgency === 'low') return null;
|
|
284
|
+
|
|
285
|
+
const sessionId = sessionKey(data);
|
|
286
|
+
if (!shouldEmit(sessionId, urgency, metrics.usagePct)) return null;
|
|
287
|
+
|
|
288
|
+
const { suggestions, savings } = buildSuggestions(metrics.usagePct, metrics.contextSize);
|
|
289
|
+
const reorganizationPlan = buildReorganizationPlan(urgency);
|
|
290
|
+
const hookEventName = getHookEventName(data);
|
|
291
|
+
const additionalContext = buildContextMessage({
|
|
292
|
+
usagePct: metrics.usagePct,
|
|
293
|
+
remainingPct: metrics.remainingPct,
|
|
294
|
+
urgency,
|
|
295
|
+
savings,
|
|
296
|
+
suggestions,
|
|
297
|
+
source: metrics.source,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
hookSpecificOutput: {
|
|
302
|
+
hookEventName,
|
|
303
|
+
additionalContext,
|
|
304
|
+
},
|
|
305
|
+
compactSuggestion: {
|
|
306
|
+
should_compact: true,
|
|
307
|
+
urgency,
|
|
308
|
+
context_usage_ratio: metrics.usagePct,
|
|
309
|
+
context_source: metrics.source,
|
|
310
|
+
estimated_token_savings: savings,
|
|
311
|
+
suggestions,
|
|
312
|
+
reorganization_plan: reorganizationPlan,
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function run(rawInput) {
|
|
318
|
+
const output = buildHookOutput(rawInput);
|
|
319
|
+
if (!output) return { exitCode: 0 };
|
|
320
|
+
return { stdout: JSON.stringify(output), exitCode: 0 };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function main() {
|
|
324
|
+
let input = '';
|
|
325
|
+
const stdinTimeout = setTimeout(() => process.exit(0), 8000);
|
|
326
|
+
process.stdin.setEncoding('utf8');
|
|
327
|
+
process.stdin.on('data', chunk => { input += chunk; });
|
|
328
|
+
process.stdin.on('end', () => {
|
|
329
|
+
clearTimeout(stdinTimeout);
|
|
330
|
+
try {
|
|
331
|
+
const output = buildHookOutput(input);
|
|
332
|
+
if (output) process.stdout.write(JSON.stringify(output));
|
|
333
|
+
} catch (_) {
|
|
334
|
+
process.exit(0);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (require.main === module) {
|
|
340
|
+
main();
|
|
75
341
|
}
|
|
76
342
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
343
|
+
module.exports = {
|
|
344
|
+
run,
|
|
345
|
+
buildHookOutput,
|
|
346
|
+
resolveContextMetrics,
|
|
347
|
+
normalizeRemainingToUsed,
|
|
348
|
+
};
|
|
@@ -718,86 +718,9 @@ function convertRulesToMdc(rulesDir, targetDir) {
|
|
|
718
718
|
}
|
|
719
719
|
}
|
|
720
720
|
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
const agentsDir = path.join(root, "agents", "roles");
|
|
725
|
-
const roleNames = fs.existsSync(agentsDir)
|
|
726
|
-
? fs
|
|
727
|
-
.readdirSync(agentsDir)
|
|
728
|
-
.filter((name) => name.endsWith(".md"))
|
|
729
|
-
.map((name) => path.parse(name).name)
|
|
730
|
-
.sort()
|
|
731
|
-
: [];
|
|
732
|
-
const roleDisplay = {
|
|
733
|
-
"tech-lead": "Tech Lead(技术负责人)",
|
|
734
|
-
"product-manager": "Product Manager(产品经理)",
|
|
735
|
-
"project-manager": "Project Manager(项目管理)",
|
|
736
|
-
architect: "Architect(架构师)",
|
|
737
|
-
"frontend-engineer": "Frontend Engineer(前端开发)",
|
|
738
|
-
"backend-engineer": "Backend Engineer(后端开发)",
|
|
739
|
-
"qa-engineer": "QA Engineer(测试工程师)",
|
|
740
|
-
"devops-engineer": "DevOps Engineer(运维工程师)",
|
|
741
|
-
};
|
|
742
|
-
const lines = [
|
|
743
|
-
OPENCODE_AGENTS_MD_MARKER,
|
|
744
|
-
"# Team Skills Platform — OpenCode Agent Index",
|
|
745
|
-
"",
|
|
746
|
-
"本文件由安装脚本自动生成。在 OpenCode 中与任何角色交互时,可直接引用下列角色和命令。",
|
|
747
|
-
"",
|
|
748
|
-
"## 可用角色",
|
|
749
|
-
"",
|
|
750
|
-
];
|
|
751
|
-
for (const role of roleNames) {
|
|
752
|
-
lines.push(`- **${roleDisplay[role] || role}**: \`plugins/team-skills-platform/agents/roles/${role}.md\``);
|
|
753
|
-
}
|
|
754
|
-
lines.push(
|
|
755
|
-
"",
|
|
756
|
-
"## 核心团队命令",
|
|
757
|
-
"",
|
|
758
|
-
"| 命令 | 用途 |",
|
|
759
|
-
"|------|------|",
|
|
760
|
-
"| `/team-help` | 根据当前阶段、artifacts 与阻塞项推荐下一步主链命令 |",
|
|
761
|
-
"| `/team-intake` | 接收需求并锁定目标、范围、约束 |",
|
|
762
|
-
"| `/team-plan` | 拆解任务、角色分工、依赖与里程碑 |",
|
|
763
|
-
"| `/team-execute` | 驱动研发角色在边界内实施 |",
|
|
764
|
-
"| `/team-review` | 做方案、质量、测试和放行评审 |",
|
|
765
|
-
"| `/team-release` | 做发布准备、上线检查与回滚保障 |",
|
|
766
|
-
"| `/team-closeout` | 在观察窗口结束后做最终收口与 backlog 回写 |",
|
|
767
|
-
"| `/handoff` | 在角色间做结构化交接 |",
|
|
768
|
-
"",
|
|
769
|
-
"## 插件根路径",
|
|
770
|
-
"",
|
|
771
|
-
"`~/.config/opencode/plugins/team-skills-platform/`",
|
|
772
|
-
"",
|
|
773
|
-
`<!-- end ${PLUGIN_NAME} -->`,
|
|
774
|
-
);
|
|
775
|
-
return `${lines.join("\n")}\n`;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
function mergeOpenCodeAgentsMd(targetPath, newContent) {
|
|
779
|
-
const markerEnd = `<!-- end ${PLUGIN_NAME} -->`;
|
|
780
|
-
if (!fs.existsSync(targetPath)) {
|
|
781
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
782
|
-
fs.writeFileSync(targetPath, newContent, "utf8");
|
|
783
|
-
return;
|
|
784
|
-
}
|
|
785
|
-
const existing = fs.readFileSync(targetPath, "utf8");
|
|
786
|
-
if (existing.includes(OPENCODE_AGENTS_MD_MARKER)) {
|
|
787
|
-
const startIdx = existing.indexOf(OPENCODE_AGENTS_MD_MARKER);
|
|
788
|
-
let endIdx = existing.indexOf(markerEnd, startIdx);
|
|
789
|
-
if (endIdx !== -1) {
|
|
790
|
-
endIdx += markerEnd.length;
|
|
791
|
-
if (existing[endIdx] === "\n") {
|
|
792
|
-
endIdx += 1;
|
|
793
|
-
}
|
|
794
|
-
fs.writeFileSync(targetPath, `${existing.slice(0, startIdx)}${newContent}`, "utf8");
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
799
|
-
fs.writeFileSync(targetPath, `${existing}${separator}${newContent}`, "utf8");
|
|
800
|
-
}
|
|
721
|
+
const { generateAgentsMd, mergeAgentsMd } = require("./lib/opencode/generate-agents-md");
|
|
722
|
+
const { convertRoleAgents, convertSpecialistAgents } = require("./lib/opencode/convert-agents");
|
|
723
|
+
const { convertHooksPlugin } = require("./lib/opencode/convert-hooks");
|
|
801
724
|
|
|
802
725
|
function installClaude(root, claudeHome) {
|
|
803
726
|
const pluginDir = path.join(claudeHome, "plugins", PLUGIN_NAME);
|
|
@@ -883,6 +806,51 @@ function installCursor(root, cursorHome) {
|
|
|
883
806
|
console.log(`Converted rules to MDC in ${mdcTarget}`);
|
|
884
807
|
}
|
|
885
808
|
|
|
809
|
+
function generateOpenCodeConfig(root, opencodeHome) {
|
|
810
|
+
const configPath = path.join(opencodeHome, "opencode.json");
|
|
811
|
+
|
|
812
|
+
// 如果配置文件已存在,保留用户自定义配置
|
|
813
|
+
let existingConfig = {};
|
|
814
|
+
if (fs.existsSync(configPath)) {
|
|
815
|
+
try {
|
|
816
|
+
existingConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
817
|
+
} catch (error) {
|
|
818
|
+
console.warn(`Warning: Could not parse existing opencode.json: ${error.message}`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// 读取插件配置
|
|
823
|
+
const pluginConfigPath = path.join(root, ".opencode-plugin", "config.json");
|
|
824
|
+
let pluginConfig = {};
|
|
825
|
+
if (fs.existsSync(pluginConfigPath)) {
|
|
826
|
+
try {
|
|
827
|
+
pluginConfig = JSON.parse(fs.readFileSync(pluginConfigPath, "utf8"));
|
|
828
|
+
} catch (error) {
|
|
829
|
+
console.warn(`Warning: Could not read plugin config: ${error.message}`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// 合并配置
|
|
834
|
+
const config = {
|
|
835
|
+
$schema: "https://opencode.ai/config.json",
|
|
836
|
+
...existingConfig,
|
|
837
|
+
// 确保这些字段始终存在
|
|
838
|
+
instructions: existingConfig.instructions || ["./AGENTS.md"],
|
|
839
|
+
plugin: existingConfig.plugin || [PLUGIN_NAME],
|
|
840
|
+
permission: existingConfig.permission || {
|
|
841
|
+
edit: "allow",
|
|
842
|
+
bash: "allow",
|
|
843
|
+
},
|
|
844
|
+
// 添加插件特定配置
|
|
845
|
+
...pluginConfig,
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
// 写入配置文件
|
|
849
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
850
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
851
|
+
console.log(`Generated OpenCode config at ${configPath}`);
|
|
852
|
+
}
|
|
853
|
+
|
|
886
854
|
function installOpenCode(root, opencodeHome) {
|
|
887
855
|
const pluginDir = path.join(opencodeHome, "plugins", PLUGIN_NAME);
|
|
888
856
|
fs.mkdirSync(path.dirname(pluginDir), { recursive: true });
|
|
@@ -916,25 +884,40 @@ function installOpenCode(root, opencodeHome) {
|
|
|
916
884
|
}
|
|
917
885
|
}
|
|
918
886
|
|
|
887
|
+
// 转换 agents 为 OpenCode 格式(添加 YAML front matter)
|
|
888
|
+
convertRoleAgents(path.join(root, "agents", "roles"), path.join(pluginDir, "agents", "roles"));
|
|
889
|
+
convertSpecialistAgents(path.join(root, "agents", "specialists"), path.join(pluginDir, "agents", "specialists"));
|
|
890
|
+
|
|
891
|
+
// 复制转换后的 agents 到 opencodeHome/agents/ 目录
|
|
919
892
|
const agentsTarget = path.join(opencodeHome, "agents");
|
|
920
893
|
fs.mkdirSync(agentsTarget, { recursive: true });
|
|
921
|
-
const
|
|
922
|
-
if (fs.existsSync(
|
|
894
|
+
const pluginAgentsDir = path.join(pluginDir, "agents");
|
|
895
|
+
if (fs.existsSync(pluginAgentsDir)) {
|
|
923
896
|
for (const agentSubdir of ["roles", "specialists"]) {
|
|
924
|
-
const agentSrcDir = path.join(
|
|
897
|
+
const agentSrcDir = path.join(pluginAgentsDir, agentSubdir);
|
|
925
898
|
if (!fs.existsSync(agentSrcDir)) {
|
|
926
899
|
continue;
|
|
927
900
|
}
|
|
928
901
|
for (const name of fs.readdirSync(agentSrcDir)) {
|
|
929
902
|
const src = path.join(agentSrcDir, name);
|
|
930
903
|
if (fs.statSync(src).isFile()) {
|
|
931
|
-
|
|
904
|
+
const targetName = agentSubdir === "specialists" ? `specialist-${name}` : name;
|
|
905
|
+
fs.copyFileSync(src, path.join(agentsTarget, targetName));
|
|
932
906
|
}
|
|
933
907
|
}
|
|
934
908
|
}
|
|
935
909
|
}
|
|
936
910
|
|
|
937
|
-
|
|
911
|
+
// 使用新的 AGENTS.md 生成脚本
|
|
912
|
+
const agentsMdContent = generateAgentsMd(root);
|
|
913
|
+
mergeAgentsMd(path.join(opencodeHome, "AGENTS.md"), agentsMdContent);
|
|
914
|
+
|
|
915
|
+
// 生成 opencode.json 配置文件
|
|
916
|
+
generateOpenCodeConfig(root, opencodeHome);
|
|
917
|
+
|
|
918
|
+
// 转换 hooks 为 OpenCode 插件格式
|
|
919
|
+
convertHooksPlugin(root, opencodeHome);
|
|
920
|
+
|
|
938
921
|
console.log(`Installed OpenCode plugin to ${pluginDir}`);
|
|
939
922
|
console.log(`Updated AGENTS.md at ${path.join(opencodeHome, "AGENTS.md")}`);
|
|
940
923
|
console.log(`Copied commands to ${commandTarget}`);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|