Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 13 additions & 16 deletions .dependency-cruiser.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ module.exports = {
name: 'workflow-definition-module-privacy',
severity: 'error',
comment:
'External code must import from workflow-definition/index.ts — domain internals (registry, states, operations) are private',
from: { pathNot: '^src/workflow-definition/' },
to: { path: '^src/workflow-definition/domain/' },
'External code must import from workflow-definition/index.ts — domain and infra internals are private',
from: { pathNot: '^src/(workflow-definition/|shell\\.)' },
to: { path: '^src/workflow-definition/(domain|infra)/' },
},

// Rule 3b: Module privacy — workflow-engine internals are private
Expand All @@ -47,30 +47,27 @@ module.exports = {
to: { path: '^src/workflow-engine/domain/' },
},

// Rule 4: workflow-definition must not import from infra
// Rule 4: workflow-definition domain must not import from workflow-analysis
{
name: 'workflow-definition-no-upward-deps',
name: 'workflow-definition-domain-isolation',
severity: 'error',
comment:
'workflow-definition may depend on workflow-dsl and workflow-engine — not on infra',
from: { path: '^src/workflow-definition/' },
'workflow-definition domain must not depend on workflow-analysis',
from: { path: '^src/workflow-definition/domain/' },
to: {
path: '^src/infra/',
path: '^src/workflow-analysis/',
dependencyTypesNot: ['type-only'],
},
},

// Rule 5: Entrypoint allowed imports — only workflow-definition/, workflow-engine/, and infra/
// Rule 5: workflow-analysis must not import workflow-definition internals (only index.ts)
{
name: 'entrypoint-allowed-imports',
name: 'workflow-analysis-no-definition-internals',
severity: 'error',
comment:
'Entrypoint must import from workflow-definition/, workflow-engine/, or infra/ — never from internal domain/ paths or other modules directly',
from: { path: '^src/autonomous-claude-agent-team-workflow\\.ts$' },
to: {
path: '^src/(?!workflow-definition/|workflow-engine/|infra/)',
pathNot: '^src/autonomous-claude-agent-team-workflow\\.ts$',
},
'workflow-analysis may import workflow-definition/index.ts and workflow-definition/infra/ (shared error types) — not domain internals',
from: { path: '^src/workflow-analysis/' },
to: { path: '^src/workflow-definition/domain/' },
},
],

Expand Down
8 changes: 4 additions & 4 deletions hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"hooks": {
"SessionStart": [{ "hooks": [{ "type": "command", "command": "npx tsx ${CLAUDE_PLUGIN_ROOT}/src/autonomous-claude-agent-team-workflow.ts", "timeout": 30 }] }],
"PreToolUse": [{ "hooks": [{ "type": "command", "command": "npx tsx ${CLAUDE_PLUGIN_ROOT}/src/autonomous-claude-agent-team-workflow.ts", "timeout": 30 }] }],
"SubagentStart": [{ "hooks": [{ "type": "command", "command": "npx tsx ${CLAUDE_PLUGIN_ROOT}/src/autonomous-claude-agent-team-workflow.ts", "timeout": 30 }] }],
"TeammateIdle": [{ "hooks": [{ "type": "command", "command": "npx tsx ${CLAUDE_PLUGIN_ROOT}/src/autonomous-claude-agent-team-workflow.ts", "timeout": 30 }] }]
"SessionStart": [{ "hooks": [{ "type": "command", "command": "npx tsx ${CLAUDE_PLUGIN_ROOT}/src/shell.ts", "timeout": 30 }] }],
"PreToolUse": [{ "hooks": [{ "type": "command", "command": "npx tsx ${CLAUDE_PLUGIN_ROOT}/src/shell.ts", "timeout": 30 }] }],
"SubagentStart": [{ "hooks": [{ "type": "command", "command": "npx tsx ${CLAUDE_PLUGIN_ROOT}/src/shell.ts", "timeout": 30 }] }],
"TeammateIdle": [{ "hooks": [{ "type": "command", "command": "npx tsx ${CLAUDE_PLUGIN_ROOT}/src/shell.ts", "timeout": 30 }] }]
}
}
3 changes: 2 additions & 1 deletion packages/agentic-workflow-builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"./dsl": "./src/dsl/index.ts",
"./engine": "./src/engine/index.ts",
"./event-store": "./src/event-store/index.ts",
"./testing": "./src/testing/index.ts"
"./testing": "./src/testing/index.ts",
"./cli": "./src/cli/index.ts"
},
"scripts": {
"typecheck": "tsc --noEmit",
Expand Down
132 changes: 132 additions & 0 deletions packages/agentic-workflow-builder/src/cli/domain/arg-helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { describe, it, expect } from 'vitest'
import { z } from 'zod'
import { arg } from './arg-helpers.js'

describe('arg.number', () => {
const parser = arg.number('count')

it('parses a valid integer', () => {
const result = parser.parse(['cmd', '42'], 1, 'cmd')
expect(result).toEqual({ ok: true, value: 42 })
})

it('returns error for missing argument', () => {
const result = parser.parse(['cmd'], 1, 'cmd')
expect(result).toEqual({ ok: false, message: 'cmd: missing required argument <count>' })
})

it('returns error for non-numeric value', () => {
const result = parser.parse(['cmd', 'abc'], 1, 'cmd')
expect(result).toEqual({ ok: false, message: "cmd: not a valid number: 'abc'" })
})

it('optional returns undefined for missing argument', () => {
const optional = parser.optional()
const result = optional.parse(['cmd'], 1, 'cmd')
expect(result).toEqual({ ok: true, value: undefined })
})

it('optional parses a valid value', () => {
const optional = parser.optional()
const result = optional.parse(['cmd', '5'], 1, 'cmd')
expect(result).toEqual({ ok: true, value: 5 })
})

it('optional returns error for invalid value', () => {
const optional = parser.optional()
const result = optional.parse(['cmd', 'xyz'], 1, 'cmd')
expect(result).toEqual({ ok: false, message: "cmd: not a valid number: 'xyz'" })
})
})

describe('arg.string', () => {
const parser = arg.string('name')

it('parses a valid string', () => {
const result = parser.parse(['cmd', 'hello'], 1, 'cmd')
expect(result).toEqual({ ok: true, value: 'hello' })
})

it('returns error for missing argument', () => {
const result = parser.parse(['cmd'], 1, 'cmd')
expect(result).toEqual({ ok: false, message: 'cmd: missing required argument <name>' })
})

it('optional returns undefined for missing argument', () => {
const optional = parser.optional()
const result = optional.parse(['cmd'], 1, 'cmd')
expect(result).toEqual({ ok: true, value: undefined })
})

it('optional parses a valid value', () => {
const optional = parser.optional()
const result = optional.parse(['cmd', 'world'], 1, 'cmd')
expect(result).toEqual({ ok: true, value: 'world' })
})
})

describe('arg.state', () => {
const StateSchema = z.enum(['planning', 'coding', 'review'])
const parser = arg.state('state', StateSchema)

it('parses a valid state', () => {
const result = parser.parse(['cmd', 'planning'], 1, 'cmd')
expect(result).toEqual({ ok: true, value: 'planning' })
})

it('returns error for missing argument', () => {
const result = parser.parse(['cmd'], 1, 'cmd')
expect(result).toEqual({ ok: false, message: 'cmd: missing required argument <state>' })
})

it('returns error for invalid state value', () => {
const result = parser.parse(['cmd', 'unknown'], 1, 'cmd')
expect(result).toEqual({ ok: false, message: "cmd: invalid state 'unknown'" })
})

it('optional returns undefined for missing argument', () => {
const optional = parser.optional()
const result = optional.parse(['cmd'], 1, 'cmd')
expect(result).toEqual({ ok: true, value: undefined })
})

it('optional parses a valid state', () => {
const optional = parser.optional()
const result = optional.parse(['cmd', 'coding'], 1, 'cmd')
expect(result).toEqual({ ok: true, value: 'coding' })
})

it('optional returns error for invalid state', () => {
const optional = parser.optional()
const result = optional.parse(['cmd', 'bad'], 1, 'cmd')
expect(result).toEqual({ ok: false, message: "cmd: invalid state 'bad'" })
})
})

describe('arg.rest', () => {
const parser = arg.rest('files')

it('collects all remaining args from position', () => {
const result = parser.parse(['cmd', 'session-1', 'a.ts', 'b.ts'], 2, 'cmd')
expect(result).toEqual({ ok: true, value: ['a.ts', 'b.ts'] })
})

it('returns empty array when no remaining args', () => {
const result = parser.parse(['cmd', 'session-1'], 2, 'cmd')
expect(result).toEqual({ ok: true, value: [] })
})

it('optional rest with no args at position returns undefined', () => {
const optional = parser.optional()
const result = optional.parse(['cmd'], 1, 'cmd')
expect(result).toEqual({ ok: true, value: undefined })
})
})

describe('double optional', () => {
it('calling optional twice still works', () => {
const parser = arg.string('name').optional().optional()
const result = parser.parse(['cmd'], 1, 'cmd')
expect(result).toEqual({ ok: true, value: undefined })
})
})
81 changes: 81 additions & 0 deletions packages/agentic-workflow-builder/src/cli/domain/arg-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { ZodType } from 'zod'

export type ArgResult<T> =
| { readonly ok: true; readonly value: T }
| { readonly ok: false; readonly message: string }

export type ArgParser<T> = {
readonly parse: (args: readonly string[], position: number, commandName: string) => ArgResult<T>
readonly optional: () => ArgParser<T | undefined>
}

function makeOptional<T>(parser: ArgParser<T>): ArgParser<T | undefined> {
return {
parse: (args, position, commandName) => {
const raw = args[position]
if (raw === undefined) {
return { ok: true, value: undefined }
}
return parser.parse(args, position, commandName)
},
optional: () => makeOptional(parser),
}
}

export const arg = {
number: (name: string): ArgParser<number> => ({
parse: (args, position, commandName) => {
const raw = args[position]
if (raw === undefined) {
return { ok: false, message: `${commandName}: missing required argument <${name}>` }
}
const parsed = Number.parseInt(raw, 10)
if (Number.isNaN(parsed)) {
return { ok: false, message: `${commandName}: not a valid number: '${raw}'` }
}
return { ok: true, value: parsed }
},
optional: function () {
return makeOptional(this)
},
}),

string: (name: string): ArgParser<string> => ({
parse: (args, position, commandName) => {
const raw = args[position]
if (raw === undefined) {
return { ok: false, message: `${commandName}: missing required argument <${name}>` }
}
return { ok: true, value: raw }
},
optional: function () {
return makeOptional(this)
},
}),

rest: (_name: string): ArgParser<readonly string[]> => ({
parse: (args, position) => {
return { ok: true, value: args.slice(position) }
},
optional: function () {
return makeOptional(this)
},
}),

state: <T extends string>(name: string, schema: ZodType<T>): ArgParser<T> => ({
parse: (args, position, commandName) => {
const raw = args[position]
if (raw === undefined) {
return { ok: false, message: `${commandName}: missing required argument <${name}>` }
}
const result = schema.safeParse(raw)
if (!result.success) {
return { ok: false, message: `${commandName}: invalid state '${raw}'` }
}
return { ok: true, value: result.data }
},
optional: function () {
return makeOptional(this)
},
}),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, it, expect } from 'vitest'
import { defineRoutes } from './command-definition.js'
import { pass } from '../../dsl/index.js'

describe('defineRoutes', () => {
it('returns the routes map unchanged', () => {
const routes = {
init: {
type: 'session-start' as const,
},
doSomething: {
type: 'transaction' as const,
handler: () => pass(),
},
}
const result = defineRoutes(routes)
expect(result).toBe(routes)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { PreconditionResult } from '../../dsl/index.js'
import type { ArgParser } from './arg-helpers.js'

type RouteHandler<TWorkflow> = (workflow: TWorkflow, ...parsedArgs: readonly unknown[]) => PreconditionResult

type TransactionRoute<TWorkflow> = {
readonly type: 'transaction'
readonly args?: readonly ArgParser<unknown>[]
readonly handler: RouteHandler<TWorkflow>
}

type TransitionRoute = {
readonly type: 'transition'
readonly args?: readonly ArgParser<unknown>[]
}

type SessionStartRoute = {
readonly type: 'session-start'
readonly args?: readonly ArgParser<unknown>[]
}

export type RouteDefinition<TWorkflow, TState> =
| TransactionRoute<TWorkflow>
| TransitionRoute
| SessionStartRoute

export type RouteMap<TWorkflow, TState> = Record<string, RouteDefinition<TWorkflow, TState>>

export function defineRoutes<TWorkflow, TState>(
routes: RouteMap<TWorkflow, TState>,
): RouteMap<TWorkflow, TState> {
return routes
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { describe, it, expect } from 'vitest'
import { EXIT_ALLOW, EXIT_ERROR, EXIT_BLOCK } from './exit-codes.js'

describe('exit-codes', () => {
it('EXIT_ALLOW is 0', () => {
expect(EXIT_ALLOW).toBe(0)
})

it('EXIT_ERROR is 1', () => {
expect(EXIT_ERROR).toBe(1)
})

it('EXIT_BLOCK is 2', () => {
expect(EXIT_BLOCK).toBe(2)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const EXIT_ALLOW = 0
export const EXIT_ERROR = 1
export const EXIT_BLOCK = 2
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, it, expect } from 'vitest'
import { extractField } from './extract-field.js'

describe('extractField', () => {
it('extracts a string field from tool input', () => {
const extract = extractField('command')
expect(extract({ command: 'git status' })).toBe('git status')
})

it('returns empty string when field is missing', () => {
const extract = extractField('command')
expect(extract({})).toBe('')
})

it('returns empty string when field is null', () => {
const extract = extractField('command')
expect(extract({ command: null })).toBe('')
})

it('throws when field is present but not a string', () => {
const extract = extractField('command')
expect(() => extract({ command: 42 })).toThrow("Expected 'command' to be a string, got number")
})
})
Loading