@mediadatafusion/pi-workflow-suite 0.0.10 → 0.0.13
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 +73 -0
- package/README.md +146 -20
- package/VERSION +1 -1
- package/agents/codebase-research.md +7 -5
- package/agents/general-worker.md +9 -7
- package/agents/implementation-planning.md +5 -3
- package/agents/quality-validation.md +9 -8
- package/agents/workflow-orchestrator.md +9 -7
- package/config/prompts/execute-approved-plan.md +12 -2
- package/config/prompts/mission-final-validation.md +38 -5
- package/config/prompts/mission-plan.md +17 -1
- package/config/prompts/mission-repair.md +16 -2
- package/config/prompts/mission-review-prompt.md +55 -0
- package/config/prompts/mission-run.md +18 -5
- package/config/prompts/validate-approved-plan.md +57 -3
- package/config/prompts/workflow-plan-prompt.md +11 -1
- package/config/prompts/workflow-repair.md +18 -2
- package/config/prompts/workflow-reviewer-prompt.md +60 -0
- package/config/prompts/workflow-summary.md +1 -4
- package/config/workflow-settings.example.json +13 -11
- package/extensions/subagent/index.ts +41 -18
- package/extensions/subagent/repolock-guard.ts +224 -4
- package/extensions/subagent/runner.ts +136 -12
- package/extensions/workflow-model-router.ts +152 -55
- package/extensions/workflow-modes.ts +4784 -1087
- package/extensions/workflow-settings-capabilities.ts +10 -0
- package/extensions/workflow-state.ts +139 -15
- package/extensions/workflow-subagent-policy.ts +13 -1
- package/extensions/workflow-summary.ts +8 -19
- package/extensions/workflow-tool-guard.ts +420 -39
- package/extensions/workflow-validation-classifier.ts +46 -4
- package/extensions/workflow-web-tools.ts +361 -1
- package/package.json +9 -5
- package/scripts/audit-live.sh +1 -1
- package/scripts/build-package-export.mjs +8 -13
- package/scripts/check-clean-release-tree.sh +3 -2
- package/scripts/check-package-media.mjs +78 -0
- package/scripts/install-to-live.sh +2 -0
- package/scripts/package-media-config.mjs +28 -0
- package/scripts/prepare-package-readme.mjs +19 -18
- package/scripts/quarantine-live-junk.sh +1 -1
- package/scripts/verify-live.sh +9 -1
- package/skills/implementation-planning/SKILL.md +1 -1
- package/skills/safe-execution/SKILL.md +1 -1
- package/skills/validation-review/SKILL.md +1 -1
|
@@ -240,6 +240,16 @@ const STANDARD_MISSION_CONTEXT_CAPABILITIES: CapabilityFactory[] = [
|
|
|
240
240
|
risk: "Can look fully enforced when current design keeps recovery supervised.",
|
|
241
241
|
action: "Preserve; label as planned/partial until user-supervised recovery actions are implemented.",
|
|
242
242
|
}),
|
|
243
|
+
() => capability({
|
|
244
|
+
path: "missions.missionHistoryLimit",
|
|
245
|
+
domain: "missions",
|
|
246
|
+
intent: "Limit retained saved Mission history records.",
|
|
247
|
+
owner: "saveMissionState / clearOldMissionStates / Mission History settings menu",
|
|
248
|
+
status: "wired",
|
|
249
|
+
related: ["workflow.planHistoryLimit"],
|
|
250
|
+
risk: "If misfiled under runtime settings, users may confuse saved history retention with Mission execution timers.",
|
|
251
|
+
action: "Keep surfaced under Mission History, parallel to Plan History.",
|
|
252
|
+
}),
|
|
243
253
|
() => capability({
|
|
244
254
|
path: "context.compactionMode",
|
|
245
255
|
domain: "context",
|
|
@@ -36,6 +36,13 @@ export interface WorkflowTypedHandoff {
|
|
|
36
36
|
payload: Record<string, unknown>;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export interface WorkflowReviewHandoffSuppression {
|
|
40
|
+
kind: "plan_typed_initial_to_approval" | "plan_typed_review_to_execution" | "mission_typed_review_to_approval";
|
|
41
|
+
createdAt: string;
|
|
42
|
+
activePlanId?: string;
|
|
43
|
+
activeMissionId?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
export interface WorkflowRepairHistoryEntry {
|
|
40
47
|
timestamp: string;
|
|
41
48
|
retry: number;
|
|
@@ -91,7 +98,7 @@ export interface StandardRuntimeState {
|
|
|
91
98
|
runtimeCounter: "running" | "paused" | "stopped";
|
|
92
99
|
}
|
|
93
100
|
|
|
94
|
-
export type PlanLifecycleStatus = "planning" | "awaiting_clarification" | "plan_ready" | "approved" | "reviewing" | "executing" | "validating" | "repairing" | "revalidating" | "completed" | "blocked";
|
|
101
|
+
export type PlanLifecycleStatus = "planning" | "awaiting_clarification" | "plan_ready" | "approved" | "reviewing" | "reviewed" | "executing" | "validating" | "repairing" | "revalidating" | "completed" | "blocked";
|
|
95
102
|
export type PlanStepStatus = "pending" | "active" | "completed" | "failed" | "blocked" | "skipped";
|
|
96
103
|
export type PlanValidationStatus = "pending" | "running" | "pass" | "partial pass" | "fail" | "unknown";
|
|
97
104
|
|
|
@@ -163,12 +170,47 @@ export interface CompletedPlanSummary {
|
|
|
163
170
|
finalReport?: string;
|
|
164
171
|
}
|
|
165
172
|
|
|
173
|
+
export interface BlockedPlanResumeSnapshot {
|
|
174
|
+
task?: string;
|
|
175
|
+
originalTask?: string;
|
|
176
|
+
approvedPlan?: string;
|
|
177
|
+
planHistoryId?: string;
|
|
178
|
+
approvedPlanHistoryId?: string;
|
|
179
|
+
executionSummary?: string;
|
|
180
|
+
validationReport?: string;
|
|
181
|
+
validationVerdict?: "PASS" | "PARTIAL PASS" | "FAIL" | "UNKNOWN";
|
|
182
|
+
lastValidationFailure?: string;
|
|
183
|
+
lastRepairAttempt?: string;
|
|
184
|
+
repairHistory?: WorkflowRepairHistoryEntry[];
|
|
185
|
+
lastRepairStatus?: "none" | "running" | "completed" | "failed" | "blocked";
|
|
186
|
+
currentValidationRetry?: number;
|
|
187
|
+
workflowValidationRetryCount?: number;
|
|
188
|
+
planRuntime?: PlanRuntimeState;
|
|
189
|
+
planProgress?: PlanProgressState;
|
|
190
|
+
reviewerReport?: string;
|
|
191
|
+
reviewerVerdict?: "PASS" | "NOTES" | "NEEDS REPAIR" | "FAIL" | "BLOCKED" | "UNKNOWN";
|
|
192
|
+
reviewHistory?: WorkflowReviewHistoryEntry[];
|
|
193
|
+
currentReviewRetry?: number;
|
|
194
|
+
workflowReviewRetryCount?: number;
|
|
195
|
+
lastReviewFailure?: string;
|
|
196
|
+
lastReviewAttempt?: string;
|
|
197
|
+
lastReviewRepairStatus?: "none" | "running" | "completed" | "failed" | "blocked";
|
|
198
|
+
reviewRepairInProgress?: boolean;
|
|
199
|
+
repairRetryState?: Partial<Record<RepairRetryGateName, RepairRetryGateState>>;
|
|
200
|
+
concreteRepairableIssue?: boolean;
|
|
201
|
+
manualVerificationRequired?: boolean;
|
|
202
|
+
evidenceGap?: boolean;
|
|
203
|
+
planTokensUsed?: number;
|
|
204
|
+
modelsUsed?: { planner?: string; executor?: string; validator?: string; reviewer?: string };
|
|
205
|
+
}
|
|
206
|
+
|
|
166
207
|
export interface WorkflowFinalStopSummary {
|
|
167
208
|
stoppedAt: string;
|
|
168
209
|
kind: "plan" | "mission";
|
|
169
210
|
status: "completed" | "blocked";
|
|
170
211
|
title: string;
|
|
171
212
|
summary: string;
|
|
213
|
+
blockedPlanSnapshot?: BlockedPlanResumeSnapshot;
|
|
172
214
|
}
|
|
173
215
|
|
|
174
216
|
export interface CompletedMissionSummary {
|
|
@@ -199,6 +241,7 @@ export interface WorkflowState {
|
|
|
199
241
|
clarifyingQuestions?: ClarificationQuestion[];
|
|
200
242
|
clarifyingAnswers?: ClarificationAnswer[];
|
|
201
243
|
lastWorkflowHandoff?: WorkflowTypedHandoff;
|
|
244
|
+
reviewHandoffSuppression?: WorkflowReviewHandoffSuppression;
|
|
202
245
|
clarificationAlreadyAsked?: boolean;
|
|
203
246
|
clarificationRequiredBeforePlan?: boolean;
|
|
204
247
|
clarificationRequirementReason?: string;
|
|
@@ -226,6 +269,9 @@ export interface WorkflowState {
|
|
|
226
269
|
executionSummary?: string;
|
|
227
270
|
validationReport?: string;
|
|
228
271
|
validationVerdict?: "PASS" | "PARTIAL PASS" | "FAIL" | "UNKNOWN";
|
|
272
|
+
currentValidationHandoffRetry?: number;
|
|
273
|
+
maxValidationHandoffRetries?: number;
|
|
274
|
+
lastValidationHandoffFailure?: string;
|
|
229
275
|
currentValidationRetry?: number;
|
|
230
276
|
workflowValidationRetryCount?: number;
|
|
231
277
|
maxValidationRetriesPerPlan?: number;
|
|
@@ -234,10 +280,21 @@ export interface WorkflowState {
|
|
|
234
280
|
lastRepairAttempt?: string;
|
|
235
281
|
repairHistory?: WorkflowRepairHistoryEntry[];
|
|
236
282
|
lastRepairStatus?: "none" | "running" | "completed" | "failed" | "blocked";
|
|
283
|
+
concreteRepairableIssue?: boolean;
|
|
284
|
+
manualVerificationRequired?: boolean;
|
|
285
|
+
evidenceGap?: boolean;
|
|
286
|
+
lastValidationCompletedAt?: string;
|
|
237
287
|
planStepValidationIndex?: number;
|
|
238
288
|
planExecutionStepIndex?: number;
|
|
239
289
|
planRuntime?: PlanRuntimeState;
|
|
290
|
+
planRuntimeHoldActive?: boolean;
|
|
240
291
|
planProgress?: PlanProgressState;
|
|
292
|
+
planProgressLastToolStep?: number;
|
|
293
|
+
planProgressLastToolStatus?: PlanStepStatus;
|
|
294
|
+
planProgressLastToolAt?: string;
|
|
295
|
+
planTokensUsed?: number;
|
|
296
|
+
missionTokensUsed?: number;
|
|
297
|
+
standardTokensUsed?: number;
|
|
241
298
|
standardRuntime?: StandardRuntimeState;
|
|
242
299
|
standardTodo?: StandardTodoState;
|
|
243
300
|
standardLastAutoCheckAt?: string;
|
|
@@ -291,6 +348,15 @@ export interface SavedWorkflowPlan {
|
|
|
291
348
|
finalReport?: string;
|
|
292
349
|
modelsUsed?: WorkflowState["modelsUsed"];
|
|
293
350
|
subagents?: Record<string, unknown>;
|
|
351
|
+
planProgress?: WorkflowState["planProgress"];
|
|
352
|
+
planRuntime?: WorkflowState["planRuntime"];
|
|
353
|
+
planExecutionStepIndex?: number;
|
|
354
|
+
planStepValidationIndex?: number;
|
|
355
|
+
currentValidationRetry?: number;
|
|
356
|
+
workflowValidationRetryCount?: number;
|
|
357
|
+
repairRetryState?: WorkflowState["repairRetryState"];
|
|
358
|
+
repairHistory?: WorkflowState["repairHistory"];
|
|
359
|
+
reviewHistory?: WorkflowState["reviewHistory"];
|
|
294
360
|
}
|
|
295
361
|
|
|
296
362
|
export interface PlanSavingOptions {
|
|
@@ -309,6 +375,10 @@ export interface PlanSavingOptions {
|
|
|
309
375
|
planHistoryLimit?: number;
|
|
310
376
|
}
|
|
311
377
|
|
|
378
|
+
export interface MissionSavingOptions {
|
|
379
|
+
missionHistoryLimit?: number;
|
|
380
|
+
}
|
|
381
|
+
|
|
312
382
|
export type MissionStatus = "draft" | "planning" | "awaiting_clarification" | "planned" | "approved" | "running" | "paused" | "checkpointing" | "validating" | "repairing" | "revalidating" | "completed" | "failed" | "blocked" | "stopped";
|
|
313
383
|
export type MissionAutonomy = "manual" | "approval_gated" | "supervised_auto" | "full_auto";
|
|
314
384
|
export type MissionMilestoneStatus = "pending" | "active" | "completed" | "failed" | "skipped";
|
|
@@ -385,6 +455,9 @@ export interface MissionState {
|
|
|
385
455
|
reviewHistory?: WorkflowReviewHistoryEntry[];
|
|
386
456
|
reviewRepairInProgress?: boolean;
|
|
387
457
|
lastValidationResult?: string;
|
|
458
|
+
concreteRepairableIssue?: boolean;
|
|
459
|
+
manualVerificationRequired?: boolean;
|
|
460
|
+
evidenceGap?: boolean;
|
|
388
461
|
modelsUsed: Record<string, string>;
|
|
389
462
|
subagentsUsed: string[];
|
|
390
463
|
approvalRequired: boolean;
|
|
@@ -397,6 +470,7 @@ export interface MissionState {
|
|
|
397
470
|
heartbeatCount?: number;
|
|
398
471
|
activeRuntimeMs?: number;
|
|
399
472
|
activeRunStartedAt?: string | null;
|
|
473
|
+
runtimeHoldActive?: boolean;
|
|
400
474
|
lastPausedAt?: string;
|
|
401
475
|
lastResumedAt?: string;
|
|
402
476
|
lastStoppedAt?: string;
|
|
@@ -427,10 +501,13 @@ export function emptyState(): WorkflowState {
|
|
|
427
501
|
return { version: 1, mode: "idle", updatedAt: new Date().toISOString() };
|
|
428
502
|
}
|
|
429
503
|
|
|
504
|
+
const VALID_MODES = new Set<string>(["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"]);
|
|
505
|
+
|
|
430
506
|
export function loadState(): WorkflowState {
|
|
431
507
|
try {
|
|
432
508
|
if (!existsSync(ACTIVE_STATE_FILE)) return emptyState();
|
|
433
509
|
const parsed = JSON.parse(readFileSync(ACTIVE_STATE_FILE, "utf8")) as WorkflowState;
|
|
510
|
+
if (parsed.mode && !VALID_MODES.has(parsed.mode)) return emptyState();
|
|
434
511
|
return { ...emptyState(), ...parsed, version: 1 };
|
|
435
512
|
} catch {
|
|
436
513
|
return emptyState();
|
|
@@ -535,6 +612,15 @@ export function saveWorkflowPlan(state: WorkflowState, options: PlanSavingOption
|
|
|
535
612
|
finalReport: options.finalReport?.trim() ? (redactSecrets(compact(options.finalReport, 5000)) ?? compact(options.finalReport, 5000)) : undefined,
|
|
536
613
|
modelsUsed: state.modelsUsed,
|
|
537
614
|
subagents: options.subagents,
|
|
615
|
+
planProgress: state.planProgress,
|
|
616
|
+
planRuntime: state.planRuntime,
|
|
617
|
+
planExecutionStepIndex: state.planExecutionStepIndex,
|
|
618
|
+
planStepValidationIndex: state.planStepValidationIndex,
|
|
619
|
+
currentValidationRetry: state.currentValidationRetry,
|
|
620
|
+
workflowValidationRetryCount: state.workflowValidationRetryCount,
|
|
621
|
+
repairRetryState: state.repairRetryState,
|
|
622
|
+
repairHistory: state.repairHistory,
|
|
623
|
+
reviewHistory: state.reviewHistory,
|
|
538
624
|
};
|
|
539
625
|
|
|
540
626
|
writeFileSync(LATEST_PLAN_FILE, JSON.stringify(record, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
@@ -659,6 +745,15 @@ export function isMissionRuntimeActiveStatus(status?: MissionStatus): boolean {
|
|
|
659
745
|
return status === "planning" || status === "running" || status === "validating" || status === "repairing" || status === "revalidating" || status === "checkpointing";
|
|
660
746
|
}
|
|
661
747
|
|
|
748
|
+
export function isMissionRuntimeActive(mission?: MissionState): boolean {
|
|
749
|
+
if (!mission || mission.status === "completed" || mission.status === "failed" || mission.status === "stopped") return false;
|
|
750
|
+
return isMissionRuntimeActiveStatus(mission.status) || mission.runtimeHoldActive === true;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export function isMissionRuntimeHeldActive(mission?: MissionState): boolean {
|
|
754
|
+
return Boolean(mission && !isMissionRuntimeActiveStatus(mission.status) && mission.runtimeHoldActive === true);
|
|
755
|
+
}
|
|
756
|
+
|
|
662
757
|
function runtimeEndReason(status: MissionStatus): MissionRuntimeSegment["reasonEnded"] {
|
|
663
758
|
if (status === "paused") return "paused";
|
|
664
759
|
if (status === "blocked") return "blocked";
|
|
@@ -686,9 +781,18 @@ export function isPlanRuntimeActiveMode(mode?: WorkflowMode): boolean {
|
|
|
686
781
|
return mode === "planning" || mode === "reviewing" || mode === "executing" || mode === "validating" || mode === "repairing" || mode === "revalidating";
|
|
687
782
|
}
|
|
688
783
|
|
|
784
|
+
export function isPlanRuntimeActive(state?: WorkflowState): boolean {
|
|
785
|
+
if (!state || state.mode === "idle" || state.mode === "cancelled") return false;
|
|
786
|
+
return isPlanRuntimeActiveMode(state.mode) || state.planRuntimeHoldActive === true;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
export function isPlanRuntimeHeldActive(state?: WorkflowState): boolean {
|
|
790
|
+
return Boolean(state && !isPlanRuntimeActiveMode(state.mode) && state.planRuntimeHoldActive === true);
|
|
791
|
+
}
|
|
792
|
+
|
|
689
793
|
export function planRuntimeCounterState(state: WorkflowState): "running" | "paused" | "stopped" {
|
|
690
794
|
if (state.mode === "idle" || state.mode === "cancelled") return "stopped";
|
|
691
|
-
if (
|
|
795
|
+
if (isPlanRuntimeActive(state)) return "running";
|
|
692
796
|
return "paused";
|
|
693
797
|
}
|
|
694
798
|
|
|
@@ -714,8 +818,10 @@ function activeElapsedMs(startedAt: string | null | undefined, nowMs: number, la
|
|
|
714
818
|
const parsed = Date.parse(startedAt ?? "");
|
|
715
819
|
if (!Number.isFinite(parsed)) return 0;
|
|
716
820
|
const updated = Date.parse(lastUpdatedAt ?? "");
|
|
717
|
-
const end = parsed < RUNTIME_SESSION_STARTED_AT_MS
|
|
718
|
-
?
|
|
821
|
+
const end = parsed < RUNTIME_SESSION_STARTED_AT_MS
|
|
822
|
+
? (Number.isFinite(updated) && updated < RUNTIME_SESSION_STARTED_AT_MS
|
|
823
|
+
? Math.max(parsed, updated)
|
|
824
|
+
: RUNTIME_SESSION_STARTED_AT_MS)
|
|
719
825
|
: nowMs;
|
|
720
826
|
return Math.max(0, end - parsed);
|
|
721
827
|
}
|
|
@@ -731,8 +837,8 @@ export function applyPlanRuntimeAccounting(previous: WorkflowState | undefined,
|
|
|
731
837
|
const createdAt = currentRuntime?.createdAt ?? previousRuntime?.createdAt ?? nowIso;
|
|
732
838
|
const baseRuntimeMs = safeRuntimeMs(currentRuntime?.activeRuntimeMs ?? previousRuntime?.activeRuntimeMs);
|
|
733
839
|
const previousStartedAt = previousRuntime?.activeRunStartedAt ?? currentRuntime?.activeRunStartedAt ?? null;
|
|
734
|
-
const previousActive =
|
|
735
|
-
const nextActive =
|
|
840
|
+
const previousActive = isPlanRuntimeActive(previous);
|
|
841
|
+
const nextActive = isPlanRuntimeActive(state);
|
|
736
842
|
|
|
737
843
|
let activeRuntimeMs = baseRuntimeMs;
|
|
738
844
|
let activeRunStartedAt = currentRuntime?.activeRunStartedAt ?? previousStartedAt ?? null;
|
|
@@ -763,14 +869,16 @@ export function applyPlanRuntimeAccounting(previous: WorkflowState | undefined,
|
|
|
763
869
|
export function planActiveRuntimeMs(state: WorkflowState, now = new Date()): number {
|
|
764
870
|
const runtime = state.planRuntime;
|
|
765
871
|
const base = safeRuntimeMs(runtime?.activeRuntimeMs);
|
|
766
|
-
if (!runtime || !
|
|
872
|
+
if (!runtime || !isPlanRuntimeActive(state)) return base;
|
|
767
873
|
return base + activeElapsedMs(runtime.activeRunStartedAt, now.getTime(), state.updatedAt);
|
|
768
874
|
}
|
|
769
875
|
|
|
770
876
|
export function planWallClockAgeMs(state: WorkflowState, now = new Date()): number {
|
|
771
877
|
const start = Date.parse(state.planRuntime?.createdAt ?? "");
|
|
772
878
|
if (!Number.isFinite(start)) return 0;
|
|
773
|
-
|
|
879
|
+
const terminalTimestamp = planRuntimeCounterState(state) === "stopped" ? state.updatedAt : undefined;
|
|
880
|
+
const end = terminalTimestamp ? Date.parse(terminalTimestamp) : now.getTime();
|
|
881
|
+
return Math.max(0, (Number.isFinite(end) ? end : now.getTime()) - start);
|
|
774
882
|
}
|
|
775
883
|
|
|
776
884
|
export function applyStandardRuntimeAccounting(previous: WorkflowState | undefined, state: WorkflowState, now = new Date()): WorkflowState {
|
|
@@ -826,14 +934,16 @@ export function standardActiveRuntimeMs(state: WorkflowState, now = new Date()):
|
|
|
826
934
|
export function standardWallClockAgeMs(state: WorkflowState, now = new Date()): number {
|
|
827
935
|
const start = Date.parse(state.standardRuntime?.createdAt ?? "");
|
|
828
936
|
if (!Number.isFinite(start)) return 0;
|
|
829
|
-
|
|
937
|
+
const terminalTimestamp = standardRuntimeCounterState(state) === "stopped" ? state.updatedAt : undefined;
|
|
938
|
+
const end = terminalTimestamp ? Date.parse(terminalTimestamp) : now.getTime();
|
|
939
|
+
return Math.max(0, (Number.isFinite(end) ? end : now.getTime()) - start);
|
|
830
940
|
}
|
|
831
941
|
|
|
832
942
|
export function applyMissionRuntimeAccounting(previous: MissionState | undefined, mission: MissionState, now = new Date()): MissionState {
|
|
833
943
|
const nowIso = now.toISOString();
|
|
834
944
|
const nowMs = now.getTime();
|
|
835
|
-
const previousActive =
|
|
836
|
-
const nextActive =
|
|
945
|
+
const previousActive = isMissionRuntimeActive(previous);
|
|
946
|
+
const nextActive = isMissionRuntimeActive(mission);
|
|
837
947
|
const previousStartedAt = previous?.activeRunStartedAt ?? mission.activeRunStartedAt ?? null;
|
|
838
948
|
const baseRuntimeMs = safeRuntimeMs(mission.activeRuntimeMs ?? previous?.activeRuntimeMs);
|
|
839
949
|
const baseSegments = mission.runtimeSegments ?? previous?.runtimeSegments ?? [];
|
|
@@ -859,7 +969,7 @@ export function applyMissionRuntimeAccounting(previous: MissionState | undefined
|
|
|
859
969
|
lastResumedAt: mission.lastResumedAt ?? nowIso,
|
|
860
970
|
};
|
|
861
971
|
} else if (nextActive && previousStartedAt) {
|
|
862
|
-
next = { ...next, activeRunStartedAt: previousStartedAt };
|
|
972
|
+
next = { ...next, activeRunStartedAt: Date.parse(previousStartedAt) < RUNTIME_SESSION_STARTED_AT_MS ? nowIso : previousStartedAt };
|
|
863
973
|
} else if (!nextActive) {
|
|
864
974
|
next = { ...next, activeRunStartedAt: null };
|
|
865
975
|
}
|
|
@@ -872,7 +982,7 @@ export function applyMissionRuntimeAccounting(previous: MissionState | undefined
|
|
|
872
982
|
|
|
873
983
|
export function missionActiveRuntimeMs(mission: MissionState, now = new Date()): number {
|
|
874
984
|
const base = safeRuntimeMs(mission.activeRuntimeMs);
|
|
875
|
-
if (!
|
|
985
|
+
if (!isMissionRuntimeActive(mission)) return base;
|
|
876
986
|
return base + activeElapsedMs(mission.activeRunStartedAt, now.getTime(), mission.updatedAt);
|
|
877
987
|
}
|
|
878
988
|
|
|
@@ -885,7 +995,7 @@ export function missionWallClockAgeMs(mission: MissionState, now = new Date()):
|
|
|
885
995
|
}
|
|
886
996
|
|
|
887
997
|
export function missionRuntimeCounterState(mission: MissionState): "running" | "paused" | "blocked" | "stopped" | "completed" | "failed" | "waiting" {
|
|
888
|
-
if (
|
|
998
|
+
if (isMissionRuntimeActive(mission)) return "running";
|
|
889
999
|
if (mission.status === "paused") return "paused";
|
|
890
1000
|
if (mission.status === "blocked") return "blocked";
|
|
891
1001
|
if (mission.status === "stopped") return "stopped";
|
|
@@ -894,7 +1004,7 @@ export function missionRuntimeCounterState(mission: MissionState): "running" | "
|
|
|
894
1004
|
return "waiting";
|
|
895
1005
|
}
|
|
896
1006
|
|
|
897
|
-
export function saveMissionState(mission: MissionState): MissionState {
|
|
1007
|
+
export function saveMissionState(mission: MissionState, options: MissionSavingOptions = {}): MissionState {
|
|
898
1008
|
mkdirSync(MISSION_HISTORY_DIR, { recursive: true });
|
|
899
1009
|
const savedAt = new Date();
|
|
900
1010
|
const accounted = applyMissionRuntimeAccounting(readExistingMissionState(mission.id), mission, savedAt);
|
|
@@ -902,6 +1012,7 @@ export function saveMissionState(mission: MissionState): MissionState {
|
|
|
902
1012
|
const content = JSON.stringify(next, null, 2) + "\n";
|
|
903
1013
|
writeFileSync(join(MISSION_HISTORY_DIR, `${next.id}.json`), content, { encoding: "utf8", mode: 0o600 });
|
|
904
1014
|
writeFileSync(LATEST_MISSION_FILE, content, { encoding: "utf8", mode: 0o600 });
|
|
1015
|
+
clearOldMissionStates(options.missionHistoryLimit ?? 50);
|
|
905
1016
|
return next;
|
|
906
1017
|
}
|
|
907
1018
|
|
|
@@ -929,6 +1040,19 @@ export function listMissionStates(): MissionState[] {
|
|
|
929
1040
|
return missions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
930
1041
|
}
|
|
931
1042
|
|
|
1043
|
+
export function clearOldMissionStates(limit = 50): number {
|
|
1044
|
+
const safeLimit = Math.max(1, Math.min(500, Math.floor(limit)));
|
|
1045
|
+
const missions = listMissionStates();
|
|
1046
|
+
let removed = 0;
|
|
1047
|
+
for (const mission of missions.slice(safeLimit)) {
|
|
1048
|
+
try {
|
|
1049
|
+
unlinkSync(join(MISSION_HISTORY_DIR, `${mission.id}.json`));
|
|
1050
|
+
removed++;
|
|
1051
|
+
} catch { /* ignore */ }
|
|
1052
|
+
}
|
|
1053
|
+
return removed;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
932
1056
|
export function addMissionCheckpoint(mission: MissionState, summary: string, nextAction: string, milestoneId?: string, details: { filesChanged?: string[]; validationResult?: string; errors?: string[] } = {}): MissionState {
|
|
933
1057
|
const id = `C${String((mission.checkpoints?.length ?? 0) + 1).padStart(4, "0")}`;
|
|
934
1058
|
const checkpoint: MissionCheckpoint = {
|
|
@@ -21,7 +21,7 @@ export interface SubagentToolProfile {
|
|
|
21
21
|
source?: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const MUTATING_SUBAGENT_TOOLS = new Set(["edit"
|
|
24
|
+
const MUTATING_SUBAGENT_TOOLS = new Set(["edit"]);
|
|
25
25
|
const ORCHESTRATOR_AGENT_NAME = "workflow-orchestrator";
|
|
26
26
|
|
|
27
27
|
export function subagentToolsAllowMutation(tools?: string[]): boolean {
|
|
@@ -176,5 +176,17 @@ export function planningNeedsOrchestrator(settings: WorkflowSettings, _mode: "pl
|
|
|
176
176
|
return orchestrationPolicy === "orchestrator_first" || orchestrationPolicy === "forced_orchestrated";
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// ── Uniform error classification (#9) ──────────────────────────
|
|
180
|
+
export type SubagentErrorClass = "transient" | "permanent" | "policy";
|
|
181
|
+
|
|
182
|
+
export function classifySubagentError(result: { exitCode: number; stopReason?: string; errorMessage?: string; stderr?: string }): SubagentErrorClass {
|
|
183
|
+
const reason = (result.errorMessage ?? result.stderr ?? "").toLowerCase();
|
|
184
|
+
if (/timed out|stale watchdog|aborted/i.test(reason) || (result.stopReason === "aborted" && /time/i.test(reason))) return "transient";
|
|
185
|
+
if (/repo lock|outside current repository/i.test(reason)) return "policy";
|
|
186
|
+
if (/unknown agent|not installed|not found/i.test(reason)) return "permanent";
|
|
187
|
+
if (result.exitCode === 0) return "transient"; // success
|
|
188
|
+
return "permanent";
|
|
189
|
+
}
|
|
190
|
+
|
|
179
191
|
// No-op default export so this helper module can be safely auto-discovered as a Pi extension.
|
|
180
192
|
export default function workflowSuiteNoopExtension(): void {}
|
|
@@ -100,15 +100,6 @@ function gitChangedFilesLine(status: string | undefined): string {
|
|
|
100
100
|
return `${files.length} changed/untracked file(s): ${preview}${files.length > 16 ? ", ..." : ""}`;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function workflowSuitePublicImpact(root: string, pkg: Record<string, unknown> | undefined, status: string | undefined): string {
|
|
104
|
-
if (pkg?.name !== "@mediadatafusion/pi-workflow-suite") return "not applicable unless the target repo is the Pi Workflow Suite package";
|
|
105
|
-
const files = (status ?? "").split("\n").map((line) => line.trim().slice(3).trim()).filter(Boolean);
|
|
106
|
-
if (!files.length) return "Pi Workflow Suite package repo detected; no current git changes detected";
|
|
107
|
-
const publicPrefixes = ["extensions/", "agents/", "skills/", "config/", "docs/", "scripts/", "README.md", "LICENSE.md", "package.json", "package-lock.json", "tsconfig.json", "AGENTS.md"];
|
|
108
|
-
const publicFiles = files.filter((file) => publicPrefixes.some((prefix) => file === prefix || file.startsWith(prefix)));
|
|
109
|
-
return publicFiles.length ? `yes — public/live package files touched: ${publicFiles.slice(0, 12).join(", ")}${publicFiles.length > 12 ? ", ..." : ""}` : "Pi Workflow Suite package repo detected; changed files are not in public package paths";
|
|
110
|
-
}
|
|
111
|
-
|
|
112
103
|
export function renderHandoffProjectContext(cwd?: string): string {
|
|
113
104
|
const current = cwd ?? process.cwd();
|
|
114
105
|
const repoRoot = safeGit(current, ["rev-parse", "--show-toplevel"]);
|
|
@@ -118,7 +109,6 @@ export function renderHandoffProjectContext(cwd?: string): string {
|
|
|
118
109
|
const head = safeGit(root, ["rev-parse", "--short", "HEAD"]);
|
|
119
110
|
const status = safeGit(root, ["status", "--short"]);
|
|
120
111
|
const instructions = detectedInstructionFiles(root);
|
|
121
|
-
const isSuite = pkg?.name === "@mediadatafusion/pi-workflow-suite";
|
|
122
112
|
return `## Target Application Context
|
|
123
113
|
- CWD: ${current}
|
|
124
114
|
- Git root: ${repoRoot ?? "not detected"}
|
|
@@ -126,14 +116,7 @@ export function renderHandoffProjectContext(cwd?: string): string {
|
|
|
126
116
|
- HEAD: ${head ?? "unknown"}
|
|
127
117
|
- Application profile: ${detectProjectProfile(root, pkg)}
|
|
128
118
|
- Project instructions detected: ${instructions.length ? instructions.join(", ") : "none"}
|
|
129
|
-
- Changed files: ${gitChangedFilesLine(status)}
|
|
130
|
-
|
|
131
|
-
## Pi Workflow Suite Context
|
|
132
|
-
- Target is Pi Workflow Suite package repo: ${isSuite ? "yes" : "no"}
|
|
133
|
-
- Context boundary: keep the target application repo, the Workflow Suite DEV worktree, the live Pi runtime, and the public main package mirror distinct.
|
|
134
|
-
- Public package impact: ${workflowSuitePublicImpact(root, pkg, status)}
|
|
135
|
-
- Live runtime sync: only confirmed when scripts/install-to-live.sh has been run and reports auth/settings/sessions/workflow state were not touched.
|
|
136
|
-
- Promotion expectation for suite package changes: validate on DEV, sync live when requested, promote the same public-safe files to main, validate main, push both branches, then verify origin/main..origin/DEV parity.`;
|
|
119
|
+
- Changed files: ${gitChangedFilesLine(status)}`;
|
|
137
120
|
}
|
|
138
121
|
|
|
139
122
|
function planNeedsClarification(text?: string): boolean {
|
|
@@ -145,11 +128,17 @@ function planNeedsClarification(text?: string): boolean {
|
|
|
145
128
|
}
|
|
146
129
|
|
|
147
130
|
function planStatus(state: WorkflowState): string {
|
|
131
|
+
if (state.planProgress?.lifecycleStatus === "blocked") return "Blocked";
|
|
132
|
+
if (planReviewRepairActive(state)) return "Repairing";
|
|
148
133
|
if (state.approvedPlan) return "Approved";
|
|
149
134
|
if (state.draftPlan) return "Draft";
|
|
150
135
|
return "None";
|
|
151
136
|
}
|
|
152
137
|
|
|
138
|
+
function planReviewRepairActive(state: WorkflowState): boolean {
|
|
139
|
+
return state.reviewRepairInProgress === true || state.lastReviewRepairStatus === "running" || state.repairRetryState?.review?.inProgress === true;
|
|
140
|
+
}
|
|
141
|
+
|
|
153
142
|
function isMissionMode(mode: string): boolean {
|
|
154
143
|
return mode === "awaiting_mission_input" || mode.startsWith("mission_");
|
|
155
144
|
}
|
|
@@ -374,7 +363,7 @@ export function renderWorkflowSummary(state: WorkflowState, cwd?: string): strin
|
|
|
374
363
|
if (finalStop && (state.mode === "awaiting_plan_input" || state.mode === "awaiting_mission_input" || state.mode === "validated" || state.mode === "mission_blocked" || state.mode === "mission_completed" || state.mode === "mission_failed" || state.mode === "mission_stopped")) {
|
|
375
364
|
return `# Workflow Summary\n\n${finalStop}`;
|
|
376
365
|
}
|
|
377
|
-
return `# Workflow Summary\n\n${renderHandoffProjectContext(cwd)}\n\n## Original Task\n${state.task ?? "(none)"}\n\n## Models Used\n- Planner: ${state.modelsUsed?.planner ?? "(not recorded)"}\n- Executor: ${state.modelsUsed?.executor ?? "(not recorded)"}\n- Validator: ${state.modelsUsed?.validator ?? "(not run)"}\n- Reviewer: ${state.modelsUsed?.reviewer ?? "(not run)"}\n\n## Current Model Configuration\n${renderWorkflowModels(settings)}\n\n## Approved Plan\n${compact(state.approvedPlan, 2200)}\n\n## Execution Summary\n${compact(state.executionSummary, 1800)}\n\n## Validation Result\n${state.validationVerdict ?? "(not validated)"}\n\n${compact(state.validationReport, 1800)}\n\n## Remaining Risks\nReview validation notes, unrun tests, changed files
|
|
366
|
+
return `# Workflow Summary\n\n${renderHandoffProjectContext(cwd)}\n\n## Original Task\n${state.task ?? "(none)"}\n\n## Models Used\n- Planner: ${state.modelsUsed?.planner ?? "(not recorded)"}\n- Executor: ${state.modelsUsed?.executor ?? "(not recorded)"}\n- Validator: ${state.modelsUsed?.validator ?? "(not run)"}\n- Reviewer: ${state.modelsUsed?.reviewer ?? "(not run)"}\n\n## Current Model Configuration\n${renderWorkflowModels(settings)}\n\n## Approved Plan\n${compact(state.approvedPlan, 2200)}\n\n## Execution Summary\n${compact(state.executionSummary, 1800)}\n\n## Validation Result\n${state.validationVerdict ?? "(not validated)"}\n\n${compact(state.validationReport, 1800)}\n\n## Remaining Risks\nReview validation notes, unrun tests, and changed files before committing or promoting.\n\n## Recommended Next Action\nRun project checks manually if they were not run, then review the target repo diff.\n\n## Exact Resume Instructions\n- Re-open the target repo shown above and confirm branch/status.\n- Run /workflow status before continuing.\n- Review this summary alongside the saved plan record when available.\n- Re-read detected project instruction files before any new edits.`;
|
|
378
367
|
}
|
|
379
368
|
|
|
380
369
|
// No-op default export so this helper module can be safely auto-discovered as a Pi extension.
|