From 1ee83f148ae3174645891ea928b052bcd1a7be02 Mon Sep 17 00:00:00 2001 From: Myke Matos <123015330+shishiv@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:34:59 -0300 Subject: [PATCH] refactor(pstack): drop unused workflow session wake and evidence modules The package export already points at delegation.ts. Callers live in runtime session, Benny schedules, and delivery evidence. Tests that only guarded the deleted files left with them. Co-authored-by: Cursor --- src/workflows/evidence.ts | 89 --------------------- src/workflows/sessions.ts | 124 ------------------------------ src/workflows/wake.ts | 34 -------- test/workflows/workflows.test.mjs | 113 --------------------------- 4 files changed, 360 deletions(-) delete mode 100644 src/workflows/evidence.ts delete mode 100644 src/workflows/sessions.ts delete mode 100644 src/workflows/wake.ts diff --git a/src/workflows/evidence.ts b/src/workflows/evidence.ts deleted file mode 100644 index c8396dc..0000000 --- a/src/workflows/evidence.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** Evidence coverage is explicit so an unavailable source cannot look searched. */ - -export const EVIDENCE_CATEGORIES = [ - "source-control", - "issue-tracker", - "long-form-documents", - "real-time-chat", - "infrastructure-observability", - "error-tracking", - "product-analytics", -] as const; - -export type EvidenceCategory = (typeof EVIDENCE_CATEGORIES)[number]; - -export interface EvidenceSource { - category: EvidenceCategory; - status: "available" | "gap"; - mcpNames: string[]; - reason?: string; -} - -export interface EvidenceMappingOptions { - availableMcps: readonly ( - | string - | { name: string; tools?: readonly string[]; resources?: readonly string[] } - )[]; -} - -export interface EvidenceMappingResult { - sources: EvidenceSource[]; - gaps: string[]; -} - -const MATCHERS: Record, RegExp> = { - "issue-tracker": /linear|jira|github[-_ ]?issues?|plane|shortcut|asana|youtrack/i, - "long-form-documents": /notion|confluence|google[-_ ]?docs?|coda|slite|document/i, - "real-time-chat": /slack|discord|teams?|mattermost|chat/i, - "infrastructure-observability": - /datadog|new[-_ ]?relic|honeycomb|grafana|splunk|observability|prometheus/i, - "error-tracking": /sentry|rollbar|bugsnag|airbrake|exception|error[-_ ]?track/i, - "product-analytics": /databricks|snowflake|bigquery|clickhouse|dbt|redshift|analytics|warehouse/i, -}; - -const CATEGORY_LABELS: Record = { - "source-control": "source control", - "issue-tracker": "issue tracker", - "long-form-documents": "long-form documents", - "real-time-chat": "real-time chat", - "infrastructure-observability": "infrastructure observability", - "error-tracking": "error tracking", - "product-analytics": "product analytics", -}; - -function mcpName( - mcp: string | { name: string; tools?: readonly string[]; resources?: readonly string[] }, -): string { - return typeof mcp === "string" ? mcp : mcp.name; -} - -function matchingNames( - category: Exclude, - names: readonly string[], -): string[] { - const matcher = MATCHERS[category]; - return names.filter((name) => matcher.test(name)); -} - -/** Map the seven evidence categories without dropping unavailable categories. */ -export function mapEvidenceSources(options: EvidenceMappingOptions): EvidenceMappingResult { - const names = options.availableMcps.map(mcpName).filter((name) => name.trim().length > 0); - const sources: EvidenceSource[] = [ - { category: "source-control", status: "available", mcpNames: ["git/gh"] }, - ]; - const gaps: string[] = []; - for (const category of EVIDENCE_CATEGORIES) { - if (category === "source-control") continue; - const matched = matchingNames(category, names); - if (matched.length > 0) { - sources.push({ category, status: "available", mcpNames: [...new Set(matched)] }); - } else { - const reason = `No matching MCP available for ${CATEGORY_LABELS[category]}; this evidence category is an explicit gap.`; - sources.push({ category, status: "gap", mcpNames: [], reason }); - gaps.push(reason); - } - } - return { sources, gaps }; -} - -export const buildEvidenceCoverageMap = mapEvidenceSources; diff --git a/src/workflows/sessions.ts b/src/workflows/sessions.ts deleted file mode 100644 index 90e5f4e..0000000 --- a/src/workflows/sessions.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { readFile } from "node:fs/promises"; -import { existsSync, lstatSync, readdirSync } from "node:fs"; -import { basename, dirname, relative, resolve, sep } from "node:path"; - -export interface SessionDiscoveryOptions { - /** PI_SESSION_FILE may be one active JSONL file or a project-scoped directory. */ - piSessionFile?: string; - env?: Readonly>; - projectDirectory: string; - sessionId?: string; -} - -function projectNames(projectDirectory: string): Set { - const absolute = resolve(projectDirectory); - const slug = absolute.replace(/^[/\\]+/, "").replace(/[\\/]/g, "-"); - return new Set([`--${slug}--`]); -} - -function within(parent: string, candidate: string): boolean { - const child = relative(resolve(parent), resolve(candidate)); - return ( - child === "" || (child !== ".." && !child.startsWith(`..${sep}`) && !child.startsWith("/")) - ); -} - -function belongsToProject(candidate: string, projectDirectory: string): boolean { - if (within(projectDirectory, candidate)) return true; - const names = projectNames(projectDirectory); - // A session file can itself be named like Pi's wrapped project slug, but - // that is not evidence that its arbitrary parent directory is this project. - // Real Pi layouts put the slug in a directory above the JSONL file. - return resolve(dirname(candidate)) - .split(/[\\/]/) - .some((part) => names.has(part)); -} - -function sessionIdMatches(candidate: string, sessionId: string | undefined): boolean { - return sessionId === undefined || basename(candidate).replace(/\.jsonl$/i, "") === sessionId; -} - -function walkJsonl( - directory: string, - projectDirectory: string, - sessionId?: string, - scoped = false, -): string[] { - if (!scoped && !belongsToProject(directory, projectDirectory)) { - // A shared transcript root is allowed, but only project-named descendants are traversed. - const descendants = readdirSync(directory, { withFileTypes: true }); - return descendants - .filter((entry) => entry.isDirectory() && projectNames(projectDirectory).has(entry.name)) - .flatMap((entry) => - walkJsonl(resolve(directory, entry.name), projectDirectory, sessionId, true), - ); - } - return readdirSync(directory, { withFileTypes: true }) - .sort((a, b) => a.name.localeCompare(b.name)) - .flatMap((entry) => { - const path = resolve(directory, entry.name); - if (entry.isDirectory()) return walkJsonl(path, projectDirectory, sessionId, scoped); - if (entry.isFile() && /\.jsonl$/i.test(entry.name) && sessionIdMatches(path, sessionId)) - return [path]; - return []; - }); -} - -/** Discover only the explicitly configured active project's JSONL files. */ -export function discoverSessionFiles(options: SessionDiscoveryOptions): string[] { - const configured = options.piSessionFile ?? options.env?.PI_SESSION_FILE; - if (!configured) return []; - const configuredPath = resolve(configured); - if (!existsSync(configuredPath)) return []; - const metadata = lstatSync(configuredPath); - if (metadata.isFile()) { - return /\.jsonl$/i.test(configuredPath) && - belongsToProject(configuredPath, options.projectDirectory) && - sessionIdMatches(configuredPath, options.sessionId) - ? [configuredPath] - : []; - } - if (!metadata.isDirectory()) return []; - return walkJsonl(configuredPath, options.projectDirectory, options.sessionId); -} - -export interface ParsedSession { - entries: Record[]; - invalidLines: number; -} - -/** Parse JSONL defensively; malformed or non-object lines are reported, never executed. */ -export function parseSessionJsonl(contents: string): ParsedSession { - const entries: Record[] = []; - let invalidLines = 0; - for (const line of contents.split(/\r?\n/)) { - if (!line.trim()) continue; - try { - const parsed: unknown = JSON.parse(line); - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { - invalidLines++; - } else { - entries.push(parsed as Record); - } - } catch { - invalidLines++; - } - } - return { entries, invalidLines }; -} - -export async function readSessionFile(filePath: string): Promise { - return readFile(filePath, "utf8"); -} - -export async function parseActiveSession( - filePath: string, - projectDirectory: string, -): Promise { - if ( - !discoverSessionFiles({ piSessionFile: filePath, projectDirectory }).includes(resolve(filePath)) - ) { - throw new Error("Active session is outside the provided project/session boundary."); - } - return parseSessionJsonl(await readSessionFile(filePath)); -} diff --git a/src/workflows/wake.ts b/src/workflows/wake.ts deleted file mode 100644 index dca563e..0000000 --- a/src/workflows/wake.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** Choose one Pi-native wake primitive for work that outlives the current turn. */ - -export type WakeMechanism = "async-child-wait" | "event-subscription" | "schedule"; - -export interface LongRunWakeRequest { - childRunId?: string; - event?: string; - schedule?: string; -} - -export type LongRunWakePlan = - | { mechanism: "async-child-wait"; childRunId: string } - | { mechanism: "event-subscription"; event: string } - | { mechanism: "schedule"; schedule: string }; - -function nonEmpty(value: string | undefined): string | undefined { - const normalized = value?.trim(); - return normalized || undefined; -} - -/** Child completion wins over event and time fallbacks because it is the precise dependency. */ -export function planLongRunWake(request: LongRunWakeRequest): LongRunWakePlan { - const childRunId = nonEmpty(request.childRunId); - if (childRunId) return { mechanism: "async-child-wait", childRunId }; - const event = nonEmpty(request.event); - if (event) return { mechanism: "event-subscription", event }; - const schedule = nonEmpty(request.schedule); - if (schedule) return { mechanism: "schedule", schedule }; - throw new Error( - "A long-run wake requires childRunId, event, or schedule; polling is not supported.", - ); -} - -export const chooseWakeStrategy = planLongRunWake; diff --git a/test/workflows/workflows.test.mjs b/test/workflows/workflows.test.mjs index 156b657..f2a8e26 100644 --- a/test/workflows/workflows.test.mjs +++ b/test/workflows/workflows.test.mjs @@ -1,15 +1,9 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { mkdtemp, mkdir, writeFile } from "node:fs/promises"; -import { join } from "node:path"; -import { tmpdir } from "node:os"; import { createJiti } from "jiti"; const jiti = createJiti(import.meta.url, { interopDefault: true }); const workflows = await jiti.import("../../src/workflows/delegation.ts"); -const evidence = await jiti.import("../../src/workflows/evidence.ts"); -const sessions = await jiti.import("../../src/workflows/sessions.ts"); -const wake = await jiti.import("../../src/workflows/wake.ts"); test("semantic roles map to Pi agents and preserve read-only boundaries", () => { assert.equal(workflows.agentForRole("explore"), "scout"); @@ -70,110 +64,3 @@ test("workflow builders emit supported, awaited APIs and safely serialized input /worktree must be boolean/, ); }); - -test("evidence mapping keeps unavailable MCP categories as explicit gaps", () => { - const result = evidence.mapEvidenceSources({ - availableMcps: ["linear", "slack"], - }); - assert.equal( - result.sources.find((source) => source.category === "issue-tracker")?.status, - "available", - ); - assert.equal( - result.sources.find((source) => source.category === "real-time-chat")?.status, - "available", - ); - assert.ok(result.gaps.some((gap) => /long-form documents/i.test(gap))); - assert.ok(result.gaps.some((gap) => /error tracking/i.test(gap))); - assert.equal(result.sources.length, evidence.EVIDENCE_CATEGORIES.length); -}); - -test("session discovery stays inside the active project and parses only supplied JSONL", async () => { - const root = await mkdtemp(join(tmpdir(), "pi-session-")); - const projectA = join(root, "project-a"); - const projectB = join(root, "project-b"); - await mkdir(projectA); - await mkdir(projectB); - const active = join(projectA, "active.jsonl"); - await writeFile( - active, - '{"type":"message","text":"ok"}\nnot-json\n{"type":"message","text":"still"}\n', - ); - await writeFile(join(projectB, "unrelated.jsonl"), '{"text":"secret"}\n'); - const found = sessions.discoverSessionFiles({ - piSessionFile: active, - projectDirectory: projectA, - }); - assert.deepEqual(found, [active]); - const parsed = sessions.parseSessionJsonl(await sessions.readSessionFile(active)); - assert.deepEqual(parsed.entries, [ - { type: "message", text: "ok" }, - { type: "message", text: "still" }, - ]); - assert.equal(parsed.invalidLines, 1); - assert.deepEqual( - sessions.discoverSessionFiles({ piSessionFile: active, projectDirectory: projectB }), - [], - ); -}); - -test("session discovery recognizes Pi's wrapped project directory slug", async () => { - const root = await mkdtemp(join(tmpdir(), "pi-session-store-")); - const project = join(root, "workspace", "app"); - const slug = project.replace(/^[/\\]+/, "").replace(/[\\/]/g, "-"); - const projectSessions = join(root, `--${slug}--`); - await mkdir(project, { recursive: true }); - await mkdir(projectSessions); - const active = join(projectSessions, "active.jsonl"); - await writeFile(active, '{"type":"session"}\n'); - - assert.deepEqual( - sessions.discoverSessionFiles({ piSessionFile: active, projectDirectory: project }), - [active], - ); -}); - -test("session discovery rejects an unrelated path that merely contains the project basename", async () => { - const root = await mkdtemp(join(tmpdir(), "pi-session-impostor-")); - const project = join(root, "workspace", "app"); - const impostorDirectory = join(root, "other", "app"); - await mkdir(project, { recursive: true }); - await mkdir(impostorDirectory, { recursive: true }); - const impostor = join(impostorDirectory, "active.jsonl"); - await writeFile(impostor, '{"type":"session"}\n'); - - assert.deepEqual( - sessions.discoverSessionFiles({ piSessionFile: impostor, projectDirectory: project }), - [], - ); -}); - -test("session discovery rejects a project-slug JSONL basename outside Pi's session layout", async () => { - const root = await mkdtemp(join(tmpdir(), "pi-session-slug-impostor-")); - const project = join(root, "workspace", "app"); - await mkdir(project, { recursive: true }); - const slug = project.replace(/^[/\\]+/, "").replace(/[\\/]/g, "-"); - const impostor = join(root, `--${slug}--.jsonl`); - await writeFile(impostor, '{"type":"session"}\n'); - - assert.deepEqual( - sessions.discoverSessionFiles({ piSessionFile: impostor, projectDirectory: project }), - [], - ); -}); - -test("long-run plans choose a native wake mechanism instead of polling", () => { - assert.deepEqual(wake.planLongRunWake({ childRunId: "run-1" }), { - mechanism: "async-child-wait", - childRunId: "run-1", - }); - assert.deepEqual(wake.planLongRunWake({ event: "ci.completed" }), { - mechanism: "event-subscription", - event: "ci.completed", - }); - assert.deepEqual(wake.planLongRunWake({ schedule: "+30m" }), { - mechanism: "schedule", - schedule: "+30m", - }); - assert.throws(() => wake.planLongRunWake({}), /childRunId, event, or schedule/); -});