jorgex-stack 1.9.15 → 1.9.17

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
@@ -185,35 +185,13 @@ Usage:
185
185
 
186
186
  GitHub authentication: requests use `GH_TOKEN`/`GITHUB_TOKEN` from the environment or, if unavailable, the token from your `gh` CLI session (`gh auth token` — local read only, never logged or persisted). Without a token, GitHub limits parallel requests and some upstreams may appear as "offline".
187
187
 
188
- ### OpenCode Goal Mode
188
+ ### OpenCode Goal Mode retirado
189
189
 
190
- The normal work-lifecycle already supports multi-PR plans: `work/{name}/PRD.md` and `plan.md` stay alive across intermediate merges, `work/{name}/pr/{NN}` stores each checkpoint, and `work/{name}/done` is reserved for the final close. Goal Mode is separate: an OpenCode plugin for long-running goals, multiple sessions, multiple slices, multiple worktrees, and, when needed, multiple PRs. It is not meant for short tasks. If the change fits without extended autonomy, do not use `/goal`.
190
+ Goal Mode de OpenCode se ha retirado. Stack ya no instala su plugin ni el comando `/goal`; la continuidad de trabajo usa el lifecycle normal: `work/{name}/PRD.md` y `plan.md` permanecen durante los merges intermedios, `work/{name}/pr/{NN}` conserva cada checkpoint y `work/{name}/done` queda reservado para el cierre final.
191
191
 
192
- It only exists in OpenCode. Claude Code and Codex do not receive it.
192
+ La retirada no migra el historial. `sync` sólo puede retirar archivos gestionados cuando dispone de un manifest legible que registre esas rutas como gestionadas y un inventario completo, con backup previo; conserva los datos existentes en `~/.jorgex-stack/goals` y los plugins ajenos cuando el manifest del Stack está íntegro. El registro local no autentica la propiedad ni corrige una lista `owned` editada o inconsistente: si falta, no se puede parsear o el inventario es incompleto, no se borra el legacy. Ante sospecha sobre el manifest, no ejecutes `sync`; revísalo o restáuralo con backup. `--target-dir` no implica limpiar ese estado.
193
193
 
194
- Available commands:
195
-
196
- - `/goal <goal>` — creates a persistent goal.
197
- - `/goal status` — shows status and next action.
198
- - `/goal plan` — shows the goal's master plan / PRD.
199
- - `/goal history` — lists events and transitions.
200
- - `/goal pause` — pauses the goal.
201
- - `/goal resume` — resumes the goal.
202
- - `/goal merged [commit]` — signals that the pending external PR has been merged.
203
- - `/goal cancel` — cancels the goal.
204
-
205
- What does not exist:
206
-
207
- - `/goal quick`
208
- - `/goal work`
209
-
210
- Operational state:
211
-
212
- - Separate SQLite database by default at `~/.jorgex-stack/goals/goals.sqlite`.
213
- - Optional override with `JORGEX_GOAL_DB`, but always inside `~/.jorgex-stack/goals/`.
214
- - Engram is not the goal's operational store: it remains memory/protocol, not the state database.
215
- - Goal Mode does not perform automatic merges; when it must wait for an external merge, the state becomes `waiting_for_merge`.
216
- - The integration uses experimental OpenCode hooks (`experimental.chat.system.transform` and `experimental.session.compacting`), so that surface may change.
194
+ La continuidad entre checkpoints sigue requiriendo trabajo aprobado, capacidades disponibles y merge humano explícito. PiGoal conserva su propio lifecycle. No se afirma disponibilidad de una alternativa GoalV2 de OpenCode.
217
195
 
218
196
  ## Status
219
197
 
@@ -253,7 +231,7 @@ La integración de este App pertenece al release del repositorio Stack. No conce
253
231
 
254
232
  ## Development
255
233
 
256
- Requirements: Node >= 22.5 and pnpm (never npm). Goal Mode uses `node:sqlite` in tests/Node CLI and OpenCode uses `bun:sqlite` at runtime.
234
+ Requirements: Node >= 22.5 and pnpm (never npm).
257
235
 
258
236
  ```
259
237
  pnpm install
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jorgex-stack",
3
- "version": "1.9.15",
3
+ "version": "1.9.17",
4
4
  "description": "Harness multi-agente portable: instala la config JorgeX (agentes, skills, hooks, Engram, MCPs) en Claude Code, Codex CLI, OpenCode y Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,4 +24,4 @@ Merge only on an explicit user order identifying the PR or intended batch, in a
24
24
 
25
25
  After reporting a ready checkpoint, continue only approved work that is safe and verifiable. Stop when that scope is exhausted, a material decision is missing, or available capabilities cannot execute the next safe work. Keep the work artifacts and report what is ready, what is blocked and why; do not call the overall roadmap complete merely because one PR is ready.
26
26
 
27
- **Current capability limit:** the bundled OpenCode Goal Mode records `waiting_for_merge` for an open PR, and its store/supervisor pause automatic continuation. This policy does not change that state machine. Do not bypass it or promise stacked automatic continuation in that mode; report the limitation and use only already-approved capabilities. Other runtimes also require their actual available Git/PR tools and permissions, not an assumed API.
27
+ **Capability boundary:** each runtime requires its actual available Git/PR tools and permissions, not an assumed API or background supervisor. Report unavailable capabilities instead of bypassing them; this policy alone does not provide unattended execution or guarantee uninterrupted sessions.
@@ -1,8 +1,5 @@
1
1
  ## Role
2
2
 
3
- ### Developer Mode
4
- For code, architecture, bugs, infrastructure, and technical work.
5
-
6
3
  - Senior full-stack developer.
7
4
  - Prefer the simplest solution that works.
8
5
  - Verify before assuming.
@@ -11,18 +8,12 @@ For code, architecture, bugs, infrastructure, and technical work.
11
8
 
12
9
  Agents, skills, and MCPs are available. Use them whenever useful.
13
10
 
14
- ### Assistant Mode
15
- For non-technical work: content, marketing, strategy, branding, copywriting, ideas, and analysis.
16
-
17
- - Be useful, direct, and opinionated.
18
- - No filler.
19
-
20
11
  ---
21
12
 
22
13
  ## Communication Style
23
14
 
24
15
  - Language: Spanish (Spain), natural and direct.
25
- - Keep answers short by default.
16
+ - Be useful, direct, and opinionated. Keep answers short by default; no filler.
26
17
  - No emojis unless explicitly requested.
27
18
  - Do not repeat or paraphrase the user's message.
28
19
  - Point out problems clearly, without sugarcoating or dramatizing.
@@ -31,13 +22,7 @@ For non-technical work: content, marketing, strategy, branding, copywriting, ide
31
22
 
32
23
  ## General Behavior
33
24
 
34
- Be critical and analytical. Don't automatically say that something is "good" or "perfect." Evaluate each proposal by looking for:
35
-
36
- - Problems or limitations
37
- - Better alternatives
38
- - Missing or poorly defined aspects
39
-
40
- If you find errors, point them out directly. If there's a better approach, suggest it. Prioritize accuracy and usefulness over being nice.
25
+ Be critical and analytical; don't automatically agree or praise proposals. Identify errors, limitations and missing or unclear aspects, and suggest better alternatives. Prioritize accuracy and usefulness over being nice.
41
26
  Ask questions when something isn't clear instead of assuming it's correct.
42
27
 
43
28
  - Work efficiently and modularly.
@@ -49,7 +34,6 @@ Ask questions when something isn't clear instead of assuming it's correct.
49
34
  - Make small, local, reviewable changes.
50
35
  - Reuse existing repo patterns before introducing new ones.
51
36
  - Do not add dependencies without explicit user approval.
52
- - Update affected docs when a change needs user or operational explanation, or makes existing claims incorrect; do not create prose for every internal edit.
53
37
  - Run lint and typecheck after significant changes when available.
54
38
 
55
39
  ---
@@ -116,8 +100,7 @@ Every piece of information about a piece of work has exactly ONE home — never
116
100
  - Use TDD for business rules, bugs/regressions, public contracts, and invariants. Do not impose it on styling, wiring, generated code, mechanical refactors, or trivial code unless they change meaningful behavior.
117
101
  - Prefer one authoritative test at the strongest seam closest to the risk. Add coverage at another layer only when it protects a distinct contract, not to repeat the same behavior.
118
102
  - Use the `tdd` skill for red-green-refactor, test-first work, or risk-based behavior testing.
119
- - Prefer targeted verification before broad suites.
120
- - Default order: specific test > partial suite > full suite.
103
+ - Prefer targeted verification: specific test > partial suite > full suite.
121
104
  - Use the real test commands and test stack of the project.
122
105
  - For testing tasks, inspect the complete contract and the actual runner, command, scope, and environment; use existing tooling and neither auto-install nor impose Node, Vitest, pnpm, or another runner.
123
106
  - For CI tasks, act only when the scope requires it: measure comparable samples, use explicit refs, and when scope is uncertain run the relevant lane or fail closed; never cancel a mutable publication.
@@ -164,7 +147,7 @@ Detect the real environment before running commands; don't assume a shell or OS.
164
147
 
165
148
  ## Documentation
166
149
 
167
- - Keep documentation accurate for changed use, contracts and operations. Use the orchestrator's documentation rule to identify the needed surfaces and consolidate the specialist's pass; reopen only affected pages after later changes.
150
+ - Update docs when changed use, contracts or operations need explanation or make existing claims incorrect, not for every internal edit. Use the orchestrator's documentation rule to identify the needed surfaces and consolidate the specialist's pass; reopen only affected pages after later changes.
168
151
  - Respect the separation between public and internal docs when it exists.
169
152
  - Keep content, navigation, and metadata in sync when docs are structured that way.
170
153
  - If docs are missing and needed, create the minimum useful documentation.
@@ -1,7 +0,0 @@
1
- ---
2
- description: Goal Mode — manage persistent long-running objectives
3
- ---
4
-
5
- The Goal Mode plugin should handle `/goal $ARGUMENTS` before this prompt runs.
6
-
7
- If you are seeing this message as a normal assistant prompt, report that Goal Mode did not intercept the slash command. Do not start work, edit files, run tools, create branches, open PRs, or change repository state.
@@ -1,142 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import type { GoalStore } from "./types.js";
4
-
5
- export interface MasterArtifactsInput {
6
- store: GoalStore;
7
- goalId: string;
8
- rootDir: string;
9
- allowedRootDir?: string;
10
- }
11
-
12
- export interface MasterArtifactsResult {
13
- created: boolean;
14
- preserved: boolean;
15
- prdPath: string;
16
- planPath: string;
17
- }
18
-
19
- export function createMasterArtifacts(input: MasterArtifactsInput): MasterArtifactsResult {
20
- const goal = input.store.getGoal(input.goalId);
21
- if (!goal) {
22
- throw new Error(`Goal ${input.goalId} not found.`);
23
- }
24
-
25
- const allowedRootDir = input.allowedRootDir ?? input.rootDir;
26
- assertSafeArtifactPath(input.rootDir, allowedRootDir, "Goal artifact root");
27
- fs.mkdirSync(input.rootDir, { recursive: true });
28
-
29
- const prdPath = path.join(input.rootDir, "PRD.md");
30
- const planPath = path.join(input.rootDir, "plan.md");
31
- assertSafeArtifactPath(prdPath, allowedRootDir, "Goal PRD artifact");
32
- assertSafeArtifactPath(planPath, allowedRootDir, "Goal plan artifact");
33
- const prdCreated = writeIfMissing(prdPath, renderMasterPrd(goal.objective));
34
- const planCreated = writeIfMissing(planPath, renderMasterPlan(goal.objective));
35
-
36
- input.store.recordArtifact(goal.id, { kind: "prd", path: prdPath });
37
- input.store.recordArtifact(goal.id, { kind: "plan", path: planPath });
38
-
39
- return {
40
- created: prdCreated || planCreated,
41
- preserved: !prdCreated || !planCreated,
42
- prdPath,
43
- planPath,
44
- };
45
- }
46
-
47
- export function assertSafeArtifactPath(filePath: string, allowedRootDir: string, label = "Goal artifact"): void {
48
- const resolvedAllowedRoot = resolveExistingPathWithoutSymlinks(allowedRootDir, `${label} root`);
49
- const resolvedPath = resolveExistingPathWithoutSymlinks(filePath, label);
50
- if (!isContainedIn(resolvedPath, resolvedAllowedRoot)) {
51
- throw new Error(`${label} must stay inside ${allowedRootDir}. Refusing: ${filePath}`);
52
- }
53
-
54
- const stats = lstatIfExists(filePath);
55
- if (stats?.isFile() && stats.nlink > 1) {
56
- throw new Error(`${label} must not be a hard link.`);
57
- }
58
- }
59
-
60
- function writeIfMissing(filePath: string, content: string): boolean {
61
- if (fs.existsSync(filePath)) return false;
62
- fs.writeFileSync(filePath, content, "utf8");
63
- return true;
64
- }
65
-
66
- function resolveExistingPathWithoutSymlinks(input: string, label: string): string {
67
- assertNoSymlinkInExistingPath(input, label);
68
- let current = path.resolve(input);
69
- const missing: string[] = [];
70
- while (!fs.existsSync(current)) {
71
- missing.push(path.basename(current));
72
- const parent = path.dirname(current);
73
- if (parent === current) break;
74
- current = parent;
75
- }
76
- const real = fs.existsSync(current) ? fs.realpathSync(current) : current;
77
- return missing.reduceRight((base, part) => path.join(base, part), real);
78
- }
79
-
80
- function assertNoSymlinkInExistingPath(input: string, label: string): void {
81
- const resolved = path.resolve(input);
82
- const root = path.parse(resolved).root;
83
- const relativeParts = path.relative(root, resolved).split(path.sep).filter(Boolean);
84
- let current = root;
85
- for (const part of relativeParts) {
86
- current = path.join(current, part);
87
- const stats = lstatIfExists(current);
88
- if (!stats) return;
89
- if (stats.isSymbolicLink()) {
90
- throw new Error(`${label} must not include symlinks.`);
91
- }
92
- }
93
- }
94
-
95
- function lstatIfExists(input: string): fs.Stats | undefined {
96
- try {
97
- return fs.lstatSync(input);
98
- } catch (error) {
99
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
100
- throw error;
101
- }
102
- }
103
-
104
- function isContainedIn(candidate: string, root: string): boolean {
105
- const relative = path.relative(root, candidate);
106
- return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
107
- }
108
-
109
- function renderMasterPrd(objective: string): string {
110
- return [
111
- "# PRD maestro",
112
- "",
113
- "## Objetivo",
114
- "",
115
- objective,
116
- "",
117
- "## Alcance",
118
- "",
119
- "- Mantener el objetivo global del Goal Mode.",
120
- "- Dividir el trabajo en slices ejecutables por el orquestador.",
121
- "- Esperar merges manuales antes de continuar.",
122
- "",
123
- ].join("\n");
124
- }
125
-
126
- function renderMasterPlan(objective: string): string {
127
- return [
128
- "# Plan maestro",
129
- "",
130
- "## Objetivo",
131
- "",
132
- objective,
133
- "",
134
- "## Fases",
135
- "",
136
- "1. Preparar PRD/plan maestro.",
137
- "2. Ejecutar slices acotados con el orquestador.",
138
- "3. Procesar reviews y esperar merge externo cuando corresponda.",
139
- "4. Verificar criterios globales antes de cerrar.",
140
- "",
141
- ].join("\n");
142
- }
@@ -1,255 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import type { GoalRecord, GoalStatus, GoalStore, NextAction } from "./types.js";
4
- import { assertSafeArtifactPath, createMasterArtifacts } from "./artifacts.js";
5
-
6
- const COMMANDS = new Set(["status", "plan", "history", "pause", "resume", "cancel", "merged"]);
7
- const EXPLICITLY_UNSUPPORTED = new Set(["quick", "work"]);
8
-
9
- export interface GoalCommandHandlersOptions {
10
- store: GoalStore;
11
- project: string;
12
- artifactsRootDir?: string;
13
- }
14
-
15
- export interface GoalCommandResponse {
16
- message: string;
17
- }
18
-
19
- export interface GoalCommandHandlers {
20
- handleGoalCommand(input: string): GoalCommandResponse;
21
- }
22
-
23
- export function createGoalCommandHandlers(options: GoalCommandHandlersOptions): GoalCommandHandlers {
24
- let currentGoalId: string | undefined;
25
-
26
- const currentGoal = () => {
27
- const byId = currentGoalId ? options.store.getGoal(currentGoalId) : undefined;
28
- const active = byId ?? options.store.getCurrentGoal(options.project);
29
- currentGoalId = active?.id ?? currentGoalId;
30
- return active;
31
- };
32
-
33
- const requireCurrentGoal = () => {
34
- const goal = currentGoal();
35
- if (!goal) {
36
- throw new Error("No active goal found. Start one with /goal <objective>.");
37
- }
38
- return goal;
39
- };
40
-
41
- return {
42
- handleGoalCommand(rawInput: string): GoalCommandResponse {
43
- const input = rawInput.trim();
44
- const [firstToken = "", ...args] = input.split(/\s+/);
45
- const firstLower = firstToken.toLowerCase();
46
- const normalized = firstLower === "merged" ? "merged" : input.toLowerCase();
47
-
48
- if (input.length === 0) {
49
- throw new Error("Goal objective cannot be empty.");
50
- }
51
-
52
- if (EXPLICITLY_UNSUPPORTED.has(normalized)) {
53
- throw new Error("/goal quick and /goal work are not supported. Use /goal <objective> for large goals.");
54
- }
55
-
56
- if (!COMMANDS.has(normalized)) {
57
- let artifactRootDir: string | undefined;
58
- const goal = options.store.transaction(() => {
59
- const createdGoal = options.store.createGoal({
60
- objective: input,
61
- project: options.project,
62
- });
63
- artifactRootDir = options.artifactsRootDir
64
- ? path.join(options.artifactsRootDir, createdGoal.id)
65
- : undefined;
66
- try {
67
- if (artifactRootDir) {
68
- createMasterArtifacts({
69
- store: options.store,
70
- goalId: createdGoal.id,
71
- rootDir: artifactRootDir,
72
- allowedRootDir: options.artifactsRootDir,
73
- });
74
- }
75
- options.store.appendEvent(createdGoal.id, {
76
- type: "goal.created",
77
- message: `Goal created: ${createdGoal.objective}`,
78
- data: { objective: createdGoal.objective, project: createdGoal.project },
79
- });
80
- } catch (error) {
81
- let cleanupFailure: string | undefined;
82
- if (artifactRootDir && options.artifactsRootDir) {
83
- cleanupFailure = cleanupBootstrappedArtifactDir(artifactRootDir, options.artifactsRootDir);
84
- }
85
- const rootCause = error instanceof Error ? error.message : String(error);
86
- throw new Error(
87
- `Goal bootstrap failed: ${rootCause}${cleanupFailure ? ` Cleanup also failed: ${cleanupFailure}` : ""}`,
88
- );
89
- }
90
- return createdGoal;
91
- });
92
- currentGoalId = goal.id;
93
-
94
- return {
95
- message: `Goal created: ${goal.objective}\nStatus: ${goal.status}\nNext action: ${formatNextAction(options.store.nextAction(goal.id))}`,
96
- };
97
- }
98
-
99
- if (normalized === "status") return statusResponse(currentGoal(), options.store);
100
- if (normalized === "plan") {
101
- return planResponse(requireCurrentGoal(), options.store, options.artifactsRootDir);
102
- }
103
- if (normalized === "history") return historyResponse(requireCurrentGoal(), options.store);
104
-
105
- const goal = requireCurrentGoal();
106
- if (normalized === "pause") {
107
- return transitionResponse(options.store, goal, "paused", "Goal paused by user.");
108
- }
109
- if (normalized === "resume") {
110
- if (goal.status === "waiting_for_merge") {
111
- throw new Error("Cannot resume while the goal is waiting for an external PR merge.");
112
- }
113
-
114
- const nextStatus = options.store.getOpenPullRequest(goal.id) ? "waiting_for_merge" : "active";
115
- return transitionResponse(options.store, goal, nextStatus, "Goal resumed by user.");
116
- }
117
- if (normalized === "cancel") {
118
- return transitionResponse(options.store, goal, "cancelled", "Goal cancelled by user.");
119
- }
120
- if (normalized === "merged") {
121
- return mergedResponse(options.store, goal, args.join(" "));
122
- }
123
-
124
- throw new Error(`Unsupported /goal command: ${input}`);
125
- },
126
- };
127
- }
128
-
129
- function statusResponse(goal: GoalRecord | undefined, store: GoalStore): GoalCommandResponse {
130
- if (!goal) {
131
- return { message: "No active goal. Start one with /goal <objective>." };
132
- }
133
-
134
- const lines = [
135
- `Goal: ${goal.objective}`,
136
- `Status: ${goal.status}`,
137
- `Next action: ${formatNextAction(store.nextAction(goal.id))}`,
138
- ];
139
- const issue = latestAutoContinueIssue(store, goal.id);
140
- if (issue) lines.push(`Operational issue: ${issue.message}`);
141
-
142
- return {
143
- message: lines.join("\n"),
144
- };
145
- }
146
-
147
- function planResponse(goal: GoalRecord, store: GoalStore, artifactsRootDir: string | undefined): GoalCommandResponse {
148
- const plan = store.getArtifact(goal.id, "plan");
149
- if (plan) {
150
- if (!fs.existsSync(plan.path)) {
151
- throw new Error(`Registered goal plan is not readable at ${plan.path}. Recreate or fix the artifact path.`);
152
- }
153
- if (artifactsRootDir) {
154
- assertSafeArtifactPath(plan.path, artifactsRootDir, "Registered goal plan");
155
- }
156
- return {
157
- message: [`Plan for goal: ${goal.objective}`, fs.readFileSync(plan.path, "utf8")].join("\n\n"),
158
- };
159
- }
160
-
161
- if (artifactsRootDir) {
162
- throw new Error("Goal master plan artifact is not registered. Recreate or repair the goal artifacts.");
163
- }
164
-
165
- return {
166
- message: [
167
- `Plan for goal: ${goal.objective}`,
168
- "Phases: master PRD/plan generation, slice execution, PR review, waiting for merge, final verification.",
169
- "Detailed master artifacts are created by the next Goal Mode slice.",
170
- ].join("\n"),
171
- };
172
- }
173
-
174
- function cleanupBootstrappedArtifactDir(rootDir: string, allowedRootDir: string): string | undefined {
175
- try {
176
- assertSafeArtifactPath(rootDir, allowedRootDir, "Goal artifact cleanup path");
177
- fs.rmSync(rootDir, { recursive: true, force: true });
178
- return undefined;
179
- } catch (error) {
180
- return error instanceof Error ? error.message : String(error);
181
- }
182
- }
183
-
184
- function mergedResponse(store: GoalStore, goal: GoalRecord, mergeCommit: string): GoalCommandResponse {
185
- const pullRequest = store.getOpenPullRequest(goal.id);
186
- if (!pullRequest) {
187
- throw new Error("No open pull request is waiting for merge.");
188
- }
189
-
190
- const merged = store.recordPullRequestMerged(pullRequest.id, {
191
- mergedAt: new Date().toISOString(),
192
- mergeCommit: mergeCommit.trim() || "manual",
193
- });
194
-
195
- return {
196
- message: `Pull request #${merged.number} marked as merged. Goal status: ${store.getGoal(goal.id)?.status ?? "unknown"}`,
197
- };
198
- }
199
-
200
- function historyResponse(goal: GoalRecord, store: GoalStore): GoalCommandResponse {
201
- const events = store.listEvents(goal.id);
202
- const lines = events.length === 0
203
- ? ["No history events recorded yet."]
204
- : events.map((event) => `- ${event.createdAt} ${event.type}: ${event.message}`);
205
-
206
- return {
207
- message: [`History for goal: ${goal.objective}`, ...lines].join("\n"),
208
- };
209
- }
210
-
211
- function transitionResponse(
212
- store: GoalStore,
213
- goal: GoalRecord,
214
- status: GoalStatus,
215
- reason: string,
216
- ): GoalCommandResponse {
217
- const updated = store.transitionGoal(goal.id, status, { reason });
218
- return {
219
- message: `Goal ${updated.status}: ${updated.objective}`,
220
- };
221
- }
222
-
223
- function latestAutoContinueIssue(store: GoalStore, goalId: string) {
224
- const events = store.listEvents(goalId);
225
- const currentStateSequence = Math.max(
226
- 0,
227
- ...events
228
- .filter((event) => !event.type.startsWith("goal.auto_continue_"))
229
- .map((event) => event.sequence),
230
- );
231
- return events
232
- .filter((event) =>
233
- (
234
- event.type === "goal.auto_continue_unavailable" ||
235
- event.type === "goal.auto_continue_failed" ||
236
- event.type === "goal.auto_continue_skipped"
237
- ) &&
238
- readEventStateSequence(event.data) === currentStateSequence,
239
- )
240
- .at(-1);
241
- }
242
-
243
- function readEventStateSequence(data: unknown): number | undefined {
244
- if (typeof data !== "object" || data === null || Array.isArray(data)) return undefined;
245
- const value = (data as { stateSequence?: unknown }).stateSequence;
246
- return typeof value === "number" ? value : undefined;
247
- }
248
-
249
- function formatNextAction(action: NextAction): string {
250
- if (action.type === "wait_for_merge") {
251
- return `waiting for external merge of ${action.pullRequestId}`;
252
- }
253
-
254
- return "continue";
255
- }
@@ -1,68 +0,0 @@
1
- import path from "node:path";
2
- import fs from "node:fs";
3
- import { createRequire } from "node:module";
4
-
5
- type SqliteRow = Record<string, unknown>;
6
-
7
- interface SqliteStatement {
8
- run(...params: unknown[]): unknown;
9
- get(...params: unknown[]): SqliteRow | undefined;
10
- all(...params: unknown[]): SqliteRow[];
11
- }
12
-
13
- interface SqliteDatabase {
14
- exec(sql: string): void;
15
- close(): void;
16
- prepare?(sql: string): SqliteStatement;
17
- query?(sql: string): SqliteStatement;
18
- }
19
-
20
- const require = createRequire(import.meta.url);
21
-
22
- function loadDatabaseCtor(): new (databasePath: string, options?: Record<string, unknown>) => SqliteDatabase {
23
- if (typeof (globalThis as { Bun?: unknown }).Bun !== "undefined") {
24
- return require("bun:sqlite").Database;
25
- }
26
-
27
- return require("node:sqlite").DatabaseSync;
28
- }
29
-
30
- export class GoalDb {
31
- private readonly db: SqliteDatabase;
32
-
33
- constructor(databasePath: string) {
34
- fs.mkdirSync(path.dirname(databasePath), { recursive: true });
35
- const Database = loadDatabaseCtor();
36
- this.db = new Database(databasePath, { create: true });
37
- this.exec("PRAGMA foreign_keys = ON;");
38
- this.exec("PRAGMA busy_timeout = 5000;");
39
- }
40
-
41
- exec(sql: string): void {
42
- this.db.exec(sql);
43
- }
44
-
45
- prepare(sql: string): SqliteStatement {
46
- const statement = this.db.prepare?.(sql) ?? this.db.query?.(sql);
47
- if (!statement) {
48
- throw new Error("SQLite runtime does not expose prepare/query.");
49
- }
50
- return statement;
51
- }
52
-
53
- get(sql: string, ...params: unknown[]): SqliteRow | undefined {
54
- return this.prepare(sql).get(...params);
55
- }
56
-
57
- all(sql: string, ...params: unknown[]): SqliteRow[] {
58
- return this.prepare(sql).all(...params);
59
- }
60
-
61
- run(sql: string, ...params: unknown[]): unknown {
62
- return this.prepare(sql).run(...params);
63
- }
64
-
65
- close(): void {
66
- this.db.close();
67
- }
68
- }