trellis
Version:
Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications
1,725 lines (1,611 loc) • 326 kB
JavaScript
import {
getSigningMaterial,
init_pairing,
pairingResolver
} from "./chunk-QC5OHKIJ.js";
import {
acquirePromoteLock,
init_promote_lock,
releasePromoteLock
} from "./chunk-DIHRG6LA.js";
import {
getDecision,
getDecisionChain,
init_decisions,
queryDecisions,
recordDecision
} from "./chunk-2DFWNMEW.js";
import {
EAVStore,
init_eav_store
} from "./chunk-G3XIHPSQ.js";
import {
JsonOpLog,
LaneOpLog,
addCriterion,
applyIssueStartCriteria,
assignIssue,
blockIssue,
buildFileStateAtOp,
checkCompletionReadiness,
closeIssue,
createBranch,
createCheckpoint,
createIssue,
createLaneMeta,
createMilestone,
createProjectAttestation,
decompose,
deleteBranch,
diffFileStates,
diffOpRange,
getActiveIssues,
getBranchHeadOpHash,
getIssue,
init_branch,
init_checkpoint,
init_decompose,
init_diff,
init_issue,
init_lane,
init_merge,
init_milestone,
init_op_log,
init_project,
init_signing_middleware,
init_transcript,
laneDir,
listBranches,
listChatMessages,
listCheckpoints,
listIssues,
listLaneMetas,
listMilestones,
loadBranchState,
loadLaneMeta,
normalizeLaneName,
pauseIssue,
recordChatMessage,
removeCriterion,
reopenIssue,
resolveLaneHeadFromJournal,
resumeIssue,
runCriteria,
saveBranchState,
saveLaneMeta,
setCriterionStatus,
shouldAdvanceBranchHead,
startIssue,
switchBranch,
threeWayMerge,
threeWayTextMerge,
triageIssue,
unblockIssue,
updateIssue,
updateLaneHead,
verifyOpBatch
} from "./chunk-Q4FKTPX4.js";
import {
allTestRunsPassed,
init_test_runner,
runPromoteRequiredTests,
runTestSuites,
tryLoadTestManifest
} from "./chunk-LNCBUJNO.js";
import {
ensureDefaultTestManifest,
init_test_manifest
} from "./chunk-PBH357QR.js";
import {
DEFAULT_CONFIG,
init_types,
issueEntityId
} from "./chunk-E2CFJKLU.js";
import {
BlobStore,
init_blob_store
} from "./chunk-MFZ22U6M.js";
import {
PROVENANCE,
init_canonical_op
} from "./chunk-RUMOVKR4.js";
import {
createVcsOp,
init_ops,
isVcsOpKind,
verifyVcsOpHash
} from "./chunk-GRWQPKYK.js";
import {
__esm
} from "./chunk-2ESYSVXG.js";
// src/watcher/fs-watcher.ts
import { watch } from "fs";
import { readdir, stat, readFile } from "fs/promises";
import { join, relative } from "path";
async function hashFile(filePath) {
const content = await readFile(filePath);
const hashBuffer = await crypto.subtle.digest("SHA-256", content);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
}
function shouldIgnore(relPath, patterns) {
for (const pattern of patterns) {
if (pattern.startsWith("*.")) {
const ext = pattern.slice(1);
if (relPath.endsWith(ext)) return true;
} else if (relPath.includes(pattern)) {
return true;
}
}
return false;
}
var FileWatcher;
var init_fs_watcher = __esm({
"src/watcher/fs-watcher.ts"() {
"use strict";
FileWatcher = class {
config;
watchers = [];
debounceTimers = /* @__PURE__ */ new Map();
knownFiles = /* @__PURE__ */ new Map();
// relPath → contentHash
running = false;
constructor(config) {
this.config = config;
}
/**
* Scans the directory tree and builds an initial map of all tracked files.
* Returns the list of FileChangeEvents for the initial state (all adds).
*/
async scan(opts) {
const events = [];
opts?.onProgress?.({
phase: "discovering",
current: 0,
total: 0,
message: "Discovering existing files\u2026"
});
const entries = await this.walkDir(this.config.rootPath);
opts?.onProgress?.({
phase: "hashing",
current: 0,
total: entries.length,
message: `Hashing ${entries.length} existing files\u2026`
});
for (let i = 0; i < entries.length; i++) {
const absPath = entries[i];
const relPath = relative(this.config.rootPath, absPath);
if (shouldIgnore(relPath, this.config.ignorePatterns)) continue;
try {
const hash = await hashFile(absPath);
const stats = await stat(absPath);
this.knownFiles.set(relPath, hash);
events.push({
type: "add",
path: relPath,
contentHash: hash,
size: stats.size,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
});
} catch {
}
if ((i + 1) % 25 === 0 || i === entries.length - 1) {
opts?.onProgress?.({
phase: "hashing",
current: i + 1,
total: entries.length,
message: `Hashed ${i + 1}/${entries.length} files`
});
}
}
opts?.onProgress?.({
phase: "done",
current: events.length,
total: events.length,
message: `Discovered ${events.length} trackable files`
});
return events;
}
/**
* Starts watching the directory tree for changes.
*/
start() {
if (this.running) return;
this.running = true;
try {
const watcher = watch(
this.config.rootPath,
{ recursive: true },
(eventType, filename) => {
if (!filename) return;
const relPath = filename.toString();
if (shouldIgnore(relPath, this.config.ignorePatterns)) return;
this.debouncedHandle(relPath);
}
);
this.watchers.push(watcher);
} catch {
console.warn("Recursive watch not supported; using scan-based polling.");
}
}
/**
* Stops all watchers.
*/
stop() {
this.running = false;
for (const w of this.watchers) {
w.close();
}
this.watchers = [];
for (const timer of this.debounceTimers.values()) {
clearTimeout(timer);
}
this.debounceTimers.clear();
}
/**
* Returns the current known file map (path → contentHash).
*/
getKnownFiles() {
return new Map(this.knownFiles);
}
debouncedHandle(relPath) {
const existing = this.debounceTimers.get(relPath);
if (existing) clearTimeout(existing);
const timer = setTimeout(async () => {
this.debounceTimers.delete(relPath);
await this.handleChange(relPath);
}, this.config.debounceMs);
this.debounceTimers.set(relPath, timer);
}
async handleChange(relPath) {
const absPath = join(this.config.rootPath, relPath);
const known = this.knownFiles.get(relPath);
try {
const stats = await stat(absPath);
if (!stats.isFile()) return;
const hash = await hashFile(absPath);
if (!known) {
this.knownFiles.set(relPath, hash);
await this.config.onEvent({
type: "add",
path: relPath,
contentHash: hash,
size: stats.size,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
});
} else if (known !== hash) {
const oldHash = known;
this.knownFiles.set(relPath, hash);
await this.config.onEvent({
type: "modify",
path: relPath,
contentHash: hash,
oldContentHash: oldHash,
size: stats.size,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
});
}
} catch {
if (known) {
this.knownFiles.delete(relPath);
await this.config.onEvent({
type: "delete",
path: relPath,
contentHash: known,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
});
}
}
}
async walkDir(dir) {
const results = [];
try {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
const relFromRoot = relative(this.config.rootPath, fullPath);
if (shouldIgnore(relFromRoot, this.config.ignorePatterns)) continue;
if (entry.isDirectory()) {
const sub = await this.walkDir(fullPath);
results.push(...sub);
} else if (entry.isFile()) {
results.push(fullPath);
}
}
} catch {
}
return results;
}
};
}
});
// src/watcher/ingestion.ts
import { extname } from "path";
function detectLanguage(filePath) {
const ext = extname(filePath).toLowerCase();
return EXT_LANGUAGE[ext];
}
var EXT_LANGUAGE, Ingestion;
var init_ingestion = __esm({
"src/watcher/ingestion.ts"() {
"use strict";
init_ops();
EXT_LANGUAGE = {
".ts": "typescript",
".tsx": "typescript",
".js": "javascript",
".jsx": "javascript",
".py": "python",
".rs": "rust",
".go": "go",
".rb": "ruby",
".java": "java",
".c": "c",
".cpp": "cpp",
".h": "c",
".hpp": "cpp",
".cs": "csharp",
".swift": "swift",
".kt": "kotlin",
".md": "markdown",
".json": "json",
".yaml": "yaml",
".yml": "yaml",
".toml": "toml",
".html": "html",
".css": "css",
".scss": "scss",
".vue": "vue",
".svelte": "svelte"
};
Ingestion = class {
agentId;
lastOpHash;
onOp;
constructor(opts) {
this.agentId = opts.agentId;
this.lastOpHash = opts.lastOpHash;
this.onOp = opts.onOp;
}
/**
* Processes a single FileChangeEvent, producing and emitting a VcsOp.
*/
async process(event) {
let kind;
switch (event.type) {
case "add":
kind = "vcs:fileAdd";
break;
case "modify":
kind = "vcs:fileModify";
break;
case "delete":
kind = "vcs:fileDelete";
break;
case "rename":
kind = "vcs:fileRename";
break;
}
const op = await createVcsOp(kind, {
agentId: this.agentId,
previousHash: this.lastOpHash,
vcs: {
filePath: event.path,
oldFilePath: event.oldPath,
contentHash: event.contentHash,
oldContentHash: event.oldContentHash,
size: event.size,
language: detectLanguage(event.path)
}
});
this.lastOpHash = op.hash;
await this.onOp(op);
return op;
}
/**
* Processes a batch of FileChangeEvents in order.
*/
async processBatch(events) {
const ops = [];
for (const event of events) {
ops.push(await this.process(event));
}
return ops;
}
getLastOpHash() {
return this.lastOpHash;
}
setLastOpHash(hash) {
this.lastOpHash = hash;
}
};
}
});
// src/scaffold/infer.ts
import { existsSync, readdirSync, statSync, readFileSync } from "fs";
import { join as join2 } from "path";
function detectEcosystem(rootPath) {
const indicators = [];
let ecosystem = null;
let name = null;
let description = null;
let domain = null;
const pkgPath = join2(rootPath, "package.json");
if (existsSync(pkgPath)) {
indicators.push("package.json");
try {
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
name = pkg.name ?? null;
description = pkg.description ?? null;
ecosystem = "node";
const deps = {
...pkg.dependencies,
...pkg.devDependencies,
...pkg.peerDependencies
};
const depNames = Object.keys(deps);
if (pkg.engines?.bun || depNames.includes("@types/bun")) {
ecosystem = "bun";
}
if (depNames.some((d) => d.includes("motion-canvas"))) {
domain = "animation-studio";
} else if (depNames.some(
(d) => ["react", "vue", "svelte", "next", "nuxt"].includes(d)
)) {
domain = "web-app";
} else if (depNames.some(
(d) => ["express", "fastify", "hono", "elysia"].includes(d)
)) {
domain = "api-server";
} else if (depNames.some(
(d) => d.includes("@tensorflow") || d.includes("langchain") || d.includes("openai")
)) {
domain = "ai-ml";
}
} catch {
}
}
if (existsSync(join2(rootPath, "pyproject.toml"))) {
indicators.push("pyproject.toml");
ecosystem = ecosystem ?? "python";
} else if (existsSync(join2(rootPath, "requirements.txt"))) {
indicators.push("requirements.txt");
ecosystem = ecosystem ?? "python";
}
const cargoPath = join2(rootPath, "Cargo.toml");
if (existsSync(cargoPath)) {
indicators.push("Cargo.toml");
ecosystem = ecosystem ?? "rust";
try {
const cargo = readFileSync(cargoPath, "utf-8");
const nameMatch = cargo.match(/^name\s*=\s*"(.+?)"/m);
if (nameMatch) name = name ?? nameMatch[1] ?? null;
} catch {
}
}
if (existsSync(join2(rootPath, "go.mod"))) {
indicators.push("go.mod");
ecosystem = ecosystem ?? "go";
}
if (existsSync(join2(rootPath, "Dockerfile")) || existsSync(join2(rootPath, "docker-compose.yml"))) {
indicators.push("Dockerfile");
domain = domain ?? "infrastructure";
}
let framework = null;
if (existsSync(pkgPath)) {
try {
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
const deps = {
...pkg.dependencies,
...pkg.devDependencies,
...pkg.peerDependencies
};
const depNames = Object.keys(deps);
if (depNames.includes("next")) {
framework = "next";
} else if (depNames.includes("nuxt")) {
framework = "nuxt";
} else if (depNames.includes("svelte")) {
framework = "svelte";
} else if (depNames.includes("vue")) {
framework = "vue";
} else if (depNames.includes("remotion") || depNames.includes("@remotion/server")) {
framework = "remotion";
} else if (depNames.includes("expo") || depNames.includes("react-native")) {
framework = "expo";
} else if (depNames.some((d) => ["commander", "cac", "oclif", "yargs"].includes(d))) {
framework = "cli";
} else if (depNames.includes("react")) {
framework = "react";
}
} catch {
}
}
return {
ecosystem: ecosystem ?? "unknown",
name,
description,
domain,
framework,
indicators
};
}
function extractReadmeDescription(rootPath) {
const candidates = ["README.md", "README.MD", "readme.md", "README.txt"];
for (const candidate of candidates) {
const readmePath = join2(rootPath, candidate);
if (!existsSync(readmePath)) continue;
try {
const content = readFileSync(readmePath, "utf-8");
const lines = content.split("\n").slice(0, 60);
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("!") && trimmed.length > 20) {
const clean = trimmed.replace(/[*_`[\]]/g, "").replace(/\(https?:\/\/[^\)]+\)/g, "").trim();
if (clean.length > 10) {
return {
description: clean.slice(0, 200),
indicators: [candidate]
};
}
}
}
} catch {
}
}
return { description: null, indicators: [] };
}
function shallowFileCount(rootPath, maxDepth = 3) {
let count = 0;
function walk(dir, depth) {
if (depth > maxDepth) return;
let entries = [];
try {
entries = readdirSync(dir);
} catch {
return;
}
for (const entry of entries) {
if (entry.startsWith(".") || entry === "node_modules" || entry === ".trellis")
continue;
try {
const full = join2(dir, entry);
const stat2 = statSync(full);
if (stat2.isDirectory()) {
walk(full, depth + 1);
} else {
count++;
}
} catch {
}
}
}
walk(rootPath, 0);
return count;
}
function computeConfidence(fileCount, indicators) {
if (fileCount <= 5) return "high";
if (indicators.length >= 2) return "high";
if (fileCount >= 1e4) return "low";
if (indicators.length === 1) return fileCount <= 500 ? "high" : "medium";
return "medium";
}
async function inferProjectContext(rootPath, opts) {
const fileCount = opts?.precomputedFileCount ?? shallowFileCount(rootPath);
const ecosystem = detectEcosystem(rootPath);
const readme = extractReadmeDescription(rootPath);
const allIndicators = [...ecosystem.indicators, ...readme.indicators];
const confidence = computeConfidence(fileCount, allIndicators);
return {
domain: ecosystem.domain,
description: readme.description ?? ecosystem.description,
ecosystem: ecosystem.ecosystem,
name: ecosystem.name,
framework: ecosystem.framework,
fileCount,
confidence,
indicators: allIndicators
};
}
var init_infer = __esm({
"src/scaffold/infer.ts"() {
"use strict";
}
});
// src/scaffold/profile.ts
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
import { join as join3 } from "path";
import { homedir } from "os";
function getProfileDir() {
return join3(homedir(), ".trellis");
}
function getProfilePath() {
return join3(getProfileDir(), "profile.json");
}
function loadProfile() {
const profilePath = getProfilePath();
if (!existsSync2(profilePath)) return null;
try {
const raw = readFileSync2(profilePath, "utf-8");
return JSON.parse(raw);
} catch {
return null;
}
}
function saveProfile(profile) {
const dir = getProfileDir();
if (!existsSync2(dir)) {
mkdirSync(dir, { recursive: true });
}
writeFileSync(getProfilePath(), JSON.stringify(profile, null, 2));
}
function hasProfile() {
return existsSync2(getProfilePath());
}
function updateProfile(updates) {
const existing = loadProfile();
const base = existing ?? {
name: "Unknown",
bio: "",
skills: [],
style: "",
preferences: { verbosity: "balanced", tone: "peer" },
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
};
const updated = {
...base,
...updates,
preferences: { ...base.preferences, ...updates.preferences ?? {} },
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
};
saveProfile(updated);
return updated;
}
var init_profile = __esm({
"src/scaffold/profile.ts"() {
"use strict";
}
});
// src/scaffold/write.ts
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync3 } from "fs";
import { dirname, join as join4 } from "path";
import { fileURLToPath } from "url";
function writeTrellisHooks(rootPath, ide) {
const hooksDir = join4(rootPath, ".cursor", "hooks");
const trellisHarnessDir = join4(hooksDir, "trellis-harness");
const adaptersDir = join4(hooksDir, "adapters");
if (!existsSync3(trellisHarnessDir)) {
mkdirSync2(trellisHarnessDir, { recursive: true });
}
if (!existsSync3(adaptersDir)) {
mkdirSync2(adaptersDir, { recursive: true });
}
writeFileSync2(
join4(trellisHarnessDir, "trellis-cli.sh"),
readHarnessTemplate("trellis-cli.sh"),
"utf-8"
);
writeFileSync2(
join4(trellisHarnessDir, "pre-prompt-recall.sh"),
renderPrePromptRecallScript(),
"utf-8"
);
writeFileSync2(
join4(trellisHarnessDir, "normalize-op.jq"),
readHarnessTemplate("normalize-op.jq"),
"utf-8"
);
writeFileSync2(
join4(trellisHarnessDir, "post-tool-oplog.sh"),
renderPostToolOplogScript(),
"utf-8"
);
writeFileSync2(
join4(trellisHarnessDir, "post-tool-memory-capture.sh"),
renderPostToolMemoryCaptureScript(),
"utf-8"
);
writeFileSync2(
join4(trellisHarnessDir, "stop-triage.sh"),
renderStopTriageScript(),
"utf-8"
);
writeFileSync2(
join4(trellisHarnessDir, "bug-intake.sh"),
renderBugIntakeScript(),
"utf-8"
);
writeFileSync2(
join4(trellisHarnessDir, "bug-investigate.sh"),
renderBugInvestigateScript(),
"utf-8"
);
writeFileSync2(
join4(trellisHarnessDir, "milestone-triage.sh"),
renderMilestoneTriageScript(),
"utf-8"
);
writeFileSync2(
join4(trellisHarnessDir, "cycle-planning.sh"),
renderCyclePlanningScript(),
"utf-8"
);
const adapterName = `${ide}-adapter.sh`;
writeFileSync2(
join4(adaptersDir, adapterName),
renderAdapterScript(ide),
"utf-8"
);
switch (ide) {
case "cursor":
writeFileSync2(
join4(rootPath, ".cursor", "hooks.json"),
JSON.stringify(renderCursorHooksConfig(), null, 2),
"utf-8"
);
break;
case "devin":
writeFileSync2(
join4(rootPath, ".devin", "hooks.json"),
JSON.stringify(renderDevinHooksConfig(), null, 2),
"utf-8"
);
break;
case "claude":
writeFileSync2(
join4(rootPath, ".claude", "settings.local.json"),
JSON.stringify(renderClaudeHooksConfig(), null, 2),
"utf-8"
);
break;
case "codex":
writeFileSync2(
join4(rootPath, ".codex", "hooks.json"),
JSON.stringify(renderCodexHooksConfig(), null, 2),
"utf-8"
);
break;
case "gemini":
writeFileSync2(
join4(rootPath, ".gemini", "settings.json"),
JSON.stringify(renderGeminiHooksConfig(), null, 2),
"utf-8"
);
break;
}
}
function renderAgentsMd(profile, context) {
const userName = profile?.name ?? "the user";
const userBio = profile?.bio || "(No bio provided \u2014 run `trellis season` to set up your profile.)";
const userSkills = profile?.skills?.length ? profile.skills.join(", ") : "(not specified)";
const userStyle = profile?.style || "(not specified)";
const userVerbosity = profile?.preferences?.verbosity ?? "balanced";
const userTone = profile?.preferences?.tone ?? "peer";
const projectName = context.name ?? "(unnamed)";
const projectDomain = context.domain ?? "(not determined \u2014 run `trellis season` to specify)";
const projectDesc = context.description ?? "(no description found)";
const projectEco = context.ecosystem ?? "unknown";
const confidence = context.confidence;
return `# Trellis Agent Context
> This file was generated by \`trellis init\` and should be kept up to date.
> Inference confidence: **${confidence}**
---
## About the User
| Field | Value |
|-------|-------|
| **Name** | ${userName} |
| **Bio** | ${userBio} |
| **Skills** | ${userSkills} |
| **Style** | ${userStyle} |
| **Preferred verbosity** | ${userVerbosity} |
| **Preferred tone** | ${userTone} |
---
## About This Project
| Field | Value |
|-------|-------|
| **Name** | ${projectName} |
| **Domain** | ${projectDomain} |
| **Description** | ${projectDesc} |
| **Ecosystem** | ${projectEco} |
| **File count** | ~${context.fileCount} |
---
## Agent Instructions
You are operating in a Trellis-tracked repository. Follow these guidelines:
1. **Read \`agent-context.json\`** in this directory for registered tools, ontologies, and domain settings.
2. **Check \`skills/\`** for domain-specific operating instructions relevant to this project.
3. **Check \`workflows/\`** for repeatable task procedures. Favor existing workflows before improvising.
4. **Update this file** as the project evolves \u2014 new teammates, new goals, new tools.
5. **Communicate through Trellis** \u2014 prefer writing to the graph kernel over mutating files directly when recording decisions or state.
6. **Multi-agent lanes** \u2014 use \`trellis issue start\` or \`trellis lane\`; \`trellis init\` sets \`lanes.worktreeBind\` and \`git.syncOnPromote\` by default.
7. **Run \`trellis season\`** if context seems incomplete or out of date.
---
## Quick Reference
\`\`\`bash
trellis status # Current repo state
trellis log # Causal operation history
trellis issue start TRL-N # Branch + agent lane (default)
trellis lane status # Active lane + worktree path when bound
trellis milestone # Create narrative checkpoints
trellis ask "..." # Semantic search across the repo
trellis season # Re-run domain onboarding
\`\`\`
`;
}
function renderConfigJson(context) {
return {
domain: context.domain,
ecosystem: context.ecosystem,
tools: [],
ontologies: [],
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
confidence: context.confidence
};
}
function renderSkillsReadme() {
return `# Skills
Place domain-specific operating instructions here as \`.md\` files.
Each skill file should follow the format:
\`\`\`markdown
---
name: Skill Name
description: When to use this skill.
---
# Instructions
...
\`\`\`
The agent will discover and read skill files relevant to the current task.
`;
}
function renderWorkflowsReadme() {
return `# Workflows
Place repeatable task procedures here as \`.md\` files.
Each workflow file should follow the format:
\`\`\`markdown
---
description: Short description of what this workflow does.
---
## Steps
1. Step one
2. Step two
// turbo
3. Auto-runnable step
\`\`\`
Add \`// turbo\` above a step to allow the agent to auto-run it without confirmation.
Add \`// turbo-all\` anywhere in the file to auto-run every step.
`;
}
function writeAgentScaffold(rootPath, input) {
const agentsDir = join4(rootPath, ".trellis", "agents");
const skillsDir = join4(agentsDir, "skills");
const workflowsDir = join4(agentsDir, "workflows");
for (const dir of [agentsDir, skillsDir, workflowsDir]) {
if (!existsSync3(dir)) {
mkdirSync2(dir, { recursive: true });
}
}
writeFileSync2(
join4(agentsDir, "AGENTS.md"),
renderAgentsMd(input.profile, input.context),
"utf-8"
);
writeFileSync2(
join4(agentsDir, "agent-context.json"),
JSON.stringify(renderConfigJson(input.context), null, 2),
"utf-8"
);
const skillsReadmePath = join4(skillsDir, "README.md");
if (!existsSync3(skillsReadmePath)) {
writeFileSync2(skillsReadmePath, renderSkillsReadme(), "utf-8");
}
const workflowsReadmePath = join4(workflowsDir, "README.md");
if (!existsSync3(workflowsReadmePath)) {
writeFileSync2(workflowsReadmePath, renderWorkflowsReadme(), "utf-8");
}
}
function renderCursorRules(input) {
const projectName = input.context.name ?? "this project";
const domain = input.context.domain ?? "general";
const eco = input.context.ecosystem ?? "unknown";
const framework = input.framework && input.framework !== "none" ? input.framework : "vanilla";
const plugins = input.plugins.length > 0 ? input.plugins.map((p) => `- ${p}`).join("\n") : "(none selected)";
return `# Cursor Rules for ${projectName}
> Generated by \`trellis init\`
## Project Context
- **Domain**: ${domain}
- **Framework**: ${framework}
- **Ecosystem**: ${eco}
- **Confidence**: ${input.context.confidence}
## Selected Features
${plugins}
## Agent Instructions
You are working in a Trellis-tracked repository. See \`.trellis/agents/AGENTS.md\` for full context.
## Commands
- \`trellis status\` \u2014 Check repo state
- \`trellis seed\` \u2014 Refresh this context file
- \`trellis log\` \u2014 View causal history
---
*Auto-generated by Trellis. Run \`trellis seed\` to refresh context.*
`;
}
function renderDevinRules(input) {
const projectName = input.context.name ?? "this project";
const domain = input.context.domain ?? "general";
const eco = input.context.ecosystem ?? "unknown";
const framework = input.framework && input.framework !== "none" ? input.framework : "vanilla";
const plugins = input.plugins.length > 0 ? input.plugins.map((p) => `- ${p}`).join("\n") : "(none selected)";
return `# Devin Rules for ${projectName}
> Generated by \`trellis init\`
## Project Context
- **Domain**: ${domain}
- **Framework**: ${framework}
- **Ecosystem**: ${eco}
- **Confidence**: ${input.context.confidence}
## Selected Features
${plugins}
## Agent Instructions
You are working in a Trellis-tracked repository. See \`.trellis/agents/AGENTS.md\` for full context.
## Commands
- \`trellis status\` \u2014 Check repo state
- \`trellis seed\` \u2014 Refresh this context file
- \`trellis log\` \u2014 View causal history
---
*Auto-generated by Trellis. Run \`trellis seed\` to refresh context.*
`;
}
function renderClaudeMd(input) {
const projectName = input.context.name ?? "this project";
const domain = input.context.domain ?? "general";
const eco = input.context.ecosystem ?? "unknown";
const framework = input.framework && input.framework !== "none" ? input.framework : "vanilla";
const plugins = input.plugins.length > 0 ? input.plugins.map((p) => `- ${p}`).join("\n") : "(none selected)";
return `# Claude Context for ${projectName}
> Generated by \`trellis init\`
## Project Context
- **Domain**: ${domain}
- **Framework**: ${framework}
- **Ecosystem**: ${eco}
- **Confidence**: ${input.context.confidence}
## Selected Features
${plugins}
## Agent Instructions
You are working in a Trellis-tracked repository. See \`.trellis/agents/AGENTS.md\` for full context.
## Commands
- \`trellis status\` \u2014 Check repo state
- \`trellis seed\` \u2014 Refresh this context file
- \`trellis log\` \u2014 View causal history
---
*Auto-generated by Trellis. Run \`trellis seed\` to refresh context.*
`;
}
function renderCopilotConfig(input) {
return {
version: "1.0",
generatedBy: "trellis init",
context: {
domain: input.context.domain,
framework: input.framework,
ecosystem: input.context.ecosystem,
projectName: input.context.name,
confidence: input.context.confidence
},
plugins: input.plugins
};
}
function renderCodexConfig(input) {
return {
version: "1.0",
generatedBy: "trellis init",
context: {
domain: input.context.domain,
framework: input.framework,
ecosystem: input.context.ecosystem,
projectName: input.context.name,
confidence: input.context.confidence
},
features: input.plugins
};
}
function renderGeminiConfig(input) {
return {
version: "1.0",
generatedBy: "trellis init",
context: {
domain: input.context.domain,
framework: input.framework,
ecosystem: input.context.ecosystem,
projectName: input.context.name,
confidence: input.context.confidence
},
features: input.plugins
};
}
function renderPrePromptRecallScript() {
return `#!/usr/bin/env bash
# Pre-prompt memory and context recall for Trellis integration
# Normalized contract: TRELLIS_ORIGIN, TRELLIS_DESK_ROOT, TRELLIS_HOOK_OUTPUT
set -euo pipefail
# Import desk root detection + trellis CLI helpers
source "$(dirname "$0")/../desk-root.sh"
source "$(dirname "$0")/trellis-cli.sh"
# Environment variables from contract
ORIGIN="\${TRELLIS_ORIGIN:-unknown}"
OUTPUT="\${TRELLIS_HOOK_OUTPUT:-stdout}"
# Only run if we're in a Trellis workspace
if ! command -v trellis >/dev/null 2>&1; then
exit 0
fi
# Try to get Trellis context (non-blocking)
if [ -f ".trellis/config.json" ] || [ -f "$(trellis_harness_vcs_path)/.trellis/config.json" ]; then
CONTEXT_OUTPUT='{"entities":[],"relations":[]}'
OPS_COUNT=$(trellis_harness_recent_log_count 3)
ISSUES_COUNT=$(trellis_harness_active_issue_count)
OPS_OUTPUT="{\\"count\\":\${OPS_COUNT}}"
ISSUES_OUTPUT="{\\"count\\":\${ISSUES_COUNT},\\"status\\":\\"in_progress\\"}"
case "$OUTPUT" in
"agent-stop")
echo "{}"
;;
"gemini")
cat << EOF
{
"decision": "allow",
"context": {
"memories": $CONTEXT_OUTPUT,
"operations": $OPS_OUTPUT,
"issues": $ISSUES_OUTPUT,
"origin": "$ORIGIN"
}
}
EOF
;;
*)
echo ""
echo "\u{1F33F} Trellis Context ($ORIGIN):"
echo " Recent ops: $OPS_COUNT"
echo " Active issues: $ISSUES_COUNT"
echo ""
;;
esac
fi
exit 0`;
}
function readHarnessTemplate(name) {
return readFileSync3(
join4(SCAFFOLD_MODULE_DIR, "..", "..", "templates", "trellis-harness", name),
"utf-8"
);
}
function renderPostToolOplogScript() {
return `#!/usr/bin/env bash
# Post-tool operation logging for Trellis integration (agent-ops v1 schema).
set -euo pipefail
source "$(dirname "$0")/../desk-root.sh"
ORIGIN="\${TRELLIS_ORIGIN:-unknown}"
TOOL_DATA=$(cat)
if [ ! -f ".trellis/config.json" ]; then
exit 0
fi
if ! command -v jq >/dev/null 2>&1; then
exit 0
fi
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
LOG_DIR=".trellis/agent-ops"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/ops-$(date +%Y-%m-%d).jsonl"
NORMALIZE_JQ="$(dirname "$0")/normalize-op.jq"
OP_ENTRY=$(printf '%s' "$TOOL_DATA" | jq -c -f "$NORMALIZE_JQ" \\
--arg ts "$TS" \\
--arg origin "$ORIGIN" 2>/dev/null) || OP_ENTRY="{\\"schema_version\\":1,\\"timestamp\\":\\"$TS\\",\\"origin\\":\\"$ORIGIN\\",\\"tool\\":\\"unknown\\",\\"action\\":\\"unknown\\",\\"target\\":\\"\\",\\"command\\":\\"\\",\\"pattern\\":\\"\\",\\"mcp_server\\":\\"\\",\\"mcp_tool\\":\\"\\",\\"model\\":\\"\\",\\"tool_use_id\\":\\"\\",\\"type\\":\\"agent-operation\\"}"
printf '%s\\n' "$OP_ENTRY" >> "$LOG_FILE"
find "$LOG_DIR" -name "ops-*.jsonl" -mtime +7 -delete 2>/dev/null || true
exit 0`;
}
function renderPostToolMemoryCaptureScript() {
return `#!/usr/bin/env bash
# Post-tool memory and entity capture for Trellis integration
# Normalized contract: TRELLIS_ORIGIN, TRELLIS_DESK_ROOT, stdin with tool data
set -euo pipefail
# Import desk root detection
source "$(dirname "$0")/../desk-root.sh"
source "$(dirname "$0")/trellis-cli.sh"
# Environment variables from contract
ORIGIN="\${TRELLIS_ORIGIN:-unknown}"
# Read tool data from stdin
TOOL_DATA=$(cat)
NORMALIZE_JQ="$(dirname "$0")/normalize-op.jq"
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
NORM=$(printf '%s' "$TOOL_DATA" | jq -c -f "$NORMALIZE_JQ" --arg ts "$TS" --arg origin "$ORIGIN" 2>/dev/null || echo "")
TOOL_NAME=$(echo "$NORM" | jq -r '.tool // "unknown"' 2>/dev/null || echo "unknown")
ACTION=$(echo "$NORM" | jq -r '.action // "unknown"' 2>/dev/null || echo "unknown")
FILE_PATH=$(echo "$NORM" | jq -r '.target // ""' 2>/dev/null || echo "")
# Only run if we're in a Trellis workspace
if ! command -v trellis >/dev/null 2>&1; then
exit 0
fi
if [ -f ".trellis/config.json" ]; then
# Suggest memory creation for significant operations
case "$TOOL_NAME-$ACTION" in
"edit-create"|"write-create"|"Edit-create"|"Write-create")
# File creation - suggest creating a memory
if [ -n "$FILE_PATH" ]; then
echo "\u{1F9E0} Consider creating a memory for the new file: $FILE_PATH"
echo " Run: trellis memory create -t "Created $FILE_PATH" -c "Created via $ORIGIN agent""
fi
;;
"edit-update"|"Edit-update")
# File update - suggest updating memory if significant
if [ -n "$FILE_PATH" ]; then
echo "\u{1F504} Consider updating relevant memories for: $FILE_PATH"
echo " Run: trellis memory list -q "$FILE_PATH""
fi
;;
"issue-create"|"issue-update")
# Issue operations - always suggest memory
echo "\u{1F4DD} Consider creating a memory for this issue operation"
echo " Run: trellis memory create -t "Issue $ACTION via $ORIGIN""
;;
esac
# Store potential memory suggestions for later
MEMORY_DIR=".trellis/agent-suggestions"
mkdir -p "$MEMORY_DIR"
SUGGESTION=$(cat << EOF
{
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)",
"origin": "$ORIGIN",
"tool": "$TOOL_NAME",
"action": "$ACTION",
"target": "$FILE_PATH",
"suggestion_type": "memory-creation"
}
EOF
)
SUGGESTIONS_FILE="$MEMORY_DIR/suggestions-$(date +%Y-%m-%d).jsonl"
echo "$SUGGESTION" >> "$SUGGESTIONS_FILE"
# Keep only last 3 days of suggestions
find "$MEMORY_DIR" -name "suggestions-*.jsonl" -mtime +3 -delete 2>/dev/null || true
fi
exit 0`;
}
function renderStopTriageScript() {
return `#!/usr/bin/env bash
# Session stop issue triage and workflow suggestions for Trellis integration
# Normalized contract: TRELLIS_ORIGIN, TRELLIS_DESK_ROOT, TRELLIS_HOOK_OUTPUT
set -euo pipefail
# Import desk root detection + trellis CLI helpers
source "$(dirname "$0")/../desk-root.sh"
source "$(dirname "$0")/trellis-cli.sh"
# Environment variables from contract
ORIGIN="\${TRELLIS_ORIGIN:-unknown}"
OUTPUT="\${TRELLIS_HOOK_OUTPUT:-stdout}"
# Only run if we're in a Trellis workspace
if ! command -v trellis >/dev/null 2>&1; then
exit 0
fi
if [ -f ".trellis/config.json" ] || [ -f "$(trellis_harness_vcs_path)/.trellis/config.json" ]; then
ACTIVE_COUNT=$(trellis_harness_active_issue_count)
# Check for pending memory suggestions
MEMORY_DIR=".trellis/agent-suggestions"
SUGGESTION_COUNT=0
if [ -d "$MEMORY_DIR" ]; then
SUGGESTION_COUNT=$(find "$MEMORY_DIR" -name "suggestions-*.jsonl" -exec wc -l {} ; 2>/dev/null | awk '{sum+=$1} END {print sum}' || echo "0")
fi
# Check for uncommitted changes
UNCOMMITTED=0
if git rev-parse --git-dir >/dev/null 2>&1; then
UNCOMMITTED=$(git status --porcelain 2>/dev/null | wc -l || echo "0")
fi
# Build recommendations
RECOMMENDATIONS=()
if [ "$ACTIVE_COUNT" -gt 0 ]; then
RECOMMENDATIONS+=("\u{1F3AF} $ACTIVE_COUNT active issues need attention - run 'trellis issue active'")
fi
if [ "$SUGGESTION_COUNT" -gt 0 ]; then
RECOMMENDATIONS+=("\u{1F9E0} $SUGGESTION_COUNT memory suggestions pending - review .trellis/agent-suggestions/")
fi
if [ "$UNCOMMITTED" -gt 0 ]; then
RECOMMENDATIONS+=("\u{1F4DD} $UNCOMMITTED uncommitted changes - consider committing or creating a checkpoint")
fi
# Format output based on hook type
case "$OUTPUT" in
"agent-stop")
# For Codex/Gemini blocking hooks
if [ \${#RECOMMENDATIONS[@]} -gt 0 ]; then
MESSAGE=$(printf '%s
' "\${RECOMMENDATIONS[@]}")
cat << EOF
{
"decision": "block",
"reason": "Trellis workflow items need attention: $MESSAGE"
}
EOF
else
echo "{}"
fi
;;
"gemini")
# For Gemini CLI format
if [ \${#RECOMMENDATIONS[@]} -gt 0 ]; then
MESSAGE=$(printf '%s\\n' "\${RECOMMENDATIONS[@]}")
cat << EOF
{
"decision": "deny",
"reason": "Trellis workflow items need attention: $MESSAGE"
}
EOF
else
echo '{"decision": "allow"}'
fi
;;
*)
# Default stdout for Cursor/Devin/Claude
if [ \${#RECOMMENDATIONS[@]} -gt 0 ]; then
echo ""
echo "\u{1F33F} Trellis Session Summary ($ORIGIN):"
printf ' %s
' "\${RECOMMENDATIONS[@]}"
echo ""
echo "Run 'trellis status' for full workspace state"
else
echo "\u{1F33F} Trellis: All clear! No pending items."
fi
;;
esac
fi
exit 0`;
}
function renderAdapterScript(ide) {
switch (ide) {
case "cursor":
return `#!/usr/bin/env bash
# Cursor adapter for Trellis harness \u2014 raw stdin \u2192 shared normalizer
set -euo pipefail
source "$(dirname "$0")/../desk-root.sh"
export TRELLIS_ORIGIN="cursor"
export TRELLIS_DESK_ROOT="$PWD"
EVENT_TYPE="\${1:-unknown}"
HARNESS="$(dirname "$0")/../trellis-harness"
log_tool_event() {
local data
data=$(cat)
printf '%s' "$data" | "$HARNESS/post-tool-oplog.sh"
printf '%s' "$data" | "$HARNESS/post-tool-memory-capture.sh"
}
case "$EVENT_TYPE" in
"session-start")
exec "$HARNESS/pre-prompt-recall.sh"
;;
"post-tool"|"post-tool-use"|"after-shell"|"afterShellExecution"|"after-mcp"|"afterMCPExecution"|"post-tool-edit"|"afterFileEdit")
log_tool_event
;;
"stop")
export TRELLIS_HOOK_OUTPUT="stdout"
exec "$HARNESS/stop-triage.sh"
;;
*)
echo "Unknown Cursor event: $EVENT_TYPE" >&2
exit 1
;;
esac`;
case "devin":
return `#!/usr/bin/env bash
# Devin adapter for Trellis harness
# Translates Devin events to normalized contract
set -euo pipefail
# Import desk root detection
source "$(dirname "$0")/../desk-root.sh"
# Set origin
export TRELLIS_ORIGIN="devin"
export TRELLIS_DESK_ROOT="$PWD"
# Route based on event type
EVENT_TYPE="\${1:-unknown}"
case "$EVENT_TYPE" in
"pre-prompt")
# Cascade pre_user_prompt hook
exec "$(dirname "$0")/../trellis-harness/pre-prompt-recall.sh"
;;
"post-tool")
# Cascade post_write_code hook - read stdin for tool data
TOOL_DATA=$(cat)
# Transform Cascade format to normalized format
NORMALIZED_DATA=$(cat << EOF
{
"tool": "write_file",
"action": "create",
"file_path": $(echo "$TOOL_DATA" | jq -r '.file_path // ""' 2>/dev/null || echo '""'),
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)"
}
EOF
)
# Call op logger
echo "$NORMALIZED_DATA" | "$(dirname "$0")/../trellis-harness/post-tool-oplog.sh"
# Call memory capture
echo "$NORMALIZED_DATA" | "$(dirname "$0")/../trellis-harness/post-tool-memory-capture.sh"
;;
"post-response")
# Cascade post_cascade_response hook
export TRELLIS_HOOK_OUTPUT="stdout"
exec "$(dirname "$0")/../trellis-harness/stop-triage.sh"
;;
*)
echo "Unknown Cascade event: $EVENT_TYPE" >&2
exit 1
;;
esac`;
case "claude":
return `#!/usr/bin/env bash
# Claude Code adapter for Trellis harness
# Translates Claude Code events to normalized contract
set -euo pipefail
# Import desk root detection
source "$(dirname "$0")/../desk-root.sh"
# Set origin
export TRELLIS_ORIGIN="claude"
export TRELLIS_DESK_ROOT="$PWD"
# Route based on event type
EVENT_TYPE="\${1:-unknown}"
case "$EVENT_TYPE" in
"pre-tool-use")
# Claude Code PreToolUse hook
exec "$(dirname "$0")/../trellis-harness/pre-prompt-recall.sh"
;;
"post-tool-use")
data=$(cat)
printf '%s' "$data" | "$(dirname "$0")/../trellis-harness/post-tool-oplog.sh"
printf '%s' "$data" | "$(dirname "$0")/../trellis-harness/post-tool-memory-capture.sh"
;;
"post-tool-batch")
jq -c '.[]' 2>/dev/null | while read -r row; do
printf '%s\\n' "$row" | "$(dirname "$0")/../trellis-harness/post-tool-oplog.sh"
printf '%s\\n' "$row" | "$(dirname "$0")/../trellis-harness/post-tool-memory-capture.sh"
done
;;
"permission-denied")
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
printf '%s\\n' "{\\"tool_name\\":\\"permission_denied\\",\\"action\\":\\"blocked\\",\\"timestamp\\":\\"$TS\\"}" | "$(dirname "$0")/../trellis-harness/post-tool-oplog.sh"
;;
*)
echo "Unknown Claude Code event: $EVENT_TYPE" >&2
exit 1
;;
esac`;
case "codex":
return `#!/usr/bin/env bash
# Codex adapter for Trellis harness
# Translates Codex events to normalized contract
set -euo pipefail
# Import desk root detection
source "$(dirname "$0")/../desk-root.sh"
# Set origin
export TRELLIS_ORIGIN="codex"
export TRELLIS_DESK_ROOT="$PWD"
# Route based on event type
EVENT_TYPE="\${1:-unknown}"
case "$EVENT_TYPE" in
"session-start")
# Codex SessionStart hook
exec "$(dirname "$0")/../trellis-harness/pre-prompt-recall.sh"
;;
"post-tool")
# Codex PostToolUse hook - read stdin for tool data
TOOL_DATA=$(cat)
# Transform Codex format to normalized format
NORMALIZED_DATA=$(cat << EOF
{
"tool": $(echo "$TOOL_DATA" | jq -r '.tool_name // "unknown"' 2>/dev/null || echo '"unknown"'),
"action": $(echo "$TOOL_DATA" | jq -r '.action // "unknown"' 2>/dev/null || echo '"unknown"'),
"file_path": $(echo "$TOOL_DATA" | jq -r '.file_path // .filePath // ""' 2>/dev/null || echo '""'),
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)"
}
EOF
)
# Call op logger
echo "$NORMALIZED_DATA" | "$(dirname "$0")/../trellis-harness/post-tool-oplog.sh"
# Call memory capture
echo "$NORMALIZED_DATA" | "$(dirname "$0")/../trellis-harness/post-tool-memory-capture.sh"
;;
"stop")
# Codex Stop hook
export TRELLIS_HOOK_OUTPUT="agent-stop"
exec "$(dirname "$0")/../trellis-harness/stop-triage.sh"
;;
*)
echo "Unknown Codex event: $EVENT_TYPE" >&2
exit 1
;;
esac`;
case "gemini":
return `#!/usr/bin/env bash
# Gemini CLI adapter for Trellis harness
# Translates Gemini events to normalized contract
set -euo pipefail
# Import desk root detection
source "$(dirname "$0")/../desk-root.sh"
# Set origin
export TRELLIS_ORIGIN="gemini"
export TRELLIS_DESK_ROOT="$PWD"
# Route based on event type
EVENT_TYPE="\${1:-unknown}"
case "$EVENT_TYPE" in
"session-start"|"before-agent")
# Gemini SessionStart or BeforeAgent hook
exec "$(dirname "$0")/../trellis-harness/pre-prompt-recall.sh"
;;
"post-tool")
# Gemini AfterTool hook - read stdin for tool data
TOOL_DATA=$(cat)
# Transform Gemini format to normalized format
NORMALIZED_DATA=$(cat << EOF
{
"tool": $(echo "$TOOL_DATA" | jq -r '.tool_name // "unknown"' 2>/dev/null || echo '"unknown"'),
"action": $(echo "$TOOL_DATA" | jq -r '.action // "unknown"' 2>/dev/null || echo '"unknown"'),
"file_path": $(echo "$TOOL_DATA" | jq -r '.file_path // .filePath // ""' 2>/dev/null || echo '""'),
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)"
}
EOF
)
# Call op logger
echo "$NORMALIZED_DATA" | "$(dirname "$0")/../trellis-harness/post-tool-oplog.sh"
# Call memory capture
echo "$NORMALIZED_DATA" | "$(dirname "$0")/../trellis-harness/post-tool-memory-capture.sh"
;;
"after-agent")
# Gemini AfterAgent hook
export TRELLIS_HOOK_OUTPUT="gemini"
exec "$(dirname "$0")/../trellis-harness/stop-triage.sh"
;;
*)
echo "Unknown Gemini event: $EVENT_TYPE" >&2
exit 1
;;
esac`;
default:
return `#!/usr/bin/env bash
# Default adapter for Trellis harness
echo "Unsupported IDE: ${ide}" >&2
exit 1`;
}
}
function renderCursorHooksConfig() {
const hookCmd = (event) => `bash .cursor/hooks/adapters/cursor-adapter.sh ${event}`;
return {
version: 1,
hooks: {
sessionStart: [{ command: hookCmd("session-start"), timeout: 15 }],
postToolUse: [{ command: hookCmd("post-tool-use"), timeout: 10 }],
afterShellExecution: [{ command: hookCmd("after-shell"), timeout: 10 }],
afterMCPExecution: [{ command: hookCmd("after-mcp"), timeout: 10 }],
afterFileEdit: [{ command: hookCmd("afterFileEdit"), timeout: 10 }],
stop: [{ command: hookCmd("stop"), timeout: 10 }]
}
};
}
function renderDevinHooksConfig() {
return {
hooks: {
pre_user_prompt: [
{
command: "bash .cursor/hooks/adapters/devin-adapter.sh pre-prompt",
show_output: true
}
],
post_write_code: [
{
command: "bash .cursor/hooks/adapters/devin-adapter.sh post-tool",
show_output: false
}
],
post_cascade_response: [
{
command: "bash .cursor/hooks/adapters/devin-adapter.sh post-response",
show_output: true
}
]
}
};
}
function renderClaudeHooksConfig() {
return {
hooks: {
PreToolUse: [
{
command: "bash .cursor/hooks/adapters/claude-adapter.sh pre-tool-use",
timeout: 15
}
],
PostToolUse: [
{
command: "bash .cursor/hooks/adapters/claude-adapter.sh post-tool-use",
timeout: 10
}
],
PostToolBatch: [
{
command: "bash .cursor/hooks/adapters/claude-adapter.sh post-tool-batch",
timeout: 15
}
],
PermissionDenied: [
{
command: "bash .cursor/hooks/adapters/claude-adapter.sh permission-denied",
timeout: 5
}
]
},
permissions: {
allow: [
"Bash(trellis --version)",
"Bash(trellis issue *)",
"Bash(trellis --help)",
"Bash(trellis search *)",
"Bash(trellis entity *)",
"Bash(trellis ontology *)",
"Bash(trellis query *)",
"Bash(lsof -nP -iTCP -sTCP:LISTEN)",
"Bash(awk '{print $9, $1, $2}')",
"Bash(curl -s --max-time 1 http://localhost:__TRACKED_VAR__/api/graph/health)",
"Bash(curl -s --max-time 1 http://localhost:__TRACKED_VAR__/api/graph/health -H 'Accept: application/json')",
'Bash(grep -ivE "node_modules|dist|.vercel|.git$|.git/|.logs")'
]
},
spinnerTipsEnabled: true
};
}
function renderCodexHooksConfig() {
return {
hooks: {
SessionStart: [
{
matcher: "startup|resume|clear",
hooks: [
{
type: "command",
command: "bash .cursor/hooks/adapters/codex-adapter.sh session-start",
timeout: 15,
statusMessage: "Loading Trellis desk context"
}
]
}
],
PostToolUse: [
{
matcher: "apply_patch|Edit|Write",
hooks: [
{
type: "command",
command: "bash .cursor/hooks/adapters/codex-adapter.sh post-tool",
timeout: 10,
statusMessage: "Recording Trellis operation"
}
]
}
],
Stop: [
{
hooks: [
{
type: "command",
command: "bash .cursor/hooks/adapters/codex-adapter.sh stop",
timeout: 10,
statusMessage: "Checking Trellis workflow status"
}
]
}
]
}
};
}
function renderGeminiHooksConfig() {
return {
hooks: {
SessionStart: [
{
matcher: "startup|resume|clear",
hooks: [
{
name: "trellis-desk-context",
type: "command",
command: "bash .cursor/hooks/adapters/gemini-adapter.sh session-start",
timeout: 15e3,
description: "Refresh pending Trellis context at session start"
}