@@ -7,6 +7,7 @@ import { Effect, Layer, Context, Schema } from "effect"
77import { Config } from "@/config/config"
88import { MCP } from "../mcp"
99import { Skill } from "../skill"
10+ import { Workflow } from "@/workflow/workflow"
1011import { EventV2 } from "@opencode-ai/core/event"
1112import PROMPT_INITIALIZE from "./template/initialize.txt"
1213import PROMPT_REVIEW from "./template/review.txt"
@@ -32,7 +33,7 @@ export const Info = Schema.Struct({
3233 description : Schema . optional ( Schema . String ) ,
3334 agent : Schema . optional ( Schema . String ) ,
3435 model : Schema . optional ( Schema . String ) ,
35- source : Schema . optional ( Schema . Literals ( [ "command" , "mcp" , "skill" ] ) ) ,
36+ source : Schema . optional ( Schema . Literals ( [ "command" , "mcp" , "skill" , "workflow" ] ) ) ,
3637 // Some command templates are lazy promises from MCP prompt resolution.
3738 template : Schema . Unknown ,
3839 subtask : Schema . optional ( Schema . Boolean ) ,
@@ -69,18 +70,41 @@ export const layer = Layer.effect(
6970 const config = yield * Config . Service
7071 const mcp = yield * MCP . Service
7172 const skill = yield * Skill . Service
73+ const workflow = yield * Workflow . Service
7274
7375 const init = Effect . fn ( "Command.state" ) ( function * ( ctx : InstanceContext ) {
7476 const cfg = yield * config . get ( )
7577 const bridge = yield * EffectBridge . make ( )
7678 const commands : Record < string , Info > = { }
7779
80+ // T5 (/init): surface discovered workflows in the generated AGENTS.md prompt
81+ // so the init pass documents them. Names + descriptions (falling back to
82+ // whenToUse). Only VALID workflows are listed; the section is omitted
83+ // entirely when none remain, so a repo with no workflows gets the unchanged
84+ // prompt. list() is the static (never-executed) reader — safe at build time.
85+ const allWorkflows = ( yield * workflow . list ( ) ) . filter ( ( wf ) => wf . valid !== false )
86+ // The init section documents REPO/PROJECT-defined workflows ("This repository
87+ // defines …"); the builtins that ship inside opencode (source_kind "builtin",
88+ // e.g. deep-research) are not repo-specific, so they are excluded from the
89+ // prompt section. They still register as Command.Info entries via the loop below.
90+ const initWorkflows = allWorkflows . filter ( ( wf ) => wf . source_kind !== "builtin" )
91+ const workflowsSection =
92+ initWorkflows . length === 0
93+ ? ""
94+ : "\n\n## Available workflows\n\nThis repository defines OpenCode workflows. Mention them in `AGENTS.md` so future sessions know they exist:\n\n" +
95+ initWorkflows
96+ . map ( ( wf ) => {
97+ const desc = wf . meta . description ?? wf . meta . whenToUse
98+ return desc ? `- \`${ wf . name } \` — ${ desc } ` : `- \`${ wf . name } \``
99+ } )
100+ . join ( "\n" )
101+
78102 commands [ Default . INIT ] = {
79103 name : Default . INIT ,
80104 description : "guided AGENTS.md setup" ,
81105 source : "command" ,
82106 get template ( ) {
83- return PROMPT_INITIALIZE . replace ( "${path}" , ctx . worktree )
107+ return PROMPT_INITIALIZE . replace ( "${path}" , ctx . worktree ) + workflowsSection
84108 } ,
85109 hints : hints ( PROMPT_INITIALIZE ) ,
86110 }
@@ -152,6 +176,33 @@ export const layer = Layer.effect(
152176 }
153177 }
154178
179+ // Spec §5.2 (3) / Delta 4+5: discovered workflows become real Command.Info
180+ // entries (source "workflow") so they appear in /help and Command.list()
181+ // (the TUI's sync.data.command), in parity with the autocomplete /<name>
182+ // path. Runs LAST so any real command/mcp/skill of the same name wins
183+ // (collision skip via `if (commands[wf.name]) continue`); invalid (broken)
184+ // workflow files cannot be started, so they are skipped too.
185+ // DELTA: source "workflow" is DISCOVERY-ONLY — the TUI dispatch starts these
186+ // via the /workflow path (see AUTOCOMPLETE), NOT via session.command, so the
187+ // empty `template` is never executed as a prompt.
188+ // Reuses `allWorkflows` (already filtered to valid, INCLUDING builtins so
189+ // deep-research still registers as a command) instead of a second
190+ // workflow.list() call. The `valid === false` guard is now redundant but kept
191+ // for clarity; the collision skip below still applies.
192+ for ( const wf of allWorkflows ) {
193+ if ( wf . valid === false ) continue
194+ if ( commands [ wf . name ] ) continue
195+ commands [ wf . name ] = {
196+ name : wf . name ,
197+ description : wf . meta . description ?? wf . meta . whenToUse ,
198+ source : "workflow" ,
199+ get template ( ) {
200+ return ""
201+ } ,
202+ hints : [ ] ,
203+ }
204+ }
205+
155206 return {
156207 commands,
157208 }
@@ -177,8 +228,9 @@ export const defaultLayer = layer.pipe(
177228 Layer . provide ( Config . defaultLayer ) ,
178229 Layer . provide ( MCP . defaultLayer ) ,
179230 Layer . provide ( Skill . defaultLayer ) ,
231+ Layer . provide ( Workflow . defaultLayer ) ,
180232)
181233
182- export const node = LayerNode . make ( layer , [ Config . node , MCP . node , Skill . node ] )
234+ export const node = LayerNode . make ( layer , [ Config . node , MCP . node , Skill . node , Workflow . node ] )
183235
184236export * as Command from "."
0 commit comments