@nt-ai-lab/deterministic-agent-workflow-cli 0.2.0 → 0.3.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.
@@ -55,6 +55,9 @@ export function createWorkflowCli(config) {
55
55
  const result = runner(args, engineDeps, workflowDeps, {
56
56
  readStdin,
57
57
  getSessionId,
58
+ getSessionRepository: () => getRepositoryName(process.cwd()),
59
+ getRepositoryRoot: () => process.cwd(),
60
+ getWorkflowEventsDbPath: () => workflowEventsDbPath,
58
61
  });
59
62
  if (result.output) {
60
63
  processDeps.writeStdout(result.output);
@@ -0,0 +1,14 @@
1
+ import type { StoredEvent } from '@nt-ai-lab/deterministic-agent-workflow-engine';
2
+ import type { ReflectionProcess, StatePeriod } from './reflection-process-types';
3
+ /** @riviere-role domain-service */
4
+ export declare function computeStatePeriods(events: readonly StoredEvent[], currentState: string): ReadonlyArray<StatePeriod>;
5
+ /** @riviere-role domain-service */
6
+ export declare function buildObservedEventTypes(events: readonly StoredEvent[]): ReflectionProcess['workflow']['observedEventTypes'];
7
+ /** @riviere-role domain-service */
8
+ export declare function buildStateDurationSummary(periods: readonly StatePeriod[]): ReflectionProcess['observations']['stateDurations'];
9
+ /** @riviere-role domain-service */
10
+ export declare function buildTransitionSummary(events: readonly StoredEvent[]): ReflectionProcess['observations']['transitions'];
11
+ /** @riviere-role domain-service */
12
+ export declare function buildDenialSummary(events: readonly StoredEvent[]): ReflectionProcess['observations']['denials'];
13
+ /** @riviere-role domain-service */
14
+ export declare function buildToolSummary(transcriptPath: string | undefined, sessionId: string, periods: readonly StatePeriod[]): ReflectionProcess['observations']['tools'];
@@ -0,0 +1,309 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { z } from 'zod';
3
+ import { openSqliteDatabase } from '@nt-ai-lab/deterministic-agent-workflow-event-store';
4
+ const jsonlToolUseSchema = z.object({
5
+ type: z.literal('tool_use'),
6
+ name: z.string(),
7
+ });
8
+ const jsonlAssistantSchema = z.object({
9
+ type: z.literal('assistant'),
10
+ timestamp: z.string().optional(),
11
+ message: z.object({ content: z.array(z.unknown()).optional() }).optional(),
12
+ });
13
+ const opencodeActivityRowSchema = z.object({
14
+ m_time: z.number().nullable(),
15
+ p_time: z.number().nullable(),
16
+ part_data: z.string(),
17
+ });
18
+ const opencodeToolPartSchema = z.object({
19
+ type: z.literal('tool'),
20
+ tool: z.string(),
21
+ });
22
+ function parseTs(raw) {
23
+ if (typeof raw !== 'string')
24
+ return 0;
25
+ const parsed = Date.parse(raw);
26
+ return Number.isNaN(parsed) ? 0 : parsed;
27
+ }
28
+ function safeParseJson(raw) {
29
+ try {
30
+ return JSON.parse(raw);
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ function collectToolUses(content, timestampMs) {
37
+ const results = [];
38
+ for (const block of content) {
39
+ const parsed = jsonlToolUseSchema.safeParse(block);
40
+ if (parsed.success) {
41
+ results.push({
42
+ name: parsed.data.name,
43
+ timestampMs,
44
+ });
45
+ }
46
+ }
47
+ return results;
48
+ }
49
+ function extractToolCallsFromJsonl(path) {
50
+ const raw = readFileSync(path, 'utf8');
51
+ const lines = raw.split('\n').filter((line) => line.trim().length > 0);
52
+ return lines.flatMap((line) => {
53
+ const parsed = jsonlAssistantSchema.safeParse(safeParseJson(line));
54
+ if (!parsed.success)
55
+ return [];
56
+ return collectToolUses(parsed.data.message?.content ?? [], parseTs(parsed.data.timestamp));
57
+ });
58
+ }
59
+ function extractOpencodeToolCall(row) {
60
+ const parsedRow = opencodeActivityRowSchema.safeParse(row);
61
+ if (!parsedRow.success)
62
+ return null;
63
+ const parsedPart = opencodeToolPartSchema.safeParse(safeParseJson(parsedRow.data.part_data));
64
+ if (!parsedPart.success)
65
+ return null;
66
+ return {
67
+ name: parsedPart.data.tool,
68
+ timestampMs: parsedRow.data.p_time ?? parsedRow.data.m_time ?? 0,
69
+ };
70
+ }
71
+ function extractToolCallsFromOpencode(path, sessionId) {
72
+ const db = openSqliteDatabase(path, { readonly: true });
73
+ try {
74
+ const rows = db.prepare(`
75
+ SELECT m.time_created as m_time, p.time_created as p_time, p.data as part_data
76
+ FROM message m
77
+ JOIN part p ON p.message_id = m.id
78
+ WHERE m.session_id = ?
79
+ ORDER BY m.time_created ASC, p.time_created ASC
80
+ `).all(sessionId);
81
+ const calls = [];
82
+ for (const row of rows) {
83
+ const call = extractOpencodeToolCall(row);
84
+ if (call !== null)
85
+ calls.push(call);
86
+ }
87
+ return calls;
88
+ }
89
+ finally {
90
+ db.close();
91
+ }
92
+ }
93
+ function readToolCalls(transcriptPath, sessionId) {
94
+ if (transcriptPath === undefined || transcriptPath.length === 0)
95
+ return [];
96
+ try {
97
+ if (transcriptPath.endsWith('.jsonl'))
98
+ return extractToolCallsFromJsonl(transcriptPath);
99
+ if (transcriptPath.endsWith('.db'))
100
+ return extractToolCallsFromOpencode(transcriptPath, sessionId);
101
+ return [];
102
+ }
103
+ catch {
104
+ return [];
105
+ }
106
+ }
107
+ /** @riviere-role domain-service */
108
+ export function computeStatePeriods(events, currentState) {
109
+ if (events.length === 0)
110
+ return [];
111
+ const firstEvent = events[0];
112
+ const lastEvent = events[events.length - 1];
113
+ const startEvent = events.find((event) => event.envelope.type === 'session-started') ?? firstEvent;
114
+ const startStateRaw = startEvent.payload['currentState'];
115
+ const initialState = typeof startStateRaw === 'string' && startStateRaw.length > 0 ? startStateRaw : currentState;
116
+ const aggregated = events.reduce((accumulator, event) => {
117
+ if (event.envelope.type !== 'transitioned')
118
+ return accumulator;
119
+ const nextState = event.payload['to'];
120
+ if (typeof nextState !== 'string' || nextState.length === 0)
121
+ return accumulator;
122
+ const endedAt = event.envelope.at;
123
+ return {
124
+ state: nextState,
125
+ startedAt: endedAt,
126
+ periods: [...accumulator.periods, {
127
+ state: accumulator.state,
128
+ startedAt: accumulator.startedAt,
129
+ endedAt,
130
+ durationMs: Math.max(parseTs(endedAt) - parseTs(accumulator.startedAt), 0),
131
+ }],
132
+ };
133
+ }, {
134
+ state: initialState,
135
+ startedAt: startEvent.envelope.at,
136
+ periods: [],
137
+ });
138
+ return [...aggregated.periods, {
139
+ state: aggregated.state,
140
+ startedAt: aggregated.startedAt,
141
+ endedAt: lastEvent.envelope.at,
142
+ durationMs: Math.max(parseTs(lastEvent.envelope.at) - parseTs(aggregated.startedAt), 0),
143
+ }];
144
+ }
145
+ /** @riviere-role domain-service */
146
+ export function buildObservedEventTypes(events) {
147
+ const counts = new Map();
148
+ const payloadKeys = new Map();
149
+ for (const event of events) {
150
+ counts.set(event.envelope.type, (counts.get(event.envelope.type) ?? 0) + 1);
151
+ const keys = payloadKeys.get(event.envelope.type) ?? new Set();
152
+ for (const key of Object.keys(event.payload))
153
+ keys.add(key);
154
+ payloadKeys.set(event.envelope.type, keys);
155
+ }
156
+ return [...counts.entries()]
157
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
158
+ .map(([type, count]) => ({
159
+ type,
160
+ count,
161
+ payloadKeys: [...(payloadKeys.get(type) ?? new Set())].sort((a, b) => a.localeCompare(b)),
162
+ }));
163
+ }
164
+ /** @riviere-role domain-service */
165
+ export function buildStateDurationSummary(periods) {
166
+ const totalDurationMs = periods.reduce((sum, period) => sum + period.durationMs, 0);
167
+ const byState = periods.reduce((map, period) => {
168
+ const current = map.get(period.state) ?? {
169
+ durationMs: 0,
170
+ entryCount: 0,
171
+ };
172
+ map.set(period.state, {
173
+ durationMs: current.durationMs + period.durationMs,
174
+ entryCount: current.entryCount + 1,
175
+ });
176
+ return map;
177
+ }, new Map());
178
+ return {
179
+ totalDurationMs,
180
+ states: [...byState.entries()]
181
+ .map(([state, value]) => ({
182
+ state,
183
+ durationMs: value.durationMs,
184
+ percentageOfSession: totalDurationMs === 0 ? 0 : Math.round((value.durationMs / totalDurationMs) * 1000) / 10,
185
+ entryCount: value.entryCount,
186
+ }))
187
+ .sort((a, b) => b.durationMs - a.durationMs || a.state.localeCompare(b.state)),
188
+ };
189
+ }
190
+ /** @riviere-role domain-service */
191
+ export function buildTransitionSummary(events) {
192
+ const transitions = events.flatMap((event) => {
193
+ if (event.envelope.type !== 'transitioned')
194
+ return [];
195
+ const from = event.payload['from'];
196
+ const to = event.payload['to'];
197
+ return typeof from === 'string' && typeof to === 'string'
198
+ ? [{
199
+ from,
200
+ to,
201
+ }]
202
+ : [];
203
+ });
204
+ const counts = transitions.reduce((map, transition) => {
205
+ const key = `${transition.from}\u0000${transition.to}`;
206
+ map.set(key, (map.get(key) ?? 0) + 1);
207
+ return map;
208
+ }, new Map());
209
+ const repeatedPathCounts = transitions.slice(0, -1).reduce((map, transition, index) => {
210
+ const next = transitions.at(index + 1);
211
+ if (next === undefined)
212
+ return map;
213
+ const key = [transition.from, transition.to, next.to].join('\u0000');
214
+ map.set(key, (map.get(key) ?? 0) + 1);
215
+ return map;
216
+ }, new Map());
217
+ return {
218
+ transitions: [...counts.entries()]
219
+ .map(([key, count]) => {
220
+ const parts = key.split('\u0000');
221
+ const from = parts[0];
222
+ const to = parts[1];
223
+ return {
224
+ from: typeof from === 'string' ? from : '',
225
+ to: typeof to === 'string' ? to : '',
226
+ count,
227
+ };
228
+ })
229
+ .sort((a, b) => b.count - a.count || a.from.localeCompare(b.from) || a.to.localeCompare(b.to)),
230
+ repeatedPaths: [...repeatedPathCounts.entries()]
231
+ .filter(([, count]) => count > 1)
232
+ .map(([key, count]) => ({
233
+ path: key.split('\u0000'),
234
+ count,
235
+ }))
236
+ .sort((a, b) => b.count - a.count || a.path.join('>').localeCompare(b.path.join('>'))),
237
+ };
238
+ }
239
+ /** @riviere-role domain-service */
240
+ export function buildDenialSummary(events) {
241
+ const byType = {
242
+ write: 0,
243
+ bash: 0,
244
+ pluginRead: 0,
245
+ idle: 0,
246
+ };
247
+ const byState = new Map();
248
+ const denialTypes = new Map([
249
+ ['write-checked', 'write'],
250
+ ['bash-checked', 'bash'],
251
+ ['plugin-read-checked', 'pluginRead'],
252
+ ['idle-checked', 'idle'],
253
+ ]);
254
+ for (const event of events) {
255
+ const key = denialTypes.get(event.envelope.type);
256
+ if (key === undefined || event.payload['allowed'] !== false)
257
+ continue;
258
+ byType[key] += 1;
259
+ const state = event.envelope.state ?? 'unknown';
260
+ byState.set(state, (byState.get(state) ?? 0) + 1);
261
+ }
262
+ return {
263
+ total: byType.write + byType.bash + byType.pluginRead + byType.idle,
264
+ byType,
265
+ byState: [...byState.entries()]
266
+ .map(([state, count]) => ({
267
+ state,
268
+ count,
269
+ }))
270
+ .sort((a, b) => b.count - a.count || a.state.localeCompare(b.state)),
271
+ };
272
+ }
273
+ function buildCounts(calls, startedAtMs, endedAtMs) {
274
+ return calls.reduce((accumulator, call) => {
275
+ if (call.timestampMs < startedAtMs || call.timestampMs > endedAtMs)
276
+ return accumulator;
277
+ accumulator.toolCounts.set(call.name, (accumulator.toolCounts.get(call.name) ?? 0) + 1);
278
+ return {
279
+ totalToolCalls: accumulator.totalToolCalls + 1,
280
+ toolCounts: accumulator.toolCounts,
281
+ };
282
+ }, {
283
+ totalToolCalls: 0,
284
+ toolCounts: new Map(),
285
+ });
286
+ }
287
+ function bucketToolCallsByState(calls, periods) {
288
+ return periods.map((period) => {
289
+ const counts = buildCounts(calls, parseTs(period.startedAt), parseTs(period.endedAt));
290
+ return {
291
+ state: period.state,
292
+ totalToolCalls: counts.totalToolCalls,
293
+ toolCounts: [...counts.toolCounts.entries()]
294
+ .map(([name, count]) => ({
295
+ name,
296
+ count,
297
+ }))
298
+ .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)),
299
+ };
300
+ });
301
+ }
302
+ /** @riviere-role domain-service */
303
+ export function buildToolSummary(transcriptPath, sessionId, periods) {
304
+ const calls = readToolCalls(transcriptPath, sessionId);
305
+ return {
306
+ usedToolNames: [...new Set(calls.map((call) => call.name))].sort((a, b) => a.localeCompare(b)),
307
+ byState: bucketToolCallsByState(calls, periods),
308
+ };
309
+ }
@@ -0,0 +1,92 @@
1
+ /** @riviere-role value-object */
2
+ export type ReflectionProcess = {
3
+ readonly schemaVersion: 1;
4
+ readonly context: {
5
+ readonly sessionId: string;
6
+ readonly repository?: string;
7
+ readonly repositoryRoot?: string;
8
+ readonly transcriptPath?: string;
9
+ readonly eventStorePath?: string;
10
+ readonly currentState: string;
11
+ };
12
+ readonly discovery: {
13
+ readonly sources: ReadonlyArray<{
14
+ readonly kind: 'repository-root' | 'transcript' | 'event-store';
15
+ readonly path: string;
16
+ readonly sessionId?: string;
17
+ }>;
18
+ };
19
+ readonly workflow: {
20
+ readonly knownStates: ReadonlyArray<string>;
21
+ readonly observedEventTypes: ReadonlyArray<{
22
+ readonly type: string;
23
+ readonly count: number;
24
+ readonly payloadKeys: ReadonlyArray<string>;
25
+ }>;
26
+ };
27
+ readonly observations: {
28
+ readonly stateDurations: {
29
+ readonly totalDurationMs: number;
30
+ readonly states: ReadonlyArray<{
31
+ readonly state: string;
32
+ readonly durationMs: number;
33
+ readonly percentageOfSession: number;
34
+ readonly entryCount: number;
35
+ }>;
36
+ };
37
+ readonly transitions: {
38
+ readonly transitions: ReadonlyArray<{
39
+ readonly from: string;
40
+ readonly to: string;
41
+ readonly count: number;
42
+ }>;
43
+ readonly repeatedPaths: ReadonlyArray<{
44
+ readonly path: ReadonlyArray<string>;
45
+ readonly count: number;
46
+ }>;
47
+ };
48
+ readonly denials: {
49
+ readonly total: number;
50
+ readonly byType: {
51
+ readonly write: number;
52
+ readonly bash: number;
53
+ readonly pluginRead: number;
54
+ readonly idle: number;
55
+ };
56
+ readonly byState: ReadonlyArray<{
57
+ readonly state: string;
58
+ readonly count: number;
59
+ }>;
60
+ };
61
+ readonly tools: {
62
+ readonly usedToolNames: ReadonlyArray<string>;
63
+ readonly byState: ReadonlyArray<{
64
+ readonly state: string;
65
+ readonly totalToolCalls: number;
66
+ readonly toolCounts: ReadonlyArray<{
67
+ readonly name: string;
68
+ readonly count: number;
69
+ }>;
70
+ }>;
71
+ };
72
+ };
73
+ readonly instructions: {
74
+ readonly objective: string;
75
+ readonly questionsToAnswer: ReadonlyArray<string>;
76
+ readonly constraints: ReadonlyArray<string>;
77
+ readonly recommendedSteps: ReadonlyArray<string>;
78
+ };
79
+ readonly output: {
80
+ readonly kind: 'reflection';
81
+ readonly schemaVersion: 1;
82
+ readonly allowedCategories: ReadonlyArray<'state-efficiency' | 'review-rework' | 'quality-gates' | 'tooling' | 'workflow-design'>;
83
+ readonly maxFindings: 10;
84
+ };
85
+ };
86
+ /** @riviere-role value-object */
87
+ export type StatePeriod = {
88
+ readonly state: string;
89
+ readonly startedAt: string;
90
+ readonly endedAt: string;
91
+ readonly durationMs: number;
92
+ };
@@ -0,0 +1,14 @@
1
+ import type { BaseWorkflowState, RehydratableWorkflow, StoredEvent, WorkflowDefinition } from '@nt-ai-lab/deterministic-agent-workflow-engine';
2
+ import type { ReflectionProcess } from './reflection-process-types';
3
+ /** @riviere-role domain-service */
4
+ export declare function buildReflectionProcess<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TDeps, TStateName extends string, TOperation extends string>(input: {
5
+ readonly sessionId: string;
6
+ readonly repository?: string;
7
+ readonly repositoryRoot?: string;
8
+ readonly transcriptPath?: string;
9
+ readonly eventStorePath?: string;
10
+ readonly workflowDefinition: WorkflowDefinition<TWorkflow, TState, TDeps, TStateName, TOperation>;
11
+ readonly events: readonly StoredEvent[];
12
+ }): ReflectionProcess;
13
+ /** @riviere-role domain-service */
14
+ export declare function resolveRepository(events: readonly StoredEvent[]): string | undefined;
@@ -0,0 +1,95 @@
1
+ import { flattenStoredEvent } from '@nt-ai-lab/deterministic-agent-workflow-engine';
2
+ import { buildDenialSummary, buildObservedEventTypes, buildStateDurationSummary, buildToolSummary, buildTransitionSummary, computeStatePeriods, } from './reflection-process-observations.js';
3
+ function computeCurrentState(workflowDefinition, events) {
4
+ return events.reduce((state, event) => workflowDefinition.fold(state, flattenStoredEvent(event)), workflowDefinition.initialState()).currentStateMachineState;
5
+ }
6
+ function buildDiscoverySources(input) {
7
+ return [
8
+ ...(input.repositoryRoot === undefined ? [] : [{
9
+ kind: 'repository-root',
10
+ path: input.repositoryRoot,
11
+ }]),
12
+ ...(input.transcriptPath === undefined || input.transcriptPath === '' ? [] : [{
13
+ kind: 'transcript',
14
+ path: input.transcriptPath,
15
+ }]),
16
+ ...(input.eventStorePath === undefined || input.eventStorePath === '' ? [] : [{
17
+ kind: 'event-store',
18
+ path: input.eventStorePath,
19
+ sessionId: input.sessionId,
20
+ }]),
21
+ ];
22
+ }
23
+ /** @riviere-role domain-service */
24
+ export function buildReflectionProcess(input) {
25
+ const currentState = computeCurrentState(input.workflowDefinition, input.events);
26
+ const periods = computeStatePeriods(input.events, currentState);
27
+ return {
28
+ schemaVersion: 1,
29
+ context: {
30
+ sessionId: input.sessionId,
31
+ ...(input.repository === undefined ? {} : { repository: input.repository }),
32
+ ...(input.repositoryRoot === undefined ? {} : { repositoryRoot: input.repositoryRoot }),
33
+ ...(input.transcriptPath === undefined || input.transcriptPath === '' ? {} : { transcriptPath: input.transcriptPath }),
34
+ ...(input.eventStorePath === undefined || input.eventStorePath === '' ? {} : { eventStorePath: input.eventStorePath }),
35
+ currentState,
36
+ },
37
+ discovery: { sources: buildDiscoverySources(input), },
38
+ workflow: {
39
+ knownStates: Object.keys(input.workflowDefinition.getRegistry()).sort((a, b) => a.localeCompare(b)),
40
+ observedEventTypes: buildObservedEventTypes(input.events),
41
+ },
42
+ observations: {
43
+ stateDurations: buildStateDurationSummary(periods),
44
+ transitions: buildTransitionSummary(input.events),
45
+ denials: buildDenialSummary(input.events),
46
+ tools: buildToolSummary(input.transcriptPath, input.sessionId, periods),
47
+ },
48
+ instructions: {
49
+ objective: 'Produce optimisation opportunities only for this session. Do not restate information already visible in the UI.',
50
+ questionsToAnswer: [
51
+ 'Where was the most time spent, and why?',
52
+ 'Did any state loops indicate rework or late discovery?',
53
+ 'Did a review phase or quality gate discover issues later than it should have?',
54
+ 'Were instructions unclear, tools blocked, or better tools unused?',
55
+ 'What concrete workflow, tooling, or process change would improve the next run?',
56
+ ],
57
+ constraints: [
58
+ 'Do not include what went well.',
59
+ 'Do not summarize the session without an improvement conclusion.',
60
+ 'Do not assume the largest numbers are the most important issues.',
61
+ 'Use raw evidence only to confirm cause and recommendation.',
62
+ 'Every finding must be evidence-backed and actionable.',
63
+ ],
64
+ recommendedSteps: [
65
+ 'Scan the repository to find the project workflow definition and related state files.',
66
+ 'Review the observed event types to understand workflow-specific signals.',
67
+ 'Inspect the event log and transcript for the observations that appear most relevant.',
68
+ 'Record only structured optimisation findings that match the required output schema.',
69
+ ],
70
+ },
71
+ output: {
72
+ kind: 'reflection',
73
+ schemaVersion: 1,
74
+ allowedCategories: [
75
+ 'state-efficiency',
76
+ 'review-rework',
77
+ 'quality-gates',
78
+ 'tooling',
79
+ 'workflow-design',
80
+ ],
81
+ maxFindings: 10,
82
+ },
83
+ };
84
+ }
85
+ /** @riviere-role domain-service */
86
+ export function resolveRepository(events) {
87
+ for (const event of events) {
88
+ if (event.envelope.type !== 'session-started')
89
+ continue;
90
+ const repository = event.payload['repository'];
91
+ if (typeof repository === 'string' && repository.length > 0)
92
+ return repository;
93
+ }
94
+ return undefined;
95
+ }
@@ -0,0 +1,7 @@
1
+ import type { BaseWorkflowState, RehydratableWorkflow, WorkflowEngineDeps } from '@nt-ai-lab/deterministic-agent-workflow-engine';
2
+ import { WorkflowEngine } from '@nt-ai-lab/deterministic-agent-workflow-engine';
3
+ import type { RunnerResult, WorkflowRunnerConfig } from '../../../platform/domain/workflow-runner-types';
4
+ /** @riviere-role cli-entrypoint */
5
+ export declare function handleGetReflectionProcessRoute<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TDeps, TStateName extends string, TOperation extends string>(engine: WorkflowEngine<TWorkflow, TState, TDeps, TStateName, TOperation>, engineDeps: WorkflowEngineDeps, config: WorkflowRunnerConfig<TWorkflow, TState, TDeps, TStateName, TOperation>, args: readonly string[], getSessionId?: () => string, getSessionTranscriptPath?: () => string, getSessionRepository?: () => string | undefined, getRepositoryRoot?: () => string, getWorkflowEventsDbPath?: () => string): RunnerResult;
6
+ /** @riviere-role cli-entrypoint */
7
+ export declare function handleRecordReflectionRoute<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TDeps, TStateName extends string, TOperation extends string>(engine: WorkflowEngine<TWorkflow, TState, TDeps, TStateName, TOperation>, engineDeps: WorkflowEngineDeps, args: readonly string[], readStdin: (() => string) | undefined, getSessionId?: () => string): RunnerResult;
@@ -0,0 +1,114 @@
1
+ import { recordReflectionInputSchema, } from '@nt-ai-lab/deterministic-agent-workflow-engine';
2
+ import { EXIT_ALLOW, EXIT_ERROR, } from '../../../shell/exit-codes.js';
3
+ import { buildReflectionProcess, resolveRepository, } from '../domain/reflection-process.js';
4
+ function jsonResult(value) {
5
+ return {
6
+ output: JSON.stringify(value, null, 2),
7
+ exitCode: EXIT_ALLOW,
8
+ };
9
+ }
10
+ function errorResult(output) {
11
+ return {
12
+ output,
13
+ exitCode: EXIT_ERROR,
14
+ };
15
+ }
16
+ function parseFlagArgs(args) {
17
+ const flags = new Map();
18
+ for (const [index, key] of args.entries()) {
19
+ if (index % 2 === 1)
20
+ continue;
21
+ const value = args[index + 1];
22
+ if (!key.startsWith('--')) {
23
+ return {
24
+ ok: false,
25
+ message: `Invalid flag: ${String(key)}`,
26
+ };
27
+ }
28
+ if (typeof value !== 'string' || value.length === 0) {
29
+ return {
30
+ ok: false,
31
+ message: `Missing value for flag: ${key}`,
32
+ };
33
+ }
34
+ flags.set(key, value);
35
+ }
36
+ return {
37
+ ok: true,
38
+ flags,
39
+ };
40
+ }
41
+ function validateReflectionFlags(flags) {
42
+ for (const key of flags.keys()) {
43
+ if (key !== '--label' && key !== '--agent-name' && key !== '--source-state') {
44
+ return `Unknown flag: ${key}`;
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+ /** @riviere-role cli-entrypoint */
50
+ export function handleGetReflectionProcessRoute(engine, engineDeps, config, args, getSessionId, getSessionTranscriptPath, getSessionRepository, getRepositoryRoot, getWorkflowEventsDbPath) {
51
+ if (args.length > 1) {
52
+ return errorResult('get-reflection-process does not accept arguments');
53
+ }
54
+ if (getSessionId === undefined) {
55
+ return errorResult('get-reflection-process requires an active workflow session');
56
+ }
57
+ const sessionId = getSessionId();
58
+ if (!engine.hasSessionStarted(sessionId)) {
59
+ return errorResult(`Session ${sessionId} has not been started`);
60
+ }
61
+ const events = engineDeps.store.readEvents(sessionId);
62
+ return jsonResult(buildReflectionProcess({
63
+ sessionId,
64
+ repository: getSessionRepository?.() ?? resolveRepository(events),
65
+ repositoryRoot: getRepositoryRoot?.(),
66
+ transcriptPath: getSessionTranscriptPath?.(),
67
+ eventStorePath: getWorkflowEventsDbPath?.(),
68
+ workflowDefinition: config.workflowDefinition,
69
+ events,
70
+ }));
71
+ }
72
+ /** @riviere-role cli-entrypoint */
73
+ export function handleRecordReflectionRoute(engine, engineDeps, args, readStdin, getSessionId) {
74
+ if (getSessionId === undefined) {
75
+ return errorResult('record-reflection requires an active workflow session');
76
+ }
77
+ if (readStdin === undefined) {
78
+ return errorResult('record-reflection requires JSON on stdin');
79
+ }
80
+ const parsedFlags = parseFlagArgs(args.slice(1));
81
+ if (!parsedFlags.ok) {
82
+ return errorResult(parsedFlags.message);
83
+ }
84
+ const flagError = validateReflectionFlags(parsedFlags.flags);
85
+ if (flagError !== null) {
86
+ return errorResult(flagError);
87
+ }
88
+ const sessionId = getSessionId();
89
+ if (!engine.hasSessionStarted(sessionId)) {
90
+ return errorResult(`Session ${sessionId} has not been started`);
91
+ }
92
+ try {
93
+ const reflectionPayload = JSON.parse(readStdin());
94
+ const parsed = recordReflectionInputSchema.safeParse({
95
+ label: parsedFlags.flags.get('--label'),
96
+ agentName: parsedFlags.flags.get('--agent-name'),
97
+ sourceState: parsedFlags.flags.get('--source-state'),
98
+ reflection: reflectionPayload,
99
+ });
100
+ if (!parsed.success) {
101
+ return errorResult(`Invalid reflection payload: ${parsed.error.message}`);
102
+ }
103
+ const stored = engineDeps.store.recordReflection(sessionId, engineDeps.now(), parsed.data);
104
+ return jsonResult({
105
+ ok: true,
106
+ id: stored.id,
107
+ sessionId: stored.sessionId,
108
+ createdAt: stored.createdAt,
109
+ });
110
+ }
111
+ catch (error) {
112
+ return errorResult(`Invalid reflection JSON: ${String(error)}`);
113
+ }
114
+ }
@@ -1,9 +1,10 @@
1
- import { WorkflowEngine } from '@nt-ai-lab/deterministic-agent-workflow-engine';
1
+ import { pass, WorkflowEngine, } from '@nt-ai-lab/deterministic-agent-workflow-engine';
2
2
  import { EXIT_ALLOW, EXIT_BLOCK, EXIT_ERROR } from '../../../shell/exit-codes.js';
3
3
  import { formatContextInjection, formatDenyDecision } from '../../../platform/infra/cli/presentation/hook-output.js';
4
4
  import { hookCommonInputSchema, preToolUseInputSchema, subagentStartInputSchema, teammateIdleInputSchema, } from '../../../platform/infra/external-clients/claude-hooks/hook-schemas.js';
5
5
  import { createPreToolUseHandler } from '../../../platform/domain/pre-tool-use-handler.js';
6
6
  import { getRepositoryName } from '../../../platform/infra/external-clients/git/repository-name.js';
7
+ import { handleGetReflectionProcessRoute, handleRecordReflectionRoute, } from './reflection-routes.js';
7
8
  function resolvePreToolUseHandler(config) {
8
9
  const hasPolicy = config.bashForbidden !== undefined || config.isWriteAllowed !== undefined || config.customGates !== undefined;
9
10
  if (config.preToolUseHandler !== undefined) {
@@ -83,7 +84,7 @@ export function createWorkflowRunner(config) {
83
84
  return (args, engineDeps, workflowDeps, options) => {
84
85
  const engine = new WorkflowEngine(config.workflowDefinition, engineDeps, workflowDeps);
85
86
  if (args.length > 0) {
86
- return handleRoute(engine, config, args, args[0], options?.getSessionId, options?.getSessionTranscriptPath, options?.getSessionRepository);
87
+ return handleRoute(engine, engineDeps, config, args, args[0], options?.readStdin, options?.getSessionId, options?.getSessionTranscriptPath, options?.getSessionRepository, options?.getRepositoryRoot, options?.getWorkflowEventsDbPath);
87
88
  }
88
89
  if (options?.readStdin === undefined)
89
90
  return {
@@ -93,7 +94,43 @@ export function createWorkflowRunner(config) {
93
94
  return handleHook(engine, resolvedHandler, options.readStdin);
94
95
  };
95
96
  }
96
- function handleRoute(engine, config, args, routeName, getSessionId, getSessionTranscriptPath, getSessionRepository) {
97
+ function handleWriteJournalRoute(engine, engineDeps, args, getSessionId) {
98
+ const hasExplicitSessionId = getSessionId === undefined;
99
+ const sessionId = hasExplicitSessionId ? args[1] : getSessionId();
100
+ const agentNameIndex = hasExplicitSessionId ? 2 : 1;
101
+ const contentIndex = hasExplicitSessionId ? 3 : 2;
102
+ const agentName = args[agentNameIndex];
103
+ const content = args.slice(contentIndex).join(' ').trim();
104
+ if (typeof sessionId !== 'string' || typeof agentName !== 'string' || content.length === 0) {
105
+ return {
106
+ output: 'write-journal requires <agent-name> and <content> arguments',
107
+ exitCode: EXIT_ERROR,
108
+ };
109
+ }
110
+ return engineResultToRunnerResult(engine.transaction(sessionId, 'write-journal', (workflow) => {
111
+ workflow.appendEvent({
112
+ type: 'journal-entry',
113
+ at: engineDeps.now(),
114
+ agentName,
115
+ content,
116
+ });
117
+ return pass();
118
+ }));
119
+ }
120
+ function handleGetStateRoute(engine, args, getSessionId) {
121
+ const sessionId = getSessionId === undefined ? args[1] : getSessionId();
122
+ if (typeof sessionId !== 'string' || sessionId.length === 0) {
123
+ return {
124
+ output: 'get-state requires <session-id> argument',
125
+ exitCode: EXIT_ERROR,
126
+ };
127
+ }
128
+ return engineResultToRunnerResult(engine.getState(sessionId));
129
+ }
130
+ function handleRoute(engine, engineDeps, config, args, routeName, readStdin, getSessionId, getSessionTranscriptPath, getSessionRepository, getRepositoryRoot, getWorkflowEventsDbPath) {
131
+ const builtin = resolveBuiltinRoute(engine, engineDeps, config, args, routeName, readStdin, getSessionId, getSessionTranscriptPath, getSessionRepository, getRepositoryRoot, getWorkflowEventsDbPath);
132
+ if (builtin !== undefined)
133
+ return builtin;
97
134
  const routeDef = Object.hasOwn(config.routes, routeName) ? config.routes[routeName] : undefined;
98
135
  if (routeDef === undefined)
99
136
  return {
@@ -127,6 +164,20 @@ function handleRoute(engine, config, args, routeName, getSessionId, getSessionTr
127
164
  return engineResultToRunnerResult(engine.transaction(resolveSessionId(), routeName, (workflow) => routeDef.handler(workflow, ...argsAfterSessionId())));
128
165
  }
129
166
  }
167
+ function resolveBuiltinRoute(engine, engineDeps, config, args, routeName, readStdin, getSessionId, getSessionTranscriptPath, getSessionRepository, getRepositoryRoot, getWorkflowEventsDbPath) {
168
+ switch (routeName) {
169
+ case 'get-state':
170
+ return handleGetStateRoute(engine, args, getSessionId);
171
+ case 'get-reflection-process':
172
+ return handleGetReflectionProcessRoute(engine, engineDeps, config, args, getSessionId, getSessionTranscriptPath, getSessionRepository, getRepositoryRoot, getWorkflowEventsDbPath);
173
+ case 'record-reflection':
174
+ return handleRecordReflectionRoute(engine, engineDeps, args, readStdin, getSessionId);
175
+ case 'write-journal':
176
+ return handleWriteJournalRoute(engine, engineDeps, args, getSessionId);
177
+ default:
178
+ return undefined;
179
+ }
180
+ }
130
181
  function handleHook(engine, resolvedHandler, readStdin) {
131
182
  const stdin = readStdin();
132
183
  const hookInput = JSON.parse(stdin);
@@ -13,6 +13,8 @@ export type RunnerOptions = {
13
13
  readonly getSessionId?: () => string;
14
14
  readonly getSessionTranscriptPath?: () => string;
15
15
  readonly getSessionRepository?: () => string | undefined;
16
+ readonly getRepositoryRoot?: () => string;
17
+ readonly getWorkflowEventsDbPath?: () => string;
16
18
  };
17
19
  /** @riviere-role value-object */
18
20
  export type WorkflowRunnerConfig<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TDeps, TStateName extends string = string, TOperation extends string = string> = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nt-ai-lab/deterministic-agent-workflow-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -11,10 +11,10 @@
11
11
  "dist"
12
12
  ],
13
13
  "dependencies": {
14
- "@nt-ai-lab/deterministic-agent-workflow-engine": "0.2.0",
15
- "@nt-ai-lab/deterministic-agent-workflow-dsl": "0.2.0",
16
- "@nt-ai-lab/deterministic-agent-workflow-event-store": "0.2.0",
17
- "zod": "^3.25.76"
14
+ "zod": "^3.25.76",
15
+ "@nt-ai-lab/deterministic-agent-workflow-engine": "0.3.0",
16
+ "@nt-ai-lab/deterministic-agent-workflow-event-store": "0.3.0",
17
+ "@nt-ai-lab/deterministic-agent-workflow-dsl": "0.3.0"
18
18
  },
19
19
  "publishConfig": {
20
20
  "access": "public"