feat(trellis): enhance bundled skills and workflow integration
- Updated bundled skills documentation to clarify the structure and usage across all platforms, ensuring consistency in skill root paths. - Introduced a new `inject-spec-context.py` hook for path-scoped spec context injection, improving the relevance of injected specs during file interactions. - Enhanced existing hooks to support workflow resolution, allowing for dynamic selection of workflows based on task context. - Added a command to manage workflow selections for active tasks, enabling better task management and workflow adherence. - Updated configuration options for spec injection, including character limits and refresh windows, to optimize performance and usability.
This commit is contained in:
@@ -772,10 +772,11 @@ function truncateUtf8(buf: Buffer, cap: number): Buffer {
|
||||
if ((lead & 0xe0) === 0xc0) seqLen = 2;
|
||||
else if ((lead & 0xf0) === 0xe0) seqLen = 3;
|
||||
else if ((lead & 0xf8) === 0xf0) seqLen = 4;
|
||||
// Drop the lead byte too if its full sequence didn't fit.
|
||||
if (i - 1 + seqLen > cap) i--;
|
||||
// Cut before the lead byte when its full sequence didn't fit;
|
||||
// otherwise the trailing sequence is complete — keep it whole.
|
||||
if (i - 1 + seqLen > cap) return buf.subarray(0, i - 1);
|
||||
}
|
||||
return buf.subarray(0, i);
|
||||
return buf.subarray(0, cap);
|
||||
}
|
||||
|
||||
function stripInlineComment(value: string): string {
|
||||
@@ -1069,10 +1070,66 @@ function readTaskDir(root: string, key: string | null): string | null {
|
||||
}
|
||||
|
||||
// ── Workflow State Breadcrumb ─────────────────────────────────────────
|
||||
const WORKFLOW_ID_RE = /^[A-Za-z0-9_-]+$/;
|
||||
const DEFAULT_WORKFLOW_RE =
|
||||
/^default_workflow:\s*(['"]?)([A-Za-z0-9_-]+)\1\s*(?:#.*)?$/m;
|
||||
|
||||
function workflowVariant(root: string, workflowId: string): string {
|
||||
if (!WORKFLOW_ID_RE.test(workflowId)) return "";
|
||||
const path = join(root, ".trellis", "workflows", `${workflowId}.md`);
|
||||
return exists(path) ? path : "";
|
||||
}
|
||||
|
||||
function developerWorkflowId(root: string): string {
|
||||
for (const line of readText(join(root, ".trellis", ".developer")).split(
|
||||
/\r?\n/,
|
||||
)) {
|
||||
if (line.startsWith("workflow="))
|
||||
return line.slice("workflow=".length).trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function configDefaultWorkflowId(root: string): string {
|
||||
return (
|
||||
readText(join(root, ".trellis", "config.yaml")).match(
|
||||
DEFAULT_WORKFLOW_RE,
|
||||
)?.[2] ?? ""
|
||||
);
|
||||
}
|
||||
|
||||
/** Mirrors common/workflow_selection.py without spawning another Python
|
||||
* process on every Pi turn. Invalid or missing selections fall through. */
|
||||
function resolveWorkflowMd(root: string, key: string | null): string {
|
||||
const taskDir = readTaskDir(root, key);
|
||||
let workflowId = "";
|
||||
if (taskDir) {
|
||||
try {
|
||||
const task = JSON.parse(
|
||||
readText(join(taskDir, "task.json")),
|
||||
) as JsonObject;
|
||||
workflowId = typeof task.workflow === "string" ? task.workflow : "";
|
||||
} catch {}
|
||||
}
|
||||
if (workflowId) {
|
||||
const pinned = workflowVariant(root, workflowId);
|
||||
if (pinned) return pinned;
|
||||
console.error(
|
||||
`Warning: active task selects workflow ${JSON.stringify(workflowId)} but .trellis/workflows/ has no matching file; using default workflow resolution`,
|
||||
);
|
||||
}
|
||||
|
||||
const personal = workflowVariant(root, developerWorkflowId(root));
|
||||
if (personal) return personal;
|
||||
const team = workflowVariant(root, configDefaultWorkflowId(root));
|
||||
if (team) return team;
|
||||
return join(root, ".trellis", "workflow.md");
|
||||
}
|
||||
|
||||
const WF_RE =
|
||||
/\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n([\s\S]*?)\n\s*\[\/workflow-state:\1\]/g;
|
||||
function workflowBreadcrumb(root: string, key: string | null): string {
|
||||
const wf = readText(join(root, ".trellis", "workflow.md"));
|
||||
const wf = readText(resolveWorkflowMd(root, key));
|
||||
if (!wf) return "";
|
||||
const templates: Record<string, string> = {};
|
||||
for (const m of wf.matchAll(WF_RE)) {
|
||||
|
||||
Reference in New Issue
Block a user