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.
@@ -0,0 +1,265 @@
1
+ import type { ReviewPolicy } from "./config.ts";
2
+ import {
3
+ newGoalId,
4
+ type MemoryCandidate,
5
+ } from "./memory.ts";
6
+ import {
7
+ MAX_GOAL_SOURCE_BYTES,
8
+ MAX_GOAL_SOURCES,
9
+ type GoalSource,
10
+ } from "./sources.ts";
11
+
12
+ export const GOAL_STATE_ENTRY = "goala-state";
13
+
14
+ export const PHASES = [
15
+ "idle",
16
+ "planning",
17
+ "awaiting-execution",
18
+ "executing",
19
+ "verifying-step",
20
+ "awaiting-review",
21
+ "verifying",
22
+ "paused",
23
+ "needs-attention",
24
+ "complete",
25
+ ] as const;
26
+ export type Phase = (typeof PHASES)[number];
27
+
28
+ export const STEP_STATUSES = ["pending", "implemented", "verified", "done"] as const;
29
+ export type StepStatus = (typeof STEP_STATUSES)[number];
30
+ export type CheckStatus = "pass" | "fail" | "not_run";
31
+
32
+ export interface VerificationCheck {
33
+ name: string;
34
+ status: CheckStatus;
35
+ evidence: string;
36
+ }
37
+
38
+ export interface VerificationResult {
39
+ verdict: "pass" | "fail";
40
+ summary: string;
41
+ checks: VerificationCheck[];
42
+ defects: string[];
43
+ at: string;
44
+ }
45
+
46
+ export interface PlanStep {
47
+ id: number;
48
+ title: string;
49
+ description: string;
50
+ verification: string;
51
+ status: StepStatus;
52
+ evidence?: string;
53
+ review?: VerificationResult;
54
+ }
55
+
56
+ export interface GoalState {
57
+ version: 1 | 2 | 3 | 4;
58
+ goalId: string;
59
+ objective: string;
60
+ sources: GoalSource[];
61
+ acceptanceCriteria: string[];
62
+ risks: string[];
63
+ phase: Phase;
64
+ plan: PlanStep[];
65
+ reviewPolicy: ReviewPolicy;
66
+ repairCycles: number;
67
+ stepRepairCycles: number;
68
+ friction: string[];
69
+ openItems: string[];
70
+ recalledMemories: MemoryCandidate[];
71
+ sessionFiles: string[];
72
+ startCommit?: string;
73
+ verification?: VerificationResult;
74
+ reviewFeedback?: string;
75
+ pausedFrom?: Phase;
76
+ blockedReason?: string;
77
+ startedAt?: string;
78
+ updatedAt: string;
79
+ }
80
+
81
+ function isRecord(value: unknown): value is Record<string, unknown> {
82
+ return typeof value === "object" && value !== null && !Array.isArray(value);
83
+ }
84
+
85
+ function stringArray(value: unknown): string[] {
86
+ return Array.isArray(value)
87
+ ? value.filter((item): item is string => typeof item === "string")
88
+ : [];
89
+ }
90
+
91
+ function optionalString(value: unknown): string | undefined {
92
+ return typeof value === "string" ? value : undefined;
93
+ }
94
+
95
+ function nonNegativeInteger(value: unknown): number {
96
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
97
+ }
98
+
99
+ function normalizeVerification(value: unknown): VerificationResult | undefined {
100
+ if (!isRecord(value)) return undefined;
101
+ if (value.verdict !== "pass" && value.verdict !== "fail") return undefined;
102
+ const checks = Array.isArray(value.checks)
103
+ ? value.checks.flatMap((check) => {
104
+ if (!isRecord(check)) return [];
105
+ if (
106
+ typeof check.name !== "string" ||
107
+ typeof check.evidence !== "string" ||
108
+ (check.status !== "pass" && check.status !== "fail" && check.status !== "not_run")
109
+ ) return [];
110
+ const status: CheckStatus = check.status;
111
+ return [{
112
+ name: check.name,
113
+ status,
114
+ evidence: check.evidence,
115
+ }];
116
+ })
117
+ : [];
118
+ return {
119
+ verdict: value.verdict,
120
+ summary: optionalString(value.summary) ?? "",
121
+ checks,
122
+ defects: stringArray(value.defects),
123
+ at: optionalString(value.at) ?? new Date(0).toISOString(),
124
+ };
125
+ }
126
+
127
+ function normalizePlan(value: unknown): PlanStep[] {
128
+ if (!Array.isArray(value)) return [];
129
+ return value.flatMap((item, index) => {
130
+ if (!isRecord(item)) return [];
131
+ if (
132
+ typeof item.title !== "string" ||
133
+ typeof item.description !== "string" ||
134
+ typeof item.verification !== "string"
135
+ ) return [];
136
+ const status = STEP_STATUSES.includes(item.status as StepStatus)
137
+ ? item.status as StepStatus
138
+ : "pending";
139
+ return [{
140
+ id:
141
+ typeof item.id === "number" && Number.isInteger(item.id) && item.id > 0
142
+ ? item.id
143
+ : index + 1,
144
+ title: item.title,
145
+ description: item.description,
146
+ verification: item.verification,
147
+ status,
148
+ evidence: optionalString(item.evidence),
149
+ review: normalizeVerification(item.review),
150
+ }];
151
+ });
152
+ }
153
+
154
+ function normalizeSources(value: unknown): GoalSource[] {
155
+ if (!Array.isArray(value)) return [];
156
+ return value.slice(0, MAX_GOAL_SOURCES).flatMap((source) => {
157
+ if (
158
+ !isRecord(source) ||
159
+ typeof source.path !== "string" ||
160
+ source.path.length === 0 ||
161
+ source.path.length > 1000 ||
162
+ source.path.startsWith("/") ||
163
+ /^[A-Za-z]:[\\/]/.test(source.path) ||
164
+ source.path.split(/[\\/]/).includes("..") ||
165
+ typeof source.sha256 !== "string" ||
166
+ !/^[a-f0-9]{64}$/.test(source.sha256) ||
167
+ typeof source.bytes !== "number" ||
168
+ !Number.isInteger(source.bytes) ||
169
+ source.bytes < 0 ||
170
+ source.bytes > MAX_GOAL_SOURCE_BYTES
171
+ ) return [];
172
+ return [{
173
+ path: source.path,
174
+ sha256: source.sha256,
175
+ bytes: source.bytes,
176
+ }];
177
+ });
178
+ }
179
+
180
+ export function emptyState(reviewPolicy: ReviewPolicy = "final"): GoalState {
181
+ return {
182
+ version: 4,
183
+ goalId: "",
184
+ objective: "",
185
+ sources: [],
186
+ acceptanceCriteria: [],
187
+ risks: [],
188
+ phase: "idle",
189
+ plan: [],
190
+ reviewPolicy,
191
+ repairCycles: 0,
192
+ stepRepairCycles: 0,
193
+ friction: [],
194
+ openItems: [],
195
+ recalledMemories: [],
196
+ sessionFiles: [],
197
+ updatedAt: new Date().toISOString(),
198
+ };
199
+ }
200
+
201
+ export function normalizeState(value: unknown): GoalState {
202
+ if (!isRecord(value) || ![1, 2, 3, 4].includes(Number(value.version))) return emptyState();
203
+ const phase = PHASES.includes(value.phase as Phase) ? value.phase as Phase : "idle";
204
+ const pausedFrom = PHASES.includes(value.pausedFrom as Phase)
205
+ ? value.pausedFrom as Phase
206
+ : undefined;
207
+ return {
208
+ ...emptyState(),
209
+ version: 4,
210
+ goalId: optionalString(value.goalId) || newGoalId(),
211
+ objective: optionalString(value.objective) ?? "",
212
+ sources: normalizeSources(value.sources),
213
+ acceptanceCriteria: stringArray(value.acceptanceCriteria),
214
+ risks: stringArray(value.risks),
215
+ phase,
216
+ plan: normalizePlan(value.plan),
217
+ reviewPolicy: value.reviewPolicy === "per-step" ? "per-step" : "final",
218
+ repairCycles: nonNegativeInteger(value.repairCycles),
219
+ stepRepairCycles: nonNegativeInteger(value.stepRepairCycles),
220
+ friction: stringArray(value.friction),
221
+ openItems: stringArray(value.openItems),
222
+ recalledMemories: Array.isArray(value.recalledMemories)
223
+ ? value.recalledMemories as MemoryCandidate[]
224
+ : [],
225
+ sessionFiles: stringArray(value.sessionFiles),
226
+ startCommit: optionalString(value.startCommit),
227
+ verification: normalizeVerification(value.verification),
228
+ reviewFeedback: optionalString(value.reviewFeedback),
229
+ pausedFrom,
230
+ blockedReason: optionalString(value.blockedReason),
231
+ startedAt: optionalString(value.startedAt),
232
+ updatedAt: optionalString(value.updatedAt) ?? new Date().toISOString(),
233
+ };
234
+ }
235
+
236
+ export function stepSymbol(status: StepStatus): string {
237
+ switch (status) {
238
+ case "done":
239
+ return "✓";
240
+ case "verified":
241
+ return "◆";
242
+ case "implemented":
243
+ return "◐";
244
+ default:
245
+ return "○";
246
+ }
247
+ }
248
+
249
+ export function verificationValidationError(
250
+ verdict: "pass" | "fail",
251
+ summary: string,
252
+ checks: VerificationCheck[],
253
+ defects: string[],
254
+ ): string | undefined {
255
+ if (!summary.trim()) return "Verification rejected: a non-empty summary is required.";
256
+ if (checks.some((check) => !check.name.trim() || !check.evidence.trim())) {
257
+ return "Verification rejected: every check requires a name and concrete evidence.";
258
+ }
259
+ if (verdict === "pass" && (checks.some((check) => check.status !== "pass") || defects.length > 0)) {
260
+ return "PASS rejected: every check must pass and the defects list must be empty.";
261
+ }
262
+ if (verdict === "fail" && (defects.length === 0 || defects.some((defect) => !defect.trim()))) {
263
+ return "FAIL rejected: provide at least one actionable defect.";
264
+ }
265
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "pi-goala",
3
+ "version": "0.2.0",
4
+ "description": "Goal-oriented agent lifecycle, evidence, and verified memory for Pi.",
5
+ "type": "module",
6
+ "author": "Barry King",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/barryking/pi-goala.git"
11
+ },
12
+ "homepage": "https://github.com/barryking/pi-goala#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/barryking/pi-goala/issues"
15
+ },
16
+ "keywords": [
17
+ "pi-package",
18
+ "pi-extension",
19
+ "coding-agent",
20
+ "plan-execute-verify",
21
+ "agent-memory",
22
+ "coala"
23
+ ],
24
+ "engines": {
25
+ "node": ">=22.19.0"
26
+ },
27
+ "files": [
28
+ "extensions",
29
+ "docs",
30
+ "eval",
31
+ "README.md",
32
+ "CHANGELOG.md",
33
+ "CONTRIBUTING.md",
34
+ "LICENSE",
35
+ "SECURITY.md"
36
+ ],
37
+ "pi": {
38
+ "extensions": [
39
+ "./extensions/goala/index.ts"
40
+ ]
41
+ },
42
+ "peerDependencies": {
43
+ "@earendil-works/pi-coding-agent": ">=0.82.1",
44
+ "@earendil-works/pi-tui": ">=0.82.1",
45
+ "typebox": "*"
46
+ },
47
+ "devDependencies": {
48
+ "@earendil-works/pi-coding-agent": "0.82.1",
49
+ "@earendil-works/pi-tui": "0.82.1",
50
+ "@types/node": "^24.0.0",
51
+ "tsx": "^4.20.0",
52
+ "typebox": "^1.1.38",
53
+ "typescript": "^5.9.0"
54
+ },
55
+ "scripts": {
56
+ "typecheck": "tsc --noEmit",
57
+ "test": "node --import tsx --test test/*.test.ts",
58
+ "pack:check": "npm pack --dry-run",
59
+ "check": "npm run typecheck && npm test && npm run pack:check",
60
+ "prepublishOnly": "npm run typecheck && npm test"
61
+ },
62
+ "publishConfig": {
63
+ "access": "public"
64
+ }
65
+ }