ostacky 0.8.8 → 0.9.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 +12 -12
- package/assets/agents/ostacky.md +7 -7
- package/assets/commands/install-stack.md +37 -29
- package/assets/docs/ostacky-reference.md +1 -1
- package/assets/mcp/ostacky-controller/controller-core.js +2 -0
- package/assets/mcp/ostacky-controller/index.js +248 -67
- package/assets/mcp/ostacky-controller/package.json +1 -1
- package/assets/plugins/controller-core.ts +2 -0
- package/assets/plugins/engram.ts +648 -646
- package/assets/plugins/ostacky-plugin.ts +258 -214
- package/dist/cli.js +266 -96
- package/manifest.json +28 -28
- package/package.json +1 -1
|
@@ -176,6 +176,53 @@ export function parseTasksMd(changeId, statePath) {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
export function parseTaskVerifications(changeId, statePath) {
|
|
180
|
+
if (!changeId || typeof changeId !== 'string') return {};
|
|
181
|
+
try {
|
|
182
|
+
const projectRoot = getProjectRoot(statePath);
|
|
183
|
+
const tasksPath = join(projectRoot, 'openspec', 'changes', changeId, 'tasks.md');
|
|
184
|
+
let content = null;
|
|
185
|
+
if (existsSync(tasksPath)) {
|
|
186
|
+
content = readFileSync(tasksPath, 'utf-8');
|
|
187
|
+
} else {
|
|
188
|
+
try {
|
|
189
|
+
const archiveDir = join(projectRoot, 'openspec', 'changes', 'archive');
|
|
190
|
+
if (existsSync(archiveDir)) {
|
|
191
|
+
for (const entry of readdirSync(archiveDir)) {
|
|
192
|
+
if (entry.endsWith(`-${changeId}`) || entry === changeId) {
|
|
193
|
+
const alt = join(archiveDir, entry, 'tasks.md');
|
|
194
|
+
if (existsSync(alt)) {
|
|
195
|
+
content = readFileSync(alt, 'utf-8');
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch {}
|
|
202
|
+
if (content === null) return {};
|
|
203
|
+
}
|
|
204
|
+
const verifs = {};
|
|
205
|
+
const lines = content.split('\n');
|
|
206
|
+
for (const line of lines) {
|
|
207
|
+
const m = line.match(/^- \[[ x]\]\s+([A-Za-z0-9][A-Za-z0-9\-_.\/:]*)/);
|
|
208
|
+
if (!m) continue;
|
|
209
|
+
const id = m[1];
|
|
210
|
+
if (!isValidTaskId(id)) continue;
|
|
211
|
+
const idx = line.indexOf('verificar:');
|
|
212
|
+
if (idx !== -1) {
|
|
213
|
+
const v = line
|
|
214
|
+
.slice(idx + 'verificar:'.length)
|
|
215
|
+
.trim()
|
|
216
|
+
.slice(0, 200);
|
|
217
|
+
if (v) verifs[id] = v;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return verifs;
|
|
221
|
+
} catch {
|
|
222
|
+
return {};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
179
226
|
function isValidTaskId(taskId) {
|
|
180
227
|
return typeof taskId === 'string' && /^[a-zA-Z0-9-_.\/:]+$/.test(taskId);
|
|
181
228
|
}
|
|
@@ -1662,16 +1709,62 @@ class OstackyController {
|
|
|
1662
1709
|
else if (cur !== stored) staleFiles.push(`${fp} (stale fingerprint)`);
|
|
1663
1710
|
}
|
|
1664
1711
|
} catch {}
|
|
1665
|
-
|
|
1712
|
+
// Generico: verificar tasks con verificacion definida
|
|
1713
|
+
let verificationFailed = [];
|
|
1714
|
+
try {
|
|
1715
|
+
const verifs = this.#state.taskVerifications || {};
|
|
1716
|
+
for (const [tid, verif] of Object.entries(verifs)) {
|
|
1717
|
+
const task = this.#state.tasks[tid];
|
|
1718
|
+
if (!task || task.status !== 'COMPLETED') continue;
|
|
1719
|
+
let needle = String(verif).trim();
|
|
1720
|
+
if (needle.startsWith('codegraph:')) needle = needle.slice('codegraph:'.length).trim();
|
|
1721
|
+
else if (needle.startsWith('file:')) {
|
|
1722
|
+
const rest = needle.slice('file:'.length).trim();
|
|
1723
|
+
const parts = rest.split('contiene');
|
|
1724
|
+
needle = (parts[1] || parts[0] || '').trim() || rest;
|
|
1725
|
+
} else if (needle.startsWith('test:')) continue;
|
|
1726
|
+
if (!needle) continue;
|
|
1727
|
+
let content = null;
|
|
1728
|
+
const fp = task.filePath;
|
|
1729
|
+
if (fp) {
|
|
1730
|
+
try {
|
|
1731
|
+
const abs =
|
|
1732
|
+
fp.startsWith('/') || /^[A-Za-z]:/.test(fp)
|
|
1733
|
+
? resolve(fp)
|
|
1734
|
+
: resolve(getProjectRoot(this.#statePath), fp);
|
|
1735
|
+
if (existsSync(abs)) content = readFileSync(abs, 'utf-8');
|
|
1736
|
+
} catch {}
|
|
1737
|
+
}
|
|
1738
|
+
const found = content ? content.includes(needle) : false;
|
|
1739
|
+
if (!found) verificationFailed.push(`${tid}: ${verif} no encontrado en ${fp || 'archivo'}`);
|
|
1740
|
+
}
|
|
1741
|
+
} catch {}
|
|
1742
|
+
const hasBlocking = pending.length > 0 || staleFiles.length > 0 || verificationFailed.length > 0;
|
|
1666
1743
|
if (hasBlocking && !force) {
|
|
1744
|
+
const pendingMsg =
|
|
1745
|
+
pending.length > 0 ? `Te faltan ${pending.length} task(s) por completar: ${pending.join(', ')}.` : '';
|
|
1746
|
+
const staleMsg =
|
|
1747
|
+
staleFiles.length > 0
|
|
1748
|
+
? `Hay ${staleFiles.length} archivo(s) con fingerprint stale o faltante: ${staleFiles.join(', ')}.`
|
|
1749
|
+
: '';
|
|
1750
|
+
const verifMsg =
|
|
1751
|
+
verificationFailed.length > 0
|
|
1752
|
+
? `Hay ${verificationFailed.length} verificación(es) fallida(s): ${verificationFailed.join(', ')}.`
|
|
1753
|
+
: '';
|
|
1754
|
+
const detail = [pendingMsg, staleMsg, verifMsg].filter(Boolean).join(' ');
|
|
1755
|
+
const isVerif = verificationFailed.length > 0;
|
|
1667
1756
|
return {
|
|
1668
|
-
error:
|
|
1757
|
+
error: isVerif
|
|
1758
|
+
? `verification failed - No puedo cerrar: ${verificationFailed.length} verificación(es) fallida(s)`
|
|
1759
|
+
: staleFiles.length > 0
|
|
1760
|
+
? `stale fingerprints - No puedo cerrar: ${pending.length} task(s) pendiente(s) y ${staleFiles.length} archivo(s) stale`
|
|
1761
|
+
: `tasks incomplete - No puedo cerrar: te faltan ${pending.length} task(s) por completar`,
|
|
1669
1762
|
pending,
|
|
1670
1763
|
staleFiles: staleFiles.length ? staleFiles : undefined,
|
|
1764
|
+
verificationFailed: verificationFailed.length ? verificationFailed : undefined,
|
|
1671
1765
|
current_state: this.#state.state,
|
|
1672
1766
|
attempted_transition: 'implementation_complete',
|
|
1673
|
-
suggestion:
|
|
1674
|
-
'Complete pending tasks via complete_task or retry with {force:true} after explicit user confirmation',
|
|
1767
|
+
suggestion: `${detail} Completá cada task con Read fresco → validate_edit → edit → complete_task (con verificación) y luego reintentá implementation_complete sin force. Solo si querés descartar trabajo pendiente, escribí literal "confirmo forzar" en tu mensaje y reintentá con {force:true}. No propongo cancelar; propongo completar.`,
|
|
1675
1768
|
};
|
|
1676
1769
|
}
|
|
1677
1770
|
if (hasBlocking && force) {
|
|
@@ -1679,16 +1772,23 @@ class OstackyController {
|
|
|
1679
1772
|
const recentAudit = [...(this.#state.audit || []).slice(-5), ...this.#auditBuffer.slice(-5)];
|
|
1680
1773
|
const hasHuman = recentAudit.some((e) => e.reasoning && /forzar|confirmo|force/i.test(e.reasoning));
|
|
1681
1774
|
if (!hasHuman) {
|
|
1775
|
+
const pendingMsg =
|
|
1776
|
+
pending.length > 0 ? `Te faltan ${pending.length} task(s): ${pending.join(', ')}.` : '';
|
|
1777
|
+
const staleMsg = staleFiles.length > 0 ? `Archivos stale: ${staleFiles.join(', ')}.` : '';
|
|
1778
|
+
const verifMsg =
|
|
1779
|
+
verificationFailed.length > 0 ? `Verificaciones fallidas: ${verificationFailed.join(', ')}.` : '';
|
|
1780
|
+
const detail = [pendingMsg, staleMsg, verifMsg].filter(Boolean).join(' ');
|
|
1682
1781
|
return {
|
|
1683
|
-
error: 'force requires human confirmation',
|
|
1782
|
+
error: 'force requires human confirmation - force requiere confirmación humana explícita',
|
|
1684
1783
|
pending,
|
|
1685
1784
|
staleFiles: staleFiles.length ? staleFiles : undefined,
|
|
1785
|
+
verificationFailed: verificationFailed.length ? verificationFailed : undefined,
|
|
1686
1786
|
current_state: this.#state.state,
|
|
1687
1787
|
attempted_transition: 'implementation_complete',
|
|
1688
|
-
suggestion:
|
|
1788
|
+
suggestion: `${detail} Para forzar el cierre, escribí literal "confirmo forzar" en tu mensaje (quedará auditado) y reintentá con {force:true}. Si no querés forzar, completá los pendientes y reintentá sin force.`,
|
|
1689
1789
|
};
|
|
1690
1790
|
}
|
|
1691
|
-
const all = [...pending, ...staleFiles].join(',');
|
|
1791
|
+
const all = [...pending, ...staleFiles, ...verificationFailed].join(',');
|
|
1692
1792
|
await this.#audit('FORCE', 'implementation_complete', `forced with pending: ${all}`);
|
|
1693
1793
|
}
|
|
1694
1794
|
await this.#transition(to);
|
|
@@ -1806,6 +1906,12 @@ class OstackyController {
|
|
|
1806
1906
|
}
|
|
1807
1907
|
this.#state.expectedTasks = [...tasksMdIds];
|
|
1808
1908
|
this.#state.expectedTaskCount = tasksMdIds.length;
|
|
1909
|
+
// --- Generico: extraer verificaciones de tasks.md (codegraph:/file:/test:)
|
|
1910
|
+
try {
|
|
1911
|
+
const verifs = parseTaskVerifications(this.#state.changeId, this.#statePath);
|
|
1912
|
+
this.#state.taskVerifications = Object.keys(verifs).length ? verifs : null;
|
|
1913
|
+
if (!this.#state.taskVerificationResults) this.#state.taskVerificationResults = {};
|
|
1914
|
+
} catch {}
|
|
1809
1915
|
await this.#persist();
|
|
1810
1916
|
await this.#audit(
|
|
1811
1917
|
'EXECUTING',
|
|
@@ -1816,15 +1922,18 @@ class OstackyController {
|
|
|
1816
1922
|
ok: true,
|
|
1817
1923
|
expectedTasks: this.#state.expectedTasks,
|
|
1818
1924
|
expectedTaskCount: this.#state.expectedTaskCount,
|
|
1925
|
+
taskVerifications: this.#state.taskVerifications,
|
|
1819
1926
|
source: 'tasks.md',
|
|
1820
1927
|
};
|
|
1821
1928
|
}
|
|
1822
1929
|
if (Array.isArray(taskIds) && taskIds.length > 0) {
|
|
1823
1930
|
this.#state.expectedTasks = [...taskIds];
|
|
1824
1931
|
this.#state.expectedTaskCount = taskIds.length;
|
|
1932
|
+
this.#state.taskVerifications = null;
|
|
1825
1933
|
} else if (typeof taskCount === 'number' && taskCount > 0) {
|
|
1826
1934
|
this.#state.expectedTasks = null;
|
|
1827
1935
|
this.#state.expectedTaskCount = taskCount;
|
|
1936
|
+
this.#state.taskVerifications = null;
|
|
1828
1937
|
} else {
|
|
1829
1938
|
return { error: 'taskIds (array) or taskCount (number) required' };
|
|
1830
1939
|
}
|
|
@@ -1834,7 +1943,12 @@ class OstackyController {
|
|
|
1834
1943
|
'set_expected_tasks',
|
|
1835
1944
|
`expected=${this.#state.expectedTaskCount ?? this.#state.expectedTasks?.length}`
|
|
1836
1945
|
);
|
|
1837
|
-
return {
|
|
1946
|
+
return {
|
|
1947
|
+
ok: true,
|
|
1948
|
+
expectedTasks: this.#state.expectedTasks,
|
|
1949
|
+
expectedTaskCount: this.#state.expectedTaskCount,
|
|
1950
|
+
taskVerifications: this.#state.taskVerifications,
|
|
1951
|
+
};
|
|
1838
1952
|
}
|
|
1839
1953
|
|
|
1840
1954
|
async verifyIntegrity() {
|
|
@@ -1867,11 +1981,46 @@ class OstackyController {
|
|
|
1867
1981
|
else if (current !== stored) staleFiles.push(`${fp} (stale fingerprint)`);
|
|
1868
1982
|
}
|
|
1869
1983
|
} catch {}
|
|
1870
|
-
|
|
1984
|
+
// Generico: verificar tasks con verificacion definida
|
|
1985
|
+
let verificationFailed = [];
|
|
1986
|
+
try {
|
|
1987
|
+
const verifs = this.#state.taskVerifications || {};
|
|
1988
|
+
for (const [tid, verif] of Object.entries(verifs)) {
|
|
1989
|
+
const task = this.#state.tasks[tid];
|
|
1990
|
+
if (!task || task.status !== 'COMPLETED') continue;
|
|
1991
|
+
// Re-verificar via file content
|
|
1992
|
+
let needle = String(verif).trim();
|
|
1993
|
+
if (needle.startsWith('codegraph:')) needle = needle.slice('codegraph:'.length).trim();
|
|
1994
|
+
else if (needle.startsWith('file:')) {
|
|
1995
|
+
const rest = needle.slice('file:'.length).trim();
|
|
1996
|
+
const parts = rest.split('contiene');
|
|
1997
|
+
needle = (parts[1] || parts[0] || '').trim() || rest;
|
|
1998
|
+
} else if (needle.startsWith('test:')) {
|
|
1999
|
+
// test: asumimos ok si ya está completa (test se corrió en agente)
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
2002
|
+
if (!needle) continue;
|
|
2003
|
+
let content = null;
|
|
2004
|
+
const fp = task.filePath;
|
|
2005
|
+
if (fp) {
|
|
2006
|
+
try {
|
|
2007
|
+
const abs =
|
|
2008
|
+
fp.startsWith('/') || /^[A-Za-z]:/.test(fp)
|
|
2009
|
+
? resolve(fp)
|
|
2010
|
+
: resolve(getProjectRoot(this.#statePath), fp);
|
|
2011
|
+
if (existsSync(abs)) content = readFileSync(abs, 'utf-8');
|
|
2012
|
+
} catch {}
|
|
2013
|
+
}
|
|
2014
|
+
const found = content ? content.includes(needle) : false;
|
|
2015
|
+
if (!found) verificationFailed.push(`${tid}: ${verif} no encontrado en ${fp || 'archivo'}`);
|
|
2016
|
+
}
|
|
2017
|
+
} catch {}
|
|
2018
|
+
const ok = pending.length === 0 && staleFiles.length === 0 && verificationFailed.length === 0;
|
|
1871
2019
|
return {
|
|
1872
2020
|
ok,
|
|
1873
2021
|
pending,
|
|
1874
2022
|
staleFiles,
|
|
2023
|
+
verificationFailed: verificationFailed.length ? verificationFailed : undefined,
|
|
1875
2024
|
completed: Object.keys(this.#state.tasks).filter((k) => this.#state.tasks[k].status === 'COMPLETED').length,
|
|
1876
2025
|
expected: this.#state.expectedTaskCount ?? this.#state.expectedTasks?.length ?? null,
|
|
1877
2026
|
state: this.#state.state,
|
|
@@ -1994,31 +2143,6 @@ class OstackyController {
|
|
|
1994
2143
|
} catch {}
|
|
1995
2144
|
}
|
|
1996
2145
|
|
|
1997
|
-
// --- 5.4 hardening-v2: cache metrics (token efficiency) ---
|
|
1998
|
-
async recordCacheHit({ tokensSaved = 500 } = {}) {
|
|
1999
|
-
this.#load();
|
|
2000
|
-
this.#state.cacheHitCount = (this.#state.cacheHitCount || 0) + 1;
|
|
2001
|
-
const saved = typeof tokensSaved === 'number' && tokensSaved > 0 ? tokensSaved : 500;
|
|
2002
|
-
this.#state.tokenSavingEstimate = (this.#state.tokenSavingEstimate || 0) + saved;
|
|
2003
|
-
try {
|
|
2004
|
-
await this.#persist();
|
|
2005
|
-
} catch {}
|
|
2006
|
-
return {
|
|
2007
|
-
ok: true,
|
|
2008
|
-
cacheHitCount: this.#state.cacheHitCount,
|
|
2009
|
-
tokenSavingEstimate: this.#state.tokenSavingEstimate,
|
|
2010
|
-
};
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
async recordCacheMiss() {
|
|
2014
|
-
this.#load();
|
|
2015
|
-
this.#state.cacheMissCount = (this.#state.cacheMissCount || 0) + 1;
|
|
2016
|
-
try {
|
|
2017
|
-
await this.#persist();
|
|
2018
|
-
} catch {}
|
|
2019
|
-
return { ok: true, cacheMissCount: this.#state.cacheMissCount };
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
2146
|
async recordUserConfirmation({ decisionId, confirmationText } = {}) {
|
|
2023
2147
|
this.#load();
|
|
2024
2148
|
if (!decisionId || typeof confirmationText !== 'string') {
|
|
@@ -2334,10 +2458,14 @@ class OstackyController {
|
|
|
2334
2458
|
}
|
|
2335
2459
|
}
|
|
2336
2460
|
// harden-task-integrity 2.2: hard gate unificado (new files eximidos) — antes INLINE hard vs SUBAGENTS WARN
|
|
2461
|
+
// Generico: test: tasks no requieren archivo nuevo (son verificaciones de comportamiento)
|
|
2462
|
+
const verifEarly = this.#state.taskVerifications?.[taskId];
|
|
2463
|
+
const isTestTaskEarly = verifEarly && verifEarly.trim().startsWith('test:');
|
|
2337
2464
|
const isNewFile = !!(
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2465
|
+
isTestTaskEarly ||
|
|
2466
|
+
(filePath &&
|
|
2467
|
+
!this.#state.fileFingerprints?.[filePath] &&
|
|
2468
|
+
!Object.values(this.#state.tasks || {}).some((t) => t.filePath === filePath))
|
|
2341
2469
|
);
|
|
2342
2470
|
if (!this.#state.lastValidated || (filePath && this.#state.lastValidated.filePath !== filePath)) {
|
|
2343
2471
|
if (!isNewFile) {
|
|
@@ -2357,6 +2485,26 @@ class OstackyController {
|
|
|
2357
2485
|
this.#state.lastValidated = null;
|
|
2358
2486
|
}
|
|
2359
2487
|
|
|
2488
|
+
// --- Generico: verificar task antes de marcar completa (evita invento)
|
|
2489
|
+
const verifForComplete = this.#state.taskVerifications?.[taskId];
|
|
2490
|
+
if (verifForComplete) {
|
|
2491
|
+
const vres = await this.verifyTask({ taskId, filePath });
|
|
2492
|
+
if (!vres.ok) {
|
|
2493
|
+
await this.#audit(
|
|
2494
|
+
'WARN',
|
|
2495
|
+
'verification_failed',
|
|
2496
|
+
`task ${taskId} verification failed: ${verifForComplete}`
|
|
2497
|
+
);
|
|
2498
|
+
return {
|
|
2499
|
+
error: `verification failed for ${taskId}: ${verifForComplete} not found in ${vres.fileHint || filePath || 'unknown file'}`,
|
|
2500
|
+
taskId,
|
|
2501
|
+
verification: verifForComplete,
|
|
2502
|
+
verificationFailed: true,
|
|
2503
|
+
vres,
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2360
2508
|
this.#state.tasks[taskId] = {
|
|
2361
2509
|
status: 'COMPLETED',
|
|
2362
2510
|
completedAt: new Date().toISOString(),
|
|
@@ -2413,6 +2561,67 @@ class OstackyController {
|
|
|
2413
2561
|
};
|
|
2414
2562
|
}
|
|
2415
2563
|
|
|
2564
|
+
async verifyTask({ taskId, filePath: filePathParam } = {}) {
|
|
2565
|
+
this.#load();
|
|
2566
|
+
if (!taskId) return { error: 'taskId required' };
|
|
2567
|
+
const verif = this.#state.taskVerifications?.[taskId];
|
|
2568
|
+
if (!verif) return { ok: true, taskId, reason: 'no verification defined' };
|
|
2569
|
+
try {
|
|
2570
|
+
const info = this.#state.tasks[taskId];
|
|
2571
|
+
// Try filePath from param, task, or verification prefix
|
|
2572
|
+
let needle = verif.trim();
|
|
2573
|
+
let fileHint = filePathParam || null;
|
|
2574
|
+
if (needle.startsWith('codegraph:')) {
|
|
2575
|
+
needle = needle.slice('codegraph:'.length).trim();
|
|
2576
|
+
fileHint = fileHint || info?.filePath || null;
|
|
2577
|
+
} else if (needle.startsWith('file:')) {
|
|
2578
|
+
const rest = needle.slice('file:'.length).trim();
|
|
2579
|
+
// file:path contiene string -> split
|
|
2580
|
+
const parts = rest.split('contiene');
|
|
2581
|
+
needle = (parts[1] || parts[0] || '').trim();
|
|
2582
|
+
const parsedPath = (parts[0] || '').trim();
|
|
2583
|
+
if (parsedPath) fileHint = parsedPath;
|
|
2584
|
+
else fileHint = fileHint || info?.filePath || null;
|
|
2585
|
+
if (!needle) needle = rest;
|
|
2586
|
+
} else if (needle.startsWith('test:')) {
|
|
2587
|
+
// test: no podemos ejecutar desde controller, consideramos ok si hay filePath (ya editado) o fileHash
|
|
2588
|
+
needle = needle.slice('test:'.length).trim();
|
|
2589
|
+
const hasFile = !!(fileHint || info?.filePath || info?.fileHash);
|
|
2590
|
+
const ok = hasFile;
|
|
2591
|
+
if (!this.#state.taskVerificationResults) this.#state.taskVerificationResults = {};
|
|
2592
|
+
this.#state.taskVerificationResults[taskId] = { ok, verif, ts: Date.now() };
|
|
2593
|
+
await this.#persist();
|
|
2594
|
+
return { ok, taskId, verif, reason: hasFile ? 'test verification assumed ok (has file)' : 'no file' };
|
|
2595
|
+
}
|
|
2596
|
+
// Generic: check file content contains needle
|
|
2597
|
+
let content = null;
|
|
2598
|
+
let checkPath = fileHint || info?.filePath || filePathParam || null;
|
|
2599
|
+
if (checkPath) {
|
|
2600
|
+
const abs =
|
|
2601
|
+
checkPath.startsWith('/') || /^[A-Za-z]:/.test(checkPath)
|
|
2602
|
+
? resolve(checkPath)
|
|
2603
|
+
: resolve(getProjectRoot(this.#statePath), checkPath);
|
|
2604
|
+
if (existsSync(abs)) {
|
|
2605
|
+
try {
|
|
2606
|
+
content = readFileSync(abs, 'utf-8');
|
|
2607
|
+
} catch {}
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
let ok = false;
|
|
2611
|
+
if (content && needle && content.includes(needle)) ok = true;
|
|
2612
|
+
else if (!checkPath && needle) {
|
|
2613
|
+
// No file hint, search in any task file? For generic, require filePath
|
|
2614
|
+
ok = false;
|
|
2615
|
+
}
|
|
2616
|
+
if (!this.#state.taskVerificationResults) this.#state.taskVerificationResults = {};
|
|
2617
|
+
this.#state.taskVerificationResults[taskId] = { ok, verif, needle, fileHint: checkPath, ts: Date.now() };
|
|
2618
|
+
await this.#persist();
|
|
2619
|
+
return { ok, taskId, verif, needle, fileHint: checkPath, found: ok };
|
|
2620
|
+
} catch (e) {
|
|
2621
|
+
return { ok: false, taskId, verif, error: e.message };
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2416
2625
|
/**
|
|
2417
2626
|
* Public flush — SYNCHRONOUS on purpose: SIGINT/SIGTERM handlers cannot await
|
|
2418
2627
|
* (Node does not wait for async shutdown work). Drains the audit buffer into
|
|
@@ -2576,7 +2785,7 @@ function safeHandler(fn, options = {}) {
|
|
|
2576
2785
|
|
|
2577
2786
|
const server = new McpServer({
|
|
2578
2787
|
name: 'ostacky-controller',
|
|
2579
|
-
version: '0.
|
|
2788
|
+
version: '0.9.0',
|
|
2580
2789
|
});
|
|
2581
2790
|
|
|
2582
2791
|
server.registerTool(
|
|
@@ -2819,34 +3028,6 @@ server.registerTool(
|
|
|
2819
3028
|
)
|
|
2820
3029
|
);
|
|
2821
3030
|
|
|
2822
|
-
server.registerTool(
|
|
2823
|
-
'record_cache_hit',
|
|
2824
|
-
{
|
|
2825
|
-
description: 'Deprecated: cache hit',
|
|
2826
|
-
inputSchema: z.object({
|
|
2827
|
-
tokensSaved: z.number().optional().describe('Estimated tokens saved (default 500)'),
|
|
2828
|
-
}),
|
|
2829
|
-
},
|
|
2830
|
-
safeHandler(async ({ tokensSaved }) => {
|
|
2831
|
-
log('tool:record_cache_hit', { tokensSaved, deprecated: true });
|
|
2832
|
-
const r = await controller.recordCacheHit({ tokensSaved });
|
|
2833
|
-
return { ...r, deprecated: true };
|
|
2834
|
-
})
|
|
2835
|
-
);
|
|
2836
|
-
|
|
2837
|
-
server.registerTool(
|
|
2838
|
-
'record_cache_miss',
|
|
2839
|
-
{
|
|
2840
|
-
description: 'Deprecated: cache miss',
|
|
2841
|
-
inputSchema: z.object({}),
|
|
2842
|
-
},
|
|
2843
|
-
safeHandler(async () => {
|
|
2844
|
-
log('tool:record_cache_miss', { deprecated: true });
|
|
2845
|
-
const r = await controller.recordCacheMiss();
|
|
2846
|
-
return { ...r, deprecated: true };
|
|
2847
|
-
})
|
|
2848
|
-
);
|
|
2849
|
-
|
|
2850
3031
|
server.registerTool(
|
|
2851
3032
|
'record_user_confirmation',
|
|
2852
3033
|
{
|
|
@@ -3176,7 +3357,7 @@ function setupGracefulShutdown(ctrl) {
|
|
|
3176
3357
|
}
|
|
3177
3358
|
|
|
3178
3359
|
async function main() {
|
|
3179
|
-
log('Starting ostacky-controller MCP v0.
|
|
3360
|
+
log('Starting ostacky-controller MCP v0.9.0...');
|
|
3180
3361
|
log('State path:', { path: statePath });
|
|
3181
3362
|
// Clean up stale tmp/lock files from previous runs
|
|
3182
3363
|
cleanupTmpFiles(statePath);
|