jorgex-stack 1.9.15 → 1.9.16
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 +5 -27
- package/package.json +1 -1
- package/stack/skills/work-lifecycle/references/pr-continuation.md +1 -1
- package/stack/commands/opencode/goal.md +0 -7
- package/stack/plugins/opencode/goal/artifacts.ts +0 -142
- package/stack/plugins/opencode/goal/command.ts +0 -255
- package/stack/plugins/opencode/goal/db.ts +0 -68
- package/stack/plugins/opencode/goal/opencode-hooks.ts +0 -311
- package/stack/plugins/opencode/goal/state.ts +0 -85
- package/stack/plugins/opencode/goal/store.ts +0 -906
- package/stack/plugins/opencode/goal/supervisor.ts +0 -269
- package/stack/plugins/opencode/goal/types.ts +0 -187
- package/stack/plugins/opencode/goal-plugin.ts +0 -184
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
|
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.
|
|
3
|
+
"version": "1.9.16",
|
|
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
|
-
**
|
|
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,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
|
-
}
|