UNPKG

paqad-ai

Version:

Spec-driven development framework — AI agents that think before they type

434 lines (432 loc) • 12.8 kB
import { createRequire as __paqadCreateRequire } from 'module'; const require = __paqadCreateRequire(import.meta.url); import { PipelineRouter, RequestClassifier } from "../chunk-T7QKKKPS.js"; import "../chunk-YAYHLNWR.js"; import { hasOpenUnendedStage } from "../chunk-FVFH4UAE.js"; import { appendRouteTurn } from "../chunk-FUBOIUNU.js"; import { paqadFrameLead } from "../chunk-VZJTQQG2.js"; import "../chunk-GVVZTJJR.js"; import "../chunk-7Q65STES.js"; import "../chunk-K4E6ACLH.js"; import "../chunk-4ZAWKFXT.js"; import "../chunk-CECHOUNN.js"; import { appendHooksLog } from "../chunk-4S3XFJAH.js"; import "../chunk-HNYZUZ7D.js"; import "../chunk-AK3HYELM.js"; import "../chunk-6XDDIYNJ.js"; import "../chunk-THDANA7U.js"; import "../chunk-I2G54BH7.js"; import "../chunk-5ETIY5I3.js"; import "../chunk-UUBQQJVE.js"; import "../chunk-EQYAXXZO.js"; import "../chunk-MDXUST3Z.js"; import { writeSessionRoute } from "../chunk-S3UQUJV2.js"; import { changeKindForWorkflow, isFeatureDevelopmentRoute, readWorkflowState, resolveRoutedWorkflow, routeWorkflow, writeWorkflowState } from "../chunk-VOJO7HJ6.js"; import { writePendingImpact, writePendingKind, writePendingLane } from "../chunk-7D5MLTG3.js"; import "../chunk-Y3O45QYV.js"; import "../chunk-P4RPU2KB.js"; import "../chunk-LYNDVGNE.js"; import { readFeatureStageUnit } from "../chunk-5M55PDWL.js"; import "../chunk-TINPSX3N.js"; import "../chunk-BTDDEK7C.js"; import { isBundleClosed, laneRank, liveStageAgents, readChangeConstants } from "../chunk-7TBVBCDH.js"; import "../chunk-RJMJYPHL.js"; import { STAGE_ORDER } from "../chunk-NCYDV2N5.js"; import "../chunk-D6VEUSB6.js"; import "../chunk-BOEVU2BD.js"; import "../chunk-NJE7NS7E.js"; import "../chunk-ACJQW7SB.js"; import "../chunk-YA6SCYMC.js"; import "../chunk-Z3NGWHFX.js"; import "../chunk-YWAORJG6.js"; import "../chunk-Q353EDKY.js"; import { resolveSessionId } from "../chunk-SG6LXLDH.js"; import { readSessionControl } from "../chunk-FMQ5YRSP.js"; import "../chunk-AZLA53A6.js"; import "../chunk-IHMBVS6H.js"; import { init_esm_shims } from "../chunk-IDFRWBMN.js"; // src/pipeline/prompt-lane.ts init_esm_shims(); var LANE_REASON = { fast: "small, low-risk change \u2014 quick path, no full spec needed", graduated: "a moderate change \u2014 I set up spec-before-build for it", full: "risky or wide-reaching \u2014 full path (spec \u2192 build \u2192 verify)" }; var ROUTE_REASON = { "feature-development": "a code change \u2014 the full build path", "project-question": "a question about the project \u2014 no code change", "documentation-update": "creating or refreshing the project documentation", "module-documentation": "documenting a module", pentest: "a security test of the app", "design-test": "auditing the UI against the design system", "codebase-health": "a health check-up of the codebase \u2014 dead code, risky deps, secrets, stale docs", "site-map": "mapping the app's surfaces, navigation, and journeys against the code", "rules-analyze": "analysing which rules can become scripts", "root-cause-analysis": "a post-incident root-cause analysis", discovery: "shaping an uncertain idea into a practical next step \u2014 no code yet", "rules-discovery": "bringing the rules and stack docs up to date with the real stack", "no-workflow": "just chatting \u2014 nothing to set up" }; var SYSTEM_NOTIFICATION_ELEMENTS = [ "task-notification", "ci-monitor-event", "background-task-notification" ]; var SYSTEM_NOTIFICATION_BANNER = "[system notification - not user input]"; var STOP_HOOK_FEEDBACK_PREFIX = "stop hook feedback:"; var SUBAGENT_HANDBACK_MARKERS = { element: "agent-message", label: "[subagent hand-back]" }; var BARE_LINK_SOURCE = "https?://\\S+|#\\d+|\\b[A-Z][A-Z0-9]+-\\d+\\b"; var BARE_LINK_RE = new RegExp(BARE_LINK_SOURCE); var BARE_LINK_STRIP_RE = new RegExp(BARE_LINK_SOURCE, "g"); var BARE_TICKET_FILLER = /* @__PURE__ */ new Set([ "implement", "fix", "fixes", "change", "update", "refactor", "rework", "rename", "build", "make", "create", "add", "wire", "do", "get", "getting", "working", "work", "this", "that", "it", "the", "a", "an", "and", "or", "please", "thanks", "thank", "ticket", "issue", "issues", "task", "item", "pr", "on", "for", "to", "in", "of", "see", "link", "resolve", "close", "closes", "handle", "complete", "small", "commits", "commit", "watch", "green", "ci", "review", "merge", "branch", "per", "with", "from", "plus", "let", "lets", "can", "you", "your", "my", "our", "us", "we", "up", "out", "now", "just", "also", "then", "next", "step", "steps", "follow", "following", "above", "below", "here", "there", "go", "ahead", "start", "finish", "done", "ok", "okay" ]); function isBareTicketPrompt(prompt) { if (!BARE_LINK_RE.test(prompt)) return false; const stripped = prompt.replace(BARE_LINK_STRIP_RE, " "); const words = stripped.toLowerCase().match(/[a-z0-9]+/g) ?? []; return words.every((word) => BARE_TICKET_FILLER.has(word)); } function isSystemNotificationPrompt(prompt) { const head = prompt.trimStart().toLowerCase(); if (head.startsWith(SYSTEM_NOTIFICATION_BANNER) || head.startsWith(STOP_HOOK_FEEDBACK_PREFIX)) { return true; } if (startsWithNotificationElement(head)) { return true; } const lines = head.split(/\r?\n/).map((line) => line.trim()); if (lines.includes(SYSTEM_NOTIFICATION_BANNER) && lines.some((line) => startsWithNotificationElement(line))) { return true; } return isSubagentHandBack(lines); } function isSubagentHandBack(lines) { const element = SUBAGENT_HANDBACK_MARKERS.element; const opensElement = lines.some( (line) => line.startsWith(`<${element} `) || line.startsWith(`<${element}>`) ); return opensElement && lines.some((line) => line.startsWith(SUBAGENT_HANDBACK_MARKERS.label)); } function startsWithNotificationElement(text) { return SYSTEM_NOTIFICATION_ELEMENTS.some((element) => text.startsWith(`<${element}>`)); } var SLASH_COMMAND_ROUTES = { "paqad-discover": "discovery", "paqad-ask": "project-question" }; var SLASH_COMMAND_RE = /^\/([A-Za-z0-9:_-]+)(\s|$)/; function slashCommandName(prompt) { const match = SLASH_COMMAND_RE.exec(prompt.trimStart()); if (!match) { return null; } const full = match[1]; const lastColon = full.lastIndexOf(":"); return lastColon === -1 ? full : full.slice(lastColon + 1); } async function resolvePromptRoute(projectRoot, request, deps = {}) { const classify = deps.classify ?? ((input) => new RequestClassifier({ projectRoot }).classify(input)); const route = deps.route ?? ((classification2) => new PipelineRouter().route(classification2)); const classification = await classify({ request }); const routed = resolveRoutedWorkflow(classification.workflow); if (isBareTicketPrompt(request)) { return { routed: "feature-development", ...isFeatureDevelopmentRoute(routed) ? { kind: changeKindForWorkflow(classification.workflow) } : {}, lane: null, reason: "reading the ticket before I set the depth \u2014 no lane guessed from a bare link" }; } if (!isFeatureDevelopmentRoute(routed)) { return { routed, lane: null, reason: ROUTE_REASON[routed] }; } const kind = changeKindForWorkflow(classification.workflow); const { lane } = route(classification); return { routed, kind, lane, impact: { database_impact: classification.database_impact, api_impact: classification.api_impact }, reason: lane === null ? ROUTE_REASON["feature-development"] : LANE_REASON[lane] }; } function narrateRoute(routed, lane, reason, resumed) { if (routed === "no-workflow") { return null; } const verb = resumed ? "resumed" : "routed to"; if (routed === "feature-development" && lane !== null) { return paqadFrameLead(`${verb} feature-development, ${lane} lane (${reason})`); } return paqadFrameLead(`${verb} ${routed} (${reason})`); } var OPEN_CHANGE_STAGE_REASON = "a step of the change already open in this session"; var STAGE_NAME_OVERRIDABLE_ROUTES = /* @__PURE__ */ new Set([ "documentation-update", "project-question", "no-workflow" ]); var STAGE_NAME_RE = new RegExp( `\\b(?:${STAGE_ORDER.map((stage) => stage.replace(/_/g, "[_ ]")).join("|")})\\b`, "i" ); function namesStageOfOpenChange(projectRoot, sessionId, prior, request) { if (!STAGE_NAME_RE.test(request)) { return false; } if (prior.active !== null && isFeatureDevelopmentRoute(prior.active.workflow)) { return true; } const open = readSessionControl(projectRoot, sessionId).active; return open !== null && !isBundleClosed(projectRoot, open); } var RUNNING_CHANGE_REASON = "the change open in this session is still running"; var RECORDED_LANE_REASON = "the lane recorded on this change"; function openChangeOf(projectRoot, sessionId) { const open = readSessionControl(projectRoot, sessionId).active; return open !== null && !isBundleClosed(projectRoot, open) ? open : null; } function changeIsRunning(projectRoot, dirName) { if (dirName === null) return false; const rows = readFeatureStageUnit(projectRoot, dirName); return hasOpenUnendedStage(rows) || liveStageAgents(rows).length > 0; } async function runPromptRouteSeam(input, deps = {}) { if (isSystemNotificationPrompt(input.request)) { return { routed: "no-workflow", lane: null, resumed: null, narration: null, notification: true }; } const slashName = slashCommandName(input.request); let routed; let lane; let reason; let kind; let impact; if (slashName !== null) { const slashRoute = SLASH_COMMAND_ROUTES[slashName]; if (slashRoute === void 0) { return { routed: "no-workflow", lane: null, resumed: null, narration: null, notification: true }; } routed = slashRoute; lane = null; reason = ROUTE_REASON[slashRoute]; } else { ({ routed, lane, reason, kind, impact } = await resolvePromptRoute( input.projectRoot, input.request, deps )); } const sessionId = resolveSessionId(input.projectRoot, input.sessionId); const prior = readWorkflowState(input.projectRoot, sessionId); if (slashName === null && STAGE_NAME_OVERRIDABLE_ROUTES.has(routed) && namesStageOfOpenChange(input.projectRoot, sessionId, prior, input.request)) { routed = "feature-development"; lane = null; kind = void 0; reason = OPEN_CHANGE_STAGE_REASON; } const openChange = openChangeOf(input.projectRoot, sessionId); if (slashName === null && STAGE_NAME_OVERRIDABLE_ROUTES.has(routed) && changeIsRunning(input.projectRoot, openChange)) { routed = "feature-development"; lane = null; kind = void 0; reason = RUNNING_CHANGE_REASON; } const recordedLane = isFeatureDevelopmentRoute(routed) && openChange !== null ? readChangeConstants(input.projectRoot, openChange).lane : null; if (recordedLane !== null && laneRank(recordedLane) >= laneRank(lane)) { lane = recordedLane; reason = RECORDED_LANE_REASON; } const anchors = lane === null ? { reason } : { lane, reason }; const transition = routeWorkflow(prior, routed, anchors); const turnStartedAt = (deps.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(); writeWorkflowState(input.projectRoot, sessionId, { ...transition.state, turn_started_at: turnStartedAt }); try { appendRouteTurn(input.projectRoot, sessionId, { at: turnStartedAt, workflow: routed }); } catch (error) { appendHooksLog( input.projectRoot, `route-turns: append failed: ${error instanceof Error ? error.message : String(error)}` ); } if (isFeatureDevelopmentRoute(routed) && lane !== null && recordedLane === null) { writePendingLane(input.projectRoot, sessionId, lane); if (impact !== void 0) writePendingImpact(input.projectRoot, sessionId, impact); } if (isFeatureDevelopmentRoute(routed) && kind !== void 0) { writePendingKind(input.projectRoot, sessionId, kind); } writeSessionRoute( input.projectRoot, { workflow: routed, query: input.request, adapter: input.adapter }, sessionId ); return { routed, lane, resumed: transition.resumed, narration: narrateRoute(routed, lane, reason, transition.resumed) }; } export { BARE_TICKET_FILLER, SLASH_COMMAND_ROUTES, STOP_HOOK_FEEDBACK_PREFIX, SUBAGENT_HANDBACK_MARKERS, SYSTEM_NOTIFICATION_BANNER, SYSTEM_NOTIFICATION_ELEMENTS, isBareTicketPrompt, isSystemNotificationPrompt, resolvePromptRoute, runPromptRouteSeam, slashCommandName };