@danielblomma/cortex-mcp 2.0.15 → 2.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +5 -3
  2. package/package.json +1 -1
  3. package/scaffold/mcp/package-lock.json +59 -60
  4. package/scaffold/mcp/package.json +2 -2
  5. package/scaffold/mcp/src/cli/stage.ts +15 -18
  6. package/scaffold/mcp/src/cli/telemetry-test.ts +2 -1
  7. package/scaffold/mcp/src/core/license.ts +3 -2
  8. package/scaffold/mcp/src/core/telemetry/collector.ts +5 -4
  9. package/scaffold/mcp/src/core/telemetry/state-dir.ts +40 -0
  10. package/scaffold/mcp/src/core/validators/builtins.ts +24 -4
  11. package/scaffold/mcp/src/core/workflow/index.ts +1 -0
  12. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +15 -25
  13. package/scaffold/mcp/src/core/workflow/resolution.ts +113 -0
  14. package/scaffold/mcp/src/daemon/main.ts +4 -3
  15. package/scaffold/mcp/src/enterprise/index.ts +16 -1
  16. package/scaffold/mcp/src/enterprise/reviews/policy-selection.ts +65 -0
  17. package/scaffold/mcp/src/enterprise/reviews/push.ts +55 -4
  18. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +305 -0
  19. package/scaffold/mcp/src/enterprise/tools/enterprise.ts +40 -47
  20. package/scaffold/mcp/src/enterprise/tools/harness.ts +2 -0
  21. package/scaffold/mcp/src/enterprise/workflow/state.ts +21 -0
  22. package/scaffold/mcp/tests/enterprise-review-trust.test.mjs +318 -0
  23. package/scaffold/mcp/tests/fixtures/org-skillz-contract.json +59 -0
  24. package/scaffold/mcp/tests/fixtures/review-trust-contract.json +111 -0
  25. package/scaffold/mcp/tests/review-policy-selection.test.mjs +85 -0
  26. package/scaffold/mcp/tests/review-trust-contract.test.mjs +167 -0
  27. package/scaffold/mcp/tests/review-validator-source.test.mjs +52 -0
  28. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +277 -0
  29. package/scaffold/mcp/tests/telemetry-collector.test.mjs +31 -1
  30. package/scaffold/mcp/tests/workflow-cli.test.mjs +102 -1
  31. package/scaffold/mcp/tests/workflow-mcp-tools.test.mjs +22 -0
  32. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +56 -0
  33. package/scaffold/scripts/parsers/node_modules/.package-lock.json +0 -1
@@ -0,0 +1,113 @@
1
+ import { DEFAULT_WORKFLOWS } from "./default-workflows.js";
2
+ import { loadSyncedWorkflows } from "./synced-registry.js";
3
+ import type { WorkflowDefinition } from "./schemas.js";
4
+
5
+ export type WorkflowSource = "bundled" | "synced" | "injected";
6
+
7
+ export type WorkflowResolutionWarning = {
8
+ code: "bundled-workflow-fallback";
9
+ workflow_id: string;
10
+ message: string;
11
+ bundled_available: boolean;
12
+ synced_available: boolean;
13
+ };
14
+
15
+ export type WorkflowResolution = {
16
+ workflow: WorkflowDefinition;
17
+ source: WorkflowSource;
18
+ warnings: WorkflowResolutionWarning[];
19
+ bundled_ids: string[];
20
+ synced_ids: string[];
21
+ available_ids: string[];
22
+ };
23
+
24
+ type ResolveWorkflowOptions = {
25
+ registry?: Record<string, WorkflowDefinition>;
26
+ syncedDir?: string;
27
+ emitBundledFallbackWarning?: boolean;
28
+ bundledFallbackPolicy?: "allow" | "warn" | "block";
29
+ };
30
+
31
+ function sortIds(ids: Iterable<string>): string[] {
32
+ return [...ids].sort((left, right) => left.localeCompare(right));
33
+ }
34
+
35
+ function formatIds(ids: string[]): string {
36
+ return ids.length > 0 ? ids.join(", ") : "<none>";
37
+ }
38
+
39
+ export function resolveWorkflowDefinition(
40
+ workflowId: string,
41
+ options: ResolveWorkflowOptions = {},
42
+ ): WorkflowResolution {
43
+ if (options.registry) {
44
+ const availableIds = sortIds(Object.keys(options.registry));
45
+ const workflow = options.registry[workflowId];
46
+ if (!workflow) {
47
+ throw new Error(
48
+ `Unknown workflow_id: ${workflowId}. Available injected: ${formatIds(availableIds)}`,
49
+ );
50
+ }
51
+ return {
52
+ workflow,
53
+ source: "injected",
54
+ warnings: [],
55
+ bundled_ids: [],
56
+ synced_ids: [],
57
+ available_ids: availableIds,
58
+ };
59
+ }
60
+
61
+ const bundled = DEFAULT_WORKFLOWS;
62
+ const synced = loadSyncedWorkflows(options.syncedDir);
63
+ const bundledIds = sortIds(Object.keys(bundled));
64
+ const syncedIds = sortIds(Object.keys(synced));
65
+ const merged = { ...bundled, ...synced };
66
+ const availableIds = sortIds(Object.keys(merged));
67
+ const workflow = merged[workflowId];
68
+
69
+ if (!workflow) {
70
+ throw new Error(
71
+ `Unknown workflow_id: ${workflowId}. Available bundled: ${formatIds(bundledIds)}. ` +
72
+ `Available synced: ${formatIds(syncedIds)}. Available all: ${formatIds(availableIds)}`,
73
+ );
74
+ }
75
+
76
+ const source: WorkflowSource = Object.prototype.hasOwnProperty.call(synced, workflowId)
77
+ ? "synced"
78
+ : "bundled";
79
+ const warnings: WorkflowResolutionWarning[] = [];
80
+ const fallbackPolicy = options.bundledFallbackPolicy ?? "allow";
81
+
82
+ if (source === "bundled" && fallbackPolicy === "block") {
83
+ throw new Error(
84
+ `Workflow "${workflowId}" is only available from the bundled registry, ` +
85
+ "but enforced govern mode requires a synced org workflow. " +
86
+ `Available synced: ${formatIds(syncedIds)}.`,
87
+ );
88
+ }
89
+
90
+ if (
91
+ source === "bundled" &&
92
+ (options.emitBundledFallbackWarning || fallbackPolicy === "warn")
93
+ ) {
94
+ warnings.push({
95
+ code: "bundled-workflow-fallback",
96
+ workflow_id: workflowId,
97
+ bundled_available: true,
98
+ synced_available: syncedIds.length > 0,
99
+ message:
100
+ `Workflow "${workflowId}" was resolved from the bundled registry because ` +
101
+ "no synced org workflow with that id exists in the local cache.",
102
+ });
103
+ }
104
+
105
+ return {
106
+ workflow,
107
+ source,
108
+ warnings,
109
+ bundled_ids: bundledIds,
110
+ synced_ids: syncedIds,
111
+ available_ids: availableIds,
112
+ };
113
+ }
@@ -13,6 +13,7 @@ import type {
13
13
  import { loadEnterpriseConfig, resolveEnterpriseActivation } from "../core/config.js";
14
14
  import { pushMetrics } from "../enterprise/telemetry/sync.js";
15
15
  import { TelemetryCollector, type TelemetryMetrics } from "../core/telemetry/collector.js";
16
+ import { resolveTelemetryStateDir, telemetryStatePath } from "../core/telemetry/state-dir.js";
16
17
  import { AuditWriter, type AuditEntry } from "../core/audit/writer.js";
17
18
  import { PolicyStore } from "../core/policy/store.js";
18
19
  import {
@@ -84,7 +85,7 @@ async function policyCheck(
84
85
  }
85
86
 
86
87
  function readMetrics(contextDir: string): TelemetryMetrics | null {
87
- const path = join(contextDir, "telemetry", "metrics.json");
88
+ const path = telemetryStatePath(contextDir, "metrics.json");
88
89
  if (!existsSync(path)) return null;
89
90
  try {
90
91
  return JSON.parse(readFileSync(path, "utf8")) as TelemetryMetrics;
@@ -103,7 +104,7 @@ type PendingPush = {
103
104
  };
104
105
 
105
106
  function pendingPushPath(contextDir: string): string {
106
- return join(contextDir, "telemetry", "pending-push.json");
107
+ return telemetryStatePath(contextDir, "pending-push.json");
107
108
  }
108
109
 
109
110
  function readPendingPush(contextDir: string): PendingPush | null {
@@ -118,7 +119,7 @@ function readPendingPush(contextDir: string): PendingPush | null {
118
119
 
119
120
  function writePendingPush(contextDir: string, pending: PendingPush): void {
120
121
  const path = pendingPushPath(contextDir);
121
- mkdirSync(join(contextDir, "telemetry"), { recursive: true });
122
+ mkdirSync(resolveTelemetryStateDir(contextDir), { recursive: true });
122
123
  writeFileSync(path, JSON.stringify(pending, null, 2), "utf8");
123
124
  }
124
125
 
@@ -17,7 +17,8 @@ import { registerEnterpriseTools } from "./tools/enterprise.js";
17
17
  import { registerHarnessTools } from "./tools/harness.js";
18
18
  import { pushViolations, setViolationPushContext } from "./violations/push.js";
19
19
  import { pushReviewResults, setReviewPushContext } from "./reviews/push.js";
20
- import { setWorkflowPushContext } from "./workflow/push.js";
20
+ import { pushWorkflowSnapshot, setWorkflowPushContext } from "./workflow/push.js";
21
+ import { hasWorkflowState, loadWorkflowState } from "./workflow/state.js";
21
22
 
22
23
  const require = createRequire(import.meta.url);
23
24
  const pkg = require("../package.json") as { version: string };
@@ -32,6 +33,7 @@ let activeAuditWriter: AuditWriter | null = null;
32
33
  let activeInstanceId: string | null = null;
33
34
  let activeSessionId: string | null = null;
34
35
  let activeRepo: string | null = null;
36
+ let activeContextDir: string | null = null;
35
37
 
36
38
  async function flushComplianceQueues(
37
39
  config: EnterpriseConfig,
@@ -64,6 +66,15 @@ async function flushComplianceQueues(
64
66
  if (!result.success) {
65
67
  process.stderr.write(`[cortex-enterprise] ${reason} reviews push failed: ${result.error}\n`);
66
68
  }
69
+ if (result.attempted && activeContextDir && hasWorkflowState(activeContextDir)) {
70
+ const workflowState = loadWorkflowState(activeContextDir);
71
+ const workflowResult = await pushWorkflowSnapshot(baseUrl, apiKey, workflowState);
72
+ if (!workflowResult.success) {
73
+ process.stderr.write(
74
+ `[cortex-enterprise] ${reason} workflow snapshot refresh after review push failed: ${workflowResult.error}\n`,
75
+ );
76
+ }
77
+ }
67
78
  } catch (err) {
68
79
  process.stderr.write(`[cortex-enterprise] ${reason} reviews push error: ${err}\n`);
69
80
  }
@@ -113,6 +124,7 @@ export function shutdown(): void {
113
124
  activeInstanceId = null;
114
125
  activeSessionId = null;
115
126
  activeRepo = null;
127
+ activeContextDir = null;
116
128
  setAuditPushContext({});
117
129
  setViolationPushContext({});
118
130
  setReviewPushContext({});
@@ -248,6 +260,7 @@ export async function onSessionEvent(event: SessionEvent): Promise<void> {
248
260
  export async function register(server: McpServer): Promise<void> {
249
261
  const projectRoot = process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
250
262
  const contextDir = path.join(projectRoot, ".context");
263
+ activeContextDir = contextDir;
251
264
 
252
265
  const config = loadEnterpriseConfig(contextDir);
253
266
  const activation = resolveEnterpriseActivation(config);
@@ -295,6 +308,8 @@ export async function register(server: McpServer): Promise<void> {
295
308
  repo: activeRepo ?? undefined,
296
309
  instance_id: activeInstanceId ?? undefined,
297
310
  session_id: activeSessionId ?? undefined,
311
+ context_dir: contextDir,
312
+ project_root: projectRoot,
298
313
  });
299
314
  setWorkflowPushContext({
300
315
  repo: activeRepo ?? undefined,
@@ -0,0 +1,65 @@
1
+ import type { OrgPolicy } from "../../core/policy/store.js";
2
+ import {
3
+ getGenericEvaluator,
4
+ getValidator,
5
+ type EnforcedPolicy,
6
+ } from "../../core/validators/engine.js";
7
+ import type { ReviewSkippedPolicy } from "./trust-state.js";
8
+
9
+ export const DEFERRED_CODE_REVIEW_REASON =
10
+ "Current context.review invocation is the review being recorded; validate this policy from workflow state on the next run.";
11
+
12
+ export type PartitionedReviewPolicies = {
13
+ enforced: EnforcedPolicy[];
14
+ skipped: ReviewSkippedPolicy[];
15
+ };
16
+
17
+ export function partitionReviewPolicies(
18
+ policies: OrgPolicy[],
19
+ ): PartitionedReviewPolicies {
20
+ const enforced: EnforcedPolicy[] = [];
21
+ const skipped: ReviewSkippedPolicy[] = [];
22
+
23
+ for (const policy of policies) {
24
+ if (!policy.enforce) continue;
25
+
26
+ if (policy.id === "require-code-review") {
27
+ skipped.push({
28
+ policy_id: policy.id,
29
+ kind: policy.kind ?? null,
30
+ type: policy.type ?? null,
31
+ reason: DEFERRED_CODE_REVIEW_REASON,
32
+ });
33
+ continue;
34
+ }
35
+
36
+ if (policy.type) {
37
+ if (!getGenericEvaluator(policy.type)) {
38
+ skipped.push({
39
+ policy_id: policy.id,
40
+ kind: policy.kind ?? null,
41
+ type: policy.type,
42
+ reason: `No evaluator registered for type "${policy.type}"`,
43
+ });
44
+ continue;
45
+ }
46
+ } else if (!getValidator(policy.id)) {
47
+ skipped.push({
48
+ policy_id: policy.id,
49
+ kind: policy.kind ?? null,
50
+ type: null,
51
+ reason: "No executable validator registered for this policy",
52
+ });
53
+ continue;
54
+ }
55
+
56
+ enforced.push({
57
+ id: policy.id,
58
+ type: policy.type ?? null,
59
+ config: policy.config ?? null,
60
+ severity: policy.severity ?? "block",
61
+ });
62
+ }
63
+
64
+ return { enforced, skipped };
65
+ }
@@ -1,3 +1,5 @@
1
+ import { recordReviewDeliveryStatus } from "./trust-state.js";
2
+
1
3
  export type ReviewPushItem = {
2
4
  policy_id: string;
3
5
  pass: boolean;
@@ -11,12 +13,16 @@ export type ReviewPushContext = {
11
13
  repo?: string;
12
14
  instance_id?: string;
13
15
  session_id?: string;
16
+ context_dir?: string;
17
+ project_root?: string;
14
18
  };
15
19
 
16
20
  export type ReviewPushResult = {
17
21
  success: boolean;
18
22
  count: number;
19
23
  error?: string;
24
+ attempted?: boolean;
25
+ delivery_status?: "accepted" | "failed";
20
26
  };
21
27
 
22
28
  const pending: ReviewPushItem[] = [];
@@ -26,6 +32,10 @@ export function setReviewPushContext(context: ReviewPushContext): void {
26
32
  activeContext = { ...context };
27
33
  }
28
34
 
35
+ export function getReviewPushContext(): ReviewPushContext {
36
+ return { ...activeContext };
37
+ }
38
+
29
39
  export function queueReviewResult(item: ReviewPushItem): void {
30
40
  pending.push(item);
31
41
  }
@@ -39,11 +49,18 @@ export async function pushReviewResults(
39
49
  apiKey: string,
40
50
  ): Promise<ReviewPushResult> {
41
51
  if (pending.length === 0) {
42
- return { success: true, count: 0 };
52
+ return { success: true, count: 0, attempted: false };
43
53
  }
44
54
 
45
55
  const reviewsUrl = `${baseUrl.replace(/\/$/, "")}/api/v1/reviews/push`;
46
56
  const batch = pending.splice(0, 100);
57
+ const attemptedAt = new Date().toISOString();
58
+ if (activeContext.context_dir) {
59
+ recordReviewDeliveryStatus(activeContext.context_dir, "pushed", {
60
+ reviewCount: batch.length,
61
+ attemptedAt,
62
+ });
63
+ }
47
64
 
48
65
  try {
49
66
  const response = await fetch(reviewsUrl, {
@@ -64,16 +81,50 @@ export async function pushReviewResults(
64
81
 
65
82
  if (!response.ok) {
66
83
  pending.unshift(...batch);
67
- return { success: false, count: 0, error: `HTTP ${response.status}` };
84
+ if (activeContext.context_dir) {
85
+ recordReviewDeliveryStatus(activeContext.context_dir, "failed", {
86
+ reviewCount: batch.length,
87
+ attemptedAt,
88
+ error: `HTTP ${response.status}`,
89
+ });
90
+ }
91
+ return {
92
+ success: false,
93
+ count: 0,
94
+ error: `HTTP ${response.status}`,
95
+ attempted: true,
96
+ delivery_status: "failed",
97
+ };
68
98
  }
69
99
 
70
- return { success: true, count: batch.length };
100
+ if (activeContext.context_dir) {
101
+ recordReviewDeliveryStatus(activeContext.context_dir, "accepted", {
102
+ reviewCount: batch.length,
103
+ attemptedAt,
104
+ });
105
+ }
106
+ return {
107
+ success: true,
108
+ count: batch.length,
109
+ attempted: true,
110
+ delivery_status: "accepted",
111
+ };
71
112
  } catch (err) {
72
113
  pending.unshift(...batch);
114
+ const error = err instanceof Error ? err.message : "unknown error";
115
+ if (activeContext.context_dir) {
116
+ recordReviewDeliveryStatus(activeContext.context_dir, "failed", {
117
+ reviewCount: batch.length,
118
+ attemptedAt,
119
+ error,
120
+ });
121
+ }
73
122
  return {
74
123
  success: false,
75
124
  count: 0,
76
- error: err instanceof Error ? err.message : "unknown error",
125
+ error,
126
+ attempted: true,
127
+ delivery_status: "failed",
77
128
  };
78
129
  }
79
130
  }
@@ -0,0 +1,305 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ writeFileSync,
6
+ } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { setWorkflowReviewTrust } from "../workflow/state.js";
9
+ import { resolveWorkflowDefinition, type WorkflowSource } from "../../core/workflow/resolution.js";
10
+
11
+ export type ReviewDeliveryStatus =
12
+ | "never_attempted"
13
+ | "queued"
14
+ | "pushed"
15
+ | "accepted"
16
+ | "failed";
17
+
18
+ export type ReviewTrustWarning = {
19
+ code:
20
+ | "workflow-source-unverified"
21
+ | "workflow-unresolved"
22
+ | "review-delivery-pending"
23
+ | "review-delivery-failed"
24
+ | "policy-not-enforceable";
25
+ message: string;
26
+ };
27
+
28
+ export type ReviewSkippedPolicy = {
29
+ policy_id: string;
30
+ kind: string | null;
31
+ type: string | null;
32
+ reason: string;
33
+ };
34
+
35
+ export type ReviewTrustState = {
36
+ version: 1;
37
+ repo: string | null;
38
+ instance_id: string | null;
39
+ session_id: string | null;
40
+ workflow: {
41
+ id: string | null;
42
+ source: WorkflowSource | "unknown";
43
+ task_id: string | null;
44
+ };
45
+ last_review_at: string | null;
46
+ review_summary: {
47
+ total: number;
48
+ passed: number;
49
+ failed: number;
50
+ warnings: number;
51
+ skipped: number;
52
+ } | null;
53
+ skipped_policies: ReviewSkippedPolicy[];
54
+ delivery: {
55
+ status: ReviewDeliveryStatus;
56
+ review_count: number;
57
+ queued_at: string | null;
58
+ last_attempted_at: string | null;
59
+ last_pushed_at: string | null;
60
+ last_accepted_at: string | null;
61
+ last_error: string | null;
62
+ };
63
+ trust_warnings: ReviewTrustWarning[];
64
+ };
65
+
66
+ type TrustContext = {
67
+ contextDir: string;
68
+ projectRoot: string;
69
+ repo?: string;
70
+ instance_id?: string;
71
+ session_id?: string;
72
+ task_id?: string;
73
+ };
74
+
75
+ type ReviewSummaryInput = {
76
+ total: number;
77
+ passed: number;
78
+ failed: number;
79
+ warnings: number;
80
+ skipped: number;
81
+ };
82
+
83
+ type WorkflowInference = {
84
+ id: string | null;
85
+ source: WorkflowSource | "unknown";
86
+ task_id: string | null;
87
+ };
88
+
89
+ function trustStatePath(contextDir: string): string {
90
+ return join(contextDir, "review-trust.json");
91
+ }
92
+
93
+ function nowIso(): string {
94
+ return new Date().toISOString();
95
+ }
96
+
97
+ function initialTrustState(): ReviewTrustState {
98
+ return {
99
+ version: 1,
100
+ repo: null,
101
+ instance_id: null,
102
+ session_id: null,
103
+ workflow: {
104
+ id: null,
105
+ source: "unknown",
106
+ task_id: null,
107
+ },
108
+ last_review_at: null,
109
+ review_summary: null,
110
+ skipped_policies: [],
111
+ delivery: {
112
+ status: "never_attempted",
113
+ review_count: 0,
114
+ queued_at: null,
115
+ last_attempted_at: null,
116
+ last_pushed_at: null,
117
+ last_accepted_at: null,
118
+ last_error: null,
119
+ },
120
+ trust_warnings: [],
121
+ };
122
+ }
123
+
124
+ function ensureContextDir(contextDir: string): void {
125
+ mkdirSync(contextDir, { recursive: true });
126
+ }
127
+
128
+ function inferWorkflow(projectRoot: string, taskId?: string): WorkflowInference {
129
+ const normalizedTaskId = taskId?.trim();
130
+ if (!normalizedTaskId) {
131
+ return { id: null, source: "unknown", task_id: null };
132
+ }
133
+
134
+ const statePath = join(projectRoot, ".agents", normalizedTaskId, "state.json");
135
+ if (!existsSync(statePath)) {
136
+ return { id: null, source: "unknown", task_id: normalizedTaskId };
137
+ }
138
+
139
+ try {
140
+ const raw = JSON.parse(readFileSync(statePath, "utf8")) as Record<string, unknown>;
141
+ const workflowId =
142
+ typeof raw.workflow_id === "string" ? raw.workflow_id : null;
143
+ if (!workflowId) {
144
+ return { id: null, source: "unknown", task_id: normalizedTaskId };
145
+ }
146
+ const resolved = resolveWorkflowDefinition(workflowId);
147
+ return {
148
+ id: workflowId,
149
+ source: resolved.source,
150
+ task_id: normalizedTaskId,
151
+ };
152
+ } catch {
153
+ return {
154
+ id: null,
155
+ source: "unknown",
156
+ task_id: normalizedTaskId,
157
+ };
158
+ }
159
+ }
160
+
161
+ function computeWarnings(state: ReviewTrustState): ReviewTrustWarning[] {
162
+ const warnings: ReviewTrustWarning[] = [];
163
+
164
+ if (!state.workflow.id) {
165
+ warnings.push({
166
+ code: "workflow-unresolved",
167
+ message:
168
+ "No active local workflow could be resolved for this review session, so workflow/review correlation is unproven.",
169
+ });
170
+ } else if (state.workflow.source !== "synced") {
171
+ warnings.push({
172
+ code: "workflow-source-unverified",
173
+ message:
174
+ `Workflow "${state.workflow.id}" is not proven to come from the synced org registry for this session.`,
175
+ });
176
+ }
177
+
178
+ if (state.delivery.status === "queued" || state.delivery.status === "pushed") {
179
+ warnings.push({
180
+ code: "review-delivery-pending",
181
+ message:
182
+ "Local review results exist for this session, but Cortex has not yet confirmed acceptance by the control plane.",
183
+ });
184
+ }
185
+
186
+ if (state.delivery.status === "failed") {
187
+ warnings.push({
188
+ code: "review-delivery-failed",
189
+ message:
190
+ `Review delivery failed${state.delivery.last_error ? `: ${state.delivery.last_error}` : ""}`,
191
+ });
192
+ }
193
+
194
+ if (state.skipped_policies.length > 0) {
195
+ warnings.push({
196
+ code: "policy-not-enforceable",
197
+ message:
198
+ `${state.skipped_policies.length} configured policy` +
199
+ `${state.skipped_policies.length === 1 ? " was" : "ies were"} not enforceable locally.`,
200
+ });
201
+ }
202
+
203
+ return warnings;
204
+ }
205
+
206
+ export function loadReviewTrustState(contextDir: string): ReviewTrustState {
207
+ ensureContextDir(contextDir);
208
+ const filePath = trustStatePath(contextDir);
209
+ if (!existsSync(filePath)) {
210
+ const initial = initialTrustState();
211
+ writeReviewTrustState(contextDir, initial);
212
+ return initial;
213
+ }
214
+
215
+ try {
216
+ const parsed = JSON.parse(readFileSync(filePath, "utf8")) as ReviewTrustState;
217
+ parsed.trust_warnings = computeWarnings(parsed);
218
+ return parsed;
219
+ } catch {
220
+ const initial = initialTrustState();
221
+ writeReviewTrustState(contextDir, initial);
222
+ return initial;
223
+ }
224
+ }
225
+
226
+ export function writeReviewTrustState(
227
+ contextDir: string,
228
+ state: ReviewTrustState,
229
+ ): ReviewTrustState {
230
+ ensureContextDir(contextDir);
231
+ state.trust_warnings = computeWarnings(state);
232
+ writeFileSync(trustStatePath(contextDir), `${JSON.stringify(state, null, 2)}\n`, "utf8");
233
+ setWorkflowReviewTrust(contextDir, state);
234
+ return state;
235
+ }
236
+
237
+ export function recordQueuedReviewTrustState(
238
+ context: TrustContext,
239
+ input: {
240
+ reviewedAt: string;
241
+ summary: ReviewSummaryInput;
242
+ skippedPolicies: ReviewSkippedPolicy[];
243
+ },
244
+ ): ReviewTrustState {
245
+ const state = loadReviewTrustState(context.contextDir);
246
+ const workflow = inferWorkflow(context.projectRoot, context.task_id);
247
+ state.repo = context.repo ?? state.repo;
248
+ state.instance_id = context.instance_id ?? state.instance_id;
249
+ state.session_id = context.session_id ?? state.session_id;
250
+ state.workflow = workflow;
251
+ state.last_review_at = input.reviewedAt;
252
+ state.review_summary = {
253
+ total: input.summary.total,
254
+ passed: input.summary.passed,
255
+ failed: input.summary.failed,
256
+ warnings: input.summary.warnings,
257
+ skipped: input.summary.skipped,
258
+ };
259
+ state.skipped_policies = input.skippedPolicies;
260
+ const hasQueuedReviews = input.summary.total > 0;
261
+ state.delivery = {
262
+ status: hasQueuedReviews ? "queued" : "never_attempted",
263
+ review_count: input.summary.total,
264
+ queued_at: hasQueuedReviews ? input.reviewedAt : null,
265
+ last_attempted_at: null,
266
+ last_pushed_at: null,
267
+ last_accepted_at: null,
268
+ last_error: null,
269
+ };
270
+ return writeReviewTrustState(context.contextDir, state);
271
+ }
272
+
273
+ export function recordReviewDeliveryStatus(
274
+ contextDir: string,
275
+ status: ReviewDeliveryStatus,
276
+ input: {
277
+ reviewCount?: number;
278
+ error?: string;
279
+ attemptedAt?: string;
280
+ } = {},
281
+ ): ReviewTrustState {
282
+ const state = loadReviewTrustState(contextDir);
283
+ const at = input.attemptedAt ?? nowIso();
284
+ state.delivery.review_count = input.reviewCount ?? state.delivery.review_count;
285
+ state.delivery.last_attempted_at = at;
286
+
287
+ if (status === "pushed") {
288
+ state.delivery.status = "pushed";
289
+ state.delivery.last_pushed_at = at;
290
+ state.delivery.last_error = null;
291
+ } else if (status === "accepted") {
292
+ state.delivery.status = "accepted";
293
+ state.delivery.last_pushed_at = at;
294
+ state.delivery.last_accepted_at = at;
295
+ state.delivery.last_error = null;
296
+ } else if (status === "failed") {
297
+ state.delivery.status = "failed";
298
+ state.delivery.last_pushed_at = at;
299
+ state.delivery.last_error = input.error ?? "unknown error";
300
+ } else {
301
+ state.delivery.status = status;
302
+ }
303
+
304
+ return writeReviewTrustState(contextDir, state);
305
+ }