@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.
- package/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +14 -4
- package/README.md +31 -124
- package/SECURITY.md +6 -2
- package/SUPPORT.md +3 -5
- package/VERSION +1 -1
- package/config/prompts/mission-final-validation.md +3 -2
- package/config/prompts/validate-approved-plan.md +4 -3
- package/extensions/subagent/index.ts +69 -3
- package/extensions/subagent/repolock-guard.ts +111 -0
- package/extensions/subagent/runner.ts +51 -3
- package/extensions/workflow-modes.ts +95 -49
- package/extensions/workflow-parsers.ts +2 -1
- package/extensions/workflow-state.ts +2 -1
- package/extensions/workflow-tool-guard.ts +68 -29
- package/extensions/workflow-validation-classifier.ts +5 -2
- package/package.json +3 -2
- package/scripts/install-to-live.sh +2 -1
|
@@ -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
|
|
62
|
-
return
|
|
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
|
|
66
|
-
const
|
|
67
|
-
return
|
|
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
|
|
71
|
-
const root =
|
|
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 (!
|
|
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
|
|
79
|
-
|
|
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 (!
|
|
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
|
-
|
|
208
|
-
|
|
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
|
|
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 === "
|
|
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.
|
|
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'
|