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:
yuxuanhui
2026-09-07 11:23:15 +08:00
parent ca3bd7a4b1
commit e567e5f717
23 changed files with 2525 additions and 130 deletions
+61 -4
View File
@@ -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)) {