@mediadatafusion/pi-workflow-suite 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/CONTRIBUTING.md +9 -0
  3. package/LICENSE.md +201 -0
  4. package/NOTICE +6 -0
  5. package/README.md +1208 -0
  6. package/SECURITY.md +7 -0
  7. package/SUPPORT.md +9 -0
  8. package/TRADEMARKS.md +14 -0
  9. package/VERSION +1 -0
  10. package/agents/codebase-research.md +42 -0
  11. package/agents/general-worker.md +26 -0
  12. package/agents/implementation-planning.md +46 -0
  13. package/agents/quality-validation.md +43 -0
  14. package/agents/workflow-orchestrator.md +44 -0
  15. package/config/prompts/execute-approved-plan.md +43 -0
  16. package/config/prompts/mission-checkpoint.md +26 -0
  17. package/config/prompts/mission-final-validation.md +21 -0
  18. package/config/prompts/mission-plan.md +129 -0
  19. package/config/prompts/mission-repair.md +33 -0
  20. package/config/prompts/mission-run.md +37 -0
  21. package/config/prompts/validate-approved-plan.md +42 -0
  22. package/config/prompts/workflow-plan-prompt.md +93 -0
  23. package/config/prompts/workflow-repair.md +20 -0
  24. package/config/prompts/workflow-summary.md +23 -0
  25. package/config/workflow-settings.example.json +335 -0
  26. package/docs/assets/mediadatafusion-logo.png +0 -0
  27. package/docs/assets/pi-workflow-suite-card.png +0 -0
  28. package/docs/assets/pi-workflow-suite-header.png +0 -0
  29. package/docs/assets/pi-workflow-suite-video-thumb.png +0 -0
  30. package/docs/assets/readme-link-commands.svg +10 -0
  31. package/docs/assets/readme-link-install.svg +10 -0
  32. package/docs/assets/readme-link-quick-start.svg +10 -0
  33. package/docs/assets/readme-link-settings.svg +10 -0
  34. package/extensions/subagent/agents.ts +149 -0
  35. package/extensions/subagent/index.ts +1136 -0
  36. package/extensions/subagent/runner.ts +291 -0
  37. package/extensions/workflow-model-router.ts +1485 -0
  38. package/extensions/workflow-modes.ts +14778 -0
  39. package/extensions/workflow-parsers.ts +212 -0
  40. package/extensions/workflow-settings-capabilities.ts +282 -0
  41. package/extensions/workflow-state.ts +978 -0
  42. package/extensions/workflow-subagent-policy.ts +180 -0
  43. package/extensions/workflow-summary.ts +381 -0
  44. package/extensions/workflow-tool-guard.ts +302 -0
  45. package/extensions/workflow-validation-classifier.ts +102 -0
  46. package/extensions/workflow-web-tools.ts +356 -0
  47. package/package.json +1 -0
  48. package/scripts/audit-live.sh +69 -0
  49. package/scripts/audit-settings.sh +136 -0
  50. package/scripts/backup-live.sh +63 -0
  51. package/scripts/bootstrap-project.sh +220 -0
  52. package/scripts/install-to-live.sh +87 -0
  53. package/scripts/quarantine-live-junk.sh +69 -0
  54. package/scripts/verify-live.sh +128 -0
  55. package/skills/codebase-discovery/SKILL.md +20 -0
  56. package/skills/find-skills/SKILL.md +155 -0
  57. package/skills/git-safe-summary/SKILL.md +20 -0
  58. package/skills/implementation-planning/SKILL.md +20 -0
  59. package/skills/project-rules-audit/SKILL.md +20 -0
  60. package/skills/safe-execution/SKILL.md +20 -0
  61. package/skills/validation-review/SKILL.md +20 -0
@@ -0,0 +1,302 @@
1
+ import { existsSync, realpathSync } from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ import { isAbsolute, resolve } from "node:path";
4
+ import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
+ import { loadWorkflowSettings } from "./workflow-model-router.js";
6
+ import type { WorkflowState } from "./workflow-state.js";
7
+
8
+ export const PLAN_TOOLS = ["read", "grep", "find", "ls"];
9
+ export const WORKFLOW_PROGRESS_TOOL = "workflow_progress";
10
+ export const WORKFLOW_DIAGRAM_TOOL = "workflow_diagram";
11
+ export const WORKFLOW_PLAN_RESULT_TOOL = "workflow_plan_result";
12
+ export const WORKFLOW_REVIEW_RESULT_TOOL = "workflow_review_result";
13
+ export const WORKFLOW_EXECUTION_RESULT_TOOL = "workflow_execution_result";
14
+ export const WORKFLOW_VALIDATION_RESULT_TOOL = "workflow_validation_result";
15
+ export const WORKFLOW_REPAIR_RESULT_TOOL = "workflow_repair_result";
16
+ export const MISSION_PLAN_RESULT_TOOL = "mission_plan_result";
17
+ export const MISSION_MILESTONE_RESULT_TOOL = "mission_milestone_result";
18
+ export const STANDARD_HANDOFF_RESULT_TOOL = "standard_handoff_result";
19
+ export const PLAN_RESULT_TOOLS = [WORKFLOW_PLAN_RESULT_TOOL, MISSION_PLAN_RESULT_TOOL];
20
+ export const REVIEW_RESULT_TOOLS = [WORKFLOW_REVIEW_RESULT_TOOL];
21
+ export const EXECUTION_RESULT_TOOLS = [WORKFLOW_EXECUTION_RESULT_TOOL, MISSION_MILESTONE_RESULT_TOOL];
22
+ export const VALIDATION_RESULT_TOOLS = [WORKFLOW_VALIDATION_RESULT_TOOL];
23
+ export const REPAIR_RESULT_TOOLS = [WORKFLOW_REPAIR_RESULT_TOOL];
24
+ 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];
28
+
29
+
30
+ const PATH_SCOPED_TOOLS = new Set(["read", "grep", "find", "ls", "edit", "write"]);
31
+
32
+ function safeRealpath(path: string): string {
33
+ try {
34
+ return realpathSync(path);
35
+ } catch {
36
+ return path;
37
+ }
38
+ }
39
+
40
+ function repoRootForCwd(cwd: string): string {
41
+ try {
42
+ const root = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
43
+ return safeRealpath(root || cwd);
44
+ } catch {
45
+ return safeRealpath(cwd);
46
+ }
47
+ }
48
+
49
+ function resolveCandidatePath(pathValue: string, cwd: string): string {
50
+ const expanded = pathValue === "~" || pathValue.startsWith("~/") ? resolve(process.env.HOME || cwd, pathValue.slice(2)) : pathValue;
51
+ const resolved = isAbsolute(expanded) ? resolve(expanded) : resolve(cwd, expanded || ".");
52
+ if (existsSync(resolved)) return safeRealpath(resolved);
53
+ const existingParent = safeRealpath(resolve(resolved, ".."));
54
+ return resolve(existingParent, resolved.split(/[\\/]/).pop() || "");
55
+ }
56
+
57
+ function pathInsideRoot(candidate: string, root: string): boolean {
58
+ return candidate === root || candidate.startsWith(`${root}/`);
59
+ }
60
+
61
+ function piRuntimeRoot(): string {
62
+ return safeRealpath(getAgentDir());
63
+ }
64
+
65
+ function pathInsideRepoOrPiRuntime(candidate: string, root: string): boolean {
66
+ const piRoot = piRuntimeRoot();
67
+ return pathInsideRoot(candidate, root) || pathInsideRoot(candidate, piRoot);
68
+ }
69
+
70
+ function repoLockPathBlock(pathValue: unknown, cwd: string): string | undefined {
71
+ const root = repoRootForCwd(cwd);
72
+ const candidate = resolveCandidatePath(typeof pathValue === "string" && pathValue.trim() ? pathValue.trim() : ".", cwd);
73
+ if (!pathInsideRepoOrPiRuntime(candidate, root)) return `Repo Lock blocked path outside current repository or Pi runtime: ${candidate} (repo root: ${root}; Pi runtime: ${piRuntimeRoot()})`;
74
+ return undefined;
75
+ }
76
+
77
+ function repoLockBashBlock(command: string, cwd: string): string | undefined {
78
+ const trimmed = command.trim();
79
+ if (!trimmed) return undefined;
80
+ const root = repoRootForCwd(cwd);
81
+ const pathCandidates = Array.from(trimmed.matchAll(/(?:^|[\s=:'"`])((?:\.{1,2}|~|\/)[^\s'"`;&|)]*)/g)).map((match) => match[1]).filter(Boolean);
82
+ for (const raw of pathCandidates) {
83
+ if (raw === "." || raw === "./") continue;
84
+ const cleaned = raw.replace(/[),]+$/, "");
85
+ if (!cleaned || cleaned.startsWith("./node_modules/.bin")) continue;
86
+ const candidate = resolveCandidatePath(cleaned, cwd);
87
+ if (!pathInsideRepoOrPiRuntime(candidate, root)) return `Repo Lock blocked bash path outside current repository or Pi runtime: ${cleaned} -> ${candidate} (repo root: ${root}; Pi runtime: ${piRuntimeRoot()})`;
88
+ }
89
+ return undefined;
90
+ }
91
+
92
+ function repoLockEnabled(settings: ReturnType<typeof loadWorkflowSettings>): boolean {
93
+ return settings.safety.repoLockEnabled === true;
94
+ }
95
+
96
+ const BLOCKED_EXECUTE_BASH: RegExp[] = [
97
+ /\brm\s+-[^\n;|&]*r[^\n;|&]*f\b/i,
98
+ /\bsudo\b/i,
99
+ /\bchmod\s+-R\b/i,
100
+ /\bchown\s+-R\b/i,
101
+ /\bgit\s+reset\b/i,
102
+ /\bgit\s+clean\b/i,
103
+ /\bgit\s+push\b/i,
104
+ /\bgit\s+checkout\b/i,
105
+ /\bgit\s+switch\b/i,
106
+ /\bnpm\s+install\b/i,
107
+ /\bpnpm\s+add\b/i,
108
+ /\byarn\s+add\b/i,
109
+ /\bpip\s+install\b/i,
110
+ /\bcurl\b[^\n]*\|\s*sh\b/i,
111
+ /\bwget\b[^\n]*\|\s*sh\b/i,
112
+ /\bvercel\s+deploy\b/i,
113
+ /\bdeploy\b/i,
114
+ /\bsupabase\s+db\s+push\b/i,
115
+ /\bsupabase\s+migration\s+up\b/i,
116
+ /\bmigration\b[^\n]*(run|up|execute)/i,
117
+ ];
118
+
119
+ export function isBlockedExecuteCommand(command: string): boolean {
120
+ return BLOCKED_EXECUTE_BASH.some((pattern) => pattern.test(command));
121
+ }
122
+
123
+ function isPlanMode(mode: WorkflowState["mode"]): boolean {
124
+ return mode === "awaiting_plan_input" || mode === "awaiting_clarification" || mode === "planning" || mode === "plan_draft" || mode === "plan_approved";
125
+ }
126
+
127
+ function isValidatorMode(mode: WorkflowState["mode"]): boolean {
128
+ return mode === "reviewing" || mode === "reviewed" || mode === "validating" || mode === "revalidating" || mode === "validated" || mode === "mission_validating" || mode === "mission_revalidating" || mode === "mission_final_validating";
129
+ }
130
+
131
+ function isValidationResultMode(mode: WorkflowState["mode"]): boolean {
132
+ return mode === "validating" || mode === "revalidating" || mode === "mission_validating" || mode === "mission_revalidating" || mode === "mission_final_validating";
133
+ }
134
+
135
+ function isExecutionMode(mode: WorkflowState["mode"]): boolean {
136
+ return mode === "executing" || mode === "repairing" || mode === "mission_running" || mode === "mission_repairing";
137
+ }
138
+
139
+ function isSubagentWorker(): boolean {
140
+ return process.env.PI_SUBAGENT_WORKER === "1";
141
+ }
142
+
143
+ function commandBlocked(command: string, cwd?: string): boolean {
144
+ const settings = loadWorkflowSettings(cwd);
145
+ return settings.safety.blockDestructiveCommands !== false && isBlockedExecuteCommand(command);
146
+ }
147
+
148
+ function standardTodoMode(settings: ReturnType<typeof loadWorkflowSettings>): "off" | "manual" | "auto" | "required" {
149
+ return settings.standard.autoTodoEnabled === false || settings.standard.todoTriggerMode === "off"
150
+ ? "off"
151
+ : settings.standard.todoTriggerMode === "manual"
152
+ ? "manual"
153
+ : settings.standard.todoTriggerMode === "required"
154
+ ? "required"
155
+ : "auto";
156
+ }
157
+
158
+ function standardTaskLooksSubstantive(task: string | undefined): boolean {
159
+ const text = task?.trim() ?? "";
160
+ if (!text || text.startsWith("/")) return false;
161
+ if (/^(?:hi|hello|hey|thanks|thank you|ok|okay|yes|no|status|help)$/i.test(text)) return false;
162
+ return text.length >= 8 || text.split(/\s+/).filter(Boolean).length >= 2;
163
+ }
164
+
165
+ function standardSafeReadOnlyBash(command: string): boolean {
166
+ const trimmed = command.trim();
167
+ if (!trimmed || isBlockedExecuteCommand(trimmed)) return false;
168
+ 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);
169
+ }
170
+
171
+ function validatorSafeEvidenceBash(command: string): boolean {
172
+ const trimmed = command.trim();
173
+ if (!trimmed || isBlockedExecuteCommand(trimmed)) return false;
174
+ 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;
175
+ 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);
176
+ }
177
+
178
+ function standardTodoTitleLooksGeneric(title: string): boolean {
179
+ const text = title.trim().toLowerCase();
180
+ if (!text) return true;
181
+ return /^(?:understand|analy[sz]e|clarify|assess|inspect)\s+(?:the\s+)?(?:request|task|requirements?|assumptions?|context)$/.test(text)
182
+ || /^summari[sz]e\s+(?:the\s+)?(?:outcome|result|work)(?:\s+and\s+(?:the\s+)?(?:next\s+)?actions?)?$/.test(text)
183
+ || /^(?:finish|complete|finali[sz]e)$/.test(text);
184
+ }
185
+
186
+ function standardTodoLooksGeneric(todo: WorkflowState["standardTodo"]): boolean {
187
+ if (!todo?.items.length) return false;
188
+ return todo.items.length <= 3 && todo.items.every((item) => standardTodoTitleLooksGeneric(item.title));
189
+ }
190
+
191
+ function standardRequiredTodoMissing(state: WorkflowState, settings: ReturnType<typeof loadWorkflowSettings>): boolean {
192
+ const task = state.task ?? state.originalTask;
193
+ return standardTodoMode(settings) === "required"
194
+ && (!state.standardTodo?.items.length || standardTodoLooksGeneric(state.standardTodo))
195
+ && !state.standardClarificationPending
196
+ && state.standardClarificationStage !== "drafting"
197
+ && state.standardClarificationStage !== "awaiting_answer"
198
+ && standardTaskLooksSubstantive(task);
199
+ }
200
+
201
+ export function registerToolGuard(pi: ExtensionAPI, getState: () => WorkflowState): void {
202
+ pi.on("tool_call", async (event, ctx) => {
203
+ const state = getState();
204
+ const tool = event.toolName;
205
+ const settings = loadWorkflowSettings(ctx.cwd);
206
+
207
+ // Sub-agent child processes should obey their own --tools allow-list from the
208
+ // agent file. Parent workflow phase guards must not remove bash/read tools.
209
+ // Destructive bash remains blocked when global safety requires it.
210
+ if (isSubagentWorker()) {
211
+ if (tool === "bash") {
212
+ const command = String((event.input as { command?: unknown }).command ?? "");
213
+ if (commandBlocked(command, ctx.cwd)) return { block: true, reason: `Workflow safety blocked destructive sub-agent bash command: ${command}` };
214
+ }
215
+ return;
216
+ }
217
+
218
+ if (repoLockEnabled(settings)) {
219
+ if (PATH_SCOPED_TOOLS.has(tool)) {
220
+ const reason = repoLockPathBlock((event.input as { path?: unknown }).path, ctx.cwd);
221
+ if (reason) return { block: true, reason };
222
+ }
223
+ if (tool === "bash") {
224
+ const command = String((event.input as { command?: unknown }).command ?? "");
225
+ const reason = repoLockBashBlock(command, ctx.cwd);
226
+ if (reason) return { block: true, reason };
227
+ }
228
+ if (tool === "subagent") {
229
+ const reason = repoLockPathBlock(".", ctx.cwd);
230
+ if (reason) return { block: true, reason };
231
+ }
232
+ }
233
+
234
+ if (tool === STANDARD_HANDOFF_RESULT_TOOL && state.mode !== "standard") return { block: true, reason: "Standard handoff result is only available while Standard Mode is active." };
235
+
236
+ 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.` };
237
+ 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." };
238
+ if (tool === WORKFLOW_EXECUTION_RESULT_TOOL && state.mode !== "executing") return { block: true, reason: "workflow_execution_result is only available during Plan execution." };
239
+ if (tool === MISSION_MILESTONE_RESULT_TOOL && state.mode !== "mission_running") return { block: true, reason: "mission_milestone_result is only available during Mission execution." };
240
+ if (tool === WORKFLOW_VALIDATION_RESULT_TOOL && !isValidationResultMode(state.mode)) return { block: true, reason: "workflow_validation_result is only available during validation phases." };
241
+ 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." };
242
+
243
+ if (tool === "standard_todo") {
244
+ if (state.mode !== "standard") return { block: true, reason: "Standard Mode To Do is only available while Standard Mode is active." };
245
+ 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." };
246
+ }
247
+
248
+ if (state.mode === "standard" && tool !== "standard_todo" && standardRequiredTodoMissing(state, settings)) {
249
+ if (tool === "edit" || tool === "write" || tool === "subagent") return { block: true, reason: `Standard Mode ${tool} is blocked until required dynamic task-specific To Do tracking is initialized with standard_todo.` };
250
+ if (tool === "bash") {
251
+ const command = String((event.input as { command?: unknown }).command ?? "");
252
+ if (!standardSafeReadOnlyBash(command)) return { block: true, reason: "Standard Mode bash is blocked until required dynamic task-specific To Do tracking is initialized with standard_todo." };
253
+ }
254
+ }
255
+
256
+ if (isPlanMode(state.mode)) {
257
+ 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)" : ""}` };
258
+ if (tool === "bash" && settings.safety.disableBashInPlanMode !== false) return { block: true, reason: `Workflow Plan Mode blocks bash. Allowed tools: ${PLAN_TOOLS.join(", ")}` };
259
+ }
260
+
261
+ if (isValidatorMode(state.mode)) {
262
+ if (tool === "edit" || tool === "write") return { block: true, reason: `Workflow Review/Validator Mode blocks ${tool}. Allowed tools: ${VALIDATOR_TOOLS.join(", ")}` };
263
+ if (tool === "bash") {
264
+ const command = String((event.input as { command?: unknown }).command ?? "");
265
+ if (!validatorSafeEvidenceBash(command)) return { block: true, reason: `Workflow Review/Validator Mode blocks unsafe bash. Allowed bash is limited to safe read-only evidence commands.` };
266
+ }
267
+ }
268
+
269
+ if ((isExecutionMode(state.mode) || isPlanMode(state.mode) || isValidatorMode(state.mode)) && tool === "bash") {
270
+ const command = String((event.input as { command?: unknown }).command ?? "");
271
+ if (commandBlocked(command, ctx.cwd)) return { block: true, reason: `Workflow safety blocked destructive or out-of-scope bash command: ${command}` };
272
+ }
273
+ });
274
+
275
+ pi.on("user_bash", (event, ctx) => {
276
+ const state = getState();
277
+ const settings = loadWorkflowSettings(ctx.cwd);
278
+
279
+ if (isSubagentWorker()) {
280
+ if (commandBlocked(event.command, ctx.cwd)) return { result: { output: `Workflow safety blocked destructive sub-agent command: ${event.command}`, exitCode: 1, cancelled: false, truncated: false } };
281
+ return;
282
+ }
283
+
284
+ if (repoLockEnabled(settings)) {
285
+ const reason = repoLockBashBlock(event.command, ctx.cwd);
286
+ if (reason) return { result: { output: reason, exitCode: 1, cancelled: false, truncated: false } };
287
+ }
288
+
289
+ if (isPlanMode(state.mode) && settings.safety.disableBashInPlanMode !== false) {
290
+ return { result: { output: `Workflow ${state.mode} blocks user bash: ${event.command}`, exitCode: 1, cancelled: false, truncated: false } };
291
+ }
292
+ if (isValidatorMode(state.mode) && !validatorSafeEvidenceBash(event.command)) {
293
+ return { result: { output: `Workflow ${state.mode} blocks unsafe user bash: ${event.command}`, exitCode: 1, cancelled: false, truncated: false } };
294
+ }
295
+ if ((isExecutionMode(state.mode) || isPlanMode(state.mode) || isValidatorMode(state.mode)) && commandBlocked(event.command, ctx.cwd)) {
296
+ return { result: { output: `Workflow safety blocked destructive command: ${event.command}`, exitCode: 1, cancelled: false, truncated: false } };
297
+ }
298
+ });
299
+ }
300
+
301
+ // No-op default export so this helper module can be safely auto-discovered as a Pi extension.
302
+ export default function workflowSuiteNoopExtension(): void {}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Validation failure classification for Pi Workflow Suite.
3
+ *
4
+ * Determines whether a validation failure is repairable by code changes,
5
+ * manual-only (visual/browser QA), or ambiguous.
6
+ *
7
+ * Extracted from workflow-modes.ts for independent testability.
8
+ */
9
+
10
+ import type { PlanValidationStatus, WorkflowState } from "./workflow-state.js";
11
+
12
+ export type ValidationFailureClassification = "manual_only" | "repairable" | "ambiguous";
13
+
14
+ function structuredValidationField(text: string, label: string): string | undefined {
15
+ const match = text.match(new RegExp(`(?:^|\\n)\\s*(?:[-*]\\s*)?(?:\\*\\*)?${label}(?:\\*\\*)?\\s*:\\s*([^\\n]+)`, "i"));
16
+ return match?.[1]?.trim().toLowerCase();
17
+ }
18
+
19
+ function structuredValidationYes(text: string, label: string): boolean | undefined {
20
+ const value = structuredValidationField(text, label);
21
+ if (!value) return undefined;
22
+ if (/^(yes|true|y)\b/.test(value)) return true;
23
+ if (/^(no|false|n)\b/.test(value)) return false;
24
+ return undefined;
25
+ }
26
+
27
+ /**
28
+ * Check whether a validation report contains concrete repairable issues.
29
+ *
30
+ * Uses generic software-validation keywords only. The regex looks for
31
+ * build errors, type errors, test failures, missing requirements,
32
+ * regressions, and similar actionable findings. It deliberately
33
+ * excludes project-specific or content-specific keywords so the
34
+ * classifier works correctly across any codebase.
35
+ */
36
+ export function validationReportHasRepairableIssue(text?: string): boolean {
37
+ if (structuredValidationYes(text ?? "", "Concrete Repairable Issue") === false) return false;
38
+ if (structuredValidationYes(text ?? "", "Concrete Repairable Issue") === true) return true;
39
+ const normalized = (text ?? "").toLowerCase();
40
+ if (!normalized.trim()) return false;
41
+ const actionable = normalized
42
+ .replace(/\bno (actual |concrete )?(code |repairable )?(failure|failures|issue|issues|defect|defects)\b/g, " ")
43
+ .replace(/\bno automated repair is needed\b/g, " ")
44
+ .replace(/\bno specific missing requirements? (?:is |are )?identified\b/g, " ")
45
+ .replace(/\bmanual[-\s]only\b/g, " ");
46
+ return /\b(needs? repair|needs? revision|repair pass|repairable (issue|failure|defect)|concrete (issue|failure|defect|regression)|critical issues?|must fix|required fixes|fixes required|missing requirements?|not fully meet|does not fully meet|not (a )?full final artifact|acceptable as (a )?checkpoint baseline but not (a )?(full )?final artifact|unexpected changes?|regression introduced|build (failed|error)|type error|tests? failed|new lint error|incomplete (file|artifact|implementation|coverage)|persistent artifact|structured artifact|risk register artifact|artifact required|(?:produce|create|add|write) (a )?(structured |persistent )?(risk register )?artifact|missing (file|config|import|export|declaration|function|module|dependency))\b/.test(actionable);
47
+ }
48
+
49
+ export function validationReportIsEvidenceGap(text?: string): boolean {
50
+ const report = text ?? "";
51
+ const evidenceGap = structuredValidationYes(report, "Evidence Gap");
52
+ const repairable = structuredValidationYes(report, "Concrete Repairable Issue");
53
+ if (evidenceGap === true && repairable !== true) return true;
54
+ const normalized = report.toLowerCase();
55
+ return /\b(evidence gap|evidence unavailable|insufficient evidence|could not verify|cannot verify|unable to verify|not enough evidence|provenance could not be proven)\b/.test(normalized)
56
+ && !validationReportHasRepairableIssue(report);
57
+ }
58
+
59
+ /**
60
+ * Check whether a validation report represents only a manual/visual QA caveat
61
+ * with no concrete repairable code issue.
62
+ */
63
+ export function validationReportIsManualOnlyCaveat(text?: string): boolean {
64
+ const report = text ?? "";
65
+ const manual = structuredValidationYes(report, "Manual Verification Required");
66
+ const repairable = structuredValidationYes(report, "Concrete Repairable Issue");
67
+ if (manual === true && repairable === false) return true;
68
+ const normalized = report.toLowerCase();
69
+ if (!normalized.trim()) return false;
70
+ const manualCaveat = /(manual|visual|browser).{0,50}(verification|qa|inspection|confirmation)|visual[-\s]?verification caveat|pass with.{0,40}caveat|manual verification needed/.test(normalized);
71
+ const noConcreteRepairableIssue = /no (actual |concrete )?(code |repairable )?(failure|failures|issue|issues|defect|defects)|no (code |repairable )?failures exist|no concrete (code |repairable )?issues?|only remaining validation item is manual|only incomplete item is manual|cannot be performed through (code )?repair|out of scope for (code )?repair/.test(normalized);
72
+ return manualCaveat && noConcreteRepairableIssue && !validationReportHasRepairableIssue(normalized);
73
+ }
74
+
75
+ /**
76
+ * Classify a validation failure as manual-only, repairable, or ambiguous.
77
+ */
78
+ export function classifyValidationFailure(verdict: WorkflowState["validationVerdict"], report: string): ValidationFailureClassification {
79
+ if (validationReportIsManualOnlyCaveat(report)) return "manual_only";
80
+ if (validationReportIsEvidenceGap(report)) return "ambiguous";
81
+ if (verdict === "FAIL" || validationReportHasRepairableIssue(report)) return "repairable";
82
+ return "ambiguous";
83
+ }
84
+
85
+ /**
86
+ * Normalize a validation verdict. A FAIL with only manual/visual QA caveats
87
+ * is upgraded to PARTIAL PASS because there is no concrete repairable defect.
88
+ */
89
+ export function normalizeValidationVerdict(verdict: WorkflowState["validationVerdict"], report: string): WorkflowState["validationVerdict"] {
90
+ if (verdict === "FAIL" && validationReportIsManualOnlyCaveat(report)) return "PARTIAL PASS";
91
+ return verdict;
92
+ }
93
+
94
+ // Re-export the verdict-to-status helper so consumers do not need workflow-parsers.
95
+ export function planValidationStatusForVerdict(verdict: WorkflowState["validationVerdict"]): PlanValidationStatus {
96
+ if (verdict === "PASS") return "pass";
97
+ if (verdict === "UNKNOWN" || verdict === "PARTIAL PASS") return "unknown";
98
+ return "fail";
99
+ }
100
+
101
+ // No-op default export so this helper module can be safely auto-discovered as a Pi extension.
102
+ export default function workflowSuiteNoopExtension(): void {}