@nt-ai-lab/deterministic-agent-workflow-engine 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.
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/platform/domain/reflection-types.d.ts +1349 -0
- package/dist/platform/domain/reflection-types.js +72 -0
- package/dist/platform/domain/stored-event.d.ts +28 -0
- package/dist/platform/domain/stored-event.js +22 -0
- package/dist/platform/domain/workflow-engine-types.d.ts +6 -2
- package/dist/platform/domain/workflow-engine.d.ts +4 -1
- package/dist/platform/domain/workflow-engine.js +62 -48
- package/dist/platform/domain/workflow-state-serialization.d.ts +3 -0
- package/dist/platform/domain/workflow-state-serialization.js +16 -0
- package/dist/platform/infra/cli/presentation/output-guidance.d.ts +2 -0
- package/dist/platform/infra/cli/presentation/output-guidance.js +17 -2
- package/package.json +1 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const reflectionCategorySchema = z.enum([
|
|
3
|
+
'state-efficiency',
|
|
4
|
+
'review-rework',
|
|
5
|
+
'quality-gates',
|
|
6
|
+
'tooling',
|
|
7
|
+
'workflow-design',
|
|
8
|
+
]);
|
|
9
|
+
export const reflectionConfidenceSchema = z.enum(['low', 'medium', 'high']);
|
|
10
|
+
const evidenceBaseSchema = z.object({ label: z.string().min(1).optional(), });
|
|
11
|
+
export const reflectionEvidenceSchema = z.discriminatedUnion('kind', [
|
|
12
|
+
evidenceBaseSchema.extend({
|
|
13
|
+
kind: z.literal('state-period'),
|
|
14
|
+
state: z.string().min(1),
|
|
15
|
+
startedAt: z.string().min(1).optional(),
|
|
16
|
+
endedAt: z.string().min(1).optional(),
|
|
17
|
+
}),
|
|
18
|
+
evidenceBaseSchema.extend({
|
|
19
|
+
kind: z.literal('event'),
|
|
20
|
+
seq: z.number().int().positive(),
|
|
21
|
+
}),
|
|
22
|
+
evidenceBaseSchema.extend({
|
|
23
|
+
kind: z.literal('event-range'),
|
|
24
|
+
startSeq: z.number().int().positive(),
|
|
25
|
+
endSeq: z.number().int().positive(),
|
|
26
|
+
}),
|
|
27
|
+
evidenceBaseSchema.extend({
|
|
28
|
+
kind: z.literal('journal-entry'),
|
|
29
|
+
at: z.string().min(1),
|
|
30
|
+
agentName: z.string().min(1).optional(),
|
|
31
|
+
}),
|
|
32
|
+
evidenceBaseSchema.extend({
|
|
33
|
+
kind: z.literal('transcript-range'),
|
|
34
|
+
startIndex: z.number().int().nonnegative(),
|
|
35
|
+
endIndex: z.number().int().nonnegative(),
|
|
36
|
+
}),
|
|
37
|
+
evidenceBaseSchema.extend({
|
|
38
|
+
kind: z.literal('tool-activity'),
|
|
39
|
+
state: z.string().min(1).optional(),
|
|
40
|
+
toolName: z.string().min(1).optional(),
|
|
41
|
+
metric: z.string().min(1).optional(),
|
|
42
|
+
}),
|
|
43
|
+
]);
|
|
44
|
+
export const reflectionFindingSchema = z.object({
|
|
45
|
+
title: z.string().min(1),
|
|
46
|
+
category: reflectionCategorySchema,
|
|
47
|
+
opportunity: z.string().min(1),
|
|
48
|
+
likelyCause: z.string().min(1),
|
|
49
|
+
suggestedChange: z.string().min(1),
|
|
50
|
+
expectedImpact: z.string().min(1),
|
|
51
|
+
confidence: reflectionConfidenceSchema.optional(),
|
|
52
|
+
evidence: z.array(reflectionEvidenceSchema).min(1),
|
|
53
|
+
});
|
|
54
|
+
export const reflectionPayloadSchema = z.object({
|
|
55
|
+
summary: z.string().min(1).optional(),
|
|
56
|
+
findings: z.array(reflectionFindingSchema).max(10),
|
|
57
|
+
}).strict();
|
|
58
|
+
export const recordReflectionInputSchema = z.object({
|
|
59
|
+
label: z.string().min(1).optional(),
|
|
60
|
+
agentName: z.string().min(1).optional(),
|
|
61
|
+
sourceState: z.string().min(1).optional(),
|
|
62
|
+
reflection: reflectionPayloadSchema,
|
|
63
|
+
}).strict();
|
|
64
|
+
export const storedReflectionSchema = z.object({
|
|
65
|
+
id: z.number().int().positive(),
|
|
66
|
+
sessionId: z.string().min(1),
|
|
67
|
+
createdAt: z.string().min(1),
|
|
68
|
+
label: z.string().min(1).optional(),
|
|
69
|
+
agentName: z.string().min(1).optional(),
|
|
70
|
+
sourceState: z.string().min(1).optional(),
|
|
71
|
+
reflection: reflectionPayloadSchema,
|
|
72
|
+
}).strict();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { BaseEvent } from './base-event';
|
|
2
|
+
/** @riviere-role value-object */
|
|
3
|
+
export interface EventEnvelope {
|
|
4
|
+
readonly type: string;
|
|
5
|
+
readonly at: string;
|
|
6
|
+
readonly state: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Platform wire / storage shape for events.
|
|
10
|
+
*
|
|
11
|
+
* Envelope fields are stamped by the platform at persist time (see
|
|
12
|
+
* `WorkflowEngine.persistEvents`). Workflow authors never construct
|
|
13
|
+
* `StoredEvent` directly — they call `appendEvent(BaseEvent)` with flat
|
|
14
|
+
* domain events. Only the engine and direct event-store readers (e.g. the
|
|
15
|
+
* control-center UI) observe this shape.
|
|
16
|
+
*
|
|
17
|
+
* @riviere-role value-object
|
|
18
|
+
*/
|
|
19
|
+
export interface StoredEvent {
|
|
20
|
+
readonly envelope: EventEnvelope;
|
|
21
|
+
readonly payload: Readonly<Record<string, unknown>>;
|
|
22
|
+
}
|
|
23
|
+
/** @riviere-role domain-service */
|
|
24
|
+
export declare function flattenStoredEvent(stored: StoredEvent): BaseEvent;
|
|
25
|
+
/** @riviere-role domain-service */
|
|
26
|
+
export declare function toPayload(event: BaseEvent): Record<string, unknown>;
|
|
27
|
+
/** @riviere-role domain-service */
|
|
28
|
+
export declare function stripEnvelopeKeys(record: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** @riviere-role domain-service */
|
|
2
|
+
export function flattenStoredEvent(stored) {
|
|
3
|
+
return {
|
|
4
|
+
...stripEnvelopeKeys(stored.payload),
|
|
5
|
+
type: stored.envelope.type,
|
|
6
|
+
at: stored.envelope.at,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/** @riviere-role domain-service */
|
|
10
|
+
export function toPayload(event) {
|
|
11
|
+
return stripEnvelopeKeys(event);
|
|
12
|
+
}
|
|
13
|
+
/** @riviere-role domain-service */
|
|
14
|
+
export function stripEnvelopeKeys(record) {
|
|
15
|
+
const result = {};
|
|
16
|
+
for (const [key, value] of Object.entries(record)) {
|
|
17
|
+
if (key === 'type' || key === 'at')
|
|
18
|
+
continue;
|
|
19
|
+
result[key] = value;
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ZodType } from 'zod';
|
|
2
2
|
import type { BaseEvent } from './base-event';
|
|
3
|
+
import type { RecordReflectionInput, StoredReflection } from './reflection-types';
|
|
4
|
+
import type { StoredEvent } from './stored-event';
|
|
3
5
|
import type { PreconditionResult } from './precondition-result';
|
|
4
6
|
import type { TranscriptReader } from '../infra/external-clients/transcript/transcript-reader';
|
|
5
7
|
import type { BaseWorkflowState } from './workflow-state';
|
|
@@ -39,10 +41,12 @@ export interface WorkflowDefinition<TWorkflow extends RehydratableWorkflow<TStat
|
|
|
39
41
|
}
|
|
40
42
|
/** @riviere-role value-object */
|
|
41
43
|
export interface WorkflowEventStore {
|
|
42
|
-
readEvents(sessionId: string): readonly
|
|
43
|
-
appendEvents(sessionId: string, events: readonly
|
|
44
|
+
readEvents(sessionId: string): readonly StoredEvent[];
|
|
45
|
+
appendEvents(sessionId: string, events: readonly StoredEvent[]): void;
|
|
44
46
|
sessionExists(sessionId: string): boolean;
|
|
45
47
|
hasSessionStarted(sessionId: string): boolean;
|
|
48
|
+
recordReflection(sessionId: string, createdAt: string, input: RecordReflectionInput): StoredReflection;
|
|
49
|
+
listReflections(sessionId: string): readonly StoredReflection[];
|
|
46
50
|
}
|
|
47
51
|
/** @riviere-role value-object */
|
|
48
52
|
export type WorkflowEngineDeps = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PreconditionResult } from './precondition-result';
|
|
2
2
|
import type { BashForbiddenConfig } from './workflow-registry';
|
|
3
3
|
import type { EngineResult, RehydratableWorkflow, WorkflowDefinition, WorkflowEngineDeps } from './workflow-engine-types';
|
|
4
|
-
import type
|
|
4
|
+
import { type BaseWorkflowState } from './workflow-state';
|
|
5
5
|
/** @riviere-role domain-service */
|
|
6
6
|
export declare class WorkflowEngine<TWorkflow extends RehydratableWorkflow<TState>, TState extends BaseWorkflowState<TStateName>, TDeps, TStateName extends string = string, TOperation extends string = string> {
|
|
7
7
|
private readonly factory;
|
|
@@ -13,11 +13,14 @@ export declare class WorkflowEngine<TWorkflow extends RehydratableWorkflow<TStat
|
|
|
13
13
|
transition(sessionId: string, target: TStateName): EngineResult;
|
|
14
14
|
checkBash(sessionId: string, toolName: string, command: string, bashForbidden: BashForbiddenConfig): EngineResult;
|
|
15
15
|
checkWrite(sessionId: string, toolName: string, filePath: string, isWriteAllowed: (filePath: string, state: TState) => boolean): EngineResult;
|
|
16
|
+
getState(sessionId: string): EngineResult;
|
|
16
17
|
persistSessionId(sessionId: string): void;
|
|
17
18
|
hasSession(sessionId: string): boolean;
|
|
18
19
|
hasSessionStarted(sessionId: string): boolean;
|
|
19
20
|
private requireSession;
|
|
20
21
|
private rehydrateFromEvents;
|
|
21
22
|
private persistEvents;
|
|
23
|
+
private wrapEvents;
|
|
24
|
+
private applyIdentityGate;
|
|
22
25
|
private verifyIdentity;
|
|
23
26
|
}
|
|
@@ -2,7 +2,9 @@ import { checkBashCommand } from './bash-enforcement.js';
|
|
|
2
2
|
import { checkIdentity } from './identity-verification.js';
|
|
3
3
|
import { buildPrefixPattern, buildProcedurePath, enrichSessionStartedEvents, getExpectedPrefix, readProcedure, } from './workflow-engine-support.js';
|
|
4
4
|
import { formatIllegalTransitionError, formatInitSuccess, formatOperationGateError, formatOperationSuccess, formatTransitionError, formatTransitionSuccess, } from '../infra/cli/presentation/output-guidance.js';
|
|
5
|
-
import {
|
|
5
|
+
import { flattenStoredEvent, toPayload, } from './stored-event.js';
|
|
6
|
+
import { WorkflowStateError, } from './workflow-state.js';
|
|
7
|
+
import { serializeWorkflowState } from './workflow-state-serialization.js';
|
|
6
8
|
/** @riviere-role domain-service */
|
|
7
9
|
export class WorkflowEngine {
|
|
8
10
|
factory;
|
|
@@ -27,7 +29,7 @@ export class WorkflowEngine {
|
|
|
27
29
|
const registry = this.factory.getRegistry();
|
|
28
30
|
const stateNames = Object.keys(registry);
|
|
29
31
|
const pendingEvents = enrichSessionStartedEvents(this.engineDeps, workflow.getPendingEvents(), transcriptPath, resolvedRepository, initialState.currentStateMachineState, stateNames);
|
|
30
|
-
this.engineDeps.store.appendEvents(sessionId, pendingEvents);
|
|
32
|
+
this.engineDeps.store.appendEvents(sessionId, this.wrapEvents(pendingEvents, initialState));
|
|
31
33
|
const procedureContent = this.engineDeps.readFile(buildProcedurePath(this.engineDeps, initialState.currentStateMachineState));
|
|
32
34
|
const expectedPrefix = getExpectedPrefix(initialState.currentStateMachineState, registry);
|
|
33
35
|
return {
|
|
@@ -39,15 +41,9 @@ export class WorkflowEngine {
|
|
|
39
41
|
this.requireSession(sessionId);
|
|
40
42
|
const workflow = this.rehydrateFromEvents(sessionId);
|
|
41
43
|
const registry = this.factory.getRegistry();
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
const currentPrefix = getExpectedPrefix(workflow.getState().currentStateMachineState, registry);
|
|
46
|
-
return {
|
|
47
|
-
type: 'blocked',
|
|
48
|
-
output: formatOperationGateError(op, identityResult, currentPrefix)
|
|
49
|
-
};
|
|
50
|
-
}
|
|
44
|
+
const gate = this.applyIdentityGate(sessionId, workflow, op);
|
|
45
|
+
if (gate !== undefined)
|
|
46
|
+
return gate;
|
|
51
47
|
const result = fn(workflow);
|
|
52
48
|
this.persistEvents(sessionId, workflow);
|
|
53
49
|
const currentPrefix = getExpectedPrefix(workflow.getState().currentStateMachineState, registry);
|
|
@@ -69,15 +65,9 @@ export class WorkflowEngine {
|
|
|
69
65
|
const state = workflow.getState();
|
|
70
66
|
const currentStateName = state.currentStateMachineState;
|
|
71
67
|
const registry = this.factory.getRegistry();
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
const currentPrefix = getExpectedPrefix(currentStateName, registry);
|
|
76
|
-
return {
|
|
77
|
-
type: 'blocked',
|
|
78
|
-
output: formatOperationGateError('transition', identityResult, currentPrefix)
|
|
79
|
-
};
|
|
80
|
-
}
|
|
68
|
+
const gate = this.applyIdentityGate(sessionId, workflow, 'transition');
|
|
69
|
+
if (gate !== undefined)
|
|
70
|
+
return gate;
|
|
81
71
|
const currentDef = registry[currentStateName];
|
|
82
72
|
if (!currentDef.canTransitionTo.includes(target)) {
|
|
83
73
|
const legalTargets = currentDef.canTransitionTo;
|
|
@@ -132,14 +122,9 @@ export class WorkflowEngine {
|
|
|
132
122
|
const registry = this.factory.getRegistry();
|
|
133
123
|
const currentStateName = workflow.getState().currentStateMachineState;
|
|
134
124
|
const currentPrefix = getExpectedPrefix(currentStateName, registry);
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
type: 'blocked',
|
|
140
|
-
output: formatOperationGateError('bash-check', identityResult, currentPrefix)
|
|
141
|
-
};
|
|
142
|
-
}
|
|
125
|
+
const gate = this.applyIdentityGate(sessionId, workflow, 'bash-check');
|
|
126
|
+
if (gate !== undefined)
|
|
127
|
+
return gate;
|
|
143
128
|
if (toolName !== 'Bash') {
|
|
144
129
|
workflow.appendEvent({
|
|
145
130
|
type: 'bash-checked',
|
|
@@ -155,9 +140,9 @@ export class WorkflowEngine {
|
|
|
155
140
|
};
|
|
156
141
|
}
|
|
157
142
|
const exemptions = registry[currentStateName].allowForbidden?.bash ?? [];
|
|
158
|
-
const
|
|
159
|
-
if (!
|
|
160
|
-
const reason = `Bash command blocked in ${currentStateName}. ${
|
|
143
|
+
const bashCheckResult = checkBashCommand(command, bashForbidden, exemptions);
|
|
144
|
+
if (!bashCheckResult.pass) {
|
|
145
|
+
const reason = `Bash command blocked in ${currentStateName}. ${bashCheckResult.reason}`;
|
|
161
146
|
workflow.appendEvent({
|
|
162
147
|
type: 'bash-checked',
|
|
163
148
|
at: this.engineDeps.now(),
|
|
@@ -191,14 +176,9 @@ export class WorkflowEngine {
|
|
|
191
176
|
const registry = this.factory.getRegistry();
|
|
192
177
|
const currentStateName = workflow.getState().currentStateMachineState;
|
|
193
178
|
const currentPrefix = getExpectedPrefix(currentStateName, registry);
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
return {
|
|
198
|
-
type: 'blocked',
|
|
199
|
-
output: formatOperationGateError('write-check', identityResult, currentPrefix)
|
|
200
|
-
};
|
|
201
|
-
}
|
|
179
|
+
const gate = this.applyIdentityGate(sessionId, workflow, 'write-check');
|
|
180
|
+
if (gate !== undefined)
|
|
181
|
+
return gate;
|
|
202
182
|
const writeTools = new Set(['Write', 'Edit', 'NotebookEdit']);
|
|
203
183
|
if (!writeTools.has(toolName)) {
|
|
204
184
|
workflow.appendEvent({
|
|
@@ -272,6 +252,10 @@ export class WorkflowEngine {
|
|
|
272
252
|
output: ''
|
|
273
253
|
};
|
|
274
254
|
}
|
|
255
|
+
getState(sessionId) {
|
|
256
|
+
this.requireSession(sessionId);
|
|
257
|
+
return serializeWorkflowState(this.rehydrateFromEvents(sessionId).getState());
|
|
258
|
+
}
|
|
275
259
|
persistSessionId(sessionId) {
|
|
276
260
|
this.engineDeps.appendToFile(this.engineDeps.getEnvFilePath(), `export CLAUDE_SESSION_ID='${sessionId}'\n`);
|
|
277
261
|
}
|
|
@@ -287,15 +271,45 @@ export class WorkflowEngine {
|
|
|
287
271
|
}
|
|
288
272
|
}
|
|
289
273
|
rehydrateFromEvents(sessionId) {
|
|
290
|
-
const
|
|
274
|
+
const stored = this.engineDeps.store.readEvents(sessionId);
|
|
275
|
+
const events = stored.map(flattenStoredEvent);
|
|
291
276
|
const state = events.reduce((accumulator, event) => this.factory.fold(accumulator, event), this.factory.initialState());
|
|
292
277
|
return this.factory.buildWorkflow(state, this.workflowDeps);
|
|
293
278
|
}
|
|
294
279
|
persistEvents(sessionId, workflow) {
|
|
295
280
|
const pending = workflow.getPendingEvents();
|
|
296
|
-
if (pending.length
|
|
297
|
-
|
|
298
|
-
|
|
281
|
+
if (pending.length === 0)
|
|
282
|
+
return;
|
|
283
|
+
const preAppendState = this.rehydrateFromEvents(sessionId).getState();
|
|
284
|
+
this.engineDeps.store.appendEvents(sessionId, this.wrapEvents(pending, preAppendState));
|
|
285
|
+
}
|
|
286
|
+
wrapEvents(events, startState) {
|
|
287
|
+
const { stored } = events.reduce((accumulator, event) => ({
|
|
288
|
+
state: this.factory.fold(accumulator.state, event),
|
|
289
|
+
stored: [...accumulator.stored, {
|
|
290
|
+
envelope: {
|
|
291
|
+
type: event.type,
|
|
292
|
+
at: event.at,
|
|
293
|
+
state: accumulator.state.currentStateMachineState,
|
|
294
|
+
},
|
|
295
|
+
payload: toPayload(event),
|
|
296
|
+
}],
|
|
297
|
+
}), {
|
|
298
|
+
state: startState,
|
|
299
|
+
stored: [],
|
|
300
|
+
});
|
|
301
|
+
return stored;
|
|
302
|
+
}
|
|
303
|
+
applyIdentityGate(sessionId, workflow, op) {
|
|
304
|
+
const identityResult = this.verifyIdentity(sessionId, workflow);
|
|
305
|
+
if (identityResult === undefined)
|
|
306
|
+
return undefined;
|
|
307
|
+
this.persistEvents(sessionId, workflow);
|
|
308
|
+
const currentPrefix = getExpectedPrefix(workflow.getState().currentStateMachineState, this.factory.getRegistry());
|
|
309
|
+
return {
|
|
310
|
+
type: 'blocked',
|
|
311
|
+
output: formatOperationGateError(op, identityResult, currentPrefix),
|
|
312
|
+
};
|
|
299
313
|
}
|
|
300
314
|
verifyIdentity(sessionId, workflow) {
|
|
301
315
|
const transcriptPath = workflow.getTranscriptPath();
|
|
@@ -304,14 +318,14 @@ export class WorkflowEngine {
|
|
|
304
318
|
const expectedPrefix = getExpectedPrefix(state, registry);
|
|
305
319
|
const pattern = buildPrefixPattern(registry);
|
|
306
320
|
const messages = this.engineDeps.transcriptReader.readMessages(transcriptPath);
|
|
307
|
-
const
|
|
308
|
-
this.engineDeps.store.appendEvents(sessionId, [{
|
|
321
|
+
const identityCheckResult = checkIdentity(messages, pattern);
|
|
322
|
+
this.engineDeps.store.appendEvents(sessionId, this.wrapEvents([{
|
|
309
323
|
type: 'identity-verified',
|
|
310
324
|
at: this.engineDeps.now(),
|
|
311
|
-
status:
|
|
325
|
+
status: identityCheckResult.status,
|
|
312
326
|
transcriptPath,
|
|
313
|
-
}]);
|
|
314
|
-
if (
|
|
327
|
+
}], workflow.getState()));
|
|
328
|
+
if (identityCheckResult.status === 'lost') {
|
|
315
329
|
return `You forgot. Next message MUST begin with: ${expectedPrefix}`;
|
|
316
330
|
}
|
|
317
331
|
return undefined;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** @riviere-role domain-service */
|
|
2
|
+
export function serializeWorkflowState(state) {
|
|
3
|
+
try {
|
|
4
|
+
return {
|
|
5
|
+
type: 'success',
|
|
6
|
+
output: JSON.stringify(state, null, 2),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11
|
+
return {
|
|
12
|
+
type: 'error',
|
|
13
|
+
output: `Failed to serialize workflow state: ${message}`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export declare const SEPARATOR = "----------------------------------------------------------------";
|
|
2
|
+
export declare const PLATFORM_NOTIFICATION_FENCE = "****************************************************************";
|
|
3
|
+
export declare const JOURNAL_GUIDANCE: string;
|
|
2
4
|
/** @riviere-role cli-output-formatter */
|
|
3
5
|
export declare function formatBlock(title: string, body: string): string;
|
|
4
6
|
/** @riviere-role cli-output-formatter */
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
export const SEPARATOR = '----------------------------------------------------------------';
|
|
2
|
+
export const PLATFORM_NOTIFICATION_FENCE = '****************************************************************';
|
|
3
|
+
export const JOURNAL_GUIDANCE = [
|
|
4
|
+
PLATFORM_NOTIFICATION_FENCE,
|
|
5
|
+
'PLATFORM NOTIFICATION',
|
|
6
|
+
'',
|
|
7
|
+
'Record your progress and reasoning as you work by calling:',
|
|
8
|
+
' write-journal <agent-name> "<1\u20133 sentence note>"',
|
|
9
|
+
'',
|
|
10
|
+
'Use it for key decisions, progress milestones, and blockers.',
|
|
11
|
+
'Every session should have a journal trail of the work performed.',
|
|
12
|
+
PLATFORM_NOTIFICATION_FENCE,
|
|
13
|
+
].join('\n');
|
|
2
14
|
/** @riviere-role cli-output-formatter */
|
|
3
15
|
export function formatBlock(title, body) {
|
|
4
16
|
return `${title}\n${SEPARATOR}\n${body}`;
|
|
5
17
|
}
|
|
18
|
+
function appendJournalGuidance(body) {
|
|
19
|
+
return `${body}\n\n${JOURNAL_GUIDANCE}`;
|
|
20
|
+
}
|
|
6
21
|
/** @riviere-role cli-output-formatter */
|
|
7
22
|
export function formatTransitionSuccess(title, procedureContent, expectedPrefix) {
|
|
8
|
-
return formatBlock(title, `${procedureContent}\n\nNext message MUST begin with: ${expectedPrefix}`);
|
|
23
|
+
return formatBlock(title, appendJournalGuidance(`${procedureContent}\n\nNext message MUST begin with: ${expectedPrefix}`));
|
|
9
24
|
}
|
|
10
25
|
/** @riviere-role cli-output-formatter */
|
|
11
26
|
export function formatTransitionError(to, reason, currentProcedure, expectedPrefix) {
|
|
@@ -25,5 +40,5 @@ export function formatOperationSuccess(op, body, expectedPrefix) {
|
|
|
25
40
|
}
|
|
26
41
|
/** @riviere-role cli-output-formatter */
|
|
27
42
|
export function formatInitSuccess(procedureContent, expectedPrefix) {
|
|
28
|
-
return formatBlock('Feature team initialized', `${procedureContent}\n\nNext message MUST begin with: ${expectedPrefix}`);
|
|
43
|
+
return formatBlock('Feature team initialized', appendJournalGuidance(`${procedureContent}\n\nNext message MUST begin with: ${expectedPrefix}`));
|
|
29
44
|
}
|