@exaudeus/workrail 3.9.2 → 3.10.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.
@@ -946,8 +946,8 @@
946
946
  "bytes": 68013
947
947
  },
948
948
  "mcp/output-schemas.js": {
949
- "sha256": "f390f2c853580c63dbaa204354dd2199f282e47c0bcd162d2accf3ab27d7ec04",
950
- "bytes": 15326
949
+ "sha256": "d8e73b9f2722193eac8f68887a11644752efb35d341775dd18296f51cc1de26f",
950
+ "bytes": 15428
951
951
  },
952
952
  "mcp/render-envelope.d.ts": {
953
953
  "sha256": "22e83e1aba52968a7136cf289125a217b5f462a5a66a1eebe4669006e3326fdb",
@@ -1446,12 +1446,12 @@
1446
1446
  "bytes": 447
1447
1447
  },
1448
1448
  "v2/durable-core/constants.d.ts": {
1449
- "sha256": "2cdc1fe66cb2024e17977e024c73b9bb50845b70c9ac3925c2635e843a94f7f8",
1450
- "bytes": 3699
1449
+ "sha256": "60311085ea51d86307877bd952623effcafeddf935fbe0f2cde8a21a165dd932",
1450
+ "bytes": 3754
1451
1451
  },
1452
1452
  "v2/durable-core/constants.js": {
1453
- "sha256": "29372d3e7f868b5c40d6270a1f39477a680524688e73b03613305fafef4a9553",
1454
- "bytes": 3558
1453
+ "sha256": "41e0aabd3754c79c9453c8496128610b1bd4a328cf7f55c4f0a8d7bc527c94a2",
1454
+ "bytes": 3642
1455
1455
  },
1456
1456
  "v2/durable-core/domain/ack-advance-append-plan.d.ts": {
1457
1457
  "sha256": "2e802606656a0c1938192e5533aa46c74bc42789b5c315c79f6de4850017b30e",
@@ -1594,8 +1594,8 @@
1594
1594
  "bytes": 1175
1595
1595
  },
1596
1596
  "v2/durable-core/domain/prompt-renderer.js": {
1597
- "sha256": "2ac48cd9cdcecd19bec6bec710e6f45437f68a62df6fac60ce3eff1cdb821b32",
1598
- "bytes": 18266
1597
+ "sha256": "f3e7cc34b753c082ee310e88839948e225ab10f13cedae1d35b2bd64869eb848",
1598
+ "bytes": 17166
1599
1599
  },
1600
1600
  "v2/durable-core/domain/reason-model.d.ts": {
1601
1601
  "sha256": "650fcb2d9969a4e6123cccbd4913f4d57aeab21a19bb907aa1e11f95e5a95089",
@@ -1621,6 +1621,14 @@
1621
1621
  "sha256": "cc53453a1617b9adc526f7d3f49a40b1491bbe63dbaff98ddc187e1f8f6d125f",
1622
1622
  "bytes": 1454
1623
1623
  },
1624
+ "v2/durable-core/domain/retrieval-contract.d.ts": {
1625
+ "sha256": "9d130876a9584ab24fcb65e145958404d5636ff96ee1c5a02a2f64b219d5eda4",
1626
+ "bytes": 4747
1627
+ },
1628
+ "v2/durable-core/domain/retrieval-contract.js": {
1629
+ "sha256": "083a2e7d9fbbf4865f3960ad232774fd416b226919c3c39ea36e8ca3102df0cf",
1630
+ "bytes": 12217
1631
+ },
1624
1632
  "v2/durable-core/domain/risk-policy-guardrails.d.ts": {
1625
1633
  "sha256": "4720f9fe6e6ae68d7f989b31070d73661eb96652109dac7d30961e7937ff55e5",
1626
1634
  "bytes": 637
@@ -2430,12 +2438,12 @@
2430
2438
  "bytes": 732
2431
2439
  },
2432
2440
  "v2/projections/resume-ranking.d.ts": {
2433
- "sha256": "b1d9e7d6fb3e59ced42f7c2e5fd9d8f9a18308aa6f4411a8aabf5dc174aa8dad",
2434
- "bytes": 3960
2441
+ "sha256": "3ac0db8e9d154eff9fba21c40eedaf981ea844d3f536f2b69b3840af8653b8f1",
2442
+ "bytes": 4033
2435
2443
  },
2436
2444
  "v2/projections/resume-ranking.js": {
2437
- "sha256": "1b256c992ded9ec52150b229799716d118ca2a3659a0cc523e727cde2ebf7b0c",
2438
- "bytes": 13660
2445
+ "sha256": "5f1ee9888446caa23653543fc8cae69ac4398e73232d71bac3d16722c9cd2158",
2446
+ "bytes": 13596
2439
2447
  },
2440
2448
  "v2/projections/run-context.d.ts": {
2441
2449
  "sha256": "a4d57470a435ac9860f60b3244d1b828853995027cd510d8da42762d21b2a687",
@@ -5,6 +5,7 @@ exports.toPendingStep = toPendingStep;
5
5
  const zod_1 = require("zod");
6
6
  const state_js_1 = require("../domain/execution/state.js");
7
7
  const token_patterns_js_1 = require("../v2/durable-core/tokens/token-patterns.js");
8
+ const constants_js_1 = require("../v2/durable-core/constants.js");
8
9
  const JsonPrimitiveSchema = zod_1.z.union([zod_1.z.string(), zod_1.z.number(), zod_1.z.boolean(), zod_1.z.null()]);
9
10
  exports.JsonValueSchema = zod_1.z.lazy(() => zod_1.z.union([JsonPrimitiveSchema, zod_1.z.array(exports.JsonValueSchema), zod_1.z.record(exports.JsonValueSchema)]));
10
11
  exports.WorkflowSummarySchema = zod_1.z.object({
@@ -264,7 +265,7 @@ exports.V2ResumeSessionOutputSchema = zod_1.z.object({
264
265
  sessionTitle: zod_1.z.string().nullable().describe('Human-readable task/session title derived from persisted workflow context or early recap text.'),
265
266
  gitBranch: zod_1.z.string().nullable().describe('Git branch associated with the session, if available.'),
266
267
  resumeToken: zod_1.z.string().regex(token_patterns_js_1.STATE_TOKEN_PATTERN, 'Invalid resumeToken format'),
267
- snippet: zod_1.z.string().max(1024),
268
+ snippet: zod_1.z.string().max(constants_js_1.MAX_RESUME_PREVIEW_BYTES),
268
269
  confidence: zod_1.z.enum(['strong', 'medium', 'weak']).describe('Coarse confidence band for how likely this candidate is the intended session.'),
269
270
  matchExplanation: zod_1.z.string().min(1).describe('Short natural-language explanation of why this candidate ranked here.'),
270
271
  pendingStepId: zod_1.z.string().nullable().describe('The current pending step ID (e.g. "phase-3-implement") if the workflow is in progress. ' +
@@ -15,7 +15,8 @@ export declare const MAX_OBSERVATION_SHORT_STRING_LENGTH = 80;
15
15
  export declare const SESSION_LOCK_RETRY_AFTER_MS = 1000;
16
16
  export declare const DEFAULT_RETRY_AFTER_MS = 1000;
17
17
  export declare const TRUNCATION_MARKER = "\n\n[TRUNCATED]";
18
- export declare const RECOVERY_BUDGET_BYTES = 12288;
18
+ export declare const RECOVERY_BUDGET_BYTES: number;
19
+ export declare const MAX_RESUME_PREVIEW_BYTES: number;
19
20
  export declare const SHA256_DIGEST_PATTERN: RegExp;
20
21
  export declare const DELIMITER_SAFE_ID_PATTERN: RegExp;
21
22
  export declare const EVENT_KIND: {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AUTONOMY_MODE = exports.ADVANCE_INTENT = exports.MANIFEST_KIND = exports.EDGE_CAUSE = exports.ADVANCE_OUTCOME = exports.ENGINE_STATE = exports.EDGE_KIND = exports.PAYLOAD_KIND = exports.OUTPUT_CHANNEL = exports.EVENT_KIND = exports.DELIMITER_SAFE_ID_PATTERN = exports.SHA256_DIGEST_PATTERN = exports.RECOVERY_BUDGET_BYTES = exports.TRUNCATION_MARKER = exports.DEFAULT_RETRY_AFTER_MS = exports.SESSION_LOCK_RETRY_AFTER_MS = exports.MAX_OBSERVATION_SHORT_STRING_LENGTH = exports.MAX_CONTEXT_DEPTH = exports.MAX_CONTEXT_BYTES = exports.MAX_VALIDATION_SUGGESTIONS_BYTES = exports.MAX_VALIDATION_ISSUES_BYTES = exports.MAX_VALIDATION_SUGGESTION_ITEM_BYTES = exports.MAX_VALIDATION_ISSUE_ITEM_BYTES = exports.MAX_OUTPUT_NOTES_MARKDOWN_BYTES = exports.MAX_DECISION_TRACE_TOTAL_BYTES = exports.MAX_DECISION_TRACE_ENTRY_SUMMARY_BYTES = exports.MAX_DECISION_TRACE_ENTRIES = exports.MAX_BLOCKER_SUGGESTED_FIX_BYTES = exports.MAX_BLOCKER_MESSAGE_BYTES = exports.MAX_BLOCKERS = void 0;
3
+ exports.AUTONOMY_MODE = exports.ADVANCE_INTENT = exports.MANIFEST_KIND = exports.EDGE_CAUSE = exports.ADVANCE_OUTCOME = exports.ENGINE_STATE = exports.EDGE_KIND = exports.PAYLOAD_KIND = exports.OUTPUT_CHANNEL = exports.EVENT_KIND = exports.DELIMITER_SAFE_ID_PATTERN = exports.SHA256_DIGEST_PATTERN = exports.MAX_RESUME_PREVIEW_BYTES = exports.RECOVERY_BUDGET_BYTES = exports.TRUNCATION_MARKER = exports.DEFAULT_RETRY_AFTER_MS = exports.SESSION_LOCK_RETRY_AFTER_MS = exports.MAX_OBSERVATION_SHORT_STRING_LENGTH = exports.MAX_CONTEXT_DEPTH = exports.MAX_CONTEXT_BYTES = exports.MAX_VALIDATION_SUGGESTIONS_BYTES = exports.MAX_VALIDATION_ISSUES_BYTES = exports.MAX_VALIDATION_SUGGESTION_ITEM_BYTES = exports.MAX_VALIDATION_ISSUE_ITEM_BYTES = exports.MAX_OUTPUT_NOTES_MARKDOWN_BYTES = exports.MAX_DECISION_TRACE_TOTAL_BYTES = exports.MAX_DECISION_TRACE_ENTRY_SUMMARY_BYTES = exports.MAX_DECISION_TRACE_ENTRIES = exports.MAX_BLOCKER_SUGGESTED_FIX_BYTES = exports.MAX_BLOCKER_MESSAGE_BYTES = exports.MAX_BLOCKERS = void 0;
4
4
  exports.MAX_BLOCKERS = 10;
5
5
  exports.MAX_BLOCKER_MESSAGE_BYTES = 512;
6
6
  exports.MAX_BLOCKER_SUGGESTED_FIX_BYTES = 1024;
@@ -18,7 +18,8 @@ exports.MAX_OBSERVATION_SHORT_STRING_LENGTH = 80;
18
18
  exports.SESSION_LOCK_RETRY_AFTER_MS = 1000;
19
19
  exports.DEFAULT_RETRY_AFTER_MS = 1000;
20
20
  exports.TRUNCATION_MARKER = '\n\n[TRUNCATED]';
21
- exports.RECOVERY_BUDGET_BYTES = 12288;
21
+ exports.RECOVERY_BUDGET_BYTES = 24 * 1024;
22
+ exports.MAX_RESUME_PREVIEW_BYTES = 2 * 1024;
22
23
  exports.SHA256_DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
23
24
  exports.DELIMITER_SAFE_ID_PATTERN = /^[a-z0-9_-]+$/;
24
25
  exports.EVENT_KIND = {
@@ -15,11 +15,13 @@ const index_js_2 = require("../schemas/artifacts/index.js");
15
15
  const run_context_js_1 = require("../../projections/run-context.js");
16
16
  const condition_evaluator_js_1 = require("../../../utils/condition-evaluator.js");
17
17
  const context_template_resolver_js_1 = require("./context-template-resolver.js");
18
- function buildNonTipSections(args) {
19
- const sections = [];
18
+ const retrieval_contract_js_1 = require("./retrieval-contract.js");
19
+ function buildNonTipSegments(args) {
20
+ const segments = [];
20
21
  const childSummary = (0, recap_recovery_js_1.buildChildSummary)({ nodeId: args.nodeId, dag: args.run });
21
- if (childSummary) {
22
- sections.push(`### Branch Summary\n${childSummary}`);
22
+ const childSummarySegment = (0, retrieval_contract_js_1.createBranchSummarySegment)(childSummary);
23
+ if (childSummarySegment) {
24
+ segments.push(childSummarySegment);
23
25
  }
24
26
  if (args.run.preferredTipNodeId && args.run.preferredTipNodeId !== String(args.nodeId)) {
25
27
  const downstreamRes = (0, recap_recovery_js_1.collectDownstreamRecap)({
@@ -29,12 +31,15 @@ function buildNonTipSections(args) {
29
31
  outputs: args.outputs,
30
32
  });
31
33
  if (downstreamRes.isOk() && downstreamRes.value.length > 0) {
32
- sections.push(`### Downstream Recap (Preferred Branch)\n${downstreamRes.value.join('\n\n')}`);
34
+ const downstreamSegment = (0, retrieval_contract_js_1.createDownstreamRecapSegment)(downstreamRes.value.join('\n\n'));
35
+ if (downstreamSegment) {
36
+ segments.push(downstreamSegment);
37
+ }
33
38
  }
34
39
  }
35
- return sections;
40
+ return segments;
36
41
  }
37
- function buildAncestrySections(args) {
42
+ function buildAncestrySegments(args) {
38
43
  const ancestryRes = (0, recap_recovery_js_1.collectAncestryRecap)({
39
44
  nodeId: args.nodeId,
40
45
  dag: args.run,
@@ -42,11 +47,12 @@ function buildAncestrySections(args) {
42
47
  includeCurrentNode: false,
43
48
  });
44
49
  if (ancestryRes.isOk() && ancestryRes.value.length > 0) {
45
- return [`### Ancestry Recap\n${ancestryRes.value.join('\n\n')}`];
50
+ const ancestrySegment = (0, retrieval_contract_js_1.createAncestryRecapSegment)(ancestryRes.value.join('\n\n'));
51
+ return ancestrySegment ? [ancestrySegment] : [];
46
52
  }
47
53
  return [];
48
54
  }
49
- function buildFunctionDefsSections(args) {
55
+ function buildFunctionDefsSegments(args) {
50
56
  const funcsRes = (0, function_definition_expander_js_1.expandFunctionDefinitions)({
51
57
  workflow: args.workflow,
52
58
  stepId: args.stepId,
@@ -55,7 +61,8 @@ function buildFunctionDefsSections(args) {
55
61
  });
56
62
  if (funcsRes.isOk() && funcsRes.value.length > 0) {
57
63
  const formatted = funcsRes.value.map(function_definition_expander_js_1.formatFunctionDef).join('\n\n');
58
- return [`### Function Definitions\n\`\`\`\n${formatted}\n\`\`\``];
64
+ const functionDefinitionsSegment = (0, retrieval_contract_js_1.createFunctionDefinitionsSegment)(`\`\`\`\n${formatted}\n\`\`\``);
65
+ return functionDefinitionsSegment ? [functionDefinitionsSegment] : [];
59
66
  }
60
67
  return [];
61
68
  }
@@ -65,12 +72,12 @@ function hasPriorNotesInRun(args) {
65
72
  e.data.outputChannel === constants_js_1.OUTPUT_CHANNEL.RECAP &&
66
73
  e.data.payload.payloadKind === constants_js_1.PAYLOAD_KIND.NOTES);
67
74
  }
68
- function buildRecoverySections(args) {
75
+ function buildRecoverySegments(args) {
69
76
  const isTip = args.run.tipNodeIds.includes(String(args.nodeId));
70
77
  return [
71
- ...(isTip ? [] : buildNonTipSections({ nodeId: args.nodeId, run: args.run, outputs: args.outputs })),
72
- ...buildAncestrySections({ nodeId: args.nodeId, run: args.run, outputs: args.outputs }),
73
- ...buildFunctionDefsSections({
78
+ ...(isTip ? [] : buildNonTipSegments({ nodeId: args.nodeId, run: args.run, outputs: args.outputs })),
79
+ ...buildAncestrySegments({ nodeId: args.nodeId, run: args.run, outputs: args.outputs }),
80
+ ...buildFunctionDefsSegments({
74
81
  workflow: args.workflow,
75
82
  stepId: args.stepId,
76
83
  loopPath: args.loopPath,
@@ -78,52 +85,6 @@ function buildRecoverySections(args) {
78
85
  }),
79
86
  ];
80
87
  }
81
- function trimToUtf8Boundary(bytes) {
82
- const n = bytes.length;
83
- if (n === 0)
84
- return bytes;
85
- let cont = 0;
86
- for (let i = n - 1; i >= 0 && i >= n - 4; i--) {
87
- const b = bytes[i];
88
- if ((b & 192) === 128) {
89
- cont++;
90
- }
91
- else {
92
- break;
93
- }
94
- }
95
- if (cont === 0)
96
- return bytes;
97
- const leadByteIndex = n - cont - 1;
98
- if (leadByteIndex < 0) {
99
- return new Uint8Array(0);
100
- }
101
- const leadByte = bytes[leadByteIndex];
102
- const expectedLen = (leadByte & 128) === 0 ? 1 :
103
- (leadByte & 224) === 192 ? 2 :
104
- (leadByte & 240) === 224 ? 3 :
105
- (leadByte & 248) === 240 ? 4 :
106
- 0;
107
- const actualLen = cont + 1;
108
- if (expectedLen === 0 || expectedLen !== actualLen) {
109
- return bytes.subarray(0, leadByteIndex);
110
- }
111
- return bytes;
112
- }
113
- function applyPromptBudget(combinedPrompt) {
114
- const encoder = new TextEncoder();
115
- const promptBytes = encoder.encode(combinedPrompt);
116
- if (promptBytes.length <= constants_js_1.RECOVERY_BUDGET_BYTES) {
117
- return combinedPrompt;
118
- }
119
- const markerText = constants_js_1.TRUNCATION_MARKER;
120
- const omissionNote = `\nOmitted recovery content due to budget constraints.`;
121
- const suffixBytes = encoder.encode(markerText + omissionNote);
122
- const maxContentBytes = constants_js_1.RECOVERY_BUDGET_BYTES - suffixBytes.length;
123
- const truncatedBytes = trimToUtf8Boundary(promptBytes.subarray(0, maxContentBytes));
124
- const decoder = new TextDecoder('utf-8');
125
- return decoder.decode(truncatedBytes) + markerText + omissionNote;
126
- }
127
88
  function resolveParentLoopStep(workflow, stepId) {
128
89
  for (const step of workflow.definition.steps) {
129
90
  if ((0, workflow_js_1.isLoopStepDefinition)(step) && Array.isArray(step.body)) {
@@ -352,9 +313,8 @@ function renderPendingPrompt(args) {
352
313
  });
353
314
  }
354
315
  const { run, outputs } = projectionsRes.value;
355
- const sections = buildRecoverySections({
316
+ const segments = buildRecoverySegments({
356
317
  nodeId: args.nodeId,
357
- dag: run,
358
318
  run,
359
319
  outputs,
360
320
  workflow: args.workflow,
@@ -362,12 +322,14 @@ function renderPendingPrompt(args) {
362
322
  loopPath: args.loopPath,
363
323
  functionReferences,
364
324
  });
365
- if (sections.length === 0) {
325
+ if (segments.length === 0) {
366
326
  return (0, neverthrow_1.ok)({ stepId: args.stepId, title: baseTitle, prompt: enhancedPrompt, agentRole, requireConfirmation });
367
327
  }
368
328
  const recoveryHeader = cleanResponseFormat ? 'Your previous work:' : '## Recovery Context';
369
- const recoveryText = `${recoveryHeader}\n\n${sections.join('\n\n')}`;
370
- const combinedPrompt = `${enhancedPrompt}\n\n${recoveryText}`;
371
- const finalPrompt = applyPromptBudget(combinedPrompt);
329
+ const recoveryText = (0, retrieval_contract_js_1.renderBudgetedRehydrateRecovery)({
330
+ header: recoveryHeader,
331
+ segments,
332
+ }).text;
333
+ const finalPrompt = `${enhancedPrompt}\n\n${recoveryText}`;
372
334
  return (0, neverthrow_1.ok)({ stepId: args.stepId, title: baseTitle, prompt: finalPrompt, agentRole, requireConfirmation });
373
335
  }
@@ -0,0 +1,105 @@
1
+ export type RetrievalPackSurface = 'rehydrate';
2
+ export type ResumePreviewSurface = 'resume_preview';
3
+ export type RetrievalPackTier = 'structural_context' | 'durable_recap' | 'reference_material';
4
+ export type ResumePreviewTier = 'identity_context' | 'durable_recap';
5
+ export interface RetrievalPackTierDefinition {
6
+ readonly tier: RetrievalPackTier;
7
+ readonly purpose: string;
8
+ readonly priority: number;
9
+ readonly retention: 'core' | 'tail';
10
+ }
11
+ export interface ResumePreviewTierDefinition {
12
+ readonly tier: ResumePreviewTier;
13
+ readonly purpose: string;
14
+ readonly priority: number;
15
+ readonly maxBytes: number;
16
+ }
17
+ export interface RetrievalPackTruncationPolicy {
18
+ readonly mode: 'drop_lower_tiers_then_global_utf8_trim';
19
+ readonly budgetScope: 'shared_recovery_prompt';
20
+ readonly antiReconstructionRule: 'select_order_and_compress_explicit_facts_only';
21
+ }
22
+ export interface RetrievalPackContract {
23
+ readonly surface: RetrievalPackSurface;
24
+ readonly tiers: readonly RetrievalPackTierDefinition[];
25
+ readonly truncation: RetrievalPackTruncationPolicy;
26
+ }
27
+ export interface ResumePreviewContract {
28
+ readonly surface: ResumePreviewSurface;
29
+ readonly tiers: readonly ResumePreviewTierDefinition[];
30
+ readonly budgetBytes: number;
31
+ }
32
+ export interface BranchSummarySegment {
33
+ readonly kind: 'branch_summary';
34
+ readonly tier: 'structural_context';
35
+ readonly source: 'deterministic_structure';
36
+ readonly title: 'Branch Summary';
37
+ readonly body: string;
38
+ }
39
+ export interface DownstreamRecapSegment {
40
+ readonly kind: 'downstream_recap';
41
+ readonly tier: 'structural_context';
42
+ readonly source: 'explicit_durable_fact';
43
+ readonly title: 'Downstream Recap (Preferred Branch)';
44
+ readonly body: string;
45
+ }
46
+ export interface AncestryRecapSegment {
47
+ readonly kind: 'ancestry_recap';
48
+ readonly tier: 'durable_recap';
49
+ readonly source: 'explicit_durable_fact';
50
+ readonly title: 'Ancestry Recap';
51
+ readonly body: string;
52
+ }
53
+ export interface FunctionDefinitionsSegment {
54
+ readonly kind: 'function_definitions';
55
+ readonly tier: 'reference_material';
56
+ readonly source: 'workflow_definition';
57
+ readonly title: 'Function Definitions';
58
+ readonly body: string;
59
+ }
60
+ export type RetrievalPackSegment = BranchSummarySegment | DownstreamRecapSegment | AncestryRecapSegment | FunctionDefinitionsSegment;
61
+ export interface RetrievalPackRenderResult {
62
+ readonly text: string;
63
+ readonly includedTiers: readonly RetrievalPackTier[];
64
+ readonly omittedTierCount: number;
65
+ readonly truncatedWithinTier: boolean;
66
+ }
67
+ export interface SessionTitlePreviewSegment {
68
+ readonly kind: 'session_title_preview';
69
+ readonly tier: 'identity_context';
70
+ readonly source: 'persisted_context';
71
+ readonly body: string;
72
+ }
73
+ export interface RecapPreviewSegment {
74
+ readonly kind: 'recap_preview';
75
+ readonly tier: 'durable_recap';
76
+ readonly source: 'explicit_durable_fact';
77
+ readonly body: string;
78
+ }
79
+ export type ResumePreviewSegment = SessionTitlePreviewSegment | RecapPreviewSegment;
80
+ export type ResumePreviewText = string & {
81
+ readonly __brand: 'ResumePreviewText';
82
+ };
83
+ export interface ResumePreviewRenderResult {
84
+ readonly text: ResumePreviewText;
85
+ readonly includedTiers: readonly ResumePreviewTier[];
86
+ }
87
+ export declare const REHYDRATE_RETRIEVAL_CONTRACT: RetrievalPackContract;
88
+ export declare const RESUME_PREVIEW_CONTRACT: ResumePreviewContract;
89
+ export declare function createBranchSummarySegment(body: string): BranchSummarySegment | null;
90
+ export declare function createDownstreamRecapSegment(body: string): DownstreamRecapSegment | null;
91
+ export declare function createAncestryRecapSegment(body: string): AncestryRecapSegment | null;
92
+ export declare function createFunctionDefinitionsSegment(body: string): FunctionDefinitionsSegment | null;
93
+ export declare function createSessionTitlePreviewSegment(body: string): SessionTitlePreviewSegment | null;
94
+ export declare function createRecapPreviewSegment(body: string): RecapPreviewSegment | null;
95
+ export declare function compareRetrievalPackSegments(a: RetrievalPackSegment, b: RetrievalPackSegment): number;
96
+ export declare function orderRetrievalPackSegments(segments: readonly RetrievalPackSegment[]): readonly RetrievalPackSegment[];
97
+ export declare function renderRetrievalPackSections(segments: readonly RetrievalPackSegment[]): readonly string[];
98
+ export declare function renderBudgetedResumePreview(args: {
99
+ readonly segments: readonly ResumePreviewSegment[];
100
+ readonly focusTerms?: readonly string[];
101
+ }): ResumePreviewRenderResult;
102
+ export declare function renderBudgetedRehydrateRecovery(args: {
103
+ readonly header: string;
104
+ readonly segments: readonly RetrievalPackSegment[];
105
+ }): RetrievalPackRenderResult;
@@ -0,0 +1,310 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RESUME_PREVIEW_CONTRACT = exports.REHYDRATE_RETRIEVAL_CONTRACT = void 0;
4
+ exports.createBranchSummarySegment = createBranchSummarySegment;
5
+ exports.createDownstreamRecapSegment = createDownstreamRecapSegment;
6
+ exports.createAncestryRecapSegment = createAncestryRecapSegment;
7
+ exports.createFunctionDefinitionsSegment = createFunctionDefinitionsSegment;
8
+ exports.createSessionTitlePreviewSegment = createSessionTitlePreviewSegment;
9
+ exports.createRecapPreviewSegment = createRecapPreviewSegment;
10
+ exports.compareRetrievalPackSegments = compareRetrievalPackSegments;
11
+ exports.orderRetrievalPackSegments = orderRetrievalPackSegments;
12
+ exports.renderRetrievalPackSections = renderRetrievalPackSections;
13
+ exports.renderBudgetedResumePreview = renderBudgetedResumePreview;
14
+ exports.renderBudgetedRehydrateRecovery = renderBudgetedRehydrateRecovery;
15
+ const constants_js_1 = require("../constants.js");
16
+ const REHYDRATE_TIER_DEFINITIONS = [
17
+ {
18
+ tier: 'structural_context',
19
+ purpose: 'Orient the agent to branch shape and preferred continuation path.',
20
+ priority: 0,
21
+ retention: 'core',
22
+ },
23
+ {
24
+ tier: 'durable_recap',
25
+ purpose: 'Surface durable notes captured from explicit recap outputs.',
26
+ priority: 1,
27
+ retention: 'core',
28
+ },
29
+ {
30
+ tier: 'reference_material',
31
+ purpose: 'Surface authored workflow definitions referenced by the current step.',
32
+ priority: 2,
33
+ retention: 'tail',
34
+ },
35
+ ];
36
+ exports.REHYDRATE_RETRIEVAL_CONTRACT = {
37
+ surface: 'rehydrate',
38
+ tiers: REHYDRATE_TIER_DEFINITIONS,
39
+ truncation: {
40
+ mode: 'drop_lower_tiers_then_global_utf8_trim',
41
+ budgetScope: 'shared_recovery_prompt',
42
+ antiReconstructionRule: 'select_order_and_compress_explicit_facts_only',
43
+ },
44
+ };
45
+ const RESUME_PREVIEW_TIER_DEFINITIONS = [
46
+ {
47
+ tier: 'identity_context',
48
+ purpose: 'Surface the best concise identity hint for the session.',
49
+ priority: 0,
50
+ maxBytes: 320,
51
+ },
52
+ {
53
+ tier: 'durable_recap',
54
+ purpose: 'Surface durable recap text, focused around the user query when possible.',
55
+ priority: 1,
56
+ maxBytes: 1600,
57
+ },
58
+ ];
59
+ exports.RESUME_PREVIEW_CONTRACT = {
60
+ surface: 'resume_preview',
61
+ tiers: RESUME_PREVIEW_TIER_DEFINITIONS,
62
+ budgetBytes: constants_js_1.MAX_RESUME_PREVIEW_BYTES,
63
+ };
64
+ const encoder = new TextEncoder();
65
+ const decoder = new TextDecoder('utf-8');
66
+ function getTierPriority(tier) {
67
+ return exports.REHYDRATE_RETRIEVAL_CONTRACT.tiers.find((candidate) => candidate.tier === tier)?.priority ?? Number.MAX_SAFE_INTEGER;
68
+ }
69
+ function getTierRetention(tier) {
70
+ return exports.REHYDRATE_RETRIEVAL_CONTRACT.tiers.find((candidate) => candidate.tier === tier)?.retention ?? 'tail';
71
+ }
72
+ function getResumePreviewTierPriority(tier) {
73
+ return exports.RESUME_PREVIEW_CONTRACT.tiers.find((candidate) => candidate.tier === tier)?.priority ?? Number.MAX_SAFE_INTEGER;
74
+ }
75
+ function getResumePreviewTierMaxBytes(tier) {
76
+ return exports.RESUME_PREVIEW_CONTRACT.tiers.find((candidate) => candidate.tier === tier)?.maxBytes ?? exports.RESUME_PREVIEW_CONTRACT.budgetBytes;
77
+ }
78
+ function compareAscii(a, b) {
79
+ return a < b ? -1 : a > b ? 1 : 0;
80
+ }
81
+ function trimToUtf8Boundary(bytes) {
82
+ const n = bytes.length;
83
+ if (n === 0)
84
+ return bytes;
85
+ let cont = 0;
86
+ for (let i = n - 1; i >= 0 && i >= n - 4; i--) {
87
+ const b = bytes[i];
88
+ if ((b & 192) === 128) {
89
+ cont++;
90
+ }
91
+ else {
92
+ break;
93
+ }
94
+ }
95
+ if (cont === 0)
96
+ return bytes;
97
+ const leadByteIndex = n - cont - 1;
98
+ if (leadByteIndex < 0) {
99
+ return new Uint8Array(0);
100
+ }
101
+ const leadByte = bytes[leadByteIndex];
102
+ const expectedLen = (leadByte & 128) === 0 ? 1 :
103
+ (leadByte & 224) === 192 ? 2 :
104
+ (leadByte & 240) === 224 ? 3 :
105
+ (leadByte & 248) === 240 ? 4 :
106
+ 0;
107
+ const actualLen = cont + 1;
108
+ if (expectedLen === 0 || expectedLen !== actualLen) {
109
+ return bytes.subarray(0, leadByteIndex);
110
+ }
111
+ return bytes;
112
+ }
113
+ function truncateUtf8(text, maxBytes) {
114
+ const bytes = encoder.encode(text);
115
+ if (bytes.length <= maxBytes) {
116
+ return text;
117
+ }
118
+ return decoder.decode(trimToUtf8Boundary(bytes.subarray(0, Math.max(0, maxBytes))));
119
+ }
120
+ function buildOmissionSuffix(omittedTierCount) {
121
+ const omissionLine = omittedTierCount > 0
122
+ ? `\nOmitted ${omittedTierCount} lower-priority tier${omittedTierCount === 1 ? '' : 's'} due to budget constraints.`
123
+ : '\nOmitted recovery content due to budget constraints.';
124
+ return `${constants_js_1.TRUNCATION_MARKER}${omissionLine}`;
125
+ }
126
+ function trimFinalRecoveryText(text, omittedTierCount) {
127
+ const suffix = buildOmissionSuffix(omittedTierCount);
128
+ const maxContentBytes = constants_js_1.RECOVERY_BUDGET_BYTES - encoder.encode(suffix).length;
129
+ const truncated = truncateUtf8(text, maxContentBytes);
130
+ return truncated + suffix;
131
+ }
132
+ function normalizePreviewFocusTerms(focusTerms) {
133
+ return [...new Set(focusTerms.map((term) => term.trim().toLowerCase()).filter((term) => term.length >= 3))];
134
+ }
135
+ function findFocusIndex(text, focusTerms) {
136
+ if (focusTerms.length === 0)
137
+ return -1;
138
+ const lower = text.toLowerCase();
139
+ return focusTerms.reduce((bestIndex, term) => {
140
+ const idx = lower.indexOf(term);
141
+ if (idx === -1)
142
+ return bestIndex;
143
+ if (bestIndex === -1)
144
+ return idx;
145
+ return Math.min(bestIndex, idx);
146
+ }, -1);
147
+ }
148
+ function excerptAroundFocus(text, maxBytes, focusTerms) {
149
+ const focusIndex = findFocusIndex(text, focusTerms);
150
+ if (focusIndex === -1) {
151
+ const truncated = truncateUtf8(text, maxBytes);
152
+ return truncated.length < text.length ? `${truncated}...` : truncated;
153
+ }
154
+ const contextChars = Math.max(80, Math.floor(maxBytes / 4));
155
+ const start = Math.max(0, focusIndex - contextChars);
156
+ const end = Math.min(text.length, focusIndex + contextChars * 2);
157
+ const slice = text.slice(start, end).trim();
158
+ const prefix = start > 0 ? '...' : '';
159
+ const suffix = end < text.length ? '...' : '';
160
+ const excerpt = `${prefix}${slice}${suffix}`;
161
+ return truncateUtf8(excerpt, maxBytes);
162
+ }
163
+ function createBranchSummarySegment(body) {
164
+ const trimmed = body.trim();
165
+ return trimmed.length === 0
166
+ ? null
167
+ : {
168
+ kind: 'branch_summary',
169
+ tier: 'structural_context',
170
+ source: 'deterministic_structure',
171
+ title: 'Branch Summary',
172
+ body: trimmed,
173
+ };
174
+ }
175
+ function createDownstreamRecapSegment(body) {
176
+ const trimmed = body.trim();
177
+ return trimmed.length === 0
178
+ ? null
179
+ : {
180
+ kind: 'downstream_recap',
181
+ tier: 'structural_context',
182
+ source: 'explicit_durable_fact',
183
+ title: 'Downstream Recap (Preferred Branch)',
184
+ body: trimmed,
185
+ };
186
+ }
187
+ function createAncestryRecapSegment(body) {
188
+ const trimmed = body.trim();
189
+ return trimmed.length === 0
190
+ ? null
191
+ : {
192
+ kind: 'ancestry_recap',
193
+ tier: 'durable_recap',
194
+ source: 'explicit_durable_fact',
195
+ title: 'Ancestry Recap',
196
+ body: trimmed,
197
+ };
198
+ }
199
+ function createFunctionDefinitionsSegment(body) {
200
+ const trimmed = body.trim();
201
+ return trimmed.length === 0
202
+ ? null
203
+ : {
204
+ kind: 'function_definitions',
205
+ tier: 'reference_material',
206
+ source: 'workflow_definition',
207
+ title: 'Function Definitions',
208
+ body: trimmed,
209
+ };
210
+ }
211
+ function createSessionTitlePreviewSegment(body) {
212
+ const trimmed = body.trim();
213
+ return trimmed.length === 0
214
+ ? null
215
+ : {
216
+ kind: 'session_title_preview',
217
+ tier: 'identity_context',
218
+ source: 'persisted_context',
219
+ body: trimmed,
220
+ };
221
+ }
222
+ function createRecapPreviewSegment(body) {
223
+ const trimmed = body.trim();
224
+ return trimmed.length === 0
225
+ ? null
226
+ : {
227
+ kind: 'recap_preview',
228
+ tier: 'durable_recap',
229
+ source: 'explicit_durable_fact',
230
+ body: trimmed,
231
+ };
232
+ }
233
+ function compareRetrievalPackSegments(a, b) {
234
+ const tierDiff = getTierPriority(a.tier) - getTierPriority(b.tier);
235
+ if (tierDiff !== 0)
236
+ return tierDiff;
237
+ const titleDiff = compareAscii(a.title, b.title);
238
+ if (titleDiff !== 0)
239
+ return titleDiff;
240
+ return compareAscii(a.body, b.body);
241
+ }
242
+ function orderRetrievalPackSegments(segments) {
243
+ return [...segments].sort(compareRetrievalPackSegments);
244
+ }
245
+ function renderRetrievalPackSections(segments) {
246
+ return orderRetrievalPackSegments(segments).map((segment) => `### ${segment.title}\n${segment.body}`);
247
+ }
248
+ function compareResumePreviewSegments(a, b) {
249
+ const tierDiff = getResumePreviewTierPriority(a.tier) - getResumePreviewTierPriority(b.tier);
250
+ if (tierDiff !== 0)
251
+ return tierDiff;
252
+ return compareAscii(a.body, b.body);
253
+ }
254
+ function renderBudgetedResumePreview(args) {
255
+ const ordered = [...args.segments].sort(compareResumePreviewSegments);
256
+ if (ordered.length === 0) {
257
+ return { text: '', includedTiers: [] };
258
+ }
259
+ const focusTerms = normalizePreviewFocusTerms(args.focusTerms ?? []);
260
+ const tierTexts = ordered.map((segment) => {
261
+ const maxBytes = getResumePreviewTierMaxBytes(segment.tier);
262
+ return {
263
+ tier: segment.tier,
264
+ text: excerptAroundFocus(segment.body, maxBytes, focusTerms),
265
+ };
266
+ });
267
+ const joined = tierTexts.map((entry) => entry.text).filter((text) => text.length > 0).join('\n\n');
268
+ const finalText = truncateUtf8(joined, exports.RESUME_PREVIEW_CONTRACT.budgetBytes);
269
+ const includedTiers = [...new Set(tierTexts.filter((entry) => entry.text.length > 0).map((entry) => entry.tier))];
270
+ return { text: finalText, includedTiers };
271
+ }
272
+ function renderBudgetedRehydrateRecovery(args) {
273
+ const ordered = orderRetrievalPackSegments(args.segments);
274
+ if (ordered.length === 0) {
275
+ return { text: '', includedTiers: [], omittedTierCount: 0, truncatedWithinTier: false };
276
+ }
277
+ const tiersInOrder = exports.REHYDRATE_RETRIEVAL_CONTRACT.tiers.map((tier) => tier.tier);
278
+ const sectionsByTier = new Map(tiersInOrder.map((tier) => [tier, ordered.filter((segment) => segment.tier === tier).map((segment) => `### ${segment.title}\n${segment.body}`)]));
279
+ const renderFromTiers = (tiers) => {
280
+ const sections = tiers.flatMap((tier) => sectionsByTier.get(tier) ?? []);
281
+ return sections.length === 0 ? '' : `${args.header}\n\n${sections.join('\n\n')}`;
282
+ };
283
+ const initiallyIncludedTiers = tiersInOrder.filter((tier) => (sectionsByTier.get(tier) ?? []).length > 0);
284
+ let includedTiers = initiallyIncludedTiers;
285
+ let recoveryText = renderFromTiers(includedTiers);
286
+ while (encoder.encode(recoveryText).length > constants_js_1.RECOVERY_BUDGET_BYTES) {
287
+ const droppableTierIndex = [...includedTiers]
288
+ .reverse()
289
+ .findIndex((tier) => getTierRetention(tier) === 'tail');
290
+ if (droppableTierIndex === -1) {
291
+ break;
292
+ }
293
+ const actualIndex = includedTiers.length - 1 - droppableTierIndex;
294
+ includedTiers = includedTiers.filter((_, index) => index !== actualIndex);
295
+ recoveryText = renderFromTiers(includedTiers);
296
+ }
297
+ const omittedTierCount = initiallyIncludedTiers.length - includedTiers.length;
298
+ const needsSuffix = omittedTierCount > 0 || encoder.encode(recoveryText).length > constants_js_1.RECOVERY_BUDGET_BYTES || includedTiers.length === 0;
299
+ const finalText = recoveryText.length === 0
300
+ ? trimFinalRecoveryText(args.header, initiallyIncludedTiers.length)
301
+ : !needsSuffix
302
+ ? recoveryText
303
+ : trimFinalRecoveryText(recoveryText, omittedTierCount);
304
+ return {
305
+ text: finalText,
306
+ includedTiers,
307
+ omittedTierCount,
308
+ truncatedWithinTier: encoder.encode(recoveryText).length > constants_js_1.RECOVERY_BUDGET_BYTES || includedTiers.length === 0,
309
+ };
310
+ }
@@ -1,4 +1,5 @@
1
1
  import type { SessionId, WorkflowHash, WorkflowId } from '../durable-core/ids/index.js';
2
+ export { MAX_RESUME_PREVIEW_BYTES } from '../durable-core/constants.js';
2
3
  export type RecapSnippet = string & {
3
4
  readonly __brand: 'RecapSnippet';
4
5
  };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MAX_RESUME_CANDIDATES = void 0;
3
+ exports.MAX_RESUME_CANDIDATES = exports.MAX_RESUME_PREVIEW_BYTES = void 0;
4
4
  exports.asRecapSnippet = asRecapSnippet;
5
5
  exports.normalizeToTokens = normalizeToTokens;
6
6
  exports.allQueryTokensMatch = allQueryTokensMatch;
@@ -11,7 +11,10 @@ exports.computeQueryRelevanceScore = computeQueryRelevanceScore;
11
11
  exports.assignTier = assignTier;
12
12
  exports.rankResumeCandidates = rankResumeCandidates;
13
13
  const constants_js_1 = require("../durable-core/constants.js");
14
- const MAX_SNIPPET_BYTES = 1024;
14
+ const retrieval_contract_js_1 = require("../durable-core/domain/retrieval-contract.js");
15
+ var constants_js_2 = require("../durable-core/constants.js");
16
+ Object.defineProperty(exports, "MAX_RESUME_PREVIEW_BYTES", { enumerable: true, get: function () { return constants_js_2.MAX_RESUME_PREVIEW_BYTES; } });
17
+ const MAX_SNIPPET_BYTES = constants_js_1.MAX_RESUME_PREVIEW_BYTES;
15
18
  function asRecapSnippet(raw) {
16
19
  const stripped = raw.endsWith(constants_js_1.TRUNCATION_MARKER)
17
20
  ? raw.slice(0, -constants_js_1.TRUNCATION_MARKER.length)
@@ -145,29 +148,14 @@ function collectMatchReasons(summary, query, tier) {
145
148
  return reasons;
146
149
  }
147
150
  function buildPreviewSnippet(summary, query) {
148
- const previewSource = buildSearchableSessionText(summary);
149
- if (!previewSource)
151
+ const segments = [
152
+ summary.sessionTitle ? (0, retrieval_contract_js_1.createSessionTitlePreviewSegment)(summary.sessionTitle) : null,
153
+ summary.recapSnippet ? (0, retrieval_contract_js_1.createRecapPreviewSegment)(summary.recapSnippet) : null,
154
+ ].filter((segment) => segment !== null);
155
+ if (segments.length === 0)
150
156
  return '';
151
- const queryTokens = query.freeTextQuery ? [...normalizeToTokens(query.freeTextQuery)] : [];
152
- if (queryTokens.length === 0)
153
- return summary.recapSnippet ?? previewSource;
154
- const lower = previewSource.toLowerCase();
155
- let bestIndex = -1;
156
- for (const token of queryTokens) {
157
- if (token.length < 3)
158
- continue;
159
- const idx = lower.indexOf(token);
160
- if (idx !== -1 && (bestIndex === -1 || idx < bestIndex))
161
- bestIndex = idx;
162
- }
163
- if (bestIndex === -1)
164
- return summary.recapSnippet ?? previewSource;
165
- const start = Math.max(0, bestIndex - 100);
166
- const end = Math.min(previewSource.length, bestIndex + 180);
167
- const slice = previewSource.slice(start, end).trim();
168
- const prefix = start > 0 ? '...' : '';
169
- const suffix = end < previewSource.length ? '...' : '';
170
- return `${prefix}${slice}${suffix}`;
157
+ const focusTerms = query.freeTextQuery ? [...normalizeToTokens(query.freeTextQuery)] : [];
158
+ return (0, retrieval_contract_js_1.renderBudgetedResumePreview)({ segments, focusTerms }).text;
171
159
  }
172
160
  function deriveConfidence(tier, reasons) {
173
161
  if (tier.kind === 'matched_exact_id' || tier.kind === 'matched_notes')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exaudeus/workrail",
3
- "version": "3.9.2",
3
+ "version": "3.10.0",
4
4
  "description": "Step-by-step workflow enforcement for AI agents via MCP",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,19 +19,22 @@
19
19
  "DEFAULT BEHAVIOR: self-execute with tools. Only ask for missing external artifacts, permissions, or business context you cannot resolve yourself.",
20
20
  "V2 DURABILITY: use output.notesMarkdown and explicit `continue_workflow` context keys as durable workflow state. Do NOT rely on the review document as required workflow memory.",
21
21
  "ARTIFACT STRATEGY: `reviewDocPath` is an optional human-facing artifact only. Create or update it only when it materially improves handoff or readability. Workflow truth lives in notes and explicit context fields.",
22
+ "NOTES QUALITY: notes should work for both a human reader and a future agent resuming later. For important phases, make clear what was learned, what was decided, what remains uncertain, and what should happen next.",
22
23
  "OWNERSHIP & DELEGATION: the main agent owns truth, synthesis, severity calibration, recommendation, and final handoff. Delegate only bounded reviewer or validation work through the WorkRail Executor.",
23
24
  "SUBAGENT SYNTHESIS: treat reviewer-family and validator output as evidence, not conclusions. State your current hypothesis before delegation, then say what was confirmed, what was new, what you reject, and what changed your mind.",
24
25
  "PARALLELISM: parallelize independent cognition; serialize canonical synthesis, coverage-ledger updates, recommendation decisions, and document writes.",
25
26
  "REVIEW MODEL: first build shared understanding, then freeze a neutral fact packet, then let reviewer families challenge it in parallel, then reconcile contradictions explicitly.",
26
27
  "COVERAGE LEDGER: explicitly track review domains as `checked`, `uncertain`, `not_applicable`, `contradicted`, or `needs_followup`. Do not finalize with unresolved material gaps unless you name them clearly.",
27
28
  "TRIGGERS: WorkRail can only react to explicit fields. Use structural fields such as `contextUnknownCount`, `criticalSurfaceTouched`, `coverageUncertainCount`, `contradictionCount`, `falsePositiveRiskCount`, `blindSpotCount`, and `needsSimulation`.",
29
+ "BOUNDARY DISCIPLINE: attempt to determine the real review target and the likely ancestor-relative review surface. If that confidence remains weak, continue with downgraded confidence and disclose the limitation clearly instead of pretending certainty.",
30
+ "SOURCE DISCOVERY: opportunistically recover PR context, ticket/docs context, and repo/user policy context from the strongest available sources. Missing sources should usually lower confidence and be disclosed, not block the workflow.",
28
31
  "BOUNDARY: do not post comments, approve, reject, or merge unless the user explicitly asks. Produce findings, recommendation, and handoff material only."
29
32
  ],
30
33
  "steps": [
31
34
  {
32
35
  "id": "phase-0-understand-and-classify",
33
- "title": "Phase 0: Understand & Classify",
34
- "prompt": "Build understanding and classify the review in one pass.\n\nStep 1 — Early exit / minimum inputs:\nBefore exploring, verify that the review target is real and inspectable. If the diff, changed files, or equivalent review material are completely absent and cannot be inferred with tools, ask for the minimum missing artifact and stop. Do NOT ask questions you can resolve with tools.\n\nStep 2 — Explore:\nUse tools to build the minimum complete understanding needed to review accurately. Read independent files in parallel when possible.\n\nGather:\n- MR title and purpose, if discoverable\n- ticket or acceptance-criteria context when available\n- changed files overview and changed-file count\n- module roots, call chain highlights, public contracts, impacted consumers, and repo patterns that matter\n- explicit unknowns, likely blind spots, and whether author intent remains unclear\n- whether any critical surface is touched\n\nStep 3 — Classify after exploration:\nSet:\n- `reviewMode`: QUICK / STANDARD / THOROUGH\n- `riskLevel`: Low / Medium / High\n- `maxParallelism`: 0 / 3 / 5\n- `criticalSurfaceTouched`: true / false\n- `needsSimulation`: true / false\n\nDecision guidance:\n- QUICK: very small, isolated, low-risk changes with little ambiguity\n- STANDARD: typical feature or bug-fix reviews with moderate ambiguity or moderate risk\n- THOROUGH: critical surfaces, architectural novelty, high risk, broad change sets, or strong need for independent reviewer perspectives\n\nStep 4 — Optional deeper context:\nIf `reviewMode` is STANDARD or THOROUGH and understanding still feels incomplete, and delegation is available, spawn TWO WorkRail Executors SIMULTANEOUSLY running `routine-context-gathering` with focus=COMPLETENESS and focus=DEPTH. Synthesize both outputs before finishing this step.\n\nStep 5 — Human-facing artifact:\nChoose `reviewDocPath` only if a live artifact will materially improve human readability. Default suggestion: `mr-review.md` at the project root. This artifact is optional and never canonical workflow state.\n\nSet these keys in the next `continue_workflow` call's `context` object:\n- `mrTitle`\n- `mrPurpose`\n- `ticketContext`\n- `focusAreas`\n- `changedFileCount`\n- `criticalSurfaceTouched`\n- `reviewMode`\n- `riskLevel`\n- `maxParallelism`\n- `reviewDocPath`\n- `contextSummary`\n- `candidateFiles`\n- `moduleRoots`\n- `contextUnknownCount`\n- `coverageGapCount`\n- `authorIntentUnclear`\n- `needsSimulation`\n- `openQuestions`\n\nRules:\n- answer your own questions with tools whenever possible\n- only keep true human-decision questions in `openQuestions`\n- keep `openQuestions` bounded to the minimum necessary\n- if the review target is missing entirely, ask only for that missing artifact\n- classify AFTER exploring, not before",
36
+ "title": "Phase 0: Locate, Bound, Enrich & Classify",
37
+ "prompt": "Build the review foundation in one pass.\n\nStep 1 — Early exit / minimum inputs:\nBefore exploring, verify that the review target is real and inspectable. If the diff, changed files, or equivalent review material are completely absent and cannot be inferred with tools, ask for the minimum missing artifact and stop. Do NOT ask questions you can resolve with tools.\n\nStep 2 — Locate and bound the review target:\nAttempt to determine the strongest available review target and boundary.\n\nAttempt to establish:\n- `reviewTargetKind` from the strongest available source such as PR/MR, branch, patch, diff, or local working tree changes\n- `reviewTargetSource` describing where the target came from\n- likely PR/MR identity when available (`prUrl`, `prNumber`)\n- likely base / ancestor reference (`baseCandidate`, `mergeBaseRef`) when available\n- whether the branch may include inherited or out-of-scope changes\n- `boundaryConfidence`: High / Medium / Low\n\nDo not over-prescribe your own investigation path. Use the strongest available evidence and record uncertainty honestly.\n\nStep 3 — Enrich with context:\nRecover the strongest available intent and policy context from whatever sources are actually available.\n\nAttempt to recover:\n- MR title and purpose\n- ticket / issue / acceptance context (`ticketRefs`, `ticketContext`)\n- supporting docs / specs / rollout context (`supportingDocsFound`)\n- repo or user policy/convention context when it is likely to affect review judgment (`policySourcesFound`)\n- `contextConfidence`: High / Medium / Low\n\nStep 4 Review-surface hygiene:\nClassify the visible change into a minimal review surface.\n\nSet:\n- `coreReviewSurface`\n- `likelyNoiseOrMechanicalChurn`\n- `likelyInheritedOrOutOfScopeChanges`\n- `reviewSurfaceSummary`\n- `reviewScopeWarnings`\n\nThe goal is not a giant ledger. The goal is to avoid treating every visible changed file as equally worthy of deep review by default.\n\nStep 5 — Classify the review:\nAfter exploration, classify the work.\n\nSet:\n- `reviewMode`: QUICK / STANDARD / THOROUGH\n- `riskLevel`: Low / Medium / High\n- `shapeProfile`: choose the best primary label from `isolated_change`, `crosscutting_change`, `mechanically_noisy_change`, or `ambiguous_boundary`\n- `changeTypeProfile`: choose the best primary label from `general_code_change`, `api_contract_change`, `data_model_or_migration`, `security_sensitive`, or `test_only`\n- `maxParallelism`: 0 / 3 / 5\n- `criticalSurfaceTouched`: true / false\n- `needsSimulation`: true / false\n- `needsBoundaryFollowup`: true / false\n- `needsContextFollowup`: true / false\n- `needsReviewerBundle`: true / false\n\nDecision guidance:\n- QUICK: very small, isolated, low-risk changes with little ambiguity\n- STANDARD: typical feature or bug-fix reviews with moderate ambiguity or moderate risk\n- THOROUGH: critical surfaces, architectural novelty, high risk, broad change sets, or strong need for independent reviewer perspectives\n\nMinimal routing guidance:\n- if `boundaryConfidence = Low`, bias toward boundary/context follow-up before strong recommendation confidence\n- if `changeTypeProfile = api_contract_change`, bias toward contract/consumer/backward-compatibility scrutiny\n- if `changeTypeProfile = data_model_or_migration`, bias toward rollout / compatibility / simulation scrutiny\n- if `changeTypeProfile = security_sensitive`, bias toward adversarial/runtime-risk scrutiny and lower tolerance for weak evidence\n- if `changeTypeProfile = test_only`, bias toward stronger false-positive suppression\n- if `shapeProfile = mechanically_noisy_change`, bias toward stronger noise filtering and lower appetite for style-only findings\n\nStep 6 — Optional deeper context:\nIf `reviewMode` is STANDARD or THOROUGH and context remains incomplete, and delegation is available, spawn TWO WorkRail Executors SIMULTANEOUSLY running `routine-context-gathering` with focus=COMPLETENESS and focus=DEPTH. Synthesize both outputs before finishing this step.\n\nStep 7 — Human-facing artifact:\nChoose `reviewDocPath` only if a live artifact will materially improve human readability. Default suggestion: `mr-review.md` at the project root. This artifact is optional and never canonical workflow state.\n\nFallback behavior:\n- if PR/MR is not found but a branch/diff is inspectable, continue with downgraded context confidence and disclose missing PR context later\n- if the branch is inspectable but merge-base / ancestor remains ambiguous, continue with downgraded boundary confidence, set `needsBoundaryFollowup = true`, and disclose the uncertainty later\n- if ticket or supporting docs are missing, continue with downgraded context confidence and avoid overclaiming intent-sensitive findings\n- if only a patch/diff is available, continue if it is inspectable, but keep lower confidence on intent/boundary-dependent conclusions\n- if the review target itself is missing, ask only for that missing artifact and stop\n\nSet these keys in the next `continue_workflow` call's `context` object:\n- `reviewTargetKind`\n- `reviewTargetSource`\n- `prUrl`\n- `prNumber`\n- `baseCandidate`\n- `mergeBaseRef`\n- `boundaryConfidence`\n- `contextConfidence`\n- `mrTitle`\n- `mrPurpose`\n- `ticketRefs`\n- `ticketContext`\n- `supportingDocsFound`\n- `policySourcesFound`\n- `accessibleContextSources`\n- `missingContextSources`\n- `focusAreas`\n- `changedFileCount`\n- `criticalSurfaceTouched`\n- `reviewMode`\n- `riskLevel`\n- `shapeProfile`\n- `changeTypeProfile`\n- `maxParallelism`\n- `reviewDocPath`\n- `contextSummary`\n- `candidateFiles`\n- `moduleRoots`\n- `contextUnknownCount`\n- `coverageGapCount`\n- `authorIntentUnclear`\n- `needsSimulation`\n- `needsBoundaryFollowup`\n- `needsContextFollowup`\n- `needsReviewerBundle`\n- `coreReviewSurface`\n- `likelyNoiseOrMechanicalChurn`\n- `likelyInheritedOrOutOfScopeChanges`\n- `reviewSurfaceSummary`\n- `reviewScopeWarnings`\n- `openQuestions`\n\nRules:\n- answer your own questions with tools whenever possible\n- only keep true human-decision questions in `openQuestions`\n- keep `openQuestions` bounded to the minimum necessary\n- classify AFTER exploring, not before\n- before leaving this phase, either establish the likely review boundary or explicitly record why you could not",
35
38
  "requireConfirmation": {
36
39
  "or": [
37
40
  { "var": "reviewMode", "equals": "THOROUGH" },
@@ -58,12 +61,13 @@
58
61
  "Keep `recommendationHypothesis` as a secondary hypothesis to challenge, not a frame to defend."
59
62
  ],
60
63
  "procedure": [
61
- "Create a neutral `reviewFactPacket` containing: MR purpose and expected behavior change, changed files and module roots, key contracts / invariants / affected consumers, call-chain highlights, relevant repo patterns and exemplars, tests/docs expectations, and explicit open unknowns.",
64
+ "Create a neutral `reviewFactPacket` containing: MR purpose and expected behavior change, review target and review-surface summary, changed files and module roots, key contracts / invariants / affected consumers, call-chain highlights, relevant repo patterns and exemplars, tests/docs expectations, discovered ticket/doc/policy context, accessible and missing context sources, and explicit open unknowns.",
62
65
  "Initialize `coverageLedger` for these domains: `correctness_logic`, `contracts_invariants`, `patterns_architecture`, `runtime_production_risk`, `tests_docs_rollout`, `security_performance`.",
63
66
  "Perform a preliminary self-review from the fact packet before choosing reviewer families.",
64
67
  "Reviewer family options: `correctness_invariants`, `patterns_architecture`, `runtime_production_risk`, `test_docs_rollout`, `false_positive_skeptic`, `missed_issue_hunter`.",
65
68
  "Selection guidance: QUICK = no bundle by default unless ambiguity still feels material; STANDARD = 3 families by default; THOROUGH = 5 families by default.",
66
69
  "Always include `correctness_invariants` unless clearly not applicable. Include `test_docs_rollout` in STANDARD and THOROUGH unless clearly not applicable. Include `runtime_production_risk` when `criticalSurfaceTouched = true` or `needsSimulation = true`. Include `missed_issue_hunter` in THOROUGH. Include `false_positive_skeptic` when Major/Critical findings seem plausible or severity inflation risk is non-trivial.",
70
+ "Routing guidance: for `api_contract_change`, bias toward contract / consumer / backward-compatibility scrutiny; for `data_model_or_migration`, bias toward rollout / compatibility / simulation scrutiny; for `security_sensitive`, bias toward runtime-risk scrutiny and lower tolerance for weak evidence; for `test_only`, bias toward stronger false-positive suppression; for `mechanically_noisy_change`, bias toward stronger noise filtering and lower appetite for style-only findings.",
67
71
  "Set `coverageUncertainCount` as the number of coverage domains not yet safely closed: `uncertain` + `contradicted` + `needs_followup`.",
68
72
  "Initialize `contradictionCount`, `blindSpotCount`, and `falsePositiveRiskCount` to `0` if no reviewer-family bundle will run."
69
73
  ],
@@ -191,8 +195,9 @@
191
195
  "Before delegating, state: what is your current recommendation, where are you least confident, and what finding would most likely change your mind now?",
192
196
  "Mode-adaptive validation: QUICK = self-validate and optionally spawn ONE WorkRail Executor running `routine-hypothesis-challenge` if a serious uncertainty remains; STANDARD = if validation is required and delegation is available, spawn TWO WorkRail Executors SIMULTANEOUSLY running `routine-hypothesis-challenge` and either `routine-execution-simulation` or `routine-plan-analysis`; THOROUGH = if validation is required and delegation is available, spawn THREE WorkRail Executors SIMULTANEOUSLY running `routine-hypothesis-challenge`, `routine-execution-simulation` when needed, and `routine-plan-analysis`.",
193
197
  "After receiving validator output, explicitly synthesize what was confirmed, what was new, what appears weak, and whether your recommendation changed.",
198
+ "Perform a compact confidence assessment using these dimensions: `boundaryConfidence`, `intentConfidence`, `evidenceConfidence`, `coverageConfidence`, and `consensusConfidence`. Rate each as High / Medium / Low, explain each in one sentence, and then derive final recommendation confidence with these rules: if boundary is Low, final confidence is Low; else if evidence is Low, final confidence is Low; else if 2 or more dimensions are Medium, final confidence is Medium; else if all key dimensions are High, final confidence is High. Unresolved disagreement can only lower confidence, never raise it.",
194
199
  "Compute `docCompletenessConcernCount` by counting one concern for each material packaging gap: missing rationale for any Critical or Major finding, missing ready-to-post MR comment for any Critical or Major finding, recommendation mismatch with canonical findings, still-uncertain / contradicted / needs-followup coverage domains not summarized clearly, or any missing required final section needed for actionability.",
195
- "Set these keys in the next `continue_workflow` call's `context` object: `validatorConsensusLevel`, `validationSummary`, `recommendationConfidenceBand`, `docCompletenessConcernCount`."
200
+ "Set these keys in the next `continue_workflow` call's `context` object: `intentConfidence`, `evidenceConfidence`, `coverageConfidence`, `consensusConfidence`, `confidenceAssessmentSummary`, `validatorConsensusLevel`, `validationSummary`, `recommendationConfidenceBand`, `docCompletenessConcernCount`."
196
201
  ],
197
202
  "verify": [
198
203
  "If 2+ validators still raise serious concerns, confidence is downgraded and synthesis is reopened.",
@@ -210,7 +215,7 @@
210
215
  {
211
216
  "id": "phase-6-final-handoff",
212
217
  "title": "Phase 6: Final Handoff",
213
- "prompt": "Provide the final MR review handoff.\n\nInclude:\n- MR title and purpose\n- review mode used\n- final recommendation and confidence band\n- counts of Critical / Major / Minor / Nit findings\n- top findings with rationale\n- strongest remaining areas of uncertainty, if any\n- summary of the coverage ledger, especially any still-uncertain domains\n- ready-to-post MR comments summary\n- any validation outcomes a human reviewer should see\n- path to the full human-facing review artifact (`reviewDocPath`) only if one was created\n\nRules:\n- the final recommendation assists a human reviewer; it does not replace them\n- if `reviewDocPath` exists, treat it as a human-facing companion artifact only\n- do not post comments, approve, reject, or merge unless the user explicitly asks",
218
+ "prompt": "Provide the final MR review handoff.\n\nInclude:\n- MR title and purpose\n- review mode used\n- final recommendation and confidence band\n- confidence assessment summary, including the most important reason confidence was capped if it was not High\n- counts of Critical / Major / Minor / Nit findings\n- top findings with rationale\n- strongest remaining areas of uncertainty, if any\n- summary of the coverage ledger, especially any still-uncertain domains\n- ready-to-post MR comments summary\n- any validation outcomes a human reviewer should see\n- review environment status:\n - what review target/context sources were successfully used\n - what important sources were missing or ambiguous\n - boundary confidence and context confidence\n - how those limits affected the review\n- path to the full human-facing review artifact (`reviewDocPath`) only if one was created\n\nRules:\n- the final recommendation assists a human reviewer; it does not replace them\n- if `reviewDocPath` exists, treat it as a human-facing companion artifact only\n- be explicit when missing PR/ticket/doc/boundary context limited confidence\n- do not post comments, approve, reject, or merge unless the user explicitly asks",
214
219
  "requireConfirmation": true
215
220
  }
216
221
  ]