@mediadatafusion/pi-workflow-suite 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/CONTRIBUTING.md +9 -0
- package/LICENSE.md +201 -0
- package/NOTICE +6 -0
- package/README.md +1208 -0
- package/SECURITY.md +7 -0
- package/SUPPORT.md +9 -0
- package/TRADEMARKS.md +14 -0
- package/VERSION +1 -0
- package/agents/codebase-research.md +42 -0
- package/agents/general-worker.md +26 -0
- package/agents/implementation-planning.md +46 -0
- package/agents/quality-validation.md +43 -0
- package/agents/workflow-orchestrator.md +44 -0
- package/config/prompts/execute-approved-plan.md +43 -0
- package/config/prompts/mission-checkpoint.md +26 -0
- package/config/prompts/mission-final-validation.md +21 -0
- package/config/prompts/mission-plan.md +129 -0
- package/config/prompts/mission-repair.md +33 -0
- package/config/prompts/mission-run.md +37 -0
- package/config/prompts/validate-approved-plan.md +42 -0
- package/config/prompts/workflow-plan-prompt.md +93 -0
- package/config/prompts/workflow-repair.md +20 -0
- package/config/prompts/workflow-summary.md +23 -0
- package/config/workflow-settings.example.json +335 -0
- package/docs/assets/mediadatafusion-logo.png +0 -0
- package/docs/assets/pi-workflow-suite-card.png +0 -0
- package/docs/assets/pi-workflow-suite-header.png +0 -0
- package/docs/assets/pi-workflow-suite-video-thumb.png +0 -0
- package/docs/assets/readme-link-commands.svg +10 -0
- package/docs/assets/readme-link-install.svg +10 -0
- package/docs/assets/readme-link-quick-start.svg +10 -0
- package/docs/assets/readme-link-settings.svg +10 -0
- package/extensions/subagent/agents.ts +149 -0
- package/extensions/subagent/index.ts +1136 -0
- package/extensions/subagent/runner.ts +291 -0
- package/extensions/workflow-model-router.ts +1485 -0
- package/extensions/workflow-modes.ts +14778 -0
- package/extensions/workflow-parsers.ts +212 -0
- package/extensions/workflow-settings-capabilities.ts +282 -0
- package/extensions/workflow-state.ts +978 -0
- package/extensions/workflow-subagent-policy.ts +180 -0
- package/extensions/workflow-summary.ts +381 -0
- package/extensions/workflow-tool-guard.ts +302 -0
- package/extensions/workflow-validation-classifier.ts +102 -0
- package/extensions/workflow-web-tools.ts +356 -0
- package/package.json +1 -0
- package/scripts/audit-live.sh +69 -0
- package/scripts/audit-settings.sh +136 -0
- package/scripts/backup-live.sh +63 -0
- package/scripts/bootstrap-project.sh +220 -0
- package/scripts/install-to-live.sh +87 -0
- package/scripts/quarantine-live-junk.sh +69 -0
- package/scripts/verify-live.sh +128 -0
- package/skills/codebase-discovery/SKILL.md +20 -0
- package/skills/find-skills/SKILL.md +155 -0
- package/skills/git-safe-summary/SKILL.md +20 -0
- package/skills/implementation-planning/SKILL.md +20 -0
- package/skills/project-rules-audit/SKILL.md +20 -0
- package/skills/safe-execution/SKILL.md +20 -0
- package/skills/validation-review/SKILL.md +20 -0
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
export type WorkflowMode = "idle" | "standard" | "awaiting_plan_input" | "awaiting_mission_input" | "awaiting_clarification" | "planning" | "plan_draft" | "plan_approved" | "reviewing" | "reviewed" | "executing" | "executed" | "validating" | "validated" | "repairing" | "revalidating" | "mission_draft" | "mission_awaiting_clarification" | "mission_planning" | "mission_plan_ready" | "mission_approved" | "mission_running" | "mission_paused" | "mission_checkpointing" | "mission_validating" | "mission_repairing" | "mission_revalidating" | "mission_final_validating" | "mission_completed" | "mission_failed" | "mission_blocked" | "mission_stopped" | "cancelled";
|
|
6
|
+
|
|
7
|
+
export interface ClarificationQuestion {
|
|
8
|
+
index: number;
|
|
9
|
+
question: string;
|
|
10
|
+
options: string[]; // e.g. ["A. Local dev server", "B. Vercel preview", ...]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ClarificationAnswer {
|
|
14
|
+
index: number;
|
|
15
|
+
letter: string; // "A", "B", "C", "D", or "S" for skip
|
|
16
|
+
custom?: string; // non-empty when letter is "D" (Other)
|
|
17
|
+
skipped?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type WorkflowTypedHandoffType =
|
|
21
|
+
| "workflow_plan_result"
|
|
22
|
+
| "workflow_review_result"
|
|
23
|
+
| "workflow_execution_result"
|
|
24
|
+
| "workflow_validation_result"
|
|
25
|
+
| "workflow_repair_result"
|
|
26
|
+
| "mission_plan_result"
|
|
27
|
+
| "mission_milestone_result"
|
|
28
|
+
| "standard_handoff_result";
|
|
29
|
+
|
|
30
|
+
export interface WorkflowTypedHandoff {
|
|
31
|
+
type: WorkflowTypedHandoffType;
|
|
32
|
+
createdAt: string;
|
|
33
|
+
sourceMode?: WorkflowMode;
|
|
34
|
+
activePlanId?: string;
|
|
35
|
+
activeMissionId?: string;
|
|
36
|
+
payload: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface WorkflowRepairHistoryEntry {
|
|
40
|
+
timestamp: string;
|
|
41
|
+
retry: number;
|
|
42
|
+
status: "running" | "completed" | "failed" | "blocked";
|
|
43
|
+
validationFailure?: string;
|
|
44
|
+
repairSummary?: string;
|
|
45
|
+
nextAction: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type RepairRetryGateName = "review" | "validation" | "missionValidation" | "missionFinalValidation";
|
|
49
|
+
|
|
50
|
+
export interface RepairRetryHistoryEntry {
|
|
51
|
+
timestamp: string;
|
|
52
|
+
retry: number;
|
|
53
|
+
status: "running" | "completed" | "failed" | "blocked";
|
|
54
|
+
failure?: string;
|
|
55
|
+
repairSummary?: string;
|
|
56
|
+
nextAction: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface RepairRetryGateState {
|
|
60
|
+
currentRetry: number;
|
|
61
|
+
workflowRetryCount: number;
|
|
62
|
+
maxRetriesPerItem: number;
|
|
63
|
+
maxRetriesPerWorkflow: number;
|
|
64
|
+
lastFailure?: string;
|
|
65
|
+
lastAttempt?: string;
|
|
66
|
+
status: "none" | "running" | "completed" | "failed" | "blocked";
|
|
67
|
+
inProgress?: boolean;
|
|
68
|
+
history?: RepairRetryHistoryEntry[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type WorkflowReviewHistoryEntry = RepairRetryHistoryEntry & {
|
|
72
|
+
reviewFailure?: string;
|
|
73
|
+
revisedPlanSummary?: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export interface PlanRuntimeState {
|
|
77
|
+
createdAt: string;
|
|
78
|
+
activeRuntimeMs: number;
|
|
79
|
+
activeRunStartedAt: string | null;
|
|
80
|
+
lastProgressAt: string;
|
|
81
|
+
runtimeCounter: "running" | "paused" | "stopped";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface StandardRuntimeState {
|
|
85
|
+
id: string;
|
|
86
|
+
createdAt: string;
|
|
87
|
+
active: boolean;
|
|
88
|
+
activeRuntimeMs: number;
|
|
89
|
+
activeRunStartedAt: string | null;
|
|
90
|
+
lastProgressAt: string;
|
|
91
|
+
runtimeCounter: "running" | "paused" | "stopped";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type PlanLifecycleStatus = "planning" | "awaiting_clarification" | "plan_ready" | "approved" | "reviewing" | "executing" | "validating" | "repairing" | "revalidating" | "completed" | "blocked";
|
|
95
|
+
export type PlanStepStatus = "pending" | "active" | "completed" | "failed" | "blocked" | "skipped";
|
|
96
|
+
export type PlanValidationStatus = "pending" | "running" | "pass" | "fail" | "unknown";
|
|
97
|
+
|
|
98
|
+
export interface PlanProgressStep {
|
|
99
|
+
id: string;
|
|
100
|
+
title: string;
|
|
101
|
+
status: PlanStepStatus;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface PlanProgressState {
|
|
105
|
+
createdAt: string;
|
|
106
|
+
lifecycleStatus: PlanLifecycleStatus;
|
|
107
|
+
currentStepIndex: number;
|
|
108
|
+
steps: PlanProgressStep[];
|
|
109
|
+
validationStatus: PlanValidationStatus;
|
|
110
|
+
lastValidationStatus?: PlanValidationStatus;
|
|
111
|
+
repairRetry: number;
|
|
112
|
+
maxRepairRetries: number;
|
|
113
|
+
repairStatus?: "none" | "running" | "completed" | "failed" | "blocked";
|
|
114
|
+
nextAction: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type StandardTodoStatus = "none" | "active" | "completed" | "paused" | "blocked";
|
|
118
|
+
export type StandardTodoItemStatus = "pending" | "active" | "completed" | "skipped" | "blocked";
|
|
119
|
+
|
|
120
|
+
export interface StandardTodoItem {
|
|
121
|
+
id: string;
|
|
122
|
+
title: string;
|
|
123
|
+
status: StandardTodoItemStatus;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface StandardTodoState {
|
|
127
|
+
createdAt: string;
|
|
128
|
+
updatedAt: string;
|
|
129
|
+
status: StandardTodoStatus;
|
|
130
|
+
task?: string;
|
|
131
|
+
currentItemIndex: number;
|
|
132
|
+
items: StandardTodoItem[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type StandardClarificationStage = "drafting" | "awaiting_answer" | "answered";
|
|
136
|
+
export type WorkflowSubagentPhase = "Planning" | "Execution" | "Repair" | "Review" | "Validation";
|
|
137
|
+
|
|
138
|
+
export interface StandardSubagentPreflightRecord {
|
|
139
|
+
task?: string;
|
|
140
|
+
required: number;
|
|
141
|
+
observed: number;
|
|
142
|
+
agents: string[];
|
|
143
|
+
satisfiedAt: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface CompletedPlanSummary {
|
|
147
|
+
completedAt: string;
|
|
148
|
+
planHistoryId?: string;
|
|
149
|
+
status: "completed";
|
|
150
|
+
stepsCompleted: number;
|
|
151
|
+
stepsTotal: number;
|
|
152
|
+
validationResult: "PASS" | "PARTIAL PASS" | "FAIL" | "UNKNOWN";
|
|
153
|
+
repairRetries: number;
|
|
154
|
+
maxRepairRetries: number;
|
|
155
|
+
repairStatus?: "none" | "running" | "completed" | "failed" | "blocked";
|
|
156
|
+
activeRuntimeMs: number;
|
|
157
|
+
elapsedMs: number;
|
|
158
|
+
nextAction: string;
|
|
159
|
+
executionSummary?: string;
|
|
160
|
+
validationReport?: string;
|
|
161
|
+
repairSummary?: string;
|
|
162
|
+
reviewerSummary?: string;
|
|
163
|
+
finalReport?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface WorkflowFinalStopSummary {
|
|
167
|
+
stoppedAt: string;
|
|
168
|
+
kind: "plan" | "mission";
|
|
169
|
+
status: "completed" | "blocked";
|
|
170
|
+
title: string;
|
|
171
|
+
summary: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface CompletedMissionSummary {
|
|
175
|
+
completedAt: string;
|
|
176
|
+
missionId?: string;
|
|
177
|
+
status: "completed";
|
|
178
|
+
milestonesCompleted: number;
|
|
179
|
+
milestonesTotal: number;
|
|
180
|
+
validationResult: string;
|
|
181
|
+
repairRetries: number;
|
|
182
|
+
maxRepairRetries: number;
|
|
183
|
+
repairStatus?: "none" | "running" | "completed" | "failed" | "blocked";
|
|
184
|
+
activeRuntimeMs: number;
|
|
185
|
+
elapsedMs: number;
|
|
186
|
+
nextAction: string;
|
|
187
|
+
executionSummary?: string;
|
|
188
|
+
validationReport?: string;
|
|
189
|
+
repairSummary?: string;
|
|
190
|
+
finalReport?: string;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface WorkflowState {
|
|
194
|
+
version: 1;
|
|
195
|
+
mode: WorkflowMode;
|
|
196
|
+
task?: string;
|
|
197
|
+
originalTask?: string;
|
|
198
|
+
draftPlan?: string;
|
|
199
|
+
clarifyingQuestions?: ClarificationQuestion[];
|
|
200
|
+
clarifyingAnswers?: ClarificationAnswer[];
|
|
201
|
+
lastWorkflowHandoff?: WorkflowTypedHandoff;
|
|
202
|
+
clarificationAlreadyAsked?: boolean;
|
|
203
|
+
clarificationRequiredBeforePlan?: boolean;
|
|
204
|
+
clarificationRequirementReason?: string;
|
|
205
|
+
clarificationSkipReason?: string;
|
|
206
|
+
clarificationQualityRetryCount?: number;
|
|
207
|
+
planningDepth?: string;
|
|
208
|
+
clarificationMode?: string;
|
|
209
|
+
approvedPlan?: string;
|
|
210
|
+
activePlanId?: string;
|
|
211
|
+
planHistoryId?: string;
|
|
212
|
+
approvedPlanHistoryId?: string;
|
|
213
|
+
activeMissionId?: string;
|
|
214
|
+
reviewerReport?: string;
|
|
215
|
+
reviewerVerdict?: "PASS" | "NOTES" | "NEEDS REPAIR" | "FAIL" | "BLOCKED" | "UNKNOWN";
|
|
216
|
+
currentReviewRetry?: number;
|
|
217
|
+
workflowReviewRetryCount?: number;
|
|
218
|
+
maxReviewRetriesPerPlan?: number;
|
|
219
|
+
maxReviewRetriesPerWorkflow?: number;
|
|
220
|
+
lastReviewFailure?: string;
|
|
221
|
+
lastReviewAttempt?: string;
|
|
222
|
+
lastReviewRepairStatus?: "none" | "running" | "completed" | "failed" | "blocked";
|
|
223
|
+
reviewHistory?: WorkflowReviewHistoryEntry[];
|
|
224
|
+
reviewRepairInProgress?: boolean;
|
|
225
|
+
repairRetryState?: Partial<Record<RepairRetryGateName, RepairRetryGateState>>;
|
|
226
|
+
executionSummary?: string;
|
|
227
|
+
validationReport?: string;
|
|
228
|
+
validationVerdict?: "PASS" | "PARTIAL PASS" | "FAIL" | "UNKNOWN";
|
|
229
|
+
currentValidationRetry?: number;
|
|
230
|
+
workflowValidationRetryCount?: number;
|
|
231
|
+
maxValidationRetriesPerPlan?: number;
|
|
232
|
+
maxValidationRetriesPerWorkflow?: number;
|
|
233
|
+
lastValidationFailure?: string;
|
|
234
|
+
lastRepairAttempt?: string;
|
|
235
|
+
repairHistory?: WorkflowRepairHistoryEntry[];
|
|
236
|
+
lastRepairStatus?: "none" | "running" | "completed" | "failed" | "blocked";
|
|
237
|
+
planStepValidationIndex?: number;
|
|
238
|
+
planRuntime?: PlanRuntimeState;
|
|
239
|
+
planProgress?: PlanProgressState;
|
|
240
|
+
standardRuntime?: StandardRuntimeState;
|
|
241
|
+
standardTodo?: StandardTodoState;
|
|
242
|
+
standardLastAutoCheckAt?: string;
|
|
243
|
+
standardLastClarificationDecision?: string;
|
|
244
|
+
standardLastClarificationReason?: string;
|
|
245
|
+
standardClarificationPending?: boolean;
|
|
246
|
+
standardClarificationStage?: StandardClarificationStage;
|
|
247
|
+
standardActivePhase?: WorkflowSubagentPhase;
|
|
248
|
+
standardWorkKind?: "read_only" | "mutation" | "validation" | "repair" | "review";
|
|
249
|
+
standardClarificationTask?: string;
|
|
250
|
+
standardClarificationAnswer?: string;
|
|
251
|
+
standardClarificationRequirementReason?: string;
|
|
252
|
+
standardClarifyingQuestions?: ClarificationQuestion[];
|
|
253
|
+
standardClarifyingAnswers?: ClarificationAnswer[];
|
|
254
|
+
standardSubagentPreflight?: Partial<Record<WorkflowSubagentPhase, StandardSubagentPreflightRecord>>;
|
|
255
|
+
standardLastTodoDecision?: string;
|
|
256
|
+
standardLastTodoReason?: string;
|
|
257
|
+
lastCompletedPlanSummary?: CompletedPlanSummary;
|
|
258
|
+
lastCompletedMissionSummary?: CompletedMissionSummary;
|
|
259
|
+
lastPlanStopSummary?: WorkflowFinalStopSummary;
|
|
260
|
+
lastMissionStopSummary?: WorkflowFinalStopSummary;
|
|
261
|
+
modelsUsed?: {
|
|
262
|
+
planner?: string;
|
|
263
|
+
executor?: string;
|
|
264
|
+
validator?: string;
|
|
265
|
+
reviewer?: string;
|
|
266
|
+
};
|
|
267
|
+
updatedAt: string;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface SavedWorkflowPlan {
|
|
271
|
+
id: string;
|
|
272
|
+
timestamp: string;
|
|
273
|
+
projectPath: string;
|
|
274
|
+
projectLabel: string;
|
|
275
|
+
planningMode: WorkflowMode;
|
|
276
|
+
planningDepth?: string;
|
|
277
|
+
clarificationMode?: string;
|
|
278
|
+
originalTask?: string;
|
|
279
|
+
clarificationQuestions?: ClarificationQuestion[];
|
|
280
|
+
clarificationAnswers?: ClarificationAnswer[];
|
|
281
|
+
finalPlan: string;
|
|
282
|
+
approvalStatus: "draft" | "approved" | "revised" | "completed" | "archived";
|
|
283
|
+
saveReason: string;
|
|
284
|
+
validationVerdict?: WorkflowState["validationVerdict"];
|
|
285
|
+
validationReport?: string;
|
|
286
|
+
executionSummary?: string;
|
|
287
|
+
reviewerReport?: string;
|
|
288
|
+
repairStatus?: WorkflowState["lastRepairStatus"];
|
|
289
|
+
repairAttempt?: string;
|
|
290
|
+
finalReport?: string;
|
|
291
|
+
modelsUsed?: WorkflowState["modelsUsed"];
|
|
292
|
+
subagents?: Record<string, unknown>;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export interface PlanSavingOptions {
|
|
296
|
+
cwd: string;
|
|
297
|
+
approvalStatus: SavedWorkflowPlan["approvalStatus"];
|
|
298
|
+
saveReason: string;
|
|
299
|
+
planningDepth?: string;
|
|
300
|
+
clarificationMode?: string;
|
|
301
|
+
subagents?: Record<string, unknown>;
|
|
302
|
+
executionSummary?: string;
|
|
303
|
+
reviewerReport?: string;
|
|
304
|
+
validationReport?: string;
|
|
305
|
+
repairAttempt?: string;
|
|
306
|
+
finalReport?: string;
|
|
307
|
+
savePlanHistory?: boolean;
|
|
308
|
+
planHistoryLimit?: number;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export type MissionStatus = "draft" | "planning" | "awaiting_clarification" | "planned" | "approved" | "running" | "paused" | "checkpointing" | "validating" | "repairing" | "revalidating" | "completed" | "failed" | "blocked" | "stopped";
|
|
312
|
+
export type MissionAutonomy = "manual" | "approval_gated" | "supervised_auto" | "full_auto";
|
|
313
|
+
export type MissionMilestoneStatus = "pending" | "active" | "completed" | "failed" | "skipped";
|
|
314
|
+
|
|
315
|
+
export interface MissionMilestone {
|
|
316
|
+
id: string;
|
|
317
|
+
title: string;
|
|
318
|
+
objective: string;
|
|
319
|
+
status: MissionMilestoneStatus;
|
|
320
|
+
steps: string[];
|
|
321
|
+
validation: string[];
|
|
322
|
+
risks: string[];
|
|
323
|
+
checkpointIds: string[];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export interface MissionCheckpoint {
|
|
327
|
+
id: string;
|
|
328
|
+
timestamp: string;
|
|
329
|
+
status: MissionStatus;
|
|
330
|
+
milestoneId?: string;
|
|
331
|
+
summary: string;
|
|
332
|
+
nextAction: string;
|
|
333
|
+
filesChanged?: string[];
|
|
334
|
+
validationResult?: string;
|
|
335
|
+
errors?: string[];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export interface MissionRepairHistoryEntry {
|
|
339
|
+
timestamp: string;
|
|
340
|
+
milestoneId?: string;
|
|
341
|
+
retry: number;
|
|
342
|
+
status: "running" | "completed" | "failed" | "blocked";
|
|
343
|
+
validationFailure?: string;
|
|
344
|
+
repairSummary?: string;
|
|
345
|
+
nextAction: string;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export interface MissionRuntimeSegment {
|
|
349
|
+
startedAt: string;
|
|
350
|
+
endedAt: string;
|
|
351
|
+
durationMs: number;
|
|
352
|
+
reasonEnded: "paused" | "blocked" | "stopped" | "completed" | "failed" | "waiting" | "status_change";
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export interface MissionState {
|
|
356
|
+
version: 1;
|
|
357
|
+
id: string;
|
|
358
|
+
status: MissionStatus;
|
|
359
|
+
goal: string;
|
|
360
|
+
createdAt: string;
|
|
361
|
+
updatedAt: string;
|
|
362
|
+
cwd?: string;
|
|
363
|
+
projectLabel?: string;
|
|
364
|
+
autonomy: MissionAutonomy;
|
|
365
|
+
autonomySource?: "settings at mission creation" | "user override";
|
|
366
|
+
allowFullAutoAtCreation?: boolean;
|
|
367
|
+
continueAcrossMilestones?: boolean;
|
|
368
|
+
pauseBetweenMilestones?: boolean;
|
|
369
|
+
currentMilestoneIndex: number;
|
|
370
|
+
milestones: MissionMilestone[];
|
|
371
|
+
checkpoints: MissionCheckpoint[];
|
|
372
|
+
clarificationQuestions?: ClarificationQuestion[];
|
|
373
|
+
clarificationAnswers?: ClarificationAnswer[];
|
|
374
|
+
planText?: string;
|
|
375
|
+
currentStep?: string;
|
|
376
|
+
reviewerReport?: string;
|
|
377
|
+
reviewerVerdict?: "PASS" | "NOTES" | "NEEDS REPAIR" | "FAIL" | "BLOCKED" | "UNKNOWN";
|
|
378
|
+
currentReviewRetry?: number;
|
|
379
|
+
missionReviewRetryCount?: number;
|
|
380
|
+
maxReviewRetriesPerMission?: number;
|
|
381
|
+
lastReviewFailure?: string;
|
|
382
|
+
lastReviewAttempt?: string;
|
|
383
|
+
lastReviewRepairStatus?: "none" | "running" | "completed" | "failed" | "blocked";
|
|
384
|
+
reviewHistory?: WorkflowReviewHistoryEntry[];
|
|
385
|
+
reviewRepairInProgress?: boolean;
|
|
386
|
+
lastValidationResult?: string;
|
|
387
|
+
modelsUsed: Record<string, string>;
|
|
388
|
+
subagentsUsed: string[];
|
|
389
|
+
approvalRequired: boolean;
|
|
390
|
+
lastSummary: string;
|
|
391
|
+
lastStopReason?: string;
|
|
392
|
+
lastBlockReason?: string;
|
|
393
|
+
nextAction?: string;
|
|
394
|
+
lastHeartbeatAt?: string;
|
|
395
|
+
lastProgressAt?: string;
|
|
396
|
+
heartbeatCount?: number;
|
|
397
|
+
activeRuntimeMs?: number;
|
|
398
|
+
activeRunStartedAt?: string | null;
|
|
399
|
+
lastPausedAt?: string;
|
|
400
|
+
lastResumedAt?: string;
|
|
401
|
+
lastStoppedAt?: string;
|
|
402
|
+
completedAt?: string;
|
|
403
|
+
runtimeSegments?: MissionRuntimeSegment[];
|
|
404
|
+
currentValidationRetry?: number;
|
|
405
|
+
missionValidationRetryCount?: number;
|
|
406
|
+
maxValidationRetriesPerMilestone?: number;
|
|
407
|
+
maxValidationRetriesPerMission?: number;
|
|
408
|
+
lastValidationFailure?: string;
|
|
409
|
+
lastRepairAttempt?: string;
|
|
410
|
+
repairHistory?: MissionRepairHistoryEntry[];
|
|
411
|
+
lastRepairStatus?: "none" | "running" | "completed" | "failed" | "blocked";
|
|
412
|
+
finalValidationRetryCount?: number;
|
|
413
|
+
maxFinalValidationRetries?: number;
|
|
414
|
+
lastFinalValidationResult?: string;
|
|
415
|
+
lastFinalValidationFailure?: string;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export const WORKFLOW_DIR = join(getAgentDir(), "workflows");
|
|
419
|
+
export const ACTIVE_STATE_FILE = join(WORKFLOW_DIR, "active.json");
|
|
420
|
+
export const PLAN_HISTORY_DIR = join(WORKFLOW_DIR, "plans");
|
|
421
|
+
export const LATEST_PLAN_FILE = join(PLAN_HISTORY_DIR, "latest.json");
|
|
422
|
+
export const MISSION_HISTORY_DIR = join(WORKFLOW_DIR, "missions");
|
|
423
|
+
export const LATEST_MISSION_FILE = join(MISSION_HISTORY_DIR, "latest.json");
|
|
424
|
+
|
|
425
|
+
export function emptyState(): WorkflowState {
|
|
426
|
+
return { version: 1, mode: "idle", updatedAt: new Date().toISOString() };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function loadState(): WorkflowState {
|
|
430
|
+
try {
|
|
431
|
+
if (!existsSync(ACTIVE_STATE_FILE)) return emptyState();
|
|
432
|
+
const parsed = JSON.parse(readFileSync(ACTIVE_STATE_FILE, "utf8")) as WorkflowState;
|
|
433
|
+
return { ...emptyState(), ...parsed, version: 1 };
|
|
434
|
+
} catch {
|
|
435
|
+
return emptyState();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function saveState(state: WorkflowState, options: { alreadyAccounted?: boolean } = {}): WorkflowState {
|
|
440
|
+
mkdirSync(WORKFLOW_DIR, { recursive: true });
|
|
441
|
+
const savedAt = new Date();
|
|
442
|
+
const previous = readExistingWorkflowState();
|
|
443
|
+
const accounted = options.alreadyAccounted ? state : applyStandardRuntimeAccounting(previous, applyPlanRuntimeAccounting(previous, state, savedAt), savedAt);
|
|
444
|
+
const next = { ...accounted, version: 1 as const, updatedAt: savedAt.toISOString() };
|
|
445
|
+
writeFileSync(ACTIVE_STATE_FILE, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
446
|
+
return next;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function resetState(): WorkflowState {
|
|
450
|
+
return saveState(emptyState());
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function redactSecrets(text: string | undefined): string | undefined {
|
|
454
|
+
if (!text) return text;
|
|
455
|
+
return text
|
|
456
|
+
.replace(/\b([A-Za-z0-9_]*?(?:API|TOKEN|SECRET|KEY|PASSWORD)[A-Za-z0-9_]*?\s*[:=]\s*)([^\s'\"]+)/gi, "$1[REDACTED]")
|
|
457
|
+
.replace(/\b(sk-[A-Za-z0-9_-]{12,})\b/g, "[REDACTED]")
|
|
458
|
+
.replace(/\b(xox[baprs]-[A-Za-z0-9-]{12,})\b/g, "[REDACTED]")
|
|
459
|
+
.replace(/\b(gh[pousr]_[A-Za-z0-9_]{12,})\b/g, "[REDACTED]");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function redactQuestion(q: ClarificationQuestion): ClarificationQuestion {
|
|
463
|
+
return {
|
|
464
|
+
...q,
|
|
465
|
+
question: redactSecrets(q.question) ?? q.question,
|
|
466
|
+
options: q.options.map((option) => redactSecrets(option) ?? option),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function redactAnswer(a: ClarificationAnswer): ClarificationAnswer {
|
|
471
|
+
return { ...a, custom: redactSecrets(a.custom) };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function workflowProjectSlug(cwd: string): string {
|
|
475
|
+
return basename(cwd).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function safePlanId(timestamp: string, cwd: string): string {
|
|
479
|
+
return `${timestamp.replace(/[:.]/g, "").replace(/[^0-9TZ-]/g, "")}-${workflowProjectSlug(cwd)}`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function planTimestampId(date = new Date()): string {
|
|
483
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
484
|
+
return `plan-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function standardTimestampId(date = new Date()): string {
|
|
488
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
489
|
+
return `standard-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export function createStandardRuntimeId(cwd: string): string {
|
|
493
|
+
return `${standardTimestampId()}-${workflowProjectSlug(cwd)}`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export function createWorkflowPlanId(cwd: string): string {
|
|
497
|
+
mkdirSync(PLAN_HISTORY_DIR, { recursive: true });
|
|
498
|
+
const timestamp = planTimestampId();
|
|
499
|
+
const project = workflowProjectSlug(cwd);
|
|
500
|
+
let id = `${timestamp}-${project}`;
|
|
501
|
+
let suffix = 1;
|
|
502
|
+
while (existsSync(join(PLAN_HISTORY_DIR, `${id}.json`))) {
|
|
503
|
+
id = `${timestamp}-${project}-${suffix++}`;
|
|
504
|
+
}
|
|
505
|
+
return id;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export function saveWorkflowPlan(state: WorkflowState, options: PlanSavingOptions): SavedWorkflowPlan | undefined {
|
|
509
|
+
const finalPlan = state.approvedPlan ?? state.draftPlan;
|
|
510
|
+
if (!finalPlan?.trim()) return undefined;
|
|
511
|
+
|
|
512
|
+
mkdirSync(PLAN_HISTORY_DIR, { recursive: true });
|
|
513
|
+
const timestamp = new Date().toISOString();
|
|
514
|
+
const record: SavedWorkflowPlan = {
|
|
515
|
+
id: state.activePlanId ?? safePlanId(timestamp, options.cwd),
|
|
516
|
+
timestamp,
|
|
517
|
+
projectPath: options.cwd,
|
|
518
|
+
projectLabel: basename(options.cwd) || options.cwd,
|
|
519
|
+
planningMode: state.mode,
|
|
520
|
+
planningDepth: options.planningDepth ?? state.planningDepth,
|
|
521
|
+
clarificationMode: options.clarificationMode ?? state.clarificationMode,
|
|
522
|
+
originalTask: redactSecrets(state.originalTask ?? state.task),
|
|
523
|
+
clarificationQuestions: state.clarifyingQuestions?.map(redactQuestion),
|
|
524
|
+
clarificationAnswers: state.clarifyingAnswers?.map(redactAnswer),
|
|
525
|
+
finalPlan: redactSecrets(finalPlan) ?? finalPlan,
|
|
526
|
+
approvalStatus: options.approvalStatus,
|
|
527
|
+
saveReason: options.saveReason,
|
|
528
|
+
validationVerdict: state.validationVerdict,
|
|
529
|
+
validationReport: redactSecrets(compact(options.validationReport ?? state.validationReport, 2400)) ?? compact(options.validationReport ?? state.validationReport, 2400),
|
|
530
|
+
executionSummary: redactSecrets(compact(options.executionSummary ?? state.executionSummary, 2400)) ?? compact(options.executionSummary ?? state.executionSummary, 2400),
|
|
531
|
+
reviewerReport: redactSecrets(compact(options.reviewerReport ?? state.reviewerReport, 1600)) ?? compact(options.reviewerReport ?? state.reviewerReport, 1600),
|
|
532
|
+
repairStatus: state.lastRepairStatus,
|
|
533
|
+
repairAttempt: redactSecrets(compact(options.repairAttempt ?? state.lastRepairAttempt, 1800)) ?? compact(options.repairAttempt ?? state.lastRepairAttempt, 1800),
|
|
534
|
+
finalReport: options.finalReport?.trim() ? (redactSecrets(compact(options.finalReport, 5000)) ?? compact(options.finalReport, 5000)) : undefined,
|
|
535
|
+
modelsUsed: state.modelsUsed,
|
|
536
|
+
subagents: options.subagents,
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
writeFileSync(LATEST_PLAN_FILE, JSON.stringify(record, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
540
|
+
if (options.savePlanHistory !== false) {
|
|
541
|
+
writeFileSync(join(PLAN_HISTORY_DIR, `${record.id}.json`), JSON.stringify(record, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
542
|
+
clearOldWorkflowPlans(options.planHistoryLimit ?? 50);
|
|
543
|
+
}
|
|
544
|
+
return record;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export function listWorkflowPlans(): SavedWorkflowPlan[] {
|
|
548
|
+
if (!existsSync(PLAN_HISTORY_DIR)) return [];
|
|
549
|
+
const plans: SavedWorkflowPlan[] = [];
|
|
550
|
+
for (const entry of readdirSync(PLAN_HISTORY_DIR, { withFileTypes: true })) {
|
|
551
|
+
if (!entry.isFile() || !entry.name.endsWith(".json") || entry.name === "latest.json") continue;
|
|
552
|
+
try {
|
|
553
|
+
plans.push(JSON.parse(readFileSync(join(PLAN_HISTORY_DIR, entry.name), "utf8")) as SavedWorkflowPlan);
|
|
554
|
+
} catch { /* skip unreadable plan */ }
|
|
555
|
+
}
|
|
556
|
+
return plans.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export function loadWorkflowPlan(id: string): SavedWorkflowPlan | undefined {
|
|
560
|
+
const file = id === "latest" ? LATEST_PLAN_FILE : join(PLAN_HISTORY_DIR, `${id.replace(/\.json$/i, "")}.json`);
|
|
561
|
+
try {
|
|
562
|
+
if (!existsSync(file)) return undefined;
|
|
563
|
+
return JSON.parse(readFileSync(file, "utf8")) as SavedWorkflowPlan;
|
|
564
|
+
} catch {
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function clearOldWorkflowPlans(limit = 50): number {
|
|
570
|
+
const safeLimit = Math.max(1, Math.min(500, Math.floor(limit)));
|
|
571
|
+
const plans = listWorkflowPlans();
|
|
572
|
+
let removed = 0;
|
|
573
|
+
for (const plan of plans.slice(safeLimit)) {
|
|
574
|
+
try {
|
|
575
|
+
unlinkSync(join(PLAN_HISTORY_DIR, `${plan.id}.json`));
|
|
576
|
+
removed++;
|
|
577
|
+
} catch { /* ignore */ }
|
|
578
|
+
}
|
|
579
|
+
return removed;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function missionTimestampId(date = new Date()): string {
|
|
583
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
584
|
+
return `mission-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export function createMissionState(goal: string, options: { cwd: string; autonomy: MissionAutonomy; autonomySource?: MissionState["autonomySource"]; allowFullAutoAtCreation?: boolean; continueAcrossMilestones?: boolean; pauseBetweenMilestones?: boolean; maxValidationRetriesPerMilestone?: number; maxValidationRetriesPerMission?: number }): MissionState {
|
|
588
|
+
mkdirSync(MISSION_HISTORY_DIR, { recursive: true });
|
|
589
|
+
const missionIdBase = `${missionTimestampId()}-${workflowProjectSlug(options.cwd)}`;
|
|
590
|
+
let id = missionIdBase;
|
|
591
|
+
let suffix = 1;
|
|
592
|
+
while (existsSync(join(MISSION_HISTORY_DIR, `${id}.json`))) {
|
|
593
|
+
id = `${missionIdBase}-${suffix++}`;
|
|
594
|
+
}
|
|
595
|
+
const timestamp = new Date().toISOString();
|
|
596
|
+
return {
|
|
597
|
+
version: 1,
|
|
598
|
+
id,
|
|
599
|
+
status: "draft",
|
|
600
|
+
goal: redactSecrets(goal) ?? goal,
|
|
601
|
+
createdAt: timestamp,
|
|
602
|
+
updatedAt: timestamp,
|
|
603
|
+
cwd: options.cwd,
|
|
604
|
+
projectLabel: basename(options.cwd) || options.cwd,
|
|
605
|
+
autonomy: options.autonomy,
|
|
606
|
+
autonomySource: options.autonomySource ?? "settings at mission creation",
|
|
607
|
+
allowFullAutoAtCreation: options.allowFullAutoAtCreation === true,
|
|
608
|
+
continueAcrossMilestones: options.continueAcrossMilestones !== false,
|
|
609
|
+
pauseBetweenMilestones: options.pauseBetweenMilestones === true,
|
|
610
|
+
currentMilestoneIndex: 0,
|
|
611
|
+
milestones: [],
|
|
612
|
+
checkpoints: [],
|
|
613
|
+
clarificationQuestions: [],
|
|
614
|
+
clarificationAnswers: [],
|
|
615
|
+
modelsUsed: {},
|
|
616
|
+
subagentsUsed: [],
|
|
617
|
+
currentReviewRetry: 0,
|
|
618
|
+
missionReviewRetryCount: 0,
|
|
619
|
+
lastReviewFailure: "",
|
|
620
|
+
lastReviewAttempt: "",
|
|
621
|
+
lastReviewRepairStatus: "none",
|
|
622
|
+
reviewHistory: [],
|
|
623
|
+
reviewRepairInProgress: false,
|
|
624
|
+
approvalRequired: true,
|
|
625
|
+
lastSummary: "Mission created. Generate or approve a milestone plan before running.",
|
|
626
|
+
lastStopReason: "",
|
|
627
|
+
lastBlockReason: "",
|
|
628
|
+
nextAction: "Generate milestone plan, then approve before running.",
|
|
629
|
+
lastHeartbeatAt: timestamp,
|
|
630
|
+
lastProgressAt: timestamp,
|
|
631
|
+
heartbeatCount: 0,
|
|
632
|
+
activeRuntimeMs: 0,
|
|
633
|
+
activeRunStartedAt: null,
|
|
634
|
+
runtimeSegments: [],
|
|
635
|
+
currentValidationRetry: 0,
|
|
636
|
+
missionValidationRetryCount: 0,
|
|
637
|
+
maxValidationRetriesPerMilestone: options.maxValidationRetriesPerMilestone ?? 2,
|
|
638
|
+
maxValidationRetriesPerMission: options.maxValidationRetriesPerMission ?? 8,
|
|
639
|
+
lastValidationFailure: "",
|
|
640
|
+
lastRepairAttempt: "",
|
|
641
|
+
repairHistory: [],
|
|
642
|
+
lastRepairStatus: "none",
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function readExistingMissionState(id: string): MissionState | undefined {
|
|
647
|
+
const safeId = id.replace(/\.json$/i, "").replace(/[^A-Za-z0-9._-]/g, "");
|
|
648
|
+
const file = join(MISSION_HISTORY_DIR, `${safeId}.json`);
|
|
649
|
+
try {
|
|
650
|
+
if (!existsSync(file)) return undefined;
|
|
651
|
+
return JSON.parse(readFileSync(file, "utf8")) as MissionState;
|
|
652
|
+
} catch {
|
|
653
|
+
return undefined;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export function isMissionRuntimeActiveStatus(status?: MissionStatus): boolean {
|
|
658
|
+
return status === "planning" || status === "running" || status === "validating" || status === "repairing" || status === "revalidating" || status === "checkpointing";
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function runtimeEndReason(status: MissionStatus): MissionRuntimeSegment["reasonEnded"] {
|
|
662
|
+
if (status === "paused") return "paused";
|
|
663
|
+
if (status === "blocked") return "blocked";
|
|
664
|
+
if (status === "stopped") return "stopped";
|
|
665
|
+
if (status === "completed") return "completed";
|
|
666
|
+
if (status === "failed") return "failed";
|
|
667
|
+
if (status === "draft" || status === "awaiting_clarification" || status === "planned" || status === "approved") return "waiting";
|
|
668
|
+
return "status_change";
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function safeRuntimeMs(value: unknown): number {
|
|
672
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function readExistingWorkflowState(): WorkflowState | undefined {
|
|
676
|
+
try {
|
|
677
|
+
if (!existsSync(ACTIVE_STATE_FILE)) return undefined;
|
|
678
|
+
return JSON.parse(readFileSync(ACTIVE_STATE_FILE, "utf8")) as WorkflowState;
|
|
679
|
+
} catch {
|
|
680
|
+
return undefined;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export function isPlanRuntimeActiveMode(mode?: WorkflowMode): boolean {
|
|
685
|
+
return mode === "planning" || mode === "reviewing" || mode === "executing" || mode === "validating" || mode === "repairing" || mode === "revalidating";
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export function planRuntimeCounterState(state: WorkflowState): "running" | "paused" | "stopped" {
|
|
689
|
+
if (state.mode === "idle" || state.mode === "cancelled") return "stopped";
|
|
690
|
+
if (isPlanRuntimeActiveMode(state.mode)) return "running";
|
|
691
|
+
return "paused";
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export function isStandardRuntimeActive(state?: WorkflowState): boolean {
|
|
695
|
+
return state?.mode === "standard" && state.standardRuntime?.active === true;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
export function standardRuntimeCounterState(state: WorkflowState): "running" | "paused" | "stopped" {
|
|
699
|
+
if (state.mode === "idle" || state.mode === "cancelled") return "stopped";
|
|
700
|
+
if (isStandardRuntimeActive(state)) return "running";
|
|
701
|
+
if (state.standardRuntime) return "paused";
|
|
702
|
+
return "stopped";
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const RUNTIME_SESSION_STARTED_AT_MS = Date.now();
|
|
706
|
+
|
|
707
|
+
function elapsedMs(startedAt: string | null | undefined, endedAtMs: number): number {
|
|
708
|
+
const parsed = Date.parse(startedAt ?? "");
|
|
709
|
+
return Number.isFinite(parsed) ? Math.max(0, endedAtMs - parsed) : 0;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function activeElapsedMs(startedAt: string | null | undefined, nowMs: number, lastUpdatedAt?: string): number {
|
|
713
|
+
const parsed = Date.parse(startedAt ?? "");
|
|
714
|
+
if (!Number.isFinite(parsed)) return 0;
|
|
715
|
+
const updated = Date.parse(lastUpdatedAt ?? "");
|
|
716
|
+
const end = parsed < RUNTIME_SESSION_STARTED_AT_MS && Number.isFinite(updated) && updated < RUNTIME_SESSION_STARTED_AT_MS
|
|
717
|
+
? Math.max(parsed, updated)
|
|
718
|
+
: nowMs;
|
|
719
|
+
return Math.max(0, end - parsed);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
export function applyPlanRuntimeAccounting(previous: WorkflowState | undefined, state: WorkflowState, now = new Date()): WorkflowState {
|
|
723
|
+
const counter = planRuntimeCounterState(state);
|
|
724
|
+
if (counter === "stopped" && !state.planRuntime) return state;
|
|
725
|
+
|
|
726
|
+
const nowIso = now.toISOString();
|
|
727
|
+
const nowMs = now.getTime();
|
|
728
|
+
const currentRuntime = state.planRuntime;
|
|
729
|
+
const previousRuntime = currentRuntime ? previous?.planRuntime : undefined;
|
|
730
|
+
const createdAt = currentRuntime?.createdAt ?? previousRuntime?.createdAt ?? nowIso;
|
|
731
|
+
const baseRuntimeMs = safeRuntimeMs(currentRuntime?.activeRuntimeMs ?? previousRuntime?.activeRuntimeMs);
|
|
732
|
+
const previousStartedAt = previousRuntime?.activeRunStartedAt ?? currentRuntime?.activeRunStartedAt ?? null;
|
|
733
|
+
const previousActive = isPlanRuntimeActiveMode(previous?.mode);
|
|
734
|
+
const nextActive = isPlanRuntimeActiveMode(state.mode);
|
|
735
|
+
|
|
736
|
+
let activeRuntimeMs = baseRuntimeMs;
|
|
737
|
+
let activeRunStartedAt = currentRuntime?.activeRunStartedAt ?? previousStartedAt ?? null;
|
|
738
|
+
|
|
739
|
+
if (previousActive && !nextActive && previousStartedAt) {
|
|
740
|
+
activeRuntimeMs = baseRuntimeMs + activeElapsedMs(previousStartedAt, nowMs, previous?.updatedAt);
|
|
741
|
+
activeRunStartedAt = null;
|
|
742
|
+
} else if (nextActive && !previousStartedAt) {
|
|
743
|
+
activeRunStartedAt = nowIso;
|
|
744
|
+
} else if (nextActive && previousStartedAt) {
|
|
745
|
+
activeRunStartedAt = previousStartedAt;
|
|
746
|
+
} else if (!nextActive) {
|
|
747
|
+
activeRunStartedAt = null;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return {
|
|
751
|
+
...state,
|
|
752
|
+
planRuntime: {
|
|
753
|
+
createdAt,
|
|
754
|
+
activeRuntimeMs,
|
|
755
|
+
activeRunStartedAt,
|
|
756
|
+
lastProgressAt: nextActive ? nowIso : (currentRuntime?.lastProgressAt ?? previousRuntime?.lastProgressAt ?? nowIso),
|
|
757
|
+
runtimeCounter: counter,
|
|
758
|
+
},
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export function planActiveRuntimeMs(state: WorkflowState, now = new Date()): number {
|
|
763
|
+
const runtime = state.planRuntime;
|
|
764
|
+
const base = safeRuntimeMs(runtime?.activeRuntimeMs);
|
|
765
|
+
if (!runtime || !isPlanRuntimeActiveMode(state.mode)) return base;
|
|
766
|
+
return base + activeElapsedMs(runtime.activeRunStartedAt, now.getTime(), state.updatedAt);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
export function planWallClockAgeMs(state: WorkflowState, now = new Date()): number {
|
|
770
|
+
const start = Date.parse(state.planRuntime?.createdAt ?? "");
|
|
771
|
+
if (!Number.isFinite(start)) return 0;
|
|
772
|
+
return Math.max(0, now.getTime() - start);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
export function applyStandardRuntimeAccounting(previous: WorkflowState | undefined, state: WorkflowState, now = new Date()): WorkflowState {
|
|
776
|
+
const counter = standardRuntimeCounterState(state);
|
|
777
|
+
if (counter === "stopped" && !state.standardRuntime) return state;
|
|
778
|
+
|
|
779
|
+
const nowIso = now.toISOString();
|
|
780
|
+
const nowMs = now.getTime();
|
|
781
|
+
const currentRuntime = state.standardRuntime;
|
|
782
|
+
const previousRuntime = currentRuntime ? previous?.standardRuntime : undefined;
|
|
783
|
+
const id = currentRuntime?.id ?? previousRuntime?.id ?? createStandardRuntimeId(process.cwd());
|
|
784
|
+
const createdAt = currentRuntime?.createdAt ?? previousRuntime?.createdAt ?? nowIso;
|
|
785
|
+
const baseRuntimeMs = safeRuntimeMs(currentRuntime?.activeRuntimeMs ?? previousRuntime?.activeRuntimeMs);
|
|
786
|
+
const previousStartedAt = previousRuntime?.activeRunStartedAt ?? currentRuntime?.activeRunStartedAt ?? null;
|
|
787
|
+
const previousActive = isStandardRuntimeActive(previous);
|
|
788
|
+
const nextActive = isStandardRuntimeActive(state);
|
|
789
|
+
|
|
790
|
+
let activeRuntimeMs = baseRuntimeMs;
|
|
791
|
+
let activeRunStartedAt = currentRuntime?.activeRunStartedAt ?? previousStartedAt ?? null;
|
|
792
|
+
|
|
793
|
+
if (previousActive && !nextActive && previousStartedAt) {
|
|
794
|
+
activeRuntimeMs = baseRuntimeMs + activeElapsedMs(previousStartedAt, nowMs, previous?.updatedAt);
|
|
795
|
+
activeRunStartedAt = null;
|
|
796
|
+
} else if (nextActive && !previousStartedAt) {
|
|
797
|
+
activeRunStartedAt = nowIso;
|
|
798
|
+
} else if (nextActive && previousStartedAt) {
|
|
799
|
+
activeRunStartedAt = previousStartedAt;
|
|
800
|
+
} else if (!nextActive) {
|
|
801
|
+
activeRunStartedAt = null;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return {
|
|
805
|
+
...state,
|
|
806
|
+
standardRuntime: {
|
|
807
|
+
id,
|
|
808
|
+
createdAt,
|
|
809
|
+
active: nextActive,
|
|
810
|
+
activeRuntimeMs,
|
|
811
|
+
activeRunStartedAt,
|
|
812
|
+
lastProgressAt: nextActive ? nowIso : (currentRuntime?.lastProgressAt ?? previousRuntime?.lastProgressAt ?? nowIso),
|
|
813
|
+
runtimeCounter: counter,
|
|
814
|
+
},
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
export function standardActiveRuntimeMs(state: WorkflowState, now = new Date()): number {
|
|
819
|
+
const runtime = state.standardRuntime;
|
|
820
|
+
const base = safeRuntimeMs(runtime?.activeRuntimeMs);
|
|
821
|
+
if (!runtime || !isStandardRuntimeActive(state)) return base;
|
|
822
|
+
return base + activeElapsedMs(runtime.activeRunStartedAt, now.getTime(), state.updatedAt);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
export function standardWallClockAgeMs(state: WorkflowState, now = new Date()): number {
|
|
826
|
+
const start = Date.parse(state.standardRuntime?.createdAt ?? "");
|
|
827
|
+
if (!Number.isFinite(start)) return 0;
|
|
828
|
+
return Math.max(0, now.getTime() - start);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
export function applyMissionRuntimeAccounting(previous: MissionState | undefined, mission: MissionState, now = new Date()): MissionState {
|
|
832
|
+
const nowIso = now.toISOString();
|
|
833
|
+
const nowMs = now.getTime();
|
|
834
|
+
const previousActive = isMissionRuntimeActiveStatus(previous?.status);
|
|
835
|
+
const nextActive = isMissionRuntimeActiveStatus(mission.status);
|
|
836
|
+
const previousStartedAt = previous?.activeRunStartedAt ?? mission.activeRunStartedAt ?? null;
|
|
837
|
+
const baseRuntimeMs = safeRuntimeMs(mission.activeRuntimeMs ?? previous?.activeRuntimeMs);
|
|
838
|
+
const baseSegments = mission.runtimeSegments ?? previous?.runtimeSegments ?? [];
|
|
839
|
+
let next: MissionState = {
|
|
840
|
+
...mission,
|
|
841
|
+
activeRuntimeMs: baseRuntimeMs,
|
|
842
|
+
activeRunStartedAt: mission.activeRunStartedAt ?? previousStartedAt ?? null,
|
|
843
|
+
runtimeSegments: baseSegments,
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
if (previousActive && !nextActive && previousStartedAt) {
|
|
847
|
+
const durationMs = activeElapsedMs(previousStartedAt, nowMs, previous?.updatedAt);
|
|
848
|
+
next = {
|
|
849
|
+
...next,
|
|
850
|
+
activeRuntimeMs: baseRuntimeMs + durationMs,
|
|
851
|
+
activeRunStartedAt: null,
|
|
852
|
+
runtimeSegments: [...baseSegments, { startedAt: previousStartedAt, endedAt: nowIso, durationMs, reasonEnded: runtimeEndReason(mission.status) }].slice(-100),
|
|
853
|
+
};
|
|
854
|
+
} else if (nextActive && !previousStartedAt) {
|
|
855
|
+
next = {
|
|
856
|
+
...next,
|
|
857
|
+
activeRunStartedAt: nowIso,
|
|
858
|
+
lastResumedAt: mission.lastResumedAt ?? nowIso,
|
|
859
|
+
};
|
|
860
|
+
} else if (nextActive && previousStartedAt) {
|
|
861
|
+
next = { ...next, activeRunStartedAt: previousStartedAt };
|
|
862
|
+
} else if (!nextActive) {
|
|
863
|
+
next = { ...next, activeRunStartedAt: null };
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (mission.status === "paused" && previous?.status !== "paused") next.lastPausedAt = mission.lastPausedAt ?? nowIso;
|
|
867
|
+
if (mission.status === "stopped" && previous?.status !== "stopped") next.lastStoppedAt = mission.lastStoppedAt ?? nowIso;
|
|
868
|
+
if (mission.status === "completed") next.completedAt = mission.completedAt ?? next.completedAt ?? mission.updatedAt ?? nowIso;
|
|
869
|
+
return next;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
export function missionActiveRuntimeMs(mission: MissionState, now = new Date()): number {
|
|
873
|
+
const base = safeRuntimeMs(mission.activeRuntimeMs);
|
|
874
|
+
if (!isMissionRuntimeActiveStatus(mission.status)) return base;
|
|
875
|
+
return base + activeElapsedMs(mission.activeRunStartedAt, now.getTime(), mission.updatedAt);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
export function missionWallClockAgeMs(mission: MissionState, now = new Date()): number {
|
|
879
|
+
const start = Date.parse(mission.createdAt ?? "");
|
|
880
|
+
if (!Number.isFinite(start)) return 0;
|
|
881
|
+
const terminalTimestamp = mission.completedAt ?? (mission.status === "completed" ? mission.updatedAt : undefined);
|
|
882
|
+
const end = terminalTimestamp ? Date.parse(terminalTimestamp) : now.getTime();
|
|
883
|
+
return Math.max(0, (Number.isFinite(end) ? end : now.getTime()) - start);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
export function missionRuntimeCounterState(mission: MissionState): "running" | "paused" | "blocked" | "stopped" | "completed" | "failed" | "waiting" {
|
|
887
|
+
if (isMissionRuntimeActiveStatus(mission.status)) return "running";
|
|
888
|
+
if (mission.status === "paused") return "paused";
|
|
889
|
+
if (mission.status === "blocked") return "blocked";
|
|
890
|
+
if (mission.status === "stopped") return "stopped";
|
|
891
|
+
if (mission.status === "completed") return "completed";
|
|
892
|
+
if (mission.status === "failed") return "failed";
|
|
893
|
+
return "waiting";
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
export function saveMissionState(mission: MissionState): MissionState {
|
|
897
|
+
mkdirSync(MISSION_HISTORY_DIR, { recursive: true });
|
|
898
|
+
const savedAt = new Date();
|
|
899
|
+
const accounted = applyMissionRuntimeAccounting(readExistingMissionState(mission.id), mission, savedAt);
|
|
900
|
+
const next = { ...accounted, version: 1 as const, updatedAt: savedAt.toISOString() };
|
|
901
|
+
const content = JSON.stringify(next, null, 2) + "\n";
|
|
902
|
+
writeFileSync(join(MISSION_HISTORY_DIR, `${next.id}.json`), content, { encoding: "utf8", mode: 0o600 });
|
|
903
|
+
writeFileSync(LATEST_MISSION_FILE, content, { encoding: "utf8", mode: 0o600 });
|
|
904
|
+
return next;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
export function loadMissionState(id = "latest"): MissionState | undefined {
|
|
908
|
+
const safeId = id === "latest" ? "latest" : id.replace(/\.json$/i, "").replace(/[^A-Za-z0-9._-]/g, "");
|
|
909
|
+
const file = safeId === "latest" ? LATEST_MISSION_FILE : join(MISSION_HISTORY_DIR, `${safeId}.json`);
|
|
910
|
+
try {
|
|
911
|
+
if (!existsSync(file)) return undefined;
|
|
912
|
+
const parsed = JSON.parse(readFileSync(file, "utf8")) as MissionState;
|
|
913
|
+
return { ...parsed, version: 1 };
|
|
914
|
+
} catch {
|
|
915
|
+
return undefined;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
export function listMissionStates(): MissionState[] {
|
|
920
|
+
if (!existsSync(MISSION_HISTORY_DIR)) return [];
|
|
921
|
+
const missions: MissionState[] = [];
|
|
922
|
+
for (const entry of readdirSync(MISSION_HISTORY_DIR, { withFileTypes: true })) {
|
|
923
|
+
if (!entry.isFile() || !entry.name.endsWith(".json") || entry.name === "latest.json") continue;
|
|
924
|
+
try {
|
|
925
|
+
missions.push(JSON.parse(readFileSync(join(MISSION_HISTORY_DIR, entry.name), "utf8")) as MissionState);
|
|
926
|
+
} catch { /* skip unreadable mission */ }
|
|
927
|
+
}
|
|
928
|
+
return missions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
export function addMissionCheckpoint(mission: MissionState, summary: string, nextAction: string, milestoneId?: string, details: { filesChanged?: string[]; validationResult?: string; errors?: string[] } = {}): MissionState {
|
|
932
|
+
const id = `C${String((mission.checkpoints?.length ?? 0) + 1).padStart(4, "0")}`;
|
|
933
|
+
const checkpoint: MissionCheckpoint = {
|
|
934
|
+
id,
|
|
935
|
+
timestamp: new Date().toISOString(),
|
|
936
|
+
status: mission.status,
|
|
937
|
+
milestoneId,
|
|
938
|
+
summary: redactSecrets(summary) ?? summary,
|
|
939
|
+
nextAction: redactSecrets(nextAction) ?? nextAction,
|
|
940
|
+
filesChanged: details.filesChanged?.map((file) => redactSecrets(file) ?? file),
|
|
941
|
+
validationResult: redactSecrets(details.validationResult) ?? details.validationResult,
|
|
942
|
+
errors: details.errors?.map((error) => redactSecrets(error) ?? error),
|
|
943
|
+
};
|
|
944
|
+
const milestones = mission.milestones.map((milestone) => milestone.id === milestoneId
|
|
945
|
+
? { ...milestone, checkpointIds: [...(milestone.checkpointIds ?? []), id] }
|
|
946
|
+
: milestone);
|
|
947
|
+
return saveMissionState({ ...mission, milestones, checkpoints: [...(mission.checkpoints ?? []), checkpoint], lastSummary: checkpoint.summary });
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
export function compact(text: string | undefined, max = 1400): string {
|
|
951
|
+
if (!text) return "(none)";
|
|
952
|
+
const trimmed = text.trim();
|
|
953
|
+
return trimmed.length <= max ? trimmed : `${trimmed.slice(0, max)}\n...`;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
export function extractVerdict(report: string): WorkflowState["validationVerdict"] {
|
|
957
|
+
const verdictPatterns = [
|
|
958
|
+
/#{1,6}\s*(?:Final\s+)?Verdict\s*\n\s*(?:\*\*)?\s*(PARTIAL PASS|PASS|FAIL)\b/gi,
|
|
959
|
+
/\b(?:Final\s+)?Verdict\s*:\s*(PARTIAL PASS|PASS|FAIL)\b/gi,
|
|
960
|
+
/\bMilestone\s+Revalidation\b[\s\S]{0,400}?\bVerdict\s*:\s*(PARTIAL PASS|PASS|FAIL)\b/gi,
|
|
961
|
+
];
|
|
962
|
+
const matches: Array<{ index: number; verdict: WorkflowState["validationVerdict"] }> = [];
|
|
963
|
+
for (const pattern of verdictPatterns) {
|
|
964
|
+
for (const match of report.matchAll(pattern)) {
|
|
965
|
+
if (match.index === undefined || !match[1]) continue;
|
|
966
|
+
matches.push({ index: match.index, verdict: match[1].toUpperCase() as WorkflowState["validationVerdict"] });
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (matches.length > 0) return matches.sort((a, b) => b.index - a.index)[0].verdict;
|
|
970
|
+
const upper = report.toUpperCase();
|
|
971
|
+
if (/\bPARTIAL PASS\b/.test(upper)) return "PARTIAL PASS";
|
|
972
|
+
if (/\bFAIL\b/.test(upper)) return "FAIL";
|
|
973
|
+
if (/\bPASS\b/.test(upper)) return "PASS";
|
|
974
|
+
return "UNKNOWN";
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// No-op default export so this helper module can be safely auto-discovered as a Pi extension.
|
|
978
|
+
export default function workflowSuiteNoopExtension(): void {}
|