@mediadatafusion/pi-workflow-suite 0.0.10 → 0.0.11

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.
@@ -163,12 +163,32 @@ export interface CompletedPlanSummary {
163
163
  finalReport?: string;
164
164
  }
165
165
 
166
+ export interface BlockedPlanResumeSnapshot {
167
+ task?: string;
168
+ originalTask?: string;
169
+ approvedPlan?: string;
170
+ planHistoryId?: string;
171
+ approvedPlanHistoryId?: string;
172
+ executionSummary?: string;
173
+ validationReport?: string;
174
+ validationVerdict?: "PASS" | "PARTIAL PASS" | "FAIL" | "UNKNOWN";
175
+ lastValidationFailure?: string;
176
+ lastRepairAttempt?: string;
177
+ repairHistory?: WorkflowRepairHistoryEntry[];
178
+ lastRepairStatus?: "none" | "running" | "completed" | "failed" | "blocked";
179
+ currentValidationRetry?: number;
180
+ workflowValidationRetryCount?: number;
181
+ planRuntime?: PlanRuntimeState;
182
+ planProgress?: PlanProgressState;
183
+ }
184
+
166
185
  export interface WorkflowFinalStopSummary {
167
186
  stoppedAt: string;
168
187
  kind: "plan" | "mission";
169
188
  status: "completed" | "blocked";
170
189
  title: string;
171
190
  summary: string;
191
+ blockedPlanSnapshot?: BlockedPlanResumeSnapshot;
172
192
  }
173
193
 
174
194
  export interface CompletedMissionSummary {
@@ -234,10 +254,20 @@ export interface WorkflowState {
234
254
  lastRepairAttempt?: string;
235
255
  repairHistory?: WorkflowRepairHistoryEntry[];
236
256
  lastRepairStatus?: "none" | "running" | "completed" | "failed" | "blocked";
257
+ concreteRepairableIssue?: boolean;
258
+ manualVerificationRequired?: boolean;
259
+ evidenceGap?: boolean;
260
+ lastValidationCompletedAt?: string;
237
261
  planStepValidationIndex?: number;
238
262
  planExecutionStepIndex?: number;
239
263
  planRuntime?: PlanRuntimeState;
240
264
  planProgress?: PlanProgressState;
265
+ planProgressLastToolStep?: number;
266
+ planProgressLastToolStatus?: PlanStepStatus;
267
+ planProgressLastToolAt?: string;
268
+ planTokensUsed?: number;
269
+ missionTokensUsed?: number;
270
+ standardTokensUsed?: number;
241
271
  standardRuntime?: StandardRuntimeState;
242
272
  standardTodo?: StandardTodoState;
243
273
  standardLastAutoCheckAt?: string;
@@ -291,6 +321,15 @@ export interface SavedWorkflowPlan {
291
321
  finalReport?: string;
292
322
  modelsUsed?: WorkflowState["modelsUsed"];
293
323
  subagents?: Record<string, unknown>;
324
+ planProgress?: WorkflowState["planProgress"];
325
+ planRuntime?: WorkflowState["planRuntime"];
326
+ planExecutionStepIndex?: number;
327
+ planStepValidationIndex?: number;
328
+ currentValidationRetry?: number;
329
+ workflowValidationRetryCount?: number;
330
+ repairRetryState?: WorkflowState["repairRetryState"];
331
+ repairHistory?: WorkflowState["repairHistory"];
332
+ reviewHistory?: WorkflowState["reviewHistory"];
294
333
  }
295
334
 
296
335
  export interface PlanSavingOptions {
@@ -385,6 +424,9 @@ export interface MissionState {
385
424
  reviewHistory?: WorkflowReviewHistoryEntry[];
386
425
  reviewRepairInProgress?: boolean;
387
426
  lastValidationResult?: string;
427
+ concreteRepairableIssue?: boolean;
428
+ manualVerificationRequired?: boolean;
429
+ evidenceGap?: boolean;
388
430
  modelsUsed: Record<string, string>;
389
431
  subagentsUsed: string[];
390
432
  approvalRequired: boolean;
@@ -535,6 +577,15 @@ export function saveWorkflowPlan(state: WorkflowState, options: PlanSavingOption
535
577
  finalReport: options.finalReport?.trim() ? (redactSecrets(compact(options.finalReport, 5000)) ?? compact(options.finalReport, 5000)) : undefined,
536
578
  modelsUsed: state.modelsUsed,
537
579
  subagents: options.subagents,
580
+ planProgress: state.planProgress,
581
+ planRuntime: state.planRuntime,
582
+ planExecutionStepIndex: state.planExecutionStepIndex,
583
+ planStepValidationIndex: state.planStepValidationIndex,
584
+ currentValidationRetry: state.currentValidationRetry,
585
+ workflowValidationRetryCount: state.workflowValidationRetryCount,
586
+ repairRetryState: state.repairRetryState,
587
+ repairHistory: state.repairHistory,
588
+ reviewHistory: state.reviewHistory,
538
589
  };
539
590
 
540
591
  writeFileSync(LATEST_PLAN_FILE, JSON.stringify(record, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
@@ -714,8 +765,10 @@ function activeElapsedMs(startedAt: string | null | undefined, nowMs: number, la
714
765
  const parsed = Date.parse(startedAt ?? "");
715
766
  if (!Number.isFinite(parsed)) return 0;
716
767
  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)
768
+ const end = parsed < RUNTIME_SESSION_STARTED_AT_MS
769
+ ? (Number.isFinite(updated) && updated < RUNTIME_SESSION_STARTED_AT_MS
770
+ ? Math.max(parsed, updated)
771
+ : RUNTIME_SESSION_STARTED_AT_MS)
719
772
  : nowMs;
720
773
  return Math.max(0, end - parsed);
721
774
  }
@@ -770,7 +823,9 @@ export function planActiveRuntimeMs(state: WorkflowState, now = new Date()): num
770
823
  export function planWallClockAgeMs(state: WorkflowState, now = new Date()): number {
771
824
  const start = Date.parse(state.planRuntime?.createdAt ?? "");
772
825
  if (!Number.isFinite(start)) return 0;
773
- return Math.max(0, now.getTime() - start);
826
+ const terminalTimestamp = planRuntimeCounterState(state) === "stopped" ? state.updatedAt : undefined;
827
+ const end = terminalTimestamp ? Date.parse(terminalTimestamp) : now.getTime();
828
+ return Math.max(0, (Number.isFinite(end) ? end : now.getTime()) - start);
774
829
  }
775
830
 
776
831
  export function applyStandardRuntimeAccounting(previous: WorkflowState | undefined, state: WorkflowState, now = new Date()): WorkflowState {
@@ -826,7 +881,9 @@ export function standardActiveRuntimeMs(state: WorkflowState, now = new Date()):
826
881
  export function standardWallClockAgeMs(state: WorkflowState, now = new Date()): number {
827
882
  const start = Date.parse(state.standardRuntime?.createdAt ?? "");
828
883
  if (!Number.isFinite(start)) return 0;
829
- return Math.max(0, now.getTime() - start);
884
+ const terminalTimestamp = standardRuntimeCounterState(state) === "stopped" ? state.updatedAt : undefined;
885
+ const end = terminalTimestamp ? Date.parse(terminalTimestamp) : now.getTime();
886
+ return Math.max(0, (Number.isFinite(end) ? end : now.getTime()) - start);
830
887
  }
831
888
 
832
889
  export function applyMissionRuntimeAccounting(previous: MissionState | undefined, mission: MissionState, now = new Date()): MissionState {
@@ -859,7 +916,7 @@ export function applyMissionRuntimeAccounting(previous: MissionState | undefined
859
916
  lastResumedAt: mission.lastResumedAt ?? nowIso,
860
917
  };
861
918
  } else if (nextActive && previousStartedAt) {
862
- next = { ...next, activeRunStartedAt: previousStartedAt };
919
+ next = { ...next, activeRunStartedAt: Date.parse(previousStartedAt) < RUNTIME_SESSION_STARTED_AT_MS ? nowIso : previousStartedAt };
863
920
  } else if (!nextActive) {
864
921
  next = { ...next, activeRunStartedAt: null };
865
922
  }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, realpathSync } from "node:fs";
2
2
  import { execFileSync } from "node:child_process";
3
- import { isAbsolute, resolve } from "node:path";
3
+ import { isAbsolute, resolve, join, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
6
  import { loadWorkflowSettings } from "./workflow-model-router.js";
6
7
  import type { WorkflowState } from "./workflow-state.js";
@@ -22,9 +23,9 @@ export const EXECUTION_RESULT_TOOLS = [WORKFLOW_EXECUTION_RESULT_TOOL, MISSION_M
22
23
  export const VALIDATION_RESULT_TOOLS = [WORKFLOW_VALIDATION_RESULT_TOOL];
23
24
  export const REPAIR_RESULT_TOOLS = [WORKFLOW_REPAIR_RESULT_TOOL];
24
25
  export const STANDARD_RESULT_TOOLS = [STANDARD_HANDOFF_RESULT_TOOL];
25
- export const BASE_EXECUTE_TOOLS = ["read", "grep", "find", "ls", "edit", "write", "bash", WORKFLOW_PROGRESS_TOOL, WORKFLOW_DIAGRAM_TOOL];
26
- export const EXECUTE_TOOLS = [...BASE_EXECUTE_TOOLS, ...EXECUTION_RESULT_TOOLS, ...REPAIR_RESULT_TOOLS];
27
- export const VALIDATOR_TOOLS = ["read", "grep", "find", "ls", "bash", WORKFLOW_DIAGRAM_TOOL, ...REVIEW_RESULT_TOOLS, ...VALIDATION_RESULT_TOOLS];
26
+ export const BASE_EXECUTE_TOOLS = ["read", "grep", "find", "ls", "edit", "write", "bash", WORKFLOW_DIAGRAM_TOOL];
27
+ export const EXECUTE_TOOLS = [...BASE_EXECUTE_TOOLS, WORKFLOW_PROGRESS_TOOL, ...EXECUTION_RESULT_TOOLS, ...REPAIR_RESULT_TOOLS];
28
+ export const VALIDATOR_TOOLS = ["read", "grep", "find", "ls", "bash", "write", WORKFLOW_DIAGRAM_TOOL, ...REVIEW_RESULT_TOOLS, ...VALIDATION_RESULT_TOOLS];
28
29
 
29
30
 
30
31
  const PATH_SCOPED_TOOLS = new Set(["read", "grep", "find", "ls", "edit", "write"]);
@@ -80,11 +81,23 @@ function piRuntimeInstructionPath(candidate: string): boolean {
80
81
  || rel === "themes" || rel.startsWith("themes/");
81
82
  }
82
83
 
84
+ function packageInstructionPath(candidate: string): boolean {
85
+ const root = safeRealpath(join(dirname(fileURLToPath(import.meta.url)), ".."));
86
+ if (!pathInsideRoot(candidate, root)) return false;
87
+ const rel = candidate === root ? "" : candidate.slice(root.length + 1);
88
+ return rel === "skills" || rel.startsWith("skills/")
89
+ || rel === "agents" || rel.startsWith("agents/")
90
+ || rel === "config/prompts" || rel.startsWith("config/prompts/")
91
+ || rel === "prompts" || rel.startsWith("prompts/")
92
+ || rel === "themes" || rel.startsWith("themes/");
93
+ }
94
+
83
95
  function repoLockPathBlock(pathValue: unknown, cwd: string, tool: string): string | undefined {
84
96
  const root = repoLockRoot(cwd);
85
97
  const candidate = resolveCandidatePath(typeof pathValue === "string" && pathValue.trim() ? pathValue.trim() : ".", cwd);
86
98
  if (!pathInsideRoot(candidate, root)) {
87
- if ((tool === "read" || tool === "grep" || tool === "find" || tool === "ls") && piRuntimeInstructionPath(candidate)) return undefined;
99
+ if ((tool === "read" || tool === "grep" || tool === "find" || tool === "ls") && (piRuntimeInstructionPath(candidate) || packageInstructionPath(candidate))) return undefined;
100
+ if (candidate.startsWith("/private/tmp/") || candidate.startsWith("/tmp/") || candidate.startsWith("/var/tmp/")) return undefined;
88
101
  return `Repo Lock blocked path outside current repository: ${candidate} (repo root: ${root})`;
89
102
  }
90
103
  if ((tool === "edit" || tool === "write") && protectedRepoPath(candidate, root)) return `Repo Lock blocked ${tool} for protected project control path: ${candidate}`;
@@ -123,6 +136,8 @@ function repoLockBashBlock(command: string, cwd: string): string | undefined {
123
136
  if (raw === "." || raw === "./" || raw === "/") continue;
124
137
  const cleaned = raw.replace(/[),]+$/, "");
125
138
  if (!cleaned || cleaned.startsWith("./node_modules/.bin")) continue;
139
+ if (cleaned.startsWith("/dev/")) continue;
140
+ if (cleaned.startsWith("/tmp/") || cleaned.startsWith("/private/tmp/") || cleaned.startsWith("/var/tmp/")) continue;
126
141
  const candidate = resolveCandidatePath(cleaned, cwd);
127
142
  if (!pathInsideRoot(candidate, root)) return `Repo Lock blocked bash path outside current repository: ${cleaned} -> ${candidate} (repo root: ${root})`;
128
143
  }
@@ -147,6 +162,26 @@ const BLOCKED_EXECUTE_BASH: RegExp[] = [
147
162
  /\bpnpm\s+add\b/i,
148
163
  /\byarn\s+add\b/i,
149
164
  /\bpip\s+install\b/i,
165
+ /\bpip3?\s+install\b/i,
166
+ /\bbundle\s+install\b/i,
167
+ /\bgem\s+install\b/i,
168
+ /\bcargo\s+install\b/i,
169
+ /\bgo\s+(?:get|install)\b/i,
170
+ /\bdeno\s+(?:install|add|cache)\b/i,
171
+ /\bcomposer\s+(?:install|require|update)\b/i,
172
+ /\bmix\s+(?:deps\.get|deps\.compile)\b/i,
173
+ /\bbrew\s+install\b/i,
174
+ /\bapt\s+(?:install|get\s+install)\b/i,
175
+ /\byum\s+install\b/i,
176
+ /\bdnf\s+install\b/i,
177
+ /\bapk\s+add\b/i,
178
+ /\bnuget\s+install\b/i,
179
+ /\bdotnet\s+(?:add\s+package|tool\s+install|restore)\b/i,
180
+ /\bcabal\s+(?:install|update)\b/i,
181
+ /\bstack\s+(?:install|update)\b/i,
182
+ /\bconan\s+install\b/i,
183
+ /\bvcpkg\s+install\b/i,
184
+ /\bcoursier\s+(?:install|fetch)\b/i,
150
185
  /\bcurl\b[^\n]*\|\s*sh\b/i,
151
186
  /\bwget\b[^\n]*\|\s*sh\b/i,
152
187
  /\bvercel\s+deploy\b/i,
@@ -165,7 +200,7 @@ function isPlanMode(mode: WorkflowState["mode"]): boolean {
165
200
  }
166
201
 
167
202
  function isValidatorMode(mode: WorkflowState["mode"]): boolean {
168
- return mode === "reviewing" || mode === "reviewed" || mode === "validating" || mode === "revalidating" || mode === "validated" || mode === "mission_validating" || mode === "mission_revalidating" || mode === "mission_final_validating";
203
+ return mode === "reviewing" || mode === "reviewed" || mode === "validating" || mode === "revalidating" || mode === "mission_validating" || mode === "mission_revalidating" || mode === "mission_final_validating";
169
204
  }
170
205
 
171
206
  function isValidationResultMode(mode: WorkflowState["mode"]): boolean {
@@ -180,9 +215,17 @@ function isSubagentWorker(): boolean {
180
215
  return process.env.PI_SUBAGENT_WORKER === "1";
181
216
  }
182
217
 
218
+ const PACKAGE_INSTALL_RE = /\b(?:npm\s+install|pnpm\s+add|yarn\s+add|pip3?\s+install|bundle\s+install|gem\s+install|cargo\s+install|go\s+(?:get|install)|deno\s+(?:install|add|cache)|composer\s+(?:install|require|update)|mix\s+deps\.(?:get|compile)|brew\s+install|apt(?:-get)?\s+install|yum\s+install|dnf\s+install|apk\s+add|nuget\s+install|dotnet\s+(?:add\s+package|tool\s+install|restore)|cabal\s+(?:install|update)|stack\s+(?:install|update)|conan\s+install|vcpkg\s+install|coursier\s+(?:install|fetch))\b/i;
219
+
220
+ function isPackageInstallCommand(command: string): boolean {
221
+ return PACKAGE_INSTALL_RE.test(command);
222
+ }
223
+
183
224
  function commandBlocked(command: string, cwd?: string): boolean {
184
225
  const settings = loadWorkflowSettings(cwd);
185
- return settings.safety.blockDestructiveCommands !== false && isBlockedExecuteCommand(command);
226
+ if (settings.safety.blockDestructiveCommands === false) return false;
227
+ if (isPackageInstallCommand(command) && settings.safety.allowPackageInstallInExecution !== false) return false;
228
+ return isBlockedExecuteCommand(command);
186
229
  }
187
230
 
188
231
  function standardTodoMode(settings: ReturnType<typeof loadWorkflowSettings>): "off" | "manual" | "auto" | "required" {
@@ -202,17 +245,32 @@ function standardTaskLooksSubstantive(task: string | undefined): boolean {
202
245
  return text.length >= 8 || text.split(/\s+/).filter(Boolean).length >= 2;
203
246
  }
204
247
 
205
- function standardSafeReadOnlyBash(command: string): boolean {
248
+ function stripTimeoutPrefix(command: string): string {
249
+ return command.replace(/^timeout\s+\d+[smhd]?\s+/, "").trim() || command;
250
+ }
251
+
252
+ const DESTRUCTIVE_WORD_RE = /\b(?:install|add|update|upgrade|publish|deploy|push|checkout|switch|commit|merge|rebase|stash|tag|apply|am|restore|sed\s+-i|perl\s+-pi|chmod|chown|curl\s.*\|\s*(?:sh|bash)|wget\s.*\|\s*(?:sh|bash))\b/i;
253
+
254
+ const SAFE_READ_ONLY_COMMANDS_RE = /^(?:git\s+(?:status|log|diff|show|branch|rev-parse|ls-files|describe|remote|tag|shortlog|count-objects|blame|name-rev)\b|cat\b|head\b|tail\b|less\b|more\b|wc\b|file\b|stat\b|which\b|where\b|command\s+-v\b|type\b|echo\b|printf\b|printenv\b|env\b|uname\b|date\b|id\b|whoami\b|hostname\b|pwd\b|ls\b|du\b|df\b|diff\b|comm\b|sort\b|uniq\b|cut\b|tr\b|awk\b|jq\b|yq\b|xq\b|(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:build|test|lint|typecheck|type-check|check[\s:]?\w*|dev|start|preview|serve|watch|format|analyze|compile|ci|validate|verify|coverage|bench|benchmark|bundle|pack|dist|static|docs|doc|stylelint|e2e|integration|unit)\b|(?:npm|pnpm|yarn|bun)\s+(?:exec|info|ls|list|query|outdated|why|view|pack\s+--dry-run)\b|npx\s+(?:serve|http-server|lite-server|tsc|vite|eslint|prettier|vitest|jest|mocha|cypress|playwright|webpack|rollup|parcel|turbo|nx|ts-node|tsx|esbuild|swc|babel|stylelint|biome|rome|knip|typedoc|compodoc|angular-cli|react-scripts|next|nuxt|remix|astro|svelte-kit)\b|pnpm\s+(?:exec|dlx)\s+\w+\b|bun\s+(?:test|check|build|run)\b|deno\s+(?:check|test|build|lint|task|info|doc|compile|fmt|eval|cache)\b|cargo\s+(?:build|test|check|clippy|doc|bench|run|metadata|locate-project|tree|version)\b|(?:rustc|rustup)\s+(?:--version|--print|which)\b|go\s+(?:build|test|vet|run|doc|list|mod\s+(?:verify|tidy|graph|download|why))\b|python3?\s+(?:--version|-V|-c\b|-m\s+(?:pytest|unittest|mypy|pylint|flake8|black|isort|ruff|json\.tool|compileall|bandit|pyright|http\.server|html\.parser|html))\b|pip3?\s+(?:list|show|check|debug|index\s+versions)\b|tsc\b|node\s+(?:--version|-v|--check|-c|-e|--eval)\b|make\s+(?:build|test|check|lint|all|verify|docs|format|static|analyze)\b|cmake\s+(?:--build|--version)\b|(?:dotnet|msbuild)\s+(?:build|test|restore|check|format|lint|pack)\b|(?:gradle|\.\/gradlew|gradlew\.bat)\s+(?:build|test|check|compile|lint|dependencies|projects|tasks)\b|mvn\s+(?:compile|test|verify|checkstyle|pmd|versions:display|dependency:tree|dependency:list)\b|(?:swift|swiftc)\s+(?:build|test|package\s+(?:describe|dump-package))\b|(?:bundle|gem)\s+(?:exec|list|check|info|query)\b|rake\s+(?:test|spec|lint|check|notes|stats|about)\b|php\s+(?:--version|-v|-l)\b|(?:php\s+)?artisan\s+(?:--version|route:list|config:show|env)\b|composer\s+(?:validate|check|show|outdated|info|diagnose)\b|mix\s+(?:test|compile|lint|format|docs)\b|bazel\s+(?:build|test|query|cquery|info|version)\b|buck\s+(?:build|test|query|audit)\b|curl\s+(?:-[^\s]*[sSfIv][^\s]*\s+)+(?:https?:\/\/|localhost|\$)|kill\s+\$!\b|kill\s+-0\s+\$\w+\b|wait\s+\$!\b|wait\s+\$\w+\b|sleep\s+[0-9.]+[smhd]?\b|ps\s+(?:aux?|-[a-z]*[eE][a-z]*|-[a-z]*[pP][a-z]*)\b|pgrep\s+-\w+\s+\w+|true\b|false\b|\.\s*\/node_modules\/\.bin\/\S+\b)/i;
255
+
256
+ export function standardSafeReadOnlyBash(command: string): boolean {
206
257
  const trimmed = command.trim();
207
258
  if (!trimmed || isBlockedExecuteCommand(trimmed)) return false;
208
- return /^(?:git\s+(?:status|log|diff|show|branch|rev-parse)\b|python3?\s+-m\s+json\.tool\b|npm\s+run\s+(?:lint|test)\b|npx\s+tsc\s+--noEmit\b|tsc\s+--noEmit\b)/i.test(trimmed);
259
+ const cmd = stripTimeoutPrefix(trimmed);
260
+ return SAFE_READ_ONLY_COMMANDS_RE.test(cmd);
261
+ }
262
+
263
+ function stripSafePreamble(command: string): string {
264
+ return command.replace(/^(?:set\s+[-+][euxo]+(?:\s+[^\n]*)?|export\s+\w+=["']?[^\n"']*["']?|\w+=\S+)\s*\n+/gm, "").trim() || command;
209
265
  }
210
266
 
211
267
  function validatorSafeEvidenceBash(command: string): boolean {
212
268
  const trimmed = command.trim();
213
- if (!trimmed || isBlockedExecuteCommand(trimmed)) return false;
214
- if (/\b(?:install|add|update|upgrade|publish|deploy|push|reset|clean|checkout|switch|commit|merge|rebase|stash|tag|apply|am|restore|rm|mv|cp|mkdir|touch|sed\s+-i|perl\s+-pi|tee|chmod|chown|kill|open)\b/i.test(trimmed)) return false;
215
- return /^(?:git\s+(?:status|log|diff|show|branch|rev-parse|ls-files)\b|npm\s+run\s+(?:typecheck|check:ts|lint|test|build)\b|npx\s+tsc\s+--noEmit\b|tsc\s+--noEmit\b|python3?\s+-m\s+json\.tool\b)/i.test(trimmed);
269
+ if (!trimmed) return false;
270
+ const cmd = stripSafePreamble(stripTimeoutPrefix(trimmed));
271
+ if (isBlockedExecuteCommand(cmd)) return false;
272
+ if (DESTRUCTIVE_WORD_RE.test(cmd)) return false;
273
+ return true;
216
274
  }
217
275
 
218
276
  function standardTodoTitleLooksGeneric(title: string): boolean {
@@ -238,6 +296,31 @@ function standardRequiredTodoMissing(state: WorkflowState, settings: ReturnType<
238
296
  && standardTaskLooksSubstantive(task);
239
297
  }
240
298
 
299
+ function planProgressRelevantWorkTool(tool: string, input: unknown): boolean {
300
+ if (tool === "edit" || tool === "write") return true;
301
+ if (tool !== "bash") return false;
302
+ const command = String((input as { command?: unknown } | undefined)?.command ?? "");
303
+ return Boolean(command.trim()) && !standardSafeReadOnlyBash(command);
304
+ }
305
+
306
+ function currentPlanProgressStepNumber(state: WorkflowState): number | undefined {
307
+ const steps = state.planProgress?.steps ?? [];
308
+ if (!steps.length) return undefined;
309
+ if (steps.every((step) => step.status === "completed" || step.status === "skipped")) return undefined;
310
+ const activeIndex = steps.findIndex((step) => step.status === "active");
311
+ const fallbackIndex = Math.max(0, Math.min(steps.length - 1, Math.floor(state.planProgress?.currentStepIndex ?? 0)));
312
+ return (activeIndex >= 0 ? activeIndex : fallbackIndex) + 1;
313
+ }
314
+
315
+ function planProgressToolRequiredBlock(state: WorkflowState, tool: string, input: unknown): string | undefined {
316
+ if (state.mode !== "executing" && state.mode !== "repairing") return undefined;
317
+ if (!planProgressRelevantWorkTool(tool, input)) return undefined;
318
+ const stepNumber = currentPlanProgressStepNumber(state);
319
+ if (!stepNumber) return undefined;
320
+ if (state.planProgressLastToolStatus === "active" && state.planProgressLastToolStep === stepNumber) return undefined;
321
+ return `Plan execution ${tool} is blocked until workflow_progress({ step: ${stepNumber}, status: "active" }) is called for the current approved Plan step.`;
322
+ }
323
+
241
324
  export function registerToolGuard(pi: ExtensionAPI, getState: () => WorkflowState): void {
242
325
  pi.on("tool_call", async (event, ctx) => {
243
326
  const state = getState();
@@ -272,13 +355,16 @@ export function registerToolGuard(pi: ExtensionAPI, getState: () => WorkflowStat
272
355
 
273
356
  if (tool === STANDARD_HANDOFF_RESULT_TOOL && state.mode !== "standard") return { block: true, reason: "Standard handoff result is only available while Standard Mode is active." };
274
357
 
275
- if ((tool === WORKFLOW_PLAN_RESULT_TOOL && state.mode !== "planning") || (tool === MISSION_PLAN_RESULT_TOOL && state.mode !== "mission_planning")) return { block: true, reason: `${tool} is only available during its planning phase.` };
358
+ if (tool === WORKFLOW_PLAN_RESULT_TOOL && state.mode !== "planning" && state.mode !== "executing" && state.mode !== "repairing") return { block: true, reason: `${tool} is only available during its planning phase.` };
359
+ if (tool === MISSION_PLAN_RESULT_TOOL && state.mode !== "mission_planning") return { block: true, reason: `${tool} is only available during its planning phase.` };
276
360
  if (tool === WORKFLOW_REVIEW_RESULT_TOOL && state.mode !== "reviewing" && state.mode !== "mission_plan_ready") return { block: true, reason: "workflow_review_result is only available during review phases." };
277
361
  if (tool === WORKFLOW_EXECUTION_RESULT_TOOL && state.mode !== "executing") return { block: true, reason: "workflow_execution_result is only available during Plan execution." };
278
362
  if (tool === MISSION_MILESTONE_RESULT_TOOL && state.mode !== "mission_running") return { block: true, reason: "mission_milestone_result is only available during Mission execution." };
279
363
  if (tool === WORKFLOW_VALIDATION_RESULT_TOOL && !isValidationResultMode(state.mode)) return { block: true, reason: "workflow_validation_result is only available during validation phases." };
280
364
  if (tool === WORKFLOW_REPAIR_RESULT_TOOL && state.mode !== "repairing" && state.mode !== "mission_repairing") return { block: true, reason: "workflow_repair_result is only available during repair phases." };
281
365
 
366
+ if (tool === WORKFLOW_PROGRESS_TOOL && state.mode !== "executing" && state.mode !== "repairing") return { block: true, reason: "Plan step progress tracking is only available during Plan execution." };
367
+
282
368
  if (tool === "standard_todo") {
283
369
  if (state.mode !== "standard") return { block: true, reason: "Standard Mode To Do is only available while Standard Mode is active." };
284
370
  if (state.standardClarificationPending || state.standardClarificationStage === "drafting" || state.standardClarificationStage === "awaiting_answer") return { block: true, reason: "Standard Mode To Do is blocked until the pending Standard clarification is answered." };
@@ -292,14 +378,18 @@ export function registerToolGuard(pi: ExtensionAPI, getState: () => WorkflowStat
292
378
  }
293
379
  }
294
380
 
381
+ const planProgressBlock = planProgressToolRequiredBlock(state, tool, event.input);
382
+ if (planProgressBlock) return { block: true, reason: planProgressBlock };
383
+
295
384
  if (isPlanMode(state.mode)) {
385
+ if (state.mode === "plan_approved" && state.approvedPlan) return;
296
386
  if (tool === "edit" || tool === "write") return { block: true, reason: `Workflow Plan Mode blocks ${tool}. Allowed tools: ${PLAN_TOOLS.join(", ")}${settings.safety.disableBashInPlanMode === false ? ", bash (safe commands)" : ""}` };
297
387
  if (tool === "bash" && settings.safety.disableBashInPlanMode !== false) return { block: true, reason: `Workflow Plan Mode blocks bash. Allowed tools: ${PLAN_TOOLS.join(", ")}` };
298
388
  }
299
389
 
300
390
  if (isValidatorMode(state.mode)) {
301
- if (tool === "edit" || tool === "write") return { block: true, reason: `Workflow Review/Validator Mode blocks ${tool}. Allowed tools: ${VALIDATOR_TOOLS.join(", ")}` };
302
- if (tool === "bash") {
391
+ if (tool === "edit") return { block: true, reason: `Workflow Review/Validator Mode blocks ${tool}. Allowed tools: ${VALIDATOR_TOOLS.join(", ")}` };
392
+ if (tool === "bash" && settings.safety.disableBashInValidatorMode !== false) {
303
393
  const command = String((event.input as { command?: unknown }).command ?? "");
304
394
  if (!validatorSafeEvidenceBash(command)) return { block: true, reason: `Workflow Review/Validator Mode blocks unsafe bash. Allowed bash is limited to safe read-only evidence commands.` };
305
395
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediadatafusion/pi-workflow-suite",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "description": "Structured workflow orchestration suite for Pi with Standard, Plan, Mission, compaction, diagrams, web access, repo lock, and safety gates.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -36,6 +36,7 @@
36
36
  "type": "module",
37
37
  "files": [
38
38
  "extensions/",
39
+ "!extensions/*.bak",
39
40
  "skills/",
40
41
  "agents/",
41
42
  "config/",
@@ -105,7 +106,7 @@
105
106
  "scripts": {
106
107
  "check:ts": "tsc --noEmit --noCheck",
107
108
  "typecheck": "tsc --noEmit",
108
- "validate": "npm run check:ts && ./scripts/check-clean-release-tree.sh && npm run check:package-size && git diff --check",
109
+ "validate": "npm run check:ts && ./scripts/test-workflow-forced-subagent-regression.sh && ./scripts/test-agent-skill-boundary-regression.sh && ./scripts/test-startup-visual-mode-entry-regression.sh && ./scripts/test-settings-health-regression.sh && ./scripts/test-handoff-visibility-regression.sh && ./scripts/test-mission-milestone-handoff-regression.sh && ./scripts/test-plan-handoff-chain-regression.sh && ./scripts/test-plan-step-progress-regression.sh && ./scripts/test-standard-mode-regression.sh && ./scripts/test-final-handoff-summary-regression.sh && ./scripts/test-clarification-answer-handoff-regression.sh && ./scripts/test-validation-evidence-contract-regression.sh && ./scripts/test-mermaid-guidance-regression.sh && ./scripts/test-runtime-web-tools-regression.sh && ./scripts/test-repolock-scope-regression.sh && ./scripts/test-repo-lock-version-regression.sh && ./scripts/test-package-menu-surface.sh && npm run check:package-size && git diff --check",
109
110
  "check:package-size": "node scripts/check-package-size.mjs",
110
111
  "prepack": "node scripts/prepare-package-readme.mjs apply",
111
112
  "postpack": "node scripts/prepare-package-readme.mjs restore --pack",