paqad-ai
Version:
Spec-driven development framework — AI agents that think before they type
308 lines (303 loc) • 10.5 kB
JavaScript
import { createRequire as __paqadCreateRequire } from 'module'; const require = __paqadCreateRequire(import.meta.url);
import {
readContractDecisions
} from "./chunk-4ZAWKFXT.js";
import {
stageIsOpen,
stagesWithKind
} from "./chunk-7D5MLTG3.js";
import {
appendFeatureStageRow,
currentFeature,
foldFeature,
readFeatureStageUnit
} from "./chunk-5M55PDWL.js";
import {
COMPLETION_DEFERRED_KIND,
RECORD_REPAIR_KIND,
STAGE_AGENT_SESSION_RELEASE,
heldInFlightOnBranch,
isBundleClosed,
liveStageAgents
} from "./chunk-7TBVBCDH.js";
import {
rowRecordedAt
} from "./chunk-RJMJYPHL.js";
import {
stripPluginAgentNamespace
} from "./chunk-NCYDV2N5.js";
import {
resolveSessionId
} from "./chunk-SG6LXLDH.js";
import {
featureChangeKey
} from "./chunk-AZLA53A6.js";
import {
PATHS
} from "./chunk-IHMBVS6H.js";
import {
init_esm_shims
} from "./chunk-IDFRWBMN.js";
// src/stage-evidence/completion-deferred.ts
init_esm_shims();
// src/checks/running-marker.ts
init_esm_shims();
import { execFileSync } from "child_process";
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
import { dirname, join } from "path";
var CHECKS_RUNNING_CEILING_MS = 30 * 60 * 1e3;
var CHECKS_RUN_COMMAND = /\bchecks\s+run\b/;
function safeSessionId(sessionId) {
return sessionId.replace(/[^A-Za-z0-9._-]/g, "_").replace(/^\.+/, "_") || "unknown";
}
function checksRunningMarkerPath(projectRoot, sessionId) {
return join(projectRoot, PATHS.CHECKS_DIR, "running", `${safeSessionId(sessionId)}.json`);
}
function writeChecksRunningMarker(projectRoot, marker) {
const path = checksRunningMarkerPath(projectRoot, marker.session_id);
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, `${JSON.stringify(marker, null, 2)}
`, "utf8");
return path;
}
function readChecksRunningMarker(projectRoot, sessionId) {
try {
const parsed = JSON.parse(
readFileSync(checksRunningMarkerPath(projectRoot, sessionId), "utf8")
);
return isMarker(parsed) ? parsed : null;
} catch {
return null;
}
}
function clearChecksRunningMarker(projectRoot, sessionId, pid) {
if (readChecksRunningMarker(projectRoot, sessionId)?.pid !== pid) {
return;
}
rmSync(checksRunningMarkerPath(projectRoot, sessionId), { force: true });
}
function liveChecksRunningMarker(projectRoot, sessionId, probe = {}) {
const marker = readChecksRunningMarker(projectRoot, sessionId);
if (!marker || marker.session_id !== sessionId) {
return null;
}
const age = (probe.now ?? (() => /* @__PURE__ */ new Date()))().getTime() - Date.parse(marker.started_at);
if (Number.isNaN(age) || age < 0 || age >= CHECKS_RUNNING_CEILING_MS) {
return null;
}
if (!(probe.isAlive ?? processIsAlive)(marker.pid)) {
return null;
}
const command = (probe.commandOf ?? processCommand)(marker.pid);
return command !== null && CHECKS_RUN_COMMAND.test(command) ? marker : null;
}
function processIsAlive(pid) {
try {
process.kill(pid, 0);
return true;
} catch (error) {
return error.code === "EPERM";
}
}
function processCommand(pid) {
if (process.platform === "win32") {
return null;
}
try {
return execFileSync("ps", ["-o", "command=", "-p", String(pid)], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"]
}).trim();
} catch {
return null;
}
}
function isMarker(value) {
if (typeof value !== "object" || value === null) {
return false;
}
const m = value;
return Number.isInteger(m.pid) && m.pid > 0 && typeof m.session_id === "string" && (m.change === null || typeof m.change === "string") && typeof m.started_at === "string";
}
// src/stage-evidence/completion-deferred.ts
var AGENT_TYPE_FIELDS = ["agent_type", "agentType", "subagent_type", "subagentType"];
var TASK_KINDS = /* @__PURE__ */ new Set([
"subagent",
"agent",
"task",
"local_agent",
"remote_agent",
"bash",
"local_bash",
"shell"
]);
var FINISHED_TASK_STATUSES = /* @__PURE__ */ new Set([
"completed",
"failed",
"killed",
"stopped",
"cancelled",
"canceled"
]);
function agentTypeOf(task) {
if (!task || typeof task !== "object") {
return void 0;
}
const record = task;
for (const field of AGENT_TYPE_FIELDS) {
const value = record[field];
if (typeof value === "string" && value.trim() !== "") {
return stripPluginAgentNamespace(value.trim());
}
}
const kind = record.type;
if (typeof kind === "string" && kind.trim() !== "" && !TASK_KINDS.has(kind.trim())) {
return stripPluginAgentNamespace(kind.trim());
}
return void 0;
}
function taskIsFinished(task) {
if (!task || typeof task !== "object") {
return false;
}
const status = task.status;
return typeof status === "string" && FINISHED_TASK_STATUSES.has(status.trim().toLowerCase());
}
function runningTasks(tasks) {
return tasks.filter((task) => !taskIsFinished(task));
}
function stageAgentTaskCount(tasks) {
return runningTasks(tasks).filter((task) => agentTypeOf(task)?.startsWith("paqad-")).length;
}
function tasksCarryAgentType(tasks) {
return runningTasks(tasks).some((task) => agentTypeOf(task) !== void 0);
}
var NEW_HOST_SESSION_SOURCES = /* @__PURE__ */ new Set(["startup", "resume"]);
function hostProcessAlive(pid) {
try {
process.kill(pid, 0);
return true;
} catch (error) {
return error.code === "EPERM";
}
}
function releaseStageAgentsAtSessionStart(projectRoot, sessionId, source, now, isAlive = hostProcessAlive) {
if (typeof source !== "string" || !NEW_HOST_SESSION_SOURCES.has(source)) return false;
const current = currentFeature(projectRoot, sessionId, now);
const dirNames = current !== null ? [current] : heldInFlightOnBranch(projectRoot, sessionId, now).map((held) => held.dirName);
let released = false;
for (const dirName of dirNames) {
const releases = liveStageAgents(readFeatureStageUnit(projectRoot, dirName)).filter(
(agent) => agent.sessionId === sessionId || agent.hostPid === null || !isAlive(agent.hostPid)
).map((agent) => agent.rowHash);
if (releases.length === 0) continue;
appendFeatureStageRow(
projectRoot,
sessionId,
dirName,
{ kind: RECORD_REPAIR_KIND, reason: STAGE_AGENT_SESSION_RELEASE, releases },
now
);
released = true;
}
return released;
}
function hasOpenUnendedStage(rows) {
for (const stage of stagesWithKind(rows, "stage_start")) {
if (stageIsOpen(rows, stage)) {
return true;
}
}
return false;
}
var CLEANLY_ENDED = /* @__PURE__ */ new Set(["complete", "redone"]);
var NOT_REACHED = /* @__PURE__ */ new Set(["missing", "not-applicable", "skipped"]);
function isBetweenStages(projectRoot, sessionId, dirName) {
if (isBundleClosed(projectRoot, dirName)) return false;
const fold = foldFeature(projectRoot, sessionId, dirName);
if (fold.lane === null || fold.completeness.missing_stages.length === 0) return false;
const reached = fold.stages.filter((stage) => !NOT_REACHED.has(stage.state));
return reached.length > 0 && reached.every((stage) => CLEANLY_ENDED.has(stage.state));
}
function pendingDecisionOf(projectRoot, dirName) {
const key = featureChangeKey(dirName);
return readContractDecisions(projectRoot).some(
({ packet, status }) => status !== "resolved" && packet.change === key
);
}
function deferCompletionForBackgroundStage(input) {
const tasks = runningTasks(Array.isArray(input.backgroundTasks) ? input.backgroundTasks : []);
const now = input.now ?? (() => /* @__PURE__ */ new Date());
const sessionId = resolveSessionId(input.projectRoot, input.sessionId ?? null);
const checksRunning = liveChecksRunningMarker(input.projectRoot, sessionId, { ...input.probe, now }) !== null;
const dirName = currentFeature(input.projectRoot, sessionId);
const rows = dirName !== null ? readFeatureStageUnit(input.projectRoot, dirName) : [];
const payloadNamesAgents = tasks.length > 0 && tasksCarryAgentType(tasks);
const ledgerAgents = payloadNamesAgents ? 0 : liveStageAgents(rows).length;
const betweenStages = dirName !== null && !hasOpenUnendedStage(rows) && isBetweenStages(input.projectRoot, sessionId, dirName);
if (tasks.length === 0 && !checksRunning && ledgerAgents === 0 && !betweenStages) {
return { deferred: false };
}
let reason;
let count;
const stageAgents = stageAgentTaskCount(tasks);
if (checksRunning) {
reason = "checks run still running";
count = tasks.length;
} else if (stageAgents > 0) {
reason = "stage agent running in background";
count = stageAgents;
} else if (ledgerAgents > 0) {
reason = "stage agent running in background";
count = ledgerAgents;
} else if (betweenStages) {
reason = "between stages";
count = tasks.length;
} else if (!tasksCarryAgentType(tasks) && dirName !== null && hasOpenUnendedStage(rows)) {
reason = "background task running while a stage is open";
count = tasks.length;
} else {
return { deferred: false };
}
const clockFree = reason === "between stages" || stageAgents === 0 && ledgerAgents > 0;
if (!clockFree && deferralCeilingReached(rows, now)) {
return { deferred: false };
}
if (dirName !== null && input.record !== false) {
appendFeatureStageRow(
input.projectRoot,
sessionId,
dirName,
{ kind: COMPLETION_DEFERRED_KIND, reason, tasks: count },
input.now
);
}
const next = reason === "between stages" && dirName !== null && !pendingDecisionOf(input.projectRoot, dirName) ? foldFeature(input.projectRoot, sessionId, dirName).completeness.missing_stages[0] : void 0;
return { deferred: true, reason, tasks: count, ...next ? { nextStage: next } : {} };
}
function deferralCeilingReached(rows, now) {
let firstDeferredAt = Number.POSITIVE_INFINITY;
for (const row of rows) {
if (row.kind === "verify") {
firstDeferredAt = Number.POSITIVE_INFINITY;
continue;
}
if (row.kind !== COMPLETION_DEFERRED_KIND) continue;
const at = Date.parse(rowRecordedAt(row));
if (Number.isNaN(at)) continue;
firstDeferredAt = Math.min(firstDeferredAt, at);
}
return now().getTime() - firstDeferredAt >= CHECKS_RUNNING_CEILING_MS;
}
export {
writeChecksRunningMarker,
clearChecksRunningMarker,
liveChecksRunningMarker,
stageAgentTaskCount,
tasksCarryAgentType,
hostProcessAlive,
releaseStageAgentsAtSessionStart,
hasOpenUnendedStage,
isBetweenStages,
deferCompletionForBackgroundStage
};