@nt-ai-lab/deterministic-agent-workflow-cli 0.1.0

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 (33) hide show
  1. package/dist/features/workflow-cli/entrypoint/workflow-cli.d.ts +4 -0
  2. package/dist/features/workflow-cli/entrypoint/workflow-cli.js +81 -0
  3. package/dist/features/workflow-runner/entrypoint/workflow-runner.d.ts +5 -0
  4. package/dist/features/workflow-runner/entrypoint/workflow-runner.js +216 -0
  5. package/dist/index.d.ts +19 -0
  6. package/dist/index.js +11 -0
  7. package/dist/platform/domain/argument-parser-types.d.ts +13 -0
  8. package/dist/platform/domain/argument-parser-types.js +1 -0
  9. package/dist/platform/domain/command-definition.d.ts +26 -0
  10. package/dist/platform/domain/command-definition.js +4 -0
  11. package/dist/platform/domain/extract-field.d.ts +2 -0
  12. package/dist/platform/domain/extract-field.js +13 -0
  13. package/dist/platform/domain/platform-context.d.ts +8 -0
  14. package/dist/platform/domain/platform-context.js +1 -0
  15. package/dist/platform/domain/pre-tool-use-handler.d.ts +21 -0
  16. package/dist/platform/domain/pre-tool-use-handler.js +44 -0
  17. package/dist/platform/domain/workflow-cli-types.d.ts +21 -0
  18. package/dist/platform/domain/workflow-cli-types.js +1 -0
  19. package/dist/platform/domain/workflow-runner-types.d.ts +25 -0
  20. package/dist/platform/domain/workflow-runner-types.js +1 -0
  21. package/dist/platform/infra/cli/input/argument-parser.d.ts +9 -0
  22. package/dist/platform/infra/cli/input/argument-parser.js +93 -0
  23. package/dist/platform/infra/cli/presentation/hook-output.d.ts +4 -0
  24. package/dist/platform/infra/cli/presentation/hook-output.js +14 -0
  25. package/dist/platform/infra/external-clients/claude-hooks/hook-schemas.d.ts +105 -0
  26. package/dist/platform/infra/external-clients/claude-hooks/hook-schemas.js +19 -0
  27. package/dist/platform/infra/external-clients/git/repository-name.d.ts +2 -0
  28. package/dist/platform/infra/external-clients/git/repository-name.js +22 -0
  29. package/dist/platform/infra/external-clients/process/default-process-deps.d.ts +3 -0
  30. package/dist/platform/infra/external-clients/process/default-process-deps.js +15 -0
  31. package/dist/shell/exit-codes.d.ts +3 -0
  32. package/dist/shell/exit-codes.js +3 -0
  33. package/package.json +22 -0
@@ -0,0 +1,4 @@
1
+ import type { BaseWorkflowState, RehydratableWorkflow } from '@nt-ai-lab/deterministic-agent-workflow-engine';
2
+ import type { WorkflowCliConfig } from '../../../platform/domain/workflow-cli-types';
3
+ /** @riviere-role cli-entrypoint */
4
+ export declare function createWorkflowCli<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState, TDeps>(config: WorkflowCliConfig<TWorkflow, TState, TDeps>): void;
@@ -0,0 +1,81 @@
1
+ import { join } from 'node:path';
2
+ import { createWorkflowRunner } from '../../workflow-runner/entrypoint/workflow-runner.js';
3
+ import { getRepositoryName } from '../../../platform/infra/external-clients/git/repository-name.js';
4
+ function buildReadEnvVar(getEnv) {
5
+ return function readEnvVar(name) {
6
+ const value = getEnv(name);
7
+ if (value === undefined || value === '') {
8
+ throw new TypeError(`Missing required environment variable: ${name}`);
9
+ }
10
+ return value;
11
+ };
12
+ }
13
+ /** @riviere-role cli-entrypoint */
14
+ export function createWorkflowCli(config) {
15
+ const { processDeps } = config;
16
+ const readEnvVar = buildReadEnvVar(processDeps.getEnv);
17
+ const pluginRoot = readEnvVar('CLAUDE_PLUGIN_ROOT');
18
+ const getSessionId = () => readEnvVar('CLAUDE_SESSION_ID');
19
+ const configuredWorkflowEventsDbPath = processDeps.getEnv('WORKFLOW_EVENTS_DB');
20
+ const workflowEventsDbPath = configuredWorkflowEventsDbPath !== undefined && configuredWorkflowEventsDbPath !== ''
21
+ ? configuredWorkflowEventsDbPath
22
+ : join(readEnvVar('HOME'), '.workflow-events.db');
23
+ const store = processDeps.buildStore(workflowEventsDbPath);
24
+ const now = () => new Date().toISOString();
25
+ const platformCtx = {
26
+ getPluginRoot: () => pluginRoot,
27
+ now,
28
+ getSessionId,
29
+ store,
30
+ };
31
+ const engineDeps = {
32
+ store,
33
+ getPluginRoot: () => pluginRoot,
34
+ getEnvFilePath: () => join(readEnvVar('HOME'), '.claude', 'claude.env'),
35
+ getRepositoryName: () => getRepositoryName(process.cwd()),
36
+ readFile: processDeps.readFile,
37
+ appendToFile: processDeps.appendToFile,
38
+ now,
39
+ transcriptReader: config.transcriptReader,
40
+ };
41
+ const workflowDeps = config.buildWorkflowDeps(platformCtx);
42
+ const readStdin = () => processDeps.readFile('/dev/stdin');
43
+ const errorLogPath = join(pluginRoot, 'error.log');
44
+ try {
45
+ const args = processDeps.getArgv().slice(2);
46
+ const command = args[0];
47
+ if (args.length > 0 && config.customRouter !== undefined) {
48
+ const custom = config.customRouter(command, args, platformCtx);
49
+ if (custom !== undefined) {
50
+ writeRunnerResult(processDeps, custom);
51
+ return;
52
+ }
53
+ }
54
+ const runner = createWorkflowRunner(config);
55
+ const result = runner(args, engineDeps, workflowDeps, {
56
+ readStdin,
57
+ getSessionId,
58
+ });
59
+ if (result.output) {
60
+ processDeps.writeStdout(result.output);
61
+ }
62
+ processDeps.exit(result.exitCode);
63
+ }
64
+ catch (error) {
65
+ const message = `[${new Date().toISOString()}] ERROR: ${String(error)}\n`;
66
+ processDeps.writeStderr(message);
67
+ try {
68
+ processDeps.appendToFile(errorLogPath, message);
69
+ }
70
+ catch {
71
+ // Ignore write failures to error log.
72
+ }
73
+ processDeps.exit(1);
74
+ }
75
+ }
76
+ function writeRunnerResult(processDeps, result) {
77
+ if (result.output !== '') {
78
+ processDeps.writeStdout(result.output);
79
+ }
80
+ processDeps.exit(result.exitCode);
81
+ }
@@ -0,0 +1,5 @@
1
+ import type { BaseWorkflowState, RehydratableWorkflow, WorkflowEngineDeps } from '@nt-ai-lab/deterministic-agent-workflow-engine';
2
+ import type { RunnerOptions, RunnerResult, WorkflowRunnerConfig } from '../../../platform/domain/workflow-runner-types';
3
+ export type { PreToolUseHandlerFn } from '../../../platform/domain/pre-tool-use-handler';
4
+ /** @riviere-role cli-entrypoint */
5
+ export declare function createWorkflowRunner<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TDeps, TStateName extends string = string, TOperation extends string = string>(config: WorkflowRunnerConfig<TWorkflow, TState, TDeps, TStateName, TOperation>): (args: readonly string[], engineDeps: WorkflowEngineDeps, workflowDeps: TDeps, options?: RunnerOptions) => RunnerResult;
@@ -0,0 +1,216 @@
1
+ import { WorkflowEngine } from '@nt-ai-lab/deterministic-agent-workflow-engine';
2
+ import { EXIT_ALLOW, EXIT_BLOCK, EXIT_ERROR } from '../../../shell/exit-codes.js';
3
+ import { formatContextInjection, formatDenyDecision } from '../../../platform/infra/cli/presentation/hook-output.js';
4
+ import { hookCommonInputSchema, preToolUseInputSchema, subagentStartInputSchema, teammateIdleInputSchema, } from '../../../platform/infra/external-clients/claude-hooks/hook-schemas.js';
5
+ import { createPreToolUseHandler } from '../../../platform/domain/pre-tool-use-handler.js';
6
+ import { getRepositoryName } from '../../../platform/infra/external-clients/git/repository-name.js';
7
+ function resolvePreToolUseHandler(config) {
8
+ const hasPolicy = config.bashForbidden !== undefined || config.isWriteAllowed !== undefined || config.customGates !== undefined;
9
+ if (config.preToolUseHandler !== undefined) {
10
+ if (hasPolicy) {
11
+ throw new TypeError('WorkflowRunnerConfig: preToolUseHandler is mutually exclusive with bashForbidden/isWriteAllowed/customGates. Provide either policy fields (default path) or a custom handler (escape hatch), not both.');
12
+ }
13
+ return config.preToolUseHandler;
14
+ }
15
+ if (config.bashForbidden === undefined && config.isWriteAllowed === undefined) {
16
+ if (config.customGates !== undefined) {
17
+ throw new TypeError('WorkflowRunnerConfig: customGates requires bashForbidden and isWriteAllowed to also be set.');
18
+ }
19
+ return undefined;
20
+ }
21
+ if (config.bashForbidden === undefined || config.isWriteAllowed === undefined) {
22
+ throw new TypeError('WorkflowRunnerConfig: bashForbidden and isWriteAllowed must be provided together.');
23
+ }
24
+ if (config.customGates === undefined) {
25
+ return createPreToolUseHandler({
26
+ bashForbidden: config.bashForbidden,
27
+ isWriteAllowed: config.isWriteAllowed
28
+ });
29
+ }
30
+ return createPreToolUseHandler({
31
+ bashForbidden: config.bashForbidden,
32
+ isWriteAllowed: config.isWriteAllowed,
33
+ customGates: config.customGates,
34
+ });
35
+ }
36
+ function engineResultToRunnerResult(result) {
37
+ switch (result.type) {
38
+ case 'success': return {
39
+ output: result.output,
40
+ exitCode: EXIT_ALLOW
41
+ };
42
+ case 'blocked': return {
43
+ output: result.output,
44
+ exitCode: EXIT_BLOCK
45
+ };
46
+ case 'error': return {
47
+ output: result.output,
48
+ exitCode: EXIT_ERROR
49
+ };
50
+ }
51
+ }
52
+ function parseArgs(argParsers, args, routeName) {
53
+ const values = [];
54
+ for (const [index, parser] of (argParsers ?? []).entries()) {
55
+ const result = parser.parse(args, index + 1, routeName);
56
+ if (!result.ok)
57
+ return {
58
+ ok: false,
59
+ message: result.message
60
+ };
61
+ values.push(result.value);
62
+ }
63
+ return {
64
+ ok: true,
65
+ values
66
+ };
67
+ }
68
+ function assertSessionId(values) {
69
+ const id = values[0];
70
+ if (typeof id !== 'string')
71
+ throw new TypeError('session-id argument must be a string');
72
+ return id;
73
+ }
74
+ function assertTarget(values) {
75
+ const target = values[1];
76
+ if (typeof target !== 'string')
77
+ throw new TypeError('target argument must be a string');
78
+ return target;
79
+ }
80
+ /** @riviere-role cli-entrypoint */
81
+ export function createWorkflowRunner(config) {
82
+ const resolvedHandler = resolvePreToolUseHandler(config);
83
+ return (args, engineDeps, workflowDeps, options) => {
84
+ const engine = new WorkflowEngine(config.workflowDefinition, engineDeps, workflowDeps);
85
+ if (args.length > 0) {
86
+ return handleRoute(engine, config, args, args[0], options?.getSessionId, options?.getSessionTranscriptPath, options?.getSessionRepository);
87
+ }
88
+ if (options?.readStdin === undefined)
89
+ return {
90
+ output: 'No command and no stdin available',
91
+ exitCode: EXIT_ERROR
92
+ };
93
+ return handleHook(engine, resolvedHandler, options.readStdin);
94
+ };
95
+ }
96
+ function handleRoute(engine, config, args, routeName, getSessionId, getSessionTranscriptPath, getSessionRepository) {
97
+ const routeDef = Object.hasOwn(config.routes, routeName) ? config.routes[routeName] : undefined;
98
+ if (routeDef === undefined)
99
+ return {
100
+ output: `Unknown command: ${routeName}`,
101
+ exitCode: EXIT_ERROR
102
+ };
103
+ const parsedArgs = parseArgs(routeDef.args, args, routeName);
104
+ if (!parsedArgs.ok)
105
+ return {
106
+ output: parsedArgs.message,
107
+ exitCode: EXIT_ERROR
108
+ };
109
+ const resolveSessionId = () => getSessionId === undefined ? assertSessionId(parsedArgs.values) : getSessionId();
110
+ const argsAfterSessionId = () => getSessionId === undefined ? parsedArgs.values.slice(1) : parsedArgs.values;
111
+ const resolveTarget = () => {
112
+ if (getSessionId === undefined)
113
+ return assertTarget(parsedArgs.values);
114
+ const target = parsedArgs.values[0];
115
+ if (typeof target !== 'string')
116
+ throw new TypeError('target argument must be a string');
117
+ return target;
118
+ };
119
+ switch (routeDef.type) {
120
+ case 'session-start': {
121
+ const transcriptPath = getSessionTranscriptPath === undefined ? '' : getSessionTranscriptPath();
122
+ return engineResultToRunnerResult(engine.startSession(resolveSessionId(), transcriptPath, getSessionRepository?.()));
123
+ }
124
+ case 'transition':
125
+ return engineResultToRunnerResult(engine.transition(resolveSessionId(), config.workflowDefinition.stateSchema.parse(resolveTarget())));
126
+ case 'transaction':
127
+ return engineResultToRunnerResult(engine.transaction(resolveSessionId(), routeName, (workflow) => routeDef.handler(workflow, ...argsAfterSessionId())));
128
+ }
129
+ }
130
+ function handleHook(engine, resolvedHandler, readStdin) {
131
+ const stdin = readStdin();
132
+ const hookInput = JSON.parse(stdin);
133
+ const commonParse = hookCommonInputSchema.safeParse(hookInput);
134
+ if (!commonParse.success)
135
+ return {
136
+ output: `Invalid hook input: ${commonParse.error.message}`,
137
+ exitCode: EXIT_ERROR
138
+ };
139
+ const common = commonParse.data;
140
+ if (common.hook_event_name === 'SessionStart') {
141
+ const result = engine.startSession(common.session_id, common.transcript_path, getRepositoryName(common.cwd));
142
+ engine.persistSessionId(common.session_id);
143
+ return engineResultToRunnerResult(result);
144
+ }
145
+ if (!engine.hasSession(common.session_id))
146
+ return {
147
+ output: '',
148
+ exitCode: EXIT_ALLOW
149
+ };
150
+ switch (common.hook_event_name) {
151
+ case 'PreToolUse': return handlePreToolUseHook(engine, resolvedHandler, stdin);
152
+ case 'SubagentStart': return handleSubagentStartHook(engine, stdin);
153
+ case 'TeammateIdle': return handleTeammateIdleHook(engine, stdin);
154
+ default: return {
155
+ output: '',
156
+ exitCode: EXIT_ALLOW
157
+ };
158
+ }
159
+ }
160
+ function handlePreToolUseHook(engine, resolvedHandler, stdin) {
161
+ const hookInput = JSON.parse(stdin);
162
+ const toolParse = preToolUseInputSchema.safeParse(hookInput);
163
+ if (!toolParse.success)
164
+ return {
165
+ output: `Invalid pre-tool-use input: ${toolParse.error.message}`,
166
+ exitCode: EXIT_ERROR
167
+ };
168
+ return handlePreToolUse(engine, resolvedHandler, toolParse.data);
169
+ }
170
+ function handlePreToolUse(engine, resolvedHandler, input) {
171
+ if (resolvedHandler === undefined)
172
+ return {
173
+ output: '',
174
+ exitCode: EXIT_ALLOW
175
+ };
176
+ const result = resolvedHandler(engine, input.session_id, input.tool_name, input.tool_input);
177
+ if (result.type === 'blocked')
178
+ return {
179
+ output: formatDenyDecision(result.output),
180
+ exitCode: EXIT_BLOCK
181
+ };
182
+ return engineResultToRunnerResult(result);
183
+ }
184
+ function handleSubagentStartHook(engine, stdin) {
185
+ const hookInput = JSON.parse(stdin);
186
+ const parsed = subagentStartInputSchema.safeParse(hookInput);
187
+ if (!parsed.success)
188
+ return {
189
+ output: `Invalid subagent-start input: ${parsed.error.message}`,
190
+ exitCode: EXIT_ERROR
191
+ };
192
+ const input = parsed.data;
193
+ const result = engine.transaction(input.session_id, 'register-agent', (workflow) => workflow.registerAgent(input.agent_type, input.agent_id));
194
+ return {
195
+ output: formatContextInjection(result.type === 'success' ? result.output : ''),
196
+ exitCode: EXIT_ALLOW
197
+ };
198
+ }
199
+ function handleTeammateIdleHook(engine, stdin) {
200
+ const hookInput = JSON.parse(stdin);
201
+ const parsed = teammateIdleInputSchema.safeParse(hookInput);
202
+ if (!parsed.success)
203
+ return {
204
+ output: `Invalid teammate-idle input: ${parsed.error.message}`,
205
+ exitCode: EXIT_ERROR
206
+ };
207
+ const input = parsed.data;
208
+ const agentName = resolveTeammateName(input.teammate_name);
209
+ return engineResultToRunnerResult(engine.transaction(input.session_id, 'check-idle', (workflow) => workflow.handleTeammateIdle(agentName)));
210
+ }
211
+ function resolveTeammateName(teammateName) {
212
+ if (teammateName === undefined) {
213
+ return '';
214
+ }
215
+ return teammateName;
216
+ }
@@ -0,0 +1,19 @@
1
+ export { arg } from './platform/infra/cli/input/argument-parser';
2
+ export type { ArgParser, ArgResult, } from './platform/domain/argument-parser-types';
3
+ export { extractField } from './platform/domain/extract-field';
4
+ export { defineRoutes } from './platform/domain/command-definition';
5
+ export type { RouteDefinition, RouteMap, } from './platform/domain/command-definition';
6
+ export { createWorkflowRunner } from './features/workflow-runner/entrypoint/workflow-runner';
7
+ export type { RunnerResult, RunnerOptions, WorkflowRunnerConfig, } from './platform/domain/workflow-runner-types';
8
+ export type { PreToolUseHandlerFn } from './platform/domain/pre-tool-use-handler';
9
+ export { createPreToolUseHandler } from './platform/domain/pre-tool-use-handler';
10
+ export type { PreToolUseHandlerConfig, CustomPreToolUseGate, } from './platform/domain/pre-tool-use-handler';
11
+ export { formatContextInjection, formatDenyDecision, } from './platform/infra/cli/presentation/hook-output';
12
+ export { createWorkflowCli } from './features/workflow-cli/entrypoint/workflow-cli';
13
+ export type { ProcessDeps, WorkflowCliConfig, } from './platform/domain/workflow-cli-types';
14
+ export { createDefaultProcessDeps } from './platform/infra/external-clients/process/default-process-deps';
15
+ export type { PlatformContext } from './platform/domain/platform-context';
16
+ export { getRepositoryName } from './platform/infra/external-clients/git/repository-name';
17
+ export { EXIT_ALLOW, EXIT_BLOCK, EXIT_ERROR, } from './shell/exit-codes';
18
+ export { hookCommonInputSchema, preToolUseInputSchema, subagentStartInputSchema, teammateIdleInputSchema, } from './platform/infra/external-clients/claude-hooks/hook-schemas';
19
+ export type { PreToolUseInput, SubagentStartInput, TeammateIdleInput, } from './platform/infra/external-clients/claude-hooks/hook-schemas';
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { arg } from './platform/infra/cli/input/argument-parser.js';
2
+ export { extractField } from './platform/domain/extract-field.js';
3
+ export { defineRoutes } from './platform/domain/command-definition.js';
4
+ export { createWorkflowRunner } from './features/workflow-runner/entrypoint/workflow-runner.js';
5
+ export { createPreToolUseHandler } from './platform/domain/pre-tool-use-handler.js';
6
+ export { formatContextInjection, formatDenyDecision, } from './platform/infra/cli/presentation/hook-output.js';
7
+ export { createWorkflowCli } from './features/workflow-cli/entrypoint/workflow-cli.js';
8
+ export { createDefaultProcessDeps } from './platform/infra/external-clients/process/default-process-deps.js';
9
+ export { getRepositoryName } from './platform/infra/external-clients/git/repository-name.js';
10
+ export { EXIT_ALLOW, EXIT_BLOCK, EXIT_ERROR, } from './shell/exit-codes.js';
11
+ export { hookCommonInputSchema, preToolUseInputSchema, subagentStartInputSchema, teammateIdleInputSchema, } from './platform/infra/external-clients/claude-hooks/hook-schemas.js';
@@ -0,0 +1,13 @@
1
+ /** @riviere-role value-object */
2
+ export type ArgResult<T> = {
3
+ readonly ok: true;
4
+ readonly value: T;
5
+ } | {
6
+ readonly ok: false;
7
+ readonly message: string;
8
+ };
9
+ /** @riviere-role value-object */
10
+ export type ArgParser<T> = {
11
+ readonly parse: (args: readonly string[], position: number, commandName: string) => ArgResult<T>;
12
+ readonly optional: () => ArgParser<T | undefined>;
13
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import type { PreconditionResult } from '@nt-ai-lab/deterministic-agent-workflow-dsl';
2
+ import type { ArgParser } from './argument-parser-types';
3
+ type RouteHandler<TWorkflow> = (workflow: TWorkflow, ...parsedArgs: readonly unknown[]) => PreconditionResult;
4
+ type TransactionRoute<TWorkflow> = {
5
+ readonly type: 'transaction';
6
+ readonly args?: readonly ArgParser<unknown>[];
7
+ readonly handler: RouteHandler<TWorkflow>;
8
+ };
9
+ type TransitionRoute = {
10
+ readonly type: 'transition';
11
+ readonly args?: readonly ArgParser<unknown>[];
12
+ };
13
+ type SessionStartRoute = {
14
+ readonly type: 'session-start';
15
+ readonly args?: readonly ArgParser<unknown>[];
16
+ };
17
+ type RouteStateMarker<TState> = {
18
+ readonly __stateBrand?: TState;
19
+ };
20
+ /** @riviere-role value-object */
21
+ export type RouteDefinition<TWorkflow, TState> = (TransactionRoute<TWorkflow> | TransitionRoute | SessionStartRoute) & RouteStateMarker<TState>;
22
+ /** @riviere-role value-object */
23
+ export type RouteMap<TWorkflow, TState> = Record<string, RouteDefinition<TWorkflow, TState>>;
24
+ /** @riviere-role domain-service */
25
+ export declare function defineRoutes<TWorkflow, TState>(routes: RouteMap<TWorkflow, TState>): RouteMap<TWorkflow, TState>;
26
+ export {};
@@ -0,0 +1,4 @@
1
+ /** @riviere-role domain-service */
2
+ export function defineRoutes(routes) {
3
+ return routes;
4
+ }
@@ -0,0 +1,2 @@
1
+ /** @riviere-role domain-service */
2
+ export declare function extractField(fieldName: string): (toolInput: Record<string, unknown>) => string;
@@ -0,0 +1,13 @@
1
+ /** @riviere-role domain-service */
2
+ export function extractField(fieldName) {
3
+ return (toolInput) => {
4
+ const value = toolInput[fieldName];
5
+ if (value === undefined || value === null) {
6
+ return '';
7
+ }
8
+ if (typeof value !== 'string') {
9
+ throw new TypeError(`Expected '${fieldName}' to be a string, got ${typeof value}`);
10
+ }
11
+ return value;
12
+ };
13
+ }
@@ -0,0 +1,8 @@
1
+ import type { WorkflowEventStore } from '@nt-ai-lab/deterministic-agent-workflow-engine';
2
+ /** @riviere-role value-object */
3
+ export type PlatformContext = {
4
+ readonly getPluginRoot: () => string;
5
+ readonly now: () => string;
6
+ readonly getSessionId: () => string;
7
+ readonly store: WorkflowEventStore;
8
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import type { BaseWorkflowState, EngineResult, RehydratableWorkflow, WorkflowEngine } from '@nt-ai-lab/deterministic-agent-workflow-engine';
2
+ import type { BashForbiddenConfig } from '@nt-ai-lab/deterministic-agent-workflow-dsl';
3
+ /** @riviere-role value-object */
4
+ export type PreToolUseHandlerFn<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TDeps, TStateName extends string = string, TOperation extends string = string> = (engine: WorkflowEngine<TWorkflow, TState, TDeps, TStateName, TOperation>, sessionId: string, toolName: string, toolInput: Record<string, unknown>) => EngineResult;
5
+ /** @riviere-role value-object */
6
+ export type CustomPreToolUseGate<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TStateName extends string = string> = {
7
+ readonly name: string;
8
+ readonly check: (workflow: TWorkflow, ctx: {
9
+ readonly toolName: string;
10
+ readonly filePath: string;
11
+ readonly command: string;
12
+ }) => true | string;
13
+ };
14
+ /** @riviere-role value-object */
15
+ export type PreToolUseHandlerConfig<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TStateName extends string = string> = {
16
+ readonly bashForbidden: BashForbiddenConfig;
17
+ readonly isWriteAllowed: (filePath: string, state: TState) => boolean;
18
+ readonly customGates?: readonly CustomPreToolUseGate<TWorkflow, TState, TStateName>[];
19
+ };
20
+ /** @riviere-role domain-service */
21
+ export declare function createPreToolUseHandler<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TDeps, TStateName extends string = string, TOperation extends string = string>(config: PreToolUseHandlerConfig<TWorkflow, TState, TStateName>): PreToolUseHandlerFn<TWorkflow, TState, TDeps, TStateName, TOperation>;
@@ -0,0 +1,44 @@
1
+ /** @riviere-role domain-service */
2
+ export function createPreToolUseHandler(config) {
3
+ return (engine, sessionId, toolName, toolInput) => {
4
+ const filePath = extractFilePath(toolInput);
5
+ const command = extractCommand(toolInput);
6
+ const ctx = {
7
+ toolName,
8
+ filePath,
9
+ command
10
+ };
11
+ for (const gate of config.customGates ?? []) {
12
+ const result = engine.transaction(sessionId, `hook:${gate.name}`, (workflow) => {
13
+ const check = gate.check(workflow, ctx);
14
+ if (check === true)
15
+ return { pass: true };
16
+ return {
17
+ pass: false,
18
+ reason: check
19
+ };
20
+ });
21
+ if (result.type === 'blocked')
22
+ return result;
23
+ }
24
+ const writeCheck = engine.checkWrite(sessionId, toolName, filePath, config.isWriteAllowed);
25
+ if (writeCheck.type === 'blocked')
26
+ return writeCheck;
27
+ return engine.checkBash(sessionId, toolName, command, config.bashForbidden);
28
+ };
29
+ }
30
+ function extractFilePath(toolInput) {
31
+ return resolveStringField(toolInput['file_path'])
32
+ || resolveStringField(toolInput['path'])
33
+ || resolveStringField(toolInput['pattern']);
34
+ }
35
+ function extractCommand(toolInput) {
36
+ return resolveStringField(toolInput['command']);
37
+ }
38
+ function resolveStringField(value) {
39
+ if (value === undefined || value === null)
40
+ return '';
41
+ if (typeof value === 'string')
42
+ return value;
43
+ throw new TypeError(`Expected string or undefined in tool_input field. Got ${typeof value}: ${String(value)}`);
44
+ }
@@ -0,0 +1,21 @@
1
+ import type { BaseWorkflowState, RehydratableWorkflow, TranscriptReader, WorkflowEventStore } from '@nt-ai-lab/deterministic-agent-workflow-engine';
2
+ import type { PlatformContext } from './platform-context';
3
+ import type { RunnerResult, WorkflowRunnerConfig } from './workflow-runner-types';
4
+ /** @riviere-role value-object */
5
+ export type ProcessDeps = {
6
+ readonly getEnv: (name: string) => string | undefined;
7
+ readonly exit: (code: number) => void;
8
+ readonly writeStdout: (s: string) => void;
9
+ readonly writeStderr: (s: string) => void;
10
+ readonly getArgv: () => readonly string[];
11
+ readonly readFile: (path: string) => string;
12
+ readonly appendToFile: (path: string, content: string) => void;
13
+ readonly buildStore: (dbPath: string) => WorkflowEventStore;
14
+ };
15
+ /** @riviere-role value-object */
16
+ export type WorkflowCliConfig<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState, TDeps> = WorkflowRunnerConfig<TWorkflow, TState, TDeps> & {
17
+ readonly buildWorkflowDeps: (platform: PlatformContext) => TDeps;
18
+ readonly customRouter?: (command: string, args: readonly string[], platform: PlatformContext) => RunnerResult | undefined;
19
+ readonly processDeps: ProcessDeps;
20
+ readonly transcriptReader: TranscriptReader;
21
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import type { BaseWorkflowState, RehydratableWorkflow, WorkflowDefinition } from '@nt-ai-lab/deterministic-agent-workflow-engine';
2
+ import type { BashForbiddenConfig } from '@nt-ai-lab/deterministic-agent-workflow-dsl';
3
+ import type { RouteMap } from './command-definition';
4
+ import type { CustomPreToolUseGate, PreToolUseHandlerFn } from './pre-tool-use-handler';
5
+ /** @riviere-role value-object */
6
+ export type RunnerResult = {
7
+ readonly output: string;
8
+ readonly exitCode: number;
9
+ };
10
+ /** @riviere-role value-object */
11
+ export type RunnerOptions = {
12
+ readonly readStdin?: () => string;
13
+ readonly getSessionId?: () => string;
14
+ readonly getSessionTranscriptPath?: () => string;
15
+ readonly getSessionRepository?: () => string | undefined;
16
+ };
17
+ /** @riviere-role value-object */
18
+ export type WorkflowRunnerConfig<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TDeps, TStateName extends string = string, TOperation extends string = string> = {
19
+ readonly workflowDefinition: WorkflowDefinition<TWorkflow, TState, TDeps, TStateName, TOperation>;
20
+ readonly routes: RouteMap<TWorkflow, TState>;
21
+ readonly bashForbidden?: BashForbiddenConfig;
22
+ readonly isWriteAllowed?: (filePath: string, state: TState) => boolean;
23
+ readonly customGates?: readonly CustomPreToolUseGate<TWorkflow, TState, TStateName>[];
24
+ readonly preToolUseHandler?: PreToolUseHandlerFn<TWorkflow, TState, TDeps, TStateName, TOperation>;
25
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { ZodType } from 'zod';
2
+ import type { ArgParser } from '../../../domain/argument-parser-types';
3
+ /** @riviere-role cli-input-validator */
4
+ export declare const arg: {
5
+ number: (name: string) => ArgParser<number>;
6
+ string: (name: string) => ArgParser<string>;
7
+ rest: () => ArgParser<readonly string[]>;
8
+ state: <T extends string>(name: string, schema: ZodType<T>) => ArgParser<T>;
9
+ };
@@ -0,0 +1,93 @@
1
+ function makeOptional(parser) {
2
+ return {
3
+ parse: (args, position, commandName) => {
4
+ if (position >= args.length) {
5
+ return {
6
+ ok: true,
7
+ value: undefined
8
+ };
9
+ }
10
+ return parser.parse(args, position, commandName);
11
+ },
12
+ optional: () => makeOptional(parser),
13
+ };
14
+ }
15
+ /** @riviere-role cli-input-validator */
16
+ export const arg = {
17
+ number: (name) => ({
18
+ parse: (args, position, commandName) => {
19
+ if (position >= args.length) {
20
+ return {
21
+ ok: false,
22
+ message: `${commandName}: missing required argument <${name}>`
23
+ };
24
+ }
25
+ const raw = args[position];
26
+ const parsed = Number.parseInt(raw, 10);
27
+ if (Number.isNaN(parsed)) {
28
+ return {
29
+ ok: false,
30
+ message: `${commandName}: not a valid number: '${raw}'`
31
+ };
32
+ }
33
+ return {
34
+ ok: true,
35
+ value: parsed
36
+ };
37
+ },
38
+ optional: function () {
39
+ return makeOptional(this);
40
+ },
41
+ }),
42
+ string: (name) => ({
43
+ parse: (args, position, commandName) => {
44
+ if (position >= args.length) {
45
+ return {
46
+ ok: false,
47
+ message: `${commandName}: missing required argument <${name}>`
48
+ };
49
+ }
50
+ return {
51
+ ok: true,
52
+ value: args[position]
53
+ };
54
+ },
55
+ optional: function () {
56
+ return makeOptional(this);
57
+ },
58
+ }),
59
+ rest: () => ({
60
+ parse: (args, position) => ({
61
+ ok: true,
62
+ value: args.slice(position)
63
+ }),
64
+ optional: function () {
65
+ return makeOptional(this);
66
+ },
67
+ }),
68
+ state: (name, schema) => ({
69
+ parse: (args, position, commandName) => {
70
+ if (position >= args.length) {
71
+ return {
72
+ ok: false,
73
+ message: `${commandName}: missing required argument <${name}>`
74
+ };
75
+ }
76
+ const raw = args[position];
77
+ const result = schema.safeParse(raw);
78
+ if (!result.success) {
79
+ return {
80
+ ok: false,
81
+ message: `${commandName}: invalid state '${raw}'`
82
+ };
83
+ }
84
+ return {
85
+ ok: true,
86
+ value: result.data
87
+ };
88
+ },
89
+ optional: function () {
90
+ return makeOptional(this);
91
+ },
92
+ }),
93
+ };
@@ -0,0 +1,4 @@
1
+ /** @riviere-role cli-output-formatter */
2
+ export declare function formatDenyDecision(reason: string): string;
3
+ /** @riviere-role cli-output-formatter */
4
+ export declare function formatContextInjection(context: string): string;
@@ -0,0 +1,14 @@
1
+ /** @riviere-role cli-output-formatter */
2
+ export function formatDenyDecision(reason) {
3
+ return JSON.stringify({
4
+ hookSpecificOutput: {
5
+ hookEventName: 'PreToolUse',
6
+ permissionDecision: 'deny',
7
+ permissionDecisionReason: reason,
8
+ },
9
+ });
10
+ }
11
+ /** @riviere-role cli-output-formatter */
12
+ export function formatContextInjection(context) {
13
+ return JSON.stringify({ additionalContext: context });
14
+ }
@@ -0,0 +1,105 @@
1
+ import { z } from 'zod';
2
+ declare const hookCommonInputSchema: z.ZodObject<{
3
+ session_id: z.ZodString;
4
+ transcript_path: z.ZodString;
5
+ cwd: z.ZodString;
6
+ permission_mode: z.ZodOptional<z.ZodString>;
7
+ hook_event_name: z.ZodString;
8
+ }, "strip", z.ZodTypeAny, {
9
+ session_id: string;
10
+ transcript_path: string;
11
+ cwd: string;
12
+ hook_event_name: string;
13
+ permission_mode?: string | undefined;
14
+ }, {
15
+ session_id: string;
16
+ transcript_path: string;
17
+ cwd: string;
18
+ hook_event_name: string;
19
+ permission_mode?: string | undefined;
20
+ }>;
21
+ declare const preToolUseInputSchema: z.ZodObject<{
22
+ session_id: z.ZodString;
23
+ transcript_path: z.ZodString;
24
+ cwd: z.ZodString;
25
+ permission_mode: z.ZodOptional<z.ZodString>;
26
+ hook_event_name: z.ZodString;
27
+ } & {
28
+ tool_name: z.ZodString;
29
+ tool_input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
30
+ tool_use_id: z.ZodString;
31
+ }, "strip", z.ZodTypeAny, {
32
+ session_id: string;
33
+ transcript_path: string;
34
+ cwd: string;
35
+ hook_event_name: string;
36
+ tool_name: string;
37
+ tool_input: Record<string, unknown>;
38
+ tool_use_id: string;
39
+ permission_mode?: string | undefined;
40
+ }, {
41
+ session_id: string;
42
+ transcript_path: string;
43
+ cwd: string;
44
+ hook_event_name: string;
45
+ tool_name: string;
46
+ tool_input: Record<string, unknown>;
47
+ tool_use_id: string;
48
+ permission_mode?: string | undefined;
49
+ }>;
50
+ declare const subagentStartInputSchema: z.ZodObject<{
51
+ session_id: z.ZodString;
52
+ transcript_path: z.ZodString;
53
+ cwd: z.ZodString;
54
+ permission_mode: z.ZodOptional<z.ZodString>;
55
+ hook_event_name: z.ZodString;
56
+ } & {
57
+ agent_id: z.ZodString;
58
+ agent_type: z.ZodString;
59
+ }, "strip", z.ZodTypeAny, {
60
+ session_id: string;
61
+ transcript_path: string;
62
+ cwd: string;
63
+ hook_event_name: string;
64
+ agent_id: string;
65
+ agent_type: string;
66
+ permission_mode?: string | undefined;
67
+ }, {
68
+ session_id: string;
69
+ transcript_path: string;
70
+ cwd: string;
71
+ hook_event_name: string;
72
+ agent_id: string;
73
+ agent_type: string;
74
+ permission_mode?: string | undefined;
75
+ }>;
76
+ declare const teammateIdleInputSchema: z.ZodObject<{
77
+ session_id: z.ZodString;
78
+ transcript_path: z.ZodString;
79
+ cwd: z.ZodString;
80
+ permission_mode: z.ZodOptional<z.ZodString>;
81
+ hook_event_name: z.ZodString;
82
+ } & {
83
+ teammate_name: z.ZodOptional<z.ZodString>;
84
+ }, "strip", z.ZodTypeAny, {
85
+ session_id: string;
86
+ transcript_path: string;
87
+ cwd: string;
88
+ hook_event_name: string;
89
+ permission_mode?: string | undefined;
90
+ teammate_name?: string | undefined;
91
+ }, {
92
+ session_id: string;
93
+ transcript_path: string;
94
+ cwd: string;
95
+ hook_event_name: string;
96
+ permission_mode?: string | undefined;
97
+ teammate_name?: string | undefined;
98
+ }>;
99
+ /** @riviere-role external-client-model */
100
+ export type PreToolUseInput = z.infer<typeof preToolUseInputSchema>;
101
+ /** @riviere-role external-client-model */
102
+ export type SubagentStartInput = z.infer<typeof subagentStartInputSchema>;
103
+ /** @riviere-role external-client-model */
104
+ export type TeammateIdleInput = z.infer<typeof teammateIdleInputSchema>;
105
+ export { hookCommonInputSchema, preToolUseInputSchema, subagentStartInputSchema, teammateIdleInputSchema, };
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod';
2
+ const hookCommonInputSchema = z.object({
3
+ session_id: z.string(),
4
+ transcript_path: z.string(),
5
+ cwd: z.string(),
6
+ permission_mode: z.string().optional(),
7
+ hook_event_name: z.string(),
8
+ });
9
+ const preToolUseInputSchema = hookCommonInputSchema.extend({
10
+ tool_name: z.string(),
11
+ tool_input: z.record(z.unknown()),
12
+ tool_use_id: z.string(),
13
+ });
14
+ const subagentStartInputSchema = hookCommonInputSchema.extend({
15
+ agent_id: z.string(),
16
+ agent_type: z.string(),
17
+ });
18
+ const teammateIdleInputSchema = hookCommonInputSchema.extend({ teammate_name: z.string().optional(), });
19
+ export { hookCommonInputSchema, preToolUseInputSchema, subagentStartInputSchema, teammateIdleInputSchema, };
@@ -0,0 +1,2 @@
1
+ /** @riviere-role external-client-service */
2
+ export declare function getRepositoryName(cwd: string): string | undefined;
@@ -0,0 +1,22 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ const httpsRemotePattern = /github\.com\/([^/]+\/[^/.]+?)(?:\.git)?$/;
3
+ const sshRemotePattern = /github\.com:([^/]+\/[^/.]+?)(?:\.git)?$/;
4
+ /** @riviere-role external-client-service */
5
+ export function getRepositoryName(cwd) {
6
+ try {
7
+ const url = execFileSync('/usr/bin/git', ['remote', 'get-url', 'origin'], {
8
+ encoding: 'utf-8',
9
+ cwd,
10
+ }).trim();
11
+ const httpsMatch = httpsRemotePattern.exec(url);
12
+ if (httpsMatch?.[1] !== undefined)
13
+ return httpsMatch[1];
14
+ const sshMatch = sshRemotePattern.exec(url);
15
+ if (sshMatch?.[1] !== undefined)
16
+ return sshMatch[1];
17
+ return undefined;
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ }
@@ -0,0 +1,3 @@
1
+ import type { ProcessDeps } from '../../../domain/workflow-cli-types';
2
+ /** @riviere-role external-client-service */
3
+ export declare function createDefaultProcessDeps(): ProcessDeps;
@@ -0,0 +1,15 @@
1
+ import { appendFileSync, readFileSync } from 'node:fs';
2
+ import { createStore } from '@nt-ai-lab/deterministic-agent-workflow-event-store';
3
+ /** @riviere-role external-client-service */
4
+ export function createDefaultProcessDeps() {
5
+ return {
6
+ getEnv: (name) => process.env[name],
7
+ exit: (code) => process.exit(code),
8
+ writeStdout: (value) => { process.stdout.write(value); },
9
+ writeStderr: (value) => { process.stderr.write(value); },
10
+ getArgv: () => process.argv,
11
+ readFile: (path) => readFileSync(path, 'utf8'),
12
+ appendToFile: (path, content) => appendFileSync(path, content),
13
+ buildStore: (dbPath) => createStore(dbPath),
14
+ };
15
+ }
@@ -0,0 +1,3 @@
1
+ export declare const EXIT_ALLOW = 0;
2
+ export declare const EXIT_ERROR = 1;
3
+ export declare const EXIT_BLOCK = 2;
@@ -0,0 +1,3 @@
1
+ export const EXIT_ALLOW = 0;
2
+ export const EXIT_ERROR = 1;
3
+ export const EXIT_BLOCK = 2;
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@nt-ai-lab/deterministic-agent-workflow-cli",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./dist/index.js"
8
+ },
9
+ "types": "./dist/index.d.ts",
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "dependencies": {
14
+ "@nt-ai-lab/deterministic-agent-workflow-engine": "workspace:*",
15
+ "@nt-ai-lab/deterministic-agent-workflow-dsl": "workspace:*",
16
+ "@nt-ai-lab/deterministic-agent-workflow-event-store": "workspace:*",
17
+ "zod": "^3.25.76"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ }
22
+ }