UNPKG

paqad-ai

Version:

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

684 lines (673 loc) • 20.2 kB
import { createRequire as __paqadCreateRequire } from 'module'; const require = __paqadCreateRequire(import.meta.url); import { readSessionRoute } from "./chunk-S3UQUJV2.js"; import { isFeatureDevelopmentRoute, readWorkflowState } from "./chunk-VOJO7HJ6.js"; import { recordMarkedStage } from "./chunk-7D5MLTG3.js"; import { BUNDLE_LOCK_NAME, currentFeature, readFeatureStageUnit } from "./chunk-5M55PDWL.js"; import { isLaneStage, sessionClosedAnyFeature } from "./chunk-7TBVBCDH.js"; import { keepingLedgerSessionCache, resolveSessionId } from "./chunk-SG6LXLDH.js"; import { FEATURE_BUNDLE_FILES, LEGACY_BUNDLE_FILES, SCREENSHOTS_DIR, featureDir, featureFilePath, isAbsolute, isFeatureDirName, relative, resolve } from "./chunk-AZLA53A6.js"; import { PATHS } from "./chunk-IHMBVS6H.js"; import { init_esm_shims } from "./chunk-IDFRWBMN.js"; // src/stage-evidence/marker-parse.ts init_esm_shims(); // src/pipeline/route-gate.ts init_esm_shims(); function routeIsAffirmativelyNonFeature(projectRoot, sessionId) { const resolved = resolveSessionId(projectRoot, sessionId); const active = readWorkflowState(projectRoot, resolved).active?.workflow; if (active) { return !isFeatureDevelopmentRoute(active); } const routed = readSessionRoute(projectRoot, resolved)?.workflow; if (routed) { return !isFeatureDevelopmentRoute(routed); } return false; } // src/stage-evidence/artifact-path.ts init_esm_shims(); import { realpathSync } from "fs"; var ArtifactOutOfTreeError = class extends Error { constructor(input) { super(`artifact must be a path inside the project; got ${input}`); this.input = input; this.name = "ArtifactOutOfTreeError"; } input; }; function realOrLexical(p) { try { return realpathSync.native(p); } catch { return p; } } function isEscape(rel) { return rel === "" || rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") || isAbsolute(rel); } function normalizeArtifactPath(projectRoot, input) { const rootResolved = resolve(projectRoot); const rootReal = realOrLexical(rootResolved); const absInput = isAbsolute(input) ? realOrLexical(input) : resolve(rootReal, input); for (const root of /* @__PURE__ */ new Set([rootReal, rootResolved])) { const rel = relative(root, absInput); if (!isEscape(rel)) { return rel.replace(/\\/g, "/"); } } throw new ArtifactOutOfTreeError(input); } // src/stage-evidence/bundle-artifact.ts init_esm_shims(); // src/feature-evidence/bundle-integrity.ts init_esm_shims(); import { readFileSync, readdirSync, statSync } from "fs"; import { join } from "path"; // src/feature-evidence/manifest.ts init_esm_shims(); function laneRunsSpecPipeline(lane) { return lane !== "fast"; } function laneHasStage(stage) { return (config) => isLaneStage(config.lane, stage); } function specPipelineApplies(config) { return config.specPipelineEnabled && laneRunsSpecPipeline(config.lane) && !config.specFrozenBeforePipeline; } function specPipelineStrictApplies(config) { return config.specPipelineStrict && laneRunsSpecPipeline(config.lane) && !config.specFrozenBeforePipeline; } var DYNAMIC_BUNDLE_FILES = [ { pattern: /^skill-[a-z0-9-]+\.json$/, writer: "paqad-ai skill release" }, { pattern: /^agent-[A-Za-z0-9-]+\.json$/, writer: "paqad-ai agent release" } ]; function bundleWriterFor(filename) { return BUNDLE_MANIFEST.find((entry) => entry.file === filename)?.writer ?? DYNAMIC_BUNDLE_FILES.find((entry) => entry.pattern.test(filename))?.writer ?? null; } var BUNDLE_MANIFEST = [ { key: "feature", file: FEATURE_BUNDLE_FILES.feature, required: "always", writer: "feature mint (paqad-ai stage start / plan compile)", validate: "json" }, { key: "plan", file: FEATURE_BUNDLE_FILES.plan, required: "always", writer: "paqad-ai plan compile", validate: "json" }, { key: "specification", file: FEATURE_BUNDLE_FILES.specification, required: laneHasStage("specification"), writer: "paqad-ai spec freeze", validate: "json" }, { key: "specMd", file: FEATURE_BUNDLE_FILES.specMd, required: laneHasStage("specification"), writer: "paqad-ai spec freeze", validate: "nonempty" }, { key: "request", file: FEATURE_BUNDLE_FILES.request, required: specPipelineApplies, writer: "paqad-ai spec pipeline start", validate: "nonempty" }, { key: "clarification", file: FEATURE_BUNDLE_FILES.clarification, required: specPipelineApplies, writer: "paqad-ai spec pipeline (label + questions)", validate: "json" }, { key: "experts", file: FEATURE_BUNDLE_FILES.experts, required: (config) => specPipelineApplies(config) && config.expertsEnabled, writer: "paqad-ai spec pipeline experts", validate: "json" }, { key: "highTable", file: FEATURE_BUNDLE_FILES.highTable, required: "optional", writer: "paqad-ai spec pipeline experts (record, brief, context, notes, synthesis) + SubagentStop hook", validate: "jsonl>=1" }, { key: "cost", file: FEATURE_BUNDLE_FILES.cost, required: "optional", writer: "Stop and SubagentStop hooks (record-only)", validate: "json" }, { key: "decisions", file: FEATURE_BUNDLE_FILES.decisions, required: "optional", writer: "paqad-ai decision resolve (decisions index)", validate: "json" }, { key: "techDebt", file: FEATURE_BUNDLE_FILES.techDebt, required: "optional", writer: "paqad-ai debt add (tech-debt index)", validate: "json" }, { key: "ruleOverrides", file: FEATURE_BUNDLE_FILES.ruleOverrides, required: "optional", writer: "paqad-ai rules override", validate: "jsonl>=1" }, { key: "fixProof", file: FEATURE_BUNDLE_FILES.fixProof, required: "optional", writer: "paqad-ai fix prove", validate: "json" }, { key: "hld", file: FEATURE_BUNDLE_FILES.hld, required: "optional", writer: "paqad-ai hld posted, not-posted, check-approval or skip", validate: "json" }, { key: "hldMd", file: FEATURE_BUNDLE_FILES.hldMd, required: "optional", writer: "paqad-ai hld not-posted or hld posted --file", validate: "nonempty" }, { key: "review", file: FEATURE_BUNDLE_FILES.review, required: laneHasStage("review"), writer: "paqad-ai review record", validate: "json" }, { key: "stageEvidence", file: FEATURE_BUNDLE_FILES.stageEvidence, required: "always", writer: "stage recorder", validate: "jsonl>=1" }, { key: "ruleRun", file: FEATURE_BUNDLE_FILES.ruleRun, required: (config) => config.ruleComplianceOn, writer: "rule-scripts runner", validate: "jsonl>=1" }, { key: "rulesLoaded", file: FEATURE_BUNDLE_FILES.rulesLoaded, required: "optional", writer: "paqad-ai rules load", validate: "json" }, { key: "delivery", file: FEATURE_BUNDLE_FILES.delivery, required: "always", writer: "feature open + paqad-ai delivery-link + paqad-ai deliver + the Stop delivery check", validate: "json" }, { key: "checks", file: FEATURE_BUNDLE_FILES.checks, required: "optional", writer: "paqad-ai checks run", validate: "json" }, { key: "changeMetrics", file: FEATURE_BUNDLE_FILES.changeMetrics, required: (config) => config.metricsEnabled, writer: "change-metrics collector", validate: "jsonl>=1" }, { key: "duplication", file: FEATURE_BUNDLE_FILES.duplication, required: (config) => config.duplicationOn, writer: "duplication scan", validate: "jsonl>=1" }, { key: "report", file: "report.html", required: (config) => config.featureReport, writer: "feature report renderer (writeFeatureReport)", validate: "nonempty" }, { key: "rag", file: FEATURE_BUNDLE_FILES.rag, required: (config) => config.ragEnabled, writer: "RAG recorder", validate: "jsonl>=1", unrecoverable: true }, { key: "receipt", file: FEATURE_BUNDLE_FILES.receipt, required: (config) => config.enterprise && config.evidenceLedger, writer: "projectFeatureReceipt", validate: "json" }, { key: "evidence", file: FEATURE_BUNDLE_FILES.evidence, required: "always", writer: "appendFeatureEvidenceRows", validate: "jsonl>=1" }, { key: "aiBom", file: FEATURE_BUNDLE_FILES.aiBom, required: (config) => config.enterprise && config.aiBom, writer: "projectFeatureReceipt (AI-BOM)", validate: "json" }, { key: "visualEvidence", file: FEATURE_BUNDLE_FILES.visualEvidence, required: "optional", writer: "paqad-ai visual-evidence run", validate: "json" } ]; function readSpecAdoption(spec) { if (typeof spec !== "object" || spec === null) return null; const record = spec; const section = record.pipeline ?? record.provenance; if (typeof section !== "object" || section === null) return null; const produced = record.pipeline ? record.pipeline.produced : section.pipeline_produced; return { produced: produced === true, ...typeof section.manual_reason === "string" ? { manual_reason: section.manual_reason } : {}, ...record.pipeline?.skipped === "fast-lane" ? { fast_lane: true } : {} }; } function isSpecFrozenBeforePipeline(spec) { return typeof spec === "object" && spec !== null && readSpecAdoption(spec) === null; } function isSpecFrozenOnFastLane(spec) { return readSpecAdoption(spec)?.fast_lane === true; } var FAST_LANE_REFREEZE_HINT = "the lane was raised after a fast-lane freeze: re-freeze the spec through the spec pipeline (paqad-ai spec pipeline start, then spec freeze --from-pipeline)"; function validateSpecificationAdoption(content) { const failure = { ok: false, error: "specification.json was not produced by the spec pipeline and records no manual reason (spec_pipeline_adoption=strict); re-freeze with --from-pipeline or --manual --reason" }; if (content === null) return failure; let adoption; try { adoption = readSpecAdoption(JSON.parse(content)); } catch { return failure; } if (!adoption) return failure; if (adoption.produced) return { ok: true }; if (adoption.fast_lane) { return { ok: false, error: `specification.json was frozen on the fast lane; ${FAST_LANE_REFREEZE_HINT}` }; } if (adoption.manual_reason !== void 0 && adoption.manual_reason.trim().length > 0) { return { ok: true }; } return failure; } function isBundleFileRequired(entry, config) { if (entry.required === "always") return true; if (entry.required === "optional") return false; return entry.required(config); } function validateBundleFileContent(validate, content) { if (content === null) { return false; } switch (validate) { case "json": try { JSON.parse(content); return true; } catch { return false; } case "jsonl>=1": return content.split("\n").some((line) => line.trim().length > 0); case "nonempty": return content.trim().length > 0; } } // src/feature-evidence/bundle-integrity.ts var ALLOWED_BUNDLE_FILENAMES = /* @__PURE__ */ new Set([ ...Object.values(FEATURE_BUNDLE_FILES), "report.html" ]); function isAllowedBundleFilename(filename) { return ALLOWED_BUNDLE_FILENAMES.has(filename) || DYNAMIC_BUNDLE_FILES.some((entry) => entry.pattern.test(filename)); } var LEGACY_BUNDLE_FILENAMES = new Set( Object.values(LEGACY_BUNDLE_FILES) ); var SCREENSHOT_ENTRY_RE = /^screenshots\/(overview\.gif|\d{2}-[a-z0-9-]{1,40}\/(image\.png|caption\.txt))$/; function isAtomicWriteTemp(filename) { return /\.tmp(-\d+)?$/.test(filename); } function classifyBundlePath(relPath) { const normalized = relPath.replace(/\\/g, "/").replace(/^\.\//, ""); const prefix = `${PATHS.FEATURE_EVIDENCE_DIR}/`; if (!normalized.startsWith(prefix)) { return null; } const rest = normalized.slice(prefix.length); const slash = rest.indexOf("/"); if (slash === -1) { return null; } const dirName = rest.slice(0, slash); const filename = rest.slice(slash + 1); if (!isFeatureDirName(dirName)) { return null; } if (filename === SCREENSHOTS_DIR || filename.startsWith(`${SCREENSHOTS_DIR}/`)) { return { dirName, filename, allowed: SCREENSHOT_ENTRY_RE.test(filename), screenshotSubtree: true }; } return { dirName, filename, allowed: isAllowedBundleFilename(filename), screenshotSubtree: false }; } function strayBundleFiles(projectRoot, dirName) { const bundleAbs = join(projectRoot, featureDir(dirName)); let entries; try { entries = readdirSync(bundleAbs); } catch { return []; } const legacy = isLegacyBundle(bundleAbs); const strays = []; for (const name of entries) { if (isAtomicWriteTemp(name) || name === BUNDLE_LOCK_NAME) { continue; } if (name === SCREENSHOTS_DIR) { strays.push(...strayScreenshotEntries(bundleAbs)); continue; } if (isAllowedBundleFilename(name)) continue; if (legacy && LEGACY_BUNDLE_FILENAMES.has(name)) continue; strays.push(name); } return strays.sort(); } function readJsonObject(absPath, firstLineOnly) { try { const text = readFileSync(absPath, "utf8"); const json = firstLineOnly ? text.split("\n").find((line) => line.trim().length > 0) : text; const parsed = json === void 0 ? null : JSON.parse(json); return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null; } catch { return null; } } function isLegacyBundle(bundleAbs) { const openRow = readJsonObject(join(bundleAbs, FEATURE_BUNDLE_FILES.stageEvidence), true); if (openRow && typeof openRow.ts === "string" && openRow.recorded_at === void 0) { return true; } const spec = readJsonObject(join(bundleAbs, FEATURE_BUNDLE_FILES.specification), false); return spec !== null && typeof spec.spec_file === "string" && spec.spec_file !== FEATURE_BUNDLE_FILES.specMd; } function strayScreenshotEntries(bundleAbs) { const strays = []; const walk = (absDir, relPrefix) => { let entries; try { entries = readdirSync(absDir); } catch { return; } for (const name of entries) { if (isAtomicWriteTemp(name)) { continue; } const abs = join(absDir, name); const rel = `${relPrefix}/${name}`; let isDir; try { isDir = statSync(abs).isDirectory(); } catch { continue; } if (isDir) { walk(abs, rel); } else if (!SCREENSHOT_ENTRY_RE.test(rel)) { strays.push(rel); } } }; walk(join(bundleAbs, SCREENSHOTS_DIR), SCREENSHOTS_DIR); return strays; } // src/stage-evidence/bundle-artifact.ts function bundleArtifactFile(stage) { if (stage === "planning") return "plan"; if (stage === "specification") return "specification"; if (stage === "review") return "review"; return null; } function bundleArtifactVerb(file) { if (file === "plan") return "paqad-ai plan compile"; if (file === "specification") return "paqad-ai spec freeze"; return "paqad-ai review record"; } function checkBundleArtifacts(projectRoot, sessionId, stage, normalizedPaths) { const file = bundleArtifactFile(stage); if (!file) { const accepted2 = []; const rejected2 = []; for (const path of normalizedPaths) { const inBundle = classifyBundlePath(path); if (inBundle && (!inBundle.allowed || inBundle.screenshotSubtree)) { rejected2.push(path); } else { accepted2.push(path); } } return { rigid: false, expected: null, accepted: accepted2, rejected: rejected2, verb: null }; } const dirName = currentFeature(projectRoot, sessionId); const expected = dirName ? featureFilePath(dirName, file) : null; const accepted = []; const rejected = []; for (const path of normalizedPaths) { if (expected !== null && path === expected) { accepted.push(path); } else { rejected.push(path); } } return { rigid: true, expected, accepted, rejected, verb: bundleArtifactVerb(file) }; } // src/stage-evidence/marker-parse.ts var MARKER = /^[ \t>*-]*paqad:stage\s+([a-z_]+)\s+(start|end)(?:\s+--\s+(\S+))?\s*$/gim; function extractAssistantText(raw) { const parts = []; let sawJson = false; for (const line of raw.split("\n")) { const trimmed = line.trim(); if (!trimmed.startsWith("{")) continue; let obj; try { obj = JSON.parse(trimmed); } catch { continue; } sawJson = true; const record = obj; const container = record.message ?? record.payload; const role = container?.role ?? record.role ?? record.type; if (role !== "assistant") continue; const content = container?.content ?? record.content; parts.push(collectText(content)); } return sawJson ? parts.join("\n") : raw; } function collectText(content) { if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; return content.map( (block) => block && typeof block === "object" && typeof block.text === "string" ? block.text : "" ).join("\n"); } function extractMarkers(text) { const out = []; MARKER.lastIndex = 0; let match; while ((match = MARKER.exec(text)) !== null) { const marker = { stage: match[1], phase: match[2] }; if (marker.phase === "end" && match[3]) marker.artifactPath = match[3]; out.push(marker); } return out; } function parseAndRecordMarkers(input) { if (input.keepSessionCache === true) { return keepingLedgerSessionCache(input.projectRoot, () => recordMarkers(input)); } return recordMarkers(input); } function recordMarkers(input) { try { const raw = input.transcriptText; if (!raw) return []; const markers = extractMarkers(extractAssistantText(raw)); if (markers.length === 0) return []; const sessionId = resolveSessionId(input.projectRoot, input.sessionId); if (routeIsAffirmativelyNonFeature(input.projectRoot, sessionId)) return []; const dirName = currentFeature(input.projectRoot, sessionId); if (!dirName && sessionClosedAnyFeature(input.projectRoot, sessionId)) return []; const existing = dirName ? readFeatureStageUnit(input.projectRoot, dirName) : []; const seen = /* @__PURE__ */ new Set(); for (const row of existing) { if (typeof row.stage !== "string") continue; if (row.kind === "stage_start") seen.add(`${row.stage}:start`); if (row.kind === "stage_end") seen.add(`${row.stage}:end`); } const recorded = []; for (const { stage, phase, artifactPath } of markers) { const key = `${stage}:${phase}`; if (seen.has(key)) continue; let normalizedArtifact; if (artifactPath) { try { normalizedArtifact = normalizeArtifactPath(input.projectRoot, artifactPath); } catch { normalizedArtifact = void 0; } if (normalizedArtifact) { const check = checkBundleArtifacts(input.projectRoot, sessionId, stage, [ normalizedArtifact ]); if (check.accepted.length === 0) { normalizedArtifact = void 0; } } } if (recordMarkedStage(input.projectRoot, { sessionId, stage, phase, artifactPaths: normalizedArtifact ? [normalizedArtifact] : void 0, adapter: input.adapter, now: input.now })) { seen.add(key); recorded.push({ stage, phase, artifactPath }); } } return recorded; } catch { return []; } } export { laneRunsSpecPipeline, specPipelineStrictApplies, bundleWriterFor, BUNDLE_MANIFEST, readSpecAdoption, isSpecFrozenBeforePipeline, isSpecFrozenOnFastLane, FAST_LANE_REFREEZE_HINT, validateSpecificationAdoption, isBundleFileRequired, validateBundleFileContent, classifyBundlePath, strayBundleFiles, normalizeArtifactPath, routeIsAffirmativelyNonFeature, bundleArtifactFile, checkBundleArtifacts, extractAssistantText, extractMarkers, parseAndRecordMarkers };