ostacky 0.5.8 → 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 +16 -13
- package/assets/agents/ostacky.md +81 -43
- package/assets/commands/install-stack.md +1 -1
- package/assets/mcp/ostacky-controller/index.js +263 -80
- package/assets/mcp/ostacky-controller/package.json +1 -1
- package/assets/tests/agent-contract.test.js +227 -0
- package/assets/tests/controller.test.js +187 -0
- package/assets/tests/validate-config.sh +18 -0
- package/dist/cli.js +27 -28
- package/dist/mcp/ostacky-controller/index.js +219 -88
- package/manifest.json +23 -23
- package/package.json +1 -1
|
@@ -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",
|
|
@@ -19539,8 +19572,13 @@ class OstackyController {
|
|
|
19539
19572
|
#loaded;
|
|
19540
19573
|
constructor(opts = {}) {
|
|
19541
19574
|
this.#statePath = opts.statePath;
|
|
19542
|
-
|
|
19543
|
-
|
|
19575
|
+
if (opts.initialState) {
|
|
19576
|
+
this.#state = { ...DEFAULT_STATE, ...opts.initialState };
|
|
19577
|
+
this.#loaded = true;
|
|
19578
|
+
} else {
|
|
19579
|
+
this.#state = null;
|
|
19580
|
+
this.#loaded = false;
|
|
19581
|
+
}
|
|
19544
19582
|
}
|
|
19545
19583
|
#load() {
|
|
19546
19584
|
if (this.#loaded)
|
|
@@ -19552,9 +19590,29 @@ class OstackyController {
|
|
|
19552
19590
|
}
|
|
19553
19591
|
try {
|
|
19554
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`);
|
|
19555
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;
|
|
19556
19610
|
} catch {
|
|
19557
|
-
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 });
|
|
19558
19616
|
}
|
|
19559
19617
|
this.#loaded = true;
|
|
19560
19618
|
}
|
|
@@ -19563,14 +19621,48 @@ class OstackyController {
|
|
|
19563
19621
|
return;
|
|
19564
19622
|
const dir = dirname(this.#statePath);
|
|
19565
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
|
+
}
|
|
19566
19638
|
const tmp = this.#statePath + ".tmp." + process.pid;
|
|
19567
|
-
writeFileSync(tmp,
|
|
19639
|
+
writeFileSync(tmp, serialized, "utf8");
|
|
19568
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 });
|
|
19569
19660
|
}
|
|
19570
19661
|
#transition(to, changes = {}) {
|
|
19571
19662
|
this.#state.revision++;
|
|
19572
19663
|
this.#state.state = to;
|
|
19573
19664
|
Object.assign(this.#state, changes);
|
|
19665
|
+
this.#trimTasks();
|
|
19574
19666
|
this.#persist();
|
|
19575
19667
|
}
|
|
19576
19668
|
#isAllowedTransition(from, via, choiceOrMode) {
|
|
@@ -19687,7 +19779,7 @@ class OstackyController {
|
|
|
19687
19779
|
return { error: `Cannot record discovery from state ${this.#state.state}` };
|
|
19688
19780
|
if (!["0", "0+1", "1+"].includes(level))
|
|
19689
19781
|
return { error: `Invalid level: ${level}` };
|
|
19690
|
-
this.#transition(
|
|
19782
|
+
this.#transition(to, {
|
|
19691
19783
|
routeDecisionId: routeDecisionId || "route-" + Date.now(),
|
|
19692
19784
|
routeChoice: null,
|
|
19693
19785
|
snapshots: { ...this.#state.snapshots, codegraph: snapshot || this.#state.snapshots.codegraph }
|
|
@@ -19872,6 +19964,7 @@ class OstackyController {
|
|
|
19872
19964
|
this.#state.fileFingerprints = {};
|
|
19873
19965
|
this.#state.fileFingerprints[filePath] = fileHash;
|
|
19874
19966
|
}
|
|
19967
|
+
this.#trimTasks();
|
|
19875
19968
|
this.#persist();
|
|
19876
19969
|
return {
|
|
19877
19970
|
taskId,
|
|
@@ -19879,12 +19972,29 @@ class OstackyController {
|
|
|
19879
19972
|
totalCompleted: Object.keys(this.#state.tasks).filter((k) => this.#state.tasks[k].status === "COMPLETED").length
|
|
19880
19973
|
};
|
|
19881
19974
|
}
|
|
19975
|
+
flush() {
|
|
19976
|
+
this.#persist();
|
|
19977
|
+
}
|
|
19882
19978
|
}
|
|
19883
19979
|
var statePath = process.env.OSTACKY_STATE_PATH || ".opencode/ostacky-state.json";
|
|
19884
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
|
+
}
|
|
19885
19995
|
var server = new McpServer({
|
|
19886
19996
|
name: "ostacky-controller",
|
|
19887
|
-
version: "0.5.
|
|
19997
|
+
version: "0.5.10"
|
|
19888
19998
|
});
|
|
19889
19999
|
server.registerTool("start_request", {
|
|
19890
20000
|
description: "Start or resume a new request in the state machine. Call this first.",
|
|
@@ -19892,29 +20002,26 @@ server.registerTool("start_request", {
|
|
|
19892
20002
|
requestId: string2().optional().describe("Unique request ID"),
|
|
19893
20003
|
changeId: string2().optional().describe("Optional change ID for OpenSpec tracking")
|
|
19894
20004
|
})
|
|
19895
|
-
}, async ({ requestId, changeId }) => {
|
|
20005
|
+
}, safeHandler(async ({ requestId, changeId }) => {
|
|
19896
20006
|
log("tool:start_request");
|
|
19897
|
-
|
|
19898
|
-
|
|
19899
|
-
});
|
|
20007
|
+
return await controller.startRequest({ requestId, changeId });
|
|
20008
|
+
}));
|
|
19900
20009
|
server.registerTool("request_clarification", {
|
|
19901
20010
|
description: "Record that clarification was requested. Transitions to CLARIFICATION_PENDING.",
|
|
19902
20011
|
inputSchema: object({
|
|
19903
20012
|
question: string2().optional().describe("The clarification question")
|
|
19904
20013
|
})
|
|
19905
|
-
}, async ({ question }) => {
|
|
20014
|
+
}, safeHandler(async ({ question }) => {
|
|
19906
20015
|
log("tool:request_clarification");
|
|
19907
|
-
|
|
19908
|
-
|
|
19909
|
-
});
|
|
20016
|
+
return await controller.requestClarification({ question });
|
|
20017
|
+
}));
|
|
19910
20018
|
server.registerTool("record_clarification", {
|
|
19911
20019
|
description: "Record that clarification was answered. Transitions to DISCOVERY.",
|
|
19912
20020
|
inputSchema: object({})
|
|
19913
|
-
}, async () => {
|
|
20021
|
+
}, safeHandler(async () => {
|
|
19914
20022
|
log("tool:record_clarification");
|
|
19915
|
-
|
|
19916
|
-
|
|
19917
|
-
});
|
|
20023
|
+
return await controller.recordClarification();
|
|
20024
|
+
}));
|
|
19918
20025
|
server.registerTool("record_discovery", {
|
|
19919
20026
|
description: "Record discovery complete with level classification. Transitions to ROUTE_DECISION_PENDING.",
|
|
19920
20027
|
inputSchema: object({
|
|
@@ -19922,115 +20029,114 @@ server.registerTool("record_discovery", {
|
|
|
19922
20029
|
routeDecisionId: string2().optional().describe("Unique route decision ID"),
|
|
19923
20030
|
snapshot: any().optional().describe("Optional CodeGraph snapshot")
|
|
19924
20031
|
})
|
|
19925
|
-
}, async ({ level, routeDecisionId, snapshot }) => {
|
|
20032
|
+
}, safeHandler(async ({ level, routeDecisionId, snapshot }) => {
|
|
19926
20033
|
log("tool:record_discovery", { level });
|
|
19927
|
-
|
|
19928
|
-
|
|
19929
|
-
});
|
|
20034
|
+
return await controller.recordDiscovery({ level, routeDecisionId, snapshot });
|
|
20035
|
+
}));
|
|
19930
20036
|
server.registerTool("consume_route_decision", {
|
|
19931
20037
|
description: "Consume the route decision (SPEC or DIRECT). Valid only in ROUTE_DECISION_PENDING.",
|
|
19932
20038
|
inputSchema: object({
|
|
19933
20039
|
decisionId: string2().describe("Route decision ID from record_discovery"),
|
|
19934
20040
|
choice: _enum(["SPEC", "DIRECT"]).describe("Route choice")
|
|
19935
20041
|
})
|
|
19936
|
-
}, async ({ decisionId, choice }) => {
|
|
20042
|
+
}, safeHandler(async ({ decisionId, choice }) => {
|
|
19937
20043
|
log("tool:consume_route_decision", { choice });
|
|
19938
|
-
|
|
19939
|
-
|
|
19940
|
-
});
|
|
20044
|
+
return await controller.consumeRouteDecision({ decisionId, choice });
|
|
20045
|
+
}));
|
|
19941
20046
|
server.registerTool("spec_complete", {
|
|
19942
20047
|
description: "Mark specification phase as complete. Transitions to EXECUTION_ANALYSIS.",
|
|
19943
20048
|
inputSchema: object({})
|
|
19944
|
-
}, async () => {
|
|
20049
|
+
}, safeHandler(async () => {
|
|
19945
20050
|
log("tool:spec_complete");
|
|
19946
|
-
|
|
19947
|
-
|
|
19948
|
-
});
|
|
20051
|
+
return await controller.specComplete();
|
|
20052
|
+
}));
|
|
19949
20053
|
server.registerTool("record_execution_analysis", {
|
|
19950
20054
|
description: "Record execution analysis with snapshot. Transitions to EXECUTION_DECISION_PENDING.",
|
|
19951
20055
|
inputSchema: object({
|
|
19952
20056
|
executionDecisionId: string2().optional().describe("Unique execution decision ID"),
|
|
19953
20057
|
snapshot: any().optional().describe("Execution analysis snapshot")
|
|
19954
20058
|
})
|
|
19955
|
-
}, async ({ executionDecisionId, snapshot }) => {
|
|
20059
|
+
}, safeHandler(async ({ executionDecisionId, snapshot }) => {
|
|
19956
20060
|
log("tool:record_execution_analysis");
|
|
19957
|
-
|
|
19958
|
-
|
|
19959
|
-
});
|
|
20061
|
+
return await controller.recordExecutionAnalysis({ executionDecisionId, snapshot });
|
|
20062
|
+
}));
|
|
19960
20063
|
server.registerTool("consume_execution_decision", {
|
|
19961
20064
|
description: "Consume the execution mode decision (INLINE or SUBAGENT_DRIVEN).",
|
|
19962
20065
|
inputSchema: object({
|
|
19963
20066
|
decisionId: string2().describe("Execution decision ID from record_execution_analysis"),
|
|
19964
20067
|
mode: _enum(["INLINE", "SUBAGENT_DRIVEN"]).describe("Execution mode")
|
|
19965
20068
|
})
|
|
19966
|
-
}, async ({ decisionId, mode }) => {
|
|
20069
|
+
}, safeHandler(async ({ decisionId, mode }) => {
|
|
19967
20070
|
log("tool:consume_execution_decision", { mode });
|
|
19968
|
-
|
|
19969
|
-
|
|
19970
|
-
});
|
|
20071
|
+
return await controller.consumeExecutionDecision({ decisionId, mode });
|
|
20072
|
+
}));
|
|
19971
20073
|
server.registerTool("implementation_complete", {
|
|
19972
20074
|
description: "Mark implementation as complete. Transitions to SYNC.",
|
|
19973
20075
|
inputSchema: object({})
|
|
19974
|
-
}, async () => {
|
|
20076
|
+
}, safeHandler(async () => {
|
|
19975
20077
|
log("tool:implementation_complete");
|
|
19976
|
-
|
|
19977
|
-
|
|
19978
|
-
});
|
|
20078
|
+
return await controller.implementationComplete();
|
|
20079
|
+
}));
|
|
19979
20080
|
server.registerTool("sync_complete", {
|
|
19980
20081
|
description: "Mark sync as complete. Transitions to DONE.",
|
|
19981
20082
|
inputSchema: object({})
|
|
19982
|
-
}, async () => {
|
|
20083
|
+
}, safeHandler(async () => {
|
|
19983
20084
|
log("tool:sync_complete");
|
|
19984
|
-
|
|
19985
|
-
|
|
19986
|
-
});
|
|
20085
|
+
return await controller.syncComplete();
|
|
20086
|
+
}));
|
|
19987
20087
|
server.registerTool("block", {
|
|
19988
20088
|
description: "Transition to BLOCKED state with an optional reason.",
|
|
19989
20089
|
inputSchema: object({
|
|
19990
20090
|
reason: string2().optional().describe("Reason for blocking")
|
|
19991
20091
|
})
|
|
19992
|
-
}, async ({ reason }) => {
|
|
20092
|
+
}, safeHandler(async ({ reason }) => {
|
|
19993
20093
|
log("tool:block");
|
|
19994
|
-
|
|
19995
|
-
|
|
19996
|
-
});
|
|
20094
|
+
return await controller.block({ reason });
|
|
20095
|
+
}));
|
|
19997
20096
|
server.registerTool("replan", {
|
|
19998
20097
|
description: "Replan from BLOCKED state back to INTERPRETATION_PENDING.",
|
|
19999
20098
|
inputSchema: object({
|
|
20000
20099
|
reason: string2().optional().describe("Reason for replanning")
|
|
20001
20100
|
})
|
|
20002
|
-
}, async ({ reason }) => {
|
|
20101
|
+
}, safeHandler(async ({ reason }) => {
|
|
20003
20102
|
log("tool:replan");
|
|
20004
|
-
|
|
20005
|
-
|
|
20006
|
-
});
|
|
20103
|
+
return await controller.replan({ reason });
|
|
20104
|
+
}));
|
|
20007
20105
|
server.registerTool("get_state", {
|
|
20008
20106
|
description: "Get the current controller state (reads persistent store).",
|
|
20009
20107
|
inputSchema: object({})
|
|
20010
|
-
}, async () => {
|
|
20011
|
-
|
|
20012
|
-
|
|
20013
|
-
});
|
|
20108
|
+
}, safeHandler(async () => {
|
|
20109
|
+
return await controller.getState();
|
|
20110
|
+
}));
|
|
20014
20111
|
server.registerTool("get_tasks", {
|
|
20015
20112
|
description: "Get current task states.",
|
|
20016
20113
|
inputSchema: object({})
|
|
20017
|
-
}, async () => {
|
|
20018
|
-
|
|
20019
|
-
|
|
20020
|
-
});
|
|
20114
|
+
}, safeHandler(async () => {
|
|
20115
|
+
return await controller.getTasks();
|
|
20116
|
+
}));
|
|
20021
20117
|
server.registerTool("validate_edit", {
|
|
20022
|
-
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.",
|
|
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.",
|
|
20023
20119
|
inputSchema: object({
|
|
20024
20120
|
oldString: string2().describe("The exact string to find in content (must be unique)."),
|
|
20025
20121
|
newString: string2().describe("The replacement string."),
|
|
20026
|
-
content: string2().describe("The current file content
|
|
20122
|
+
content: string2().describe("REQUIRED — The current file content. Read the file first with Read tool, then pass the full content here."),
|
|
20027
20123
|
taskId: string2().optional().describe("Optional task ID for tracking.")
|
|
20028
20124
|
})
|
|
20029
|
-
}, async ({ oldString, newString, content, taskId }) => {
|
|
20030
|
-
log("tool:validate_edit", {
|
|
20031
|
-
|
|
20032
|
-
|
|
20033
|
-
|
|
20125
|
+
}, safeHandler(async ({ oldString, newString, content, taskId }) => {
|
|
20126
|
+
log("tool:validate_edit", {
|
|
20127
|
+
taskId,
|
|
20128
|
+
oldLen: oldString?.length,
|
|
20129
|
+
newLen: newString?.length,
|
|
20130
|
+
hasContent: !!content
|
|
20131
|
+
});
|
|
20132
|
+
if (typeof content !== "string" || typeof oldString !== "string" || typeof newString !== "string") {
|
|
20133
|
+
return {
|
|
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."
|
|
20136
|
+
};
|
|
20137
|
+
}
|
|
20138
|
+
return await controller.validateEdit({ oldString, newString, content, taskId });
|
|
20139
|
+
}));
|
|
20034
20140
|
server.registerTool("complete_task", {
|
|
20035
20141
|
description: "Mark a task as completed and optionally record a file fingerprint. " + "Only valid in EXECUTING_INLINE or EXECUTING_SUBAGENTS states.",
|
|
20036
20142
|
inputSchema: object({
|
|
@@ -20038,19 +20144,44 @@ server.registerTool("complete_task", {
|
|
|
20038
20144
|
filePath: string2().optional().describe("Optional file path that was modified."),
|
|
20039
20145
|
fileHash: string2().optional().describe("Optional SHA-256 hash of the file after modification.")
|
|
20040
20146
|
})
|
|
20041
|
-
}, async ({ taskId, filePath, fileHash }) => {
|
|
20147
|
+
}, safeHandler(async ({ taskId, filePath, fileHash }) => {
|
|
20042
20148
|
log("tool:complete_task", { taskId, filePath });
|
|
20043
|
-
|
|
20044
|
-
|
|
20045
|
-
|
|
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
|
+
}
|
|
20046
20169
|
async function main() {
|
|
20047
20170
|
log("Starting ostacky-controller MCP...");
|
|
20048
20171
|
log("State path:", { path: statePath });
|
|
20172
|
+
cleanupTmpFiles(statePath);
|
|
20173
|
+
setupGracefulShutdown(controller);
|
|
20049
20174
|
const transport = new StdioServerTransport;
|
|
20050
20175
|
await server.connect(transport);
|
|
20051
20176
|
log("ostacky-controller connected and ready");
|
|
20052
20177
|
}
|
|
20053
|
-
|
|
20054
|
-
|
|
20055
|
-
|
|
20056
|
-
|
|
20178
|
+
var isDirectRun = process.argv[1] && (process.argv[1].endsWith("/index.js") || process.argv[1].endsWith("\\index.js"));
|
|
20179
|
+
if (isDirectRun) {
|
|
20180
|
+
main().catch((error2) => {
|
|
20181
|
+
console.error("Fatal error:", error2);
|
|
20182
|
+
process.exit(1);
|
|
20183
|
+
});
|
|
20184
|
+
}
|
|
20185
|
+
export {
|
|
20186
|
+
OstackyController
|
|
20187
|
+
};
|
package/manifest.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.5.
|
|
2
|
+
"version": "0.5.10",
|
|
3
3
|
"repo": "JaimeHoracio/Ostacky",
|
|
4
|
-
"tag": "v0.5.
|
|
4
|
+
"tag": "v0.5.10",
|
|
5
5
|
"agents": [
|
|
6
6
|
{
|
|
7
7
|
"name": "ostacky",
|
|
8
8
|
"file": "assets/agents/ostacky.md",
|
|
9
9
|
"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",
|
|
10
|
-
"version": "0.5.
|
|
11
|
-
"sha256": "
|
|
10
|
+
"version": "0.5.10",
|
|
11
|
+
"sha256": "c624b9163a97fe0aa17a4688f59e2bcfa958c64fbde71c05c0c071911f62b7ad"
|
|
12
12
|
}
|
|
13
13
|
],
|
|
14
14
|
"commands": [
|
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
"name": "install-stack",
|
|
17
17
|
"file": "assets/commands/install-stack.md",
|
|
18
18
|
"description": "Instala el stack tecnológico del proyecto (CodeGraph, skills, OpenSpec, Engram, Context7, controller MCP)",
|
|
19
|
-
"version": "0.5.
|
|
20
|
-
"sha256": "
|
|
19
|
+
"version": "0.5.10",
|
|
20
|
+
"sha256": "61f18cb616bffe4a982d874ee9426011c203ab9c17efb88d44592e202c699330"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"name": "opsx-sync",
|
|
24
24
|
"file": "assets/commands/opsx-sync.md",
|
|
25
25
|
"description": "Sincroniza delta specs del change activo sin inicializar CodeGraph si ya existe índice",
|
|
26
|
-
"version": "0.5.
|
|
26
|
+
"version": "0.5.10",
|
|
27
27
|
"sha256": "ed9948f1910743b672e1dfad496bc96972a224a98b063232be39290d43068c50"
|
|
28
28
|
}
|
|
29
29
|
],
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"name": "ostacky-controller",
|
|
33
33
|
"file": "assets/mcp/ostacky-controller/",
|
|
34
34
|
"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",
|
|
35
|
-
"version": "0.5.
|
|
36
|
-
"sha256": "
|
|
35
|
+
"version": "0.5.10",
|
|
36
|
+
"sha256": "4469b9e26910a5ba016a0fe7a2d77507d40715d95ce4cbb31c498bf7001b4d0d"
|
|
37
37
|
}
|
|
38
38
|
],
|
|
39
39
|
"skills": [
|
|
@@ -41,98 +41,98 @@
|
|
|
41
41
|
"name": "thinking",
|
|
42
42
|
"file": "assets/skills/thinking/SKILL.md",
|
|
43
43
|
"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)",
|
|
44
|
-
"version": "0.5.
|
|
44
|
+
"version": "0.5.10",
|
|
45
45
|
"sha256": "6a1fdac86357f89e132013e56cb84c1920880c1c035a8dcf53f69e01824724bb"
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
"name": "execution-mode-evaluation",
|
|
49
49
|
"file": "assets/skills/execution-mode-evaluation/SKILL.md",
|
|
50
50
|
"description": "Skill de análisis de modo de ejecución — output reconciliado con controller snapshot contract (recommendation field)",
|
|
51
|
-
"version": "0.5.
|
|
51
|
+
"version": "0.5.10",
|
|
52
52
|
"sha256": "048925aec576dff014eed0b44e2a553c635ae405f5a245773f480be9b84d18a7"
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"name": "writing-plans",
|
|
56
56
|
"file": "assets/skills/writing-plans/SKILL.md",
|
|
57
57
|
"description": "Skill de planificación de implementación (Superpowers) — execution handoff removido, Ostacky decide modo de ejecución",
|
|
58
|
-
"version": "0.5.
|
|
58
|
+
"version": "0.5.10",
|
|
59
59
|
"sha256": "13f6e43522567505da90793934d414d290d56f4ea587ac696f66416af9e7633f"
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
"name": "tdd",
|
|
63
63
|
"file": "assets/skills/tdd/SKILL.md",
|
|
64
64
|
"description": "Skill de test-driven development (Superpowers)",
|
|
65
|
-
"version": "0.5.
|
|
65
|
+
"version": "0.5.10",
|
|
66
66
|
"sha256": "7eacd8ee81dc5c0b85065c0392e37ee10314661f7edb59dd8e70a9e0dae8f371"
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
69
|
"name": "subagent-driven-development",
|
|
70
70
|
"file": "assets/skills/subagent-driven-development/SKILL.md",
|
|
71
71
|
"description": "Skill de ejecución con subagentes (Superpowers) — ejecuta solo después de confirmación del coordinador Ostacky",
|
|
72
|
-
"version": "0.5.
|
|
72
|
+
"version": "0.5.10",
|
|
73
73
|
"sha256": "cdacae2e5b86f6a642ad0316172cee7dc8d19e909ef4f9ab7fcf7e84a248dda3"
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
76
|
"name": "dispatching-parallel-agents",
|
|
77
77
|
"file": "assets/skills/dispatching-parallel-agents/SKILL.md",
|
|
78
78
|
"description": "Skill de dispatch paralelo de agentes (Superpowers)",
|
|
79
|
-
"version": "0.5.
|
|
79
|
+
"version": "0.5.10",
|
|
80
80
|
"sha256": "281edf0c38f358497c7e2066fa8217a2ba3e2a39b4205c4af0c41d328fc035a1"
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"name": "review",
|
|
84
84
|
"file": "assets/skills/review/SKILL.md",
|
|
85
85
|
"description": "Skill de revisión de código (Superpowers)",
|
|
86
|
-
"version": "0.5.
|
|
86
|
+
"version": "0.5.10",
|
|
87
87
|
"sha256": "14831d9ef3746ef6e2e6047c868fe7aa296d01f0644227ce776fe99436357b1a"
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
"name": "receiving-code-review",
|
|
91
91
|
"file": "assets/skills/receiving-code-review/SKILL.md",
|
|
92
92
|
"description": "Skill de recibir y procesar feedback de code review",
|
|
93
|
-
"version": "0.5.
|
|
93
|
+
"version": "0.5.10",
|
|
94
94
|
"sha256": "0a5780e8a41539d15428114b7c46f36670acc16aba0db3348d36be1175433872"
|
|
95
95
|
},
|
|
96
96
|
{
|
|
97
97
|
"name": "openspec-propose",
|
|
98
98
|
"file": "assets/skills/openspec-propose/SKILL.md",
|
|
99
99
|
"description": "Skill de generación de proposal (OpenSpec)",
|
|
100
|
-
"version": "0.5.
|
|
100
|
+
"version": "0.5.10",
|
|
101
101
|
"sha256": "3306b21ba9cb8c1611e8ba335126982f3767f7d317b5ed506c57a14c5f88b263"
|
|
102
102
|
},
|
|
103
103
|
{
|
|
104
104
|
"name": "openspec-apply-change",
|
|
105
105
|
"file": "assets/skills/openspec-apply-change/SKILL.md",
|
|
106
106
|
"description": "Skill de aplicación de change (OpenSpec)",
|
|
107
|
-
"version": "0.5.
|
|
107
|
+
"version": "0.5.10",
|
|
108
108
|
"sha256": "31c1313c3de4616a07efd89306c4c058175d018f42d4381cc7da7030fd03ba30"
|
|
109
109
|
},
|
|
110
110
|
{
|
|
111
111
|
"name": "openspec-archive-change",
|
|
112
112
|
"file": "assets/skills/openspec-archive-change/SKILL.md",
|
|
113
113
|
"description": "Skill de archivo de change (OpenSpec)",
|
|
114
|
-
"version": "0.5.
|
|
114
|
+
"version": "0.5.10",
|
|
115
115
|
"sha256": "5bf65d1848457bd57be1d21707e55fe6746cdee33bce2f32f12d87661b6aa3a4"
|
|
116
116
|
},
|
|
117
117
|
{
|
|
118
118
|
"name": "using-git-worktrees",
|
|
119
119
|
"file": "assets/skills/using-git-worktrees/SKILL.md",
|
|
120
120
|
"description": "Skill de uso de git worktrees para aislamiento de trabajo",
|
|
121
|
-
"version": "0.5.
|
|
121
|
+
"version": "0.5.10",
|
|
122
122
|
"sha256": "33433c24f753d566cde16c0931f70d746b6b0cb4d1f3daaa619e928b009e4fc9"
|
|
123
123
|
},
|
|
124
124
|
{
|
|
125
125
|
"name": "using-superpowers",
|
|
126
126
|
"file": "assets/skills/using-superpowers/SKILL.md",
|
|
127
127
|
"description": "Skill de orquestación de Superpowers skills",
|
|
128
|
-
"version": "0.5.
|
|
128
|
+
"version": "0.5.10",
|
|
129
129
|
"sha256": "fba542003aa788b5edb4ae2d1135773a1dde8111d1dd60378e2f61b6695d46fc"
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
"name": "writing-skills",
|
|
133
133
|
"file": "assets/skills/writing-skills/SKILL.md",
|
|
134
134
|
"description": "Skill de creación y edición de skills",
|
|
135
|
-
"version": "0.5.
|
|
135
|
+
"version": "0.5.10",
|
|
136
136
|
"sha256": "7f74ffe049283803640d78f8c9ac1ea5f5e626dedb885ecff29a87c5b5d41598"
|
|
137
137
|
}
|
|
138
138
|
]
|