ostacky 0.8.2 → 0.8.4

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.
@@ -16,6 +16,7 @@ import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync, statSyn
16
16
  import { join, dirname, basename, resolve, relative } from "node:path"
17
17
  import { SENSITIVE_DEFAULT, BASH_SENSITIVE_RE, isSensitive, extractPathsFromBash } from "../../src/security.ts"
18
18
  import { isTrivial } from "../../src/tiered.ts"
19
+ import { STATES, TRANSITIONS, DEFAULT_STATE } from "./controller-core.ts"
19
20
 
20
21
  // ─── Constants ───────────────────────────────────────────────────────────────
21
22
 
@@ -25,127 +26,7 @@ const IDLE_THRESHOLD_MS = 45_000
25
26
  const PURPLE_TENUE = "\x1b[38;5;183m"
26
27
  const PURPLE_RESET = "\x1b[0m"
27
28
 
28
- const STATES = Object.freeze({
29
- INTERPRETATION_PENDING: "INTERPRETATION_PENDING",
30
- CLARIFICATION_PENDING: "CLARIFICATION_PENDING",
31
- DISCOVERY: "DISCOVERY",
32
- ROUTE_DECISION_PENDING: "ROUTE_DECISION_PENDING",
33
- SPECIFICATION: "SPECIFICATION",
34
- EXECUTION_ANALYSIS: "EXECUTION_ANALYSIS",
35
- EXECUTION_DECISION_PENDING: "EXECUTION_DECISION_PENDING",
36
- EXECUTING_INLINE: "EXECUTING_INLINE",
37
- EXECUTING_SUBAGENTS: "EXECUTING_SUBAGENTS",
38
- SYNC: "SYNC",
39
- DONE: "DONE",
40
- BLOCKED: "BLOCKED",
41
- } as const)
42
-
43
- const TRANSITIONS: Record<string, Array<{ via: string; to: string; choice?: string; mode?: string }>> = {
44
- INTERPRETATION_PENDING: [
45
- { via: "request_clarification", to: "CLARIFICATION_PENDING" },
46
- { via: "proceed_to_discovery", to: "DISCOVERY" },
47
- { via: "record_discovery", to: "ROUTE_DECISION_PENDING" },
48
- { via: "block", to: "BLOCKED" },
49
- ],
50
- CLARIFICATION_PENDING: [
51
- { via: "record_clarification", to: "DISCOVERY" },
52
- { via: "block", to: "BLOCKED" },
53
- { via: "abandon", to: "BLOCKED" },
54
- ],
55
- DISCOVERY: [
56
- { via: "record_discovery", to: "ROUTE_DECISION_PENDING" },
57
- { via: "block", to: "BLOCKED" },
58
- { via: "abandon", to: "BLOCKED" },
59
- ],
60
- ROUTE_DECISION_PENDING: [
61
- { via: "consume_route_decision", to: "SPECIFICATION", choice: "SPEC" },
62
- { via: "consume_route_decision", to: "EXECUTION_ANALYSIS", choice: "DIRECT" },
63
- { via: "block", to: "BLOCKED" },
64
- { via: "abandon", to: "BLOCKED" },
65
- ],
66
- SPECIFICATION: [
67
- { via: "spec_complete", to: "EXECUTION_ANALYSIS" },
68
- { via: "block", to: "BLOCKED" },
69
- { via: "abandon", to: "BLOCKED" },
70
- ],
71
- EXECUTION_ANALYSIS: [
72
- { via: "record_execution_analysis", to: "EXECUTION_DECISION_PENDING" },
73
- { via: "block", to: "BLOCKED" },
74
- { via: "abandon", to: "BLOCKED" },
75
- ],
76
- EXECUTION_DECISION_PENDING: [
77
- { via: "consume_execution_decision", to: "EXECUTING_INLINE", mode: "INLINE" },
78
- { via: "consume_execution_decision", to: "EXECUTING_SUBAGENTS", mode: "SUBAGENT_DRIVEN" },
79
- { via: "block", to: "BLOCKED" },
80
- { via: "abandon", to: "BLOCKED" },
81
- ],
82
- EXECUTING_INLINE: [
83
- { via: "implementation_complete", to: "SYNC" },
84
- { via: "block", to: "BLOCKED" },
85
- ],
86
- EXECUTING_SUBAGENTS: [
87
- { via: "implementation_complete", to: "SYNC" },
88
- { via: "block", to: "BLOCKED" },
89
- ],
90
- BLOCKED: [
91
- { via: "replan", to: "INTERPRETATION_PENDING" },
92
- { via: "abandon", to: "DONE" },
93
- ],
94
- SYNC: [
95
- { via: "sync_complete", to: "DONE" },
96
- { via: "block", to: "BLOCKED" },
97
- ],
98
- DONE: [],
99
- }
100
-
101
- const DEFAULT_STATE: any = {
102
- state: STATES.INTERPRETATION_PENDING,
103
- revision: 0,
104
- requestId: null,
105
- changeId: null,
106
- routeDecisionId: null,
107
- routeChoice: null,
108
- level: null,
109
- executionDecisionId: null,
110
- executionMode: null,
111
- snapshots: { codegraph: null, execution: null },
112
- tasks: {},
113
- fileFingerprints: {},
114
- error: null,
115
- lastHandoff: null,
116
- expectedTasks: null,
117
- expectedTaskCount: null,
118
- auditSeq: 0,
119
- degraded: false,
120
- schemaVersion: 1,
121
- stateOversizedCount: 0,
122
- codegraphBypassCount: 0,
123
- degradedEditsCount: 0,
124
- cacheHitCount: 0,
125
- cacheMissCount: 0,
126
- tokenSavingEstimate: 0,
127
- discoveryCacheHitCount: 0,
128
- redundantCallCount: 0,
129
- cacheMissWithoutPutCount: 0,
130
- stateCheckCount: 0,
131
- toolCallCount: 0,
132
- lastProposal: null,
133
- allowedFiles: {},
134
- deniedFiles: {},
135
- sensitivePatterns: SENSITIVE_DEFAULT,
136
- sensitiveAccess: { allowed: 0, denied: 0, blockedAttempts: 0 },
137
- staleContentAttempts: 0,
138
- completeWithoutValidateCount: 0,
139
- toolTimeoutCount: 0,
140
- lastToolDurationMs: 0,
141
- stateDurationMs: 0,
142
- subagentFailedCount: 0,
143
- lastValidated: null,
144
- pendingFileAccess: {},
145
- lastHeartbeat: 0,
146
- watchdogEnabled: true,
147
- ts: Date.now(),
148
- }
29
+ // DEFAULT_STATE imported from controller-core.ts
149
30
 
150
31
  // ─── Helpers ─────────────────────────────────────────────────────────────────
151
32
 
@@ -558,13 +439,20 @@ export const OstackyController: Plugin = async (ctx) => {
558
439
  const s = readState((input as any).ctx?.directory ?? "")
559
440
  } catch {}
560
441
  }
561
- // Heartbeat + color purple tenue: Ostacky vs modelo (fácil)
442
+ // Heartbeat + color purple tenue: Ostacky vs modelo (fácil) — single-writer: plugin memo, no persist cada tool (D10)
443
+ let lastHeartbeatMem = 0
562
444
  try {
563
445
  const dir = ctx.directory
564
446
  const s = readState(dir)
565
447
  if (s) {
566
- s.lastHeartbeat = Date.now()
567
- try { persistState(dir, s) } catch {}
448
+ const now = Date.now()
449
+ lastHeartbeatMem = now
450
+ // Single-writer: solo persiste si OSTACKY_PLUGIN_PERSIST=1 o idle>30s o state cambió
451
+ const shouldPersist = process.env.OSTACKY_PLUGIN_PERSIST === "1" || (now - (s.lastHeartbeat || 0) > 30000)
452
+ if (shouldPersist) {
453
+ s.lastHeartbeat = now
454
+ try { persistState(dir, s) } catch {}
455
+ }
568
456
  if (["EXECUTING_INLINE", "EXECUTING_SUBAGENTS", "SYNC"].includes(s.state) && output && typeof output.title === "string" && output.title && !output.title.includes("🟣")) {
569
457
  output.title = `🟣 ${PURPLE_TENUE}[OSTACKY]${PURPLE_RESET} ${output.title}`
570
458
  }
@@ -40,10 +40,12 @@ Do NOT invoke any implementation skill, write any code, or scaffold any project
40
40
  2. **Ask clarifying questions** — one at a time, understand purpose/constraints/success criteria
41
41
  4. **Propose 2-3 approaches (hardening-v2 — SHALL)** — con tabla trade-offs (coste|riesgo|complejidad) + evidencia CodeGraph+Engram sin alucinar, YAGNI, y recomendación con razón; cada approach cita symbols existentes y mem_search hits verificables
42
42
  5. **Present design** — in sections scaled to complexity, get user approval after each section. **Gate post-brainstorming (hardening-v2):** tras presentar diseño, preguntar "¿Procedo con este diseño o querés ajustar algo?" y esperar confirmación explícita antes de `record_discovery`, `openspec-propose` o implementación directa
43
- 6. **Write design doc (output path condicional — router exclusivo):**
44
- - Si trigger + `level 1+` no-downgradeable (`estLines>30` o `fileCount>2` o API pública) y change activo → escribir `openspec/changes/<id>/design.md` sección `## Alternatives Considered` con 2-3 approaches (tabla coste|riesgo|complejidad + evidencia CodeGraph+Engram), no `docs/`. No invocar `openspec-propose` separado este es el diseño.
45
- - Si trigger + `0/0+1` o `1+` downgradeable (`estLines<30`&&`fileCount==1`&&sin API) → solo `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` + gate único `¿Procedo?` DIRECT sin change. SHALL sugerir downgrade: `"Esto parece 0+1 (~X líneas, 1 archivo). ¿Lo tratamos sin spec?"` y esperar.
43
+ 6. **Write design doc (output path condicional — router exclusivo) — SHALL read fresco + edit no write + sync proactiva:**
44
+ - Antes de `write` SHALL `read` fresco de `openspec/changes/<id>/{proposal,design,tasks}` o `docs/superpowers/specs/*.md` si existen; si existen SHALL `edit` (no `write`); `write` solo si no existía. `getDiscoverySnapshot` no aplica a specs/docs. Para `open-explore` sin archivo SHALL `mem_save topic_key:brainstorm/<hash>` por iteración.
45
+ - Si trigger + `level 1+` no-downgradeable (`estLines>30` o `fileCount>2` o API pública) y change activo escribir `openspec/changes/<id>/design.md` sección `## Alternatives Considered` con 2-3 approaches (tabla coste|riesgo|complejidad + evidencia CodeGraph+Engram), no `docs/`. No invocar `openspec-propose` separado este es el diseño. Si ya existe, SHALL `edit` no `write`.
46
+ - Si trigger + `0/0+1` o `1+` downgradeable (`estLines<30`&&`fileCount==1`&&sin API) → solo `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` + gate único `¿Procedo?` → DIRECT sin change. SHALL sugerir downgrade: `"Esto parece 0+1 (~X líneas, 1 archivo). ¿Lo tratamos sin spec?"` y esperar. Si `docsSpec` ya existe SHALL `read` + `edit`.
46
47
  - Gate post-brainstorming es **único**; no hay segundo `¿Procedo con spec?`. `get_audit` `WARN:duplicate_design_generated` si ambos artefactos mismo `requestId`.
48
+ - **Sync proactiva SHALL:** al final de cada turno con decisiones nuevas no reflejadas en disco SHALL listar cálido `Noté que lo que acordamos (X por Z) aún no está en <archivo>: 1) ...` (max 3) y proponer en UNA sola pregunta `¿Querés que agregue [X, Y] a <archivo> e implemente <mejor propuesta> —la recomiendo por <tradeoff>—?` (respetando Regla 5). Para brainstorming SHALL indicar `¿Querés que implementemos lo último que charlamos (B, recomendada por <tradeoff>) o preferís que vaya con A?`
47
49
  7. **Spec self-review** — check for placeholders, contradictions, ambiguity, scope
48
50
  8. **User reviews spec** — ask user to review before proceeding
49
51
  9. **Save to Engram** — `engram_mem_save` with the design decision and tradeoffs