@mediadatafusion/pi-workflow-suite 0.0.10 → 0.0.13
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 +73 -0
- package/README.md +146 -20
- package/VERSION +1 -1
- package/agents/codebase-research.md +7 -5
- package/agents/general-worker.md +9 -7
- package/agents/implementation-planning.md +5 -3
- package/agents/quality-validation.md +9 -8
- package/agents/workflow-orchestrator.md +9 -7
- package/config/prompts/execute-approved-plan.md +12 -2
- package/config/prompts/mission-final-validation.md +38 -5
- package/config/prompts/mission-plan.md +17 -1
- package/config/prompts/mission-repair.md +16 -2
- package/config/prompts/mission-review-prompt.md +55 -0
- package/config/prompts/mission-run.md +18 -5
- package/config/prompts/validate-approved-plan.md +57 -3
- package/config/prompts/workflow-plan-prompt.md +11 -1
- package/config/prompts/workflow-repair.md +18 -2
- package/config/prompts/workflow-reviewer-prompt.md +60 -0
- package/config/prompts/workflow-summary.md +1 -4
- package/config/workflow-settings.example.json +13 -11
- package/extensions/subagent/index.ts +41 -18
- package/extensions/subagent/repolock-guard.ts +224 -4
- package/extensions/subagent/runner.ts +136 -12
- package/extensions/workflow-model-router.ts +152 -55
- package/extensions/workflow-modes.ts +4784 -1087
- package/extensions/workflow-settings-capabilities.ts +10 -0
- package/extensions/workflow-state.ts +139 -15
- package/extensions/workflow-subagent-policy.ts +13 -1
- package/extensions/workflow-summary.ts +8 -19
- package/extensions/workflow-tool-guard.ts +420 -39
- package/extensions/workflow-validation-classifier.ts +46 -4
- package/extensions/workflow-web-tools.ts +361 -1
- package/package.json +9 -5
- package/scripts/audit-live.sh +1 -1
- package/scripts/build-package-export.mjs +8 -13
- package/scripts/check-clean-release-tree.sh +3 -2
- package/scripts/check-package-media.mjs +78 -0
- package/scripts/install-to-live.sh +2 -0
- package/scripts/package-media-config.mjs +28 -0
- package/scripts/prepare-package-readme.mjs +19 -18
- package/scripts/quarantine-live-junk.sh +1 -1
- package/scripts/verify-live.sh +9 -1
- package/skills/implementation-planning/SKILL.md +1 -1
- package/skills/safe-execution/SKILL.md +1 -1
- package/skills/validation-review/SKILL.md +1 -1
|
@@ -45,11 +45,14 @@ export function validationReportHasRepairableIssue(text?: string): boolean {
|
|
|
45
45
|
.replace(/\bno automated repair is needed\b/g, " ")
|
|
46
46
|
.replace(/\bno specific missing requirements? (?:is |are )?identified\b/g, " ")
|
|
47
47
|
.replace(/\bmanual[-\s]only\b/g, " ");
|
|
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);
|
|
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|fix(?:es)? needed|fix\s*:\s*\S|one fix needed|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)|add\s+\S+\s+(?:attribute|to\s+(?:the\s+)?form|to\s+(?:the\s+)?element)|change\s+\S+\s+to\s+\S+|update\s+\S+\s+to\s+\S+|single\s+(?:non[- ]destructive|safe|trivial)\s+(?:attribute\s+)?change|the fix is\s)\b/.test(actionable);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export function validationReportIsEvidenceGap(text?: string): boolean {
|
|
52
52
|
const report = text ?? "";
|
|
53
|
+
// If the report body contains a concrete repairable issue, it is not
|
|
54
|
+
// purely an evidence gap — route to repair so the fix can be applied.
|
|
55
|
+
if (validationReportHasRepairableIssue(report)) return false;
|
|
53
56
|
const evidenceGap = structuredValidationYes(report, "Evidence Gap");
|
|
54
57
|
const repairable = structuredValidationYes(report, "Concrete Repairable Issue");
|
|
55
58
|
if (evidenceGap === true && repairable !== true) return true;
|
|
@@ -58,17 +61,30 @@ export function validationReportIsEvidenceGap(text?: string): boolean {
|
|
|
58
61
|
&& !validationReportHasRepairableIssue(report);
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Patterns that indicate the report describes automatable evidence that was
|
|
66
|
+
* NOT gathered, rather than genuinely human-only verification. These must
|
|
67
|
+
* classify as repairable/evidence-acquisition failures, not manual_only.
|
|
68
|
+
*/
|
|
69
|
+
const AUTOMATABLE_EVIDENCE_MISSING_RE = /\b(browser qa not performed|dev server not (run|started|launched)|localstorage not verified|automated runtime evidence missing|runtime checks? not (run|performed|executed)|preview server not (run|started)|app not launched|endpoint not (tested|verified|checked)|api not (tested|verified|checked)|server not (started|tested|verified)|e2e (test|check|suite) not run|integration test not run|smoke test not (run|performed)|not fully verified|not (fully |independently )?(verified|checked|tested|confirmed)|no (browser|headless|automated) (runner|test|check|verification)|(could not|cannot|unable to) (verify|check|test|confirm|run|start|launch|access)|(was |were )?not (attempted|performed|executed|run|gathered|available)|manual (qa|check|inspection|review) (is |may be )?(still )?required|evidence gaps?)\b/i;
|
|
70
|
+
|
|
61
71
|
/**
|
|
62
72
|
* Check whether a validation report represents only a manual/visual QA caveat
|
|
63
73
|
* with no concrete repairable code issue.
|
|
74
|
+
*
|
|
75
|
+
* IMPORTANT: Reports that mention automatable evidence not being gathered
|
|
76
|
+
* (browser QA, dev server, localStorage, runtime checks) are NOT manual-only.
|
|
77
|
+
* They represent evidence-acquisition failures that should route to repair.
|
|
64
78
|
*/
|
|
65
79
|
export function validationReportIsManualOnlyCaveat(text?: string): boolean {
|
|
66
80
|
const report = text ?? "";
|
|
67
81
|
const manual = structuredValidationYes(report, "Manual Verification Required");
|
|
68
82
|
const repairable = structuredValidationYes(report, "Concrete Repairable Issue");
|
|
69
|
-
if (manual === true && repairable === false) return true;
|
|
83
|
+
if (manual === true && repairable === false && !AUTOMATABLE_EVIDENCE_MISSING_RE.test(report)) return true;
|
|
70
84
|
const normalized = report.toLowerCase();
|
|
71
85
|
if (!normalized.trim()) return false;
|
|
86
|
+
// Automatable evidence patterns must not classify as manual_only
|
|
87
|
+
if (AUTOMATABLE_EVIDENCE_MISSING_RE.test(normalized)) return false;
|
|
72
88
|
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);
|
|
73
89
|
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);
|
|
74
90
|
return manualCaveat && noConcreteRepairableIssue && !validationReportHasRepairableIssue(normalized);
|
|
@@ -77,10 +93,36 @@ export function validationReportIsManualOnlyCaveat(text?: string): boolean {
|
|
|
77
93
|
/**
|
|
78
94
|
* Classify a validation failure as manual-only, repairable, or ambiguous.
|
|
79
95
|
*/
|
|
80
|
-
export function classifyValidationFailure(
|
|
81
|
-
|
|
96
|
+
export function classifyValidationFailure(
|
|
97
|
+
verdict: WorkflowState["validationVerdict"],
|
|
98
|
+
report: string,
|
|
99
|
+
opts?: { concreteRepairableIssue?: boolean; manualVerificationRequired?: boolean },
|
|
100
|
+
): ValidationFailureClassification {
|
|
101
|
+
const manualField = structuredValidationYes(report, "Manual Verification Required");
|
|
102
|
+
const repairableField = structuredValidationYes(report, "Concrete Repairable Issue");
|
|
103
|
+
|
|
104
|
+
// Positive repairability signals win over manual caveats.
|
|
105
|
+
if (opts?.concreteRepairableIssue === true || repairableField === true) return "repairable";
|
|
106
|
+
|
|
107
|
+
// Automatable evidence not gathered is repairable/evidence-acquisition work,
|
|
108
|
+
// even if a legacy report also marks manual verification as required.
|
|
109
|
+
if (AUTOMATABLE_EVIDENCE_MISSING_RE.test(report)) return "repairable";
|
|
110
|
+
|
|
111
|
+
// Manual-only is valid only when no automatable or concrete repair signal exists.
|
|
112
|
+
if (opts?.manualVerificationRequired === true && opts?.concreteRepairableIssue === false) return "manual_only";
|
|
113
|
+
if (manualField === true && repairableField === false) return "manual_only";
|
|
114
|
+
|
|
115
|
+
// Evidence gaps without a concrete repairable issue are ambiguous, not repair loops.
|
|
82
116
|
if (validationReportIsEvidenceGap(report)) return "ambiguous";
|
|
117
|
+
|
|
118
|
+
// FAIL or concrete repairable issues → repairable.
|
|
83
119
|
if (verdict === "FAIL" || validationReportHasRepairableIssue(report)) return "repairable";
|
|
120
|
+
|
|
121
|
+
if (verdict === "PARTIAL PASS") {
|
|
122
|
+
if (validationReportIsManualOnlyCaveat(report)) return "manual_only";
|
|
123
|
+
return "ambiguous";
|
|
124
|
+
}
|
|
125
|
+
|
|
84
126
|
return "ambiguous";
|
|
85
127
|
}
|
|
86
128
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { type ExtensionAPI, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
3
4
|
import { Type } from "typebox";
|
|
4
5
|
|
|
5
6
|
type RuntimeToolInfo = {
|
|
@@ -32,9 +33,44 @@ export interface WorkflowWebFetchDetails {
|
|
|
32
33
|
fetchedAt: string;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
export interface WorkflowBrowserAction {
|
|
37
|
+
action: "click" | "type" | "wait" | "waitForSelector" | "select" | "evaluate" | "screenshot" | "reload" | "readText" | "readAttr";
|
|
38
|
+
selector?: string;
|
|
39
|
+
value?: string;
|
|
40
|
+
timeout?: number;
|
|
41
|
+
label?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface WorkflowBrowserActionResult {
|
|
45
|
+
label?: string;
|
|
46
|
+
action: string;
|
|
47
|
+
ok: boolean;
|
|
48
|
+
error?: string;
|
|
49
|
+
text?: string;
|
|
50
|
+
attrValue?: string | null;
|
|
51
|
+
result?: unknown;
|
|
52
|
+
found?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface WorkflowBrowserCheckDetails {
|
|
56
|
+
url: string;
|
|
57
|
+
title: string;
|
|
58
|
+
consoleMessages: string[];
|
|
59
|
+
pageErrors: string[];
|
|
60
|
+
elementCounts: Record<string, number>;
|
|
61
|
+
localStorageValues: Record<string, string | null>;
|
|
62
|
+
screenshotPath?: string;
|
|
63
|
+
loadTimeMs: number;
|
|
64
|
+
error?: string;
|
|
65
|
+
actionResults?: WorkflowBrowserActionResult[];
|
|
66
|
+
}
|
|
67
|
+
|
|
35
68
|
const WORKFLOW_WEB_SEARCH_TOOL = "workflow_web_search";
|
|
36
69
|
const WORKFLOW_WEB_FETCH_TOOL = "workflow_web_fetch";
|
|
37
|
-
const
|
|
70
|
+
const WORKFLOW_BROWSER_CHECK_TOOL = "workflow_browser_check";
|
|
71
|
+
const WORKFLOW_STOP_SERVER_TOOL = "workflow_stop_server";
|
|
72
|
+
const WORKFLOW_WEB_TOOLS = [WORKFLOW_WEB_SEARCH_TOOL, WORKFLOW_WEB_FETCH_TOOL, WORKFLOW_BROWSER_CHECK_TOOL, WORKFLOW_STOP_SERVER_TOOL];
|
|
73
|
+
const workflowWebErrorMessage = (error: unknown): string => error instanceof Error ? error.message : String(error ?? "");
|
|
38
74
|
const SEARCH_TIMEOUT_MS = 12_000;
|
|
39
75
|
const FETCH_TIMEOUT_MS = 12_000;
|
|
40
76
|
const MAX_SEARCH_RESULTS = 10;
|
|
@@ -73,6 +109,24 @@ const WorkflowWebFetchParams = Type.Object({
|
|
|
73
109
|
maxChars: Type.Optional(Type.Number({ description: "Maximum extracted text characters to return, 1000-18000. Default 12000.", minimum: 1000, maximum: 18000 })),
|
|
74
110
|
});
|
|
75
111
|
|
|
112
|
+
const WorkflowBrowserCheckParams = Type.Object({
|
|
113
|
+
url: Type.String({ description: "Full URL to check in a headless browser, e.g. http://localhost:8017." }),
|
|
114
|
+
selectors: Type.Optional(Type.Array(Type.String(), { description: "CSS selectors to count matching elements." })),
|
|
115
|
+
localStorageKeys: Type.Optional(Type.Array(Type.String(), { description: "localStorage keys to retrieve values for." })),
|
|
116
|
+
screenshot: Type.Optional(Type.Boolean({ description: "Take a full-page screenshot saved to /tmp/validator_screenshot.png." })),
|
|
117
|
+
actions: Type.Optional(Type.Array(Type.Object({
|
|
118
|
+
action: Type.String({ description: "Interaction type: click, type, wait, waitForSelector, select, evaluate, screenshot, reload, readText, readAttr." }),
|
|
119
|
+
selector: Type.Optional(Type.String({ description: "CSS selector for the target element (required for click, type, waitForSelector, readText, readAttr)." })),
|
|
120
|
+
value: Type.Optional(Type.String({ description: "Text to type (type action), JS expression (evaluate action), or attribute name (readAttr action)." })),
|
|
121
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in ms. Default 5000." })),
|
|
122
|
+
label: Type.Optional(Type.String({ description: "Human-readable label for this step in results." })),
|
|
123
|
+
}), { description: "Sequential browser interaction steps to perform after page load." })),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const WorkflowStopServerParams = Type.Object({
|
|
127
|
+
port: Type.Number({ description: "Port number to kill any process listening on (1-65535).", minimum: 1, maximum: 65535 }),
|
|
128
|
+
});
|
|
129
|
+
|
|
76
130
|
function normalizeToolName(name: string): string {
|
|
77
131
|
return name.trim().toLowerCase();
|
|
78
132
|
}
|
|
@@ -270,6 +324,211 @@ export async function workflowWebFetch(rawUrl: string, maxChars = 12_000): Promi
|
|
|
270
324
|
}
|
|
271
325
|
}
|
|
272
326
|
|
|
327
|
+
export async function workflowBrowserCheck(
|
|
328
|
+
url: string,
|
|
329
|
+
selectors?: string[],
|
|
330
|
+
localStorageKeys?: string[],
|
|
331
|
+
screenshot?: boolean,
|
|
332
|
+
actions?: WorkflowBrowserAction[],
|
|
333
|
+
): Promise<WorkflowBrowserCheckDetails> {
|
|
334
|
+
const startTime = Date.now();
|
|
335
|
+
const cleanUrl = url.trim();
|
|
336
|
+
if (!cleanUrl) throw new Error("URL is required.");
|
|
337
|
+
let puppeteer: typeof import("puppeteer");
|
|
338
|
+
try {
|
|
339
|
+
puppeteer = (await import("puppeteer")).default as typeof import("puppeteer");
|
|
340
|
+
} catch {
|
|
341
|
+
return {
|
|
342
|
+
url: cleanUrl, title: "", consoleMessages: [], pageErrors: [],
|
|
343
|
+
elementCounts: {}, localStorageValues: {}, loadTimeMs: Date.now() - startTime,
|
|
344
|
+
error: "Puppeteer is not available in this environment.",
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
const browser = await puppeteer.launch({ headless: "shell", args: ["--no-sandbox"] });
|
|
348
|
+
try {
|
|
349
|
+
const page = await browser.newPage();
|
|
350
|
+
const consoleMessages: string[] = [];
|
|
351
|
+
const pageErrors: string[] = [];
|
|
352
|
+
page.on("console", (msg) => consoleMessages.push(`${msg.type()}: ${msg.text()}`));
|
|
353
|
+
page.on("pageerror", (err) => pageErrors.push(workflowWebErrorMessage(err)));
|
|
354
|
+
await page.goto(cleanUrl, { waitUntil: "networkidle2", timeout: 15000 });
|
|
355
|
+
const title = await page.title();
|
|
356
|
+
const elementCounts: Record<string, number> = {};
|
|
357
|
+
if (selectors && selectors.length > 0) {
|
|
358
|
+
for (const sel of selectors) {
|
|
359
|
+
try {
|
|
360
|
+
elementCounts[sel] = await page.$$eval(sel, (els) => els.length);
|
|
361
|
+
} catch {
|
|
362
|
+
elementCounts[sel] = 0;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const localStorageValues: Record<string, string | null> = {};
|
|
367
|
+
if (localStorageKeys && localStorageKeys.length > 0) {
|
|
368
|
+
for (const key of localStorageKeys) {
|
|
369
|
+
try {
|
|
370
|
+
localStorageValues[key] = await page.evaluate((k) => localStorage.getItem(k), key);
|
|
371
|
+
} catch {
|
|
372
|
+
localStorageValues[key] = null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
let screenshotPath: string | undefined;
|
|
377
|
+
if (screenshot) {
|
|
378
|
+
screenshotPath = "/tmp/validator_screenshot.png";
|
|
379
|
+
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
380
|
+
}
|
|
381
|
+
const actionResults: WorkflowBrowserActionResult[] = [];
|
|
382
|
+
if (actions && actions.length > 0) {
|
|
383
|
+
for (const act of actions) {
|
|
384
|
+
const result: WorkflowBrowserActionResult = { label: act.label, action: act.action, ok: false };
|
|
385
|
+
const timeout = act.timeout ?? 5000;
|
|
386
|
+
try {
|
|
387
|
+
switch (act.action) {
|
|
388
|
+
case "click":
|
|
389
|
+
if (!act.selector) throw new Error("selector required for click");
|
|
390
|
+
await page.waitForSelector(act.selector, { timeout });
|
|
391
|
+
await page.click(act.selector);
|
|
392
|
+
result.ok = true;
|
|
393
|
+
break;
|
|
394
|
+
case "type":
|
|
395
|
+
if (!act.selector) throw new Error("selector required for type");
|
|
396
|
+
if (act.value === undefined) throw new Error("value required for type");
|
|
397
|
+
await page.waitForSelector(act.selector, { timeout });
|
|
398
|
+
await page.type(act.selector, act.value, { delay: 20 });
|
|
399
|
+
result.ok = true;
|
|
400
|
+
break;
|
|
401
|
+
case "wait":
|
|
402
|
+
await new Promise((r) => setTimeout(r, timeout));
|
|
403
|
+
result.ok = true;
|
|
404
|
+
break;
|
|
405
|
+
case "waitForSelector":
|
|
406
|
+
if (!act.selector) throw new Error("selector required for waitForSelector");
|
|
407
|
+
await page.waitForSelector(act.selector, { timeout });
|
|
408
|
+
result.found = true;
|
|
409
|
+
result.ok = true;
|
|
410
|
+
break;
|
|
411
|
+
case "select":
|
|
412
|
+
if (!act.selector) throw new Error("selector required for select");
|
|
413
|
+
if (act.value === undefined) throw new Error("value required for select");
|
|
414
|
+
await page.waitForSelector(act.selector, { timeout });
|
|
415
|
+
await page.select(act.selector, act.value);
|
|
416
|
+
result.ok = true;
|
|
417
|
+
break;
|
|
418
|
+
case "evaluate":
|
|
419
|
+
if (act.value === undefined) throw new Error("value (JS expression) required for evaluate");
|
|
420
|
+
result.result = await page.evaluate(act.value);
|
|
421
|
+
result.ok = true;
|
|
422
|
+
break;
|
|
423
|
+
case "screenshot": {
|
|
424
|
+
const stepPath = `/tmp/validator_step_${actionResults.length}.png`;
|
|
425
|
+
await page.screenshot({ path: stepPath, fullPage: true });
|
|
426
|
+
result.result = stepPath;
|
|
427
|
+
result.ok = true;
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
case "reload":
|
|
431
|
+
await page.reload({ waitUntil: "networkidle2", timeout });
|
|
432
|
+
result.ok = true;
|
|
433
|
+
break;
|
|
434
|
+
case "readText":
|
|
435
|
+
if (!act.selector) throw new Error("selector required for readText");
|
|
436
|
+
await page.waitForSelector(act.selector, { timeout });
|
|
437
|
+
result.text = await page.$eval(act.selector, (el) => (el as HTMLElement).innerText?.trim() ?? el.textContent?.trim() ?? "");
|
|
438
|
+
result.ok = true;
|
|
439
|
+
break;
|
|
440
|
+
case "readAttr":
|
|
441
|
+
if (!act.selector) throw new Error("selector required for readAttr");
|
|
442
|
+
if (!act.value) throw new Error("value (attribute name) required for readAttr");
|
|
443
|
+
await page.waitForSelector(act.selector, { timeout });
|
|
444
|
+
result.attrValue = await page.$eval(act.selector, (el, attr) => el.getAttribute(attr as string), act.value);
|
|
445
|
+
result.ok = true;
|
|
446
|
+
break;
|
|
447
|
+
default:
|
|
448
|
+
result.error = `Unknown action type: ${act.action}`;
|
|
449
|
+
}
|
|
450
|
+
} catch (err) {
|
|
451
|
+
result.error = err instanceof Error ? err.message : String(err);
|
|
452
|
+
}
|
|
453
|
+
actionResults.push(result);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
url: cleanUrl, title, consoleMessages, pageErrors,
|
|
458
|
+
elementCounts, localStorageValues, screenshotPath,
|
|
459
|
+
loadTimeMs: Date.now() - startTime,
|
|
460
|
+
...(actionResults.length > 0 ? { actionResults } : {}),
|
|
461
|
+
};
|
|
462
|
+
} finally {
|
|
463
|
+
await browser.close();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
// ── Cross-platform port-kill utility ──────────────────────────
|
|
469
|
+
|
|
470
|
+
function killOnPortUnix(port: number): boolean {
|
|
471
|
+
// Primary: lsof — works on macOS (built-in) and most Linux systems
|
|
472
|
+
try {
|
|
473
|
+
const out = execSync("lsof -ti :" + port + " 2>/dev/null", { timeout: 3000, encoding: "utf8" });
|
|
474
|
+
const pids = out.trim().split("\n").map(Number).filter(pid => pid > 0);
|
|
475
|
+
if (pids.length > 0) {
|
|
476
|
+
for (const pid of pids) {
|
|
477
|
+
try { process.kill(pid, "SIGTERM"); } catch { /* already dead */ }
|
|
478
|
+
}
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
} catch { /* lsof unavailable or port free */ }
|
|
482
|
+
// Fallback 1: fuser — Linux psmisc
|
|
483
|
+
try {
|
|
484
|
+
execSync("fuser -k " + port + "/tcp 2>/dev/null || true", { timeout: 3000 });
|
|
485
|
+
return true;
|
|
486
|
+
} catch { /* fuser unavailable */ }
|
|
487
|
+
// Fallback 2: ss — Linux iproute2 (always available, even in minimal containers)
|
|
488
|
+
try {
|
|
489
|
+
const out = execSync("ss -tlnp 'sport = :" + port + "' 2>/dev/null", { timeout: 3000, encoding: "utf8" });
|
|
490
|
+
const match = out.match(/pid=(\d+)/);
|
|
491
|
+
if (match) {
|
|
492
|
+
try { process.kill(parseInt(match[1], 10), "SIGTERM"); } catch { /* already dead */ }
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
} catch { /* ss unavailable */ }
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function killOnPortWindows(port: number): boolean {
|
|
500
|
+
try {
|
|
501
|
+
const out = execSync("netstat -ano | findstr :" + port, { timeout: 3000, encoding: "utf8", shell: "cmd.exe" });
|
|
502
|
+
const lines = out.split("\n");
|
|
503
|
+
let killed = false;
|
|
504
|
+
for (const line of lines) {
|
|
505
|
+
const parts = line.trim().split(/\s+/);
|
|
506
|
+
if (parts.length >= 5 && parts[1] && parts[1].endsWith(":" + port)) {
|
|
507
|
+
const pid = parseInt(parts[parts.length - 1], 10);
|
|
508
|
+
if (pid > 0) {
|
|
509
|
+
try {
|
|
510
|
+
execSync("taskkill /PID " + pid + " /F", { timeout: 3000, shell: "cmd.exe" });
|
|
511
|
+
killed = true;
|
|
512
|
+
} catch { /* taskkill failed */ }
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return killed;
|
|
517
|
+
} catch { return false; }
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function killProcessOnPort(port: number): boolean {
|
|
521
|
+
try {
|
|
522
|
+
if (process.platform === "win32") {
|
|
523
|
+
return killOnPortWindows(port);
|
|
524
|
+
} else {
|
|
525
|
+
return killOnPortUnix(port);
|
|
526
|
+
}
|
|
527
|
+
} catch {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
273
532
|
export function refreshRuntimeWebTools(pi: ExtensionAPI): string[] {
|
|
274
533
|
const tools = pi.getAllTools() as RuntimeToolInfo[];
|
|
275
534
|
discoveredRuntimeWebTools = Array.from(new Set(
|
|
@@ -351,6 +610,107 @@ export function registerWorkflowWebTools(pi: ExtensionAPI): void {
|
|
|
351
610
|
}
|
|
352
611
|
},
|
|
353
612
|
} as ToolDefinition<typeof WorkflowWebFetchParams, WorkflowWebFetchDetails>);
|
|
613
|
+
|
|
614
|
+
pi.registerTool({
|
|
615
|
+
name: WORKFLOW_BROWSER_CHECK_TOOL,
|
|
616
|
+
label: "Workflow Browser Check",
|
|
617
|
+
description: "Launch a headless browser to verify a web app. Supports passive checks (console errors, DOM elements, localStorage) and interactive actions (click, type, wait, read text, read attributes, screenshots, page reload). Use for browser-level validation evidence when dev server or static HTTP server is running.",
|
|
618
|
+
promptSnippet: "Launch headless browser for passive checks and interactive actions at a URL",
|
|
619
|
+
promptGuidelines: [
|
|
620
|
+
"Use workflow_browser_check to gather browser-level validation evidence. For passive checks, pass selectors and localStorageKeys. For interactive testing, pass an actions array with click, type, waitForSelector, readText, readAttr, screenshot, reload, evaluate, wait, or select steps.",
|
|
621
|
+
"Start a dev server or python3 http.server first, then pass the URL to workflow_browser_check.",
|
|
622
|
+
"After browser checks complete, stop the server with workflow_stop_server({ port: PORT }) — do not rely on platform-specific shell commands.",
|
|
623
|
+
"Actions execute sequentially after page load. Each action returns ok/error and type-specific results (text, attrValue, result, found). Failed actions do not abort remaining actions.",
|
|
624
|
+
"Use readText to extract visible text from elements. Use readAttr to read attribute values like 'value', 'class', 'disabled'. Use evaluate for arbitrary JS like document.title or localStorage.getItem('key').",
|
|
625
|
+
"Set screenshot: true for initial page, or use a screenshot action step after interactions.",
|
|
626
|
+
],
|
|
627
|
+
parameters: WorkflowBrowserCheckParams,
|
|
628
|
+
executionMode: "sequential",
|
|
629
|
+
async execute(_toolCallId, params, signal): Promise<AgentToolResult<WorkflowBrowserCheckDetails>> {
|
|
630
|
+
if (signal?.aborted) throw new Error("Browser check aborted.");
|
|
631
|
+
try {
|
|
632
|
+
const p = params as { url?: unknown; selectors?: unknown; localStorageKeys?: unknown; screenshot?: unknown; actions?: unknown };
|
|
633
|
+
const rawActions = Array.isArray(p.actions) ? p.actions : undefined;
|
|
634
|
+
const parsedActions: WorkflowBrowserAction[] | undefined = rawActions?.map((a: unknown) => {
|
|
635
|
+
const item = a as Record<string, unknown>;
|
|
636
|
+
return {
|
|
637
|
+
action: String(item.action ?? ""),
|
|
638
|
+
selector: typeof item.selector === "string" ? item.selector : undefined,
|
|
639
|
+
value: typeof item.value === "string" ? item.value : undefined,
|
|
640
|
+
timeout: typeof item.timeout === "number" ? item.timeout : undefined,
|
|
641
|
+
label: typeof item.label === "string" ? item.label : undefined,
|
|
642
|
+
} as WorkflowBrowserAction;
|
|
643
|
+
});
|
|
644
|
+
const details = await workflowBrowserCheck(
|
|
645
|
+
String(p.url ?? ""),
|
|
646
|
+
Array.isArray(p.selectors) ? p.selectors.map(String) : undefined,
|
|
647
|
+
Array.isArray(p.localStorageKeys) ? p.localStorageKeys.map(String) : undefined,
|
|
648
|
+
Boolean(p.screenshot),
|
|
649
|
+
parsedActions,
|
|
650
|
+
);
|
|
651
|
+
const lines: string[] = [
|
|
652
|
+
`URL: ${details.url}`,
|
|
653
|
+
`Title: ${details.title || "(none)"}`,
|
|
654
|
+
`Load time: ${details.loadTimeMs}ms`,
|
|
655
|
+
];
|
|
656
|
+
if (details.error) {
|
|
657
|
+
lines.push(`Error: ${details.error}`);
|
|
658
|
+
} else {
|
|
659
|
+
lines.push(`Console messages: ${details.consoleMessages.length}`);
|
|
660
|
+
for (const msg of details.consoleMessages.slice(0, 20)) lines.push(` ${msg}`);
|
|
661
|
+
if (details.consoleMessages.length > 20) lines.push(` ... and ${details.consoleMessages.length - 20} more`);
|
|
662
|
+
lines.push(`Page errors: ${details.pageErrors.length}`);
|
|
663
|
+
for (const err of details.pageErrors) lines.push(` ${err}`);
|
|
664
|
+
if (details.elementCounts && Object.keys(details.elementCounts).length > 0) {
|
|
665
|
+
lines.push("Element counts:");
|
|
666
|
+
for (const [sel, count] of Object.entries(details.elementCounts)) lines.push(` ${sel}: ${count}`);
|
|
667
|
+
}
|
|
668
|
+
if (details.localStorageValues && Object.keys(details.localStorageValues).length > 0) {
|
|
669
|
+
lines.push("localStorage:");
|
|
670
|
+
for (const [key, val] of Object.entries(details.localStorageValues)) lines.push(` ${key}: ${val ?? "(null)"}`);
|
|
671
|
+
}
|
|
672
|
+
if (details.screenshotPath) lines.push(`Screenshot: ${details.screenshotPath}`);
|
|
673
|
+
if (details.actionResults && details.actionResults.length > 0) {
|
|
674
|
+
lines.push(`Actions: ${details.actionResults.length} step(s)`);
|
|
675
|
+
for (const r of details.actionResults) {
|
|
676
|
+
const label = r.label ? `[${r.label}] ` : "";
|
|
677
|
+
const parts = [` ${label}${r.action}: ${r.ok ? "OK" : "FAILED"}`];
|
|
678
|
+
if (r.error) parts.push(`| error: ${r.error}`);
|
|
679
|
+
if (r.text !== undefined) parts.push(`| text: "${r.text.slice(0, 200)}"`);
|
|
680
|
+
if (r.attrValue !== undefined) parts.push(`| attr: ${r.attrValue}`);
|
|
681
|
+
if (r.found !== undefined) parts.push(`| found: ${r.found}`);
|
|
682
|
+
if (r.result !== undefined) parts.push(`| result: ${typeof r.result === "string" ? r.result : JSON.stringify(r.result)}`);
|
|
683
|
+
lines.push(parts.join(" "));
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details };
|
|
688
|
+
} catch (error) {
|
|
689
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
690
|
+
return { content: [{ type: "text", text: `Workflow browser check failed: ${message}` }], details: { url: String((params as { url?: unknown }).url ?? ""), title: "", consoleMessages: [], pageErrors: [], elementCounts: {}, localStorageValues: {}, loadTimeMs: 0, error: message } };
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
} as ToolDefinition<typeof WorkflowBrowserCheckParams, WorkflowBrowserCheckDetails>);
|
|
694
|
+
|
|
695
|
+
pi.registerTool({
|
|
696
|
+
name: WORKFLOW_STOP_SERVER_TOOL,
|
|
697
|
+
label: "Workflow Stop Server",
|
|
698
|
+
description: "Kill any process listening on the given port. Cross-platform (macOS, Windows, Linux). Use to stop dev servers, static HTTP servers, or any background process started for validation.",
|
|
699
|
+
promptSnippet: "Stop a server process on a port",
|
|
700
|
+
promptGuidelines: [
|
|
701
|
+
"Use workflow_stop_server to reliably stop dev servers or static servers after validation instead of shell commands.",
|
|
702
|
+
"Pass the port number the server is listening on (e.g., 3017 for a server started on port 3017).",
|
|
703
|
+
"This works cross-platform and does not require fuser, lsof, or other platform-specific tools installed.",
|
|
704
|
+
],
|
|
705
|
+
parameters: WorkflowStopServerParams,
|
|
706
|
+
executionMode: "parallel",
|
|
707
|
+
async execute(_toolCallId, params, signal): Promise<AgentToolResult<{ port: number; killed: boolean }>> {
|
|
708
|
+
if (signal?.aborted) throw new Error("Server stop aborted.");
|
|
709
|
+
const port = (params as { port: number }).port;
|
|
710
|
+
const killed = killProcessOnPort(port);
|
|
711
|
+
return { content: [{ type: "text", text: killed ? "Stopped process on port " + port + "." : "No process found on port " + port + " (already free or unable to kill)." }], details: { port, killed } };
|
|
712
|
+
},
|
|
713
|
+
} as ToolDefinition<typeof WorkflowStopServerParams, { port: number; killed: boolean }>);
|
|
354
714
|
}
|
|
355
715
|
|
|
356
716
|
export default function workflowWebToolsNoopExtension(): void {}
|
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.13",
|
|
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/",
|
|
@@ -49,6 +50,8 @@
|
|
|
49
50
|
"scripts/bootstrap-project.sh",
|
|
50
51
|
"scripts/check-clean-release-tree.sh",
|
|
51
52
|
"scripts/check-package-size.mjs",
|
|
53
|
+
"scripts/check-package-media.mjs",
|
|
54
|
+
"scripts/package-media-config.mjs",
|
|
52
55
|
"scripts/prepare-package-readme.mjs",
|
|
53
56
|
"scripts/build-package-export.mjs",
|
|
54
57
|
"README.md",
|
|
@@ -77,8 +80,8 @@
|
|
|
77
80
|
"themes": [
|
|
78
81
|
"./themes"
|
|
79
82
|
],
|
|
80
|
-
"image": "https://cdn.jsdelivr.net/npm/@mediadatafusion/pi-workflow-suite@0.0.
|
|
81
|
-
"video": "https://cdn.jsdelivr.net/npm/@mediadatafusion/pi-workflow-suite@0.0.
|
|
83
|
+
"image": "https://cdn.jsdelivr.net/npm/@mediadatafusion/pi-workflow-suite@0.0.12/docs/assets/pi-workflow-suite-header.png",
|
|
84
|
+
"video": "https://cdn.jsdelivr.net/npm/@mediadatafusion/pi-workflow-suite@0.0.12/docs/assets/pi-workflow-suite-demo.mp4"
|
|
82
85
|
},
|
|
83
86
|
"peerDependencies": {
|
|
84
87
|
"@earendil-works/pi-agent-core": "*",
|
|
@@ -103,9 +106,10 @@
|
|
|
103
106
|
"typescript": "^6.0.3"
|
|
104
107
|
},
|
|
105
108
|
"scripts": {
|
|
106
|
-
"check:ts": "tsc --noEmit
|
|
109
|
+
"check:ts": "tsc --noEmit",
|
|
107
110
|
"typecheck": "tsc --noEmit",
|
|
108
|
-
"validate": "npm run check:ts &&
|
|
111
|
+
"validate": "npm run check:ts && npm run check:package-media && npm run check:package-size && git diff --check",
|
|
112
|
+
"check:package-media": "node scripts/check-package-media.mjs",
|
|
109
113
|
"check:package-size": "node scripts/check-package-size.mjs",
|
|
110
114
|
"prepack": "node scripts/prepare-package-readme.mjs apply",
|
|
111
115
|
"postpack": "node scripts/prepare-package-readme.mjs restore --pack",
|
package/scripts/audit-live.sh
CHANGED
|
@@ -28,7 +28,7 @@ while IFS= read -r rel; do
|
|
|
28
28
|
printf 'MISSING: %s\n' "$rel"
|
|
29
29
|
missing=1
|
|
30
30
|
fi
|
|
31
|
-
done < <(cd "$REPO_DIR" && find agents skills extensions config -type f ! -name '.DS_Store' ! -name '*.backup.*' ! -name '*.broken.*' | sort)
|
|
31
|
+
done < <(cd "$REPO_DIR" && find agents skills extensions config -type f ! -name '.DS_Store' ! -name '*.bak' ! -name '*.backup.*' ! -name '*.broken.*' | sort)
|
|
32
32
|
if [[ "$missing" -eq 0 ]]; then
|
|
33
33
|
printf 'OK: no missing canonical managed files\n'
|
|
34
34
|
fi
|
|
@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { packageMediaUrl, packageMediaUrls } from './package-media-config.mjs';
|
|
8
9
|
|
|
9
10
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const repoRoot = resolve(scriptDir, '..');
|
|
@@ -39,20 +40,14 @@ function parseArgs(argv) {
|
|
|
39
40
|
return args;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
const mediaVersion = '0.0.6';
|
|
43
|
-
|
|
44
|
-
function mediaCdn(assetPath) {
|
|
45
|
-
return `https://cdn.jsdelivr.net/npm/@mediadatafusion/pi-workflow-suite@${mediaVersion}/${assetPath}`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
43
|
function buildPackageReadme(sourceReadme, version) {
|
|
49
44
|
const headerBlock = `# Pi Workflow Suite\n\n${
|
|
50
|
-
``
|
|
51
46
|
}\n\n${[
|
|
52
|
-
`[](#installation)`,
|
|
48
|
+
`[](#quick-start)`,
|
|
49
|
+
`[](#core-commands)`,
|
|
50
|
+
`[](#settings-reference)`,
|
|
56
51
|
].join(' ')}\n\n**Workflow Suite Version:** `;
|
|
57
52
|
|
|
58
53
|
let readme = sourceReadme.replace(
|
|
@@ -60,7 +55,7 @@ function buildPackageReadme(sourceReadme, version) {
|
|
|
60
55
|
headerBlock,
|
|
61
56
|
);
|
|
62
57
|
|
|
63
|
-
const packageMediaBlock = `## Quick Demo\n\nSee Pi Workflow Suite in action: structured workflow modes, settings, runtime status, and guided execution inside Pi.\n\n[](${packageMediaUrls.demoMp4})\n\n## Screenshots\n\n${[
|
|
64
59
|
['Pi Workflow Suite Mission Home with workflow graphs', 'docs/assets/screenshots/00-mission-home.png'],
|
|
65
60
|
['Pi Workflow Suite startup logo', 'docs/assets/screenshots/01-startup-Logo.png'],
|
|
66
61
|
['Workflow Suite theme settings', 'docs/assets/screenshots/02-theme-settings.png'],
|
|
@@ -68,7 +63,7 @@ function buildPackageReadme(sourceReadme, version) {
|
|
|
68
63
|
['Workflow Suite shared sub-agent settings', 'docs/assets/screenshots/04-SharedSubAgentsSettings.png'],
|
|
69
64
|
['Mission Mode milestone progress', 'docs/assets/screenshots/05-mission-mode.png'],
|
|
70
65
|
['Workflow Suite Mermaid diagram output', 'docs/assets/screenshots/06-diagram-mermaid.png'],
|
|
71
|
-
].map(([alt, path]) => ` => `})`).join('\n\n')}\n\n`;
|
|
72
67
|
|
|
73
68
|
readme = readme.replace(/## Quick Demo[\s\S]*?## Contents\n/, `${packageMediaBlock}## Contents\n`);
|
|
74
69
|
return readme;
|
|
@@ -11,6 +11,7 @@ report() {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
if [[ -e AGENTS.md && -n "$(git ls-files AGENTS.md)" ]]; then report 'AGENTS.md must not be present on clean release main'; fi
|
|
14
|
+
if [[ -e CLAUDE.md && -n "$(git ls-files CLAUDE.md)" ]]; then report 'CLAUDE.md must not be present on clean release main'; fi
|
|
14
15
|
if [[ -e .github && -n "$(git ls-files .github)" ]]; then report '.github/ must not be tracked on clean release main'; fi
|
|
15
16
|
if [[ -e .factory && -n "$(git ls-files .factory)" ]]; then report '.factory/ must not be tracked on clean release main'; fi
|
|
16
17
|
if [[ -e .kilo && -n "$(git ls-files .kilo)" ]]; then report '.kilo/ must not be tracked on clean release main'; fi
|
|
@@ -18,7 +19,7 @@ if [[ -e .cursor && -n "$(git ls-files .cursor)" ]]; then report '.cursor/ must
|
|
|
18
19
|
|
|
19
20
|
while IFS= read -r path; do
|
|
20
21
|
case "$path" in
|
|
21
|
-
agents/*|config/*|extensions/*|skills/*|docs/assets/*|themes/*|scripts/install-to-live.sh|scripts/verify-live.sh|scripts/audit-live.sh|scripts/quarantine-live-junk.sh|scripts/backup-live.sh|scripts/audit-settings.sh|scripts/bootstrap-project.sh|scripts/check-clean-release-tree.sh|scripts/check-package-size.mjs|scripts/prepare-package-readme.mjs|scripts/build-package-export.mjs|README.md|CHANGELOG.md|CONTRIBUTING.md|LICENSE.md|NOTICE|SECURITY.md|SUPPORT.md|TRADEMARKS.md|VERSION|package.json|package-lock.json|tsconfig.json|.gitignore)
|
|
22
|
+
agents/*|config/*|extensions/*|skills/*|docs/assets/*|themes/*|scripts/install-to-live.sh|scripts/verify-live.sh|scripts/audit-live.sh|scripts/quarantine-live-junk.sh|scripts/backup-live.sh|scripts/audit-settings.sh|scripts/bootstrap-project.sh|scripts/check-clean-release-tree.sh|scripts/check-package-size.mjs|scripts/check-package-media.mjs|scripts/package-media-config.mjs|scripts/prepare-package-readme.mjs|scripts/build-package-export.mjs|README.md|CHANGELOG.md|CONTRIBUTING.md|LICENSE.md|NOTICE|SECURITY.md|SUPPORT.md|TRADEMARKS.md|VERSION|package.json|package-lock.json|tsconfig.json|.gitignore)
|
|
22
23
|
;;
|
|
23
24
|
docs/*)
|
|
24
25
|
report "non-asset docs file is not allowed: $path"
|
|
@@ -26,7 +27,7 @@ while IFS= read -r path; do
|
|
|
26
27
|
scripts/test-*|scripts/sync-from-live.sh)
|
|
27
28
|
report "internal script is not allowed: $path"
|
|
28
29
|
;;
|
|
29
|
-
.github/*|AGENTS.md|.factory/*|.kilo/*|.cursor/*)
|
|
30
|
+
.github/*|AGENTS.md|CLAUDE.md|.factory/*|.kilo/*|.cursor/*)
|
|
30
31
|
report "internal path is not allowed: $path"
|
|
31
32
|
;;
|
|
32
33
|
*auth.json|*settings.json|*workflow-settings.json|*active.json|workflows/*|*sessions/*|*missions/*|*plans/*|*logs/*|*.env|*.env.*|*.DS_Store)
|