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/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.9",
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.9",
882
+ version: "0.5.10",
883
883
  repo: "JaimeHoracio/Ostacky",
884
- tag: "v0.5.9",
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.9",
891
- sha256: "ede9800f644d890fa81c4827592ca62ce00756a25fbdb594cb8a7b40cd7ca2fc"
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.9",
900
- sha256: "598b54bed669586f832193226ebeddbda325325f8f237ba5104ab3ee58f2ac29"
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.9",
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.9",
916
- sha256: "68b42dfb65938e423a7c81d9e9b179c0d82533047d5c0205cf459f5aa2fc6fa2"
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.9",
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.9",
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.9",
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.9",
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.9",
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.9",
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.9",
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.9",
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.9",
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.9",
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.9",
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.9",
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.9",
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.9",
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.9",
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
- throw new Error(`MCP server "${item.name}" copiado pero ${installCmd} falló: ${e2.message}.
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 err = gen.name("err");
12266
+ const err2 = gen.name("err");
12267
12267
  gen.forRange("i", errsCount, names_1.default.errors, (i) => {
12268
- gen.const(err, (0, codegen_1._)`${names_1.default.vErrors}[${i}]`);
12269
- gen.if((0, codegen_1._)`${err}.instancePath === undefined`, () => gen.assign((0, codegen_1._)`${err}.instancePath`, (0, codegen_1.strConcat)(names_1.default.instancePath, it.errorPath)));
12270
- gen.assign((0, codegen_1._)`${err}.schemaPath`, (0, codegen_1.str)`${it.errSchemaPath}/${keyword}`);
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._)`${err}.schema`, schemaValue);
12273
- gen.assign((0, codegen_1._)`${err}.data`, data);
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 err = gen.const("err", errObj);
12280
- gen.if((0, codegen_1._)`${names_1.default.vErrors} === null`, () => gen.assign(names_1.default.vErrors, (0, codegen_1._)`[${err}]`), (0, codegen_1._)`${names_1.default.vErrors}.push(${err})`);
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 ? ` ${JSON.stringify(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 = { ...DEFAULT_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, JSON.stringify(this.#state, null, 2), "utf8");
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("ROUTE_DECISION_PENDING", {
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.9"
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
- const result = await controller.startRequest({ requestId, changeId });
19903
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
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
- const result = await controller.requestClarification({ question });
19913
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
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
- const result = await controller.recordClarification();
19921
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
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
- const result = await controller.recordDiscovery({ level, routeDecisionId, snapshot });
19933
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
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
- const result = await controller.consumeRouteDecision({ decisionId, choice });
19944
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
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
- const result = await controller.specComplete();
19952
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
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
- const result = await controller.recordExecutionAnalysis({ executionDecisionId, snapshot });
19963
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
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
- const result = await controller.consumeExecutionDecision({ decisionId, mode });
19974
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
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
- const result = await controller.implementationComplete();
19982
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
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
- const result = await controller.syncComplete();
19990
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
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
- const result = await controller.block({ reason });
20000
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
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
- const result = await controller.replan({ reason });
20010
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
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
- const result = await controller.getState();
20017
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
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
- const result = await controller.getTasks();
20024
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
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().optional().describe("The current file content. REQUIRED — read the file first with Read tool."),
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
- content: [
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
- const result = await controller.validateEdit({ oldString, newString, content, taskId });
20055
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
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
- const result = await controller.completeTask({ taskId, filePath, fileHash });
20067
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
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");