diff --git a/docs/workflow/workflow-schema.json b/docs/workflow/workflow-schema.json deleted file mode 100644 index ff58e3cc..00000000 --- a/docs/workflow/workflow-schema.json +++ /dev/null @@ -1,328 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://anomaly.co/schemas/workflow/v1.0.0/workflow-schema.json", - "title": "Workflow Descriptor", - "description": "Machine-readable schema for stateful work-item workflows. Defines valid state transitions, commands, invariants, and roles for the AMPA engine and human collaborators. See workflow-language.md for the specification.", - "type": "object", - "required": ["version", "metadata", "status", "stage", "invariants", "commands"], - "additionalProperties": false, - "properties": { - "version": { - "type": "string", - "pattern": "^\\d+\\.\\d+\\.\\d+$", - "description": "Semantic version of the workflow spec (e.g., '1.0.0')." - }, - "metadata": { - "$ref": "#/$defs/Metadata" - }, - "status": { - "type": "array", - "items": { "type": "string" }, - "minItems": 1, - "uniqueItems": true, - "description": "Ordered list of allowed coarse lifecycle statuses (e.g., open, in_progress, blocked, closed)." - }, - "stage": { - "type": "array", - "items": { "type": "string" }, - "minItems": 1, - "uniqueItems": true, - "description": "Ordered list of allowed finer-grained phases (e.g., idea, intake_complete, plan_complete, in_progress, in_review, done)." - }, - "states": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/StateTuple" - }, - "description": "Map of friendly alias names to {status, stage} tuples. Aliases can be used in command from/to fields." - }, - "terminal_states": { - "type": "array", - "items": { "type": "string" }, - "uniqueItems": true, - "description": "List of state aliases (or inline tuples) that are terminal — dead-end states that require no outbound transitions. Used by validation to suppress dead-end warnings." - }, - "invariants": { - "type": "array", - "items": { - "$ref": "#/$defs/Invariant" - }, - "description": "List of named invariant definitions that commands reference in pre/post fields." - }, - "commands": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/Command" - }, - "minProperties": 1, - "description": "Map of command definitions keyed by command name. Command names should be imperative verbs (e.g., intake, plan, delegate)." - } - }, - "$defs": { - "Metadata": { - "type": "object", - "required": ["name", "description", "owner", "roles"], - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "description": "Short identifier for this workflow." - }, - "description": { - "type": "string", - "description": "Human-readable description of the workflow's purpose." - }, - "owner": { - "type": "string", - "description": "Team or individual responsible for maintaining this workflow definition." - }, - "links": { - "type": "object", - "additionalProperties": { "type": "string", "format": "uri" }, - "description": "Optional map of related links (e.g., documentation, source repos)." - }, - "roles": { - "type": "array", - "items": { - "$ref": "#/$defs/Role" - }, - "minItems": 1, - "uniqueItems": true, - "description": "List of role definitions used by commands. The executor maps roles to concrete humans or agents." - } - } - }, - "Role": { - "oneOf": [ - { - "type": "string", - "description": "Simple role identifier (e.g., 'Producer')." - }, - { - "type": "object", - "required": ["name"], - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "description": "Role identifier referenced by commands." - }, - "description": { - "type": "string", - "description": "Human-readable description of the role's responsibilities." - }, - "type": { - "type": "string", - "enum": ["human", "agent", "either"], - "description": "Whether this role is typically filled by a human, an AI agent, or either." - } - } - } - ] - }, - "StateTuple": { - "type": "object", - "required": ["status", "stage"], - "additionalProperties": false, - "properties": { - "status": { - "type": "string", - "description": "Must reference a value declared in the top-level status array." - }, - "stage": { - "type": "string", - "description": "Must reference a value declared in the top-level stage array." - } - } - }, - "StateRef": { - "description": "A reference to a state: either a string alias (referencing the states map) or an inline {status, stage} tuple.", - "oneOf": [ - { - "type": "string", - "description": "Alias name defined in the top-level states map." - }, - { - "$ref": "#/$defs/StateTuple" - } - ] - }, - "Invariant": { - "type": "object", - "required": ["name", "description", "when"], - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "description": "Unique identifier for this invariant, referenced by command pre/post lists." - }, - "description": { - "type": "string", - "description": "Human-readable description of what this invariant checks." - }, - "when": { - "oneOf": [ - { - "type": "string", - "enum": ["pre", "post", "both"] - }, - { - "type": "array", - "items": { - "type": "string", - "enum": ["pre", "post"] - }, - "minItems": 1, - "maxItems": 2, - "uniqueItems": true - } - ], - "description": "When this invariant is evaluated: 'pre' (before command), 'post' (after command), 'both', or an array ['pre', 'post']." - }, - "logic": { - "type": "string", - "description": "Machine-checkable rule expression. Keep declarative. Exact expression language is implementation-specific but must be documented alongside the workflow." - } - } - }, - "InputField": { - "type": "object", - "required": ["type"], - "additionalProperties": false, - "properties": { - "type": { - "type": "string", - "enum": ["string", "number", "boolean", "array", "object"], - "description": "Data type of the input field." - }, - "required": { - "type": "boolean", - "default": false, - "description": "Whether this input is required for the command to execute." - }, - "description": { - "type": "string", - "description": "Human-readable description of the input." - }, - "enum": { - "type": "array", - "items": {}, - "description": "Optional list of allowed values." - }, - "default": { - "description": "Optional default value if the input is not provided." - } - } - }, - "Effects": { - "type": "object", - "additionalProperties": true, - "properties": { - "add_tags": { - "type": "array", - "items": { "type": "string" }, - "description": "Tags to add to the work item after the command succeeds." - }, - "remove_tags": { - "type": "array", - "items": { "type": "string" }, - "description": "Tags to remove from the work item after the command succeeds." - }, - "set_assignee": { - "type": "string", - "description": "Assign the work item to this role or agent after the command succeeds." - }, - "set_needs_producer_review": { - "type": "boolean", - "description": "Set the needs-producer-review flag on the work item." - }, - "notifications": { - "type": "array", - "items": { - "type": "object", - "required": ["channel"], - "properties": { - "channel": { - "type": "string", - "description": "Notification channel (e.g., 'discord', 'email', 'webhook')." - }, - "message": { - "type": "string", - "description": "Optional message template." - } - } - }, - "description": "Notifications to emit after the command succeeds." - }, - "audit": { - "type": "object", - "properties": { - "record_prompt_hash": { "type": "boolean" }, - "record_model": { "type": "boolean" }, - "record_response_ids": { "type": "boolean" }, - "record_agent_id": { "type": "boolean" } - }, - "description": "Audit details to record for AI-driven commands." - } - }, - "description": "Optional side effects to assert/emit after successful command execution." - }, - "Command": { - "type": "object", - "required": ["description", "from", "to", "actor"], - "additionalProperties": false, - "properties": { - "description": { - "type": "string", - "description": "Short human-readable summary of what the command does." - }, - "from": { - "type": "array", - "items": { - "$ref": "#/$defs/StateRef" - }, - "minItems": 1, - "description": "List of allowed source state tuples. Each entry is a state alias or an inline {status, stage} tuple." - }, - "to": { - "$ref": "#/$defs/StateRef", - "description": "Target state tuple applied when the command succeeds. Must be a single alias or {status, stage} tuple." - }, - "actor": { - "type": "string", - "description": "Role that executes this command. Must reference a role declared in metadata.roles." - }, - "pre": { - "type": "array", - "items": { "type": "string" }, - "description": "List of invariant names that must pass before the command may run." - }, - "post": { - "type": "array", - "items": { "type": "string" }, - "description": "List of invariant names that must pass after the transition is applied." - }, - "inputs": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/InputField" - }, - "description": "Schema-like object describing required/optional arguments for this command." - }, - "prompt_ref": { - "type": "string", - "description": "Optional path to a versioned prompt template (e.g., 'prompts/delegate.md'). Template variables must correspond to inputs." - }, - "effects": { - "$ref": "#/$defs/Effects", - "description": "Optional additional side effects (tags, notifications, audit records)." - }, - "dispatch_map": { - "type": "object", - "additionalProperties": { "type": "string" }, - "description": "Maps each from-state alias to a shell command template. Templates may use {id} for work item ID substitution." - } - } - } - } -} diff --git a/src/tui/controller.ts b/src/tui/controller.ts index d4a6e941..78184819 100644 --- a/src/tui/controller.ts +++ b/src/tui/controller.ts @@ -4328,11 +4328,12 @@ function updateDetailForIndex(idx: number, visible?: VisibleNode[]) { }); // Close selected item - screen.key(KEY_CLOSE_ITEM, () => { - if (state.moveMode) return; - if (detailModal.hidden && !helpMenu.isVisible() && closeDialog.hidden) { - openCloseDialog(); - } + screen.key(KEY_CLOSE_ITEM, () => { + if (state.moveMode) return; + // Guard: only open close dialog when no overlays/modals are visible and + // we're not inside other dialogs (create/update/next/detail/help). + if (!detailModal.hidden || helpMenu.isVisible() || !closeDialog.hidden || !updateDialog.hidden || !nextDialog.hidden || isCreateDialogOpen()) return; + openCloseDialog(); }); // Update selected item (quick edit) - shortcut U diff --git a/test/tui-integration.test.ts b/test/tui-integration.test.ts index f102cdea..4fedd9c6 100644 --- a/test/tui-integration.test.ts +++ b/test/tui-integration.test.ts @@ -819,4 +819,71 @@ describe('TUI integration: style preservation', () => { clickHandler({ y: 1, x: 1 }); expect(detailModal.show).toHaveBeenCalled(); }); + + it("doesn't open the Close dialog when typing 'x' into create dialog textarea", async () => { + vi.resetModules(); + let savedAction: Function | null = null; + const program: any = { + opts: () => ({ verbose: false }), + command() { return this; }, + description() { return this; }, + option() { return this; }, + action(fn: Function) { savedAction = fn; return this; }, + }; + + const utils = { + requireInitialized: () => {}, + getDatabase: () => ({ + list: () => [{ id: 'WL-TEST-1', title: 'Item', status: 'open' }], + getPrefix: () => 'default', + getCommentsForWorkItem: (_id: string) => [], + get: () => ({ id: 'WL-TEST-1', title: 'Item', status: 'open' }), + }), + }; + + const opencodeClient = { + getStatus: () => ({ status: 'running', port: 9999 }), + startServer: vi.fn().mockResolvedValue(undefined), + stopServer: vi.fn(), + sendPrompt: vi.fn().mockResolvedValue(undefined), + }; + + vi.doMock('../src/tui/opencode-client.js', () => ({ + OpencodeClient: function() { return opencodeClient; }, + })); + + const mod = await import('../src/commands/tui'); + const register = mod.default || mod; + register({ program, utils, blessed: blessedMock } as any); + + await (savedAction as any)({}); + + // Open Create dialog using the registered shortcut (Capital C) + const createHandler = handlers['screen-key:C']; + expect(typeof createHandler).toBe('function'); + // Invoke handler to open the create dialog + createHandler(null, { name: 'C' }); + + // Find the last created textarea (should be the description textarea) + const ta = (blessedMock as any)._lastTextarea; + expect(ta).toBeTruthy(); + + // Focus the textarea to simulate user typing + ta.focus?.(); + + // Ensure Close dialog exists in box calls + const boxMock = (blessedMock as any).box?.mock; + const boxCalls = boxMock?.calls || []; + const closeIndex = boxCalls.findIndex((call: any[]) => call?.[0]?.label === ' Close Work Item '); + const closeDialog = closeIndex >= 0 ? boxMock.results[closeIndex]?.value : null; + expect(closeDialog).toBeTruthy(); + + // Press 'x' (invoke screen-level handler) + const screenCloseHandler = handlers['screen-key:x'] || handlers['screen-key:X']; + expect(typeof screenCloseHandler).toBe('function'); + screenCloseHandler(null, { name: 'x' }); + + // Close dialog should NOT have been shown + expect(closeDialog?.show).not.toHaveBeenCalled(); + }); });