pi-ultracode 0.1.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,213 @@
1
+ /**
2
+ * AST-validated parser for ultracode workflow scripts.
3
+ *
4
+ * A workflow script is plain JavaScript whose first statement must be a literal
5
+ * `export const meta = { name, description, ... }`. The rest of the script runs
6
+ * inside a deterministic vm sandbox (see runtime.ts), so we statically reject the
7
+ * non-deterministic primitives that would break reproducible / resumable runs.
8
+ */
9
+
10
+ import { parse } from "acorn";
11
+ import type { Node } from "acorn";
12
+
13
+ export interface WorkflowMetaPhase {
14
+ title: string;
15
+ detail?: string;
16
+ model?: string;
17
+ }
18
+
19
+ export interface WorkflowMeta {
20
+ name: string;
21
+ description: string;
22
+ whenToUse?: string;
23
+ phases?: WorkflowMetaPhase[];
24
+ [key: string]: unknown;
25
+ }
26
+
27
+ export interface ParsedWorkflow {
28
+ meta: WorkflowMeta;
29
+ /** Script with the `export const meta` statement removed, ready to wrap in an async IIFE. */
30
+ body: string;
31
+ }
32
+
33
+ type AnyNode = Node & { [key: string]: any; start: number; end: number };
34
+
35
+ const NONDETERMINISM_ERROR =
36
+ "Workflow scripts must be deterministic: Date.now(), Math.random(), and new Date() are unavailable. Pass timestamps via args and vary randomness by agent index.";
37
+
38
+ /** Strip a single Markdown code fence if the model wrapped the script in one. */
39
+ export function normalizeScript(script: string): string {
40
+ let text = script.trim();
41
+ const fence = text.match(/^```(?:js|javascript|mjs)?\s*\n([\s\S]*?)\n```$/i);
42
+ if (fence) text = fence[1].trim();
43
+ return text;
44
+ }
45
+
46
+ export function parseWorkflowScript(rawScript: string): ParsedWorkflow {
47
+ const script = normalizeScript(rawScript);
48
+ let ast: AnyNode;
49
+ try {
50
+ ast = parse(script, {
51
+ ecmaVersion: "latest",
52
+ sourceType: "module",
53
+ allowAwaitOutsideFunction: true,
54
+ allowReturnOutsideFunction: true,
55
+ }) as AnyNode;
56
+ } catch (error) {
57
+ const detail = error instanceof Error ? error.message : String(error);
58
+ throw new Error(`workflow script is not valid JavaScript: ${detail}`);
59
+ }
60
+
61
+ assertDeterministicAst(ast);
62
+
63
+ const first = ast.body?.[0] as AnyNode | undefined;
64
+ if (first?.type !== "ExportNamedDeclaration") {
65
+ throw new Error("`export const meta = { name, description }` must be the first statement in the script");
66
+ }
67
+
68
+ const declaration = first.declaration as AnyNode | null;
69
+ if (declaration?.type !== "VariableDeclaration" || declaration.kind !== "const") {
70
+ throw new Error("meta export must be `export const meta = ...`");
71
+ }
72
+ if (declaration.declarations.length !== 1) {
73
+ throw new Error("the meta export must declare only `meta`");
74
+ }
75
+
76
+ const declarator = declaration.declarations[0] as AnyNode;
77
+ if (declarator.id?.type !== "Identifier" || declarator.id.name !== "meta") {
78
+ throw new Error("the meta export must declare `meta`");
79
+ }
80
+ if (!declarator.init) throw new Error("meta must have a literal value");
81
+
82
+ const meta = evaluateLiteral(declarator.init, "meta") as WorkflowMeta;
83
+ validateMeta(meta);
84
+
85
+ return {
86
+ meta,
87
+ body: script.slice(0, first.start) + script.slice(first.end),
88
+ };
89
+ }
90
+
91
+ /** Evaluate a strictly-literal AST node (no identifiers, calls, or interpolation). */
92
+ function evaluateLiteral(node: AnyNode, path: string): unknown {
93
+ switch (node.type) {
94
+ case "ObjectExpression": {
95
+ const out: Record<string, unknown> = {};
96
+ for (const prop of node.properties as AnyNode[]) {
97
+ if (prop.type === "SpreadElement") throw new Error(`spread is not allowed in ${path}`);
98
+ if (prop.type !== "Property") throw new Error(`only plain properties are allowed in ${path}`);
99
+ if (prop.computed) throw new Error(`computed keys are not allowed in ${path}`);
100
+ if (prop.kind !== "init" || prop.method) throw new Error(`methods/accessors are not allowed in ${path}`);
101
+ const key = propertyKey(prop.key as AnyNode, path);
102
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
103
+ throw new Error(`reserved key name is not allowed in ${path}: ${key}`);
104
+ }
105
+ out[key] = evaluateLiteral(prop.value as AnyNode, `${path}.${key}`);
106
+ }
107
+ return out;
108
+ }
109
+ case "ArrayExpression":
110
+ return (node.elements as Array<AnyNode | null>).map((element, index) => {
111
+ if (!element) throw new Error(`sparse arrays are not allowed in ${path}`);
112
+ if (element.type === "SpreadElement") throw new Error(`spread is not allowed in ${path}`);
113
+ return evaluateLiteral(element, `${path}[${index}]`);
114
+ });
115
+ case "Literal":
116
+ return node.value;
117
+ case "TemplateLiteral":
118
+ if (node.expressions.length > 0) throw new Error(`template interpolation is not allowed in ${path}`);
119
+ return node.quasis.map((quasi: AnyNode) => quasi.value.cooked ?? quasi.value.raw).join("");
120
+ case "UnaryExpression":
121
+ if (node.operator === "-" && node.argument?.type === "Literal" && typeof node.argument.value === "number") {
122
+ return -node.argument.value;
123
+ }
124
+ throw new Error(`only the negative-number unary operator is allowed in ${path}`);
125
+ default:
126
+ throw new Error(`non-literal node type in ${path}: ${node.type}`);
127
+ }
128
+ }
129
+
130
+ function propertyKey(node: AnyNode, path: string): string {
131
+ if (node.type === "Identifier") return node.name;
132
+ if (node.type === "Literal" && (typeof node.value === "string" || typeof node.value === "number")) {
133
+ return String(node.value);
134
+ }
135
+ throw new Error(`unsupported key type in ${path}: ${node.type}`);
136
+ }
137
+
138
+ function assertDeterministicAst(node: AnyNode): void {
139
+ if (isDateNowCall(node) || isMathRandomCall(node) || isNewDateExpression(node)) {
140
+ throw new Error(NONDETERMINISM_ERROR);
141
+ }
142
+ for (const child of astChildren(node)) assertDeterministicAst(child);
143
+ }
144
+
145
+ function astChildren(node: AnyNode): AnyNode[] {
146
+ const children: AnyNode[] = [];
147
+ for (const value of Object.values(node)) {
148
+ if (Array.isArray(value)) children.push(...value.filter(isAstNode));
149
+ else if (isAstNode(value)) children.push(value);
150
+ }
151
+ return children;
152
+ }
153
+
154
+ function isAstNode(value: unknown): value is AnyNode {
155
+ return !!value && typeof value === "object" && typeof (value as AnyNode).type === "string";
156
+ }
157
+
158
+ function isDateNowCall(node: AnyNode): boolean {
159
+ return node.type === "CallExpression" && isMemberExpression(node.callee, "Date", "now");
160
+ }
161
+
162
+ function isMathRandomCall(node: AnyNode): boolean {
163
+ return node.type === "CallExpression" && isMemberExpression(node.callee, "Math", "random");
164
+ }
165
+
166
+ function isNewDateExpression(node: AnyNode): boolean {
167
+ return node.type === "NewExpression" && node.callee?.type === "Identifier" && node.callee.name === "Date";
168
+ }
169
+
170
+ function isMemberExpression(node: AnyNode | undefined, objectName: string, propertyName: string): boolean {
171
+ if (node?.type !== "MemberExpression" || node.object?.type !== "Identifier" || node.object.name !== objectName) {
172
+ return false;
173
+ }
174
+ return propertyNameOf(node) === propertyName;
175
+ }
176
+
177
+ function propertyNameOf(node: AnyNode): string | undefined {
178
+ if (!node.computed && node.property?.type === "Identifier") return node.property.name;
179
+ return staticStringOf(node.property);
180
+ }
181
+
182
+ function staticStringOf(node: AnyNode | undefined): string | undefined {
183
+ if (node?.type === "Literal" && typeof node.value === "string") return node.value;
184
+ if (node?.type === "TemplateLiteral" && node.expressions.length === 0) {
185
+ return node.quasis.map((quasi: AnyNode) => quasi.value.cooked ?? quasi.value.raw).join("");
186
+ }
187
+ if (node?.type === "BinaryExpression" && node.operator === "+") {
188
+ const left = staticStringOf(node.left);
189
+ const right = staticStringOf(node.right);
190
+ if (left !== undefined && right !== undefined) return left + right;
191
+ }
192
+ return undefined;
193
+ }
194
+
195
+ function validateMeta(meta: unknown): asserts meta is WorkflowMeta {
196
+ if (!meta || typeof meta !== "object") throw new Error("meta must be an object");
197
+ const value = meta as WorkflowMeta;
198
+ if (typeof value.name !== "string" || !value.name.trim()) throw new Error("meta.name must be a non-empty string");
199
+ if (typeof value.description !== "string" || !value.description.trim()) {
200
+ throw new Error("meta.description must be a non-empty string");
201
+ }
202
+ if (value.whenToUse !== undefined && typeof value.whenToUse !== "string") {
203
+ throw new Error("meta.whenToUse must be a string");
204
+ }
205
+ if (value.phases !== undefined) {
206
+ if (!Array.isArray(value.phases)) throw new Error("meta.phases must be an array");
207
+ for (const phase of value.phases) {
208
+ if (!phase || typeof phase !== "object" || typeof (phase as WorkflowMetaPhase).title !== "string") {
209
+ throw new Error("each meta phase must have a title string");
210
+ }
211
+ }
212
+ }
213
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Process-local registry of workflow runs, so the `/workflows` command can list
3
+ * recent and in-flight runs and show live progress. Snapshots are updated in place
4
+ * by the workflow tool as a run progresses.
5
+ */
6
+
7
+ import type { WorkflowSnapshot } from "./display.ts";
8
+
9
+ export interface RunHandle {
10
+ snapshot: WorkflowSnapshot;
11
+ abort: () => void;
12
+ startedAt: number;
13
+ }
14
+
15
+ export class WorkflowRegistry {
16
+ private readonly runs = new Map<string, RunHandle>();
17
+ private order: string[] = [];
18
+
19
+ register(runId: string, snapshot: WorkflowSnapshot, abort: () => void): RunHandle {
20
+ const handle: RunHandle = { snapshot, abort, startedAt: Date.now() };
21
+ this.runs.set(runId, handle);
22
+ this.order = this.order.filter((id) => id !== runId);
23
+ this.order.push(runId);
24
+ // Keep at most the 50 most recent runs in memory.
25
+ while (this.order.length > 50) {
26
+ const evict = this.order.shift();
27
+ if (evict) this.runs.delete(evict);
28
+ }
29
+ return handle;
30
+ }
31
+
32
+ get(runId: string): RunHandle | undefined {
33
+ return this.runs.get(runId);
34
+ }
35
+
36
+ list(): RunHandle[] {
37
+ return this.order
38
+ .map((id) => this.runs.get(id))
39
+ .filter((h): h is RunHandle => Boolean(h))
40
+ .reverse();
41
+ }
42
+
43
+ active(): RunHandle[] {
44
+ return this.list().filter((h) => h.snapshot.status === "running");
45
+ }
46
+
47
+ abortAll(): void {
48
+ for (const handle of this.runs.values()) {
49
+ if (handle.snapshot.status === "running") {
50
+ try {
51
+ handle.abort();
52
+ } catch {
53
+ // ignore
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ let singleton: WorkflowRegistry | undefined;
61
+
62
+ export function getRegistry(): WorkflowRegistry {
63
+ if (!singleton) singleton = new WorkflowRegistry();
64
+ return singleton;
65
+ }