@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.
- package/CHANGELOG.md +31 -0
- package/README.md +126 -9
- package/VERSION +1 -1
- package/config/prompts/mission-review-prompt.md +42 -0
- package/config/prompts/workflow-reviewer-prompt.md +44 -0
- package/extensions/workflow-model-router.ts +28 -14
- package/extensions/workflow-modes.ts +1184 -311
- package/extensions/workflow-state.ts +62 -5
- package/extensions/workflow-tool-guard.ts +105 -15
- package/package.json +3 -2
|
@@ -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
|
|
718
|
-
?
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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 === "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
214
|
-
|
|
215
|
-
|
|
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 (
|
|
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"
|
|
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.
|
|
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/
|
|
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",
|