@exaudeus/workrail 1.14.2 → 1.15.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.
@@ -114,6 +114,25 @@ function applyPromptBudget(combinedPrompt) {
114
114
  const decoder = new TextDecoder('utf-8');
115
115
  return decoder.decode(truncatedBytes) + markerText + omissionNote;
116
116
  }
117
+ function buildLoopContextBanner(args) {
118
+ if (args.loopPath.length === 0 || args.isExitStep)
119
+ return '';
120
+ const current = args.loopPath[args.loopPath.length - 1];
121
+ const iterationLabel = `Iteration ${current.iteration + 1}`;
122
+ return [
123
+ `---`,
124
+ `**LOOP: ${current.loopId} | ${iterationLabel}** — This step repeats intentionally; the workflow is not stuck or broken.`,
125
+ ``,
126
+ `Choose the instruction that matches your current task:`,
127
+ `- **Drafting / updating**: incorporate amendments discovered in previous iterations before writing.`,
128
+ `- **Auditing / reviewing**: look for what previous passes *missed*, not what they already caught.`,
129
+ `- **Applying changes**: follow prior findings precisely; don't re-debate settled decisions.`,
130
+ ``,
131
+ `Prior iteration work is visible in the **Ancestry Recap** section below (if present).`,
132
+ `---`,
133
+ ``,
134
+ ].join('\n');
135
+ }
117
136
  function formatOutputContractRequirements(outputContract) {
118
137
  const contractRef = outputContract?.contractRef;
119
138
  if (!contractRef)
@@ -123,7 +142,8 @@ function formatOutputContractRequirements(outputContract) {
123
142
  return [
124
143
  `Artifact contract: ${index_js_2.LOOP_CONTROL_CONTRACT_REF}`,
125
144
  `Provide an artifact with kind: "wr.loop_control"`,
126
- `Fields: loopId (lowercase, delimiter-safe), decision ("continue" | "stop")`,
145
+ `Required field: decision ("continue" | "stop")`,
146
+ `Optional field: loopId — omit unless targeting a specific named loop`,
127
147
  ];
128
148
  default:
129
149
  return [
@@ -154,14 +174,16 @@ function renderPendingPrompt(args) {
154
174
  const basePrompt = step?.prompt ?? `Pending step: ${args.stepId}`;
155
175
  const requireConfirmation = Boolean(step?.requireConfirmation);
156
176
  const functionReferences = step?.functionReferences ?? [];
177
+ const outputContract = step && typeof step === 'object' && 'outputContract' in step
178
+ ? step.outputContract
179
+ : undefined;
180
+ const isExitStep = outputContract?.contractRef === index_js_2.LOOP_CONTROL_CONTRACT_REF;
181
+ const loopBanner = buildLoopContextBanner({ loopPath: args.loopPath, isExitStep });
157
182
  const validationCriteria = step?.validationCriteria;
158
183
  const requirements = (0, validation_requirements_extractor_js_1.extractValidationRequirements)(validationCriteria);
159
184
  const requirementsSection = requirements.length > 0
160
185
  ? `\n\n**OUTPUT REQUIREMENTS:**\n${requirements.map(r => `- ${r}`).join('\n')}`
161
186
  : '';
162
- const outputContract = step && typeof step === 'object' && 'outputContract' in step
163
- ? step.outputContract
164
- : undefined;
165
187
  const contractRequirements = formatOutputContractRequirements(outputContract);
166
188
  const contractSection = contractRequirements.length > 0
167
189
  ? `\n\n**OUTPUT REQUIREMENTS (System):**\n${contractRequirements.map(r => `- ${r}`).join('\n')}`
@@ -170,8 +192,35 @@ function renderPendingPrompt(args) {
170
192
  (step !== null && step !== undefined && 'notesOptional' in step && step.notesOptional === true);
171
193
  const notesSection = isNotesOptional
172
194
  ? ''
173
- : '\n\n**NOTES REQUIRED (System):** You must include `output.notesMarkdown` documenting what you did and why. Omitting notes will block this step — use the `retryAckToken` to fix and retry.';
174
- const enhancedPrompt = basePrompt + requirementsSection + contractSection + notesSection;
195
+ : '\n\n**NOTES REQUIRED (System):** You must include `output.notesMarkdown` when advancing. ' +
196
+ 'These notes are displayed to the user in a markdown viewer and serve as the durable record of your work. Write them for a human reader.\n\n' +
197
+ 'Include:\n' +
198
+ '- **What you did** and the key decisions or trade-offs you made\n' +
199
+ '- **What you produced** — files changed, functions added, test results, specific numbers\n' +
200
+ '- **Anything notable** — risks, open questions, things you deliberately chose NOT to do and why\n\n' +
201
+ 'Formatting: Use markdown headings, bullet lists, `code references`, and **bold** for emphasis. ' +
202
+ 'Be specific — file paths, function names, counts, not vague summaries. ' +
203
+ '10–30 lines is ideal. Too short is worse than too long.\n\n' +
204
+ 'Scope: THIS step only — WorkRail concatenates notes across steps automatically. Never repeat previous step notes.\n\n' +
205
+ 'Example of BAD notes:\n' +
206
+ '> Reviewed the code and found some issues. Made improvements to error handling.\n\n' +
207
+ 'Example of GOOD notes:\n' +
208
+ '> ## Review: Authentication Module\n' +
209
+ '> **Files examined:** `src/auth/oauth2.ts`, `src/auth/middleware.ts`, `tests/auth.test.ts`\n' +
210
+ '>\n' +
211
+ '> ### Key findings\n' +
212
+ '> - Token refresh logic in `refreshAccessToken()` silently swallows network errors — changed to propagate as `AuthRefreshError`\n' +
213
+ '> - Added missing `audience` validation in JWT verification (was accepting any audience)\n' +
214
+ '> - **3 Critical**, 2 Major, 4 Minor findings total\n' +
215
+ '>\n' +
216
+ '> ### Decisions\n' +
217
+ '> - Did NOT flag the deprecated `passport` import — it\'s used only in the legacy path scheduled for removal in Q2\n' +
218
+ '> - Recommended extracting token storage into a `TokenStore` interface for testability\n' +
219
+ '>\n' +
220
+ '> ### Open questions\n' +
221
+ '> - Should refresh tokens be rotated on every use? Current impl reuses until expiry.\n\n' +
222
+ 'Omitting notes will block this step — use the `retryAckToken` to fix and retry.';
223
+ const enhancedPrompt = loopBanner + basePrompt + requirementsSection + contractSection + notesSection;
175
224
  if (!args.rehydrateOnly) {
176
225
  return (0, neverthrow_1.ok)({ stepId: args.stepId, title: baseTitle, prompt: enhancedPrompt, requireConfirmation });
177
226
  }
@@ -180,7 +180,7 @@ function reasonToBlocker(reason) {
180
180
  code: 'MISSING_REQUIRED_NOTES',
181
181
  pointer: { kind: 'workflow_step', stepId },
182
182
  message: `Step "${stepId}" requires notes documenting your work (output.notesMarkdown).`,
183
- suggestedFix: 'Add output.notesMarkdown with meaningful documentation of what you did and why. Use the retryAckToken to retry without rehydrating.',
183
+ suggestedFix: 'Add output.notesMarkdown with a detailed recap: what you did, key decisions/trade-offs, what you produced (files, paths, numbers), and anything notable. Use markdown formatting. 10–30 lines. Use the retryAckToken to retry without rehydrating.',
184
184
  }))
185
185
  .andThen(ensureBlockerTextBudgets);
186
186
  case 'required_capability_unknown':
@@ -21,7 +21,7 @@ export declare const LoopControlMetadataV1Schema: z.ZodOptional<z.ZodObject<{
21
21
  export type LoopControlMetadataV1 = z.infer<typeof LoopControlMetadataV1Schema>;
22
22
  export declare const LoopControlArtifactV1Schema: z.ZodObject<{
23
23
  kind: z.ZodLiteral<"wr.loop_control">;
24
- loopId: z.ZodString;
24
+ loopId: z.ZodOptional<z.ZodString>;
25
25
  decision: z.ZodEnum<["continue", "stop"]>;
26
26
  metadata: z.ZodOptional<z.ZodObject<{
27
27
  reason: z.ZodOptional<z.ZodString>;
@@ -41,8 +41,8 @@ export declare const LoopControlArtifactV1Schema: z.ZodObject<{
41
41
  }>>;
42
42
  }, "strict", z.ZodTypeAny, {
43
43
  kind: "wr.loop_control";
44
- loopId: string;
45
44
  decision: "continue" | "stop";
45
+ loopId?: string | undefined;
46
46
  metadata?: {
47
47
  reason?: string | undefined;
48
48
  issuesFound?: number | undefined;
@@ -51,8 +51,8 @@ export declare const LoopControlArtifactV1Schema: z.ZodObject<{
51
51
  } | undefined;
52
52
  }, {
53
53
  kind: "wr.loop_control";
54
- loopId: string;
55
54
  decision: "continue" | "stop";
55
+ loopId?: string | undefined;
56
56
  metadata?: {
57
57
  reason?: string | undefined;
58
58
  issuesFound?: number | undefined;
@@ -63,4 +63,4 @@ export declare const LoopControlArtifactV1Schema: z.ZodObject<{
63
63
  export type LoopControlArtifactV1 = z.infer<typeof LoopControlArtifactV1Schema>;
64
64
  export declare function isLoopControlArtifact(artifact: unknown): artifact is LoopControlArtifactV1;
65
65
  export declare function parseLoopControlArtifact(artifact: unknown): LoopControlArtifactV1 | null;
66
- export declare function findLoopControlArtifact(artifacts: readonly unknown[], loopId: string): LoopControlArtifactV1 | null;
66
+ export declare function findLoopControlArtifact(artifacts: readonly unknown[], expectedLoopId: string): LoopControlArtifactV1 | null;
@@ -21,7 +21,7 @@ exports.LoopControlMetadataV1Schema = zod_1.z
21
21
  exports.LoopControlArtifactV1Schema = zod_1.z
22
22
  .object({
23
23
  kind: zod_1.z.literal('wr.loop_control'),
24
- loopId: zod_1.z.string().min(1).max(64).regex(constants_js_1.DELIMITER_SAFE_ID_PATTERN, 'loopId must be delimiter-safe: [a-z0-9_-]+'),
24
+ loopId: zod_1.z.string().min(1).max(64).regex(constants_js_1.DELIMITER_SAFE_ID_PATTERN, 'loopId must be delimiter-safe: [a-z0-9_-]+').optional(),
25
25
  decision: exports.LoopControlDecisionSchema,
26
26
  metadata: exports.LoopControlMetadataV1Schema,
27
27
  })
@@ -35,13 +35,15 @@ function parseLoopControlArtifact(artifact) {
35
35
  const result = exports.LoopControlArtifactV1Schema.safeParse(artifact);
36
36
  return result.success ? result.data : null;
37
37
  }
38
- function findLoopControlArtifact(artifacts, loopId) {
38
+ function findLoopControlArtifact(artifacts, expectedLoopId) {
39
39
  for (let i = artifacts.length - 1; i >= 0; i--) {
40
40
  const artifact = artifacts[i];
41
41
  if (!isLoopControlArtifact(artifact))
42
42
  continue;
43
43
  const parsed = parseLoopControlArtifact(artifact);
44
- if (parsed && parsed.loopId === loopId) {
44
+ if (!parsed)
45
+ continue;
46
+ if (parsed.loopId === undefined || parsed.loopId === expectedLoopId) {
45
47
  return parsed;
46
48
  }
47
49
  }
@@ -29,6 +29,16 @@ function mountConsoleRoutes(app, consoleService) {
29
29
  res.status(status).json({ success: false, error: error.message });
30
30
  });
31
31
  });
32
+ app.get('/api/v2/sessions/:sessionId/nodes/:nodeId', async (req, res) => {
33
+ const { sessionId, nodeId } = req.params;
34
+ const result = await consoleService.getNodeDetail(sessionId, nodeId);
35
+ result.match((data) => res.json({ success: true, data }), (error) => {
36
+ const status = error.code === 'NODE_NOT_FOUND' ? 404
37
+ : error.code === 'SESSION_LOAD_FAILED' ? 404
38
+ : 500;
39
+ res.status(status).json({ success: false, error: error.message });
40
+ });
41
+ });
32
42
  const consoleDist = resolveConsoleDist();
33
43
  if (consoleDist) {
34
44
  app.use('/console', express_1.default.static(consoleDist));
@@ -1,22 +1,27 @@
1
- import type { ResultAsync } from 'neverthrow';
1
+ import { type ResultAsync } from 'neverthrow';
2
2
  import type { DirectoryListingPortV2 } from '../ports/directory-listing.port.js';
3
3
  import type { DataDirPortV2 } from '../ports/data-dir.port.js';
4
4
  import type { SessionEventLogReadonlyStorePortV2 } from '../ports/session-event-log-store.port.js';
5
- import type { ConsoleSessionListResponse, ConsoleSessionDetail } from './console-types.js';
5
+ import type { SnapshotStorePortV2 } from '../ports/snapshot-store.port.js';
6
+ import type { PinnedWorkflowStorePortV2 } from '../ports/pinned-workflow-store.port.js';
7
+ import type { ConsoleSessionListResponse, ConsoleSessionDetail, ConsoleNodeDetail } from './console-types.js';
6
8
  export interface ConsoleServicePorts {
7
9
  readonly directoryListing: DirectoryListingPortV2;
8
10
  readonly dataDir: DataDirPortV2;
9
11
  readonly sessionStore: SessionEventLogReadonlyStorePortV2;
12
+ readonly snapshotStore: SnapshotStorePortV2;
13
+ readonly pinnedWorkflowStore: PinnedWorkflowStorePortV2;
10
14
  }
11
15
  export declare class ConsoleService {
12
16
  private readonly ports;
13
17
  constructor(ports: ConsoleServicePorts);
14
18
  getSessionList(): ResultAsync<ConsoleSessionListResponse, ConsoleServiceError>;
15
19
  getSessionDetail(sessionIdStr: string): ResultAsync<ConsoleSessionDetail, ConsoleServiceError>;
20
+ getNodeDetail(sessionIdStr: string, nodeId: string): ResultAsync<ConsoleNodeDetail, ConsoleServiceError>;
16
21
  private collectSessionSummaries;
17
22
  private loadSessionSummary;
18
23
  }
19
24
  export interface ConsoleServiceError {
20
- readonly code: 'ENUMERATION_FAILED' | 'SESSION_LOAD_FAILED';
25
+ readonly code: 'ENUMERATION_FAILED' | 'SESSION_LOAD_FAILED' | 'NODE_NOT_FOUND';
21
26
  readonly message: string;
22
27
  }