@pennixrv/trellis 0.6.45 → 0.7.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/cli/index.d.ts.map +1 -1
  2. package/dist/cli/index.js +21 -3
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/commands/mem.d.ts.map +1 -1
  5. package/dist/commands/mem.js +6 -13
  6. package/dist/commands/mem.js.map +1 -1
  7. package/dist/commands/workflow.d.ts +17 -0
  8. package/dist/commands/workflow.d.ts.map +1 -1
  9. package/dist/commands/workflow.js +222 -0
  10. package/dist/commands/workflow.js.map +1 -1
  11. package/dist/configurators/claude.d.ts.map +1 -1
  12. package/dist/configurators/claude.js.map +1 -1
  13. package/dist/configurators/dsh.d.ts +16 -19
  14. package/dist/configurators/dsh.d.ts.map +1 -1
  15. package/dist/configurators/dsh.js +38 -26
  16. package/dist/configurators/dsh.js.map +1 -1
  17. package/dist/configurators/gemini.d.ts.map +1 -1
  18. package/dist/configurators/gemini.js.map +1 -1
  19. package/dist/configurators/opencode.d.ts.map +1 -1
  20. package/dist/configurators/opencode.js +4 -0
  21. package/dist/configurators/opencode.js.map +1 -1
  22. package/dist/migrations/manifests/0.6.17.json +2 -2
  23. package/dist/migrations/manifests/0.7.0-beta.10.json +9 -0
  24. package/dist/migrations/manifests/0.7.0-beta.4.json +9 -0
  25. package/dist/migrations/manifests/0.7.0-beta.4.pennix.1.json +9 -0
  26. package/dist/migrations/manifests/0.7.0-beta.5.json +9 -0
  27. package/dist/migrations/manifests/0.7.0-beta.6.json +9 -0
  28. package/dist/migrations/manifests/0.7.0-beta.7.json +9 -0
  29. package/dist/migrations/manifests/0.7.0-beta.8.json +9 -0
  30. package/dist/migrations/manifests/0.7.0-beta.9.json +9 -0
  31. package/dist/templates/claude/hooks/statusline.py +7 -0
  32. package/dist/templates/claude/settings.json +22 -0
  33. package/dist/templates/codex/hooks/session-start.py +20 -1
  34. package/dist/templates/codex/hooks.json +25 -0
  35. package/dist/templates/common/bundled-skills/trellis-channel/references/subnode-work.md +32 -3
  36. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +2 -2
  37. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +1 -0
  38. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +2 -1
  39. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/hooks-and-settings.md +3 -2
  40. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +1 -1
  41. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +5 -4
  42. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +1 -0
  43. package/dist/templates/common/bundled-skills/trellis-session-insight/SKILL.md +2 -2
  44. package/dist/templates/common/bundled-skills/trellis-session-insight/references/cli-quick-reference.md +3 -2
  45. package/dist/templates/common/commands/continue.md +4 -2
  46. package/dist/templates/common/skills/brainstorm.md +14 -6
  47. package/dist/templates/copilot/hooks/session-start.py +20 -1
  48. package/dist/templates/copilot/prompts/brainstorm.prompt.md +16 -6
  49. package/dist/templates/dsh/DSH.md +61 -37
  50. package/dist/templates/dsh/agents/trellis-check.md +99 -0
  51. package/dist/templates/dsh/agents/trellis-implement.md +106 -0
  52. package/dist/templates/dsh/agents/trellis-research.md +134 -0
  53. package/dist/templates/dsh/index.d.ts +12 -11
  54. package/dist/templates/dsh/index.d.ts.map +1 -1
  55. package/dist/templates/dsh/index.js +14 -12
  56. package/dist/templates/dsh/index.js.map +1 -1
  57. package/dist/templates/opencode/plugins/inject-spec-context.js +121 -0
  58. package/dist/templates/opencode/plugins/inject-workflow-state.js +111 -10
  59. package/dist/templates/pi/extensions/trellis/index.ts.txt +164 -10
  60. package/dist/templates/shared-hooks/index.d.ts +8 -2
  61. package/dist/templates/shared-hooks/index.d.ts.map +1 -1
  62. package/dist/templates/shared-hooks/index.js +13 -1
  63. package/dist/templates/shared-hooks/index.js.map +1 -1
  64. package/dist/templates/shared-hooks/inject-shell-session-context.py +3 -3
  65. package/dist/templates/shared-hooks/inject-spec-context.py +844 -0
  66. package/dist/templates/shared-hooks/inject-workflow-state.py +35 -9
  67. package/dist/templates/shared-hooks/session-start.py +22 -1
  68. package/dist/templates/trellis/agents/subnode.md +11 -4
  69. package/dist/templates/trellis/config.yaml +49 -0
  70. package/dist/templates/trellis/index.d.ts +3 -0
  71. package/dist/templates/trellis/index.d.ts.map +1 -1
  72. package/dist/templates/trellis/index.js +6 -0
  73. package/dist/templates/trellis/index.js.map +1 -1
  74. package/dist/templates/trellis/scripts/common/active_task.py +26 -4
  75. package/dist/templates/trellis/scripts/common/cli_adapter.py +38 -6
  76. package/dist/templates/trellis/scripts/common/config.py +18 -0
  77. package/dist/templates/trellis/scripts/common/context_projection.py +2 -2
  78. package/dist/templates/trellis/scripts/common/git_context.py +34 -2
  79. package/dist/templates/trellis/scripts/common/paths.py +28 -0
  80. package/dist/templates/trellis/scripts/common/spec_inject.py +439 -0
  81. package/dist/templates/trellis/scripts/common/spec_match.py +395 -0
  82. package/dist/templates/trellis/scripts/common/task_store.py +88 -1
  83. package/dist/templates/trellis/scripts/common/trellis_config.py +46 -7
  84. package/dist/templates/trellis/scripts/common/workflow_phase.py +3 -2
  85. package/dist/templates/trellis/scripts/common/workflow_selection.py +177 -0
  86. package/dist/templates/trellis/scripts/subnode_artifact.py +153 -15
  87. package/dist/templates/trellis/scripts/task.py +160 -0
  88. package/dist/templates/trellis/workflow.md +49 -34
  89. package/dist/types/ai-tools.d.ts.map +1 -1
  90. package/dist/types/ai-tools.js +26 -13
  91. package/dist/types/ai-tools.js.map +1 -1
  92. package/package.json +2 -2
@@ -0,0 +1,134 @@
1
+ ---
2
+ name: trellis-research
3
+ user-invocable: false
4
+ description: |
5
+ Child-agent-only research role for Trellis. Main sessions must not load this
6
+ skill directly. Finds files, patterns, and technical solutions, and persists
7
+ every finding to the current task's research/ directory. No code changes
8
+ outside that directory.
9
+ ---
10
+ # Research Agent
11
+
12
+ You are the Research Agent in the Trellis workflow.
13
+
14
+ ## Core Principle
15
+
16
+ **You do one thing: find, explain, and PERSIST information.**
17
+
18
+ Conversations get compacted; files don't. Every research output MUST end up as a file under `{TASK_DIR}/research/`. Returning findings only through the chat reply is a failure - the caller cannot read them next session.
19
+
20
+ ## Core Responsibilities
21
+
22
+ 1. **Internal Search** - locate files/components, understand code logic, discover patterns
23
+ 2. **External Search** - library docs, API references, best practices
24
+ 3. **Persist** - write each research topic to `{TASK_DIR}/research/<topic>.md`
25
+ 4. **Report** - return file paths + one-line summaries to the main agent, not full content
26
+
27
+ ---
28
+
29
+ ## Workflow
30
+
31
+ ### Step 1: Resolve Current Task
32
+
33
+ Run `python3 ./.trellis/scripts/task.py current --source` to get the active task path. Prefer an `Active task: <path>` line in the dispatch prompt when present. If no active task is set, ask the user where to write output; do NOT guess.
34
+
35
+ Ensure `{TASK_DIR}/research/` exists before writing findings.
36
+
37
+ ### Step 2: Understand Search Request
38
+
39
+ Classify the request as internal, external, or mixed. Determine scope and expected output shape.
40
+
41
+ ### Step 3: Execute Search
42
+
43
+ Run independent searches in parallel where possible. Read relevant source files, specs, and documentation before forming conclusions.
44
+
45
+ ### Step 4: Persist Each Topic
46
+
47
+ For each distinct research topic, write a markdown file at `{TASK_DIR}/research/<topic-slug>.md` using the File Format below.
48
+
49
+ ### Step 5: Report to Main Agent
50
+
51
+ Reply with ONLY:
52
+
53
+ - List of files written, paths relative to repo root
54
+ - One-line summary per file
55
+ - Any critical caveats that the main agent needs to know right now
56
+
57
+ Do NOT paste full research content into the reply. The files are the contract.
58
+
59
+ ---
60
+
61
+ ## Scope Limits
62
+
63
+ ### Write Allowed
64
+
65
+ - `{TASK_DIR}/research/*.md` - your own output
66
+ - Creating `{TASK_DIR}/research/` if it doesn't exist
67
+
68
+ ### Write Forbidden
69
+
70
+ - Code files (`src/`, `lib/`, etc.)
71
+ - Spec files (`.trellis/spec/`) - main agent should use the update-spec skill instead
72
+ - `.trellis/scripts/`, `.trellis/workflow.md`, platform config (`.dsh/`, `.claude/`, `.cursor/`, etc.)
73
+ - Other task directories
74
+ - Any git operation
75
+
76
+ If the user asks you to edit code, decline and suggest spawning `trellis-implement` instead.
77
+
78
+ ---
79
+
80
+ ## File Format
81
+
82
+ Each `{TASK_DIR}/research/<topic>.md` should follow:
83
+
84
+ ```markdown
85
+ # Research: <topic>
86
+
87
+ - Query: <original query>
88
+ - Scope: internal / external / mixed
89
+ - Date: YYYY-MM-DD
90
+
91
+ ## Findings
92
+
93
+ ### Files Found
94
+
95
+ | File Path | Description |
96
+ |---|---|
97
+ | `src/services/xxx.ts` | Main implementation |
98
+ | `src/types/xxx.ts` | Type definitions |
99
+
100
+ ### Code Patterns
101
+
102
+ <describe patterns, cite file:line>
103
+
104
+ ### External References
105
+
106
+ - [Library X docs](url) - <why relevant, version constraints>
107
+
108
+ ### Related Specs
109
+
110
+ - `.trellis/spec/xxx.md` - <description>
111
+
112
+ ## Caveats / Not Found
113
+
114
+ <anything incomplete or uncertain>
115
+ ```
116
+
117
+ ---
118
+
119
+ ## Guidelines
120
+
121
+ ### Do
122
+
123
+ - Provide specific file paths and line numbers
124
+ - Quote actual code snippets only when they are relevant
125
+ - Persist every topic to its own file
126
+ - Return file paths in your reply, not the full content
127
+ - Mark "not found" explicitly when searches come up empty
128
+
129
+ ### Don't
130
+
131
+ - Don't write code or modify files outside `{TASK_DIR}/research/`
132
+ - Don't guess uncertain info
133
+ - Don't paste full research text into the reply
134
+ - Don't propose implementation changes unless the main agent explicitly asked for research options
@@ -1,21 +1,22 @@
1
1
  /**
2
2
  * DeepSeek Harness (dsh) template module.
3
3
  *
4
- * dsh is a class-2 pull-based, skills-only platform:
5
- * - Workflow + bundled skills go to the shared `.agents/skills/` root via
6
- * the neutral resolver (byte-identical to Codex/Gemini/Pi/Kimi writes).
4
+ * dsh is a class-2 pull-based platform:
5
+ * - Workflow/bundled skills go to the shared `.agents/skills/` root via the
6
+ * neutral resolver (byte-identical to Codex/Gemini/Pi/Kimi writes).
7
7
  * - User-invocable entry points (`trellis-start` / `trellis-continue` /
8
- * `trellis-finish-work`, loaded by the dsh agent through its skill-loader
9
- * tool) live under `.dsh/skills/<name>/SKILL.md` — dsh's own highest-rank
10
- * project skill root.
8
+ * `trellis-finish-work`) and Trellis role prompts live under
9
+ * `.dsh/skills/trellis-agent-<role>/SKILL.md`.
11
10
  * - Operator guide `.dsh/DSH.md`.
12
11
  *
13
- * dsh has no shipped session-start hook, so `trellis-start` is kept as a
14
- * user-invocable skill. dsh ships no project-level sub-agent definition
15
- * surface, so no trellis-implement / trellis-check / trellis-research agent
16
- * prompts are written; implement/check/research run inline through the
17
- * workflow skills.
12
+ * dsh has no project-level hooks/settings file Trellis may write and no
13
+ * declarative custom sub-agent definitions, so the Trellis agent prompts ship
14
+ * as skills; the main session dispatches them through the `subagent` tool and
15
+ * trellis-agent-implement / trellis-agent-check get the pull-based prelude.
18
16
  */
17
+ import { type AgentTemplate } from "../template-utils.js";
18
+ /** Source role prompts; the configurator installs collision-free DSH skills. */
19
+ export declare function getAllAgents(): AgentTemplate[];
19
20
  /** Operator guide copied to `.dsh/DSH.md`. */
20
21
  export declare function getDshGuide(): string;
21
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/dsh/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH,8CAA8C;AAC9C,wBAAgB,WAAW,IAAI,MAAM,CAEpC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/dsh/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAIhF,gFAAgF;AAChF,wBAAgB,YAAY,IAAI,aAAa,EAAE,CAE9C;AAED,8CAA8C;AAC9C,wBAAgB,WAAW,IAAI,MAAM,CAEpC"}
@@ -1,23 +1,25 @@
1
1
  /**
2
2
  * DeepSeek Harness (dsh) template module.
3
3
  *
4
- * dsh is a class-2 pull-based, skills-only platform:
5
- * - Workflow + bundled skills go to the shared `.agents/skills/` root via
6
- * the neutral resolver (byte-identical to Codex/Gemini/Pi/Kimi writes).
4
+ * dsh is a class-2 pull-based platform:
5
+ * - Workflow/bundled skills go to the shared `.agents/skills/` root via the
6
+ * neutral resolver (byte-identical to Codex/Gemini/Pi/Kimi writes).
7
7
  * - User-invocable entry points (`trellis-start` / `trellis-continue` /
8
- * `trellis-finish-work`, loaded by the dsh agent through its skill-loader
9
- * tool) live under `.dsh/skills/<name>/SKILL.md` — dsh's own highest-rank
10
- * project skill root.
8
+ * `trellis-finish-work`) and Trellis role prompts live under
9
+ * `.dsh/skills/trellis-agent-<role>/SKILL.md`.
11
10
  * - Operator guide `.dsh/DSH.md`.
12
11
  *
13
- * dsh has no shipped session-start hook, so `trellis-start` is kept as a
14
- * user-invocable skill. dsh ships no project-level sub-agent definition
15
- * surface, so no trellis-implement / trellis-check / trellis-research agent
16
- * prompts are written; implement/check/research run inline through the
17
- * workflow skills.
12
+ * dsh has no project-level hooks/settings file Trellis may write and no
13
+ * declarative custom sub-agent definitions, so the Trellis agent prompts ship
14
+ * as skills; the main session dispatches them through the `subagent` tool and
15
+ * trellis-agent-implement / trellis-agent-check get the pull-based prelude.
18
16
  */
19
17
  import { createTemplateReader } from "../template-utils.js";
20
- const { readTemplate } = createTemplateReader(import.meta.url);
18
+ const { listMdAgents, readTemplate } = createTemplateReader(import.meta.url);
19
+ /** Source role prompts; the configurator installs collision-free DSH skills. */
20
+ export function getAllAgents() {
21
+ return listMdAgents();
22
+ }
21
23
  /** Operator guide copied to `.dsh/DSH.md`. */
22
24
  export function getDshGuide() {
23
25
  return readTemplate("DSH.md");
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/dsh/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,MAAM,EAAE,YAAY,EAAE,GAAG,oBAAoB,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/D,8CAA8C;AAC9C,MAAM,UAAU,WAAW;IACzB,OAAO,YAAY,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/dsh/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,oBAAoB,EAAsB,MAAM,sBAAsB,CAAC;AAEhF,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,GAAG,oBAAoB,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE7E,gFAAgF;AAChF,MAAM,UAAU,YAAY;IAC1B,OAAO,YAAY,EAAE,CAAC;AACxB,CAAC;AAED,8CAA8C;AAC9C,MAAM,UAAU,WAAW;IACzB,OAAO,YAAY,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC"}
@@ -0,0 +1,121 @@
1
+ /* global process */
2
+ /**
3
+ * Trellis Dynamic Spec Injection Plugin
4
+ *
5
+ * OpenCode tool hooks cannot add context to the current model turn. A FULL
6
+ * spec emission therefore blocks the write once with a model-visible error;
7
+ * the shared Python engine records the emission, so the model's retry proceeds.
8
+ */
9
+
10
+ import { execFileSync } from "child_process";
11
+ import { existsSync } from "fs";
12
+ import { join } from "path";
13
+ import { debugLog } from "../lib/trellis-context.js";
14
+
15
+ const PYTHON_CMD = process.platform === "win32" ? "python" : "python3";
16
+
17
+ function runSpecHook(directory, payload) {
18
+ const hookPath = join(
19
+ directory,
20
+ ".opencode",
21
+ "hooks",
22
+ "inject-spec-context.py",
23
+ );
24
+ if (!existsSync(hookPath)) return null;
25
+
26
+ try {
27
+ const stdout = execFileSync(PYTHON_CMD, [hookPath], {
28
+ cwd: directory,
29
+ encoding: "utf-8",
30
+ input: JSON.stringify(payload),
31
+ stdio: ["pipe", "pipe", "pipe"],
32
+ timeout: 15000,
33
+ });
34
+ if (!stdout.trim()) return null;
35
+ const parsed = JSON.parse(stdout);
36
+ return parsed?.hookSpecificOutput ?? null;
37
+ } catch (error) {
38
+ debugLog(
39
+ "spec",
40
+ "Spec hook failed open:",
41
+ error instanceof Error ? error.message : String(error),
42
+ );
43
+ return null;
44
+ }
45
+ }
46
+
47
+ function buildToolPayload(directory, input, output) {
48
+ const tool = input?.tool?.toLowerCase();
49
+ const args = output?.args;
50
+ if (!args) return null;
51
+
52
+ if (tool === "edit" || tool === "write") {
53
+ if (typeof args.filePath !== "string" || !args.filePath.trim()) return null;
54
+ return {
55
+ hook_event_name: "PreToolUse",
56
+ session_id: input?.sessionID,
57
+ cwd: directory,
58
+ tool_name: tool === "edit" ? "Edit" : "Write",
59
+ tool_input: { file_path: args.filePath },
60
+ };
61
+ }
62
+
63
+ if (tool === "apply_patch") {
64
+ if (typeof args.patchText !== "string" || !args.patchText.trim())
65
+ return null;
66
+ return {
67
+ hook_event_name: "PreToolUse",
68
+ session_id: input?.sessionID,
69
+ cwd: directory,
70
+ tool_name: "apply_patch",
71
+ tool_input: { command: args.patchText },
72
+ };
73
+ }
74
+
75
+ return null;
76
+ }
77
+
78
+ export default async ({ directory }) => ({
79
+ event: async ({ event }) => {
80
+ if (
81
+ process.env.TRELLIS_HOOKS === "0" ||
82
+ process.env.TRELLIS_DISABLE_HOOKS === "1"
83
+ ) {
84
+ return;
85
+ }
86
+ if (event?.type !== "session.compacted" || !event?.properties?.sessionID) {
87
+ return;
88
+ }
89
+
90
+ runSpecHook(directory, {
91
+ hook_event_name: "SessionStart",
92
+ session_id: event.properties.sessionID,
93
+ source: "compact",
94
+ cwd: directory,
95
+ });
96
+ },
97
+
98
+ "tool.execute.before": async (input, output) => {
99
+ if (
100
+ process.env.TRELLIS_HOOKS === "0" ||
101
+ process.env.TRELLIS_DISABLE_HOOKS === "1"
102
+ ) {
103
+ return;
104
+ }
105
+
106
+ const payload = buildToolPayload(directory, input, output);
107
+ if (!payload) return;
108
+
109
+ const result = runSpecHook(directory, payload);
110
+ const context = result?.additionalContext;
111
+ if (result?.permissionDecision !== "deny" || typeof context !== "string") {
112
+ return;
113
+ }
114
+
115
+ throw new Error(
116
+ "Trellis loaded governing specs before this tool call. " +
117
+ "Follow them, then retry the same tool call.\n\n" +
118
+ context,
119
+ );
120
+ },
121
+ });
@@ -1,4 +1,4 @@
1
- /* global process */
1
+ /* global process, console */
2
2
  /**
3
3
  * Trellis Workflow State Injection Plugin
4
4
  *
@@ -9,7 +9,12 @@
9
9
  * `experimental.chat.messages.transform`. Stored history and the TUI
10
10
  * are not modified (issue #553). Breadcrumb text is pulled exclusively
11
11
  * from the project's workflow.md [workflow-state:STATUS] tag blocks —
12
- * workflow.md is the single source of truth. There are no fallback
12
+ * workflow.md is the single source of truth. When the active task's
13
+ * task.json selects a workflow variant ("workflow": "<id>"), the tag
14
+ * blocks are read from .trellis/workflows/<id>.md instead (missing
15
+ * variant file → one stderr warning + fallback through personal/team/global
16
+ * defaults; see
17
+ * resolveWorkflowMd). There are no fallback
13
18
  * tables in this plugin: when workflow.md is missing or a tag is
14
19
  * absent, the breadcrumb degrades to a generic
15
20
  * "Refer to workflow.md for current step." line so users see (and fix)
@@ -21,7 +26,7 @@
21
26
  * - task.json malformed or missing status
22
27
  */
23
28
 
24
- import { existsSync, readFileSync } from "fs"
29
+ import { existsSync, readFileSync, statSync } from "fs"
25
30
  import { join } from "path"
26
31
  import {
27
32
  MESSAGES_TRANSFORM_HOOK,
@@ -111,17 +116,113 @@ function promptHasSkipKeyword(text, keyword) {
111
116
  return pattern.test(text)
112
117
  }
113
118
 
119
+ // Per-task workflow selection (mirrors the Python resolver in
120
+ // .trellis/scripts/common/workflow_selection.py). Ids are restricted to
121
+ // [A-Za-z0-9_-]+ so a task.json value can never traverse outside
122
+ // .trellis/workflows/.
123
+ const WORKFLOW_ID_RE = /^[A-Za-z0-9_-]+$/
124
+
125
+ // Map a workflow id to its .trellis/workflows/<id>.md file if valid and
126
+ // present, else "". Shared by every resolution layer. Never throws.
127
+ function libraryVariant(directory, workflowId) {
128
+ if (!workflowId || !WORKFLOW_ID_RE.test(workflowId)) return ""
129
+ const variantPath = join(directory, ".trellis", "workflows", `${workflowId}.md`)
130
+ try {
131
+ if (statSync(variantPath).isFile()) return variantPath
132
+ } catch {
133
+ // ENOENT etc. — missing variant file.
134
+ }
135
+ return ""
136
+ }
137
+
138
+ // Personal override id from the gitignored .developer file (`workflow=<id>`
139
+ // line). "" when absent/unreadable. Never throws.
140
+ function developerWorkflowId(directory) {
141
+ try {
142
+ const text = readFileSync(join(directory, ".trellis", ".developer"), "utf-8")
143
+ for (const line of text.split(/\r?\n/)) {
144
+ if (line.startsWith("workflow=")) return line.slice("workflow=".length).trim()
145
+ }
146
+ } catch {
147
+ // No .developer / unreadable — no personal override.
148
+ }
149
+ return ""
150
+ }
151
+
152
+ // Team-shared default id from config.yaml's top-level `default_workflow:` key.
153
+ // Commented lines (starting with '#') are naturally excluded by the anchor.
154
+ // "" when unset/unreadable. Never throws.
155
+ const DEFAULT_WORKFLOW_RE = /^default_workflow:\s*(['"]?)([A-Za-z0-9_-]+)\1\s*(?:#.*)?$/m
156
+ function configDefaultWorkflowId(directory) {
157
+ try {
158
+ const text = readFileSync(join(directory, ".trellis", "config.yaml"), "utf-8")
159
+ const m = text.match(DEFAULT_WORKFLOW_RE)
160
+ if (m) return m[2]
161
+ } catch {
162
+ // No config.yaml / unreadable — no team default.
163
+ }
164
+ return ""
165
+ }
166
+
167
+ /**
168
+ * Resolve which workflow markdown file feeds this turn's breadcrumbs.
169
+ *
170
+ * Precedence (identical across all Trellis consumers), highest to lowest —
171
+ * each layer resolves an id to .trellis/workflows/<id>.md and falls through
172
+ * when unset, invalid, or naming a missing file:
173
+ * 1. Per-task pin — active task's task.json "workflow" (session-bound; a
174
+ * bad id / missing file warns once on stderr, then falls through).
175
+ * 2. Personal — .developer `workflow=<id>` (gitignored, per-developer).
176
+ * 3. Team default — config.yaml `default_workflow` (git-tracked, shared).
177
+ * 4. Global — .trellis/workflow.md.
178
+ * With neither a pin nor the personal/team keys set, this is the global path.
179
+ * Never throws.
180
+ */
181
+ function resolveWorkflowMd(ctx, directory, platformInput) {
182
+ const globalPath = join(directory, ".trellis", "workflow.md")
183
+
184
+ // 1. Per-task pin (session-bound explicit choice).
185
+ let workflowId = ""
186
+ try {
187
+ const active = ctx.getActiveTask(platformInput)
188
+ if (active.taskPath && !active.stale) {
189
+ const taskDir = ctx.resolveTaskDir(active.taskPath)
190
+ if (taskDir) {
191
+ const data = JSON.parse(readFileSync(join(taskDir, "task.json"), "utf-8"))
192
+ if (data && typeof data.workflow === "string") workflowId = data.workflow
193
+ }
194
+ }
195
+ } catch {
196
+ // No active task / unreadable task.json — not a selection.
197
+ workflowId = ""
198
+ }
199
+ if (workflowId) {
200
+ const pinned = libraryVariant(directory, workflowId)
201
+ if (pinned) return pinned
202
+ console.error(
203
+ `Warning: active task selects workflow ${JSON.stringify(workflowId)} but .trellis/workflows/ has no matching file; using default workflow resolution`,
204
+ )
205
+ }
206
+
207
+ // 2. Personal override, then 3. team default — silent on miss (defaults,
208
+ // not an explicit per-task choice). 4. Global fallback.
209
+ const personal = libraryVariant(directory, developerWorkflowId(directory))
210
+ if (personal) return personal
211
+ const team = libraryVariant(directory, configDefaultWorkflowId(directory))
212
+ if (team) return team
213
+ return globalPath
214
+ }
215
+
114
216
  /**
115
- * Parse workflow.md for [workflow-state:STATUS] blocks.
217
+ * Parse the resolved workflow markdown for [workflow-state:STATUS] blocks.
116
218
  *
117
- * Returns {status: body}. workflow.md is the single source of truth —
118
- * there are no fallback tables here. Missing tags (or a missing /
119
- * unreadable workflow.md) fall back to a generic line in
219
+ * Returns {status: body}. The workflow file is the single source of
220
+ * truth — there are no fallback tables here. Missing tags (or a missing /
221
+ * unreadable workflow file) fall back to a generic line in
120
222
  * buildBreadcrumb so users see the broken state and fix workflow.md
121
223
  * rather than the plugin silently masking it.
122
224
  */
123
- function loadBreadcrumbs(directory) {
124
- const workflowPath = join(directory, ".trellis", "workflow.md")
225
+ function loadBreadcrumbs(workflowPath) {
125
226
  if (!existsSync(workflowPath)) return {}
126
227
  let content
127
228
  try {
@@ -228,7 +329,7 @@ export default async ({ directory }) => {
228
329
  return
229
330
  }
230
331
 
231
- const templates = loadBreadcrumbs(directory)
332
+ const templates = loadBreadcrumbs(resolveWorkflowMd(ctx, directory, platformInput))
232
333
  const task = getActiveTask(ctx, platformInput)
233
334
  const breadcrumb = task
234
335
  ? buildBreadcrumb(task.id, task.status, templates, task.candidates)