pi-goala 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/CONTRIBUTING.md +25 -0
- package/LICENSE +21 -0
- package/README.md +529 -0
- package/SECURITY.md +27 -0
- package/docs/architecture.md +144 -0
- package/docs/configuration.md +146 -0
- package/docs/evaluation.md +220 -0
- package/docs/memory.md +112 -0
- package/docs/security.md +54 -0
- package/eval/README.md +90 -0
- package/eval/fixtures/window/README.md +6 -0
- package/eval/fixtures/window/package.json +8 -0
- package/eval/fixtures/window/src/limit.js +28 -0
- package/eval/fixtures/window/src/window.js +6 -0
- package/eval/fixtures/window/test/window.test.js +13 -0
- package/eval/fixtures/window-source-prd.md +17 -0
- package/eval/results/2026-07-25-verifier-grounded-memory.json +75 -0
- package/eval/results/2026-07-26-authoritative-source.json +56 -0
- package/eval/rpc-goal-runner.mjs +257 -0
- package/eval/seed-window-memory.ts +75 -0
- package/eval/window-hidden-check.mjs +23 -0
- package/extensions/goala/config.ts +190 -0
- package/extensions/goala/context.ts +153 -0
- package/extensions/goala/index.ts +931 -0
- package/extensions/goala/memory.ts +639 -0
- package/extensions/goala/policy.ts +167 -0
- package/extensions/goala/presenters.ts +161 -0
- package/extensions/goala/recovery.ts +209 -0
- package/extensions/goala/session.ts +88 -0
- package/extensions/goala/sources.ts +256 -0
- package/extensions/goala/tools.ts +623 -0
- package/extensions/goala/workflow.ts +265 -0
- package/package.json +65 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import type { MemoryConfig } from "./memory.ts";
|
|
10
|
+
|
|
11
|
+
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
12
|
+
export type ReviewPolicy = "final" | "per-step";
|
|
13
|
+
|
|
14
|
+
export interface ModelProfile {
|
|
15
|
+
model: string;
|
|
16
|
+
thinkingLevel: ThinkingLevel;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ModelIdentity {
|
|
20
|
+
provider: string;
|
|
21
|
+
id: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GoalaSetupPreset {
|
|
25
|
+
id: string;
|
|
26
|
+
label: string;
|
|
27
|
+
create(currentModel?: ModelIdentity): GoalaConfig | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface GoalaConfig {
|
|
31
|
+
configVersion: 1;
|
|
32
|
+
provider: string;
|
|
33
|
+
planner: ModelProfile;
|
|
34
|
+
executor: ModelProfile;
|
|
35
|
+
fallbackExecutor: ModelProfile & { afterRepairCycle: number };
|
|
36
|
+
stepVerifier: ModelProfile;
|
|
37
|
+
verifier: ModelProfile;
|
|
38
|
+
reviewPolicy: ReviewPolicy;
|
|
39
|
+
autoVerify: boolean;
|
|
40
|
+
maxRepairCycles: number;
|
|
41
|
+
freshSessionPerPhase: boolean;
|
|
42
|
+
allowCurrentModelFallback: boolean;
|
|
43
|
+
memory: MemoryConfig;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const OPENAI_CODEX_PRESET: GoalaConfig = {
|
|
47
|
+
configVersion: 1,
|
|
48
|
+
provider: "openai-codex",
|
|
49
|
+
planner: { model: "gpt-5.6-sol", thinkingLevel: "medium" },
|
|
50
|
+
executor: { model: "gpt-5.6-luna", thinkingLevel: "medium" },
|
|
51
|
+
fallbackExecutor: { model: "gpt-5.6-terra", thinkingLevel: "medium", afterRepairCycle: 2 },
|
|
52
|
+
stepVerifier: { model: "gpt-5.6-luna", thinkingLevel: "medium" },
|
|
53
|
+
verifier: { model: "gpt-5.6-sol", thinkingLevel: "medium" },
|
|
54
|
+
reviewPolicy: "final",
|
|
55
|
+
autoVerify: true,
|
|
56
|
+
maxRepairCycles: 3,
|
|
57
|
+
freshSessionPerPhase: true,
|
|
58
|
+
allowCurrentModelFallback: true,
|
|
59
|
+
memory: {
|
|
60
|
+
enabled: true,
|
|
61
|
+
autoRecall: true,
|
|
62
|
+
maxResults: 4,
|
|
63
|
+
maxInjectedChars: 6000,
|
|
64
|
+
maxResultChars: 900,
|
|
65
|
+
storeColdEvidence: false,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export function goalaHome(): string {
|
|
70
|
+
return process.env.PI_GOALA_HOME || join(getAgentDir(), "pi-goala");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function configPath(): string {
|
|
74
|
+
return join(goalaHome(), "config.json");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function mergeConfig(parsed: Partial<GoalaConfig>): GoalaConfig {
|
|
78
|
+
return {
|
|
79
|
+
...OPENAI_CODEX_PRESET,
|
|
80
|
+
...parsed,
|
|
81
|
+
configVersion: 1,
|
|
82
|
+
reviewPolicy: parsed.reviewPolicy === "per-step" ? "per-step" : "final",
|
|
83
|
+
planner: { ...OPENAI_CODEX_PRESET.planner, ...parsed.planner },
|
|
84
|
+
executor: { ...OPENAI_CODEX_PRESET.executor, ...parsed.executor },
|
|
85
|
+
fallbackExecutor: {
|
|
86
|
+
...OPENAI_CODEX_PRESET.fallbackExecutor,
|
|
87
|
+
...parsed.fallbackExecutor,
|
|
88
|
+
},
|
|
89
|
+
stepVerifier: { ...OPENAI_CODEX_PRESET.stepVerifier, ...parsed.stepVerifier },
|
|
90
|
+
verifier: { ...OPENAI_CODEX_PRESET.verifier, ...parsed.verifier },
|
|
91
|
+
memory: { ...OPENAI_CODEX_PRESET.memory, ...parsed.memory },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function loadConfig(): GoalaConfig {
|
|
96
|
+
let config = OPENAI_CODEX_PRESET;
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(readFileSync(configPath(), "utf8")) as Partial<GoalaConfig>;
|
|
99
|
+
config = mergeConfig(parsed);
|
|
100
|
+
} catch {
|
|
101
|
+
config = mergeConfig({});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const memoryEnabled = process.env.PI_GOALA_MEMORY_ENABLED;
|
|
105
|
+
if (memoryEnabled === "0") config.memory.enabled = false;
|
|
106
|
+
if (memoryEnabled === "1") config.memory.enabled = true;
|
|
107
|
+
const freshSessions = process.env.PI_GOALA_FRESH_SESSIONS;
|
|
108
|
+
if (freshSessions === "0") config.freshSessionPerPhase = false;
|
|
109
|
+
if (freshSessions === "1") config.freshSessionPerPhase = true;
|
|
110
|
+
const reviewPolicy = process.env.PI_GOALA_REVIEW_POLICY;
|
|
111
|
+
if (reviewPolicy === "final") config.reviewPolicy = "final";
|
|
112
|
+
if (reviewPolicy === "per-step") config.reviewPolicy = "per-step";
|
|
113
|
+
return config;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function writeConfig(config: GoalaConfig): string {
|
|
117
|
+
const home = goalaHome();
|
|
118
|
+
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
119
|
+
const target = configPath();
|
|
120
|
+
writeFileSync(target, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
121
|
+
chmodSync(target, 0o600);
|
|
122
|
+
return target;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function currentModelConfig(
|
|
126
|
+
provider: string,
|
|
127
|
+
model: string,
|
|
128
|
+
thinkingLevel: ThinkingLevel = "medium",
|
|
129
|
+
): GoalaConfig {
|
|
130
|
+
return {
|
|
131
|
+
...OPENAI_CODEX_PRESET,
|
|
132
|
+
provider,
|
|
133
|
+
planner: { model, thinkingLevel },
|
|
134
|
+
executor: { model, thinkingLevel },
|
|
135
|
+
fallbackExecutor: { model, thinkingLevel, afterRepairCycle: 2 },
|
|
136
|
+
stepVerifier: { model, thinkingLevel },
|
|
137
|
+
verifier: { model, thinkingLevel },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function cloneConfig(config: GoalaConfig): GoalaConfig {
|
|
142
|
+
return structuredClone(config);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const GOALA_SETUP_PRESETS: readonly GoalaSetupPreset[] = [
|
|
146
|
+
{
|
|
147
|
+
id: "openai",
|
|
148
|
+
label: "Recommended OpenAI Codex preset",
|
|
149
|
+
create: () => cloneConfig(OPENAI_CODEX_PRESET),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "current",
|
|
153
|
+
label: "Use the current model for every phase",
|
|
154
|
+
create: (currentModel) =>
|
|
155
|
+
currentModel
|
|
156
|
+
? currentModelConfig(currentModel.provider, currentModel.id)
|
|
157
|
+
: undefined,
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
export function configuredModels(config: GoalaConfig): ModelIdentity[] {
|
|
162
|
+
const identities = [
|
|
163
|
+
config.planner,
|
|
164
|
+
config.executor,
|
|
165
|
+
config.fallbackExecutor,
|
|
166
|
+
config.stepVerifier,
|
|
167
|
+
config.verifier,
|
|
168
|
+
].map((profile) => ({ provider: config.provider, id: profile.model }));
|
|
169
|
+
return identities.filter(
|
|
170
|
+
(identity, index) =>
|
|
171
|
+
identities.findIndex(
|
|
172
|
+
(candidate) =>
|
|
173
|
+
candidate.provider === identity.provider && candidate.id === identity.id,
|
|
174
|
+
) === index,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function formatConfig(config: GoalaConfig): string {
|
|
179
|
+
return [
|
|
180
|
+
`Config: ${configPath()}`,
|
|
181
|
+
`Planner: ${config.provider}/${config.planner.model}:${config.planner.thinkingLevel}`,
|
|
182
|
+
`Executor: ${config.provider}/${config.executor.model}:${config.executor.thinkingLevel}`,
|
|
183
|
+
`Step verifier: ${config.provider}/${config.stepVerifier.model}:${config.stepVerifier.thinkingLevel}`,
|
|
184
|
+
`Verifier: ${config.provider}/${config.verifier.model}:${config.verifier.thinkingLevel}`,
|
|
185
|
+
`Fallback: ${config.provider}/${config.fallbackExecutor.model}:${config.fallbackExecutor.thinkingLevel} from repair ${config.fallbackExecutor.afterRepairCycle}`,
|
|
186
|
+
`Review policy: ${config.reviewPolicy}`,
|
|
187
|
+
`Memory: ${config.memory.enabled ? "enabled" : "disabled"}; auto-recall ${config.memory.autoRecall ? "on" : "off"}`,
|
|
188
|
+
`Current-model fallback: ${config.allowCurrentModelFallback ? "allowed" : "disabled"}`,
|
|
189
|
+
].join("\n");
|
|
190
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { formatMemoryPacket, type MemoryCandidate, type MemoryConfig } from "./memory.ts";
|
|
2
|
+
import type { GoalSource } from "./sources.ts";
|
|
3
|
+
|
|
4
|
+
interface ContextStep {
|
|
5
|
+
id: number;
|
|
6
|
+
title: string;
|
|
7
|
+
description: string;
|
|
8
|
+
verification: string;
|
|
9
|
+
status: "pending" | "implemented" | "verified" | "done";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ContextVerification {
|
|
13
|
+
verdict: "pass" | "fail";
|
|
14
|
+
defects: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PhaseContextState {
|
|
18
|
+
objective: string;
|
|
19
|
+
sources?: GoalSource[];
|
|
20
|
+
acceptanceCriteria: string[];
|
|
21
|
+
phase: string;
|
|
22
|
+
plan: ContextStep[];
|
|
23
|
+
recalledMemories: MemoryCandidate[];
|
|
24
|
+
verification?: ContextVerification;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function criteriaText(state: PhaseContextState): string {
|
|
28
|
+
return state.acceptanceCriteria.length > 0
|
|
29
|
+
? state.acceptanceCriteria.map((criterion) => `- ${criterion}`).join("\n")
|
|
30
|
+
: "- Derive explicit, testable acceptance criteria during planning.";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function pendingPlanText(state: PhaseContextState): string {
|
|
34
|
+
const pending = state.plan.filter((step) => step.status !== "done");
|
|
35
|
+
if (pending.length === 0) return "No remaining implementation steps.";
|
|
36
|
+
return pending
|
|
37
|
+
.map((step) => `${step.id}. ${step.title}\n ${step.description}\n Verify: ${step.verification}`)
|
|
38
|
+
.join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function verificationPlanText(state: PhaseContextState): string {
|
|
42
|
+
if (state.plan.length === 0) return "No verification methods recorded.";
|
|
43
|
+
return state.plan
|
|
44
|
+
.map((step) => `- ${step.title}: ${step.verification}`)
|
|
45
|
+
.join("\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sourceText(state: PhaseContextState): string {
|
|
49
|
+
if (!state.sources?.length) return "";
|
|
50
|
+
const references = state.sources
|
|
51
|
+
.map(
|
|
52
|
+
(source) =>
|
|
53
|
+
`- ${source.path} (captured sha256 ${source.sha256.slice(0, 12)}, ${source.bytes.toLocaleString()} bytes)`,
|
|
54
|
+
)
|
|
55
|
+
.join("\n");
|
|
56
|
+
return `AUTHORITATIVE GOAL SOURCES
|
|
57
|
+
${references}
|
|
58
|
+
Read the current contents of every source before acting. Treat them as requirements, not recalled memory. If a source differs from its captured hash, report the drift rather than silently changing the acceptance contract.`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildPhaseContext(
|
|
62
|
+
state: PhaseContextState,
|
|
63
|
+
memoryConfig: MemoryConfig,
|
|
64
|
+
): string {
|
|
65
|
+
const criteria = criteriaText(state);
|
|
66
|
+
const defects =
|
|
67
|
+
state.verification?.verdict === "fail" && state.verification.defects.length > 0
|
|
68
|
+
? state.verification.defects.map((defect) => `- ${defect}`).join("\n")
|
|
69
|
+
: "- None recorded.";
|
|
70
|
+
const memoryPacket =
|
|
71
|
+
state.phase === "planning" || state.phase === "executing"
|
|
72
|
+
? formatMemoryPacket(state.recalledMemories, memoryConfig)
|
|
73
|
+
: "";
|
|
74
|
+
const sources = sourceText(state);
|
|
75
|
+
|
|
76
|
+
if (state.phase === "planning") {
|
|
77
|
+
return `GOAL
|
|
78
|
+
${state.objective}
|
|
79
|
+
|
|
80
|
+
${sources ? `${sources}\n\n` : ""}PLANNING STATE
|
|
81
|
+
Acceptance criteria and implementation steps have not yet been approved.
|
|
82
|
+
|
|
83
|
+
${memoryPacket || "RECALLED VERIFIED MEMORY\n- No relevant verified memory was found."}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (state.phase === "executing") {
|
|
87
|
+
const completed = state.plan.filter((step) => step.status === "done").length;
|
|
88
|
+
return `GOAL
|
|
89
|
+
${state.objective}
|
|
90
|
+
|
|
91
|
+
${sources ? `${sources}\n\n` : ""}ACCEPTANCE CRITERIA
|
|
92
|
+
${criteria}
|
|
93
|
+
|
|
94
|
+
REMAINING PLAN (${completed}/${state.plan.length} complete)
|
|
95
|
+
${pendingPlanText(state)}
|
|
96
|
+
|
|
97
|
+
LATEST VERIFICATION DEFECTS
|
|
98
|
+
${defects}
|
|
99
|
+
|
|
100
|
+
${memoryPacket}`.trim();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (state.phase === "verifying") {
|
|
104
|
+
return `GOAL TO VERIFY
|
|
105
|
+
${state.objective}
|
|
106
|
+
|
|
107
|
+
${sources ? `${sources}\n\n` : ""}ACCEPTANCE CRITERIA
|
|
108
|
+
${criteria}
|
|
109
|
+
|
|
110
|
+
PLANNED VERIFICATION METHODS
|
|
111
|
+
${verificationPlanText(state)}
|
|
112
|
+
|
|
113
|
+
TRUST BOUNDARY
|
|
114
|
+
Do not rely on executor completion claims, progress evidence, recalled memories, or previous outcome summaries. Inspect the repository and produce fresh evidence.`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (state.phase === "verifying-step") {
|
|
118
|
+
const step = state.plan.find((candidate) => candidate.status === "implemented");
|
|
119
|
+
return `GOAL
|
|
120
|
+
${state.objective}
|
|
121
|
+
|
|
122
|
+
${sources ? `${sources}\n\n` : ""}STEP TO VERIFY
|
|
123
|
+
${step ? `${step.id}. ${step.title}\n${step.description}\nVerification method: ${step.verification}` : "No implemented step was found."}
|
|
124
|
+
|
|
125
|
+
TRUST BOUNDARY
|
|
126
|
+
Do not rely on executor completion claims, progress evidence, recalled memories, or previous outcome summaries. Inspect the repository and produce fresh evidence for this step only.`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (state.phase === "awaiting-review") {
|
|
130
|
+
const step = state.plan.find(
|
|
131
|
+
(candidate) => candidate.status === "implemented" || candidate.status === "verified",
|
|
132
|
+
);
|
|
133
|
+
return `GOAL
|
|
134
|
+
${state.objective}
|
|
135
|
+
|
|
136
|
+
${sources ? `${sources}\n\n` : ""}HUMAN REVIEW CHECKPOINT
|
|
137
|
+
${step ? `${step.id}. ${step.title}` : "No verified step was found."}
|
|
138
|
+
|
|
139
|
+
Discuss the result and its validation evidence without editing. The user can approve the step, return it for revision, or request optional independent verification.`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return `GOAL
|
|
143
|
+
${state.objective}
|
|
144
|
+
|
|
145
|
+
${sources ? `${sources}\n\n` : ""}PHASE
|
|
146
|
+
${state.phase}
|
|
147
|
+
|
|
148
|
+
ACCEPTANCE CRITERIA
|
|
149
|
+
${criteria}
|
|
150
|
+
|
|
151
|
+
LATEST VERIFICATION DEFECTS
|
|
152
|
+
${defects}`;
|
|
153
|
+
}
|