@osolmaz/pi-workflows 0.12.1 → 0.13.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.
Files changed (209) hide show
  1. package/README.md +52 -28
  2. package/dist/builtins/autodoc.workflow.d.ts +4 -4
  3. package/dist/builtins/autoimplement.workflow.d.ts +369 -73
  4. package/dist/builtins/autoplan.workflow.d.ts +126 -13
  5. package/dist/builtins/autoplan.workflow.js +200 -29
  6. package/dist/builtins/autoplan.workflow.js.map +1 -1
  7. package/dist/builtins/catalog.js +6 -4
  8. package/dist/builtins/catalog.js.map +1 -1
  9. package/dist/builtins/index.d.ts +2 -1
  10. package/dist/builtins/index.js +1 -0
  11. package/dist/builtins/index.js.map +1 -1
  12. package/dist/builtins/monitor.workflow.d.ts +56 -21
  13. package/dist/builtins/monitor.workflow.js +497 -216
  14. package/dist/builtins/monitor.workflow.js.map +1 -1
  15. package/dist/builtins/plain-summary.workflow.d.ts +32 -0
  16. package/dist/builtins/plain-summary.workflow.js +134 -0
  17. package/dist/builtins/plain-summary.workflow.js.map +1 -0
  18. package/dist/builtins/plan-change.workflow.d.ts +170 -22
  19. package/dist/builtins/sanity-check.workflow.d.ts +45 -3
  20. package/dist/builtins/sanity-check.workflow.js +45 -7
  21. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  22. package/dist/controllers/effects.d.ts +3 -2
  23. package/dist/controllers/effects.js +8 -1
  24. package/dist/controllers/effects.js.map +1 -1
  25. package/dist/controllers/index.d.ts +1 -1
  26. package/dist/controllers/index.js +1 -1
  27. package/dist/controllers/index.js.map +1 -1
  28. package/dist/controllers/manager.d.ts +1 -0
  29. package/dist/controllers/manager.js +41 -17
  30. package/dist/controllers/manager.js.map +1 -1
  31. package/dist/controllers/sqlite.d.ts +107 -85
  32. package/dist/controllers/sqlite.js +1611 -1486
  33. package/dist/controllers/sqlite.js.map +1 -1
  34. package/dist/controllers/store.d.ts +9 -8
  35. package/dist/controllers/store.js +1 -31
  36. package/dist/controllers/store.js.map +1 -1
  37. package/dist/controllers/types.d.ts +3 -0
  38. package/dist/controllers/workflow-engine-scheduler.d.ts +1 -1
  39. package/dist/controllers/workflow-engine-scheduler.js +3 -5
  40. package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
  41. package/dist/controllers/workflows.d.ts +2 -2
  42. package/dist/controllers/workflows.js +11 -10
  43. package/dist/controllers/workflows.js.map +1 -1
  44. package/dist/extension/controller-host.js +2 -2
  45. package/dist/extension/controller-host.js.map +1 -1
  46. package/dist/extension/decision-channels.d.ts +2 -0
  47. package/dist/extension/decision-channels.js +311 -118
  48. package/dist/extension/decision-channels.js.map +1 -1
  49. package/dist/extension/executor.d.ts +9 -2
  50. package/dist/extension/executor.js +89 -2
  51. package/dist/extension/executor.js.map +1 -1
  52. package/dist/extension/herdr-viewer.d.ts +0 -1
  53. package/dist/extension/herdr-viewer.js +0 -4
  54. package/dist/extension/herdr-viewer.js.map +1 -1
  55. package/dist/extension/index.js +156 -103
  56. package/dist/extension/index.js.map +1 -1
  57. package/dist/extension/recorder.d.ts +3 -10
  58. package/dist/extension/recorder.js +13 -22
  59. package/dist/extension/recorder.js.map +1 -1
  60. package/dist/extension/step-message.d.ts +7 -2
  61. package/dist/extension/step-message.js +94 -2
  62. package/dist/extension/step-message.js.map +1 -1
  63. package/dist/extension/widget.js +6 -0
  64. package/dist/extension/widget.js.map +1 -1
  65. package/dist/host/rpc-executor.d.ts +3 -0
  66. package/dist/host/rpc-executor.js +2 -0
  67. package/dist/host/rpc-executor.js.map +1 -1
  68. package/dist/host/runner.d.ts +3 -6
  69. package/dist/host/runner.js +83 -32
  70. package/dist/host/runner.js.map +1 -1
  71. package/dist/render/graph-render.js +14 -8
  72. package/dist/render/graph-render.js.map +1 -1
  73. package/dist/state/database.d.ts +35 -0
  74. package/dist/state/database.js +287 -0
  75. package/dist/state/database.js.map +1 -0
  76. package/dist/state/index.d.ts +4 -0
  77. package/dist/state/index.js +5 -0
  78. package/dist/state/index.js.map +1 -0
  79. package/dist/state/json.d.ts +6 -0
  80. package/dist/state/json.js +38 -0
  81. package/dist/state/json.js.map +1 -0
  82. package/dist/state/mutation.d.ts +57 -0
  83. package/dist/state/mutation.js +255 -0
  84. package/dist/state/mutation.js.map +1 -0
  85. package/dist/state/schema.d.ts +6 -0
  86. package/dist/state/schema.js +473 -0
  87. package/dist/state/schema.js.map +1 -0
  88. package/dist/viewer/cli.d.ts +2 -2
  89. package/dist/viewer/cli.js +142 -122
  90. package/dist/viewer/cli.js.map +1 -1
  91. package/dist/viewer/render.d.ts +4 -4
  92. package/dist/viewer/render.js +2 -17
  93. package/dist/viewer/render.js.map +1 -1
  94. package/dist/viewer/session-reducer.d.ts +1 -1
  95. package/dist/viewer/session-reducer.js +1 -1
  96. package/dist/viewer/tui.d.ts +3 -3
  97. package/dist/viewer/tui.js +15 -13
  98. package/dist/viewer/tui.js.map +1 -1
  99. package/dist/viewer/watch.d.ts +2 -5
  100. package/dist/viewer/watch.js +13 -13
  101. package/dist/viewer/watch.js.map +1 -1
  102. package/dist/workflows/composition.js +12 -2
  103. package/dist/workflows/composition.js.map +1 -1
  104. package/dist/workflows/definition.d.ts +6 -2
  105. package/dist/workflows/definition.js +17 -0
  106. package/dist/workflows/definition.js.map +1 -1
  107. package/dist/workflows/engine.d.ts +6 -6
  108. package/dist/workflows/engine.js +183 -103
  109. package/dist/workflows/engine.js.map +1 -1
  110. package/dist/workflows/errors.d.ts +3 -3
  111. package/dist/workflows/errors.js +3 -3
  112. package/dist/workflows/human-decision.d.ts +29 -4
  113. package/dist/workflows/human-decision.js +547 -283
  114. package/dist/workflows/human-decision.js.map +1 -1
  115. package/dist/workflows/index.d.ts +4 -5
  116. package/dist/workflows/index.js +3 -4
  117. package/dist/workflows/index.js.map +1 -1
  118. package/dist/workflows/schema.js +17 -1
  119. package/dist/workflows/schema.js.map +1 -1
  120. package/dist/workflows/store.d.ts +83 -103
  121. package/dist/workflows/store.js +902 -951
  122. package/dist/workflows/store.js.map +1 -1
  123. package/dist/workflows/types.d.ts +55 -81
  124. package/docs/CONTROLLERS.md +10 -8
  125. package/docs/DEFERRED_TURNS.md +2 -2
  126. package/docs/DESIGN_PHILOSOPHY.md +4 -2
  127. package/docs/HUMAN_DECISIONS.md +13 -14
  128. package/docs/MONITOR.md +179 -231
  129. package/docs/SQLITE_STATE.md +219 -0
  130. package/docs/WORKFLOW_COMPOSITION.md +19 -19
  131. package/docs/WORKFLOW_STEP_MESSAGES.md +20 -18
  132. package/docs/WORKFLOW_UPDATES.md +10 -9
  133. package/docs/development.md +9 -9
  134. package/docs/live-replay-protocol.md +28 -37
  135. package/docs/plans/2026-08-16-workflow-updates-plan.md +1 -1
  136. package/docs/plans/2026-08-19-human-decision-gates-plan.md +1 -1
  137. package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +1 -1
  138. package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +1 -1
  139. package/docs/plans/2026-08-21-sanity-check-plan.md +30 -16
  140. package/docs/plans/2026-08-22-goal-finishing-monitor-plan.md +200 -0
  141. package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +701 -0
  142. package/docs/plans/2026-08-23-sqlite-state-plan.md +970 -0
  143. package/docs/plans/replayable-run-bundles-implementation-plan.md +2 -2
  144. package/docs/plans/session-event-replay-implementation-plan.md +1 -1
  145. package/docs/plans/tui-viewer-implementation-plan.md +1 -1
  146. package/docs/session-event-journal.md +63 -434
  147. package/docs/tui-viewer.md +13 -15
  148. package/docs/workflows.md +104 -50
  149. package/examples/workflows/plain-summary.workflow.ts +1 -0
  150. package/herdr-plugin.toml +1 -1
  151. package/package.json +1 -1
  152. package/plugins/herdr/viewer.mjs +1 -11
  153. package/skills/autoplan/SKILL.md +5 -4
  154. package/skills/monitor/SKILL.md +91 -163
  155. package/skills/pi-workflows/SKILL.md +7 -7
  156. package/skills/sanity-check/SKILL.md +1 -1
  157. package/src/builtins/autoplan.workflow.ts +290 -40
  158. package/src/builtins/catalog.ts +6 -4
  159. package/src/builtins/index.ts +7 -1
  160. package/src/builtins/monitor.workflow.ts +663 -249
  161. package/src/builtins/plain-summary.workflow.ts +185 -0
  162. package/src/builtins/sanity-check.workflow.ts +62 -7
  163. package/src/controllers/effects.ts +7 -0
  164. package/src/controllers/index.ts +0 -6
  165. package/src/controllers/manager.ts +72 -32
  166. package/src/controllers/sqlite.ts +2420 -1948
  167. package/src/controllers/store.ts +17 -38
  168. package/src/controllers/types.ts +3 -0
  169. package/src/controllers/workflow-engine-scheduler.ts +4 -8
  170. package/src/controllers/workflows.ts +45 -21
  171. package/src/extension/controller-host.ts +1 -2
  172. package/src/extension/decision-channels.ts +427 -144
  173. package/src/extension/executor.ts +105 -2
  174. package/src/extension/herdr-viewer.ts +0 -5
  175. package/src/extension/index.ts +180 -118
  176. package/src/extension/recorder.ts +13 -22
  177. package/src/extension/step-message.ts +106 -4
  178. package/src/extension/widget.ts +8 -0
  179. package/src/host/rpc-executor.ts +4 -0
  180. package/src/host/runner.ts +93 -41
  181. package/src/render/graph-render.ts +11 -4
  182. package/src/state/database.ts +375 -0
  183. package/src/state/index.ts +29 -0
  184. package/src/state/json.ts +42 -0
  185. package/src/state/mutation.ts +435 -0
  186. package/src/state/schema.ts +475 -0
  187. package/src/viewer/cli.ts +151 -144
  188. package/src/viewer/render.ts +12 -24
  189. package/src/viewer/session-reducer.ts +1 -1
  190. package/src/viewer/tui.ts +18 -17
  191. package/src/viewer/watch.ts +13 -17
  192. package/src/workflows/composition.ts +12 -3
  193. package/src/workflows/definition.ts +36 -3
  194. package/src/workflows/engine.ts +212 -116
  195. package/src/workflows/errors.ts +3 -3
  196. package/src/workflows/human-decision.ts +795 -340
  197. package/src/workflows/index.ts +15 -19
  198. package/src/workflows/schema.ts +19 -1
  199. package/src/workflows/store.ts +1295 -1163
  200. package/src/workflows/types.ts +61 -81
  201. package/dist/workflows/artifacts.d.ts +0 -40
  202. package/dist/workflows/artifacts.js +0 -155
  203. package/dist/workflows/artifacts.js.map +0 -1
  204. package/dist/workflows/migrate-sources.d.ts +0 -42
  205. package/dist/workflows/migrate-sources.js +0 -133
  206. package/dist/workflows/migrate-sources.js.map +0 -1
  207. package/docs/run-bundles.md +0 -481
  208. package/src/workflows/artifacts.ts +0 -188
  209. package/src/workflows/migrate-sources.ts +0 -178
@@ -21,41 +21,32 @@ import type {
21
21
  } from "../workflows/types.js";
22
22
  import { validateProgressData } from "../workflows/updates.js";
23
23
  import autoimplementWorkflow, { type AutoimplementInput } from "./autoimplement.workflow.js";
24
- import { parsePlanApprovalPolicy, type PlanApprovalPolicy } from "./plan-approval.workflow.js";
24
+ import {
25
+ parsePlanApprovalPolicy,
26
+ type ResolvedPlanApprovalPolicy,
27
+ } from "./plan-approval.workflow.js";
25
28
  import planChangeWorkflow, { type NormalizedPlanChangeInput } from "./plan-change.workflow.js";
26
29
 
27
30
  const MIN_INTERVAL_MINUTES = 1;
28
31
  const MAX_INTERVAL_MINUTES = 24 * 60;
29
32
  const DEFAULT_INTERVAL_MINUTES = 30;
30
- const MIN_CHECK_TIMEOUT_MINUTES = 5;
31
- const MAX_CHECK_TIMEOUT_MINUTES = 24 * 60;
32
- const DEFAULT_MIN_CHECK_TIMEOUT_MINUTES = 60;
33
+ const DEFAULT_CHECK_TIMEOUT_MINUTES = 60;
33
34
  const DEFAULT_MAX_CHECKS = 1_000;
34
35
  const MAX_CHECKS = 1_000;
35
36
  const MAX_TRACKS = 256;
36
37
  const MAX_OBSERVATION_CHARS = 8_000;
37
38
  const MAX_REPORT_CHARS = 4_000;
38
39
  const MAX_REASON_CHARS = 2_000;
40
+ const MAX_ACTION_TEXT_CHARS = 8_000;
41
+ const MAX_ID_CHARS = 256;
39
42
  const SLEEP_TIMEOUT_MARGIN_MS = 60_000;
40
43
  const NODE_TIMEOUT_MARGIN_MS = 2 * 60_000;
41
44
 
42
- export type MonitorRepairPolicy = {
43
- authorized: true;
44
- scope?: string;
45
- constraints?: string[];
46
- repository?: string;
47
- baseBranch?: string;
48
- merge?: boolean;
49
- approval?: PlanApprovalPolicy;
50
- };
51
-
52
45
  export type MonitorInput = {
53
46
  task: string;
54
47
  everyMinutes?: number;
55
48
  stopWhen?: string;
56
49
  maxChecks?: number;
57
- checkTimeoutMinutes?: number;
58
- repair?: MonitorRepairPolicy;
59
50
  };
60
51
 
61
52
  type MonitorConfig = {
@@ -63,25 +54,78 @@ type MonitorConfig = {
63
54
  everyMinutes: number;
64
55
  stopWhen: string;
65
56
  maxChecks: number;
66
- checkTimeoutMinutes: number;
67
- repair?: MonitorRepairPolicy;
68
57
  };
69
- type MonitorRoute = "continue" | "repair" | "stop";
58
+ type MonitorRoute = "wait" | "act" | "stop";
59
+ type MonitorGoalState = "complete" | "incomplete" | "blocked";
60
+ type MonitorWorkState = "running" | "waiting" | "idle" | "failed" | "stopped" | "unknown";
61
+ type MonitorActionKind = "advance" | "recover" | "repair";
70
62
  type MonitorTrack = { key: string; data: WorkflowProgressData };
71
- type MonitorRepairRequest = {
72
- problem: string;
63
+ type MonitorAuthority = {
64
+ status: "authorized" | "outside";
65
+ basis: string;
66
+ allowedMutations: string[];
67
+ forbiddenMutations: string[];
68
+ costLimit: string;
69
+ providerRuntime: string;
70
+ requiredChecks: string[];
71
+ stopConditions: string[];
72
+ allowedRecoveryActions: string[];
73
+ repository?: string;
74
+ baseBranch?: string;
75
+ merge: boolean;
76
+ repairApproval: ResolvedPlanApprovalPolicy;
77
+ };
78
+ type MonitorCostSafety = {
79
+ paidAction: boolean;
80
+ status: "not-applicable" | "within-limit" | "missing" | "exceeded";
81
+ evidence: string;
82
+ };
83
+ type MonitorDefectSafety = {
84
+ sharedCodeOrDataDefect: boolean;
85
+ paidWorkers: "not-applicable" | "stopped" | "running";
86
+ evidence: string;
87
+ };
88
+ type MonitorActionRequest = {
89
+ kind: MonitorActionKind;
90
+ incomplete: string;
73
91
  evidence: unknown;
74
- issueFingerprint: string;
92
+ nextAction: string;
93
+ authority: MonitorAuthority;
94
+ cost: MonitorCostSafety;
95
+ defect: MonitorDefectSafety;
96
+ verification: string;
97
+ failureId: string;
98
+ targetStateId: string;
75
99
  };
76
- type MonitorCheck = {
100
+ export type MonitorObservation = {
77
101
  route: MonitorRoute;
102
+ goalState: MonitorGoalState;
103
+ workState: MonitorWorkState;
78
104
  observation: string;
79
105
  report: string;
106
+ targetStateId: string;
107
+ authorizedActions: string[];
80
108
  progress?: { tracks: MonitorTrack[] };
81
- repair?: MonitorRepairRequest;
109
+ action?: MonitorActionRequest;
82
110
  reason: string;
83
111
  };
112
+ type MonitorActionResult = {
113
+ status: "succeeded" | "failed" | "blocked";
114
+ summary: string;
115
+ evidence: unknown;
116
+ verification: string;
117
+ failureId: string;
118
+ targetStateId: string;
119
+ };
84
120
  type MonitorEstimate = { tracks: ProgressTrackState[] };
121
+ type RecordedAction = {
122
+ status: "succeeded" | "failed" | "blocked";
123
+ summary: string;
124
+ evidence: unknown;
125
+ verification: string;
126
+ failureId: string;
127
+ targetStateId: string;
128
+ };
85
129
 
86
130
  function requireRecord(value: unknown, label: string): Record<string, unknown> {
87
131
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
@@ -90,6 +134,15 @@ function requireRecord(value: unknown, label: string): Record<string, unknown> {
90
134
  return value as Record<string, unknown>;
91
135
  }
92
136
 
137
+ function requireExactKeys(
138
+ value: Record<string, unknown>,
139
+ allowed: readonly string[],
140
+ label: string,
141
+ ): void {
142
+ const unexpected = Object.keys(value).find((key) => !allowed.includes(key));
143
+ if (unexpected !== undefined) throw new Error(`${label} field ${unexpected} is not supported`);
144
+ }
145
+
93
146
  function requireBoundedString(value: unknown, label: string, maxChars: number): string {
94
147
  if (typeof value !== "string") throw new Error(`${label} must be a string`);
95
148
  const trimmed = value.trim();
@@ -98,6 +151,15 @@ function requireBoundedString(value: unknown, label: string, maxChars: number):
98
151
  return trimmed;
99
152
  }
100
153
 
154
+ function requireStringArray(value: unknown, label: string): string[] {
155
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
156
+ throw new Error(`${label} must be an array of strings`);
157
+ }
158
+ return value.map((item, index) =>
159
+ requireBoundedString(item, `${label}[${index}]`, MAX_ACTION_TEXT_CHARS),
160
+ );
161
+ }
162
+
101
163
  async function waitForUpdateSlot(signal: AbortSignal): Promise<void> {
102
164
  await new Promise<void>((resolve, reject) => {
103
165
  const onAbort = () => {
@@ -115,17 +177,8 @@ async function waitForUpdateSlot(signal: AbortSignal): Promise<void> {
115
177
 
116
178
  export function prepareMonitorInput(input: unknown): MonitorConfig {
117
179
  const value = requireRecord(input, "monitor input") as Partial<MonitorInput>;
118
- const allowed = new Set([
119
- "task",
120
- "everyMinutes",
121
- "stopWhen",
122
- "maxChecks",
123
- "checkTimeoutMinutes",
124
- "repair",
125
- ]);
126
- for (const field of Object.keys(value)) {
127
- if (!allowed.has(field)) throw new Error(`monitor input field ${field} is not supported`);
128
- }
180
+ const allowed = ["task", "everyMinutes", "stopWhen", "maxChecks"] as const;
181
+ requireExactKeys(value, allowed, "monitor input");
129
182
  const task = requireBoundedString(value.task, "task", 8_000);
130
183
  const everyMinutes = value.everyMinutes ?? DEFAULT_INTERVAL_MINUTES;
131
184
  if (
@@ -141,47 +194,6 @@ export function prepareMonitorInput(input: unknown): MonitorConfig {
141
194
  if (!Number.isInteger(maxChecks) || maxChecks <= 0 || maxChecks > MAX_CHECKS) {
142
195
  throw new Error(`maxChecks must be an integer from 1 through ${MAX_CHECKS}`);
143
196
  }
144
- const checkTimeoutMinutes =
145
- value.checkTimeoutMinutes ?? Math.max(DEFAULT_MIN_CHECK_TIMEOUT_MINUTES, everyMinutes);
146
- if (
147
- !Number.isInteger(checkTimeoutMinutes) ||
148
- checkTimeoutMinutes < MIN_CHECK_TIMEOUT_MINUTES ||
149
- checkTimeoutMinutes > MAX_CHECK_TIMEOUT_MINUTES
150
- ) {
151
- throw new Error(
152
- `checkTimeoutMinutes must be an integer from ${MIN_CHECK_TIMEOUT_MINUTES} through ${MAX_CHECK_TIMEOUT_MINUTES}`,
153
- );
154
- }
155
- let repair: MonitorRepairPolicy | undefined;
156
- if (value.repair !== undefined) {
157
- const raw = requireRecord(value.repair, "repair policy");
158
- if (raw.authorized !== true) throw new Error("repair policy must set authorized to true");
159
- if (
160
- raw.constraints !== undefined &&
161
- (!Array.isArray(raw.constraints) || raw.constraints.some((item) => typeof item !== "string"))
162
- ) {
163
- throw new Error("repair constraints must be an array of strings");
164
- }
165
- if (raw.merge !== undefined && typeof raw.merge !== "boolean") {
166
- throw new Error("repair merge must be a boolean");
167
- }
168
- const approval = parsePlanApprovalPolicy(raw.approval);
169
- repair = {
170
- authorized: true,
171
- ...(raw.scope !== undefined
172
- ? { scope: requireBoundedString(raw.scope, "repair scope", 4_000) }
173
- : {}),
174
- ...(raw.constraints !== undefined ? { constraints: [...raw.constraints] as string[] } : {}),
175
- ...(raw.repository !== undefined
176
- ? { repository: requireBoundedString(raw.repository, "repair repository", 4_000) }
177
- : {}),
178
- ...(raw.baseBranch !== undefined
179
- ? { baseBranch: requireBoundedString(raw.baseBranch, "repair base branch", 256) }
180
- : {}),
181
- ...(raw.merge !== undefined ? { merge: raw.merge !== false } : {}),
182
- approval,
183
- };
184
- }
185
197
  return {
186
198
  task,
187
199
  everyMinutes,
@@ -190,8 +202,6 @@ export function prepareMonitorInput(input: unknown): MonitorConfig {
190
202
  ? "Stop only when the user explicitly asks to stop."
191
203
  : requireBoundedString(value.stopWhen, "stopWhen", 4_000),
192
204
  maxChecks,
193
- checkTimeoutMinutes,
194
- ...(repair !== undefined ? { repair } : {}),
195
205
  };
196
206
  }
197
207
 
@@ -199,63 +209,305 @@ function configFrom(outputs: Record<string, unknown>): MonitorConfig {
199
209
  return outputs.prepare as MonitorConfig;
200
210
  }
201
211
 
202
- function completedChecks(context: WorkflowNodeContext): number {
203
- return context.state.steps.filter((step) => step.nodeId === "check" && step.outcome === "ok")
212
+ function completedObservations(context: WorkflowNodeContext): number {
213
+ return context.state.steps.filter((step) => step.nodeId === "observe" && step.outcome === "ok")
204
214
  .length;
205
215
  }
206
216
 
207
- export function validateMonitorCheck(output: unknown, repairAuthorized = false): MonitorCheck {
208
- const value = requireRecord(output, "monitor check output");
209
- const allowed = new Set(["route", "observation", "report", "progress", "repair", "reason"]);
210
- for (const key of Object.keys(value))
211
- if (!allowed.has(key)) throw new Error(`monitor check field ${key} is not supported`);
212
- if (value.route !== "continue" && value.route !== "repair" && value.route !== "stop") {
213
- throw new Error("route must be continue, repair, or stop");
217
+ function parseAuthority(input: unknown): MonitorAuthority {
218
+ const value = requireRecord(input, "action authority");
219
+ requireExactKeys(
220
+ value,
221
+ [
222
+ "status",
223
+ "basis",
224
+ "allowedMutations",
225
+ "forbiddenMutations",
226
+ "costLimit",
227
+ "providerRuntime",
228
+ "requiredChecks",
229
+ "stopConditions",
230
+ "allowedRecoveryActions",
231
+ "repository",
232
+ "baseBranch",
233
+ "merge",
234
+ "repairApproval",
235
+ ],
236
+ "action authority",
237
+ );
238
+ if (value.status !== "authorized" && value.status !== "outside") {
239
+ throw new Error("action authority status must be authorized or outside");
240
+ }
241
+ if (typeof value.merge !== "boolean") throw new Error("action authority merge must be boolean");
242
+ const approval =
243
+ value.repairApproval === undefined
244
+ ? undefined
245
+ : requireRecord(value.repairApproval, "repair approval");
246
+ return {
247
+ status: value.status,
248
+ basis: requireBoundedString(value.basis, "action authority basis", MAX_ACTION_TEXT_CHARS),
249
+ allowedMutations: requireStringArray(
250
+ value.allowedMutations,
251
+ "action authority allowedMutations",
252
+ ),
253
+ forbiddenMutations: requireStringArray(
254
+ value.forbiddenMutations,
255
+ "action authority forbiddenMutations",
256
+ ),
257
+ costLimit: requireBoundedString(
258
+ value.costLimit,
259
+ "action authority costLimit",
260
+ MAX_ACTION_TEXT_CHARS,
261
+ ),
262
+ providerRuntime: requireBoundedString(
263
+ value.providerRuntime,
264
+ "action authority providerRuntime",
265
+ MAX_ACTION_TEXT_CHARS,
266
+ ),
267
+ requiredChecks: requireStringArray(value.requiredChecks, "action authority requiredChecks"),
268
+ stopConditions: requireStringArray(value.stopConditions, "action authority stopConditions"),
269
+ allowedRecoveryActions: requireStringArray(
270
+ value.allowedRecoveryActions,
271
+ "action authority allowedRecoveryActions",
272
+ ),
273
+ ...(value.repository !== undefined
274
+ ? {
275
+ repository: requireBoundedString(value.repository, "action authority repository", 4_000),
276
+ }
277
+ : {}),
278
+ ...(value.baseBranch !== undefined
279
+ ? {
280
+ baseBranch: requireBoundedString(value.baseBranch, "action authority baseBranch", 256),
281
+ }
282
+ : {}),
283
+ merge: value.merge,
284
+ repairApproval: parsePlanApprovalPolicy(approval),
285
+ };
286
+ }
287
+
288
+ function parseCostSafety(input: unknown): MonitorCostSafety {
289
+ const value = requireRecord(input, "action cost");
290
+ requireExactKeys(value, ["paidAction", "status", "evidence"], "action cost");
291
+ if (typeof value.paidAction !== "boolean")
292
+ throw new Error("action cost paidAction must be boolean");
293
+ if (
294
+ value.status !== "not-applicable" &&
295
+ value.status !== "within-limit" &&
296
+ value.status !== "missing" &&
297
+ value.status !== "exceeded"
298
+ ) {
299
+ throw new Error("action cost status is invalid");
300
+ }
301
+ return {
302
+ paidAction: value.paidAction,
303
+ status: value.status,
304
+ evidence: requireBoundedString(value.evidence, "action cost evidence", MAX_ACTION_TEXT_CHARS),
305
+ };
306
+ }
307
+
308
+ function parseDefectSafety(input: unknown): MonitorDefectSafety {
309
+ const value = requireRecord(input, "action defect");
310
+ requireExactKeys(value, ["sharedCodeOrDataDefect", "paidWorkers", "evidence"], "action defect");
311
+ if (typeof value.sharedCodeOrDataDefect !== "boolean") {
312
+ throw new Error("action defect sharedCodeOrDataDefect must be boolean");
313
+ }
314
+ if (
315
+ value.paidWorkers !== "not-applicable" &&
316
+ value.paidWorkers !== "stopped" &&
317
+ value.paidWorkers !== "running"
318
+ ) {
319
+ throw new Error("action defect paidWorkers is invalid");
320
+ }
321
+ return {
322
+ sharedCodeOrDataDefect: value.sharedCodeOrDataDefect,
323
+ paidWorkers: value.paidWorkers,
324
+ evidence: requireBoundedString(value.evidence, "action defect evidence", MAX_ACTION_TEXT_CHARS),
325
+ };
326
+ }
327
+
328
+ function parseActionRequest(input: unknown): MonitorActionRequest {
329
+ const value = requireRecord(input, "monitor action request");
330
+ requireExactKeys(
331
+ value,
332
+ [
333
+ "kind",
334
+ "incomplete",
335
+ "evidence",
336
+ "nextAction",
337
+ "authority",
338
+ "cost",
339
+ "defect",
340
+ "verification",
341
+ "failureId",
342
+ "targetStateId",
343
+ ],
344
+ "monitor action request",
345
+ );
346
+ if (value.kind !== "advance" && value.kind !== "recover" && value.kind !== "repair") {
347
+ throw new Error("monitor action kind must be advance, recover, or repair");
348
+ }
349
+ const request: MonitorActionRequest = {
350
+ kind: value.kind,
351
+ incomplete: requireBoundedString(
352
+ value.incomplete,
353
+ "monitor action incomplete work",
354
+ MAX_ACTION_TEXT_CHARS,
355
+ ),
356
+ evidence: value.evidence ?? null,
357
+ nextAction: requireBoundedString(
358
+ value.nextAction,
359
+ "monitor action nextAction",
360
+ MAX_ACTION_TEXT_CHARS,
361
+ ),
362
+ authority: parseAuthority(value.authority),
363
+ cost: parseCostSafety(value.cost),
364
+ defect: parseDefectSafety(value.defect),
365
+ verification: requireBoundedString(
366
+ value.verification,
367
+ "monitor action verification",
368
+ MAX_ACTION_TEXT_CHARS,
369
+ ),
370
+ failureId: requireBoundedString(value.failureId, "monitor action failureId", MAX_ID_CHARS),
371
+ targetStateId: requireBoundedString(
372
+ value.targetStateId,
373
+ "monitor action targetStateId",
374
+ MAX_ID_CHARS,
375
+ ),
376
+ };
377
+ if (request.authority.status !== "authorized") {
378
+ throw new Error("route act requires action authority status authorized");
379
+ }
380
+ if (request.authority.allowedMutations.length === 0) {
381
+ throw new Error("route act requires at least one allowed mutation");
382
+ }
383
+ if (
384
+ request.cost.paidAction &&
385
+ (request.cost.status === "missing" || request.cost.status === "exceeded")
386
+ ) {
387
+ throw new Error("route act cannot launch paid work without verified remaining authority");
388
+ }
389
+ if (request.cost.paidAction && request.cost.status !== "within-limit") {
390
+ throw new Error("paid route act requires cost status within-limit");
391
+ }
392
+ if (!request.cost.paidAction && request.cost.status !== "not-applicable") {
393
+ throw new Error("unpaid route act requires cost status not-applicable");
394
+ }
395
+ if (
396
+ request.kind === "repair" &&
397
+ request.defect.sharedCodeOrDataDefect &&
398
+ request.defect.paidWorkers === "running"
399
+ ) {
400
+ throw new Error("paid workers must stop before a shared code or data repair");
401
+ }
402
+ return request;
403
+ }
404
+
405
+ export function validateMonitorObservation(output: unknown): MonitorObservation {
406
+ const value = requireRecord(output, "monitor observation output");
407
+ requireExactKeys(
408
+ value,
409
+ [
410
+ "route",
411
+ "goalState",
412
+ "workState",
413
+ "observation",
414
+ "report",
415
+ "targetStateId",
416
+ "authorizedActions",
417
+ "progress",
418
+ "action",
419
+ "reason",
420
+ ],
421
+ "monitor observation",
422
+ );
423
+ if (value.route !== "wait" && value.route !== "act" && value.route !== "stop") {
424
+ throw new Error("route must be wait, act, or stop");
425
+ }
426
+ if (
427
+ value.goalState !== "complete" &&
428
+ value.goalState !== "incomplete" &&
429
+ value.goalState !== "blocked"
430
+ ) {
431
+ throw new Error("goalState must be complete, incomplete, or blocked");
214
432
  }
215
- if (value.route === "repair" && !repairAuthorized) {
216
- throw new Error("route repair requires explicit monitor repair authorization");
433
+ if (
434
+ value.workState !== "running" &&
435
+ value.workState !== "waiting" &&
436
+ value.workState !== "idle" &&
437
+ value.workState !== "failed" &&
438
+ value.workState !== "stopped" &&
439
+ value.workState !== "unknown"
440
+ ) {
441
+ throw new Error("workState is invalid");
217
442
  }
218
- const check: MonitorCheck = {
443
+ const observation: MonitorObservation = {
219
444
  route: value.route,
445
+ goalState: value.goalState,
446
+ workState: value.workState,
220
447
  observation: requireBoundedString(value.observation, "observation", MAX_OBSERVATION_CHARS),
221
448
  report: requireBoundedString(value.report, "report", MAX_REPORT_CHARS),
449
+ targetStateId: requireBoundedString(value.targetStateId, "targetStateId", MAX_ID_CHARS),
450
+ authorizedActions: requireStringArray(value.authorizedActions, "authorizedActions"),
222
451
  reason: requireBoundedString(value.reason, "reason", MAX_REASON_CHARS),
223
452
  };
224
- if (value.progress !== undefined) check.progress = validateMonitorProgress(value.progress);
225
- if (value.repair !== undefined) {
226
- const repair = requireRecord(value.repair, "monitor repair request");
227
- check.repair = {
228
- problem: requireBoundedString(repair.problem, "repair problem", 8_000),
229
- evidence: repair.evidence ?? null,
230
- issueFingerprint: requireBoundedString(
231
- repair.issueFingerprint,
232
- "repair issue fingerprint",
233
- 256,
234
- ),
235
- };
453
+ if (value.progress !== undefined) observation.progress = validateMonitorProgress(value.progress);
454
+ if (value.action !== undefined) observation.action = parseActionRequest(value.action);
455
+ if (observation.route === "act" && observation.action === undefined) {
456
+ throw new Error("route act requires action details");
457
+ }
458
+ if (
459
+ observation.action !== undefined &&
460
+ observation.action.targetStateId !== observation.targetStateId
461
+ ) {
462
+ throw new Error("monitor action targetStateId must match the observed targetStateId");
463
+ }
464
+ if (observation.route !== "act" && observation.action !== undefined) {
465
+ throw new Error("action details are only valid for route act");
236
466
  }
237
- if (value.route === "repair" && check.repair === undefined) {
238
- throw new Error("route repair requires repair details");
467
+ if (observation.route === "act" && observation.goalState !== "incomplete") {
468
+ throw new Error("route act requires goalState incomplete");
239
469
  }
240
- return check;
470
+ if (
471
+ observation.route === "act" &&
472
+ observation.workState !== "idle" &&
473
+ observation.workState !== "failed" &&
474
+ observation.workState !== "stopped"
475
+ ) {
476
+ throw new Error("route act requires idle, failed, or stopped work");
477
+ }
478
+ if (observation.route === "wait" && observation.goalState !== "incomplete") {
479
+ throw new Error("route wait requires goalState incomplete");
480
+ }
481
+ if (
482
+ observation.route === "wait" &&
483
+ observation.workState !== "running" &&
484
+ observation.workState !== "waiting"
485
+ ) {
486
+ throw new Error("route wait requires running work or an external wait");
487
+ }
488
+ if (observation.goalState === "complete" && observation.route !== "stop") {
489
+ throw new Error("goalState complete requires route stop");
490
+ }
491
+ if (observation.goalState === "blocked" && observation.route !== "stop") {
492
+ throw new Error("goalState blocked requires route stop");
493
+ }
494
+ return observation;
241
495
  }
242
496
 
243
497
  function validateMonitorProgress(input: unknown): { tracks: MonitorTrack[] } {
244
498
  const value = requireRecord(input, "progress");
245
- if (Object.keys(value).some((key) => key !== "tracks"))
246
- throw new Error("progress only supports tracks");
499
+ requireExactKeys(value, ["tracks"], "progress");
247
500
  if (!Array.isArray(value.tracks) || value.tracks.length < 1 || value.tracks.length > MAX_TRACKS) {
248
501
  throw new Error(`progress.tracks must contain 1 through ${MAX_TRACKS} entries`);
249
502
  }
250
503
  const keys = new Set<string>();
251
504
  const tracks = value.tracks.map((raw, index) => {
252
505
  const track = requireRecord(raw, `progress.tracks[${index}]`);
253
- if (Object.keys(track).some((key) => key !== "key" && key !== "data")) {
254
- throw new Error(`progress.tracks[${index}] has an unsupported field`);
255
- }
506
+ requireExactKeys(track, ["key", "data"], `progress.tracks[${index}]`);
256
507
  const key = requireBoundedString(track.key, `progress.tracks[${index}].key`, 128);
257
- if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(key))
508
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(key)) {
258
509
  throw new Error(`progress.tracks[${index}].key is invalid`);
510
+ }
259
511
  if (keys.has(key)) throw new Error(`progress track key ${key} is duplicated`);
260
512
  keys.add(key);
261
513
  return {
@@ -266,14 +518,18 @@ function validateMonitorProgress(input: unknown): { tracks: MonitorTrack[] } {
266
518
  return { tracks };
267
519
  }
268
520
 
521
+ function currentObservation(outputs: Record<string, unknown>): MonitorObservation {
522
+ return (outputs.guard ?? outputs.observe) as MonitorObservation;
523
+ }
524
+
269
525
  function estimateTracks(outputs: Record<string, unknown>): MonitorEstimate {
270
- const check = outputs.check as MonitorCheck;
271
- if (check.progress === undefined) return { tracks: [] };
526
+ const observation = currentObservation(outputs);
527
+ if (observation.progress === undefined) return { tracks: [] };
272
528
  const previous = outputs.estimate as MonitorEstimate | undefined;
273
529
  const previousByKey = new Map((previous?.tracks ?? []).map((track) => [track.key, track]));
274
530
  const at = new Date().toISOString();
275
531
  return {
276
- tracks: check.progress.tracks.map((track) => {
532
+ tracks: observation.progress.tracks.map((track) => {
277
533
  const samples: ProgressSample[] = [
278
534
  ...(previousByKey.get(track.key)?.samples ?? []),
279
535
  { at, data: track.data },
@@ -283,24 +539,93 @@ function estimateTracks(outputs: Record<string, unknown>): MonitorEstimate {
283
539
  };
284
540
  }
285
541
 
286
- function repeatedRepairWithoutProgress(context: WorkflowNodeContext): boolean {
287
- const current = context.outputs.check as MonitorCheck;
288
- const fingerprint = current.repair?.issueFingerprint;
289
- if (fingerprint === undefined) return false;
290
- const steps = context.state.steps;
291
- const currentCheckIndex = steps.findLastIndex((step) => step.nodeId === "check");
292
- for (let index = currentCheckIndex - 1; index >= 0; index -= 1) {
293
- const step = steps[index];
294
- if (step?.nodeId !== "check") continue;
295
- const prior = step.output as MonitorCheck;
296
- if (prior.repair?.issueFingerprint !== fingerprint) continue;
297
- return steps
298
- .slice(index + 1, currentCheckIndex)
299
- .some((candidate) => candidate.nodeId === "implementation");
542
+ function actionFrom(outputs: Record<string, unknown>): MonitorActionRequest {
543
+ const request = currentObservation(outputs).action;
544
+ if (request === undefined) throw new Error("monitor action details are missing");
545
+ return request;
546
+ }
547
+
548
+ function repeatedCompletedRepair(context: WorkflowNodeContext): boolean {
549
+ const action = (context.outputs.observe as MonitorObservation).action;
550
+ if (action?.kind !== "repair") return false;
551
+ const currentIndex = context.state.steps.findLastIndex((step) => step.nodeId === "observe");
552
+ for (let index = currentIndex - 1; index >= 0; index -= 1) {
553
+ const step = context.state.steps[index];
554
+ if (step?.nodeId !== "observe") continue;
555
+ const prior = step.output as MonitorObservation;
556
+ if (
557
+ prior.action?.kind !== "repair" ||
558
+ prior.action.failureId !== action.failureId ||
559
+ prior.action.targetStateId !== action.targetStateId
560
+ ) {
561
+ continue;
562
+ }
563
+ return context.state.steps
564
+ .slice(index + 1, currentIndex)
565
+ .some((candidate) => candidate.nodeId === "repairComplete" && candidate.outcome === "ok");
300
566
  }
301
567
  return false;
302
568
  }
303
569
 
570
+ function guardObservation(context: WorkflowNodeContext): MonitorObservation {
571
+ const observation = context.outputs.observe as MonitorObservation;
572
+ if (!repeatedCompletedRepair(context)) return observation;
573
+ const action = observation.action;
574
+ if (action === undefined) return observation;
575
+ return {
576
+ route: "stop",
577
+ goalState: "blocked",
578
+ workState: observation.workState,
579
+ observation: observation.observation,
580
+ report: "The same failure and target state returned after one completed repair.",
581
+ targetStateId: observation.targetStateId,
582
+ authorizedActions: observation.authorizedActions,
583
+ ...(observation.progress !== undefined ? { progress: observation.progress } : {}),
584
+ reason: `Repair stopped because ${action.failureId} returned in target state ${action.targetStateId}.`,
585
+ };
586
+ }
587
+
588
+ export function validateMonitorActionResult(
589
+ output: unknown,
590
+ expected: Pick<MonitorActionRequest, "failureId" | "targetStateId">,
591
+ ): MonitorActionResult {
592
+ const value = requireRecord(output, "monitor action result");
593
+ requireExactKeys(
594
+ value,
595
+ ["status", "summary", "evidence", "verification", "failureId", "targetStateId"],
596
+ "monitor action result",
597
+ );
598
+ if (value.status !== "succeeded" && value.status !== "failed" && value.status !== "blocked") {
599
+ throw new Error("monitor action result status must be succeeded, failed, or blocked");
600
+ }
601
+ const result: MonitorActionResult = {
602
+ status: value.status,
603
+ summary: requireBoundedString(value.summary, "monitor action result summary", MAX_REPORT_CHARS),
604
+ evidence: value.evidence ?? null,
605
+ verification: requireBoundedString(
606
+ value.verification,
607
+ "monitor action result verification",
608
+ MAX_ACTION_TEXT_CHARS,
609
+ ),
610
+ failureId: requireBoundedString(
611
+ value.failureId,
612
+ "monitor action result failureId",
613
+ MAX_ID_CHARS,
614
+ ),
615
+ targetStateId: requireBoundedString(
616
+ value.targetStateId,
617
+ "monitor action result targetStateId",
618
+ MAX_ID_CHARS,
619
+ ),
620
+ };
621
+ if (result.failureId !== expected.failureId || result.targetStateId !== expected.targetStateId) {
622
+ throw new Error(
623
+ "monitor action result must preserve the requested failure and target-state IDs",
624
+ );
625
+ }
626
+ return result;
627
+ }
628
+
304
629
  function currentRepairPlan(outputs: Record<string, unknown>): {
305
630
  plan: unknown;
306
631
  planDigest: string;
@@ -315,9 +640,31 @@ function currentRepairPlan(outputs: Record<string, unknown>): {
315
640
  };
316
641
  }
317
642
 
643
+ function authorityScope(actionRequest: MonitorActionRequest): string {
644
+ const authority = actionRequest.authority;
645
+ return [
646
+ `Authorization basis: ${authority.basis}`,
647
+ `Allowed mutations: ${authority.allowedMutations.join("; ")}`,
648
+ `Forbidden mutations: ${authority.forbiddenMutations.join("; ") || "none recorded"}`,
649
+ `Cost limit: ${authority.costLimit}`,
650
+ `Provider/runtime contract: ${authority.providerRuntime}`,
651
+ `Allowed recovery actions: ${authority.allowedRecoveryActions.join("; ") || "none recorded"}`,
652
+ ].join("\n");
653
+ }
654
+
655
+ function authorityConstraints(actionRequest: MonitorActionRequest): string[] {
656
+ const authority = actionRequest.authority;
657
+ return [
658
+ ...authority.forbiddenMutations.map((item) => `Forbidden: ${item}`),
659
+ `Cost limit: ${authority.costLimit}`,
660
+ `Provider/runtime contract: ${authority.providerRuntime}`,
661
+ ...authority.requiredChecks.map((item) => `Required check: ${item}`),
662
+ ...authority.stopConditions.map((item) => `Stop condition: ${item}`),
663
+ `Repair verification: ${actionRequest.verification}`,
664
+ ];
665
+ }
666
+
318
667
  function repairBlockedReason(outputs: Record<string, unknown>): string {
319
- const guard = outputs.repairGuard as { reason?: string; route?: string } | undefined;
320
- if (guard?.route === "blocked" && guard.reason !== undefined) return guard.reason;
321
668
  const planChange = outputs.planChange as
322
669
  | { exit?: string; output?: { reason?: string } }
323
670
  | undefined;
@@ -327,36 +674,81 @@ function repairBlockedReason(outputs: Record<string, unknown>): string {
327
674
  return (
328
675
  implementation?.output?.reason ??
329
676
  planChange?.output?.reason ??
330
- "The repair did not produce new verified progress."
677
+ "The repair did not produce a verified result."
331
678
  );
332
679
  }
333
680
 
681
+ function recordedActionFromStep(
682
+ step: WorkflowNodeContext["state"]["steps"][number],
683
+ ): RecordedAction | undefined {
684
+ if (step.outcome !== "ok") return undefined;
685
+ if (step.nodeId === "act") {
686
+ const result = step.output as MonitorActionResult;
687
+ return {
688
+ status: result.status,
689
+ summary: result.summary,
690
+ evidence: result.evidence,
691
+ verification: result.verification,
692
+ failureId: result.failureId,
693
+ targetStateId: result.targetStateId,
694
+ };
695
+ }
696
+ if (step.nodeId === "repairComplete") return step.output as RecordedAction;
697
+ return undefined;
698
+ }
699
+
700
+ function latestRecordedAction(context: WorkflowNodeContext): RecordedAction | undefined {
701
+ for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
702
+ const step = context.state.steps[index];
703
+ if (step === undefined) continue;
704
+ const recorded = recordedActionFromStep(step);
705
+ if (recorded !== undefined) return recorded;
706
+ }
707
+ return undefined;
708
+ }
709
+
334
710
  function reportMessage(context: WorkflowNodeContext): string {
335
- const check = context.outputs.check as MonitorCheck;
711
+ const observation = currentObservation(context.outputs);
336
712
  const estimate = context.outputs.estimate as MonitorEstimate;
337
713
  const config = configFrom(context.outputs);
338
- const suffix: string[] = [];
339
- if (estimate.tracks.length > 0) {
340
- suffix.push(
341
- formatProgressReport(
342
- estimate.tracks.map((track) => track.estimate),
343
- check.route === "continue" ? config.everyMinutes : undefined,
344
- new Date(),
345
- 2_000,
346
- ),
347
- );
714
+ const progress =
715
+ estimate.tracks.length === 0
716
+ ? "Progress: unavailable"
717
+ : formatProgressReport(
718
+ estimate.tracks.map((track) => track.estimate),
719
+ undefined,
720
+ new Date(),
721
+ 2_000,
722
+ );
723
+ const lines = [
724
+ `Monitor: ${observation.route === "stop" ? "stopping" : "active"}`,
725
+ `Goal: ${observation.goalState}`,
726
+ `Work: ${observation.workState}`,
727
+ progress,
728
+ ];
729
+ const lastAction = latestRecordedAction(context);
730
+ if (lastAction !== undefined) lines.push(`Last action: ${lastAction.summary}`);
731
+ if (observation.action !== undefined && completedObservations(context) < config.maxChecks) {
732
+ lines.push(`Next action: ${observation.action.nextAction}`);
348
733
  }
349
- if (check.route === "continue" && completedChecks(context) >= config.maxChecks) {
350
- suffix.push(`Reached the ${config.maxChecks}-check safety limit.`);
351
- }
352
- const suffixText = suffix.filter(Boolean).join("\n");
353
- if (suffixText.length === 0) return check.report;
354
- const reportBudget = Math.max(1, MAX_REPORT_CHARS - suffixText.length - 1);
355
- const report =
356
- check.report.length <= reportBudget
357
- ? check.report
358
- : `${check.report.slice(0, Math.max(0, reportBudget - 1))}…`;
359
- return `${report}\n${suffixText}`;
734
+ if (observation.route === "wait" && completedObservations(context) < config.maxChecks) {
735
+ lines.push(`Next check: ${config.everyMinutes} minutes`);
736
+ }
737
+ if (completedObservations(context) >= config.maxChecks && observation.route !== "stop") {
738
+ lines.push(`Safety limit: reached ${config.maxChecks} observations`);
739
+ }
740
+ lines.push(`Status: ${observation.report}`);
741
+ const message = lines.join("\n");
742
+ return message.length <= MAX_REPORT_CHARS
743
+ ? message
744
+ : `${message.slice(0, MAX_REPORT_CHARS - 1)}…`;
745
+ }
746
+
747
+ function previousActionPrompt(context: WorkflowNodeContext): string {
748
+ const actionRecord = latestRecordedAction(context);
749
+ return actionRecord === undefined
750
+ ? "There is no previous completed action."
751
+ : `Previous action result: ${JSON.stringify(actionRecord)}`;
360
752
  }
361
753
 
362
754
  const monitorWorkflow: WorkflowDefinition = defineWorkflow({
@@ -376,26 +768,23 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
376
768
  includes: {
377
769
  planChange: includeWorkflow(planChangeWorkflow, {
378
770
  input: ({ outputs }): NormalizedPlanChangeInput => {
379
- const config = configFrom(outputs);
380
- const repair = (outputs.check as MonitorCheck).repair;
381
- if (repair === undefined) throw new Error("monitor repair details are missing");
382
- const prior = outputs.planChange as
383
- | { exit?: string; output?: { plan?: unknown } }
384
- | undefined;
771
+ const request = actionFrom(outputs);
772
+ if (request.kind !== "repair") throw new Error("monitor repair action is missing");
385
773
  return {
386
- task: repair.problem,
387
- ...(config.repair?.scope !== undefined ? { scope: config.repair.scope } : {}),
388
- ...(config.repair?.constraints !== undefined
389
- ? { constraints: config.repair.constraints }
390
- : {}),
391
- ...(config.repair?.repository !== undefined
392
- ? { repository: config.repair.repository }
393
- : {}),
394
- ...(prior?.exit === "ready" && prior.output?.plan !== undefined
395
- ? { previousPlan: prior.output.plan }
774
+ task: request.nextAction,
775
+ scope: authorityScope(request),
776
+ constraints: authorityConstraints(request),
777
+ ...(request.authority.repository !== undefined
778
+ ? { repository: request.authority.repository }
396
779
  : {}),
397
- newEvidence: repair.evidence,
398
- approval: parsePlanApprovalPolicy(config.repair?.approval),
780
+ newEvidence: {
781
+ incomplete: request.incomplete,
782
+ evidence: request.evidence,
783
+ failureId: request.failureId,
784
+ targetStateId: request.targetStateId,
785
+ defect: request.defect,
786
+ },
787
+ approval: request.authority.repairApproval,
399
788
  };
400
789
  },
401
790
  }),
@@ -403,120 +792,143 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
403
792
  workflow: "autoimplement",
404
793
  contract: autoimplementWorkflow,
405
794
  input: ({ outputs }) => {
406
- const config = configFrom(outputs);
407
- const repair = (outputs.check as MonitorCheck).repair;
795
+ const request = actionFrom(outputs);
408
796
  const documented = currentRepairPlan(outputs);
409
- if (repair === undefined) throw new Error("monitor repair details are missing");
410
- const request: AutoimplementInput = {
411
- task: repair.problem,
797
+ if (request.kind !== "repair") throw new Error("monitor repair action is missing");
798
+ const implementationInput: AutoimplementInput = {
799
+ task: request.nextAction,
412
800
  plan: documented.plan,
413
801
  documentation: {
414
802
  status: "current",
415
803
  planDigest: documented.planDigest,
416
804
  documents: documented.documents,
417
805
  },
418
- ...(config.repair?.scope !== undefined ? { scope: config.repair.scope } : {}),
419
- ...(config.repair?.constraints !== undefined
420
- ? { constraints: config.repair.constraints }
806
+ scope: authorityScope(request),
807
+ constraints: authorityConstraints(request),
808
+ ...(request.authority.repository !== undefined
809
+ ? { repository: request.authority.repository }
421
810
  : {}),
422
- ...(config.repair?.repository !== undefined
423
- ? { repository: config.repair.repository }
811
+ ...(request.authority.baseBranch !== undefined
812
+ ? { baseBranch: request.authority.baseBranch }
424
813
  : {}),
425
- ...(config.repair?.baseBranch !== undefined
426
- ? { baseBranch: config.repair.baseBranch }
427
- : {}),
428
- approval: parsePlanApprovalPolicy(config.repair?.approval),
429
- merge: config.repair?.merge === true,
814
+ approval: request.authority.repairApproval,
815
+ merge: request.authority.merge,
430
816
  };
431
- return request;
817
+ return implementationInput;
432
818
  },
433
819
  }),
434
820
  },
435
821
  nodes: {
436
822
  prepare: compute({ run: ({ input }) => prepareMonitorInput(input) }),
437
- check: agent({
438
- statusDetail: "checking monitored target",
439
- timeoutMs: ({ outputs }) => configFrom(outputs).checkTimeoutMinutes * 60_000,
823
+ observe: agent({
824
+ statusDetail: "observing monitored goal",
825
+ timeoutMs: DEFAULT_CHECK_TIMEOUT_MINUTES * 60_000,
440
826
  prompt: (context) => {
441
827
  const config = configFrom(context.outputs);
442
- const previous = context.outputs.check as MonitorCheck | undefined;
828
+ const previous = context.outputs.observe as MonitorObservation | undefined;
443
829
  const priorEstimate = context.outputs.estimate as MonitorEstimate | undefined;
444
830
  return [
445
- `Perform monitoring check ${completedChecks(context) + 1} of at most ${config.maxChecks}.`,
446
- `Task: ${config.task}`,
831
+ `Perform read-only observation ${completedObservations(context) + 1} of at most ${config.maxChecks}.`,
832
+ `Goal and authority: ${config.task}`,
447
833
  `Stop when: ${config.stopWhen}`,
834
+ "Use normal read-only tools to inspect authoritative target state, durable outputs, failures, checkpoints, active work, and applicable authority. Do not mutate any file, process, Job, service, remote resource, or configuration during this step.",
835
+ "Preserve the exact goal, allowed files and systems, forbidden changes, cost ceiling, provider/runtime contract, required checks, stop conditions, and allowed recovery actions from the task, conversation, and repository instructions. Do not invent permission.",
836
+ "List the safe actions the user has already authorized, even when no action is needed now. Choose wait only when useful target work is moving or an external event must finish. Choose act only when the goal is incomplete, work is idle, failed, or stopped, and one safe action is fully authorized. Choose stop when the goal is complete or safe continuation is blocked.",
837
+ "For act, describe one exact action. Use advance for normal next work, recover for an operational restart or resume, and repair only for a code or configuration defect. A normal start, resume, or restart must not become repair. If a paid action lacks verified remaining authority or exceeds its limit, choose stop. Before repair of a shared code or data defect, affected paid workers must already be stopped.",
838
+ "You are the regular Pi model and observation adapter. Publish measured facts with workflow update when useful. Do not require the target to implement a Pi-specific API, file, store, schema, command, service, transport, or dependency. Do not invent counts, rates, or ETA values.",
448
839
  previous === undefined
449
- ? "There is no previous observation."
450
- : `Previous accepted observation: ${previous.observation}`,
840
+ ? "There is no previous accepted observation."
841
+ : `Previous accepted observation: ${JSON.stringify(previous)}`,
842
+ previousActionPrompt(context),
451
843
  priorEstimate?.tracks.length
452
844
  ? `Previous progress: ${formatProgressReport(priorEstimate.tracks.map((track) => track.estimate))}`
453
845
  : "There is no previous measured progress.",
454
- config.repair === undefined
455
- ? "Observe only. This monitor has no mutation authorization."
456
- : "Repair is explicitly authorized within the supplied repair policy. Choose repair only for a concrete issue that can be changed within that scope. Include a stable issue fingerprint based on the issue and observed target state. Do not change protected model, benchmark, credential, hardware, spending, or scope decisions.",
457
- "Use available tools to inspect the current source of truth.",
458
- "You are the regular Pi model running this check and the observation adapter. When useful measurable facts appear during the check, publish them with workflow action update. Include the latest tracks in the final submission. Do not require the monitored target to implement a Pi-specific progress API, file, store, schema, or command.",
459
- "Every accepted check must include a concise user-facing report. Add progress tracks only when the target provides measurable facts. Submit observed counts and target-provided finish times; do not invent rates or an ETA.",
460
- config.repair === undefined
461
- ? "Choose route continue or stop."
462
- : "Choose route continue, repair, or stop.",
463
846
  ].join("\n\n");
464
847
  },
465
848
  expectedOutput:
466
- '{ "route": "continue" | "repair" | "stop", "observation": "current factual state", "report": "concise status update", "progress": { "tracks": [{ "key": "stable-key", "data": { "schema": "pi-workflows.progress.v1", "status": "running", "completed": 1, "total": 2, "unit": "items" } }] } (optional), "repair": { "problem": "fixable issue", "evidence": "observed evidence", "issueFingerprint": "stable issue and target-state fingerprint" } (required for repair), "reason": "short reason" }',
467
- validate: (output, context) =>
468
- validateMonitorCheck(output, configFrom(context.outputs).repair !== undefined),
849
+ '{ "route": "wait" | "act" | "stop", "goalState": "complete" | "incomplete" | "blocked", "workState": "running" | "waiting" | "idle" | "failed" | "stopped" | "unknown", "observation": "factual state", "report": "concise factual summary", "targetStateId": "stable observed target-state ID", "authorizedActions": ["safe action already authorized by the user"], "progress": { "tracks": [{ "key": "stable-key", "data": { "schema": "pi-workflows.progress.v1", "status": "running", "completed": 1, "total": 2, "unit": "items" } }] } (optional), "action": { "kind": "advance" | "recover" | "repair", "incomplete": "what remains", "evidence": {}, "nextAction": "one exact action", "authority": { "status": "authorized", "basis": "existing authority", "allowedMutations": ["allowed file, system, or resource"], "forbiddenMutations": [], "costLimit": "recorded limit or not applicable", "providerRuntime": "recorded contract or not applicable", "requiredChecks": [], "stopConditions": [], "allowedRecoveryActions": [], "repository": "optional absolute path", "baseBranch": "optional branch", "merge": false, "repairApproval": { "mode": "auto" | "required" | "skip" } }, "cost": { "paidAction": false, "status": "not-applicable" | "within-limit", "evidence": "cost evidence" }, "defect": { "sharedCodeOrDataDefect": false, "paidWorkers": "not-applicable" | "stopped" | "running", "evidence": "worker evidence" }, "verification": "how to prove success", "failureId": "stable failure ID", "targetStateId": "stable target-state ID" } (required only for act), "reason": "short reason" }',
850
+ validate: validateMonitorObservation,
469
851
  }),
852
+ guard: compute({ run: guardObservation }),
470
853
  estimate: compute({ run: ({ outputs }) => estimateTracks(outputs) }),
471
854
  publish_progress: action({
472
855
  statusDetail: "publishing monitor progress",
473
856
  run: async ({ outputs, publishUpdate, signal }) => {
474
- const check = outputs.check as MonitorCheck;
475
- if (check.progress === undefined) return { published: 0 };
476
- for (const track of check.progress.tracks) {
857
+ const observation = currentObservation(outputs);
858
+ if (observation.progress === undefined) return { published: 0 };
859
+ for (const track of observation.progress.tracks) {
477
860
  await waitForUpdateSlot(signal);
478
861
  await publishUpdate({ type: "progress", key: track.key, data: track.data });
479
862
  }
480
- return { published: check.progress.tracks.length };
863
+ return { published: observation.progress.tracks.length };
481
864
  },
482
865
  }),
483
866
  report: notify({
484
867
  statusDetail: "queueing monitor update",
485
- message: (context) => reportMessage(context),
868
+ message: reportMessage,
486
869
  kind: "progress",
487
870
  }),
488
871
  decide: compute({
489
872
  run: (context) => {
490
- const check = context.outputs.check as MonitorCheck;
873
+ const observation = currentObservation(context.outputs);
491
874
  const config = configFrom(context.outputs);
492
- const checks = completedChecks(context);
493
- if (check.route === "stop") return { route: "stop", reason: check.reason, checks };
875
+ const checks = completedObservations(context);
876
+ if (observation.route === "stop")
877
+ return { route: "stop", reason: observation.reason, checks };
494
878
  if (checks >= config.maxChecks) {
495
879
  return {
496
880
  route: "stop",
497
- reason: `Reached the ${config.maxChecks}-check safety limit.`,
881
+ reason: `Reached the ${config.maxChecks}-observation safety limit.`,
498
882
  checks,
499
883
  };
500
884
  }
501
- if (check.route === "repair") return { route: "repair", reason: check.reason, checks };
502
- return { route: "continue", reason: check.reason, checks };
885
+ if (observation.route === "wait")
886
+ return { route: "wait", reason: observation.reason, checks };
887
+ const request = actionFrom(context.outputs);
888
+ return { route: request.kind, reason: observation.reason, checks };
889
+ },
890
+ }),
891
+ act: agent({
892
+ statusDetail: "performing authorized monitor action",
893
+ timeoutMs: DEFAULT_CHECK_TIMEOUT_MINUTES * 60_000,
894
+ prompt: ({ outputs }) => {
895
+ const request = actionFrom(outputs);
896
+ if (request.kind === "repair") throw new Error("repair must use the composed repair path");
897
+ return [
898
+ `Perform this one ${request.kind} action with normal tools: ${request.nextAction}`,
899
+ `Incomplete work: ${request.incomplete}`,
900
+ `Evidence: ${JSON.stringify(request.evidence)}`,
901
+ `Authorization: ${JSON.stringify(request.authority)}`,
902
+ `Cost safety: ${JSON.stringify(request.cost)}`,
903
+ `Defect safety: ${JSON.stringify(request.defect)}`,
904
+ `Verification: ${request.verification}`,
905
+ "Perform only the stated action and only on the allowed files, systems, and resources. Do not plan, document, redesign, broaden scope, change a protected contract, or perform another action. Verify the direct result before submitting. Preserve the supplied failure and target-state IDs exactly.",
906
+ ].join("\n\n");
503
907
  },
908
+ expectedOutput:
909
+ '{ "status": "succeeded" | "failed" | "blocked", "summary": "action performed and real result", "evidence": {}, "verification": "verification performed", "failureId": "unchanged failure ID", "targetStateId": "unchanged target-state ID" }',
910
+ validate: (output, context) =>
911
+ validateMonitorActionResult(output, actionFrom(context.outputs)),
504
912
  }),
505
- repairGuard: compute({
506
- run: (context) =>
507
- repeatedRepairWithoutProgress(context)
508
- ? {
509
- route: "blocked",
510
- reason:
511
- "The same issue returned after a completed repair with no changed target evidence.",
512
- }
513
- : { route: "repair", reason: "The issue is new or has changed evidence." },
913
+ repairComplete: compute({
914
+ run: ({ outputs }): RecordedAction => {
915
+ const request = actionFrom(outputs);
916
+ if (request.kind !== "repair") throw new Error("monitor repair action is missing");
917
+ return {
918
+ status: "succeeded",
919
+ summary: request.nextAction,
920
+ evidence: outputs.implementation,
921
+ verification: request.verification,
922
+ failureId: request.failureId,
923
+ targetStateId: request.targetStateId,
924
+ };
925
+ },
514
926
  }),
515
927
  repairBlocked: compute({
516
928
  run: (context) => ({
517
929
  reason: repairBlockedReason(context.outputs),
518
- observation: (context.outputs.check as MonitorCheck).observation,
519
- checks: completedChecks(context),
930
+ observation: currentObservation(context.outputs).observation,
931
+ checks: completedObservations(context),
520
932
  reported: true,
521
933
  }),
522
934
  }),
@@ -525,11 +937,11 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
525
937
  kind: "final",
526
938
  message: ({ outputs }) => {
527
939
  const result = outputs.repairBlocked as { reason: string };
528
- return `Automatic repair stopped: ${result.reason}`;
940
+ return `Monitor repair stopped: ${result.reason}`;
529
941
  },
530
942
  }),
531
943
  schedule: action({
532
- statusDetail: "scheduling next monitor check",
944
+ statusDetail: "scheduling next monitor observation",
533
945
  run: async ({ outputs, publishUpdate }) => {
534
946
  const config = configFrom(outputs);
535
947
  const lastCheckAt = new Date().toISOString();
@@ -550,7 +962,7 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
550
962
  },
551
963
  }),
552
964
  sleep: shell({
553
- statusDetail: "waiting for next monitor check",
965
+ statusDetail: "waiting for next monitor observation",
554
966
  timeoutMs: MAX_INTERVAL_MINUTES * 60_000 + NODE_TIMEOUT_MARGIN_MS,
555
967
  exec: ({ outputs }) => {
556
968
  const sleepMs = configFrom(outputs).everyMinutes * 60_000;
@@ -565,26 +977,24 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
565
977
  }),
566
978
  finish: compute({
567
979
  run: ({ outputs }) => {
568
- const check = outputs.check as MonitorCheck;
980
+ const observation = currentObservation(outputs);
569
981
  const decision = outputs.decide as { reason?: string; checks?: number } | undefined;
570
982
  const repair = outputs.repairBlocked as { reason?: string } | undefined;
571
983
  return {
572
- reason: repair?.reason ?? decision?.reason ?? check.reason,
573
- observation: check.observation,
984
+ reason: repair?.reason ?? decision?.reason ?? observation.reason,
985
+ observation: observation.observation,
986
+ goalState: repair === undefined ? observation.goalState : "blocked",
987
+ workState: observation.workState,
574
988
  checks: decision?.checks ?? 1,
575
989
  reported: true,
576
- ...(outputs.implementation !== undefined
577
- ? { repair: outputs.implementation }
578
- : repair !== undefined
579
- ? { repair }
580
- : {}),
581
990
  };
582
991
  },
583
992
  }),
584
993
  },
585
994
  edges: [
586
- { from: "prepare", to: "check" },
587
- { from: "check", to: "estimate" },
995
+ { from: "prepare", to: "observe" },
996
+ { from: "observe", to: "guard" },
997
+ { from: "guard", to: "estimate" },
588
998
  { from: "estimate", to: "publish_progress" },
589
999
  { from: "publish_progress", to: "report" },
590
1000
  { from: "report", to: "decide" },
@@ -592,21 +1002,25 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
592
1002
  from: "decide",
593
1003
  switch: {
594
1004
  on: "$.route",
595
- cases: { stop: "finish", continue: "schedule", repair: "repairGuard" },
1005
+ cases: {
1006
+ stop: "finish",
1007
+ wait: "schedule",
1008
+ advance: "act",
1009
+ recover: "act",
1010
+ repair: "planChange",
1011
+ },
596
1012
  },
597
1013
  },
598
- {
599
- from: "repairGuard",
600
- switch: { on: "$.route", cases: { repair: "planChange", blocked: "repairBlocked" } },
601
- },
1014
+ { from: "act", to: "observe" },
602
1015
  { from: "planChange.ready", to: "implementation" },
603
1016
  { from: "planChange.blocked", to: "repairBlocked" },
604
- { from: "implementation.completed", to: "check" },
1017
+ { from: "implementation.completed", to: "repairComplete" },
605
1018
  { from: "implementation.blocked", to: "repairBlocked" },
1019
+ { from: "repairComplete", to: "observe" },
606
1020
  { from: "repairBlocked", to: "repairReport" },
607
1021
  { from: "repairReport", to: "finish" },
608
1022
  { from: "schedule", to: "sleep" },
609
- { from: "sleep", to: "check" },
1023
+ { from: "sleep", to: "observe" },
610
1024
  ],
611
1025
  });
612
1026