ostacky 0.5.0 → 0.5.2
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
CHANGED
|
@@ -215,33 +215,33 @@ Tras instalar, el proyecto queda así:
|
|
|
215
215
|
|
|
216
216
|
```json
|
|
217
217
|
{
|
|
218
|
-
"version": "0.5.
|
|
218
|
+
"version": "0.5.2",
|
|
219
219
|
"lockedAt": "2025-01-01T00:00:00.000Z",
|
|
220
220
|
"repo": "JaimeHoracio/Ostacky",
|
|
221
|
-
"tag": "v0.5.
|
|
221
|
+
"tag": "v0.5.2",
|
|
222
222
|
"agents": {
|
|
223
223
|
"ostacky": {
|
|
224
|
-
"version": "0.5.
|
|
224
|
+
"version": "0.5.2",
|
|
225
225
|
"installedAt": "2025-01-01T00:00:00.000Z",
|
|
226
226
|
"sha256": "abc123..."
|
|
227
227
|
}
|
|
228
228
|
},
|
|
229
229
|
"commands": {
|
|
230
230
|
"install-stack": {
|
|
231
|
-
"version": "0.5.
|
|
231
|
+
"version": "0.5.2",
|
|
232
232
|
"installedAt": "2025-01-01T00:00:00.000Z",
|
|
233
233
|
"sha256": "def456..."
|
|
234
234
|
},
|
|
235
235
|
"opsx-sync": {
|
|
236
|
-
"version": "0.5.
|
|
236
|
+
"version": "0.5.2",
|
|
237
237
|
"installedAt": "2025-01-01T00:00:00.000Z",
|
|
238
238
|
"sha256": "ghi789..."
|
|
239
239
|
}
|
|
240
240
|
},
|
|
241
241
|
"skills": {
|
|
242
|
-
"brainstorming": { "version": "0.5.
|
|
243
|
-
"execution-mode-evaluation": { "version": "0.5.
|
|
244
|
-
"openspec-propose": { "version": "0.5.
|
|
242
|
+
"brainstorming": { "version": "0.5.2", ... },
|
|
243
|
+
"execution-mode-evaluation": { "version": "0.5.2", ... },
|
|
244
|
+
"openspec-propose": { "version": "0.5.2", ... }
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
```
|
|
@@ -271,7 +271,7 @@ Es opcional y solo necesario si algo falló durante la instalación o si querés
|
|
|
271
271
|
## Seguridad
|
|
272
272
|
|
|
273
273
|
- `opencode.jsonc` se versiona en el repo para compartir permisos y MCP de forma reproducible.
|
|
274
|
-
- Las URLs de descarga usan **tags de GitHub** (ej. `v0.5.
|
|
274
|
+
- Las URLs de descarga usan **tags de GitHub** (ej. `v0.5.2`), nunca `main` — instalaciones reproducibles
|
|
275
275
|
- Cada path de archivo descargado es validado para prevenir **path traversal**
|
|
276
276
|
- Los archivos incluyen **checksum SHA-256** opcional; si el manifest lo define, el contenido se verifica antes de escribir
|
|
277
277
|
- El cache local (`~/.opencode/cache/`) también valida integridad al servir archivos cacheados
|
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.2",
|
|
165
165
|
description: "Instalador interactivo de agentes y comandos para OpenCode",
|
|
166
166
|
type: "module",
|
|
167
167
|
bin: {
|
|
@@ -876,16 +876,16 @@ var L2 = () => {
|
|
|
876
876
|
import { join as join5 } from "path";
|
|
877
877
|
// manifest.json
|
|
878
878
|
var manifest_default = {
|
|
879
|
-
version: "0.5.
|
|
879
|
+
version: "0.5.2",
|
|
880
880
|
repo: "JaimeHoracio/Ostacky",
|
|
881
|
-
tag: "v0.5.
|
|
881
|
+
tag: "v0.5.2",
|
|
882
882
|
agents: [
|
|
883
883
|
{
|
|
884
884
|
name: "ostacky",
|
|
885
885
|
file: "assets/agents/ostacky.md",
|
|
886
886
|
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",
|
|
887
|
-
version: "0.5.
|
|
888
|
-
sha256: "
|
|
887
|
+
version: "0.5.2",
|
|
888
|
+
sha256: "92c533260f1747f660be123d699c227b77b3f36e3c8f33929a6f470b56fef6dc"
|
|
889
889
|
}
|
|
890
890
|
],
|
|
891
891
|
commands: [
|
|
@@ -893,31 +893,31 @@ var manifest_default = {
|
|
|
893
893
|
name: "install-stack",
|
|
894
894
|
file: "assets/commands/install-stack.md",
|
|
895
895
|
description: "Instala el stack tecnológico del proyecto (CodeGraph, skills, OpenSpec, Engram, Context7, controller MCP)",
|
|
896
|
-
version: "0.5.
|
|
896
|
+
version: "0.5.2",
|
|
897
897
|
sha256: "e8b75efe505766d47dd4d38cfb055b6801552cd457d99bce018c196a346e4560"
|
|
898
898
|
},
|
|
899
899
|
{
|
|
900
900
|
name: "opsx-sync",
|
|
901
901
|
file: "assets/commands/opsx-sync.md",
|
|
902
902
|
description: "Sincroniza delta specs del change activo sin inicializar CodeGraph si ya existe índice",
|
|
903
|
-
version: "0.5.
|
|
903
|
+
version: "0.5.2",
|
|
904
904
|
sha256: "ed9948f1910743b672e1dfad496bc96972a224a98b063232be39290d43068c50"
|
|
905
905
|
}
|
|
906
906
|
],
|
|
907
907
|
mcpServers: [
|
|
908
908
|
{
|
|
909
|
-
name: "ask-user",
|
|
909
|
+
name: "ask-user-server",
|
|
910
910
|
file: "assets/mcp/ask-user-server/",
|
|
911
911
|
description: "MCP server con tool ask_user para consultar al usuario con bloqueo de ejecución",
|
|
912
|
-
version: "0.5.
|
|
913
|
-
sha256: "
|
|
912
|
+
version: "0.5.2",
|
|
913
|
+
sha256: "010eb59e1224ccdcb6aa4b5d99b242b6316570a65198f1df6efa375db75a5506"
|
|
914
914
|
},
|
|
915
915
|
{
|
|
916
916
|
name: "ostacky-controller",
|
|
917
917
|
file: "assets/mcp/ostacky-controller/",
|
|
918
918
|
description: "Máquina de estados persistida para Ostacky: validación de transiciones, consumo de decisiones, autorización de side effects, edición segura y snapshots reutilizables",
|
|
919
|
-
version: "0.5.
|
|
920
|
-
sha256: "
|
|
919
|
+
version: "0.5.2",
|
|
920
|
+
sha256: "c66291cf170a7579e611580e8ac6f4bb15857d7e71b43fb793d92ea3a0013951"
|
|
921
921
|
}
|
|
922
922
|
],
|
|
923
923
|
skills: [
|
|
@@ -925,77 +925,77 @@ var manifest_default = {
|
|
|
925
925
|
name: "execution-mode-evaluation",
|
|
926
926
|
file: "assets/skills/execution-mode-evaluation/SKILL.md",
|
|
927
927
|
description: "Skill de análisis de modo de ejecución — output reconciliado con controller snapshot contract (recommendation field)",
|
|
928
|
-
version: "0.5.
|
|
929
|
-
sha256: "
|
|
928
|
+
version: "0.5.2",
|
|
929
|
+
sha256: "1063bc501b284d84daa181198cfb8194beff481594ba9931e6bbd46d89cfdabd"
|
|
930
930
|
},
|
|
931
931
|
{
|
|
932
932
|
name: "brainstorming",
|
|
933
933
|
file: "assets/skills/brainstorming/SKILL.md",
|
|
934
934
|
description: "Skill de descubrimiento y exploración (Superpowers)",
|
|
935
|
-
version: "0.5.
|
|
935
|
+
version: "0.5.2",
|
|
936
936
|
sha256: "ad15458c54912007ae3f708c698a11f0a4a58e2a7c7860a1ae8874db573c9e98"
|
|
937
937
|
},
|
|
938
938
|
{
|
|
939
939
|
name: "writing-plans",
|
|
940
940
|
file: "assets/skills/writing-plans/SKILL.md",
|
|
941
941
|
description: "Skill de planificación de implementación (Superpowers) — execution handoff removido, Ostacky decide modo de ejecución",
|
|
942
|
-
version: "0.5.
|
|
943
|
-
sha256: "
|
|
942
|
+
version: "0.5.2",
|
|
943
|
+
sha256: "ff242e9205666f74dc1b28bf19bd77270d46dcc5c322d643faa49aabf8e8d096"
|
|
944
944
|
},
|
|
945
945
|
{
|
|
946
946
|
name: "tdd",
|
|
947
947
|
file: "assets/skills/tdd/SKILL.md",
|
|
948
948
|
description: "Skill de test-driven development (Superpowers)",
|
|
949
|
-
version: "0.5.
|
|
949
|
+
version: "0.5.2",
|
|
950
950
|
sha256: "7eacd8ee81dc5c0b85065c0392e37ee10314661f7edb59dd8e70a9e0dae8f371"
|
|
951
951
|
},
|
|
952
952
|
{
|
|
953
953
|
name: "subagent-driven-development",
|
|
954
954
|
file: "assets/skills/subagent-driven-development/SKILL.md",
|
|
955
955
|
description: "Skill de ejecución con subagentes (Superpowers) — ejecuta solo después de confirmación del coordinador Ostacky",
|
|
956
|
-
version: "0.5.
|
|
957
|
-
sha256: "
|
|
956
|
+
version: "0.5.2",
|
|
957
|
+
sha256: "a9e6c9070f63dcac0a065a1abd91c9218fa07e201f594d1ce5f6a0dc0bfa0674"
|
|
958
958
|
},
|
|
959
959
|
{
|
|
960
960
|
name: "dispatching-parallel-agents",
|
|
961
961
|
file: "assets/skills/dispatching-parallel-agents/SKILL.md",
|
|
962
962
|
description: "Skill de dispatch paralelo de agentes (Superpowers)",
|
|
963
|
-
version: "0.5.
|
|
963
|
+
version: "0.5.2",
|
|
964
964
|
sha256: "281edf0c38f358497c7e2066fa8217a2ba3e2a39b4205c4af0c41d328fc035a1"
|
|
965
965
|
},
|
|
966
966
|
{
|
|
967
967
|
name: "review",
|
|
968
968
|
file: "assets/skills/review/SKILL.md",
|
|
969
969
|
description: "Skill de revisión de código (Superpowers)",
|
|
970
|
-
version: "0.5.
|
|
970
|
+
version: "0.5.2",
|
|
971
971
|
sha256: "c78d3beff45e9bb6b5e9e1d7155910f379e082d7aaf3c61b90d651d09e0d2e2a"
|
|
972
972
|
},
|
|
973
973
|
{
|
|
974
974
|
name: "openspec-explore",
|
|
975
975
|
file: "assets/skills/openspec-explore/SKILL.md",
|
|
976
976
|
description: "Skill de exploración pre-spec (OpenSpec)",
|
|
977
|
-
version: "0.5.
|
|
977
|
+
version: "0.5.2",
|
|
978
978
|
sha256: "1c75cab8672c2eb8b12f27a0b31f0b7fdb73c21c72583c1e8a566e09fe4108a6"
|
|
979
979
|
},
|
|
980
980
|
{
|
|
981
981
|
name: "openspec-propose",
|
|
982
982
|
file: "assets/skills/openspec-propose/SKILL.md",
|
|
983
983
|
description: "Skill de generación de proposal (OpenSpec)",
|
|
984
|
-
version: "0.5.
|
|
984
|
+
version: "0.5.2",
|
|
985
985
|
sha256: "ab88e9aa22db09559ccf7987d08adc0469b7c6fbe46498e63a3b64926f19e66b"
|
|
986
986
|
},
|
|
987
987
|
{
|
|
988
988
|
name: "openspec-apply-change",
|
|
989
989
|
file: "assets/skills/openspec-apply-change/SKILL.md",
|
|
990
990
|
description: "Skill de aplicación de change (OpenSpec)",
|
|
991
|
-
version: "0.5.
|
|
991
|
+
version: "0.5.2",
|
|
992
992
|
sha256: "847c2f88a66e8d5946c1fec422a5367c89eca5340839b659bbaaeb6f0bdac487"
|
|
993
993
|
},
|
|
994
994
|
{
|
|
995
995
|
name: "openspec-archive-change",
|
|
996
996
|
file: "assets/skills/openspec-archive-change/SKILL.md",
|
|
997
997
|
description: "Skill de archivo de change (OpenSpec)",
|
|
998
|
-
version: "0.5.
|
|
998
|
+
version: "0.5.2",
|
|
999
999
|
sha256: "3d35905ec40a081b8fd77b144426e540c67b1f50cc5801b655313a4fdfaa087a"
|
|
1000
1000
|
}
|
|
1001
1001
|
]
|
|
@@ -1186,7 +1186,7 @@ function clearLockfile(opencodeRoot) {
|
|
|
1186
1186
|
const lockfile = readLockfile(opencodeRoot);
|
|
1187
1187
|
if (!lockfile) {
|
|
1188
1188
|
writeLockfile(opencodeRoot, {
|
|
1189
|
-
version: "0.5.
|
|
1189
|
+
version: "0.5.2",
|
|
1190
1190
|
lockedAt: new Date().toISOString(),
|
|
1191
1191
|
repo: "",
|
|
1192
1192
|
tag: "",
|
package/manifest.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.5.
|
|
2
|
+
"version": "0.5.2",
|
|
3
3
|
"repo": "JaimeHoracio/Ostacky",
|
|
4
|
-
"tag": "v0.5.
|
|
4
|
+
"tag": "v0.5.2",
|
|
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.2",
|
|
11
|
+
"sha256": "92c533260f1747f660be123d699c227b77b3f36e3c8f33929a6f470b56fef6dc"
|
|
12
12
|
}
|
|
13
13
|
],
|
|
14
14
|
"commands": [
|
|
@@ -16,31 +16,31 @@
|
|
|
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.
|
|
19
|
+
"version": "0.5.2",
|
|
20
20
|
"sha256": "e8b75efe505766d47dd4d38cfb055b6801552cd457d99bce018c196a346e4560"
|
|
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.2",
|
|
27
27
|
"sha256": "ed9948f1910743b672e1dfad496bc96972a224a98b063232be39290d43068c50"
|
|
28
28
|
}
|
|
29
29
|
],
|
|
30
30
|
"mcpServers": [
|
|
31
31
|
{
|
|
32
|
-
"name": "ask-user",
|
|
32
|
+
"name": "ask-user-server",
|
|
33
33
|
"file": "assets/mcp/ask-user-server/",
|
|
34
34
|
"description": "MCP server con tool ask_user para consultar al usuario con bloqueo de ejecución",
|
|
35
|
-
"version": "0.5.
|
|
36
|
-
"sha256": "
|
|
35
|
+
"version": "0.5.2",
|
|
36
|
+
"sha256": "010eb59e1224ccdcb6aa4b5d99b242b6316570a65198f1df6efa375db75a5506"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"name": "ostacky-controller",
|
|
40
40
|
"file": "assets/mcp/ostacky-controller/",
|
|
41
41
|
"description": "Máquina de estados persistida para Ostacky: validación de transiciones, consumo de decisiones, autorización de side effects, edición segura y snapshots reutilizables",
|
|
42
|
-
"version": "0.5.
|
|
43
|
-
"sha256": "
|
|
42
|
+
"version": "0.5.2",
|
|
43
|
+
"sha256": "c66291cf170a7579e611580e8ac6f4bb15857d7e71b43fb793d92ea3a0013951"
|
|
44
44
|
}
|
|
45
45
|
],
|
|
46
46
|
"skills": [
|
|
@@ -48,77 +48,77 @@
|
|
|
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.
|
|
52
|
-
"sha256": "
|
|
51
|
+
"version": "0.5.2",
|
|
52
|
+
"sha256": "1063bc501b284d84daa181198cfb8194beff481594ba9931e6bbd46d89cfdabd"
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"name": "brainstorming",
|
|
56
56
|
"file": "assets/skills/brainstorming/SKILL.md",
|
|
57
57
|
"description": "Skill de descubrimiento y exploración (Superpowers)",
|
|
58
|
-
"version": "0.5.
|
|
58
|
+
"version": "0.5.2",
|
|
59
59
|
"sha256": "ad15458c54912007ae3f708c698a11f0a4a58e2a7c7860a1ae8874db573c9e98"
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
"name": "writing-plans",
|
|
63
63
|
"file": "assets/skills/writing-plans/SKILL.md",
|
|
64
64
|
"description": "Skill de planificación de implementación (Superpowers) — execution handoff removido, Ostacky decide modo de ejecución",
|
|
65
|
-
"version": "0.5.
|
|
66
|
-
"sha256": "
|
|
65
|
+
"version": "0.5.2",
|
|
66
|
+
"sha256": "ff242e9205666f74dc1b28bf19bd77270d46dcc5c322d643faa49aabf8e8d096"
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
69
|
"name": "tdd",
|
|
70
70
|
"file": "assets/skills/tdd/SKILL.md",
|
|
71
71
|
"description": "Skill de test-driven development (Superpowers)",
|
|
72
|
-
"version": "0.5.
|
|
72
|
+
"version": "0.5.2",
|
|
73
73
|
"sha256": "7eacd8ee81dc5c0b85065c0392e37ee10314661f7edb59dd8e70a9e0dae8f371"
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
76
|
"name": "subagent-driven-development",
|
|
77
77
|
"file": "assets/skills/subagent-driven-development/SKILL.md",
|
|
78
78
|
"description": "Skill de ejecución con subagentes (Superpowers) — ejecuta solo después de confirmación del coordinador Ostacky",
|
|
79
|
-
"version": "0.5.
|
|
80
|
-
"sha256": "
|
|
79
|
+
"version": "0.5.2",
|
|
80
|
+
"sha256": "a9e6c9070f63dcac0a065a1abd91c9218fa07e201f594d1ce5f6a0dc0bfa0674"
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"name": "dispatching-parallel-agents",
|
|
84
84
|
"file": "assets/skills/dispatching-parallel-agents/SKILL.md",
|
|
85
85
|
"description": "Skill de dispatch paralelo de agentes (Superpowers)",
|
|
86
|
-
"version": "0.5.
|
|
86
|
+
"version": "0.5.2",
|
|
87
87
|
"sha256": "281edf0c38f358497c7e2066fa8217a2ba3e2a39b4205c4af0c41d328fc035a1"
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
"name": "review",
|
|
91
91
|
"file": "assets/skills/review/SKILL.md",
|
|
92
92
|
"description": "Skill de revisión de código (Superpowers)",
|
|
93
|
-
"version": "0.5.
|
|
93
|
+
"version": "0.5.2",
|
|
94
94
|
"sha256": "c78d3beff45e9bb6b5e9e1d7155910f379e082d7aaf3c61b90d651d09e0d2e2a"
|
|
95
95
|
},
|
|
96
96
|
{
|
|
97
97
|
"name": "openspec-explore",
|
|
98
98
|
"file": "assets/skills/openspec-explore/SKILL.md",
|
|
99
99
|
"description": "Skill de exploración pre-spec (OpenSpec)",
|
|
100
|
-
"version": "0.5.
|
|
100
|
+
"version": "0.5.2",
|
|
101
101
|
"sha256": "1c75cab8672c2eb8b12f27a0b31f0b7fdb73c21c72583c1e8a566e09fe4108a6"
|
|
102
102
|
},
|
|
103
103
|
{
|
|
104
104
|
"name": "openspec-propose",
|
|
105
105
|
"file": "assets/skills/openspec-propose/SKILL.md",
|
|
106
106
|
"description": "Skill de generación de proposal (OpenSpec)",
|
|
107
|
-
"version": "0.5.
|
|
107
|
+
"version": "0.5.2",
|
|
108
108
|
"sha256": "ab88e9aa22db09559ccf7987d08adc0469b7c6fbe46498e63a3b64926f19e66b"
|
|
109
109
|
},
|
|
110
110
|
{
|
|
111
111
|
"name": "openspec-apply-change",
|
|
112
112
|
"file": "assets/skills/openspec-apply-change/SKILL.md",
|
|
113
113
|
"description": "Skill de aplicación de change (OpenSpec)",
|
|
114
|
-
"version": "0.5.
|
|
114
|
+
"version": "0.5.2",
|
|
115
115
|
"sha256": "847c2f88a66e8d5946c1fec422a5367c89eca5340839b659bbaaeb6f0bdac487"
|
|
116
116
|
},
|
|
117
117
|
{
|
|
118
118
|
"name": "openspec-archive-change",
|
|
119
119
|
"file": "assets/skills/openspec-archive-change/SKILL.md",
|
|
120
120
|
"description": "Skill de archivo de change (OpenSpec)",
|
|
121
|
-
"version": "0.5.
|
|
121
|
+
"version": "0.5.2",
|
|
122
122
|
"sha256": "3d35905ec40a081b8fd77b144426e540c67b1f50cc5801b655313a4fdfaa087a"
|
|
123
123
|
}
|
|
124
124
|
]
|
package/package.json
CHANGED
|
@@ -1,447 +0,0 @@
|
|
|
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
|
-
});
|