ostacky 0.7.4 → 0.8.0
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 +17 -19
- package/assets/agents/ostacky.md +45 -512
- package/assets/commands/install-stack.md +2 -2
- package/assets/docs/engram-protocol.md +79 -0
- package/assets/docs/ostacky-reference.md +79 -0
- package/assets/mcp/ostacky-controller/index.js +169 -69
- package/assets/mcp/ostacky-controller/package.json +1 -1
- package/assets/plugins/engram.ts +47 -79
- package/assets/plugins/ostacky-guard.ts +11 -205
- package/assets/plugins/ostacky-plugin.ts +646 -0
- package/assets/skills/brainstorming/SKILL.md +14 -14
- package/assets/skills/execution-mode-evaluation/SKILL.md +9 -9
- package/dist/cli.js +233 -83
- package/manifest.json +30 -30
- package/package.json +1 -1
|
@@ -118,14 +118,10 @@ const TRANSITIONS = {
|
|
|
118
118
|
{ via: 'abandon', to: 'BLOCKED' },
|
|
119
119
|
],
|
|
120
120
|
DISCOVERY: [
|
|
121
|
-
{ via: 'record_discovery', to: '
|
|
121
|
+
{ via: 'record_discovery', to: 'ROUTE_DECISION_PENDING' },
|
|
122
122
|
{ via: 'block', to: 'BLOCKED' },
|
|
123
123
|
{ via: 'abandon', to: 'BLOCKED' },
|
|
124
124
|
],
|
|
125
|
-
LEVEL_RESOLVED: [
|
|
126
|
-
{ via: 'proceed_to_route', to: 'ROUTE_DECISION_PENDING' },
|
|
127
|
-
{ via: 'block', to: 'BLOCKED' },
|
|
128
|
-
],
|
|
129
125
|
ROUTE_DECISION_PENDING: [
|
|
130
126
|
{ via: 'consume_route_decision', to: 'SPECIFICATION', choice: 'SPEC' },
|
|
131
127
|
{ via: 'consume_route_decision', to: 'EXECUTION_ANALYSIS', choice: 'DIRECT' },
|
|
@@ -329,7 +325,6 @@ const STATES = Object.freeze({
|
|
|
329
325
|
INTERPRETATION_PENDING: 'INTERPRETATION_PENDING',
|
|
330
326
|
CLARIFICATION_PENDING: 'CLARIFICATION_PENDING',
|
|
331
327
|
DISCOVERY: 'DISCOVERY',
|
|
332
|
-
LEVEL_RESOLVED: 'LEVEL_RESOLVED',
|
|
333
328
|
ROUTE_DECISION_PENDING: 'ROUTE_DECISION_PENDING',
|
|
334
329
|
SPECIFICATION: 'SPECIFICATION',
|
|
335
330
|
EXECUTION_ANALYSIS: 'EXECUTION_ANALYSIS',
|
|
@@ -375,6 +370,11 @@ const DEFAULT_STATE = Object.freeze({
|
|
|
375
370
|
cacheHitCount: 0, // 5.4 hardening-v2
|
|
376
371
|
cacheMissCount: 0,
|
|
377
372
|
tokenSavingEstimate: 0,
|
|
373
|
+
discoveryCacheHitCount: 0, // mejora-acciones-controller F2
|
|
374
|
+
redundantCallCount: 0,
|
|
375
|
+
cacheMissWithoutPutCount: 0,
|
|
376
|
+
stateCheckCount: 0,
|
|
377
|
+
toolCallCount: 0,
|
|
378
378
|
lastProposal: null, // 8.1
|
|
379
379
|
allowedFiles: {}, // 9.2
|
|
380
380
|
deniedFiles: {}, // 9.2
|
|
@@ -734,6 +734,10 @@ class OstackyController {
|
|
|
734
734
|
const redactRecursively = (obj) => {
|
|
735
735
|
if (!obj || typeof obj !== 'object') return;
|
|
736
736
|
for (const k of Object.keys(obj)) {
|
|
737
|
+
if (k === 'tokenSavingEstimate') {
|
|
738
|
+
if (typeof obj[k] === 'object') redactRecursively(obj[k]);
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
737
741
|
if (SENSITIVE_REDACT_RE.test(k)) {
|
|
738
742
|
obj[k] = '[REDACTED]';
|
|
739
743
|
} else if (typeof obj[k] === 'string' && SENSITIVE_REDACT_RE.test(obj[k])) {
|
|
@@ -975,7 +979,6 @@ class OstackyController {
|
|
|
975
979
|
INTERPRETATION_PENDING: 'Call start_request or proceed_to_discovery first.',
|
|
976
980
|
CLARIFICATION_PENDING: 'Answer the clarification question, then call record_clarification.',
|
|
977
981
|
DISCOVERY: 'Call record_discovery with a level classification.',
|
|
978
|
-
LEVEL_RESOLVED: 'Call proceed_to_route to move to route decision.',
|
|
979
982
|
ROUTE_DECISION_PENDING: 'Call consume_route_decision with SPEC or DIRECT.',
|
|
980
983
|
SPECIFICATION: 'Call spec_complete when specification is done.',
|
|
981
984
|
EXECUTION_ANALYSIS: 'Call record_execution_analysis with a snapshot.',
|
|
@@ -1167,7 +1170,7 @@ class OstackyController {
|
|
|
1167
1170
|
snapshots: { ...this.#state.snapshots, codegraph: compressedSnapshot },
|
|
1168
1171
|
lastProposal,
|
|
1169
1172
|
});
|
|
1170
|
-
await this.#audit('
|
|
1173
|
+
await this.#audit('ROUTE_DECISION_PENDING', 'record_discovery', `level=${level}, default=${defaultChoice}`);
|
|
1171
1174
|
// 8.2: reasoning sin plan → WARN
|
|
1172
1175
|
if (!shownToUser && !isTrivial) {
|
|
1173
1176
|
const auditId = `aud-${Date.now()}-${this.#state.auditSeq}`;
|
|
@@ -1209,6 +1212,29 @@ class OstackyController {
|
|
|
1209
1212
|
auditId: lastAudit?.id || auditId,
|
|
1210
1213
|
};
|
|
1211
1214
|
}
|
|
1215
|
+
// Router determinista: 1+ exige Alternatives si estLines>30||fileCount>2||hasAPI, sino downgrade
|
|
1216
|
+
const fileCount = proposalFiles.length;
|
|
1217
|
+
const hasAPI = !!(snapshot?.hasAPI || snapshot?.reasoning?.hasAPI || snapshot?.hasExplicitContract);
|
|
1218
|
+
const estLinesVal = estLines || snapshot?.estLines || snapshot?.reasoning?.estLines || 0;
|
|
1219
|
+
const isOnePlus = level === '1+';
|
|
1220
|
+
const needsBrainstorming = isOnePlus && (estLinesVal > 30 || fileCount > 2 || hasAPI);
|
|
1221
|
+
const isDowngradeable = isOnePlus && estLinesVal < 30 && fileCount === 1 && !hasAPI;
|
|
1222
|
+
if (isOnePlus && needsBrainstorming) {
|
|
1223
|
+
// mark that Alternatives required — will be checked in openspec-propose gate
|
|
1224
|
+
this.#state._routerNeedsAlternatives = true;
|
|
1225
|
+
this.#state._routerDowngradeSuggested = false;
|
|
1226
|
+
log('info:router_brainstorming_required', { level, estLines: estLinesVal, fileCount, hasAPI });
|
|
1227
|
+
} else if (isDowngradeable) {
|
|
1228
|
+
this.#state._routerNeedsAlternatives = false;
|
|
1229
|
+
this.#state._routerDowngradeSuggested = true;
|
|
1230
|
+
log('info:router_downgrade_to_direct', { level, estLines: estLinesVal, fileCount, hasAPI });
|
|
1231
|
+
// override defaultChoice to DIRECT for downgradeable
|
|
1232
|
+
// keep stored defaultChoice as DIRECT already, but hint downgrade
|
|
1233
|
+
} else {
|
|
1234
|
+
this.#state._routerNeedsAlternatives = false;
|
|
1235
|
+
this.#state._routerDowngradeSuggested = false;
|
|
1236
|
+
}
|
|
1237
|
+
await this.#persist();
|
|
1212
1238
|
// 8.6: Bypass solo para CI
|
|
1213
1239
|
if (process.env.OSTACKY_REQUIRE_CONFIRMATION === 'false' && this.#state.state === 'ROUTE_DECISION_PENDING') {
|
|
1214
1240
|
await this.#audit('AUTO', 'auto-confirm (CI)', `auto-consume ${defaultChoice} for CI`);
|
|
@@ -1232,16 +1258,28 @@ class OstackyController {
|
|
|
1232
1258
|
level,
|
|
1233
1259
|
routeDecisionId: this.#state.routeDecisionId,
|
|
1234
1260
|
defaultChoice,
|
|
1261
|
+
routerNeedsAlternatives: this.#state._routerNeedsAlternatives || false,
|
|
1262
|
+
routerDowngradeSuggested: this.#state._routerDowngradeSuggested || false,
|
|
1235
1263
|
};
|
|
1236
1264
|
}
|
|
1237
1265
|
|
|
1238
1266
|
async proceedToRoute() {
|
|
1239
1267
|
this.#load();
|
|
1268
|
+
// deprecated alias: if already ROUTE_DECISION_PENDING, return no-op deprecated
|
|
1269
|
+
if (this.#state.state === 'ROUTE_DECISION_PENDING') {
|
|
1270
|
+
await this.#audit('WARN', 'proceed_to_route', 'deprecated: already in ROUTE_DECISION_PENDING');
|
|
1271
|
+
return {
|
|
1272
|
+
state: this.#state.state,
|
|
1273
|
+
revision: this.#state.revision,
|
|
1274
|
+
deprecated: true,
|
|
1275
|
+
warning: 'proceed_to_route deprecated, use record_discovery directly',
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1240
1278
|
const to = this.#isAllowedTransition(this.#state.state, 'proceed_to_route');
|
|
1241
1279
|
if (!to) return this.#makeError(`Cannot proceed to route from state ${this.#state.state}`, 'proceed_to_route');
|
|
1242
1280
|
await this.#transition(to);
|
|
1243
1281
|
await this.#audit('ROUTE_DECISION_PENDING', 'proceed_to_route');
|
|
1244
|
-
return { state: this.#state.state, revision: this.#state.revision };
|
|
1282
|
+
return { state: this.#state.state, revision: this.#state.revision, deprecated: true };
|
|
1245
1283
|
}
|
|
1246
1284
|
|
|
1247
1285
|
async abandon({ reason } = {}) {
|
|
@@ -1380,11 +1418,20 @@ class OstackyController {
|
|
|
1380
1418
|
}
|
|
1381
1419
|
// C2: warning if missing codegraphUsed+recommendation and not degraded — snapshot missing also counts
|
|
1382
1420
|
// 1.7: early-exit with taskCount<=2 is valid without codegraphUsed, do not warn
|
|
1383
|
-
|
|
1421
|
+
// mejora-acciones-controller F3: discovery-cache counts as valid evidence if discovery snapshot exists
|
|
1422
|
+
const isDiscoveryCacheEvidence =
|
|
1384
1423
|
snapshot &&
|
|
1385
1424
|
Array.isArray(snapshot.codegraphUsed) &&
|
|
1386
|
-
snapshot.codegraphUsed.
|
|
1387
|
-
|
|
1425
|
+
snapshot.codegraphUsed.includes('discovery-cache') &&
|
|
1426
|
+
this.#state.snapshots.codegraph != null &&
|
|
1427
|
+
Array.isArray(snapshot.expectedTaskIds) &&
|
|
1428
|
+
snapshot.expectedTaskIds.length > 0;
|
|
1429
|
+
const hasEvidence =
|
|
1430
|
+
(snapshot &&
|
|
1431
|
+
Array.isArray(snapshot.codegraphUsed) &&
|
|
1432
|
+
snapshot.codegraphUsed.length > 0 &&
|
|
1433
|
+
snapshot.recommendation != null) ||
|
|
1434
|
+
isDiscoveryCacheEvidence;
|
|
1388
1435
|
if (!hasEvidence && !this.#degraded && !isEarlyExitExec) {
|
|
1389
1436
|
this.#state.codegraphBypassCount = (this.#state.codegraphBypassCount || 0) + 1;
|
|
1390
1437
|
const auditId = `aud-${Date.now()}-${this.#state.auditSeq}`;
|
|
@@ -1728,6 +1775,11 @@ class OstackyController {
|
|
|
1728
1775
|
toolTimeoutCount: this.#state.toolTimeoutCount || 0,
|
|
1729
1776
|
lastToolDurationMs: this.#state.lastToolDurationMs || 0,
|
|
1730
1777
|
stateDurationMs: this.#state.stateDurationMs || 0,
|
|
1778
|
+
discoveryCacheHitCount: this.#state.discoveryCacheHitCount || 0,
|
|
1779
|
+
redundantCallCount: this.#state.redundantCallCount || 0,
|
|
1780
|
+
cacheMissWithoutPutCount: this.#state.cacheMissWithoutPutCount || 0,
|
|
1781
|
+
stateCheckCount: this.#state.stateCheckCount || 0,
|
|
1782
|
+
toolCallCount: this.#state.toolCallCount || 0,
|
|
1731
1783
|
};
|
|
1732
1784
|
}
|
|
1733
1785
|
|
|
@@ -1958,6 +2010,36 @@ class OstackyController {
|
|
|
1958
2010
|
}
|
|
1959
2011
|
}
|
|
1960
2012
|
}
|
|
2013
|
+
// mejora-acciones-controller: hash: prefix alias for validate_edit without full content
|
|
2014
|
+
if (typeof content === 'string' && content.startsWith('hash:') && filePath) {
|
|
2015
|
+
const hashArg = content.slice(5);
|
|
2016
|
+
try {
|
|
2017
|
+
const projectRoot = getProjectRoot(this.#statePath);
|
|
2018
|
+
const absolutePath =
|
|
2019
|
+
filePath.startsWith('/') || /^[A-Za-z]:/.test(filePath)
|
|
2020
|
+
? resolve(filePath)
|
|
2021
|
+
: resolve(projectRoot, filePath);
|
|
2022
|
+
const currentHash = fastFingerprint(absolutePath);
|
|
2023
|
+
if (
|
|
2024
|
+
currentHash &&
|
|
2025
|
+
hashArg === currentHash &&
|
|
2026
|
+
this.#state.lastValidated?.filePath === filePath &&
|
|
2027
|
+
this.#state.lastValidated?.hash === currentHash
|
|
2028
|
+
) {
|
|
2029
|
+
try {
|
|
2030
|
+
content = readFileSync(absolutePath, 'utf8');
|
|
2031
|
+
} catch {
|
|
2032
|
+
return { outcome: 'CONFLICT', reason: 'stale fingerprint', filePath };
|
|
2033
|
+
}
|
|
2034
|
+
} else {
|
|
2035
|
+
this.#state.staleContentAttempts = (this.#state.staleContentAttempts || 0) + 1;
|
|
2036
|
+
await this.#persist();
|
|
2037
|
+
return { outcome: 'CONFLICT', reason: 'stale fingerprint', filePath };
|
|
2038
|
+
}
|
|
2039
|
+
} catch (e) {
|
|
2040
|
+
return { outcome: 'CONFLICT', reason: 'hash validation failed' };
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
1961
2043
|
// 5.3: optimization — si fastFingerprint no cambió, no re-enviar content completo
|
|
1962
2044
|
if (
|
|
1963
2045
|
(typeof content !== 'string' || content.length === 0) &&
|
|
@@ -2289,14 +2371,13 @@ function safeHandler(fn, options = {}) {
|
|
|
2289
2371
|
|
|
2290
2372
|
const server = new McpServer({
|
|
2291
2373
|
name: 'ostacky-controller',
|
|
2292
|
-
version: '0.
|
|
2374
|
+
version: '0.8.0',
|
|
2293
2375
|
});
|
|
2294
2376
|
|
|
2295
2377
|
server.registerTool(
|
|
2296
2378
|
'start_request',
|
|
2297
2379
|
{
|
|
2298
|
-
description:
|
|
2299
|
-
'Start or resume a request. By default, resumes in-progress work (non-terminal states). Use force=true to always reset.',
|
|
2380
|
+
description: 'Start or resume request',
|
|
2300
2381
|
inputSchema: z.object({
|
|
2301
2382
|
requestId: z.string().optional().describe('Unique request ID'),
|
|
2302
2383
|
changeId: z.string().optional().describe('Optional change ID for OpenSpec tracking'),
|
|
@@ -2312,8 +2393,7 @@ server.registerTool(
|
|
|
2312
2393
|
server.registerTool(
|
|
2313
2394
|
'request_clarification',
|
|
2314
2395
|
{
|
|
2315
|
-
description:
|
|
2316
|
-
'Pause execution to ask the user for clarification. Use when the request is too vague to classify. Transitions to CLARIFICATION_PENDING — you MUST stop and wait for user response.',
|
|
2396
|
+
description: 'Request clarification',
|
|
2317
2397
|
inputSchema: z.object({
|
|
2318
2398
|
question: z.string().optional().describe('The clarification question'),
|
|
2319
2399
|
}),
|
|
@@ -2327,7 +2407,7 @@ server.registerTool(
|
|
|
2327
2407
|
server.registerTool(
|
|
2328
2408
|
'record_clarification',
|
|
2329
2409
|
{
|
|
2330
|
-
description: 'Record
|
|
2410
|
+
description: 'Record clarification',
|
|
2331
2411
|
inputSchema: z.object({}),
|
|
2332
2412
|
},
|
|
2333
2413
|
safeHandler(async () => {
|
|
@@ -2339,8 +2419,7 @@ server.registerTool(
|
|
|
2339
2419
|
server.registerTool(
|
|
2340
2420
|
'record_discovery',
|
|
2341
2421
|
{
|
|
2342
|
-
description:
|
|
2343
|
-
'Record discovery complete with level classification. From INTERPRETATION_PENDING goes to ROUTE_DECISION_PENDING. From DISCOVERY goes to LEVEL_RESOLVED.',
|
|
2422
|
+
description: 'Record discovery (level)',
|
|
2344
2423
|
inputSchema: z.object({
|
|
2345
2424
|
level: z.enum(['0', '0+1', '1+']).describe('Impact level'),
|
|
2346
2425
|
routeDecisionId: z.string().optional().describe('Unique route decision ID'),
|
|
@@ -2356,7 +2435,7 @@ server.registerTool(
|
|
|
2356
2435
|
server.registerTool(
|
|
2357
2436
|
'consume_route_decision',
|
|
2358
2437
|
{
|
|
2359
|
-
description: 'Consume
|
|
2438
|
+
description: 'Consume route decision (SPEC/DIRECT)',
|
|
2360
2439
|
inputSchema: z.object({
|
|
2361
2440
|
decisionId: z.string().describe('Route decision ID from record_discovery'),
|
|
2362
2441
|
choice: z.enum(['SPEC', 'DIRECT']).describe('Route choice'),
|
|
@@ -2371,7 +2450,7 @@ server.registerTool(
|
|
|
2371
2450
|
server.registerTool(
|
|
2372
2451
|
'spec_complete',
|
|
2373
2452
|
{
|
|
2374
|
-
description: '
|
|
2453
|
+
description: 'Spec complete',
|
|
2375
2454
|
inputSchema: z.object({}),
|
|
2376
2455
|
},
|
|
2377
2456
|
safeHandler(async () => {
|
|
@@ -2383,7 +2462,7 @@ server.registerTool(
|
|
|
2383
2462
|
server.registerTool(
|
|
2384
2463
|
'record_execution_analysis',
|
|
2385
2464
|
{
|
|
2386
|
-
description: 'Record execution analysis
|
|
2465
|
+
description: 'Record execution analysis',
|
|
2387
2466
|
inputSchema: z.object({
|
|
2388
2467
|
executionDecisionId: z.string().optional().describe('Unique execution decision ID'),
|
|
2389
2468
|
snapshot: z.any().optional().describe('Execution analysis snapshot'),
|
|
@@ -2398,7 +2477,7 @@ server.registerTool(
|
|
|
2398
2477
|
server.registerTool(
|
|
2399
2478
|
'consume_execution_decision',
|
|
2400
2479
|
{
|
|
2401
|
-
description: 'Consume
|
|
2480
|
+
description: 'Consume execution decision (INLINE/SUBAGENT)',
|
|
2402
2481
|
inputSchema: z.object({
|
|
2403
2482
|
decisionId: z.string().describe('Execution decision ID from record_execution_analysis'),
|
|
2404
2483
|
mode: z.enum(['INLINE', 'SUBAGENT_DRIVEN']).describe('Execution mode'),
|
|
@@ -2431,7 +2510,7 @@ server.registerTool(
|
|
|
2431
2510
|
server.registerTool(
|
|
2432
2511
|
'sync_complete',
|
|
2433
2512
|
{
|
|
2434
|
-
description: '
|
|
2513
|
+
description: 'Sync complete',
|
|
2435
2514
|
inputSchema: z.object({}),
|
|
2436
2515
|
},
|
|
2437
2516
|
safeHandler(async () => {
|
|
@@ -2443,7 +2522,7 @@ server.registerTool(
|
|
|
2443
2522
|
server.registerTool(
|
|
2444
2523
|
'block',
|
|
2445
2524
|
{
|
|
2446
|
-
description: '
|
|
2525
|
+
description: 'Block',
|
|
2447
2526
|
inputSchema: z.object({
|
|
2448
2527
|
reason: z.string().optional().describe('Reason for blocking'),
|
|
2449
2528
|
}),
|
|
@@ -2457,7 +2536,7 @@ server.registerTool(
|
|
|
2457
2536
|
server.registerTool(
|
|
2458
2537
|
'replan',
|
|
2459
2538
|
{
|
|
2460
|
-
description: 'Replan
|
|
2539
|
+
description: 'Replan',
|
|
2461
2540
|
inputSchema: z.object({
|
|
2462
2541
|
reason: z.string().optional().describe('Reason for replanning'),
|
|
2463
2542
|
}),
|
|
@@ -2503,11 +2582,11 @@ server.registerTool(
|
|
|
2503
2582
|
server.registerTool(
|
|
2504
2583
|
'get_audit',
|
|
2505
2584
|
{
|
|
2506
|
-
description: 'Get
|
|
2585
|
+
description: 'Get audit (paginated)',
|
|
2507
2586
|
inputSchema: z.object({
|
|
2508
2587
|
limit: z.number().optional().describe('Max entries (default 20)'),
|
|
2509
2588
|
offset: z.number().optional().describe('Offset from end (default 0)'),
|
|
2510
|
-
phase: z.string().optional().describe('Filter by phase (e.g. WARN,
|
|
2589
|
+
phase: z.string().optional().describe('Filter by phase (e.g. WARN, ROUTE_DECISION_PENDING)'),
|
|
2511
2590
|
since: z.number().optional().describe('Filter by timestamp >= since'),
|
|
2512
2591
|
}),
|
|
2513
2592
|
},
|
|
@@ -2523,8 +2602,7 @@ server.registerTool(
|
|
|
2523
2602
|
server.registerTool(
|
|
2524
2603
|
'get_metrics',
|
|
2525
2604
|
{
|
|
2526
|
-
description:
|
|
2527
|
-
'Get controller metrics read-only (revision, state, degraded, taskCounts, auditSize, stateFileSize, diskFreeMB, uptimeMs, stateOversizedCount, codegraphBypassCount)',
|
|
2605
|
+
description: 'Get metrics',
|
|
2528
2606
|
inputSchema: z.object({}),
|
|
2529
2607
|
},
|
|
2530
2608
|
safeHandler(
|
|
@@ -2539,28 +2617,28 @@ server.registerTool(
|
|
|
2539
2617
|
server.registerTool(
|
|
2540
2618
|
'record_cache_hit',
|
|
2541
2619
|
{
|
|
2542
|
-
description:
|
|
2543
|
-
'Record a CodeGraph cache hit — increments cacheHitCount and tokenSavingEstimate. Call after reusing getCachedCodegraph result.',
|
|
2620
|
+
description: 'Deprecated: cache hit',
|
|
2544
2621
|
inputSchema: z.object({
|
|
2545
2622
|
tokensSaved: z.number().optional().describe('Estimated tokens saved (default 500)'),
|
|
2546
2623
|
}),
|
|
2547
2624
|
},
|
|
2548
2625
|
safeHandler(async ({ tokensSaved }) => {
|
|
2549
|
-
log('tool:record_cache_hit', { tokensSaved });
|
|
2550
|
-
|
|
2626
|
+
log('tool:record_cache_hit', { tokensSaved, deprecated: true });
|
|
2627
|
+
const r = await controller.recordCacheHit({ tokensSaved });
|
|
2628
|
+
return { ...r, deprecated: true };
|
|
2551
2629
|
})
|
|
2552
2630
|
);
|
|
2553
2631
|
|
|
2554
2632
|
server.registerTool(
|
|
2555
2633
|
'record_cache_miss',
|
|
2556
2634
|
{
|
|
2557
|
-
description:
|
|
2558
|
-
'Record a CodeGraph cache miss — increments cacheMissCount. Call after getCachedCodegraph returns null.',
|
|
2635
|
+
description: 'Deprecated: cache miss',
|
|
2559
2636
|
inputSchema: z.object({}),
|
|
2560
2637
|
},
|
|
2561
2638
|
safeHandler(async () => {
|
|
2562
|
-
log('tool:record_cache_miss');
|
|
2563
|
-
|
|
2639
|
+
log('tool:record_cache_miss', { deprecated: true });
|
|
2640
|
+
const r = await controller.recordCacheMiss();
|
|
2641
|
+
return { ...r, deprecated: true };
|
|
2564
2642
|
})
|
|
2565
2643
|
);
|
|
2566
2644
|
|
|
@@ -2584,7 +2662,7 @@ server.registerTool(
|
|
|
2584
2662
|
'check_file_access',
|
|
2585
2663
|
{
|
|
2586
2664
|
description:
|
|
2587
|
-
'Check if file is sensitive and requires ALLOW.
|
|
2665
|
+
'[deprecated if plugin active] Check if file is sensitive and requires ALLOW. plugin enforces when active.',
|
|
2588
2666
|
inputSchema: z.object({
|
|
2589
2667
|
filePath: z.string().describe('File path to check'),
|
|
2590
2668
|
reason: z.string().optional().describe('Reason for access'),
|
|
@@ -2592,6 +2670,21 @@ server.registerTool(
|
|
|
2592
2670
|
},
|
|
2593
2671
|
safeHandler(async ({ filePath, reason }) => {
|
|
2594
2672
|
log('tool:check_file_access', { filePath });
|
|
2673
|
+
try {
|
|
2674
|
+
const pluginPath = join(process.cwd(), '.opencode', 'plugins', 'ostacky-plugin.ts');
|
|
2675
|
+
const assetsPath = join(process.cwd(), 'assets', 'plugins', 'ostacky-plugin.ts');
|
|
2676
|
+
const legacyPluginPath = join(process.cwd(), '.opencode', 'plugins', 'ostacky-controller.ts');
|
|
2677
|
+
const legacyAssetsPath = join(process.cwd(), 'assets', 'plugins', 'ostacky-controller.ts');
|
|
2678
|
+
if (
|
|
2679
|
+
existsSync(pluginPath) ||
|
|
2680
|
+
existsSync(assetsPath) ||
|
|
2681
|
+
existsSync(legacyPluginPath) ||
|
|
2682
|
+
existsSync(legacyAssetsPath)
|
|
2683
|
+
) {
|
|
2684
|
+
const res = await controller.checkFileAccess({ filePath, reason });
|
|
2685
|
+
return { ...res, deprecated: true, hint: 'plugin enforces' };
|
|
2686
|
+
}
|
|
2687
|
+
} catch {}
|
|
2595
2688
|
return await controller.checkFileAccess({ filePath, reason });
|
|
2596
2689
|
})
|
|
2597
2690
|
);
|
|
@@ -2599,7 +2692,7 @@ server.registerTool(
|
|
|
2599
2692
|
server.registerTool(
|
|
2600
2693
|
'consume_file_access_decision',
|
|
2601
2694
|
{
|
|
2602
|
-
description: 'Consume file access decision
|
|
2695
|
+
description: 'Consume file access decision',
|
|
2603
2696
|
inputSchema: z.object({
|
|
2604
2697
|
decisionId: z.string().describe('Decision ID from check_file_access'),
|
|
2605
2698
|
choice: z.enum(['ALLOW', 'DENY']).describe('Choice'),
|
|
@@ -2614,20 +2707,19 @@ server.registerTool(
|
|
|
2614
2707
|
server.registerTool(
|
|
2615
2708
|
'proceed_to_route',
|
|
2616
2709
|
{
|
|
2617
|
-
description:
|
|
2618
|
-
'Proceed from LEVEL_RESOLVED to ROUTE_DECISION_PENDING after discovery is confirmed. Only valid from LEVEL_RESOLVED — call this after asking the user about the route decision.',
|
|
2710
|
+
description: 'Proceed to route (deprecated)',
|
|
2619
2711
|
inputSchema: z.object({}),
|
|
2620
2712
|
},
|
|
2621
2713
|
safeHandler(async () => {
|
|
2622
|
-
log('tool:proceed_to_route');
|
|
2623
|
-
return
|
|
2714
|
+
log('tool:proceed_to_route deprecated');
|
|
2715
|
+
return { deprecated: true, state: 'ROUTE_DECISION_PENDING' };
|
|
2624
2716
|
})
|
|
2625
2717
|
);
|
|
2626
2718
|
|
|
2627
2719
|
server.registerTool(
|
|
2628
2720
|
'abandon',
|
|
2629
2721
|
{
|
|
2630
|
-
description: 'Abandon
|
|
2722
|
+
description: 'Abandon request',
|
|
2631
2723
|
inputSchema: z.object({
|
|
2632
2724
|
reason: z.string().optional().describe('Reason for abandoning'),
|
|
2633
2725
|
}),
|
|
@@ -2669,7 +2761,7 @@ server.registerTool(
|
|
|
2669
2761
|
server.registerTool(
|
|
2670
2762
|
'get_state',
|
|
2671
2763
|
{
|
|
2672
|
-
description: 'Get
|
|
2764
|
+
description: 'Get state',
|
|
2673
2765
|
inputSchema: z.object({}),
|
|
2674
2766
|
},
|
|
2675
2767
|
safeHandler(
|
|
@@ -2683,7 +2775,7 @@ server.registerTool(
|
|
|
2683
2775
|
server.registerTool(
|
|
2684
2776
|
'get_tasks',
|
|
2685
2777
|
{
|
|
2686
|
-
description: 'Get
|
|
2778
|
+
description: 'Get tasks',
|
|
2687
2779
|
inputSchema: z.object({}),
|
|
2688
2780
|
},
|
|
2689
2781
|
safeHandler(
|
|
@@ -2697,7 +2789,7 @@ server.registerTool(
|
|
|
2697
2789
|
server.registerTool(
|
|
2698
2790
|
'get_available_transitions',
|
|
2699
2791
|
{
|
|
2700
|
-
description: 'Get
|
|
2792
|
+
description: 'Get available transitions',
|
|
2701
2793
|
inputSchema: z.object({}),
|
|
2702
2794
|
},
|
|
2703
2795
|
safeHandler(
|
|
@@ -2727,7 +2819,7 @@ server.registerTool(
|
|
|
2727
2819
|
server.registerTool(
|
|
2728
2820
|
'get_handoff',
|
|
2729
2821
|
{
|
|
2730
|
-
description: '
|
|
2822
|
+
description: 'Get handoff',
|
|
2731
2823
|
inputSchema: z.object({}),
|
|
2732
2824
|
},
|
|
2733
2825
|
safeHandler(
|
|
@@ -2741,7 +2833,7 @@ server.registerTool(
|
|
|
2741
2833
|
server.registerTool(
|
|
2742
2834
|
'clear_handoff',
|
|
2743
2835
|
{
|
|
2744
|
-
description: '
|
|
2836
|
+
description: 'Clear handoff',
|
|
2745
2837
|
inputSchema: z.object({}),
|
|
2746
2838
|
},
|
|
2747
2839
|
safeHandler(async () => {
|
|
@@ -2753,15 +2845,19 @@ server.registerTool(
|
|
|
2753
2845
|
'check_pending_state',
|
|
2754
2846
|
{
|
|
2755
2847
|
description:
|
|
2756
|
-
'Check if
|
|
2757
|
-
'MUST be called before ANY tool call when controller is available. ' +
|
|
2758
|
-
'Returns ALLOW or BLOCKED with reason. ' +
|
|
2759
|
-
'EXCEPTION: controller tools (consume_route_decision, consume_execution_decision, ' +
|
|
2760
|
-
'record_clarification, abandon) are ALWAYS allowed — they unlock the state.',
|
|
2848
|
+
'[deprecated] Check if in pending state. Returns ALLOW/BLOCKED. plugin enforces — deprecated:true if plugin active',
|
|
2761
2849
|
inputSchema: z.object({}),
|
|
2762
2850
|
},
|
|
2763
2851
|
safeHandler(
|
|
2764
2852
|
async () => {
|
|
2853
|
+
// If plugin active, delegate to hint
|
|
2854
|
+
try {
|
|
2855
|
+
const pluginPath = join(process.cwd(), '.opencode', 'plugins', 'ostacky-controller.ts');
|
|
2856
|
+
const assetsPath = join(process.cwd(), 'assets', 'plugins', 'ostacky-controller.ts');
|
|
2857
|
+
if (existsSync(pluginPath) || existsSync(assetsPath)) {
|
|
2858
|
+
return { deprecated: true, hint: 'plugin enforces', status: 'ALLOW' };
|
|
2859
|
+
}
|
|
2860
|
+
} catch {}
|
|
2765
2861
|
const state = await controller.getState();
|
|
2766
2862
|
const pendingStates = ['CLARIFICATION_PENDING', 'ROUTE_DECISION_PENDING', 'EXECUTION_DECISION_PENDING'];
|
|
2767
2863
|
if (pendingStates.includes(state.state)) {
|
|
@@ -2782,21 +2878,11 @@ server.registerTool(
|
|
|
2782
2878
|
server.registerTool(
|
|
2783
2879
|
'validate_edit',
|
|
2784
2880
|
{
|
|
2785
|
-
description:
|
|
2786
|
-
'Validate an edit against current file content. Returns EDITABLE, ALREADY_APPLIED, or CONFLICT. ' +
|
|
2787
|
-
'Call BEFORE executing an edit tool. Only valid in EXECUTING_INLINE or EXECUTING_SUBAGENTS states. ' +
|
|
2788
|
-
'IMPORTANT: content parameter is REQUIRED. Read the file first, then pass the full content.',
|
|
2881
|
+
description: '[deprecated] Validate edit',
|
|
2789
2882
|
inputSchema: z.object({
|
|
2790
2883
|
oldString: z.string().describe('The exact string to find in content (must be unique).'),
|
|
2791
2884
|
newString: z.string().describe('The replacement string.'),
|
|
2792
|
-
content: z
|
|
2793
|
-
.string()
|
|
2794
|
-
.describe(
|
|
2795
|
-
'REQUIRED — The full file content. ' +
|
|
2796
|
-
'You MUST read the file first with the Read tool, then pass the complete content here. ' +
|
|
2797
|
-
'Example: call Read on the file, store the output, then call validate_edit with that content. ' +
|
|
2798
|
-
'Without this parameter, validate_edit will fail.'
|
|
2799
|
-
),
|
|
2885
|
+
content: z.string().describe('Required: full file content or hash:<fp> if fingerprint unchanged.'),
|
|
2800
2886
|
taskId: z.string().optional().describe('Optional task ID for tracking.'),
|
|
2801
2887
|
filePath: z.string().optional().describe('Optional file path for traversal validation.'),
|
|
2802
2888
|
}),
|
|
@@ -2809,6 +2895,20 @@ server.registerTool(
|
|
|
2809
2895
|
hasContent: !!content,
|
|
2810
2896
|
filePath,
|
|
2811
2897
|
});
|
|
2898
|
+
try {
|
|
2899
|
+
const pluginPath = join(process.cwd(), '.opencode', 'plugins', 'ostacky-plugin.ts');
|
|
2900
|
+
const assetsPath = join(process.cwd(), 'assets', 'plugins', 'ostacky-plugin.ts');
|
|
2901
|
+
const legacyPluginPath = join(process.cwd(), '.opencode', 'plugins', 'ostacky-controller.ts');
|
|
2902
|
+
const legacyAssetsPath = join(process.cwd(), 'assets', 'plugins', 'ostacky-controller.ts');
|
|
2903
|
+
if (
|
|
2904
|
+
existsSync(pluginPath) ||
|
|
2905
|
+
existsSync(assetsPath) ||
|
|
2906
|
+
existsSync(legacyPluginPath) ||
|
|
2907
|
+
existsSync(legacyAssetsPath)
|
|
2908
|
+
) {
|
|
2909
|
+
return { deprecated: true, hint: 'plugin enforces' };
|
|
2910
|
+
}
|
|
2911
|
+
} catch {}
|
|
2812
2912
|
if (typeof content !== 'string' || typeof oldString !== 'string' || typeof newString !== 'string') {
|
|
2813
2913
|
return {
|
|
2814
2914
|
outcome: 'CONFLICT',
|
|
@@ -2871,7 +2971,7 @@ function setupGracefulShutdown(ctrl) {
|
|
|
2871
2971
|
}
|
|
2872
2972
|
|
|
2873
2973
|
async function main() {
|
|
2874
|
-
log('Starting ostacky-controller MCP v0.
|
|
2974
|
+
log('Starting ostacky-controller MCP v0.8.0...');
|
|
2875
2975
|
log('State path:', { path: statePath });
|
|
2876
2976
|
// Clean up stale tmp/lock files from previous runs
|
|
2877
2977
|
cleanupTmpFiles(statePath);
|