paqad-ai
Version:
Spec-driven development framework — AI agents that think before they type
684 lines (673 loc) • 20.2 kB
JavaScript
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
};