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
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
const ASSETS_DIR = join(__dirname, '..');
|
|
10
|
+
const AGENT_FILE = join(ASSETS_DIR, 'agents', 'ostacky.md');
|
|
11
|
+
const CONTROLLER_FILE = join(ASSETS_DIR, 'mcp', 'ostacky-controller', 'index.js');
|
|
12
|
+
|
|
13
|
+
const agentContent = readFileSync(AGENT_FILE, 'utf8');
|
|
14
|
+
const controllerContent = readFileSync(CONTROLLER_FILE, 'utf8');
|
|
15
|
+
|
|
16
|
+
// Tools registered in the MCP controller
|
|
17
|
+
const MCP_TOOLS = [
|
|
18
|
+
'start_request', 'request_clarification', 'record_clarification',
|
|
19
|
+
'record_discovery', 'consume_route_decision', 'spec_complete',
|
|
20
|
+
'record_execution_analysis', 'consume_execution_decision',
|
|
21
|
+
'implementation_complete', 'sync_complete', 'block', 'replan',
|
|
22
|
+
'get_state', 'get_tasks', 'validate_edit', 'complete_task',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// CodeGraph tools referenced in agent instructions
|
|
26
|
+
const CODEGRAPH_TOOLS = [
|
|
27
|
+
'codegraph_explore', 'codegraph_node', 'codegraph_search',
|
|
28
|
+
'codegraph_callers', 'codegraph_callees', 'codegraph_impact',
|
|
29
|
+
'codegraph_files', 'codegraph_status',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// Native OpenCode tools
|
|
33
|
+
const NATIVE_TOOLS = ['question', 'Read', 'Edit', 'Bash', 'Grep', 'Glob'];
|
|
34
|
+
|
|
35
|
+
const ALL_VALID_TOOLS = [...MCP_TOOLS, ...CODEGRAPH_TOOLS, ...NATIVE_TOOLS];
|
|
36
|
+
|
|
37
|
+
describe('Agent contract — ostacky.md', () => {
|
|
38
|
+
describe('Core Instructions section', () => {
|
|
39
|
+
it('has "Core Instructions" heading', () => {
|
|
40
|
+
assert.ok(agentContent.includes('## Core Instructions'),
|
|
41
|
+
'Missing "## Core Instructions" section');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('has SINGLE SOURCE OF VERDAD marker', () => {
|
|
45
|
+
assert.ok(agentContent.includes('SINGLE SOURCE OF VERDAD'),
|
|
46
|
+
'Missing "SINGLE SOURCE OF VERDAD" marker');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('CodeGraph section', () => {
|
|
51
|
+
it('has CodeGraph — búsqueda de código subsection', () => {
|
|
52
|
+
assert.ok(agentContent.includes('### CodeGraph — búsqueda de código'),
|
|
53
|
+
'Missing "### CodeGraph — búsqueda de código" section');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('has tool table with codegraph_explore', () => {
|
|
57
|
+
assert.ok(agentContent.includes('codegraph_explore'),
|
|
58
|
+
'Missing codegraph_explore in tool table');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('Engram section', () => {
|
|
63
|
+
it('has Engram — memoria persistente subsection', () => {
|
|
64
|
+
assert.ok(agentContent.includes('### Engram — memoria persistente'),
|
|
65
|
+
'Missing "### Engram — memoria persistente" section');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('has engram_mem_context in Discovery flow', () => {
|
|
69
|
+
assert.ok(agentContent.includes('engram_mem_context'),
|
|
70
|
+
'Missing engram_mem_context — agent should consult memory at start of request');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('has engram_mem_search reference', () => {
|
|
74
|
+
assert.ok(agentContent.includes('engram_mem_search'),
|
|
75
|
+
'Missing engram_mem_search — agent should search before deciding');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('has engram_mem_save reference', () => {
|
|
79
|
+
assert.ok(agentContent.includes('engram_mem_save'),
|
|
80
|
+
'Missing engram_mem_save — agent should save after significant work');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('HARD-STOP rules', () => {
|
|
85
|
+
it('has at least 2 HARD-STOP blocks (HTML tags or inline markers)', () => {
|
|
86
|
+
const htmlBlocks = (agentContent.match(/<HARD-STOP>/g) || []).length;
|
|
87
|
+
const inlineMarkers = (agentContent.match(/\*\*HARD-STOP\*\*/g) || []).length;
|
|
88
|
+
const total = htmlBlocks + inlineMarkers;
|
|
89
|
+
assert.ok(total >= 2,
|
|
90
|
+
`Expected at least 2 HARD-STOP blocks (HTML or inline), found ${total} (${htmlBlocks} HTML, ${inlineMarkers} inline)`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('has closing HARD-STOP tags matching opening tags', () => {
|
|
94
|
+
const opens = (agentContent.match(/<HARD-STOP>/g) || []).length;
|
|
95
|
+
const closes = (agentContent.match(/<\/HARD-STOP>/g) || []).length;
|
|
96
|
+
assert.equal(opens, closes,
|
|
97
|
+
`Mismatched HARD-STOP tags: ${opens} opens, ${closes} closes`);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('has HARD-STOP in Regla de oro (global question stop)', () => {
|
|
101
|
+
const reglaDeOro = agentContent.substring(
|
|
102
|
+
agentContent.indexOf('## Regla de oro'),
|
|
103
|
+
agentContent.indexOf('## Flujo')
|
|
104
|
+
);
|
|
105
|
+
assert.ok(reglaDeOro.includes('<HARD-STOP>'),
|
|
106
|
+
'Missing HARD-STOP in Regla de Oro section — agent can continue after questions');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('Tool name references — no stale names', () => {
|
|
111
|
+
it('does NOT reference "ask_user" (non-existent tool)', () => {
|
|
112
|
+
const refs = agentContent.match(/ask_user/g);
|
|
113
|
+
const count = refs ? refs.length : 0;
|
|
114
|
+
assert.equal(count, 0,
|
|
115
|
+
`Found ${count} reference(s) to "ask_user" — this tool does not exist. Use "question" instead.`);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('does NOT reference "codegraph_context" (stale name)', () => {
|
|
119
|
+
// Allow it in comments or descriptions, but not as an active tool call
|
|
120
|
+
const lines = agentContent.split('\n');
|
|
121
|
+
const offenders = lines.filter(line =>
|
|
122
|
+
line.includes('codegraph_context') &&
|
|
123
|
+
!line.startsWith('|') &&
|
|
124
|
+
!line.includes('NUNCA') &&
|
|
125
|
+
!line.includes('Prohibido')
|
|
126
|
+
);
|
|
127
|
+
assert.equal(offenders.length, 0,
|
|
128
|
+
`Found stale "codegraph_context" references (use codegraph_explore): ${offenders.join('; ')}`);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('references valid MCP tools in flow instructions', () => {
|
|
132
|
+
// Extract tool names used in flow steps (lines with backtick-quoted tool names)
|
|
133
|
+
const toolRefs = agentContent.match(/`([a-z_]+)`/g) || [];
|
|
134
|
+
const uniqueTools = [...new Set(toolRefs.map(t => t.replace(/`/g, '')))];
|
|
135
|
+
|
|
136
|
+
const flowTools = uniqueTools.filter(t =>
|
|
137
|
+
t.includes('request') || t.includes('record') || t.includes('consume') ||
|
|
138
|
+
t.includes('spec_') || t.includes('implementation') || t.includes('sync') ||
|
|
139
|
+
t.includes('validate') || t.includes('complete') || t.includes('block') ||
|
|
140
|
+
t.includes('replan') || t.includes('start_') || t.includes('get_')
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const invalidTools = flowTools.filter(t => !MCP_TOOLS.includes(t));
|
|
144
|
+
assert.equal(invalidTools.length, 0,
|
|
145
|
+
`References non-existent MCP tools: ${invalidTools.join(', ')}`);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('validate_edit instructions', () => {
|
|
150
|
+
it('mentions content as required parameter', () => {
|
|
151
|
+
assert.ok(
|
|
152
|
+
agentContent.includes('content') &&
|
|
153
|
+
(agentContent.includes('content es OBLIGATORIO') ||
|
|
154
|
+
agentContent.includes('`content` es OBLIGATORIO')),
|
|
155
|
+
'Missing explicit instruction that content is required for validate_edit'
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('instructs to Read file before validate_edit', () => {
|
|
160
|
+
const executionSection = agentContent.substring(
|
|
161
|
+
agentContent.indexOf('### 4. Execution'),
|
|
162
|
+
agentContent.indexOf('### 5. Sync')
|
|
163
|
+
);
|
|
164
|
+
assert.ok(executionSection.includes('Read'),
|
|
165
|
+
'Execution section should instruct to Read file before validate_edit');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('Classification rules', () => {
|
|
170
|
+
it('has Nivel 0, 0+1, and 1+', () => {
|
|
171
|
+
assert.ok(agentContent.includes('Nivel 0'), 'Missing Nivel 0');
|
|
172
|
+
assert.ok(agentContent.includes('Nivel 0+1'), 'Missing Nivel 0+1');
|
|
173
|
+
assert.ok(agentContent.includes('Nivel 1+'), 'Missing Nivel 1+');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('has defaultChoice mapping', () => {
|
|
177
|
+
assert.ok(agentContent.includes('defaultChoice'), 'Missing defaultChoice mapping');
|
|
178
|
+
assert.ok(agentContent.includes('"DIRECT"'), 'Missing DIRECT default');
|
|
179
|
+
assert.ok(agentContent.includes('"SPEC"'), 'Missing SPEC default');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('Guardrails', () => {
|
|
184
|
+
it('has Eficiencia de tokens section', () => {
|
|
185
|
+
assert.ok(agentContent.includes('### Eficiencia de tokens'),
|
|
186
|
+
'Missing "### Eficiencia de tokens" guardrail section');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('has Decisiones y estado section', () => {
|
|
190
|
+
assert.ok(agentContent.includes('### Decisiones y estado'),
|
|
191
|
+
'Missing "### Decisiones y estado" guardrail section');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('Controller contract — index.js', () => {
|
|
197
|
+
it('exports OstackyController', () => {
|
|
198
|
+
assert.ok(controllerContent.includes('export { OstackyController }'),
|
|
199
|
+
'Missing OstackyController export');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('has validate_edit tool registered', () => {
|
|
203
|
+
assert.ok(controllerContent.includes("server.registerTool(\n 'validate_edit'") ||
|
|
204
|
+
controllerContent.includes("server.registerTool('validate_edit'"),
|
|
205
|
+
'validate_edit not registered as MCP tool');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('validate_edit schema has content as required (z.string())', () => {
|
|
209
|
+
// Find the validate_edit schema section
|
|
210
|
+
const validateSection = controllerContent.substring(
|
|
211
|
+
controllerContent.indexOf("'validate_edit'"),
|
|
212
|
+
controllerContent.indexOf("'complete_task'")
|
|
213
|
+
);
|
|
214
|
+
assert.ok(validateSection.includes('content: z.string()') &&
|
|
215
|
+
!validateSection.includes('content: z.string().optional()'),
|
|
216
|
+
'validate_edit content param should be z.string() required — prevents silent state corruption');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('validate_edit handler validates content type', () => {
|
|
220
|
+
const validateSection = controllerContent.substring(
|
|
221
|
+
controllerContent.indexOf("'validate_edit'"),
|
|
222
|
+
controllerContent.indexOf("'complete_task'")
|
|
223
|
+
);
|
|
224
|
+
assert.ok(validateSection.includes("typeof content !== 'string'"),
|
|
225
|
+
'validate_edit handler should check typeof content');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { OstackyController } from '../mcp/ostacky-controller/index.js';
|
|
4
|
+
|
|
5
|
+
function createController(initialState = {}) {
|
|
6
|
+
return new OstackyController({
|
|
7
|
+
statePath: null,
|
|
8
|
+
initialState: {
|
|
9
|
+
state: 'INTERPRETATION_PENDING',
|
|
10
|
+
revision: 0,
|
|
11
|
+
requestId: 'test',
|
|
12
|
+
changeId: null,
|
|
13
|
+
routeDecisionId: null,
|
|
14
|
+
routeChoice: null,
|
|
15
|
+
executionDecisionId: null,
|
|
16
|
+
executionMode: null,
|
|
17
|
+
snapshots: { codegraph: null, execution: null },
|
|
18
|
+
tasks: {},
|
|
19
|
+
fileFingerprints: {},
|
|
20
|
+
error: null,
|
|
21
|
+
...initialState,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('OstackyController', () => {
|
|
27
|
+
describe('startRequest', () => {
|
|
28
|
+
it('transitions to INTERPRETATION_PENDING', async () => {
|
|
29
|
+
const ctrl = createController({ state: 'DONE' });
|
|
30
|
+
const result = await ctrl.startRequest({ requestId: 'req-1' });
|
|
31
|
+
assert.equal(result.state, 'INTERPRETATION_PENDING');
|
|
32
|
+
assert.equal(result.requestId, 'req-1');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('generates requestId when not provided', async () => {
|
|
36
|
+
const ctrl = createController({ state: 'DONE' });
|
|
37
|
+
const result = await ctrl.startRequest();
|
|
38
|
+
assert.ok(result.requestId.startsWith('req-'));
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('validateEdit', () => {
|
|
43
|
+
it('returns CONFLICT when content is undefined (the bug we fixed)', async () => {
|
|
44
|
+
const ctrl = createController({ state: 'EXECUTING_INLINE' });
|
|
45
|
+
const result = await ctrl.validateEdit({
|
|
46
|
+
oldString: 'foo',
|
|
47
|
+
newString: 'bar',
|
|
48
|
+
content: undefined,
|
|
49
|
+
});
|
|
50
|
+
assert.equal(result.outcome, 'CONFLICT');
|
|
51
|
+
assert.ok(result.reason.includes('Missing required fields'));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns CONFLICT when content is not a string', async () => {
|
|
55
|
+
const ctrl = createController({ state: 'EXECUTING_INLINE' });
|
|
56
|
+
const result = await ctrl.validateEdit({
|
|
57
|
+
oldString: 'foo',
|
|
58
|
+
newString: 'bar',
|
|
59
|
+
content: 123,
|
|
60
|
+
});
|
|
61
|
+
assert.equal(result.outcome, 'CONFLICT');
|
|
62
|
+
assert.ok(result.reason.includes('Missing required fields'));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('returns EDITABLE when oldString found exactly once', async () => {
|
|
66
|
+
const ctrl = createController({ state: 'EXECUTING_INLINE' });
|
|
67
|
+
const result = await ctrl.validateEdit({
|
|
68
|
+
oldString: 'hello world',
|
|
69
|
+
newString: 'hello universe',
|
|
70
|
+
content: 'say hello world to everyone',
|
|
71
|
+
});
|
|
72
|
+
assert.equal(result.outcome, 'EDITABLE');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns ALREADY_APPLIED when oldString equals newString', async () => {
|
|
76
|
+
const ctrl = createController({ state: 'EXECUTING_INLINE' });
|
|
77
|
+
const result = await ctrl.validateEdit({
|
|
78
|
+
oldString: 'hello',
|
|
79
|
+
newString: 'hello',
|
|
80
|
+
content: 'hello world',
|
|
81
|
+
});
|
|
82
|
+
assert.equal(result.outcome, 'ALREADY_APPLIED');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('returns ALREADY_APPLIED when newString already present and oldString not found', async () => {
|
|
86
|
+
const ctrl = createController({ state: 'EXECUTING_INLINE' });
|
|
87
|
+
const result = await ctrl.validateEdit({
|
|
88
|
+
oldString: 'old text',
|
|
89
|
+
newString: 'new text',
|
|
90
|
+
content: 'already has new text here',
|
|
91
|
+
});
|
|
92
|
+
assert.equal(result.outcome, 'ALREADY_APPLIED');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('returns CONFLICT when oldString not found and newString not present', async () => {
|
|
96
|
+
const ctrl = createController({ state: 'EXECUTING_INLINE' });
|
|
97
|
+
const result = await ctrl.validateEdit({
|
|
98
|
+
oldString: 'nonexistent',
|
|
99
|
+
newString: 'replacement',
|
|
100
|
+
content: 'some content without the old string',
|
|
101
|
+
});
|
|
102
|
+
assert.equal(result.outcome, 'CONFLICT');
|
|
103
|
+
assert.ok(result.reason.includes('not found'));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('returns CONFLICT when oldString found multiple times', async () => {
|
|
107
|
+
const ctrl = createController({ state: 'EXECUTING_INLINE' });
|
|
108
|
+
const result = await ctrl.validateEdit({
|
|
109
|
+
oldString: 'dup',
|
|
110
|
+
newString: 'replaced',
|
|
111
|
+
content: 'dup and dup again',
|
|
112
|
+
});
|
|
113
|
+
assert.equal(result.outcome, 'CONFLICT');
|
|
114
|
+
assert.ok(result.reason.includes('2 times'));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('returns CONFLICT when not in executing state', async () => {
|
|
118
|
+
const ctrl = createController();
|
|
119
|
+
const result = await ctrl.validateEdit({
|
|
120
|
+
oldString: 'foo',
|
|
121
|
+
newString: 'bar',
|
|
122
|
+
content: 'foo bar baz',
|
|
123
|
+
});
|
|
124
|
+
assert.equal(result.outcome, 'CONFLICT');
|
|
125
|
+
assert.ok(result.reason.includes('Cannot validate edit from state'));
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('state transitions', () => {
|
|
130
|
+
it('full flow: start → discovery → route → execution → sync → done', async () => {
|
|
131
|
+
const ctrl = createController({ state: 'DONE' });
|
|
132
|
+
let r = await ctrl.startRequest({ requestId: 'test' });
|
|
133
|
+
assert.equal(r.state, 'INTERPRETATION_PENDING');
|
|
134
|
+
|
|
135
|
+
r = await ctrl.recordDiscovery({ level: '0', routeDecisionId: 'rd-1' });
|
|
136
|
+
assert.equal(r.state, 'ROUTE_DECISION_PENDING');
|
|
137
|
+
assert.equal(r.defaultChoice, 'DIRECT');
|
|
138
|
+
|
|
139
|
+
r = await ctrl.consumeRouteDecision({ decisionId: 'rd-1', choice: 'DIRECT' });
|
|
140
|
+
assert.equal(r.state, 'EXECUTION_ANALYSIS');
|
|
141
|
+
|
|
142
|
+
r = await ctrl.recordExecutionAnalysis({ executionDecisionId: 'ed-1' });
|
|
143
|
+
assert.equal(r.state, 'EXECUTION_DECISION_PENDING');
|
|
144
|
+
|
|
145
|
+
r = await ctrl.consumeExecutionDecision({ decisionId: 'ed-1', mode: 'INLINE' });
|
|
146
|
+
assert.equal(r.state, 'EXECUTING_INLINE');
|
|
147
|
+
|
|
148
|
+
r = await ctrl.implementationComplete();
|
|
149
|
+
assert.equal(r.state, 'SYNC');
|
|
150
|
+
|
|
151
|
+
r = await ctrl.syncComplete();
|
|
152
|
+
assert.equal(r.state, 'DONE');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('rejects invalid transition: start from EXECUTING_INLINE', async () => {
|
|
156
|
+
const ctrl = createController({ state: 'EXECUTING_INLINE', requestId: 'original' });
|
|
157
|
+
const r = await ctrl.startRequest({ requestId: 'nope' });
|
|
158
|
+
assert.equal(r.state, 'EXECUTING_INLINE');
|
|
159
|
+
assert.equal(r.requestId, 'original');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('level 1+ defaults to SPEC', async () => {
|
|
163
|
+
const ctrl = createController();
|
|
164
|
+
const r = await ctrl.recordDiscovery({ level: '1+', routeDecisionId: 'rd-2' });
|
|
165
|
+
assert.equal(r.defaultChoice, 'SPEC');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('completeTask', () => {
|
|
170
|
+
it('marks task as completed', async () => {
|
|
171
|
+
const ctrl = createController({ state: 'EXECUTING_INLINE' });
|
|
172
|
+
const r = await ctrl.completeTask({
|
|
173
|
+
taskId: 'task-1',
|
|
174
|
+
filePath: 'src/foo.ts',
|
|
175
|
+
fileHash: 'abc123',
|
|
176
|
+
});
|
|
177
|
+
assert.equal(r.status, 'COMPLETED');
|
|
178
|
+
assert.equal(r.totalCompleted, 1);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('rejects completeTask from wrong state', async () => {
|
|
182
|
+
const ctrl = createController();
|
|
183
|
+
const r = await ctrl.completeTask({ taskId: 'task-1' });
|
|
184
|
+
assert.ok(r.error.includes('Cannot complete task from state'));
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -236,6 +236,24 @@ else
|
|
|
236
236
|
fi
|
|
237
237
|
echo ""
|
|
238
238
|
|
|
239
|
+
# ─── 14. Runtime tests (node:test) ──────────────────────────────────────────
|
|
240
|
+
echo "14. Runtime tests"
|
|
241
|
+
if command -v node &>/dev/null; then
|
|
242
|
+
TEST_OUTPUT=$(cd "$ASSETS_DIR" && node --test tests/controller.test.js tests/agent-contract.test.js 2>&1)
|
|
243
|
+
TEST_EXIT=$?
|
|
244
|
+
TEST_PASS=$(echo "$TEST_OUTPUT" | grep "^# pass" | awk '{print $3}')
|
|
245
|
+
TEST_FAIL=$(echo "$TEST_OUTPUT" | grep "^# fail" | awk '{print $3}')
|
|
246
|
+
if [ "$TEST_EXIT" -eq 0 ]; then
|
|
247
|
+
pass "runtime tests: ${TEST_PASS}/${TEST_PASS} passed"
|
|
248
|
+
else
|
|
249
|
+
fail "runtime tests: ${TEST_FAIL} failed out of $((TEST_PASS + TEST_FAIL))"
|
|
250
|
+
echo "$TEST_OUTPUT" | grep "not ok" | head -5
|
|
251
|
+
fi
|
|
252
|
+
else
|
|
253
|
+
warn "node not found — skipping runtime tests"
|
|
254
|
+
fi
|
|
255
|
+
echo ""
|
|
256
|
+
|
|
239
257
|
# ─── Summary ──────────────────────────────────────────────────────────────────
|
|
240
258
|
echo "═══════════════════════════════════════════════════════════"
|
|
241
259
|
echo " Summary"
|
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);
|