@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +146 -20
  3. package/VERSION +1 -1
  4. package/agents/codebase-research.md +7 -5
  5. package/agents/general-worker.md +9 -7
  6. package/agents/implementation-planning.md +5 -3
  7. package/agents/quality-validation.md +9 -8
  8. package/agents/workflow-orchestrator.md +9 -7
  9. package/config/prompts/execute-approved-plan.md +12 -2
  10. package/config/prompts/mission-final-validation.md +38 -5
  11. package/config/prompts/mission-plan.md +17 -1
  12. package/config/prompts/mission-repair.md +16 -2
  13. package/config/prompts/mission-review-prompt.md +55 -0
  14. package/config/prompts/mission-run.md +18 -5
  15. package/config/prompts/validate-approved-plan.md +57 -3
  16. package/config/prompts/workflow-plan-prompt.md +11 -1
  17. package/config/prompts/workflow-repair.md +18 -2
  18. package/config/prompts/workflow-reviewer-prompt.md +60 -0
  19. package/config/prompts/workflow-summary.md +1 -4
  20. package/config/workflow-settings.example.json +13 -11
  21. package/extensions/subagent/index.ts +41 -18
  22. package/extensions/subagent/repolock-guard.ts +224 -4
  23. package/extensions/subagent/runner.ts +136 -12
  24. package/extensions/workflow-model-router.ts +152 -55
  25. package/extensions/workflow-modes.ts +4784 -1087
  26. package/extensions/workflow-settings-capabilities.ts +10 -0
  27. package/extensions/workflow-state.ts +139 -15
  28. package/extensions/workflow-subagent-policy.ts +13 -1
  29. package/extensions/workflow-summary.ts +8 -19
  30. package/extensions/workflow-tool-guard.ts +420 -39
  31. package/extensions/workflow-validation-classifier.ts +46 -4
  32. package/extensions/workflow-web-tools.ts +361 -1
  33. package/package.json +9 -5
  34. package/scripts/audit-live.sh +1 -1
  35. package/scripts/build-package-export.mjs +8 -13
  36. package/scripts/check-clean-release-tree.sh +3 -2
  37. package/scripts/check-package-media.mjs +78 -0
  38. package/scripts/install-to-live.sh +2 -0
  39. package/scripts/package-media-config.mjs +28 -0
  40. package/scripts/prepare-package-readme.mjs +19 -18
  41. package/scripts/quarantine-live-junk.sh +1 -1
  42. package/scripts/verify-live.sh +9 -1
  43. package/skills/implementation-planning/SKILL.md +1 -1
  44. package/skills/safe-execution/SKILL.md +1 -1
  45. 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 (isPlanRuntimeActiveMode(state.mode)) return "running";
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 && Number.isFinite(updated) && updated < RUNTIME_SESSION_STARTED_AT_MS
718
- ? Math.max(parsed, updated)
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 = isPlanRuntimeActiveMode(previous?.mode);
735
- const nextActive = isPlanRuntimeActiveMode(state.mode);
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 || !isPlanRuntimeActiveMode(state.mode)) return base;
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
- return Math.max(0, now.getTime() - start);
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
- return Math.max(0, now.getTime() - start);
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 = isMissionRuntimeActiveStatus(previous?.status);
836
- const nextActive = isMissionRuntimeActiveStatus(mission.status);
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 (!isMissionRuntimeActiveStatus(mission.status)) return base;
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 (isMissionRuntimeActiveStatus(mission.status)) return "running";
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", "write"]);
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, and public/internal package impact before committing or promoting.\n\n## Recommended Next Action\nRun project checks manually if they were not run, then review the target repo diff. For Pi Workflow Suite package work, complete DEV validation, live sync if requested, main promotion, main validation, and branch parity verification.\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.\n\n## Suggested Commit Message\nImplement approved workflow plan`;
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.