@kal-elsam/kairo-runtime 0.6.0 → 0.8.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.
Files changed (43) hide show
  1. package/README.md +36 -0
  2. package/global-template/components/catalog.json +4 -1
  3. package/global-template/components/orchestrator/extensions/pi/kairo-minion.js +604 -0
  4. package/package.json +1 -1
  5. package/scripts/cockpit-smoke.mjs +1 -1
  6. package/src/cli.js +69 -1
  7. package/src/global/adapters/pi.js +1 -1
  8. package/src/global/ink/cockpit-controller.js +10 -0
  9. package/src/global/ink/cockpit-focus.js +18 -3
  10. package/src/global/ink/cockpit-models.js +8 -1
  11. package/src/global/ink/cockpit-reviews.js +62 -0
  12. package/src/global/ink/cockpit-runs.js +11 -2
  13. package/src/global/ink/cockpit-views.js +19 -2
  14. package/src/global/ink/orchestrator-app.js +23 -3
  15. package/src/global/ink/orchestrator-state.js +2 -0
  16. package/src/global/ink/use-orchestrator-data.js +30 -0
  17. package/src/global/paths.js +1 -0
  18. package/src/global/runtime/execution-adapters/codex.js +2 -1
  19. package/src/global/runtime/execution-adapters/create-execution-adapter.js +1 -0
  20. package/src/global/runtime/execution-adapters/index.js +1 -0
  21. package/src/global/runtime/execution-adapters/pi.js +14 -3
  22. package/src/global/runtime/orchestration/index.js +23 -0
  23. package/src/global/runtime/orchestration/orch-receipts.js +234 -0
  24. package/src/global/runtime/orchestration/orch-types.js +173 -0
  25. package/src/global/runtime/orchestration/orch-validate.js +63 -0
  26. package/src/global/runtime/review/index.js +37 -0
  27. package/src/global/runtime/review/review-cli.js +150 -0
  28. package/src/global/runtime/review/review-codex.js +132 -0
  29. package/src/global/runtime/review/review-exec.js +136 -0
  30. package/src/global/runtime/review/review-fs.js +52 -0
  31. package/src/global/runtime/review/review-git.js +212 -0
  32. package/src/global/runtime/review/review-patch.js +122 -0
  33. package/src/global/runtime/review/review-pi.js +168 -0
  34. package/src/global/runtime/review/review-receipts.js +128 -0
  35. package/src/global/runtime/review/review-runner.js +119 -0
  36. package/src/global/runtime/review/review-types.js +108 -0
  37. package/src/global/runtime/review/review-validate.js +280 -0
  38. package/src/global/runtime/run-cli.js +1 -0
  39. package/src/global/runtime/run-manager.js +59 -4
  40. package/src/global/runtime/run-strategy.js +71 -0
  41. package/src/global/runtime/run-supervisor.js +22 -2
  42. package/src/global/runtime/run-types.js +5 -1
  43. package/src/global/runtime/write-atomic-json.js +24 -20
@@ -0,0 +1,234 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { runPaths } from "../../paths.js";
5
+ import { writeAtomicJson } from "../write-atomic-json.js";
6
+ import {
7
+ DAG_NODE_STATES, ORCH_ERROR_CODES, OrchContractError, RUN_STRATEGIES,
8
+ createBudgetUsage, createDagNode, createMinionResult, createOrchLineage,
9
+ digestAllowlisted, isTerminalDagState, normalizeRunStrategy
10
+ } from "./orch-types.js";
11
+ import { assertOrchReceiptSecretFree, walkForbiddenKeys } from "./orch-validate.js";
12
+
13
+ export function orchPaths(homeDir, rootRunId) {
14
+ if (typeof rootRunId !== "string" || !rootRunId) {
15
+ throw new OrchContractError("rootRunId is required for orchestration paths.", {
16
+ code: ORCH_ERROR_CODES.INVALID_LINEAGE
17
+ });
18
+ }
19
+ const { runDir } = runPaths(homeDir, rootRunId);
20
+ const orchDir = join(runDir, "orchestration");
21
+ return {
22
+ runDir, orchDir,
23
+ receiptPath: join(orchDir, "receipt.json"),
24
+ statePath: join(orchDir, "state.json")
25
+ };
26
+ }
27
+
28
+ function normalizeNodes(nodes = []) {
29
+ return nodes.map((node) => {
30
+ const { objective, ...rest } = node;
31
+ return createDagNode({
32
+ ...rest,
33
+ budget: createBudgetUsage(rest.budget ?? {}),
34
+ objectiveDigest: rest.objectiveDigest
35
+ ?? (objective ? digestAllowlisted({ objective }) : null)
36
+ });
37
+ });
38
+ }
39
+
40
+ export function buildOrchReceipt({
41
+ rootRunId, strategy = RUN_STRATEGIES.ORCHESTRATED, lineage = null,
42
+ nodes = [], results = [], cliVersion = null, createdAt = null, recovered = false
43
+ } = {}) {
44
+ const normalizedStrategy = normalizeRunStrategy(strategy);
45
+ const normalizedLineage = createOrchLineage(lineage ?? { rootRunId, parentRunId: null, depth: 0 });
46
+ if (normalizedLineage.rootRunId !== rootRunId) {
47
+ throw new OrchContractError("lineage.rootRunId must match receipt rootRunId.", {
48
+ code: ORCH_ERROR_CODES.INVALID_LINEAGE
49
+ });
50
+ }
51
+ return assertOrchReceiptSecretFree({
52
+ version: 1, strategy: normalizedStrategy, rootRunId, lineage: normalizedLineage,
53
+ nodes: normalizeNodes(nodes),
54
+ results: results.map((entry) => createMinionResult(entry)),
55
+ createdAt: createdAt ?? new Date().toISOString(),
56
+ cliVersion,
57
+ recovered: Boolean(recovered)
58
+ });
59
+ }
60
+
61
+ export async function saveOrchReceipt(receipt, { homeDir } = {}) {
62
+ const sanitized = assertOrchReceiptSecretFree(receipt);
63
+ const { orchDir, receiptPath } = orchPaths(homeDir, sanitized.rootRunId);
64
+ await mkdir(orchDir, { recursive: true });
65
+ try {
66
+ await writeAtomicJson(receiptPath, sanitized, { createExclusive: true });
67
+ } catch (error) {
68
+ if (error?.code === "EEXIST") {
69
+ throw new OrchContractError(`Orchestration receipt already exists: ${sanitized.rootRunId}`, {
70
+ code: ORCH_ERROR_CODES.RECEIPT_EXISTS,
71
+ details: { rootRunId: sanitized.rootRunId, path: receiptPath }
72
+ });
73
+ }
74
+ throw error;
75
+ }
76
+ return { path: receiptPath, receipt: sanitized };
77
+ }
78
+
79
+ export async function loadOrchReceipt(rootRunId, { homeDir } = {}) {
80
+ const { receiptPath } = orchPaths(homeDir, rootRunId);
81
+ if (!existsSync(receiptPath)) {
82
+ throw new OrchContractError(`Orchestration receipt not found: ${rootRunId}`, {
83
+ code: ORCH_ERROR_CODES.INVALID_LINEAGE, details: { rootRunId }
84
+ });
85
+ }
86
+ return assertOrchReceiptSecretFree(JSON.parse(await readFile(receiptPath, "utf8")));
87
+ }
88
+
89
+ export function createOrchState({
90
+ rootRunId, strategy = RUN_STRATEGIES.ORCHESTRATED, lineage = null,
91
+ nodes = [], results = [], cliVersion = null, updatedAt = null
92
+ } = {}) {
93
+ const normalizedStrategy = normalizeRunStrategy(strategy);
94
+ if (normalizedStrategy !== RUN_STRATEGIES.ORCHESTRATED) {
95
+ throw new OrchContractError("Orchestration state requires strategy orchestrated.", {
96
+ code: ORCH_ERROR_CODES.INVALID_STRATEGY
97
+ });
98
+ }
99
+ const normalizedLineage = createOrchLineage(lineage ?? { rootRunId, parentRunId: null, depth: 0 });
100
+ if (normalizedLineage.rootRunId !== rootRunId) {
101
+ throw new OrchContractError("lineage.rootRunId must match state rootRunId.", {
102
+ code: ORCH_ERROR_CODES.INVALID_LINEAGE
103
+ });
104
+ }
105
+ const state = {
106
+ version: 1, strategy: normalizedStrategy, rootRunId, lineage: normalizedLineage,
107
+ nodes: normalizeNodes(nodes),
108
+ results: (results ?? []).map((entry) => createMinionResult(entry)),
109
+ cliVersion: cliVersion ?? null,
110
+ updatedAt: updatedAt ?? new Date().toISOString()
111
+ };
112
+ walkForbiddenKeys(state);
113
+ return state;
114
+ }
115
+
116
+ export async function saveOrchState(state, { homeDir } = {}) {
117
+ const sanitized = createOrchState(state);
118
+ const { orchDir, statePath } = orchPaths(homeDir, sanitized.rootRunId);
119
+ await mkdir(orchDir, { recursive: true });
120
+ await writeAtomicJson(statePath, sanitized);
121
+ return { path: statePath, state: sanitized };
122
+ }
123
+
124
+ export async function loadOrchState(rootRunId, { homeDir } = {}) {
125
+ const { statePath } = orchPaths(homeDir, rootRunId);
126
+ if (!existsSync(statePath)) {
127
+ throw new OrchContractError(`Orchestration state not found: ${rootRunId}`, {
128
+ code: ORCH_ERROR_CODES.INVALID_NODE, details: { rootRunId }
129
+ });
130
+ }
131
+ try {
132
+ return createOrchState(JSON.parse(await readFile(statePath, "utf8")));
133
+ } catch {
134
+ throw new OrchContractError(`Corrupt orchestration state: ${rootRunId}`, {
135
+ code: ORCH_ERROR_CODES.INVALID_NODE, details: { rootRunId }
136
+ });
137
+ }
138
+ }
139
+
140
+ export function terminalizeOrchNodes(nodes, { recovered = false } = {}) {
141
+ return (nodes ?? []).map((node) => {
142
+ if (isTerminalDagState(node.state)) return createDagNode(node);
143
+ return createDagNode({
144
+ ...node,
145
+ state: recovered ? DAG_NODE_STATES.CANCELLED : DAG_NODE_STATES.COMPLETED,
146
+ error: recovered ? (node.error ?? { code: "interrupted" }) : node.error
147
+ });
148
+ });
149
+ }
150
+
151
+ const orchWriteLocks = new Map();
152
+
153
+ function withOrchWriteLock(rootRunId, work) {
154
+ const previous = orchWriteLocks.get(rootRunId) ?? Promise.resolve();
155
+ const next = previous.then(work);
156
+ orchWriteLocks.set(rootRunId, next.catch(() => {}));
157
+ return next;
158
+ }
159
+
160
+ /** Serialized load → mutate → save for concurrent minion DAG updates. */
161
+ export async function updateOrchState(rootRunId, mutator, { homeDir } = {}) {
162
+ return withOrchWriteLock(rootRunId, async () => {
163
+ const current = await loadOrchState(rootRunId, { homeDir });
164
+ return saveOrchState(await mutator(current), { homeDir });
165
+ });
166
+ }
167
+
168
+ /** Upsert one depth-1 node by taskId; append/replace MinionResult on completed. */
169
+ export async function applyMinionDagUpdate(rootRunId, {
170
+ homeDir, taskId, parentTaskId, attempt = 0, state,
171
+ objectiveDigest = null, result = null, error = null
172
+ } = {}) {
173
+ return updateOrchState(rootRunId, (current) => {
174
+ const rootTaskId = current.lineage?.taskId;
175
+ if (!rootTaskId || taskId === rootTaskId || parentTaskId !== rootTaskId) {
176
+ throw new OrchContractError("Minion taskId/parentTaskId must honor supervisor rootTaskId.", {
177
+ code: ORCH_ERROR_CODES.INVALID_NODE, details: { taskId, parentTaskId, rootTaskId }
178
+ });
179
+ }
180
+ const node = createDagNode({
181
+ taskId, parentTaskId, depth: 1, state, attempt,
182
+ objectiveDigest: objectiveDigest ?? null, error: error ?? null
183
+ });
184
+ const nodes = [...current.nodes];
185
+ const idx = nodes.findIndex((entry) => entry.taskId === taskId);
186
+ if (idx >= 0) {
187
+ nodes[idx] = createDagNode({
188
+ ...nodes[idx], ...node,
189
+ objectiveDigest: node.objectiveDigest ?? nodes[idx].objectiveDigest
190
+ });
191
+ } else {
192
+ nodes.push(node);
193
+ }
194
+ let results = current.results;
195
+ if (state === DAG_NODE_STATES.COMPLETED && result) {
196
+ const sealed = createMinionResult(result);
197
+ results = [...results.filter((entry) => entry.taskId !== taskId), sealed];
198
+ }
199
+ return { ...current, nodes, results, updatedAt: new Date().toISOString() };
200
+ }, { homeDir });
201
+ }
202
+
203
+ export async function finalizeOrchState(rootRunId, { homeDir, recovered = false } = {}) {
204
+ return withOrchWriteLock(rootRunId, async () => {
205
+ const { receiptPath } = orchPaths(homeDir, rootRunId);
206
+ if (existsSync(receiptPath)) {
207
+ return { path: receiptPath, receipt: await loadOrchReceipt(rootRunId, { homeDir }), idempotent: true };
208
+ }
209
+ const state = await loadOrchState(rootRunId, { homeDir });
210
+ const nodes = terminalizeOrchNodes(state.nodes, { recovered });
211
+ await saveOrchState({ ...state, nodes, updatedAt: new Date().toISOString() }, { homeDir });
212
+ try {
213
+ const saved = await saveOrchReceipt(buildOrchReceipt({
214
+ rootRunId: state.rootRunId, strategy: state.strategy, lineage: state.lineage,
215
+ nodes, results: state.results, cliVersion: state.cliVersion, recovered
216
+ }), { homeDir });
217
+ return { ...saved, idempotent: false };
218
+ } catch (error) {
219
+ if (error?.code === ORCH_ERROR_CODES.RECEIPT_EXISTS) {
220
+ return { path: receiptPath, receipt: await loadOrchReceipt(rootRunId, { homeDir }), idempotent: true };
221
+ }
222
+ throw error;
223
+ }
224
+ });
225
+ }
226
+
227
+ export async function reconcileOrchState(rootRunId, { homeDir } = {}) {
228
+ const { statePath, receiptPath } = orchPaths(homeDir, rootRunId);
229
+ if (existsSync(receiptPath)) {
230
+ return { rootRunId, path: receiptPath, receipt: await loadOrchReceipt(rootRunId, { homeDir }), idempotent: true };
231
+ }
232
+ if (!existsSync(statePath)) return null;
233
+ return finalizeOrchState(rootRunId, { homeDir, recovered: true });
234
+ }
@@ -0,0 +1,173 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ export const RUN_STRATEGIES = Object.freeze({ DIRECT: "direct", ORCHESTRATED: "orchestrated" });
3
+ export const DAG_NODE_STATES = Object.freeze({
4
+ PENDING: "pending", READY: "ready", RUNNING: "running", COMPACTING: "compacting",
5
+ COMPLETED: "completed", FAILED: "failed", CANCELLED: "cancelled", BLOCKED: "blocked"
6
+ });
7
+ export const DAG_TERMINAL_STATES = new Set([
8
+ DAG_NODE_STATES.COMPLETED, DAG_NODE_STATES.FAILED, DAG_NODE_STATES.CANCELLED
9
+ ]);
10
+ export const ORCH_LIMITS = Object.freeze({
11
+ MAX_DEPTH: 1, DEFAULT_CONCURRENCY: 2, MAX_ATTEMPTS: 2, COMPACT_RATIO: 0.7, STOP_RATIO: 0.9
12
+ });
13
+ export const ORCH_ERROR_CODES = Object.freeze({
14
+ INVALID_STRATEGY: "invalid_strategy", INVALID_DEPTH: "invalid_depth",
15
+ INVALID_LINEAGE: "invalid_lineage", INVALID_HANDOFF: "invalid_handoff",
16
+ INVALID_NODE: "invalid_node", LIMIT_EXCEEDED: "limit_exceeded",
17
+ FORBIDDEN_FIELD: "forbidden_field", RECEIPT_EXISTS: "receipt_exists"
18
+ });
19
+ export class OrchContractError extends Error {
20
+ constructor(message, { code, details = null } = {}) {
21
+ super(message);
22
+ this.name = "OrchContractError";
23
+ this.code = code;
24
+ this.details = details;
25
+ }
26
+ }
27
+ export function createTaskId() {
28
+ return `task_${randomBytes(8).toString("hex")}`;
29
+ }
30
+ export function isTerminalDagState(state) {
31
+ return DAG_TERMINAL_STATES.has(state);
32
+ }
33
+
34
+ function assertDepth(depth) {
35
+ const d = Number(depth);
36
+ if (!Number.isInteger(d) || d < 0) {
37
+ throw new OrchContractError(`Invalid depth "${depth}".`, {
38
+ code: ORCH_ERROR_CODES.INVALID_DEPTH, details: { depth }
39
+ });
40
+ }
41
+ if (d > ORCH_LIMITS.MAX_DEPTH) {
42
+ throw new OrchContractError(`Orchestration depth ${d} exceeds max ${ORCH_LIMITS.MAX_DEPTH}.`, {
43
+ code: ORCH_ERROR_CODES.INVALID_DEPTH, details: { depth: d }
44
+ });
45
+ }
46
+ return d;
47
+ }
48
+
49
+ function assertParentForDepth(depth, parentId, { rootMsg, minionMsg, code }) {
50
+ if (depth === 0 && parentId != null) {
51
+ throw new OrchContractError(rootMsg, { code });
52
+ }
53
+ if (depth > 0 && !parentId) {
54
+ throw new OrchContractError(minionMsg, { code });
55
+ }
56
+ }
57
+
58
+ export function normalizeRunStrategy(value = RUN_STRATEGIES.DIRECT) {
59
+ const strategy = String(value ?? RUN_STRATEGIES.DIRECT).trim().toLowerCase();
60
+ if (!Object.values(RUN_STRATEGIES).includes(strategy)) {
61
+ throw new OrchContractError(`Invalid run strategy "${value}". Use direct or orchestrated.`, {
62
+ code: ORCH_ERROR_CODES.INVALID_STRATEGY, details: { value }
63
+ });
64
+ }
65
+ return strategy;
66
+ }
67
+
68
+ /** Lineage: max depth 1 (root=0, minion=1). */
69
+ export function createOrchLineage({ rootRunId, parentRunId = null, taskId = null, depth = 0 } = {}) {
70
+ if (typeof rootRunId !== "string" || !rootRunId) {
71
+ throw new OrchContractError("rootRunId is required.", { code: ORCH_ERROR_CODES.INVALID_LINEAGE });
72
+ }
73
+ const d = assertDepth(depth);
74
+ assertParentForDepth(d, parentRunId, {
75
+ rootMsg: "Root nodes must not set parentRunId.",
76
+ minionMsg: "Minion nodes require parentRunId.",
77
+ code: ORCH_ERROR_CODES.INVALID_LINEAGE
78
+ });
79
+ return { rootRunId, parentRunId: parentRunId ?? null, taskId: taskId ?? createTaskId(), depth: d };
80
+ }
81
+
82
+ export function createBudgetUsage({
83
+ contextTokens = 0, contextLimit = 0,
84
+ compactRatio = ORCH_LIMITS.COMPACT_RATIO, stopRatio = ORCH_LIMITS.STOP_RATIO
85
+ } = {}) {
86
+ const tokens = Math.max(0, Number(contextTokens) || 0);
87
+ const limit = Math.max(0, Number(contextLimit) || 0);
88
+ const ratio = limit > 0 ? tokens / limit : 0;
89
+ return {
90
+ contextTokens: tokens, contextLimit: limit, ratio,
91
+ shouldCompact: limit > 0 && ratio >= compactRatio && ratio < stopRatio,
92
+ shouldStop: limit > 0 && ratio >= stopRatio
93
+ };
94
+ }
95
+
96
+ export function createDagNode({
97
+ taskId = null, runId = null, parentTaskId = null, depth = 0,
98
+ state = DAG_NODE_STATES.PENDING, dependsOn = [], attempt = 0,
99
+ objectiveDigest = null, budget = null, resultDigest = null, error = null
100
+ } = {}) {
101
+ if (!Object.values(DAG_NODE_STATES).includes(state)) {
102
+ throw new OrchContractError(`Invalid DAG node state "${state}".`, {
103
+ code: ORCH_ERROR_CODES.INVALID_NODE, details: { state }
104
+ });
105
+ }
106
+ const d = assertDepth(depth);
107
+ assertParentForDepth(d, parentTaskId, {
108
+ rootMsg: "Root nodes must not set parentTaskId.",
109
+ minionMsg: "Minion nodes require parentTaskId.",
110
+ code: ORCH_ERROR_CODES.INVALID_NODE
111
+ });
112
+ const attempts = Number(attempt) || 0;
113
+ if (attempts < 0 || attempts > ORCH_LIMITS.MAX_ATTEMPTS) {
114
+ throw new OrchContractError(`Attempt ${attempts} outside 0..${ORCH_LIMITS.MAX_ATTEMPTS}.`, {
115
+ code: ORCH_ERROR_CODES.LIMIT_EXCEEDED, details: { attempt: attempts }
116
+ });
117
+ }
118
+ const deps = [...new Set((dependsOn ?? []).map((id) => {
119
+ if (typeof id !== "string" || !id) {
120
+ throw new OrchContractError("Dependency taskId is required.", { code: ORCH_ERROR_CODES.INVALID_NODE });
121
+ }
122
+ return id;
123
+ }))];
124
+ return {
125
+ taskId: taskId ?? createTaskId(), runId: runId ?? null, parentTaskId: parentTaskId ?? null,
126
+ depth: d, state, dependsOn: deps, attempt: attempts,
127
+ objectiveDigest, budget: budget ?? null, resultDigest, error: error ?? null
128
+ };
129
+ }
130
+
131
+ export function createMinionBrief({
132
+ objective, constraints = [], admittedPaths = [], exitCriteria = [],
133
+ parentTaskId = null, taskId = null
134
+ } = {}) {
135
+ if (typeof objective !== "string" || !objective.trim()) {
136
+ throw new OrchContractError("Minion brief requires a non-empty objective.", {
137
+ code: ORCH_ERROR_CODES.INVALID_HANDOFF
138
+ });
139
+ }
140
+ return {
141
+ taskId: taskId ?? createTaskId(), parentTaskId: parentTaskId ?? null,
142
+ objective: objective.trim(), constraints: (constraints ?? []).map(String),
143
+ admittedPaths: (admittedPaths ?? []).map(String), exitCriteria: (exitCriteria ?? []).map(String)
144
+ };
145
+ }
146
+
147
+ export function createMinionResult({
148
+ taskId = null, summary, decisions = [], files = [], risks = [],
149
+ evidence = [], usage = null, compact = false
150
+ } = {}) {
151
+ if (typeof taskId !== "string" || !taskId) {
152
+ throw new OrchContractError("Minion result requires taskId.", { code: ORCH_ERROR_CODES.INVALID_HANDOFF });
153
+ }
154
+ if (typeof summary !== "string" || !summary.trim()) {
155
+ throw new OrchContractError("Minion result requires a non-empty summary.", {
156
+ code: ORCH_ERROR_CODES.INVALID_HANDOFF
157
+ });
158
+ }
159
+ return {
160
+ taskId, summary: summary.trim(),
161
+ decisions: (decisions ?? []).map(String), files: (files ?? []).map(String),
162
+ risks: (risks ?? []).map(String), evidence: (evidence ?? []).map(String),
163
+ usage: {
164
+ inputTokens: usage?.inputTokens ?? null, outputTokens: usage?.outputTokens ?? null,
165
+ totalTokens: usage?.totalTokens ?? null, cost: usage?.cost ?? null
166
+ },
167
+ compact: Boolean(compact)
168
+ };
169
+ }
170
+
171
+ export function digestAllowlisted(value) {
172
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
173
+ }
@@ -0,0 +1,63 @@
1
+ import {
2
+ ORCH_ERROR_CODES, OrchContractError, createBudgetUsage, createDagNode,
3
+ createMinionResult, createOrchLineage, normalizeRunStrategy
4
+ } from "./orch-types.js";
5
+ export const FORBIDDEN_KEYS = new Set([
6
+ "prompt", "diff", "transcript", "raw", "rawOutput", "stdout", "stderr", "output",
7
+ "message", "messages", "content", "secret", "secrets", "token", "apiKey",
8
+ "conversation", "history", "toolArgs", "arguments", "objective"
9
+ ]);
10
+ export function walkForbiddenKeys(value, path = "") {
11
+ if (!value || typeof value !== "object") return;
12
+ if (Array.isArray(value)) {
13
+ value.forEach((item, i) => walkForbiddenKeys(item, `${path}[${i}]`));
14
+ return;
15
+ }
16
+ for (const [key, child] of Object.entries(value)) {
17
+ if (FORBIDDEN_KEYS.has(key)) {
18
+ throw new OrchContractError(`Forbidden field "${path}${key}" in orchestration receipt.`, {
19
+ code: ORCH_ERROR_CODES.FORBIDDEN_FIELD, details: { key, path }
20
+ });
21
+ }
22
+ walkForbiddenKeys(child, `${path}${key}.`);
23
+ }
24
+ }
25
+ export function assertOrchReceiptSecretFree(receipt) {
26
+ if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
27
+ throw new OrchContractError("Invalid orchestration receipt: expected object.", {
28
+ code: ORCH_ERROR_CODES.FORBIDDEN_FIELD
29
+ });
30
+ }
31
+ walkForbiddenKeys(receipt);
32
+ if (receipt.version !== 1) {
33
+ throw new OrchContractError("Orchestration receipt version must be 1.", {
34
+ code: ORCH_ERROR_CODES.FORBIDDEN_FIELD
35
+ });
36
+ }
37
+ normalizeRunStrategy(receipt.strategy);
38
+ if (typeof receipt.rootRunId !== "string" || !receipt.rootRunId) {
39
+ throw new OrchContractError("rootRunId is required on receipt.", {
40
+ code: ORCH_ERROR_CODES.INVALID_LINEAGE
41
+ });
42
+ }
43
+ createOrchLineage(receipt.lineage);
44
+ if (receipt.lineage.rootRunId !== receipt.rootRunId) {
45
+ throw new OrchContractError("lineage.rootRunId must match receipt.rootRunId.", {
46
+ code: ORCH_ERROR_CODES.INVALID_LINEAGE
47
+ });
48
+ }
49
+ if (!Array.isArray(receipt.nodes) || !Array.isArray(receipt.results)) {
50
+ throw new OrchContractError("Receipt requires nodes[] and results[].", {
51
+ code: ORCH_ERROR_CODES.INVALID_NODE
52
+ });
53
+ }
54
+ for (const node of receipt.nodes) {
55
+ createDagNode(node);
56
+ if (node.budget) createBudgetUsage(node.budget);
57
+ }
58
+ for (const result of receipt.results) createMinionResult(result);
59
+ if (typeof receipt.createdAt !== "string" || !receipt.createdAt) {
60
+ throw new OrchContractError("createdAt is required.", { code: ORCH_ERROR_CODES.FORBIDDEN_FIELD });
61
+ }
62
+ return receipt;
63
+ }
@@ -0,0 +1,37 @@
1
+ export {
2
+ REVIEW_SCOPE_MODES, REVIEW_SEVERITIES, REVIEW_STATES, REVIEW_EXIT_CODES, REVIEW_AGENTS,
3
+ REVIEW_LIMITS, REVIEW_SNAPSHOT_ERROR_CODES, ReviewSnapshotError, resolveReviewScopeMode,
4
+ createFindingId, canonicalFingerprint, isReviewPrivatePath, isBinaryContent,
5
+ assertReviewPathSafe, assertWithinReviewLimits, requirePrivateConsent
6
+ } from "./review-types.js";
7
+ export {
8
+ resolveReviewSnapshot, fingerprintReviewSnapshot, detectReviewSnapshotDrift,
9
+ readReviewRegularFile
10
+ } from "./review-git.js";
11
+ export {
12
+ REVIEW_PATCH_ERROR_CODES, filterDiffToAdmittedPaths, buildScopedReviewPatch
13
+ } from "./review-patch.js";
14
+ export {
15
+ REVIEW_VALIDATION_ERROR_CODES, ReviewValidationError,
16
+ validateReviewOutput, assertReceiptSecretFree
17
+ } from "./review-validate.js";
18
+ export {
19
+ assertSafeReviewId, createReviewId, reviewPaths,
20
+ buildReviewReceipt, saveReviewReceipt, loadReviewReceipt, listReviewReceipts
21
+ } from "./review-receipts.js";
22
+ export {
23
+ REVIEW_EXEC_ERROR_CODES, REVIEW_EXEC_LIMITS, ReviewExecError,
24
+ runBoundedProcess, assertBoundedProcessOk
25
+ } from "./review-exec.js";
26
+ export {
27
+ REVIEW_CODEX_ERROR_CODES, buildCodexReviewArgs, buildCodexCliEnv,
28
+ buildCodexReviewPrompt, parseCodexReviewJsonl, runCodexReview
29
+ } from "./review-codex.js";
30
+ export {
31
+ REVIEW_PI_ERROR_CODES, buildPiReviewArgs, buildPiCliEnv,
32
+ buildPiReviewPrompt, buildPiReviewStdin, parsePiReviewJsonl, runPiReview
33
+ } from "./review-pi.js";
34
+ export {
35
+ REVIEW_RUNNER_ERROR_CODES, ReviewRunnerError,
36
+ resolveReviewAgent, resolveReviewExitCode, runReview
37
+ } from "./review-runner.js";
@@ -0,0 +1,150 @@
1
+ import { resolveHomeDir } from "../../paths.js";
2
+ import { printJson } from "../../json-output.js";
3
+ import { commandHeader } from "../../brand/index.js";
4
+ import { formatCliCommand } from "../../brand/cli.js";
5
+ import {
6
+ isInteractiveTerminal, promptApplyConfirmation
7
+ } from "../../apply-confirmation.js";
8
+ import {
9
+ REVIEW_EXIT_CODES, REVIEW_SEVERITIES,
10
+ assertReceiptSecretFree, assertSafeReviewId,
11
+ listReviewReceipts, loadReviewReceipt
12
+ } from "./index.js";
13
+ import { runReview } from "./review-runner.js";
14
+
15
+ const FAIL_ON = new Set(Object.values(REVIEW_SEVERITIES));
16
+
17
+ function parseFailOn(value) {
18
+ if (value == null || value === "") return null;
19
+ const normalized = String(value).trim().toLowerCase();
20
+ if (!FAIL_ON.has(normalized)) {
21
+ throw new Error(`Invalid --fail-on "${value}". Use high, medium, or low.`);
22
+ }
23
+ return normalized;
24
+ }
25
+
26
+ async function resolvePrivateConfirmed(options, { prompt = promptApplyConfirmation } = {}) {
27
+ if (!options.includePrivate) return { privateConfirmed: false, cancelled: false };
28
+ if (options.yes || options.confirm) return { privateConfirmed: true, cancelled: false };
29
+ if (!isInteractiveTerminal(options.interactive)) {
30
+ throw new Error(
31
+ "Including private paths requires --include-private with --yes/--confirm, or a TTY confirmation."
32
+ );
33
+ }
34
+ const ok = await prompt({
35
+ command: "review",
36
+ question: "Include private paths in this review? [Y/n]: "
37
+ });
38
+ return { privateConfirmed: Boolean(ok), cancelled: !ok };
39
+ }
40
+
41
+ function publicReceipt(receipt) {
42
+ return assertReceiptSecretFree(receipt);
43
+ }
44
+
45
+ function printReviewHuman(receipt, exitCode) {
46
+ const counts = { high: 0, medium: 0, low: 0 };
47
+ for (const f of receipt.findings ?? []) {
48
+ if (counts[f.severity] != null) counts[f.severity] += 1;
49
+ }
50
+ console.log(commandHeader(`review ${receipt.reviewId}`));
51
+ console.log(`Agent: ${receipt.agentId} · state: ${receipt.state} · exit: ${exitCode}`);
52
+ console.log(
53
+ `Findings: ${(receipt.findings ?? []).length}`
54
+ + ` (high ${counts.high}, medium ${counts.medium}, low ${counts.low})`
55
+ );
56
+ console.log(
57
+ `Snapshot: ${receipt.snapshot.mode} · files ${receipt.snapshot.totals.fileCount}`
58
+ + ` · ${receipt.snapshot.fingerprint.slice(0, 12)}`
59
+ );
60
+ if ((receipt.warnings ?? []).length) console.log(`Warnings: ${receipt.warnings.length}`);
61
+ }
62
+
63
+ export async function runGlobalReview(options, packageManifest, deps = {}) {
64
+ const homeDir = deps.homeDir ?? resolveHomeDir();
65
+ try {
66
+ if (!options.agent) {
67
+ throw new Error(`Missing --agent. Use: ${formatCliCommand("review --agent codex|pi")}`);
68
+ }
69
+ const failOn = parseFailOn(options.failOn);
70
+ const consent = await resolvePrivateConfirmed(options, { prompt: deps.prompt });
71
+ if (consent.cancelled) {
72
+ if (options.json) {
73
+ printJson({
74
+ ok: false, cancelled: true, exitCode: REVIEW_EXIT_CODES.ERROR,
75
+ error: "Private path inclusion cancelled."
76
+ });
77
+ } else {
78
+ console.log("Review cancelled: private paths not included.");
79
+ }
80
+ process.exitCode = REVIEW_EXIT_CODES.ERROR;
81
+ return { cancelled: true, exitCode: REVIEW_EXIT_CODES.ERROR };
82
+ }
83
+
84
+ const result = await (deps.runReview ?? runReview)({
85
+ cwd: options.cwd, agent: options.agent, base: options.base ?? null,
86
+ commit: options.commit ?? null, model: options.model ?? null,
87
+ includePrivate: Boolean(options.includePrivate),
88
+ privateConfirmed: consent.privateConfirmed, failOn,
89
+ homeDir, cliVersion: packageManifest?.version ?? null
90
+ });
91
+ const receipt = publicReceipt(result.receipt);
92
+ if (options.json) printJson({ ok: result.exitCode === 0, exitCode: result.exitCode, receipt });
93
+ else printReviewHuman(receipt, result.exitCode);
94
+ process.exitCode = result.exitCode;
95
+ return { receipt, exitCode: result.exitCode };
96
+ } catch (error) {
97
+ const exitCode = REVIEW_EXIT_CODES.ERROR;
98
+ const message = String(error?.message ?? error);
99
+ if (options.json) printJson({ ok: false, exitCode, error: message, code: error?.code ?? null });
100
+ else console.error(message);
101
+ process.exitCode = exitCode;
102
+ return { exitCode, error };
103
+ }
104
+ }
105
+
106
+ export async function runGlobalReviews(options, _packageManifest, deps = {}) {
107
+ const homeDir = deps.homeDir ?? resolveHomeDir();
108
+ try {
109
+ const action = options.reviewsAction ?? "list";
110
+ if (action === "list") {
111
+ const receipts = (await listReviewReceipts({ homeDir, limit: options.limit }))
112
+ .map((r) => publicReceipt(r));
113
+ if (options.json) printJson({ receipts });
114
+ else {
115
+ console.log(commandHeader("reviews"));
116
+ if (receipts.length === 0) console.log(" (no reviews yet)");
117
+ for (const r of receipts) {
118
+ console.log(
119
+ ` ${r.reviewId} ${String(r.state).padEnd(10)} ${String(r.agentId).padEnd(6)} ${r.createdAt}`
120
+ );
121
+ }
122
+ }
123
+ return { receipts };
124
+ }
125
+ if (action === "show") {
126
+ if (!options.reviewId) {
127
+ throw new Error(`Missing review id. Use: ${formatCliCommand("reviews show <reviewId>")}`);
128
+ }
129
+ try { assertSafeReviewId(options.reviewId); }
130
+ catch { throw new Error(`Invalid review id "${options.reviewId}".`); }
131
+ let receipt;
132
+ try {
133
+ receipt = publicReceipt(await loadReviewReceipt(options.reviewId, { homeDir }));
134
+ } catch {
135
+ throw new Error(`Review receipt not found: ${options.reviewId}`);
136
+ }
137
+ if (options.json) printJson({ receipt });
138
+ else printReviewHuman(receipt, REVIEW_EXIT_CODES.OK);
139
+ return { receipt };
140
+ }
141
+ throw new Error(`Unknown reviews action "${action}". Use list or show.`);
142
+ } catch (error) {
143
+ const exitCode = REVIEW_EXIT_CODES.ERROR;
144
+ const message = String(error?.message ?? error);
145
+ if (options.json) printJson({ ok: false, exitCode, error: message, code: error?.code ?? null });
146
+ else console.error(message);
147
+ process.exitCode = exitCode;
148
+ return { exitCode, error };
149
+ }
150
+ }