@danielblomma/cortex-mcp 2.0.16 → 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 (27) 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/core/validators/builtins.ts +24 -4
  7. package/scaffold/mcp/src/core/workflow/index.ts +1 -0
  8. package/scaffold/mcp/src/core/workflow/mcp-tools.ts +15 -25
  9. package/scaffold/mcp/src/core/workflow/resolution.ts +113 -0
  10. package/scaffold/mcp/src/enterprise/index.ts +16 -1
  11. package/scaffold/mcp/src/enterprise/reviews/policy-selection.ts +65 -0
  12. package/scaffold/mcp/src/enterprise/reviews/push.ts +55 -4
  13. package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +305 -0
  14. package/scaffold/mcp/src/enterprise/tools/enterprise.ts +40 -47
  15. package/scaffold/mcp/src/enterprise/tools/harness.ts +2 -0
  16. package/scaffold/mcp/src/enterprise/workflow/state.ts +21 -0
  17. package/scaffold/mcp/tests/enterprise-review-trust.test.mjs +318 -0
  18. package/scaffold/mcp/tests/fixtures/org-skillz-contract.json +59 -0
  19. package/scaffold/mcp/tests/fixtures/review-trust-contract.json +111 -0
  20. package/scaffold/mcp/tests/review-policy-selection.test.mjs +85 -0
  21. package/scaffold/mcp/tests/review-trust-contract.test.mjs +167 -0
  22. package/scaffold/mcp/tests/review-validator-source.test.mjs +52 -0
  23. package/scaffold/mcp/tests/skill-sync-checker.test.mjs +277 -0
  24. package/scaffold/mcp/tests/workflow-cli.test.mjs +102 -1
  25. package/scaffold/mcp/tests/workflow-mcp-tools.test.mjs +22 -0
  26. package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +56 -0
  27. package/scaffold/scripts/parsers/node_modules/.package-lock.json +0 -1
@@ -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
+ }
@@ -13,7 +13,9 @@ import type { InjectionMatch } from "../../core/policy/injection.js";
13
13
  import { getLastPush } from "../telemetry/sync.js";
14
14
  import { syncFromCloud, syncFromLocal, getLastSync } from "../policy/sync.js";
15
15
  import { queueViolation } from "../violations/push.js";
16
- import { queueReviewResult } from "../reviews/push.js";
16
+ import { getReviewPushContext, queueReviewResult } from "../reviews/push.js";
17
+ import { partitionReviewPolicies } from "../reviews/policy-selection.js";
18
+ import { recordQueuedReviewTrustState } from "../reviews/trust-state.js";
17
19
  import { pushWorkflowSnapshot } from "../workflow/push.js";
18
20
  import { OUTBOUND_DATA_BOUNDARY } from "../privacy/boundary.js";
19
21
  import {
@@ -31,11 +33,7 @@ import {
31
33
  } from "../workflow/state.js";
32
34
  import { queryAuditLog } from "../../core/audit/query.js";
33
35
  import { checkAccess, getAccessDeniedMessage, type Role } from "../../core/rbac/check.js";
34
- import {
35
- getGenericEvaluator,
36
- getValidator,
37
- runValidators,
38
- } from "../../core/validators/engine.js";
36
+ import { runValidators } from "../../core/validators/engine.js";
39
37
  import "../../core/validators/builtins.js";
40
38
  import { recordToolActivity } from "../index.js";
41
39
 
@@ -904,41 +902,10 @@ export function registerEnterpriseTools(
904
902
  // rules. Predefined rules leave type/config null and route to the
905
903
  // name-based validator registry.
906
904
  const policies = policyStore.getMergedPolicies();
907
- const skippedPolicies = policies
908
- .filter((p) => p.enforce)
909
- .filter((p) => p.id === "require-code-review" || (p.type ? !getGenericEvaluator(p.type) : !getValidator(p.id)))
910
- .map((p) => {
911
- if (p.id === "require-code-review") {
912
- return {
913
- policy_id: p.id,
914
- kind: p.kind ?? null,
915
- type: p.type ?? null,
916
- reason: "Current context.review invocation is the review being recorded; validate this policy from workflow state on the next run.",
917
- };
918
- }
919
- return {
920
- policy_id: p.id,
921
- kind: p.kind ?? null,
922
- type: p.type ?? null,
923
- reason: p.type
924
- ? `No evaluator registered for type "${p.type}"`
925
- : "No executable validator registered for this policy",
926
- };
927
- });
928
-
929
- const enforced = policies
930
- .filter((p) => p.enforce)
931
- .filter((p) => {
932
- if (p.id === "require-code-review") return false;
933
- if (p.type) return Boolean(getGenericEvaluator(p.type));
934
- return Boolean(getValidator(p.id));
935
- })
936
- .map((p) => ({
937
- id: p.id,
938
- type: p.type ?? null,
939
- config: p.config ?? null,
940
- severity: p.severity ?? "block",
941
- }));
905
+ const {
906
+ enforced,
907
+ skipped: skippedPolicies,
908
+ } = partitionReviewPolicies(policies);
942
909
 
943
910
  const now = new Date().toISOString();
944
911
 
@@ -964,9 +931,9 @@ export function registerEnterpriseTools(
964
931
  occurred_at: now,
965
932
  });
966
933
  }
967
- queueReviewResult({
968
- policy_id: r.policy_id,
969
- pass: r.pass,
934
+ queueReviewResult({
935
+ policy_id: r.policy_id,
936
+ pass: r.pass,
970
937
  severity: r.severity,
971
938
  message: r.message,
972
939
  detail: r.detail,
@@ -1001,20 +968,43 @@ export function registerEnterpriseTools(
1001
968
  reviewed_files: reviewedFiles,
1002
969
  });
1003
970
  const lastReview = workflowState.last_review;
971
+ const reviewedAt = lastReview?.reviewed_at ?? now;
1004
972
  if (lastReview) {
1005
973
  writeFileSync(
1006
974
  join(contextDir, "review-status.json"),
1007
975
  `${JSON.stringify({
1008
976
  reviewed: lastReview.status === "passed",
1009
977
  reviewer: "context.review",
1010
- timestamp: lastReview.reviewed_at,
978
+ timestamp: reviewedAt,
1011
979
  scope: parsed.scope,
1012
980
  reviewed_files: reviewedFiles,
1013
981
  }, null, 2)}\n`,
1014
982
  "utf8",
1015
983
  );
1016
984
  }
1017
- await pushWorkflowStateIfConfigured(config, workflowState);
985
+ const trustState = recordQueuedReviewTrustState(
986
+ {
987
+ contextDir,
988
+ projectRoot,
989
+ repo: getReviewPushContext().repo,
990
+ instance_id: getReviewPushContext().instance_id,
991
+ session_id: getReviewPushContext().session_id,
992
+ task_id: process.env.CORTEX_ACTIVE_TASK_ID?.trim() || undefined,
993
+ },
994
+ {
995
+ reviewedAt,
996
+ summary: {
997
+ total: output.summary.total,
998
+ passed: output.summary.passed,
999
+ failed: output.summary.failed,
1000
+ warnings: output.summary.warnings,
1001
+ skipped: skippedPolicies.length,
1002
+ },
1003
+ skippedPolicies: skippedPolicies,
1004
+ },
1005
+ );
1006
+ const workflowWithTrust = loadWorkflowState(contextDir);
1007
+ await pushWorkflowStateIfConfigured(config, workflowWithTrust);
1018
1008
 
1019
1009
  return buildToolResult({
1020
1010
  scope: parsed.scope,
@@ -1024,7 +1014,10 @@ export function registerEnterpriseTools(
1024
1014
  ...output.summary,
1025
1015
  skipped: skippedPolicies.length,
1026
1016
  },
1027
- workflow: workflowState,
1017
+ workflow: workflowWithTrust,
1018
+ workflow_source: trustState.workflow.source,
1019
+ delivery: trustState.delivery,
1020
+ trust_warnings: trustState.trust_warnings,
1028
1021
  });
1029
1022
  },
1030
1023
  );
@@ -1,4 +1,5 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { isEnforcedMode } from "../../hooks/shared.js";
2
3
  import {
3
4
  WorkflowAdvanceInput,
4
5
  WorkflowEnvelopeInput,
@@ -38,6 +39,7 @@ export function registerHarnessTools(server: McpServer): void {
38
39
  async (input) => buildResult(
39
40
  runWorkflowStart(WorkflowStartInput.parse(input ?? {}), {
40
41
  cwd: resolveProjectRoot(),
42
+ bundledFallbackPolicy: isEnforcedMode(resolveProjectRoot()) ? "block" : "allow",
41
43
  }) as ToolPayload,
42
44
  ),
43
45
  );
@@ -10,6 +10,7 @@ import type {
10
10
  ReviewResult,
11
11
  ReviewSummary,
12
12
  } from "../../core/validators/engine.js";
13
+ import type { ReviewTrustState } from "../reviews/trust-state.js";
13
14
 
14
15
  export type WorkflowPhase =
15
16
  | "planning"
@@ -111,6 +112,7 @@ export type WorkflowState = {
111
112
  notes: WorkflowNote[];
112
113
  todos: WorkflowTodo[];
113
114
  history: WorkflowHistoryEntry[];
115
+ review_trust: ReviewTrustState | null;
114
116
  };
115
117
 
116
118
  export type WorkflowMutationResult = {
@@ -145,6 +147,10 @@ function workflowStatePath(contextDir: string): string {
145
147
  return join(workflowDir(contextDir), "state.json");
146
148
  }
147
149
 
150
+ export function hasWorkflowState(contextDir: string): boolean {
151
+ return existsSync(workflowStatePath(contextDir));
152
+ }
153
+
148
154
  function workflowReviewsDir(contextDir: string): string {
149
155
  return join(workflowDir(contextDir), "reviews");
150
156
  }
@@ -195,6 +201,7 @@ function initialState(): WorkflowState {
195
201
  notes: [],
196
202
  todos: [],
197
203
  history: [],
204
+ review_trust: null,
198
205
  };
199
206
  }
200
207
 
@@ -457,6 +464,20 @@ export function recordWorkflowUpdate(
457
464
  );
458
465
  }
459
466
 
467
+ export function setWorkflowReviewTrust(
468
+ contextDir: string,
469
+ reviewTrust: ReviewTrustState,
470
+ ): WorkflowState {
471
+ if (!hasWorkflowState(contextDir)) {
472
+ const state = initialState();
473
+ state.review_trust = reviewTrust;
474
+ return writeState(contextDir, withRecalculatedApproval(state, true));
475
+ }
476
+ const state = loadWorkflowState(contextDir);
477
+ state.review_trust = reviewTrust;
478
+ return writeState(contextDir, withRecalculatedApproval(state, true));
479
+ }
480
+
460
481
  export function addWorkflowNote(
461
482
  contextDir: string,
462
483
  input: { title: string; details: string }