ostacky 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/assets/agents/ostacky.md +78 -218
- package/assets/commands/install-stack.md +25 -0
- package/assets/commands/opsx-sync.md +2 -1
- package/assets/mcp/ask-user-server/index.js +271 -0
- package/assets/mcp/ask-user-server/package.json +6 -0
- package/assets/mcp/ostacky-controller/controller.test.mjs +447 -0
- package/assets/mcp/ostacky-controller/index.js +1017 -0
- package/assets/skills/execution-mode-evaluation/SKILL.md +45 -55
- package/assets/skills/subagent-driven-development/SKILL.md +9 -1
- package/assets/skills/writing-plans/SKILL.md +3 -19
- package/dist/cli.js +160 -34
- package/manifest.json +44 -28
- package/package.json +1 -1
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile, readFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import {
|
|
7
|
+
OstackyController,
|
|
8
|
+
buildExecutionSnapshot,
|
|
9
|
+
RESULTS,
|
|
10
|
+
STATES,
|
|
11
|
+
} from "./index.js";
|
|
12
|
+
|
|
13
|
+
describe("OstackyController", () => {
|
|
14
|
+
let stateDir;
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
stateDir = await mkdtemp(join(tmpdir(), "ostacky-controller-"));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
await rm(stateDir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("consumes the route decision once and authorizes the selected route", async () => {
|
|
25
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
26
|
+
const started = await controller.startRequest({ requestId: "req-1" });
|
|
27
|
+
const discovered = await controller.recordDiscovery({
|
|
28
|
+
level: "0+1",
|
|
29
|
+
routeDecisionId: "route-1",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
assert.strictEqual(started.state, STATES.INTERPRETATION_PENDING);
|
|
33
|
+
assert.strictEqual(discovered.state, STATES.ROUTE_DECISION_PENDING);
|
|
34
|
+
|
|
35
|
+
const selected = await controller.consumeRouteDecision({
|
|
36
|
+
decisionId: "route-1",
|
|
37
|
+
choice: "SPEC",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
assert.strictEqual(selected.status, RESULTS.OK);
|
|
41
|
+
assert.strictEqual(selected.state, STATES.SPECIFICATION);
|
|
42
|
+
assert.strictEqual((await controller.authorize("openspec-propose")).status, RESULTS.OK);
|
|
43
|
+
|
|
44
|
+
const repeated = await controller.consumeRouteDecision({
|
|
45
|
+
decisionId: "route-1",
|
|
46
|
+
choice: "DIRECT",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
assert.strictEqual(repeated.status, RESULTS.DECISION_ALREADY_CONSUMED);
|
|
50
|
+
assert.strictEqual(repeated.state, STATES.SPECIFICATION);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("rejects side effects before a valid route decision", async () => {
|
|
54
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
55
|
+
await controller.startRequest({ requestId: "req-2" });
|
|
56
|
+
|
|
57
|
+
const result = await controller.authorize("openspec-propose");
|
|
58
|
+
|
|
59
|
+
assert.strictEqual(result.status, RESULTS.INVALID_TRANSITION);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("validates editable, already-applied, and conflicting edits", async () => {
|
|
63
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
64
|
+
|
|
65
|
+
assert.strictEqual(
|
|
66
|
+
controller.validateEdit({
|
|
67
|
+
oldString: "old",
|
|
68
|
+
newString: "new",
|
|
69
|
+
content: "before old after",
|
|
70
|
+
}).status,
|
|
71
|
+
RESULTS.EDITABLE,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
assert.strictEqual(
|
|
75
|
+
controller.validateEdit({
|
|
76
|
+
oldString: "same",
|
|
77
|
+
newString: "same",
|
|
78
|
+
content: "same",
|
|
79
|
+
}).status,
|
|
80
|
+
RESULTS.ALREADY_APPLIED,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
assert.strictEqual(
|
|
84
|
+
controller.validateEdit({
|
|
85
|
+
oldString: "old",
|
|
86
|
+
newString: "new",
|
|
87
|
+
content: "before new after",
|
|
88
|
+
}).status,
|
|
89
|
+
RESULTS.ALREADY_APPLIED,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
assert.strictEqual(
|
|
93
|
+
controller.validateEdit({
|
|
94
|
+
oldString: "old",
|
|
95
|
+
newString: "new",
|
|
96
|
+
content: "unchanged",
|
|
97
|
+
}).status,
|
|
98
|
+
RESULTS.CONFLICT,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("persists state and restores it in a new controller instance", async () => {
|
|
103
|
+
const statePath = join(stateDir, "state.json");
|
|
104
|
+
const first = new OstackyController({ statePath });
|
|
105
|
+
await first.startRequest({ requestId: "req-3", changeId: "change-3" });
|
|
106
|
+
await first.recordDiscovery({ level: "1+", routeDecisionId: "route-3" });
|
|
107
|
+
await first.consumeRouteDecision({ decisionId: "route-3", choice: "DIRECT" });
|
|
108
|
+
|
|
109
|
+
const restored = new OstackyController({ statePath });
|
|
110
|
+
const state = await restored.getState();
|
|
111
|
+
|
|
112
|
+
assert.strictEqual(state.requestId, "req-3");
|
|
113
|
+
assert.strictEqual(state.changeId, "change-3");
|
|
114
|
+
assert.strictEqual(state.state, STATES.EXECUTION_ANALYSIS);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("resumes active flow without resetting on repeated startRequest", async () => {
|
|
118
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
119
|
+
await controller.startRequest({ requestId: "req-4" });
|
|
120
|
+
await controller.recordDiscovery({ level: "0+1", routeDecisionId: "route-4" });
|
|
121
|
+
await controller.consumeRouteDecision({ decisionId: "route-4", choice: "SPEC" });
|
|
122
|
+
|
|
123
|
+
// Second startRequest on existing flow — should resume, not reset
|
|
124
|
+
const resume = await controller.startRequest({ requestId: "req-5" });
|
|
125
|
+
|
|
126
|
+
assert.strictEqual(resume.state, STATES.SPECIFICATION);
|
|
127
|
+
assert.strictEqual(resume.requestId, "req-4"); // preserved original
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("rejects duplicate route decision consumption with same decisionId", async () => {
|
|
131
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
132
|
+
await controller.startRequest({ requestId: "req-6" });
|
|
133
|
+
await controller.recordDiscovery({ level: "0+1", routeDecisionId: "route-6" });
|
|
134
|
+
|
|
135
|
+
const first = await controller.consumeRouteDecision({ decisionId: "route-6", choice: "SPEC" });
|
|
136
|
+
assert.strictEqual(first.status, RESULTS.OK);
|
|
137
|
+
|
|
138
|
+
const second = await controller.consumeRouteDecision({ decisionId: "route-6", choice: "SPEC" });
|
|
139
|
+
assert.strictEqual(second.status, RESULTS.DECISION_ALREADY_CONSUMED);
|
|
140
|
+
|
|
141
|
+
const third = await controller.consumeRouteDecision({ decisionId: "route-6", choice: "DIRECT" });
|
|
142
|
+
assert.strictEqual(third.status, RESULTS.DECISION_ALREADY_CONSUMED);
|
|
143
|
+
assert.strictEqual(third.state, STATES.SPECIFICATION); // still SPEC
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("rejects consumeRouteDecision with wrong decision ID", async () => {
|
|
147
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
148
|
+
await controller.startRequest({ requestId: "req-7" });
|
|
149
|
+
await controller.recordDiscovery({ level: "0+1", routeDecisionId: "route-7" });
|
|
150
|
+
|
|
151
|
+
const result = await controller.consumeRouteDecision({ decisionId: "wrong-id", choice: "SPEC" });
|
|
152
|
+
assert.strictEqual(result.status, RESULTS.INVALID_TRANSITION);
|
|
153
|
+
assert.match(result.reason, /Decision ID mismatch/);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("supports clarification flow: interpret → clarify → discover", async () => {
|
|
157
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
158
|
+
await controller.startRequest({ requestId: "req-8" });
|
|
159
|
+
|
|
160
|
+
const clarified = await controller.requestClarification({ question: "What area?" });
|
|
161
|
+
assert.strictEqual(clarified.state, STATES.CLARIFICATION_PENDING);
|
|
162
|
+
|
|
163
|
+
const answered = await controller.recordClarification();
|
|
164
|
+
assert.strictEqual(answered.state, STATES.DISCOVERY);
|
|
165
|
+
|
|
166
|
+
const discovered = await controller.recordDiscovery({ level: "1+", routeDecisionId: "route-8" });
|
|
167
|
+
assert.strictEqual(discovered.state, STATES.ROUTE_DECISION_PENDING);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("supports cancel flow via block", async () => {
|
|
171
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
172
|
+
await controller.startRequest({ requestId: "req-9" });
|
|
173
|
+
|
|
174
|
+
const blocked = await controller.block({ reason: "Cancelled by user" });
|
|
175
|
+
assert.strictEqual(blocked.state, STATES.BLOCKED);
|
|
176
|
+
|
|
177
|
+
const state = await controller.getState();
|
|
178
|
+
assert.strictEqual(state.error, "Cancelled by user");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("supports replan from BLOCKED back to INTERPRETATION_PENDING", async () => {
|
|
182
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
183
|
+
await controller.startRequest({ requestId: "req-10", changeId: "change-10" });
|
|
184
|
+
await controller.block({ reason: "Blocked" });
|
|
185
|
+
|
|
186
|
+
const replanned = await controller.replan({ reason: "Retry with new approach" });
|
|
187
|
+
assert.strictEqual(replanned.state, STATES.INTERPRETATION_PENDING);
|
|
188
|
+
|
|
189
|
+
const state = await controller.getState();
|
|
190
|
+
// Replan preserves requestId and changeId but resets workflow
|
|
191
|
+
assert.strictEqual(state.requestId, "req-10");
|
|
192
|
+
assert.strictEqual(state.changeId, "change-10");
|
|
193
|
+
assert.strictEqual(state.routeDecisionId, null);
|
|
194
|
+
assert.strictEqual(state.routeChoice, null);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("records execution analysis and consumes execution decision", async () => {
|
|
198
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
199
|
+
await controller.startRequest({ requestId: "req-11" });
|
|
200
|
+
await controller.recordDiscovery({ level: "0+1", routeDecisionId: "route-11" });
|
|
201
|
+
await controller.consumeRouteDecision({ decisionId: "route-11", choice: "DIRECT" });
|
|
202
|
+
|
|
203
|
+
// Now in EXECUTION_ANALYSIS
|
|
204
|
+
const analysis = await controller.recordExecutionAnalysis({
|
|
205
|
+
executionDecisionId: "exec-11",
|
|
206
|
+
snapshot: {
|
|
207
|
+
recommendation: "INLINE",
|
|
208
|
+
sharedFiles: ["assets/agents/ostacky.md"],
|
|
209
|
+
estimatedLines: 30,
|
|
210
|
+
reasons: ["Single file change, no subagent overhead"],
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
assert.strictEqual(analysis.state, STATES.EXECUTION_DECISION_PENDING);
|
|
214
|
+
assert.strictEqual(analysis.executionDecisionId, "exec-11");
|
|
215
|
+
|
|
216
|
+
const confirmed = await controller.consumeExecutionDecision({
|
|
217
|
+
decisionId: "exec-11",
|
|
218
|
+
mode: "INLINE",
|
|
219
|
+
});
|
|
220
|
+
assert.strictEqual(confirmed.status, RESULTS.OK);
|
|
221
|
+
assert.strictEqual(confirmed.state, STATES.EXECUTING_INLINE);
|
|
222
|
+
assert.strictEqual(confirmed.executionMode, "INLINE");
|
|
223
|
+
assert.ok(confirmed.allowedActions.includes("execution-start"));
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("rejects stale revisions via validateSnapshot", async () => {
|
|
227
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
228
|
+
const before = await controller.getState();
|
|
229
|
+
const revBefore = before.revision;
|
|
230
|
+
|
|
231
|
+
await controller.startRequest({ requestId: "req-12" });
|
|
232
|
+
|
|
233
|
+
const after = await controller.getState();
|
|
234
|
+
const revAfter = after.revision;
|
|
235
|
+
|
|
236
|
+
// Revision changed after mutation
|
|
237
|
+
assert.notStrictEqual(revAfter, revBefore);
|
|
238
|
+
|
|
239
|
+
// Old revision should be stale
|
|
240
|
+
const valid = await controller.validateSnapshot(revBefore);
|
|
241
|
+
assert.strictEqual(valid.valid, false);
|
|
242
|
+
assert.strictEqual(valid.currentRevision, revAfter);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("enforces recommendation → user confirmation → execution mode consistency (cannot diverge)", async () => {
|
|
246
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
247
|
+
await controller.startRequest({ requestId: "req-14" });
|
|
248
|
+
await controller.recordDiscovery({ level: "0+1", routeDecisionId: "route-14" });
|
|
249
|
+
await controller.consumeRouteDecision({ decisionId: "route-14", choice: "DIRECT" });
|
|
250
|
+
|
|
251
|
+
// Record analysis with INLINE recommendation
|
|
252
|
+
await controller.recordExecutionAnalysis({
|
|
253
|
+
executionDecisionId: "exec-14",
|
|
254
|
+
snapshot: buildExecutionSnapshot({
|
|
255
|
+
recommendation: "INLINE",
|
|
256
|
+
filesPerTask: { "1": ["a.ts"] },
|
|
257
|
+
sharedFiles: {},
|
|
258
|
+
fileClusters: [["1"]],
|
|
259
|
+
taskCount: 1,
|
|
260
|
+
clusterCount: 1,
|
|
261
|
+
estLines: 5,
|
|
262
|
+
reasons: ["Single task, no shared files"],
|
|
263
|
+
}),
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// User confirms SUBAGENT_DRIVEN despite INLINE recommendation — user is the authority
|
|
267
|
+
const confirmed = await controller.consumeExecutionDecision({
|
|
268
|
+
decisionId: "exec-14",
|
|
269
|
+
mode: "SUBAGENT_DRIVEN",
|
|
270
|
+
});
|
|
271
|
+
assert.strictEqual(confirmed.status, RESULTS.OK);
|
|
272
|
+
assert.strictEqual(confirmed.state, STATES.EXECUTING_SUBAGENTS);
|
|
273
|
+
assert.strictEqual(confirmed.executionMode, "SUBAGENT_DRIVEN");
|
|
274
|
+
|
|
275
|
+
// Verify persisted mode matches confirmed mode (not recommendation)
|
|
276
|
+
const state = await controller.getState();
|
|
277
|
+
assert.strictEqual(state.executionMode, "SUBAGENT_DRIVEN");
|
|
278
|
+
// The recommendation is still stored in the snapshot
|
|
279
|
+
assert.strictEqual(state.snapshots.execution.recommendation, "INLINE");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("prevents execution start without an execution decision", async () => {
|
|
283
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
284
|
+
await controller.startRequest({ requestId: "req-15" });
|
|
285
|
+
await controller.recordDiscovery({ level: "1+", routeDecisionId: "route-15" });
|
|
286
|
+
await controller.consumeRouteDecision({ decisionId: "route-15", choice: "SPEC" });
|
|
287
|
+
await controller.specComplete();
|
|
288
|
+
// In EXECUTION_ANALYSIS now — no decision consumed yet
|
|
289
|
+
|
|
290
|
+
// Try to authorize execution-start before confirmation
|
|
291
|
+
const auth = await controller.authorize("execution-start");
|
|
292
|
+
assert.strictEqual(auth.status, RESULTS.INVALID_TRANSITION);
|
|
293
|
+
assert.strictEqual(auth.allowed, false);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("gates edit authorization on EDITABLE validation result", async () => {
|
|
297
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
298
|
+
await controller.startRequest({ requestId: "req-16" });
|
|
299
|
+
await controller.recordDiscovery({ level: "0+1", routeDecisionId: "route-16" });
|
|
300
|
+
await controller.consumeRouteDecision({ decisionId: "route-16", choice: "DIRECT" });
|
|
301
|
+
await controller.recordExecutionAnalysis({
|
|
302
|
+
executionDecisionId: "exec-16",
|
|
303
|
+
snapshot: { recommendation: "INLINE", sharedFiles: {}, estLines: 5, reasons: [] },
|
|
304
|
+
});
|
|
305
|
+
await controller.consumeExecutionDecision({ decisionId: "exec-16", mode: "INLINE" });
|
|
306
|
+
|
|
307
|
+
// Authorize with ALREADY_APPLIED — should be rejected
|
|
308
|
+
const badAuth = await controller.authorize("edit", { editResult: RESULTS.ALREADY_APPLIED });
|
|
309
|
+
assert.strictEqual(badAuth.status, RESULTS.ACTION_NOT_AUTHORIZED);
|
|
310
|
+
assert.strictEqual(badAuth.allowed, false);
|
|
311
|
+
|
|
312
|
+
// Authorize with EDITABLE — should pass
|
|
313
|
+
const goodAuth = await controller.authorize("edit", { editResult: RESULTS.EDITABLE });
|
|
314
|
+
assert.strictEqual(goodAuth.status, RESULTS.OK);
|
|
315
|
+
assert.strictEqual(goodAuth.allowed, true);
|
|
316
|
+
|
|
317
|
+
// Authorize without editResult — should be rejected
|
|
318
|
+
const noResultAuth = await controller.authorize("edit");
|
|
319
|
+
assert.strictEqual(noResultAuth.status, RESULTS.ACTION_NOT_AUTHORIZED);
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
// Authorize with CONFLICT — should be rejected
|
|
323
|
+
const conflictAuth = await controller.authorize("edit", { editResult: RESULTS.CONFLICT });
|
|
324
|
+
assert.strictEqual(conflictAuth.status, RESULTS.ACTION_NOT_AUTHORIZED);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("authorizes task-complete and sync only from correct states", async () => {
|
|
328
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
329
|
+
// From INTERPRETATION_PENDING — no ops authorized
|
|
330
|
+
assert.strictEqual((await controller.authorize("task-complete")).status, RESULTS.INVALID_TRANSITION);
|
|
331
|
+
assert.strictEqual((await controller.authorize("sync")).status, RESULTS.INVALID_TRANSITION);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("buildExecutionSnapshot validates recommendation field", () => {
|
|
335
|
+
assert.throws(
|
|
336
|
+
() => buildExecutionSnapshot({ recommendation: "INVALID" }),
|
|
337
|
+
/Invalid recommendation/,
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const valid = buildExecutionSnapshot({ recommendation: "INLINE", estLines: 10 });
|
|
341
|
+
assert.strictEqual(valid.recommendation, "INLINE");
|
|
342
|
+
assert.strictEqual(valid.estLines, 10);
|
|
343
|
+
assert.strictEqual(valid.taskCount, 0); // default
|
|
344
|
+
assert.deepStrictEqual(valid.filesPerTask, {});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("records and retrieves CodeGraph snapshots for phase reuse", async () => {
|
|
348
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
349
|
+
|
|
350
|
+
await controller.recordCodegraphSnapshot({
|
|
351
|
+
context: { symbols: ["AuthService"], files: ["src/auth.ts"] },
|
|
352
|
+
calls: ["codegraph_context"],
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const snapshot = await controller.getCodegraphSnapshot();
|
|
356
|
+
assert.ok(snapshot);
|
|
357
|
+
assert.ok(snapshot.capturedAt);
|
|
358
|
+
assert.deepStrictEqual(snapshot.data.context.symbols, ["AuthService"]);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("detects stale file fingerprints and returns REPLAN_REQUIRED", async () => {
|
|
362
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
363
|
+
// Use the revision after a state transition (startRequest increments revision)
|
|
364
|
+
await controller.startRequest({ requestId: "req-fp" });
|
|
365
|
+
const rev = (await controller.getState()).revision;
|
|
366
|
+
|
|
367
|
+
// Record fingerprints for a file
|
|
368
|
+
await controller.recordFileFingerprints({
|
|
369
|
+
"src/auth.ts": { mtime: 1000, size: 500 },
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Same fingerprints — valid
|
|
373
|
+
const valid = await controller.validateSnapshot(rev, {
|
|
374
|
+
"src/auth.ts": { mtime: 1000, size: 500 },
|
|
375
|
+
});
|
|
376
|
+
assert.strictEqual(valid.valid, true);
|
|
377
|
+
|
|
378
|
+
// Changed mtime — stale
|
|
379
|
+
const stale = await controller.validateSnapshot(rev, {
|
|
380
|
+
"src/auth.ts": { mtime: 2000, size: 500 },
|
|
381
|
+
});
|
|
382
|
+
assert.strictEqual(stale.valid, false);
|
|
383
|
+
assert.strictEqual(stale.status, RESULTS.REPLAN_REQUIRED);
|
|
384
|
+
assert.deepStrictEqual(stale.staleFingerprints, ["src/auth.ts"]);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("records and retrieves Engram keys for boundary persistence", async () => {
|
|
388
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
389
|
+
|
|
390
|
+
await controller.recordEngramKey({
|
|
391
|
+
changeKey: "change/redesign-ostacky-orchestration",
|
|
392
|
+
taskKey: "task/controller-core",
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const keys = await controller.getEngramKeys();
|
|
396
|
+
assert.strictEqual(keys.changeKey, "change/redesign-ostacky-orchestration");
|
|
397
|
+
assert.strictEqual(keys.taskKey, "task/controller-core");
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("increments and retrieves instrumentation counters", async () => {
|
|
401
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
402
|
+
|
|
403
|
+
const val1 = await controller.incrementCounter("codegraph_calls");
|
|
404
|
+
assert.strictEqual(val1, 1);
|
|
405
|
+
|
|
406
|
+
const val2 = await controller.incrementCounter("codegraph_calls");
|
|
407
|
+
assert.strictEqual(val2, 2);
|
|
408
|
+
|
|
409
|
+
const val3 = await controller.incrementCounter("subagent_dispatches", 3);
|
|
410
|
+
assert.strictEqual(val3, 3);
|
|
411
|
+
|
|
412
|
+
const counters = await controller.getCounters();
|
|
413
|
+
assert.strictEqual(counters.codegraph_calls, 2);
|
|
414
|
+
assert.strictEqual(counters.subagent_dispatches, 3);
|
|
415
|
+
|
|
416
|
+
await controller.resetCounters();
|
|
417
|
+
const empty = await controller.getCounters();
|
|
418
|
+
assert.deepStrictEqual(empty, {});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("supports complete task lifecycle: execute, complete task, sync, done", async () => {
|
|
422
|
+
const controller = new OstackyController({ statePath: join(stateDir, "state.json") });
|
|
423
|
+
await controller.startRequest({ requestId: "req-13" });
|
|
424
|
+
await controller.recordDiscovery({ level: "0+1", routeDecisionId: "route-13" });
|
|
425
|
+
await controller.consumeRouteDecision({ decisionId: "route-13", choice: "DIRECT" });
|
|
426
|
+
await controller.recordExecutionAnalysis({
|
|
427
|
+
executionDecisionId: "exec-13",
|
|
428
|
+
snapshot: { recommendation: "INLINE", sharedFiles: [], estimatedLines: 10, reasons: [] },
|
|
429
|
+
});
|
|
430
|
+
await controller.consumeExecutionDecision({ decisionId: "exec-13", mode: "INLINE" });
|
|
431
|
+
|
|
432
|
+
const taskResult = await controller.completeTask({ taskId: "task-1", note: "Implemented controller" });
|
|
433
|
+
assert.strictEqual(taskResult.status, RESULTS.OK);
|
|
434
|
+
assert.ok(taskResult.taskState.completedAt);
|
|
435
|
+
|
|
436
|
+
const tasks = await controller.getTasks();
|
|
437
|
+
assert.ok(tasks["task-1"]);
|
|
438
|
+
assert.strictEqual(tasks["task-1"].note, "Implemented controller");
|
|
439
|
+
|
|
440
|
+
// Complete implementation, sync, mark done
|
|
441
|
+
const implDone = await controller.implementationComplete();
|
|
442
|
+
assert.strictEqual(implDone.state, STATES.SYNC);
|
|
443
|
+
|
|
444
|
+
const syncDone = await controller.syncComplete();
|
|
445
|
+
assert.strictEqual(syncDone.state, STATES.DONE);
|
|
446
|
+
});
|
|
447
|
+
});
|