@mediadatafusion/pi-workflow-suite 0.0.9 → 0.0.10

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.
@@ -58,33 +58,73 @@ function pathInsideRoot(candidate: string, root: string): boolean {
58
58
  return candidate === root || candidate.startsWith(`${root}/`);
59
59
  }
60
60
 
61
- function piRuntimeRoot(): string {
62
- return safeRealpath(getAgentDir());
61
+ function repoLockRoot(cwd: string): string {
62
+ return process.env.PI_WORKFLOW_REPO_LOCK_ENABLED === "1" && process.env.PI_WORKFLOW_REPO_LOCK_ROOT
63
+ ? safeRealpath(process.env.PI_WORKFLOW_REPO_LOCK_ROOT)
64
+ : repoRootForCwd(cwd);
63
65
  }
64
66
 
65
- function pathInsideRepoOrPiRuntime(candidate: string, root: string): boolean {
66
- const piRoot = piRuntimeRoot();
67
- return pathInsideRoot(candidate, root) || pathInsideRoot(candidate, piRoot);
67
+ function protectedRepoPath(candidate: string, root: string): boolean {
68
+ const rel = candidate === root ? "" : candidate.slice(root.length + 1);
69
+ return rel === ".pi" || rel.startsWith(".pi/");
68
70
  }
69
71
 
70
- function repoLockPathBlock(pathValue: unknown, cwd: string): string | undefined {
71
- const root = repoRootForCwd(cwd);
72
+ function piRuntimeInstructionPath(candidate: string): boolean {
73
+ const root = safeRealpath(getAgentDir());
74
+ if (!pathInsideRoot(candidate, root)) return false;
75
+ const rel = candidate === root ? "" : candidate.slice(root.length + 1);
76
+ return rel === "skills" || rel.startsWith("skills/")
77
+ || rel === "agents" || rel.startsWith("agents/")
78
+ || rel === "config/prompts" || rel.startsWith("config/prompts/")
79
+ || rel === "prompts" || rel.startsWith("prompts/")
80
+ || rel === "themes" || rel.startsWith("themes/");
81
+ }
82
+
83
+ function repoLockPathBlock(pathValue: unknown, cwd: string, tool: string): string | undefined {
84
+ const root = repoLockRoot(cwd);
72
85
  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()})`;
86
+ if (!pathInsideRoot(candidate, root)) {
87
+ if ((tool === "read" || tool === "grep" || tool === "find" || tool === "ls") && piRuntimeInstructionPath(candidate)) return undefined;
88
+ return `Repo Lock blocked path outside current repository: ${candidate} (repo root: ${root})`;
89
+ }
90
+ if ((tool === "edit" || tool === "write") && protectedRepoPath(candidate, root)) return `Repo Lock blocked ${tool} for protected project control path: ${candidate}`;
74
91
  return undefined;
75
92
  }
76
93
 
94
+ function stripHereDocBodies(command: string): string {
95
+ const lines = command.split("\n");
96
+ const kept: string[] = [];
97
+ for (let i = 0; i < lines.length; i++) {
98
+ const line = lines[i];
99
+ kept.push(line);
100
+ const match = line.match(/<<[-]?\s*['\"]?([A-Za-z_][A-Za-z0-9_]*)['\"]?/);
101
+ if (!match) continue;
102
+ const marker = match[1];
103
+ i++;
104
+ while (i < lines.length && lines[i].trim() !== marker) i++;
105
+ }
106
+ return kept.join("\n");
107
+ }
108
+
109
+ function stripUriTokens(command: string): string {
110
+ return command.replace(/\b[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s'"`;&|)]*/g, " ");
111
+ }
112
+
113
+ function bashPathCandidates(command: string): string[] {
114
+ const trimmed = stripUriTokens(stripHereDocBodies(command)).trim();
115
+ if (!trimmed) return [];
116
+ return Array.from(trimmed.matchAll(/(?:^|[\s=:'"`])((?:\.{1,2}|~|\/)[^\s'"`;&|)]*)/g)).map((match) => match[1]).filter(Boolean);
117
+ }
118
+
77
119
  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);
120
+ const root = repoLockRoot(cwd);
121
+ const pathCandidates = bashPathCandidates(command);
82
122
  for (const raw of pathCandidates) {
83
- if (raw === "." || raw === "./") continue;
123
+ if (raw === "." || raw === "./" || raw === "/") continue;
84
124
  const cleaned = raw.replace(/[),]+$/, "");
85
125
  if (!cleaned || cleaned.startsWith("./node_modules/.bin")) continue;
86
126
  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()})`;
127
+ if (!pathInsideRoot(candidate, root)) return `Repo Lock blocked bash path outside current repository: ${cleaned} -> ${candidate} (repo root: ${root})`;
88
128
  }
89
129
  return undefined;
90
130
  }
@@ -204,20 +244,11 @@ export function registerToolGuard(pi: ExtensionAPI, getState: () => WorkflowStat
204
244
  const tool = event.toolName;
205
245
  const settings = loadWorkflowSettings(ctx.cwd);
206
246
 
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)) {
247
+ const effectiveRepoLockEnabled = repoLockEnabled(settings) || process.env.PI_WORKFLOW_REPO_LOCK_ENABLED === "1";
248
+ if (effectiveRepoLockEnabled) {
219
249
  if (PATH_SCOPED_TOOLS.has(tool)) {
220
- const reason = repoLockPathBlock((event.input as { path?: unknown }).path, ctx.cwd);
250
+ const input = event.input as { path?: unknown; file_path?: unknown };
251
+ const reason = repoLockPathBlock(input.path ?? input.file_path, ctx.cwd, tool);
221
252
  if (reason) return { block: true, reason };
222
253
  }
223
254
  if (tool === "bash") {
@@ -226,11 +257,19 @@ export function registerToolGuard(pi: ExtensionAPI, getState: () => WorkflowStat
226
257
  if (reason) return { block: true, reason };
227
258
  }
228
259
  if (tool === "subagent") {
229
- const reason = repoLockPathBlock(".", ctx.cwd);
260
+ const reason = repoLockPathBlock(".", ctx.cwd, tool);
230
261
  if (reason) return { block: true, reason };
231
262
  }
232
263
  }
233
264
 
265
+ if (isSubagentWorker()) {
266
+ if (tool === "bash") {
267
+ const command = String((event.input as { command?: unknown }).command ?? "");
268
+ if (commandBlocked(command, ctx.cwd)) return { block: true, reason: `Workflow safety blocked destructive sub-agent bash command: ${command}` };
269
+ }
270
+ return;
271
+ }
272
+
234
273
  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
274
 
236
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.` };
@@ -281,7 +320,7 @@ export function registerToolGuard(pi: ExtensionAPI, getState: () => WorkflowStat
281
320
  return;
282
321
  }
283
322
 
284
- if (repoLockEnabled(settings)) {
323
+ if (repoLockEnabled(settings) || process.env.PI_WORKFLOW_REPO_LOCK_ENABLED === "1") {
285
324
  const reason = repoLockBashBlock(event.command, ctx.cwd);
286
325
  if (reason) return { result: { output: reason, exitCode: 1, cancelled: false, truncated: false } };
287
326
  }
@@ -40,10 +40,12 @@ export function validationReportHasRepairableIssue(text?: string): boolean {
40
40
  if (!normalized.trim()) return false;
41
41
  const actionable = normalized
42
42
  .replace(/\bno (actual |concrete )?(code |repairable )?(failure|failures|issue|issues|defect|defects)\b/g, " ")
43
+ .replace(/\bno (blocking|remaining|required) (issue|issues|action|actions|fix|fixes|gap|gaps)\b/g, " ")
44
+ .replace(/\brequired action (?:is )?(?:manual|visual|browser) (?:verification|qa|inspection|confirmation)\b/g, " ")
43
45
  .replace(/\bno automated repair is needed\b/g, " ")
44
46
  .replace(/\bno specific missing requirements? (?:is |are )?identified\b/g, " ")
45
47
  .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);
48
+ return /\b(needs? repair|needs? revision|repair pass|repairable (issue|failure|defect)|concrete (issue|failure|defect|regression)|blocking issues?|critical issues?|must fix|required (fixes?|actions?)|fixes required|remaining (fixes?|issues?|gaps?)|should be fixed before advancing|apply (the )?(two |[0-9]+ )?remaining fixes?|needs? to be (replaced|updated|expanded|corrected)|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
49
  }
48
50
 
49
51
  export function validationReportIsEvidenceGap(text?: string): boolean {
@@ -94,7 +96,8 @@ export function normalizeValidationVerdict(verdict: WorkflowState["validationVer
94
96
  // Re-export the verdict-to-status helper so consumers do not need workflow-parsers.
95
97
  export function planValidationStatusForVerdict(verdict: WorkflowState["validationVerdict"]): PlanValidationStatus {
96
98
  if (verdict === "PASS") return "pass";
97
- if (verdict === "UNKNOWN" || verdict === "PARTIAL PASS") return "unknown";
99
+ if (verdict === "PARTIAL PASS") return "partial pass";
100
+ if (verdict === "UNKNOWN") return "unknown";
98
101
  return "fail";
99
102
  }
100
103
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediadatafusion/pi-workflow-suite",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
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": {
@@ -71,7 +71,8 @@
71
71
  "./skills"
72
72
  ],
73
73
  "prompts": [
74
- "./config/prompts"
74
+ "./config/prompts",
75
+ "!*.md"
75
76
  ],
76
77
  "themes": [
77
78
  "./themes"
@@ -14,7 +14,7 @@ printf 'A live backup will be created before installing files.\n'
14
14
  is_forbidden_path() {
15
15
  local rel="$1"
16
16
  case "$rel" in
17
- auth.json|settings.json|workflow-settings.json|active.json|workflows/*|missions/*|plans/*|sessions/*|logs/*|*.log|*.backup.*|*.broken.*|.env|.env.*|.factory/*|.cursor/*|*.DS_Store|*.tmp)
17
+ auth.json|settings.json|workflow-settings.json|active.json|workflows/*|missions/*|plans/*|sessions/*|logs/*|*.log|*.backup.*|*.broken.*|.env|.env.*|.factory/*|.cursor/*|.kilo/*|node_modules/*|*.DS_Store|*.tmp)
18
18
  return 0
19
19
  ;;
20
20
  esac
@@ -83,5 +83,6 @@ install_dir "extensions"
83
83
  install_dir "agents"
84
84
  install_dir "skills"
85
85
  install_dir "config"
86
+ install_dir "themes"
86
87
 
87
88
  printf 'install complete; auth, settings, and workflow state were not touched\n'