ostacky 0.5.9 → 0.5.10
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 +9 -9
- package/assets/agents/ostacky.md +18 -0
- package/assets/commands/install-stack.md +1 -1
- package/assets/mcp/ostacky-controller/index.js +231 -82
- package/assets/mcp/ostacky-controller/package.json +1 -1
- package/assets/tests/agent-contract.test.js +4 -3
- package/dist/cli.js +27 -28
- package/dist/mcp/ostacky-controller/index.js +191 -89
- package/manifest.json +23 -23
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -161,7 +161,7 @@ var require_picocolors = __commonJS((exports, module) => {
|
|
|
161
161
|
// package.json
|
|
162
162
|
var package_default = {
|
|
163
163
|
name: "ostacky",
|
|
164
|
-
version: "0.5.
|
|
164
|
+
version: "0.5.10",
|
|
165
165
|
description: "Instalador interactivo de agentes y comandos para OpenCode",
|
|
166
166
|
type: "module",
|
|
167
167
|
bin: {
|
|
@@ -879,16 +879,16 @@ import { join as join5 } from "path";
|
|
|
879
879
|
import { existsSync as existsSync4 } from "fs";
|
|
880
880
|
// manifest.json
|
|
881
881
|
var manifest_default = {
|
|
882
|
-
version: "0.5.
|
|
882
|
+
version: "0.5.10",
|
|
883
883
|
repo: "JaimeHoracio/Ostacky",
|
|
884
|
-
tag: "v0.5.
|
|
884
|
+
tag: "v0.5.10",
|
|
885
885
|
agents: [
|
|
886
886
|
{
|
|
887
887
|
name: "ostacky",
|
|
888
888
|
file: "assets/agents/ostacky.md",
|
|
889
889
|
description: "Orquestador principal con ruteo por nivel de impacto, máquina de estados persistida (controller MCP), edición segura con 3 outcomes, y delegación en OpenSpec + Superpowers",
|
|
890
|
-
version: "0.5.
|
|
891
|
-
sha256: "
|
|
890
|
+
version: "0.5.10",
|
|
891
|
+
sha256: "c624b9163a97fe0aa17a4688f59e2bcfa958c64fbde71c05c0c071911f62b7ad"
|
|
892
892
|
}
|
|
893
893
|
],
|
|
894
894
|
commands: [
|
|
@@ -896,14 +896,14 @@ var manifest_default = {
|
|
|
896
896
|
name: "install-stack",
|
|
897
897
|
file: "assets/commands/install-stack.md",
|
|
898
898
|
description: "Instala el stack tecnológico del proyecto (CodeGraph, skills, OpenSpec, Engram, Context7, controller MCP)",
|
|
899
|
-
version: "0.5.
|
|
900
|
-
sha256: "
|
|
899
|
+
version: "0.5.10",
|
|
900
|
+
sha256: "61f18cb616bffe4a982d874ee9426011c203ab9c17efb88d44592e202c699330"
|
|
901
901
|
},
|
|
902
902
|
{
|
|
903
903
|
name: "opsx-sync",
|
|
904
904
|
file: "assets/commands/opsx-sync.md",
|
|
905
905
|
description: "Sincroniza delta specs del change activo sin inicializar CodeGraph si ya existe índice",
|
|
906
|
-
version: "0.5.
|
|
906
|
+
version: "0.5.10",
|
|
907
907
|
sha256: "ed9948f1910743b672e1dfad496bc96972a224a98b063232be39290d43068c50"
|
|
908
908
|
}
|
|
909
909
|
],
|
|
@@ -912,8 +912,8 @@ var manifest_default = {
|
|
|
912
912
|
name: "ostacky-controller",
|
|
913
913
|
file: "assets/mcp/ostacky-controller/",
|
|
914
914
|
description: "Máquina de estados persistida para Ostacky: 13 tools MCP para ciclo completo de request (start_request → discovery → route → execution → sync → done), edición segura con validación de transiciones y persistencia atómica",
|
|
915
|
-
version: "0.5.
|
|
916
|
-
sha256: "
|
|
915
|
+
version: "0.5.10",
|
|
916
|
+
sha256: "4469b9e26910a5ba016a0fe7a2d77507d40715d95ce4cbb31c498bf7001b4d0d"
|
|
917
917
|
}
|
|
918
918
|
],
|
|
919
919
|
skills: [
|
|
@@ -921,98 +921,98 @@ var manifest_default = {
|
|
|
921
921
|
name: "thinking",
|
|
922
922
|
file: "assets/skills/thinking/SKILL.md",
|
|
923
923
|
description: "Skill unificado de pensamiento con dos modos: creative-design (producción de diseño → transición a writing-plans o openspec-propose) y open-exploration (exploración libre)",
|
|
924
|
-
version: "0.5.
|
|
924
|
+
version: "0.5.10",
|
|
925
925
|
sha256: "6a1fdac86357f89e132013e56cb84c1920880c1c035a8dcf53f69e01824724bb"
|
|
926
926
|
},
|
|
927
927
|
{
|
|
928
928
|
name: "execution-mode-evaluation",
|
|
929
929
|
file: "assets/skills/execution-mode-evaluation/SKILL.md",
|
|
930
930
|
description: "Skill de análisis de modo de ejecución — output reconciliado con controller snapshot contract (recommendation field)",
|
|
931
|
-
version: "0.5.
|
|
931
|
+
version: "0.5.10",
|
|
932
932
|
sha256: "048925aec576dff014eed0b44e2a553c635ae405f5a245773f480be9b84d18a7"
|
|
933
933
|
},
|
|
934
934
|
{
|
|
935
935
|
name: "writing-plans",
|
|
936
936
|
file: "assets/skills/writing-plans/SKILL.md",
|
|
937
937
|
description: "Skill de planificación de implementación (Superpowers) — execution handoff removido, Ostacky decide modo de ejecución",
|
|
938
|
-
version: "0.5.
|
|
938
|
+
version: "0.5.10",
|
|
939
939
|
sha256: "13f6e43522567505da90793934d414d290d56f4ea587ac696f66416af9e7633f"
|
|
940
940
|
},
|
|
941
941
|
{
|
|
942
942
|
name: "tdd",
|
|
943
943
|
file: "assets/skills/tdd/SKILL.md",
|
|
944
944
|
description: "Skill de test-driven development (Superpowers)",
|
|
945
|
-
version: "0.5.
|
|
945
|
+
version: "0.5.10",
|
|
946
946
|
sha256: "7eacd8ee81dc5c0b85065c0392e37ee10314661f7edb59dd8e70a9e0dae8f371"
|
|
947
947
|
},
|
|
948
948
|
{
|
|
949
949
|
name: "subagent-driven-development",
|
|
950
950
|
file: "assets/skills/subagent-driven-development/SKILL.md",
|
|
951
951
|
description: "Skill de ejecución con subagentes (Superpowers) — ejecuta solo después de confirmación del coordinador Ostacky",
|
|
952
|
-
version: "0.5.
|
|
952
|
+
version: "0.5.10",
|
|
953
953
|
sha256: "cdacae2e5b86f6a642ad0316172cee7dc8d19e909ef4f9ab7fcf7e84a248dda3"
|
|
954
954
|
},
|
|
955
955
|
{
|
|
956
956
|
name: "dispatching-parallel-agents",
|
|
957
957
|
file: "assets/skills/dispatching-parallel-agents/SKILL.md",
|
|
958
958
|
description: "Skill de dispatch paralelo de agentes (Superpowers)",
|
|
959
|
-
version: "0.5.
|
|
959
|
+
version: "0.5.10",
|
|
960
960
|
sha256: "281edf0c38f358497c7e2066fa8217a2ba3e2a39b4205c4af0c41d328fc035a1"
|
|
961
961
|
},
|
|
962
962
|
{
|
|
963
963
|
name: "review",
|
|
964
964
|
file: "assets/skills/review/SKILL.md",
|
|
965
965
|
description: "Skill de revisión de código (Superpowers)",
|
|
966
|
-
version: "0.5.
|
|
966
|
+
version: "0.5.10",
|
|
967
967
|
sha256: "14831d9ef3746ef6e2e6047c868fe7aa296d01f0644227ce776fe99436357b1a"
|
|
968
968
|
},
|
|
969
969
|
{
|
|
970
970
|
name: "receiving-code-review",
|
|
971
971
|
file: "assets/skills/receiving-code-review/SKILL.md",
|
|
972
972
|
description: "Skill de recibir y procesar feedback de code review",
|
|
973
|
-
version: "0.5.
|
|
973
|
+
version: "0.5.10",
|
|
974
974
|
sha256: "0a5780e8a41539d15428114b7c46f36670acc16aba0db3348d36be1175433872"
|
|
975
975
|
},
|
|
976
976
|
{
|
|
977
977
|
name: "openspec-propose",
|
|
978
978
|
file: "assets/skills/openspec-propose/SKILL.md",
|
|
979
979
|
description: "Skill de generación de proposal (OpenSpec)",
|
|
980
|
-
version: "0.5.
|
|
980
|
+
version: "0.5.10",
|
|
981
981
|
sha256: "3306b21ba9cb8c1611e8ba335126982f3767f7d317b5ed506c57a14c5f88b263"
|
|
982
982
|
},
|
|
983
983
|
{
|
|
984
984
|
name: "openspec-apply-change",
|
|
985
985
|
file: "assets/skills/openspec-apply-change/SKILL.md",
|
|
986
986
|
description: "Skill de aplicación de change (OpenSpec)",
|
|
987
|
-
version: "0.5.
|
|
987
|
+
version: "0.5.10",
|
|
988
988
|
sha256: "31c1313c3de4616a07efd89306c4c058175d018f42d4381cc7da7030fd03ba30"
|
|
989
989
|
},
|
|
990
990
|
{
|
|
991
991
|
name: "openspec-archive-change",
|
|
992
992
|
file: "assets/skills/openspec-archive-change/SKILL.md",
|
|
993
993
|
description: "Skill de archivo de change (OpenSpec)",
|
|
994
|
-
version: "0.5.
|
|
994
|
+
version: "0.5.10",
|
|
995
995
|
sha256: "5bf65d1848457bd57be1d21707e55fe6746cdee33bce2f32f12d87661b6aa3a4"
|
|
996
996
|
},
|
|
997
997
|
{
|
|
998
998
|
name: "using-git-worktrees",
|
|
999
999
|
file: "assets/skills/using-git-worktrees/SKILL.md",
|
|
1000
1000
|
description: "Skill de uso de git worktrees para aislamiento de trabajo",
|
|
1001
|
-
version: "0.5.
|
|
1001
|
+
version: "0.5.10",
|
|
1002
1002
|
sha256: "33433c24f753d566cde16c0931f70d746b6b0cb4d1f3daaa619e928b009e4fc9"
|
|
1003
1003
|
},
|
|
1004
1004
|
{
|
|
1005
1005
|
name: "using-superpowers",
|
|
1006
1006
|
file: "assets/skills/using-superpowers/SKILL.md",
|
|
1007
1007
|
description: "Skill de orquestación de Superpowers skills",
|
|
1008
|
-
version: "0.5.
|
|
1008
|
+
version: "0.5.10",
|
|
1009
1009
|
sha256: "fba542003aa788b5edb4ae2d1135773a1dde8111d1dd60378e2f61b6695d46fc"
|
|
1010
1010
|
},
|
|
1011
1011
|
{
|
|
1012
1012
|
name: "writing-skills",
|
|
1013
1013
|
file: "assets/skills/writing-skills/SKILL.md",
|
|
1014
1014
|
description: "Skill de creación y edición de skills",
|
|
1015
|
-
version: "0.5.
|
|
1015
|
+
version: "0.5.10",
|
|
1016
1016
|
sha256: "7f74ffe049283803640d78f8c9ac1ea5f5e626dedb885ecff29a87c5b5d41598"
|
|
1017
1017
|
}
|
|
1018
1018
|
]
|
|
@@ -1204,7 +1204,7 @@ function clearLockfile(opencodeRoot) {
|
|
|
1204
1204
|
const lockfile = readLockfile(opencodeRoot);
|
|
1205
1205
|
if (!lockfile) {
|
|
1206
1206
|
writeLockfile(opencodeRoot, {
|
|
1207
|
-
version: "0.5.
|
|
1207
|
+
version: "0.5.10",
|
|
1208
1208
|
lockedAt: new Date().toISOString(),
|
|
1209
1209
|
repo: "",
|
|
1210
1210
|
tag: "",
|
|
@@ -1514,9 +1514,8 @@ async function installMcpServer(item, manifest, paths) {
|
|
|
1514
1514
|
if (output)
|
|
1515
1515
|
console.error(output);
|
|
1516
1516
|
} catch (e2) {
|
|
1517
|
-
|
|
1518
|
-
` + `Resolvé las dependencias manualmente:
|
|
1519
|
-
` + ` cd .opencode/mcp/${item.name} && bun install`);
|
|
1517
|
+
console.error(`[WARN] MCP server "${item.name}" copiado pero ${installCmd} falló: ${e2.message}.
|
|
1518
|
+
` + ` Resolvé las dependencias manualmente: cd .opencode/mcp/${item.name} && bun install`);
|
|
1520
1519
|
}
|
|
1521
1520
|
}
|
|
1522
1521
|
upsertLockfile(paths, "mcpServers", item, manifest, treeHash);
|
|
@@ -12263,21 +12263,21 @@ var require_errors = /* @__PURE__ */ __commonJSMin((exports) => {
|
|
|
12263
12263
|
function extendErrors({ gen, keyword, schemaValue, data, errsCount, it }) {
|
|
12264
12264
|
if (errsCount === undefined)
|
|
12265
12265
|
throw new Error("ajv implementation error");
|
|
12266
|
-
const
|
|
12266
|
+
const err2 = gen.name("err");
|
|
12267
12267
|
gen.forRange("i", errsCount, names_1.default.errors, (i) => {
|
|
12268
|
-
gen.const(
|
|
12269
|
-
gen.if((0, codegen_1._)`${
|
|
12270
|
-
gen.assign((0, codegen_1._)`${
|
|
12268
|
+
gen.const(err2, (0, codegen_1._)`${names_1.default.vErrors}[${i}]`);
|
|
12269
|
+
gen.if((0, codegen_1._)`${err2}.instancePath === undefined`, () => gen.assign((0, codegen_1._)`${err2}.instancePath`, (0, codegen_1.strConcat)(names_1.default.instancePath, it.errorPath)));
|
|
12270
|
+
gen.assign((0, codegen_1._)`${err2}.schemaPath`, (0, codegen_1.str)`${it.errSchemaPath}/${keyword}`);
|
|
12271
12271
|
if (it.opts.verbose) {
|
|
12272
|
-
gen.assign((0, codegen_1._)`${
|
|
12273
|
-
gen.assign((0, codegen_1._)`${
|
|
12272
|
+
gen.assign((0, codegen_1._)`${err2}.schema`, schemaValue);
|
|
12273
|
+
gen.assign((0, codegen_1._)`${err2}.data`, data);
|
|
12274
12274
|
}
|
|
12275
12275
|
});
|
|
12276
12276
|
}
|
|
12277
12277
|
exports.extendErrors = extendErrors;
|
|
12278
12278
|
function addError(gen, errObj) {
|
|
12279
|
-
const
|
|
12280
|
-
gen.if((0, codegen_1._)`${names_1.default.vErrors} === null`, () => gen.assign(names_1.default.vErrors, (0, codegen_1._)`[${
|
|
12279
|
+
const err2 = gen.const("err", errObj);
|
|
12280
|
+
gen.if((0, codegen_1._)`${names_1.default.vErrors} === null`, () => gen.assign(names_1.default.vErrors, (0, codegen_1._)`[${err2}]`), (0, codegen_1._)`${names_1.default.vErrors}.push(${err2})`);
|
|
12281
12281
|
gen.code((0, codegen_1._)`${names_1.default.errors}++`);
|
|
12282
12282
|
}
|
|
12283
12283
|
function returnErrors(it, errs) {
|
|
@@ -19496,13 +19496,46 @@ var StdioServerTransport = class {
|
|
|
19496
19496
|
};
|
|
19497
19497
|
|
|
19498
19498
|
// assets/mcp/ostacky-controller/index.js
|
|
19499
|
-
import { readFileSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
|
|
19500
|
-
import { dirname } from "node:path";
|
|
19499
|
+
import { readFileSync, writeFileSync, renameSync, mkdirSync, readdirSync, unlinkSync } from "node:fs";
|
|
19500
|
+
import { dirname, basename } from "node:path";
|
|
19501
|
+
var MAX_TASKS = 50;
|
|
19502
|
+
var MAX_SNAPSHOT_JSON_LENGTH = 100 * 1024;
|
|
19503
|
+
var MAX_STATE_FILE_SIZE = 1024 * 1024;
|
|
19504
|
+
function safeJsonStringify(obj, pretty = false) {
|
|
19505
|
+
const seen = new WeakSet;
|
|
19506
|
+
try {
|
|
19507
|
+
return JSON.stringify(obj, (key, value) => {
|
|
19508
|
+
if (typeof value === "object" && value !== null) {
|
|
19509
|
+
if (seen.has(value))
|
|
19510
|
+
return "[Circular]";
|
|
19511
|
+
seen.add(value);
|
|
19512
|
+
}
|
|
19513
|
+
return value;
|
|
19514
|
+
}, pretty ? 2 : undefined);
|
|
19515
|
+
} catch (e) {
|
|
19516
|
+
return `[Unstringifiable: ${e.message}]`;
|
|
19517
|
+
}
|
|
19518
|
+
}
|
|
19501
19519
|
function log(event, data) {
|
|
19502
19520
|
const ts = new Date().toISOString();
|
|
19503
|
-
const payload = data ? ` ${
|
|
19521
|
+
const payload = data ? ` ${safeJsonStringify(data)}` : "";
|
|
19504
19522
|
console.error(`[${ts}] ${event}${payload}`);
|
|
19505
19523
|
}
|
|
19524
|
+
function cleanupTmpFiles(statePath) {
|
|
19525
|
+
if (!statePath)
|
|
19526
|
+
return;
|
|
19527
|
+
const dir = dirname(statePath);
|
|
19528
|
+
const name = basename(statePath);
|
|
19529
|
+
try {
|
|
19530
|
+
for (const entry of readdirSync(dir)) {
|
|
19531
|
+
if (entry.startsWith(name + ".tmp.")) {
|
|
19532
|
+
try {
|
|
19533
|
+
unlinkSync(dir + "/" + entry);
|
|
19534
|
+
} catch {}
|
|
19535
|
+
}
|
|
19536
|
+
}
|
|
19537
|
+
} catch {}
|
|
19538
|
+
}
|
|
19506
19539
|
var STATES = Object.freeze({
|
|
19507
19540
|
INTERPRETATION_PENDING: "INTERPRETATION_PENDING",
|
|
19508
19541
|
CLARIFICATION_PENDING: "CLARIFICATION_PENDING",
|
|
@@ -19557,9 +19590,29 @@ class OstackyController {
|
|
|
19557
19590
|
}
|
|
19558
19591
|
try {
|
|
19559
19592
|
const raw = readFileSync(this.#statePath, "utf8");
|
|
19593
|
+
if (raw.length > MAX_STATE_FILE_SIZE)
|
|
19594
|
+
throw new Error(`State file too large: ${raw.length} bytes`);
|
|
19560
19595
|
this.#state = { ...DEFAULT_STATE, ...JSON.parse(raw) };
|
|
19596
|
+
this.#loaded = true;
|
|
19597
|
+
return;
|
|
19598
|
+
} catch (err2) {
|
|
19599
|
+
log("warn:load_primary_failed", { error: err2.message });
|
|
19600
|
+
}
|
|
19601
|
+
const backupPath = this.#statePath + ".backup";
|
|
19602
|
+
try {
|
|
19603
|
+
const raw = readFileSync(backupPath, "utf8");
|
|
19604
|
+
if (raw.length > MAX_STATE_FILE_SIZE)
|
|
19605
|
+
throw new Error(`Backup too large: ${raw.length} bytes`);
|
|
19606
|
+
this.#state = { ...DEFAULT_STATE, ...JSON.parse(raw), error: "State restored from backup" };
|
|
19607
|
+
log("warn:state_restored_from_backup");
|
|
19608
|
+
this.#loaded = true;
|
|
19609
|
+
return;
|
|
19561
19610
|
} catch {
|
|
19562
|
-
this.#state = {
|
|
19611
|
+
this.#state = {
|
|
19612
|
+
...DEFAULT_STATE,
|
|
19613
|
+
error: `State file corrupt: ${err.message}. No backup available. State reset to default.`
|
|
19614
|
+
};
|
|
19615
|
+
log("warn:state_reset", { error: err.message });
|
|
19563
19616
|
}
|
|
19564
19617
|
this.#loaded = true;
|
|
19565
19618
|
}
|
|
@@ -19568,14 +19621,48 @@ class OstackyController {
|
|
|
19568
19621
|
return;
|
|
19569
19622
|
const dir = dirname(this.#statePath);
|
|
19570
19623
|
mkdirSync(dir, { recursive: true });
|
|
19624
|
+
const serialized = safeJsonStringify(this.#state, true);
|
|
19625
|
+
if (serialized.length > MAX_STATE_FILE_SIZE) {
|
|
19626
|
+
log("warn:state_oversized", { size: serialized.length });
|
|
19627
|
+
this.#state.snapshots = { codegraph: null, execution: null };
|
|
19628
|
+
const trimmed = safeJsonStringify(this.#state, true);
|
|
19629
|
+
if (trimmed.length > MAX_STATE_FILE_SIZE) {
|
|
19630
|
+
log("error:state_too_large_even_after_trim");
|
|
19631
|
+
return;
|
|
19632
|
+
}
|
|
19633
|
+
const tmp2 = this.#statePath + ".tmp." + process.pid;
|
|
19634
|
+
writeFileSync(tmp2, trimmed, "utf8");
|
|
19635
|
+
renameSync(tmp2, this.#statePath);
|
|
19636
|
+
return;
|
|
19637
|
+
}
|
|
19571
19638
|
const tmp = this.#statePath + ".tmp." + process.pid;
|
|
19572
|
-
writeFileSync(tmp,
|
|
19639
|
+
writeFileSync(tmp, serialized, "utf8");
|
|
19573
19640
|
renameSync(tmp, this.#statePath);
|
|
19641
|
+
try {
|
|
19642
|
+
const backupPath = this.#statePath + ".backup";
|
|
19643
|
+
writeFileSync(backupPath, serialized, "utf8");
|
|
19644
|
+
} catch {}
|
|
19645
|
+
}
|
|
19646
|
+
#trimTasks() {
|
|
19647
|
+
if (!this.#state.tasks)
|
|
19648
|
+
return;
|
|
19649
|
+
const entries = Object.entries(this.#state.tasks);
|
|
19650
|
+
if (entries.length <= MAX_TASKS)
|
|
19651
|
+
return;
|
|
19652
|
+
entries.sort((a, b) => {
|
|
19653
|
+
const da = a[1].completedAt || "";
|
|
19654
|
+
const db = b[1].completedAt || "";
|
|
19655
|
+
return db.localeCompare(da);
|
|
19656
|
+
});
|
|
19657
|
+
const trimmed = Object.fromEntries(entries.slice(0, MAX_TASKS));
|
|
19658
|
+
this.#state.tasks = trimmed;
|
|
19659
|
+
log("warn:tasks_trimmed", { before: entries.length, after: MAX_TASKS });
|
|
19574
19660
|
}
|
|
19575
19661
|
#transition(to, changes = {}) {
|
|
19576
19662
|
this.#state.revision++;
|
|
19577
19663
|
this.#state.state = to;
|
|
19578
19664
|
Object.assign(this.#state, changes);
|
|
19665
|
+
this.#trimTasks();
|
|
19579
19666
|
this.#persist();
|
|
19580
19667
|
}
|
|
19581
19668
|
#isAllowedTransition(from, via, choiceOrMode) {
|
|
@@ -19692,7 +19779,7 @@ class OstackyController {
|
|
|
19692
19779
|
return { error: `Cannot record discovery from state ${this.#state.state}` };
|
|
19693
19780
|
if (!["0", "0+1", "1+"].includes(level))
|
|
19694
19781
|
return { error: `Invalid level: ${level}` };
|
|
19695
|
-
this.#transition(
|
|
19782
|
+
this.#transition(to, {
|
|
19696
19783
|
routeDecisionId: routeDecisionId || "route-" + Date.now(),
|
|
19697
19784
|
routeChoice: null,
|
|
19698
19785
|
snapshots: { ...this.#state.snapshots, codegraph: snapshot || this.#state.snapshots.codegraph }
|
|
@@ -19877,6 +19964,7 @@ class OstackyController {
|
|
|
19877
19964
|
this.#state.fileFingerprints = {};
|
|
19878
19965
|
this.#state.fileFingerprints[filePath] = fileHash;
|
|
19879
19966
|
}
|
|
19967
|
+
this.#trimTasks();
|
|
19880
19968
|
this.#persist();
|
|
19881
19969
|
return {
|
|
19882
19970
|
taskId,
|
|
@@ -19884,12 +19972,29 @@ class OstackyController {
|
|
|
19884
19972
|
totalCompleted: Object.keys(this.#state.tasks).filter((k) => this.#state.tasks[k].status === "COMPLETED").length
|
|
19885
19973
|
};
|
|
19886
19974
|
}
|
|
19975
|
+
flush() {
|
|
19976
|
+
this.#persist();
|
|
19977
|
+
}
|
|
19887
19978
|
}
|
|
19888
19979
|
var statePath = process.env.OSTACKY_STATE_PATH || ".opencode/ostacky-state.json";
|
|
19889
19980
|
var controller = new OstackyController({ statePath });
|
|
19981
|
+
function safeHandler(fn) {
|
|
19982
|
+
return async (params) => {
|
|
19983
|
+
try {
|
|
19984
|
+
const result = await fn(params);
|
|
19985
|
+
return { content: [{ type: "text", text: safeJsonStringify(result) }] };
|
|
19986
|
+
} catch (error2) {
|
|
19987
|
+
log("tool:error", { name: fn.name || "anonymous", error: error2.message });
|
|
19988
|
+
return {
|
|
19989
|
+
content: [{ type: "text", text: safeJsonStringify({ error: error2.message }) }],
|
|
19990
|
+
isError: true
|
|
19991
|
+
};
|
|
19992
|
+
}
|
|
19993
|
+
};
|
|
19994
|
+
}
|
|
19890
19995
|
var server = new McpServer({
|
|
19891
19996
|
name: "ostacky-controller",
|
|
19892
|
-
version: "0.5.
|
|
19997
|
+
version: "0.5.10"
|
|
19893
19998
|
});
|
|
19894
19999
|
server.registerTool("start_request", {
|
|
19895
20000
|
description: "Start or resume a new request in the state machine. Call this first.",
|
|
@@ -19897,29 +20002,26 @@ server.registerTool("start_request", {
|
|
|
19897
20002
|
requestId: string2().optional().describe("Unique request ID"),
|
|
19898
20003
|
changeId: string2().optional().describe("Optional change ID for OpenSpec tracking")
|
|
19899
20004
|
})
|
|
19900
|
-
}, async ({ requestId, changeId }) => {
|
|
20005
|
+
}, safeHandler(async ({ requestId, changeId }) => {
|
|
19901
20006
|
log("tool:start_request");
|
|
19902
|
-
|
|
19903
|
-
|
|
19904
|
-
});
|
|
20007
|
+
return await controller.startRequest({ requestId, changeId });
|
|
20008
|
+
}));
|
|
19905
20009
|
server.registerTool("request_clarification", {
|
|
19906
20010
|
description: "Record that clarification was requested. Transitions to CLARIFICATION_PENDING.",
|
|
19907
20011
|
inputSchema: object({
|
|
19908
20012
|
question: string2().optional().describe("The clarification question")
|
|
19909
20013
|
})
|
|
19910
|
-
}, async ({ question }) => {
|
|
20014
|
+
}, safeHandler(async ({ question }) => {
|
|
19911
20015
|
log("tool:request_clarification");
|
|
19912
|
-
|
|
19913
|
-
|
|
19914
|
-
});
|
|
20016
|
+
return await controller.requestClarification({ question });
|
|
20017
|
+
}));
|
|
19915
20018
|
server.registerTool("record_clarification", {
|
|
19916
20019
|
description: "Record that clarification was answered. Transitions to DISCOVERY.",
|
|
19917
20020
|
inputSchema: object({})
|
|
19918
|
-
}, async () => {
|
|
20021
|
+
}, safeHandler(async () => {
|
|
19919
20022
|
log("tool:record_clarification");
|
|
19920
|
-
|
|
19921
|
-
|
|
19922
|
-
});
|
|
20023
|
+
return await controller.recordClarification();
|
|
20024
|
+
}));
|
|
19923
20025
|
server.registerTool("record_discovery", {
|
|
19924
20026
|
description: "Record discovery complete with level classification. Transitions to ROUTE_DECISION_PENDING.",
|
|
19925
20027
|
inputSchema: object({
|
|
@@ -19927,111 +20029,100 @@ server.registerTool("record_discovery", {
|
|
|
19927
20029
|
routeDecisionId: string2().optional().describe("Unique route decision ID"),
|
|
19928
20030
|
snapshot: any().optional().describe("Optional CodeGraph snapshot")
|
|
19929
20031
|
})
|
|
19930
|
-
}, async ({ level, routeDecisionId, snapshot }) => {
|
|
20032
|
+
}, safeHandler(async ({ level, routeDecisionId, snapshot }) => {
|
|
19931
20033
|
log("tool:record_discovery", { level });
|
|
19932
|
-
|
|
19933
|
-
|
|
19934
|
-
});
|
|
20034
|
+
return await controller.recordDiscovery({ level, routeDecisionId, snapshot });
|
|
20035
|
+
}));
|
|
19935
20036
|
server.registerTool("consume_route_decision", {
|
|
19936
20037
|
description: "Consume the route decision (SPEC or DIRECT). Valid only in ROUTE_DECISION_PENDING.",
|
|
19937
20038
|
inputSchema: object({
|
|
19938
20039
|
decisionId: string2().describe("Route decision ID from record_discovery"),
|
|
19939
20040
|
choice: _enum(["SPEC", "DIRECT"]).describe("Route choice")
|
|
19940
20041
|
})
|
|
19941
|
-
}, async ({ decisionId, choice }) => {
|
|
20042
|
+
}, safeHandler(async ({ decisionId, choice }) => {
|
|
19942
20043
|
log("tool:consume_route_decision", { choice });
|
|
19943
|
-
|
|
19944
|
-
|
|
19945
|
-
});
|
|
20044
|
+
return await controller.consumeRouteDecision({ decisionId, choice });
|
|
20045
|
+
}));
|
|
19946
20046
|
server.registerTool("spec_complete", {
|
|
19947
20047
|
description: "Mark specification phase as complete. Transitions to EXECUTION_ANALYSIS.",
|
|
19948
20048
|
inputSchema: object({})
|
|
19949
|
-
}, async () => {
|
|
20049
|
+
}, safeHandler(async () => {
|
|
19950
20050
|
log("tool:spec_complete");
|
|
19951
|
-
|
|
19952
|
-
|
|
19953
|
-
});
|
|
20051
|
+
return await controller.specComplete();
|
|
20052
|
+
}));
|
|
19954
20053
|
server.registerTool("record_execution_analysis", {
|
|
19955
20054
|
description: "Record execution analysis with snapshot. Transitions to EXECUTION_DECISION_PENDING.",
|
|
19956
20055
|
inputSchema: object({
|
|
19957
20056
|
executionDecisionId: string2().optional().describe("Unique execution decision ID"),
|
|
19958
20057
|
snapshot: any().optional().describe("Execution analysis snapshot")
|
|
19959
20058
|
})
|
|
19960
|
-
}, async ({ executionDecisionId, snapshot }) => {
|
|
20059
|
+
}, safeHandler(async ({ executionDecisionId, snapshot }) => {
|
|
19961
20060
|
log("tool:record_execution_analysis");
|
|
19962
|
-
|
|
19963
|
-
|
|
19964
|
-
});
|
|
20061
|
+
return await controller.recordExecutionAnalysis({ executionDecisionId, snapshot });
|
|
20062
|
+
}));
|
|
19965
20063
|
server.registerTool("consume_execution_decision", {
|
|
19966
20064
|
description: "Consume the execution mode decision (INLINE or SUBAGENT_DRIVEN).",
|
|
19967
20065
|
inputSchema: object({
|
|
19968
20066
|
decisionId: string2().describe("Execution decision ID from record_execution_analysis"),
|
|
19969
20067
|
mode: _enum(["INLINE", "SUBAGENT_DRIVEN"]).describe("Execution mode")
|
|
19970
20068
|
})
|
|
19971
|
-
}, async ({ decisionId, mode }) => {
|
|
20069
|
+
}, safeHandler(async ({ decisionId, mode }) => {
|
|
19972
20070
|
log("tool:consume_execution_decision", { mode });
|
|
19973
|
-
|
|
19974
|
-
|
|
19975
|
-
});
|
|
20071
|
+
return await controller.consumeExecutionDecision({ decisionId, mode });
|
|
20072
|
+
}));
|
|
19976
20073
|
server.registerTool("implementation_complete", {
|
|
19977
20074
|
description: "Mark implementation as complete. Transitions to SYNC.",
|
|
19978
20075
|
inputSchema: object({})
|
|
19979
|
-
}, async () => {
|
|
20076
|
+
}, safeHandler(async () => {
|
|
19980
20077
|
log("tool:implementation_complete");
|
|
19981
|
-
|
|
19982
|
-
|
|
19983
|
-
});
|
|
20078
|
+
return await controller.implementationComplete();
|
|
20079
|
+
}));
|
|
19984
20080
|
server.registerTool("sync_complete", {
|
|
19985
20081
|
description: "Mark sync as complete. Transitions to DONE.",
|
|
19986
20082
|
inputSchema: object({})
|
|
19987
|
-
}, async () => {
|
|
20083
|
+
}, safeHandler(async () => {
|
|
19988
20084
|
log("tool:sync_complete");
|
|
19989
|
-
|
|
19990
|
-
|
|
19991
|
-
});
|
|
20085
|
+
return await controller.syncComplete();
|
|
20086
|
+
}));
|
|
19992
20087
|
server.registerTool("block", {
|
|
19993
20088
|
description: "Transition to BLOCKED state with an optional reason.",
|
|
19994
20089
|
inputSchema: object({
|
|
19995
20090
|
reason: string2().optional().describe("Reason for blocking")
|
|
19996
20091
|
})
|
|
19997
|
-
}, async ({ reason }) => {
|
|
20092
|
+
}, safeHandler(async ({ reason }) => {
|
|
19998
20093
|
log("tool:block");
|
|
19999
|
-
|
|
20000
|
-
|
|
20001
|
-
});
|
|
20094
|
+
return await controller.block({ reason });
|
|
20095
|
+
}));
|
|
20002
20096
|
server.registerTool("replan", {
|
|
20003
20097
|
description: "Replan from BLOCKED state back to INTERPRETATION_PENDING.",
|
|
20004
20098
|
inputSchema: object({
|
|
20005
20099
|
reason: string2().optional().describe("Reason for replanning")
|
|
20006
20100
|
})
|
|
20007
|
-
}, async ({ reason }) => {
|
|
20101
|
+
}, safeHandler(async ({ reason }) => {
|
|
20008
20102
|
log("tool:replan");
|
|
20009
|
-
|
|
20010
|
-
|
|
20011
|
-
});
|
|
20103
|
+
return await controller.replan({ reason });
|
|
20104
|
+
}));
|
|
20012
20105
|
server.registerTool("get_state", {
|
|
20013
20106
|
description: "Get the current controller state (reads persistent store).",
|
|
20014
20107
|
inputSchema: object({})
|
|
20015
|
-
}, async () => {
|
|
20016
|
-
|
|
20017
|
-
|
|
20018
|
-
});
|
|
20108
|
+
}, safeHandler(async () => {
|
|
20109
|
+
return await controller.getState();
|
|
20110
|
+
}));
|
|
20019
20111
|
server.registerTool("get_tasks", {
|
|
20020
20112
|
description: "Get current task states.",
|
|
20021
20113
|
inputSchema: object({})
|
|
20022
|
-
}, async () => {
|
|
20023
|
-
|
|
20024
|
-
|
|
20025
|
-
});
|
|
20114
|
+
}, safeHandler(async () => {
|
|
20115
|
+
return await controller.getTasks();
|
|
20116
|
+
}));
|
|
20026
20117
|
server.registerTool("validate_edit", {
|
|
20027
20118
|
description: "Validate an edit against current file content. Returns EDITABLE, ALREADY_APPLIED, or CONFLICT. " + "Call BEFORE executing an edit tool. Only valid in EXECUTING_INLINE or EXECUTING_SUBAGENTS states. " + "IMPORTANT: content parameter is REQUIRED. Read the file first, then pass the full content.",
|
|
20028
20119
|
inputSchema: object({
|
|
20029
20120
|
oldString: string2().describe("The exact string to find in content (must be unique)."),
|
|
20030
20121
|
newString: string2().describe("The replacement string."),
|
|
20031
|
-
content: string2().
|
|
20122
|
+
content: string2().describe("REQUIRED — The current file content. Read the file first with Read tool, then pass the full content here."),
|
|
20032
20123
|
taskId: string2().optional().describe("Optional task ID for tracking.")
|
|
20033
20124
|
})
|
|
20034
|
-
}, async ({ oldString, newString, content, taskId }) => {
|
|
20125
|
+
}, safeHandler(async ({ oldString, newString, content, taskId }) => {
|
|
20035
20126
|
log("tool:validate_edit", {
|
|
20036
20127
|
taskId,
|
|
20037
20128
|
oldLen: oldString?.length,
|
|
@@ -20040,20 +20131,12 @@ server.registerTool("validate_edit", {
|
|
|
20040
20131
|
});
|
|
20041
20132
|
if (typeof content !== "string" || typeof oldString !== "string" || typeof newString !== "string") {
|
|
20042
20133
|
return {
|
|
20043
|
-
|
|
20044
|
-
|
|
20045
|
-
type: "text",
|
|
20046
|
-
text: JSON.stringify({
|
|
20047
|
-
outcome: "CONFLICT",
|
|
20048
|
-
reason: "Missing required fields: content, oldString, and newString are all required. Read the file first, then pass content to validate_edit."
|
|
20049
|
-
})
|
|
20050
|
-
}
|
|
20051
|
-
]
|
|
20134
|
+
outcome: "CONFLICT",
|
|
20135
|
+
reason: "Missing required fields: content, oldString, and newString are all required. Read the file first, then pass content to validate_edit."
|
|
20052
20136
|
};
|
|
20053
20137
|
}
|
|
20054
|
-
|
|
20055
|
-
|
|
20056
|
-
});
|
|
20138
|
+
return await controller.validateEdit({ oldString, newString, content, taskId });
|
|
20139
|
+
}));
|
|
20057
20140
|
server.registerTool("complete_task", {
|
|
20058
20141
|
description: "Mark a task as completed and optionally record a file fingerprint. " + "Only valid in EXECUTING_INLINE or EXECUTING_SUBAGENTS states.",
|
|
20059
20142
|
inputSchema: object({
|
|
@@ -20061,14 +20144,33 @@ server.registerTool("complete_task", {
|
|
|
20061
20144
|
filePath: string2().optional().describe("Optional file path that was modified."),
|
|
20062
20145
|
fileHash: string2().optional().describe("Optional SHA-256 hash of the file after modification.")
|
|
20063
20146
|
})
|
|
20064
|
-
}, async ({ taskId, filePath, fileHash }) => {
|
|
20147
|
+
}, safeHandler(async ({ taskId, filePath, fileHash }) => {
|
|
20065
20148
|
log("tool:complete_task", { taskId, filePath });
|
|
20066
|
-
|
|
20067
|
-
|
|
20068
|
-
|
|
20149
|
+
return await controller.completeTask({ taskId, filePath, fileHash });
|
|
20150
|
+
}));
|
|
20151
|
+
function setupGracefulShutdown(ctrl) {
|
|
20152
|
+
const shutdown = (signal) => {
|
|
20153
|
+
log("shutdown", { signal });
|
|
20154
|
+
try {
|
|
20155
|
+
if (ctrl)
|
|
20156
|
+
ctrl.flush();
|
|
20157
|
+
} catch {}
|
|
20158
|
+
try {
|
|
20159
|
+
cleanupTmpFiles(statePath);
|
|
20160
|
+
} catch {}
|
|
20161
|
+
process.exit(signal === "SIGINT" ? 130 : 0);
|
|
20162
|
+
};
|
|
20163
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
20164
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
20165
|
+
process.on("unhandledRejection", (reason) => {
|
|
20166
|
+
log("unhandled_rejection", { reason: String(reason) });
|
|
20167
|
+
});
|
|
20168
|
+
}
|
|
20069
20169
|
async function main() {
|
|
20070
20170
|
log("Starting ostacky-controller MCP...");
|
|
20071
20171
|
log("State path:", { path: statePath });
|
|
20172
|
+
cleanupTmpFiles(statePath);
|
|
20173
|
+
setupGracefulShutdown(controller);
|
|
20072
20174
|
const transport = new StdioServerTransport;
|
|
20073
20175
|
await server.connect(transport);
|
|
20074
20176
|
log("ostacky-controller connected and ready");
|