pi-ultracode 0.3.2 → 0.5.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/README.md +32 -14
- package/README.zh-CN.md +32 -14
- package/extensions/ultracode.ts +11 -10
- package/package.json +17 -7
- package/src/commands.ts +21 -22
- package/src/depth.ts +38 -0
- package/src/index.ts +9 -1
- package/src/mode.ts +82 -58
- package/src/prompts.ts +65 -32
- package/src/thinking.ts +1 -1
- package/src/workflow/agent-runner.ts +23 -8
- package/src/workflow/agent-types.ts +19 -15
- package/src/workflow/hermetic-git.ts +31 -0
- package/src/workflow/journal.ts +134 -3
- package/src/workflow/parser.ts +254 -38
- package/src/workflow/registry.ts +44 -67
- package/src/workflow/repository-context.ts +76 -0
- package/src/workflow/run-artifacts.ts +60 -14
- package/src/workflow/runtime.ts +380 -180
- package/src/workflow/saved-workflow.ts +37 -0
- package/src/workflow/script-executor.ts +56 -10
- package/src/workflow/script-worker.mjs +65 -19
- package/src/workflow/tool.ts +108 -58
- package/src/workflow/workflow-overlay.ts +4 -4
- package/src/workflow/worktree.ts +1965 -244
- package/types/workflow.d.ts +2 -4
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* places:
|
|
7
7
|
* - Built-ins defined below (claude, Explore, Plan, general-purpose, code-reviewer).
|
|
8
8
|
* - Markdown files with YAML-ish frontmatter discovered under:
|
|
9
|
-
* <cwd>/.pi/ultracode/agents/*.md (project)
|
|
9
|
+
* <cwd>/.pi/ultracode/agents/*.md (project-specific)
|
|
10
|
+
* <cwd>/.pi/agents/*.md (project-shared)
|
|
10
11
|
* ~/.pi/ultracode/agents/*.md (user)
|
|
11
12
|
* ~/.pi/agent/agents/*.md (shared with other tools)
|
|
12
13
|
*/
|
|
@@ -54,7 +55,7 @@ const BUILTIN_AGENT_TYPES: AgentTypeDef[] = [
|
|
|
54
55
|
systemPrompt:
|
|
55
56
|
"You are a read-only exploration subagent. Sweep many files/directories and report the conclusion with precise file:line references. Read excerpts rather than whole files. Do NOT modify anything.",
|
|
56
57
|
systemPromptMode: "append",
|
|
57
|
-
tools: ["read", "grep", "find", "ls"
|
|
58
|
+
tools: ["read", "grep", "find", "ls"],
|
|
58
59
|
source: "builtin",
|
|
59
60
|
},
|
|
60
61
|
{
|
|
@@ -63,7 +64,7 @@ const BUILTIN_AGENT_TYPES: AgentTypeDef[] = [
|
|
|
63
64
|
systemPrompt:
|
|
64
65
|
"You are a software-architect subagent. Produce a concrete, step-by-step implementation plan: critical files, sequence, trade-offs, and risks. Do NOT modify files.",
|
|
65
66
|
systemPromptMode: "append",
|
|
66
|
-
tools: ["read", "grep", "find", "ls"
|
|
67
|
+
tools: ["read", "grep", "find", "ls"],
|
|
67
68
|
source: "builtin",
|
|
68
69
|
},
|
|
69
70
|
{
|
|
@@ -76,14 +77,20 @@ const BUILTIN_AGENT_TYPES: AgentTypeDef[] = [
|
|
|
76
77
|
},
|
|
77
78
|
];
|
|
78
79
|
|
|
79
|
-
export function discoverAgentTypes(cwd: string): Map<string, AgentTypeDef> {
|
|
80
|
+
export function discoverAgentTypes(cwd: string, projectTrusted = false): Map<string, AgentTypeDef> {
|
|
80
81
|
const map = new Map<string, AgentTypeDef>();
|
|
81
|
-
for (const def of BUILTIN_AGENT_TYPES) map.set(def.name, def);
|
|
82
|
+
for (const def of BUILTIN_AGENT_TYPES) map.set(def.name.toLowerCase(), def);
|
|
83
|
+
const protectedBuiltins = new Set(["explore", "plan"]);
|
|
82
84
|
|
|
83
85
|
const dirs: Array<{ dir: string; source: AgentTypeDef["source"] }> = [
|
|
84
86
|
{ dir: path.join(os.homedir(), ".pi", "agent", "agents"), source: "user" },
|
|
85
87
|
{ dir: path.join(os.homedir(), ".pi", "ultracode", "agents"), source: "user" },
|
|
86
|
-
|
|
88
|
+
...(projectTrusted
|
|
89
|
+
? [
|
|
90
|
+
{ dir: path.join(cwd, ".pi", "agents"), source: "project" as const },
|
|
91
|
+
{ dir: path.join(cwd, ".pi", "ultracode", "agents"), source: "project" as const },
|
|
92
|
+
]
|
|
93
|
+
: []),
|
|
87
94
|
];
|
|
88
95
|
|
|
89
96
|
for (const { dir, source } of dirs) {
|
|
@@ -98,7 +105,9 @@ export function discoverAgentTypes(cwd: string): Map<string, AgentTypeDef> {
|
|
|
98
105
|
try {
|
|
99
106
|
const content = fs.readFileSync(path.join(dir, entry), "utf8");
|
|
100
107
|
const def = parseAgentTypeFile(content, entry.replace(/\.md$/, ""), source);
|
|
101
|
-
if (def
|
|
108
|
+
if (def && !protectedBuiltins.has(def.name.toLowerCase())) {
|
|
109
|
+
map.set(def.name.trim().toLowerCase(), def);
|
|
110
|
+
}
|
|
102
111
|
} catch {
|
|
103
112
|
// ignore unreadable / malformed agent files
|
|
104
113
|
}
|
|
@@ -112,14 +121,9 @@ export function resolveAgentType(
|
|
|
112
121
|
types: Map<string, AgentTypeDef>,
|
|
113
122
|
): AgentTypeDef | undefined {
|
|
114
123
|
if (!agentType) return undefined;
|
|
115
|
-
const
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
const lower = agentType.toLowerCase();
|
|
119
|
-
for (const def of types.values()) {
|
|
120
|
-
if (def.name.toLowerCase() === lower) return def;
|
|
121
|
-
}
|
|
122
|
-
return undefined;
|
|
124
|
+
const normalized = agentType.trim();
|
|
125
|
+
if (!normalized) return undefined;
|
|
126
|
+
return types.get(normalized.toLowerCase());
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
export function parseAgentTypeFile(
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared hermetic Git environment for workflow Git plumbing.
|
|
3
|
+
*
|
|
4
|
+
* Strips ambient GIT_* influence and pins locale/config so identity and
|
|
5
|
+
* worktree operations observe the same repository view.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
|
|
10
|
+
/** Build a process env that isolates Git from ambient config and locale. */
|
|
11
|
+
export function hermeticGitEnvironment(overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
|
|
12
|
+
const env: NodeJS.ProcessEnv = {};
|
|
13
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
14
|
+
if (key.toUpperCase().startsWith("GIT_")) continue;
|
|
15
|
+
env[key] = value;
|
|
16
|
+
}
|
|
17
|
+
env.LC_ALL = "C";
|
|
18
|
+
env.LANG = "C";
|
|
19
|
+
env.GIT_CONFIG_NOSYSTEM = "1";
|
|
20
|
+
env.GIT_CONFIG_GLOBAL = os.devNull;
|
|
21
|
+
env.GIT_ATTR_NOSYSTEM = "1";
|
|
22
|
+
env.GIT_NO_REPLACE_OBJECTS = "1";
|
|
23
|
+
return { ...env, ...overrides };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Strip a single trailing LF or CRLF without touching other whitespace. */
|
|
27
|
+
export function stripOneLineEnding(value: string): string {
|
|
28
|
+
if (value.endsWith("\r\n")) return value.slice(0, -2);
|
|
29
|
+
if (value.endsWith("\n")) return value.slice(0, -1);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
package/src/workflow/journal.ts
CHANGED
|
@@ -23,21 +23,27 @@ import {
|
|
|
23
23
|
import * as crypto from "node:crypto";
|
|
24
24
|
import { assertWorkflowArgsLimit, assertWorkflowOutputLimit } from "./value-limits.ts";
|
|
25
25
|
|
|
26
|
-
export const RUN_JOURNAL_VERSION =
|
|
26
|
+
export const RUN_JOURNAL_VERSION = 4;
|
|
27
27
|
export const MAX_JOURNAL_BYTES = 64 * 1024 * 1024;
|
|
28
28
|
const NO_FOLLOW = fs.constants.O_NOFOLLOW ?? 0;
|
|
29
29
|
|
|
30
30
|
export interface JournalRunMeta {
|
|
31
31
|
type: "run";
|
|
32
|
-
journalVersion?:
|
|
32
|
+
journalVersion?: 4;
|
|
33
33
|
runId: string;
|
|
34
34
|
name: string;
|
|
35
35
|
scriptHash: string;
|
|
36
36
|
args?: unknown;
|
|
37
|
+
projectTrusted: boolean;
|
|
38
|
+
targetIdentity: string;
|
|
37
39
|
startedAt: number;
|
|
38
40
|
maxAgents?: number;
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
export interface JournalResumeOptions {
|
|
44
|
+
validateDelivery?: (record: JournalAgentRecord) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
export interface JournalResumeRecord {
|
|
42
48
|
type: "resume";
|
|
43
49
|
startedAt: number;
|
|
@@ -80,6 +86,13 @@ export interface JournalNestedSourceRecord {
|
|
|
80
86
|
sourceHash: string;
|
|
81
87
|
}
|
|
82
88
|
|
|
89
|
+
export interface JournalDeliveryStartRecord {
|
|
90
|
+
type: "delivery-start";
|
|
91
|
+
callPath: string;
|
|
92
|
+
patchPath: string;
|
|
93
|
+
patchHash: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
83
96
|
export interface JournalAgentRecord {
|
|
84
97
|
type: "agent";
|
|
85
98
|
/** Stable structural identity assigned in the script Worker. */
|
|
@@ -106,6 +119,9 @@ export interface JournalAgentRecord {
|
|
|
106
119
|
agentType?: string;
|
|
107
120
|
isolation?: string;
|
|
108
121
|
structuredOutput?: boolean;
|
|
122
|
+
/** Durable material retained to verify a completed worktree side effect on resume. */
|
|
123
|
+
deliveryPatchPath?: string;
|
|
124
|
+
deliveryPatchHash?: string;
|
|
109
125
|
transcriptPath?: string;
|
|
110
126
|
startedAt?: number;
|
|
111
127
|
durationMs?: number;
|
|
@@ -128,6 +144,7 @@ export type JournalRecord =
|
|
|
128
144
|
| JournalPanelBranchRecord
|
|
129
145
|
| JournalPanelRecord
|
|
130
146
|
| JournalNestedSourceRecord
|
|
147
|
+
| JournalDeliveryStartRecord
|
|
131
148
|
| JournalAgentRecord
|
|
132
149
|
| JournalResultRecord;
|
|
133
150
|
|
|
@@ -136,6 +153,10 @@ export function hashString(input: string): string {
|
|
|
136
153
|
return crypto.createHash("sha256").update(input).digest("hex");
|
|
137
154
|
}
|
|
138
155
|
|
|
156
|
+
export function hashBytes(input: Uint8Array): string {
|
|
157
|
+
return crypto.createHash("sha256").update(input).digest("hex");
|
|
158
|
+
}
|
|
159
|
+
|
|
139
160
|
export function agentCallKey(prompt: string, opts: unknown): string {
|
|
140
161
|
return hashString(`${prompt}\u0000${stableStringify(opts)}`);
|
|
141
162
|
}
|
|
@@ -152,11 +173,14 @@ interface LoadedJournal {
|
|
|
152
173
|
completePanels: Map<string, JournalPanelRecord>;
|
|
153
174
|
panelBranches: Map<string, Map<number, JournalPanelBranchRecord>>;
|
|
154
175
|
nestedSources: Map<string, string>;
|
|
176
|
+
pendingDeliveries: Map<string, JournalDeliveryStartRecord>;
|
|
155
177
|
}
|
|
156
178
|
|
|
157
179
|
export class RunJournal {
|
|
158
180
|
readonly filePath: string;
|
|
159
181
|
readonly effectiveMaxAgents: number;
|
|
182
|
+
readonly projectTrusted: boolean;
|
|
183
|
+
readonly targetIdentity: string;
|
|
160
184
|
private readonly fd: number;
|
|
161
185
|
private readonly priorAgents: Map<string, JournalAgentRecord>;
|
|
162
186
|
private readonly admittedPaths: Set<string>;
|
|
@@ -167,17 +191,21 @@ export class RunJournal {
|
|
|
167
191
|
private readonly openPanelBranches = new Map<string, Map<number, JournalPanelBranchRecord>>();
|
|
168
192
|
private readonly openPanelAdmissions = new Map<string, Set<string>>();
|
|
169
193
|
private readonly nestedSources: Map<string, string>;
|
|
194
|
+
private readonly pendingDeliveries: Map<string, JournalDeliveryStartRecord>;
|
|
170
195
|
private _agentsUsed: number;
|
|
171
196
|
private bytes: number;
|
|
172
197
|
private fdClosed = false;
|
|
173
198
|
private closed = false;
|
|
174
199
|
private writeFailure?: WorkflowPolicyError;
|
|
175
200
|
private closeFailure?: unknown;
|
|
201
|
+
private resultRecorded = false;
|
|
176
202
|
|
|
177
203
|
private constructor(filePath: string, fd: number, loaded: LoadedJournal) {
|
|
178
204
|
this.filePath = filePath;
|
|
179
205
|
this.fd = fd;
|
|
180
206
|
this.effectiveMaxAgents = loaded.effectiveMaxAgents;
|
|
207
|
+
this.projectTrusted = loaded.header.projectTrusted;
|
|
208
|
+
this.targetIdentity = loaded.header.targetIdentity;
|
|
181
209
|
this._agentsUsed = loaded.agentsUsed;
|
|
182
210
|
this.bytes = loaded.bytes;
|
|
183
211
|
this.priorAgents = loaded.agents;
|
|
@@ -187,6 +215,7 @@ export class RunJournal {
|
|
|
187
215
|
this.completePanels = loaded.completePanels;
|
|
188
216
|
this.panelBranches = loaded.panelBranches;
|
|
189
217
|
this.nestedSources = loaded.nestedSources;
|
|
218
|
+
this.pendingDeliveries = loaded.pendingDeliveries;
|
|
190
219
|
}
|
|
191
220
|
|
|
192
221
|
get agentsUsed(): number {
|
|
@@ -200,6 +229,7 @@ export class RunJournal {
|
|
|
200
229
|
const header: JournalRunMeta = {
|
|
201
230
|
...meta,
|
|
202
231
|
journalVersion: RUN_JOURNAL_VERSION,
|
|
232
|
+
projectTrusted: meta.projectTrusted,
|
|
203
233
|
maxAgents: effectiveMaxAgents,
|
|
204
234
|
};
|
|
205
235
|
validateRunRecord(header);
|
|
@@ -222,6 +252,7 @@ export class RunJournal {
|
|
|
222
252
|
completePanels: new Map(),
|
|
223
253
|
panelBranches: new Map(),
|
|
224
254
|
nestedSources: new Map(),
|
|
255
|
+
pendingDeliveries: new Map(),
|
|
225
256
|
});
|
|
226
257
|
try {
|
|
227
258
|
journal.append(header);
|
|
@@ -239,7 +270,12 @@ export class RunJournal {
|
|
|
239
270
|
}
|
|
240
271
|
|
|
241
272
|
/** Resume the same immutable run ledger, optionally raising its lifetime cap. */
|
|
242
|
-
static resume(
|
|
273
|
+
static resume(
|
|
274
|
+
dir: string,
|
|
275
|
+
runId: string,
|
|
276
|
+
meta: JournalRunMeta,
|
|
277
|
+
options: JournalResumeOptions = {},
|
|
278
|
+
): RunJournal {
|
|
243
279
|
assertWorkflowArgsLimit(meta.args);
|
|
244
280
|
validateRunRecord(meta);
|
|
245
281
|
ensurePrivateArtifactDirectory(dir);
|
|
@@ -256,6 +292,23 @@ export class RunJournal {
|
|
|
256
292
|
if (JSON.stringify(loaded.header.args) !== JSON.stringify(meta.args)) {
|
|
257
293
|
throw new WorkflowPolicyError(`workflow ${runId} has immutable args; start a new run for changed args`);
|
|
258
294
|
}
|
|
295
|
+
if (loaded.header.projectTrusted !== meta.projectTrusted) {
|
|
296
|
+
throw new WorkflowPolicyError(`workflow ${runId} has an immutable project trust context; start a new run after trust changes`);
|
|
297
|
+
}
|
|
298
|
+
if (loaded.header.targetIdentity !== meta.targetIdentity) {
|
|
299
|
+
throw new WorkflowPolicyError(`workflow ${runId} has an immutable repository/cwd target; start a new run after changing location`);
|
|
300
|
+
}
|
|
301
|
+
if (loaded.pendingDeliveries.size > 0) {
|
|
302
|
+
const pending = [...loaded.pendingDeliveries.values()][0]!;
|
|
303
|
+
throw new WorkflowPolicyError(
|
|
304
|
+
`workflow delivery at ${pending.callPath} requires recovery before resume; patch: ${pending.patchPath}`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
if (options.validateDelivery) {
|
|
308
|
+
for (const agent of loaded.agents.values()) {
|
|
309
|
+
if (agent.deliveryPatchPath) options.validateDelivery(agent);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
259
312
|
|
|
260
313
|
const requested = meta.maxAgents === undefined
|
|
261
314
|
? loaded.effectiveMaxAgents
|
|
@@ -299,6 +352,12 @@ export class RunJournal {
|
|
|
299
352
|
}
|
|
300
353
|
|
|
301
354
|
lookup(callPath: string, key: string): JournalAgentRecord | undefined {
|
|
355
|
+
const pending = this.pendingDeliveries.get(callPath);
|
|
356
|
+
if (pending) {
|
|
357
|
+
throw new WorkflowPolicyError(
|
|
358
|
+
`workflow delivery at ${callPath} requires recovery before resume; patch: ${pending.patchPath}`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
302
361
|
const prior = this.priorAgents.get(callPath);
|
|
303
362
|
if (!prior) return undefined;
|
|
304
363
|
if (prior.key !== key) {
|
|
@@ -478,19 +537,36 @@ export class RunJournal {
|
|
|
478
537
|
return credit;
|
|
479
538
|
}
|
|
480
539
|
|
|
540
|
+
recordDeliveryStart(callPath: string, patchPath: string, patchHash: string): void {
|
|
541
|
+
if (this.pendingDeliveries.has(callPath) || this.priorAgents.has(callPath)) {
|
|
542
|
+
throw new WorkflowPolicyError(`workflow delivery already started at ${callPath}`);
|
|
543
|
+
}
|
|
544
|
+
const record: JournalDeliveryStartRecord = { type: "delivery-start", callPath, patchPath, patchHash };
|
|
545
|
+
validateDeliveryStartRecord(record);
|
|
546
|
+
this.append(record);
|
|
547
|
+
this.pendingDeliveries.set(callPath, record);
|
|
548
|
+
}
|
|
549
|
+
|
|
481
550
|
recordAgent(record: Omit<JournalAgentRecord, "type">): void {
|
|
482
551
|
const full: JournalAgentRecord = { type: "agent", ...record };
|
|
483
552
|
assertWorkflowOutputLimit(full.value, "workflow agent output");
|
|
484
553
|
validateAgentRecord(full);
|
|
554
|
+
const pending = this.pendingDeliveries.get(full.callPath);
|
|
555
|
+
assertDeliveryCompletionMatches(pending, full);
|
|
485
556
|
this.append(full);
|
|
486
557
|
this.priorAgents.set(full.callPath, full);
|
|
558
|
+
this.pendingDeliveries.delete(full.callPath);
|
|
487
559
|
}
|
|
488
560
|
|
|
489
561
|
recordResult(record: Omit<JournalResultRecord, "type">): void {
|
|
562
|
+
if (this.resultRecorded) {
|
|
563
|
+
throw new WorkflowPolicyError("workflow journal generation already has a terminal result");
|
|
564
|
+
}
|
|
490
565
|
if (record.ok) assertWorkflowOutputLimit(record.result);
|
|
491
566
|
const full: JournalResultRecord = { type: "result", ...record };
|
|
492
567
|
validateResultRecord(full);
|
|
493
568
|
this.append(full);
|
|
569
|
+
this.resultRecorded = true;
|
|
494
570
|
}
|
|
495
571
|
|
|
496
572
|
close(): void {
|
|
@@ -554,6 +630,10 @@ export class RunJournal {
|
|
|
554
630
|
}
|
|
555
631
|
|
|
556
632
|
private append(record: JournalRecord): void {
|
|
633
|
+
if (this.resultRecorded && record.type !== "resume") {
|
|
634
|
+
throw new WorkflowPolicyError("workflow journal generation already has a terminal result");
|
|
635
|
+
}
|
|
636
|
+
if (record.type === "resume") this.resultRecorded = false;
|
|
557
637
|
if (this.writeFailure) throw this.writeFailure;
|
|
558
638
|
if (this.closeFailure) throw this.closeFailure;
|
|
559
639
|
if (this.closed || this.fdClosed) throw new WorkflowPolicyError("workflow journal is already closed");
|
|
@@ -617,7 +697,13 @@ function loadJournal(filePath: string): LoadedJournal {
|
|
|
617
697
|
const completePanels = new Map<string, JournalPanelRecord>();
|
|
618
698
|
const panelBranches = new Map<string, Map<number, JournalPanelBranchRecord>>();
|
|
619
699
|
const nestedSources = new Map<string, string>();
|
|
700
|
+
const pendingDeliveries = new Map<string, JournalDeliveryStartRecord>();
|
|
701
|
+
let generationTerminated = false;
|
|
620
702
|
for (const record of records.slice(1)) {
|
|
703
|
+
if (generationTerminated && record.type !== "resume") {
|
|
704
|
+
throw new WorkflowPolicyError("workflow journal contains a record after a generation terminal result");
|
|
705
|
+
}
|
|
706
|
+
if (record.type === "resume") generationTerminated = false;
|
|
621
707
|
switch (record.type) {
|
|
622
708
|
case "resume": {
|
|
623
709
|
if (!isNonNegativeFinite(record.startedAt)) invalidJournalRecord("resume");
|
|
@@ -648,12 +734,21 @@ function loadJournal(filePath: string): LoadedJournal {
|
|
|
648
734
|
admittedPaths.add(record.callPath);
|
|
649
735
|
admissionHashes.set(record.callPath, record.inputHash);
|
|
650
736
|
break;
|
|
737
|
+
case "delivery-start":
|
|
738
|
+
validateDeliveryStartRecord(record);
|
|
739
|
+
if (pendingDeliveries.has(record.callPath) || agents.has(record.callPath)) {
|
|
740
|
+
throw new WorkflowPolicyError(`workflow journal contains duplicate delivery start: ${record.callPath}`);
|
|
741
|
+
}
|
|
742
|
+
pendingDeliveries.set(record.callPath, record);
|
|
743
|
+
break;
|
|
651
744
|
case "agent":
|
|
652
745
|
validateAgentRecord(record);
|
|
653
746
|
if (agents.has(record.callPath)) {
|
|
654
747
|
throw new WorkflowPolicyError(`workflow journal contains duplicate agent result: ${record.callPath}`);
|
|
655
748
|
}
|
|
749
|
+
assertDeliveryCompletionMatches(pendingDeliveries.get(record.callPath), record);
|
|
656
750
|
agents.set(record.callPath, record);
|
|
751
|
+
pendingDeliveries.delete(record.callPath);
|
|
657
752
|
break;
|
|
658
753
|
case "panel-open":
|
|
659
754
|
if (
|
|
@@ -756,6 +851,7 @@ function loadJournal(filePath: string): LoadedJournal {
|
|
|
756
851
|
}
|
|
757
852
|
case "result":
|
|
758
853
|
validateResultRecord(record);
|
|
854
|
+
generationTerminated = true;
|
|
759
855
|
break;
|
|
760
856
|
case "run":
|
|
761
857
|
throw new WorkflowPolicyError("workflow journal contains more than one run header");
|
|
@@ -793,6 +889,7 @@ function loadJournal(filePath: string): LoadedJournal {
|
|
|
793
889
|
completePanels,
|
|
794
890
|
panelBranches,
|
|
795
891
|
nestedSources,
|
|
892
|
+
pendingDeliveries,
|
|
796
893
|
};
|
|
797
894
|
}
|
|
798
895
|
|
|
@@ -843,6 +940,9 @@ function validateJournalRecordForWrite(record: JournalRecord): void {
|
|
|
843
940
|
invalidJournalRecord("nested-source");
|
|
844
941
|
}
|
|
845
942
|
return;
|
|
943
|
+
case "delivery-start":
|
|
944
|
+
validateDeliveryStartRecord(record);
|
|
945
|
+
return;
|
|
846
946
|
case "agent":
|
|
847
947
|
validateAgentRecord(record);
|
|
848
948
|
return;
|
|
@@ -863,6 +963,8 @@ function validateRunRecord(record: JournalRunMeta): void {
|
|
|
863
963
|
|| !isNonEmptyString(record.name)
|
|
864
964
|
|| !isNonEmptyString(record.scriptHash)
|
|
865
965
|
|| !isNonNegativeFinite(record.startedAt)
|
|
966
|
+
|| typeof record.projectTrusted !== "boolean"
|
|
967
|
+
|| !isNonEmptyString(record.targetIdentity)
|
|
866
968
|
) invalidJournalRecord("run header");
|
|
867
969
|
if (record.journalVersion !== undefined && record.journalVersion !== RUN_JOURNAL_VERSION) {
|
|
868
970
|
invalidJournalRecord("run header");
|
|
@@ -875,6 +977,33 @@ function validateRunRecord(record: JournalRunMeta): void {
|
|
|
875
977
|
}
|
|
876
978
|
}
|
|
877
979
|
|
|
980
|
+
function validateDeliveryStartRecord(record: JournalDeliveryStartRecord): void {
|
|
981
|
+
if (
|
|
982
|
+
!isNonEmptyString(record.callPath)
|
|
983
|
+
|| !isNonEmptyString(record.patchPath)
|
|
984
|
+
|| !isNonEmptyString(record.patchHash)
|
|
985
|
+
) invalidJournalRecord("delivery-start");
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function assertDeliveryCompletionMatches(
|
|
989
|
+
pending: JournalDeliveryStartRecord | undefined,
|
|
990
|
+
agent: JournalAgentRecord,
|
|
991
|
+
): void {
|
|
992
|
+
const hasPath = agent.deliveryPatchPath !== undefined;
|
|
993
|
+
const hasHash = agent.deliveryPatchHash !== undefined;
|
|
994
|
+
if (hasPath !== hasHash) invalidJournalRecord("agent delivery completion");
|
|
995
|
+
if (!pending) {
|
|
996
|
+
if (hasPath) invalidJournalRecord("agent delivery completion");
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (
|
|
1000
|
+
agent.deliveryPatchPath !== pending.patchPath
|
|
1001
|
+
|| agent.deliveryPatchHash !== pending.patchHash
|
|
1002
|
+
) {
|
|
1003
|
+
throw new WorkflowPolicyError(`workflow journal agent delivery does not match intent: ${agent.callPath}`);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
878
1007
|
function validateAgentRecord(record: JournalAgentRecord): void {
|
|
879
1008
|
const integerFields: Array<keyof JournalAgentRecord> = [
|
|
880
1009
|
"seq",
|
|
@@ -914,6 +1043,8 @@ function validateAgentRecord(record: JournalAgentRecord): void {
|
|
|
914
1043
|
"effort",
|
|
915
1044
|
"agentType",
|
|
916
1045
|
"isolation",
|
|
1046
|
+
"deliveryPatchPath",
|
|
1047
|
+
"deliveryPatchHash",
|
|
917
1048
|
"transcriptPath",
|
|
918
1049
|
] as const) {
|
|
919
1050
|
if (record[field] !== undefined && typeof record[field] !== "string") invalidJournalRecord("agent");
|