@kal-elsam/kairo-runtime 0.10.0 → 0.12.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.
- package/global-template/components/agent-skills/LICENSE +21 -0
- package/global-template/components/agent-skills/PROVENANCE.md +26 -0
- package/global-template/components/agent-skills/skills/context-engineering/SKILL.md +289 -0
- package/global-template/components/agent-skills/skills/frontend-ui-engineering/SKILL.md +328 -0
- package/global-template/components/agent-skills/skills/observability-and-instrumentation/SKILL.md +203 -0
- package/global-template/components/agent-skills/skills/performance-optimization/SKILL.md +396 -0
- package/global-template/components/agent-skills/skills/source-driven-development/SKILL.md +194 -0
- package/global-template/components/catalog.json +29 -0
- package/package.json +5 -2
- package/scripts/cockpit-smoke.mjs +1 -1
- package/src/cli.js +136 -8
- package/src/global/component-builders.js +3 -1
- package/src/global/components/agent-skills.js +27 -0
- package/src/global/ink/brand/wordmark.js +50 -0
- package/src/global/ink/cockpit/primitives.js +45 -35
- package/src/global/ink/cockpit-control-center.js +104 -4
- package/src/global/ink/cockpit-scan.js +20 -2
- package/src/global/ink/cockpit-views.js +2 -1
- package/src/global/ink/ecosystem-updates-display.js +37 -0
- package/src/global/ink/launch-input.js +32 -1
- package/src/global/ink/orchestrator-app.js +14 -5
- package/src/global/ink/orchestrator-state.js +17 -2
- package/src/global/ink/system-resources-display.js +109 -0
- package/src/global/ink/theme.js +13 -8
- package/src/global/ink/use-orchestrator-data.js +40 -4
- package/src/global/ink/ux/live-activity.js +4 -7
- package/src/global/ink/ux/live-alerts.js +2 -5
- package/src/global/ink/ux/live-governance.js +2 -6
- package/src/global/ink/ux/live-orchestration.js +2 -5
- package/src/global/ink/ux/live-overview.js +88 -28
- package/src/global/ink/ux/live-settings.js +3 -6
- package/src/global/ink/ux/live-usage.js +5 -7
- package/src/global/ink/ux/semantic.js +23 -6
- package/src/global/mcp/kairo-mcp.js +230 -0
- package/src/global/observability/build-companion-snapshot.js +281 -0
- package/src/global/observability/build-observability-snapshot.js +24 -0
- package/src/global/observability/ecosystem-updates.js +224 -0
- package/src/global/observability/gentle-bundle-export.js +71 -0
- package/src/global/observability/gentle-bundle-import.js +122 -0
- package/src/global/observability/gentle-probe.js +155 -0
- package/src/global/observability/graphify-ops.js +133 -0
- package/src/global/observability/graphify-parse-cache.js +90 -0
- package/src/global/observability/graphify-probe.js +185 -0
- package/src/global/observability/hermes-activity.js +163 -0
- package/src/global/observability/hermes-probe.js +171 -0
- package/src/global/observability/index.js +85 -0
- package/src/global/observability/passive-snapshot-flight.js +93 -0
- package/src/global/observability/probe-contract.js +38 -0
- package/src/global/observability/probe-registry.js +30 -0
- package/src/global/observability/resource-advisor.js +71 -0
- package/src/global/observability/system-resources.js +171 -0
- package/src/global/runtime/alerts/alert-cli.js +31 -0
- package/src/global/runtime/alerts/alert-store.js +29 -6
- package/src/global/runtime/alerts/alert-validate.js +25 -1
- package/src/global/runtime/alerts/controlled-alert-actions.js +56 -0
- package/src/global/runtime/execution-adapters/claude.js +2 -1
- package/src/global/runtime/execution-adapters/codex.js +2 -1
- package/src/global/runtime/execution-adapters/create-execution-adapter.js +3 -14
- package/src/global/runtime/execution-adapters/cursor.js +2 -1
- package/src/global/runtime/execution-adapters/opencode.js +2 -1
- package/src/global/runtime/execution-adapters/pi.js +2 -1
- package/src/global/runtime/review/index.js +1 -1
- package/src/global/runtime/review/review-cli.js +113 -3
- package/src/global/runtime/review/review-git.js +142 -11
- package/src/global/runtime/review/review-patch.js +2 -0
- package/src/global/runtime/review/review-receipts.js +12 -7
- package/src/global/runtime/review/review-runner.js +2 -2
- package/src/global/runtime/review/review-types.js +8 -5
- package/src/global/runtime/review/review-validate.js +5 -1
- package/src/global/runtime/run-cli.js +2 -0
- package/src/global/runtime/run-manager.js +39 -18
- package/src/global/runtime/run-permissions.js +231 -0
- package/src/global/runtime/run-profile.js +2 -0
- package/src/global/runtime/run-supervisor.js +77 -37
- package/src/global/runtime/run-types.js +2 -0
- package/src/global/updates-cli.js +41 -0
|
@@ -0,0 +1,31 @@
|
|
|
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 { controlledDismissAlert, controlledResolveAlert } from "./controlled-alert-actions.js";
|
|
6
|
+
|
|
7
|
+
export async function runGlobalAlerts(options) {
|
|
8
|
+
const homeDir = resolveHomeDir();
|
|
9
|
+
const action = options.alertsAction;
|
|
10
|
+
const alertId = options.alertId;
|
|
11
|
+
if ((action !== "resolve" && action !== "dismiss") || !alertId) {
|
|
12
|
+
throw new Error(`Use: ${formatCliCommand("alerts resolve|dismiss <alertId> --confirm-…")}`);
|
|
13
|
+
}
|
|
14
|
+
const isResolve = action === "resolve";
|
|
15
|
+
const confirmed = Boolean(isResolve ? options.confirmResolve : options.confirmDismiss);
|
|
16
|
+
const result = await (isResolve ? controlledResolveAlert : controlledDismissAlert)({
|
|
17
|
+
alertId, confirmed, source: "cli", homeDir
|
|
18
|
+
});
|
|
19
|
+
if (options.json) {
|
|
20
|
+
printJson({
|
|
21
|
+
ok: result.ok, code: result.code, alertId, state: result.alert?.state ?? null,
|
|
22
|
+
permissionAuthority: result.permissionAuthority, diagnostics: result.diagnostics
|
|
23
|
+
});
|
|
24
|
+
} else if (result.ok) {
|
|
25
|
+
console.log(`${commandHeader(`alerts ${action}`)}\n${alertId} → ${result.alert.state}`);
|
|
26
|
+
} else {
|
|
27
|
+
console.error(`alerts ${action} failed (${result.code}).`);
|
|
28
|
+
}
|
|
29
|
+
if (!result.ok) process.exitCode = 2;
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
createAlertFingerprint
|
|
11
11
|
} from "./alert-types.js";
|
|
12
12
|
import { assertAlertSecretFree } from "./alert-validate.js";
|
|
13
|
+
import {
|
|
14
|
+
PermissionAuthorityError,
|
|
15
|
+
UNSAFE_OPERATIONS,
|
|
16
|
+
assertUnsafePermissionAuthority
|
|
17
|
+
} from "../run-permissions.js";
|
|
13
18
|
|
|
14
19
|
const TERMINAL_ALERT_STATES = new Set([ALERT_STATES.RESOLVED, ALERT_STATES.DISMISSED]);
|
|
15
20
|
|
|
@@ -190,7 +195,7 @@ export async function listAlerts({ homeDir, state = null, limit = null } = {}) {
|
|
|
190
195
|
return Number.isInteger(limit) && limit >= 0 ? filtered.slice(0, limit) : filtered;
|
|
191
196
|
}
|
|
192
197
|
|
|
193
|
-
async function transitionAlert(alertId, nextState, { homeDir } = {}) {
|
|
198
|
+
async function transitionAlert(alertId, nextState, { homeDir, permissionAuthority } = {}) {
|
|
194
199
|
const current = await loadAlert(alertId, { homeDir });
|
|
195
200
|
if (current.state !== ALERT_STATES.OPEN) return current;
|
|
196
201
|
const now = new Date().toISOString();
|
|
@@ -198,7 +203,8 @@ async function transitionAlert(alertId, nextState, { homeDir } = {}) {
|
|
|
198
203
|
...current,
|
|
199
204
|
state: nextState,
|
|
200
205
|
updatedAt: now,
|
|
201
|
-
resolvedAt: now
|
|
206
|
+
resolvedAt: now,
|
|
207
|
+
permissionAuthority
|
|
202
208
|
}, { homeDir });
|
|
203
209
|
const indexPath = openIndexPath(homeDir, current.fingerprint);
|
|
204
210
|
await unlink(indexPath).catch((error) => {
|
|
@@ -207,10 +213,27 @@ async function transitionAlert(alertId, nextState, { homeDir } = {}) {
|
|
|
207
213
|
return updated;
|
|
208
214
|
}
|
|
209
215
|
|
|
210
|
-
|
|
211
|
-
|
|
216
|
+
function requireMutationAuthority(permissionAuthority, expectedOperation) {
|
|
217
|
+
try {
|
|
218
|
+
return assertUnsafePermissionAuthority(permissionAuthority, { expectedOperation });
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if (error instanceof PermissionAuthorityError) {
|
|
221
|
+
throw new AlertStoreError(error.message, {
|
|
222
|
+
code: error.code ?? "permission_authority_required", details: error.details
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
212
227
|
}
|
|
213
228
|
|
|
214
|
-
export async function
|
|
215
|
-
return transitionAlert(alertId, ALERT_STATES.
|
|
229
|
+
export async function resolveAlert(alertId, { homeDir, permissionAuthority } = {}) {
|
|
230
|
+
return transitionAlert(alertId, ALERT_STATES.RESOLVED, {
|
|
231
|
+
homeDir, permissionAuthority: requireMutationAuthority(permissionAuthority, UNSAFE_OPERATIONS.ALERT_RESOLVE)
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function dismissAlert(alertId, { homeDir, permissionAuthority } = {}) {
|
|
236
|
+
return transitionAlert(alertId, ALERT_STATES.DISMISSED, {
|
|
237
|
+
homeDir, permissionAuthority: requireMutationAuthority(permissionAuthority, UNSAFE_OPERATIONS.ALERT_DISMISS)
|
|
238
|
+
});
|
|
216
239
|
}
|
|
@@ -19,8 +19,10 @@ const FORBIDDEN_KEYS = new Set([
|
|
|
19
19
|
]);
|
|
20
20
|
const ALLOWED = new Set([
|
|
21
21
|
"version", "alertId", "kind", "severity", "title", "summary",
|
|
22
|
-
"source", "fingerprint", "state", "createdAt", "updatedAt", "resolvedAt"
|
|
22
|
+
"source", "fingerprint", "state", "createdAt", "updatedAt", "resolvedAt",
|
|
23
|
+
"permissionAuthority"
|
|
23
24
|
]);
|
|
25
|
+
const PA_KEYS = new Set(["mode", "source", "consent", "operation"]);
|
|
24
26
|
|
|
25
27
|
export class AlertValidationError extends Error {
|
|
26
28
|
constructor(message, { code, details = null } = {}) {
|
|
@@ -102,6 +104,28 @@ export function assertAlertSecretFree(alert) {
|
|
|
102
104
|
code: ALERT_VALIDATION_ERROR_CODES.INVALID_ALERT
|
|
103
105
|
});
|
|
104
106
|
}
|
|
107
|
+
if (alert.permissionAuthority != null) {
|
|
108
|
+
const pa = alert.permissionAuthority;
|
|
109
|
+
if (!pa || typeof pa !== "object" || Array.isArray(pa)) {
|
|
110
|
+
throw new AlertValidationError("Invalid alert.permissionAuthority.", {
|
|
111
|
+
code: ALERT_VALIDATION_ERROR_CODES.INVALID_ALERT
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
for (const key of Object.keys(pa)) {
|
|
115
|
+
if (!PA_KEYS.has(key) || typeof pa[key] !== "string" || !pa[key].trim()) {
|
|
116
|
+
throw new AlertValidationError(`Invalid alert.permissionAuthority.${key}.`, {
|
|
117
|
+
code: ALERT_VALIDATION_ERROR_CODES.INVALID_ALERT, details: { key }
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const key of PA_KEYS) {
|
|
122
|
+
if (typeof pa[key] !== "string" || !pa[key].trim()) {
|
|
123
|
+
throw new AlertValidationError(`Missing alert.permissionAuthority.${key}.`, {
|
|
124
|
+
code: ALERT_VALIDATION_ERROR_CODES.INVALID_ALERT, details: { key }
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
105
129
|
const expected = createAlertFingerprint({
|
|
106
130
|
kind: alert.kind,
|
|
107
131
|
source: alert.source,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONSENT_TYPES,
|
|
3
|
+
PermissionAuthorityError,
|
|
4
|
+
UNSAFE_OPERATIONS,
|
|
5
|
+
authorizeUnsafeOperation as defaultAuthorize
|
|
6
|
+
} from "../run-permissions.js";
|
|
7
|
+
import { dismissAlert, resolveAlert } from "./alert-store.js";
|
|
8
|
+
|
|
9
|
+
function fail(code, message, permissionAuthority = null) {
|
|
10
|
+
return { ok: false, code, message, alert: null, permissionAuthority, diagnostics: [code] };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function runControlled({
|
|
14
|
+
operation, confirmed, source, consentType, alertId, homeDir,
|
|
15
|
+
authorizeUnsafeOperation = defaultAuthorize, mutate
|
|
16
|
+
}) {
|
|
17
|
+
let permissionAuthority = null;
|
|
18
|
+
try {
|
|
19
|
+
({ permissionAuthority } = authorizeUnsafeOperation({
|
|
20
|
+
operation, confirmed: Boolean(confirmed), source, consentType
|
|
21
|
+
}));
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error instanceof PermissionAuthorityError) {
|
|
24
|
+
return fail(error.code ?? "unsafe_consent_required", error.message, null);
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
const alert = await mutate(alertId, { homeDir, permissionAuthority });
|
|
29
|
+
return {
|
|
30
|
+
ok: true, code: "ok", message: null, alert, permissionAuthority, diagnostics: []
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function controlledResolveAlert({
|
|
35
|
+
alertId, confirmed = false, source = "cli", consentType = null, homeDir,
|
|
36
|
+
authorizeUnsafeOperation, resolve = resolveAlert
|
|
37
|
+
} = {}) {
|
|
38
|
+
return runControlled({
|
|
39
|
+
operation: UNSAFE_OPERATIONS.ALERT_RESOLVE, confirmed, source, consentType, alertId, homeDir,
|
|
40
|
+
authorizeUnsafeOperation,
|
|
41
|
+
mutate: (id, opts) => resolve(id, opts)
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function controlledDismissAlert({
|
|
46
|
+
alertId, confirmed = false, source = "cli", consentType = null, homeDir,
|
|
47
|
+
authorizeUnsafeOperation, dismiss = dismissAlert
|
|
48
|
+
} = {}) {
|
|
49
|
+
return runControlled({
|
|
50
|
+
operation: UNSAFE_OPERATIONS.ALERT_DISMISS, confirmed, source, consentType, alertId, homeDir,
|
|
51
|
+
authorizeUnsafeOperation,
|
|
52
|
+
mutate: (id, opts) => dismiss(id, opts)
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { CONSENT_TYPES, UNSAFE_OPERATIONS };
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { isExecutableAvailable } from "../../cli-probe.js";
|
|
2
|
+
import { buildPermissionsArgs } from "../run-permissions.js";
|
|
3
|
+
|
|
4
|
+
export { buildPermissionsArgs };
|
|
2
5
|
|
|
3
6
|
export function createExecutionAdapter({
|
|
4
7
|
id,
|
|
@@ -77,17 +80,3 @@ export function parseNdjsonLine(line) {
|
|
|
77
80
|
return null;
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
|
-
|
|
81
|
-
export function buildPermissionsArgs(permissions = []) {
|
|
82
|
-
const normalized = new Set(permissions.map((entry) => String(entry).toLowerCase()));
|
|
83
|
-
|
|
84
|
-
if (normalized.has("all") || normalized.has("force")) {
|
|
85
|
-
return ["--force"];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (normalized.has("yolo") || normalized.has("dangerously-skip-permissions")) {
|
|
89
|
-
return ["--dangerously-skip-permissions"];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return [];
|
|
93
|
-
}
|
|
@@ -96,7 +96,8 @@ export default createExecutionAdapter({
|
|
|
96
96
|
tokens: true,
|
|
97
97
|
diff: false,
|
|
98
98
|
cancel: true,
|
|
99
|
-
transcript: true
|
|
99
|
+
transcript: true,
|
|
100
|
+
permissionModes: ["force", "yolo"]
|
|
100
101
|
},
|
|
101
102
|
checkAvailability: checkCursorAvailability,
|
|
102
103
|
buildLaunch: buildCursorLaunch,
|
|
@@ -186,7 +186,8 @@ export default createExecutionAdapter({
|
|
|
186
186
|
diff: false,
|
|
187
187
|
cancel: true,
|
|
188
188
|
transcript: true,
|
|
189
|
-
reviewCompatible: true
|
|
189
|
+
reviewCompatible: true,
|
|
190
|
+
permissionModes: ["read-only"]
|
|
190
191
|
},
|
|
191
192
|
checkAvailability: checkPiAvailability,
|
|
192
193
|
buildLaunch: buildPiLaunch,
|
|
@@ -6,7 +6,7 @@ export {
|
|
|
6
6
|
} from "./review-types.js";
|
|
7
7
|
export {
|
|
8
8
|
resolveReviewSnapshot, fingerprintReviewSnapshot, detectReviewSnapshotDrift,
|
|
9
|
-
readReviewRegularFile
|
|
9
|
+
verifyStagedReviewReceipt, readReviewRegularFile
|
|
10
10
|
} from "./review-git.js";
|
|
11
11
|
export {
|
|
12
12
|
REVIEW_PATCH_ERROR_CODES, filterDiffToAdmittedPaths, buildScopedReviewPatch
|
|
@@ -5,10 +5,12 @@ import { formatCliCommand } from "../../brand/cli.js";
|
|
|
5
5
|
import {
|
|
6
6
|
isInteractiveTerminal, promptApplyConfirmation
|
|
7
7
|
} from "../../apply-confirmation.js";
|
|
8
|
+
import { exportGentleReviewBundle as defaultExportGentleReviewBundle } from "../../observability/gentle-bundle-export.js";
|
|
9
|
+
import { importGentleReviewBundle as defaultImportGentleReviewBundle } from "../../observability/gentle-bundle-import.js";
|
|
8
10
|
import {
|
|
9
11
|
REVIEW_EXIT_CODES, REVIEW_SEVERITIES,
|
|
10
12
|
assertReceiptSecretFree, assertSafeReviewId,
|
|
11
|
-
listReviewReceipts, loadReviewReceipt
|
|
13
|
+
listReviewReceipts, loadReviewReceipt, verifyStagedReviewReceipt
|
|
12
14
|
} from "./index.js";
|
|
13
15
|
import { runReview } from "./review-runner.js";
|
|
14
16
|
|
|
@@ -83,7 +85,8 @@ export async function runGlobalReview(options, packageManifest, deps = {}) {
|
|
|
83
85
|
|
|
84
86
|
const result = await (deps.runReview ?? runReview)({
|
|
85
87
|
cwd: options.cwd, agent: options.agent, base: options.base ?? null,
|
|
86
|
-
commit: options.commit ?? null,
|
|
88
|
+
commit: options.commit ?? null, staged: Boolean(options.staged),
|
|
89
|
+
model: options.model ?? null,
|
|
87
90
|
includePrivate: Boolean(options.includePrivate),
|
|
88
91
|
privateConfirmed: consent.privateConfirmed, failOn,
|
|
89
92
|
homeDir, cliVersion: packageManifest?.version ?? null
|
|
@@ -138,7 +141,114 @@ export async function runGlobalReviews(options, _packageManifest, deps = {}) {
|
|
|
138
141
|
else printReviewHuman(receipt, REVIEW_EXIT_CODES.OK);
|
|
139
142
|
return { receipt };
|
|
140
143
|
}
|
|
141
|
-
|
|
144
|
+
if (action === "verify") {
|
|
145
|
+
if (!options.reviewId) {
|
|
146
|
+
throw new Error(`Missing review id. Use: ${formatCliCommand("reviews verify <reviewId> --staged")}`);
|
|
147
|
+
}
|
|
148
|
+
if (!options.staged) {
|
|
149
|
+
throw new Error(`Staged verification requires --staged. Use: ${formatCliCommand("reviews verify <reviewId> --staged")}`);
|
|
150
|
+
}
|
|
151
|
+
try { assertSafeReviewId(options.reviewId); }
|
|
152
|
+
catch { throw new Error(`Invalid review id "${options.reviewId}".`); }
|
|
153
|
+
let receipt;
|
|
154
|
+
try {
|
|
155
|
+
receipt = publicReceipt(await loadReviewReceipt(options.reviewId, { homeDir }));
|
|
156
|
+
} catch {
|
|
157
|
+
throw new Error(`Review receipt not found: ${options.reviewId}`);
|
|
158
|
+
}
|
|
159
|
+
const verified = await (deps.verifyStaged ?? verifyStagedReviewReceipt)(receipt, {
|
|
160
|
+
cwd: options.cwd ?? process.cwd()
|
|
161
|
+
});
|
|
162
|
+
const exitCode = verified.ok ? REVIEW_EXIT_CODES.OK : REVIEW_EXIT_CODES.ERROR;
|
|
163
|
+
if (options.json) {
|
|
164
|
+
printJson({
|
|
165
|
+
ok: verified.ok, exitCode, stale: verified.stale, reviewId: receipt.reviewId,
|
|
166
|
+
previousFingerprint: verified.previousFingerprint,
|
|
167
|
+
nextFingerprint: verified.nextFingerprint, headSha: verified.headSha
|
|
168
|
+
});
|
|
169
|
+
} else {
|
|
170
|
+
console.log(commandHeader(`reviews verify ${receipt.reviewId}`));
|
|
171
|
+
console.log(verified.ok ? "Staged candidate matches receipt." : "Staged candidate drifted; receipt invalid.");
|
|
172
|
+
}
|
|
173
|
+
process.exitCode = exitCode;
|
|
174
|
+
return { ...verified, exitCode, receipt };
|
|
175
|
+
}
|
|
176
|
+
if (action === "export") {
|
|
177
|
+
if (!options.lineage) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Missing lineage. Use: ${formatCliCommand("reviews export <lineage> --out <path>")}`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (!options.outPath) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`Missing --out. Use: ${formatCliCommand("reviews export <lineage> --out <path>")}`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const exported = await (deps.exportGentleReviewBundle ?? defaultExportGentleReviewBundle)({
|
|
188
|
+
lineage: options.lineage,
|
|
189
|
+
outPath: options.outPath,
|
|
190
|
+
cwd: options.cwd ?? process.cwd()
|
|
191
|
+
});
|
|
192
|
+
const exitCode = exported.ok ? REVIEW_EXIT_CODES.OK : REVIEW_EXIT_CODES.ERROR;
|
|
193
|
+
process.exitCode = exitCode;
|
|
194
|
+
if (options.json) {
|
|
195
|
+
printJson({
|
|
196
|
+
ok: exported.ok,
|
|
197
|
+
exitCode,
|
|
198
|
+
code: exported.code,
|
|
199
|
+
lineage: exported.lineage,
|
|
200
|
+
outPath: exported.outPath,
|
|
201
|
+
diagnostics: exported.diagnostics,
|
|
202
|
+
providerStatus: exported.providerStatus ?? null,
|
|
203
|
+
timedOut: Boolean(exported.timedOut)
|
|
204
|
+
});
|
|
205
|
+
} else {
|
|
206
|
+
console.log(commandHeader(`reviews export ${exported.lineage ?? options.lineage}`));
|
|
207
|
+
if (exported.ok) console.log(`Exported via Gentle AI → ${exported.outPath}`);
|
|
208
|
+
else {
|
|
209
|
+
console.error(`Export failed (${exported.code}).`);
|
|
210
|
+
for (const line of exported.diagnostics ?? []) console.error(` ${line}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return { ...exported, exitCode };
|
|
214
|
+
}
|
|
215
|
+
if (action === "import") {
|
|
216
|
+
if (!options.bundlePath) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Missing --bundle. Use: ${formatCliCommand("reviews import --bundle <path> --confirm-import")}`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
const imported = await (deps.importGentleReviewBundle ?? defaultImportGentleReviewBundle)({
|
|
222
|
+
bundlePath: options.bundlePath,
|
|
223
|
+
cwd: options.cwd ?? process.cwd(),
|
|
224
|
+
confirmImport: Boolean(options.confirmImport)
|
|
225
|
+
});
|
|
226
|
+
const exitCode = imported.ok ? REVIEW_EXIT_CODES.OK : REVIEW_EXIT_CODES.ERROR;
|
|
227
|
+
process.exitCode = exitCode;
|
|
228
|
+
if (options.json) {
|
|
229
|
+
printJson({
|
|
230
|
+
ok: imported.ok,
|
|
231
|
+
exitCode,
|
|
232
|
+
code: imported.code,
|
|
233
|
+
mutationOutcome: imported.mutationOutcome,
|
|
234
|
+
bundlePath: imported.bundlePath,
|
|
235
|
+
cwd: imported.cwd,
|
|
236
|
+
diagnostics: imported.diagnostics,
|
|
237
|
+
providerStatus: imported.providerStatus ?? null,
|
|
238
|
+
timedOut: Boolean(imported.timedOut),
|
|
239
|
+
permissionAuthority: imported.permissionAuthority ?? null
|
|
240
|
+
});
|
|
241
|
+
} else {
|
|
242
|
+
console.log(commandHeader(`reviews import ${imported.bundlePath ?? options.bundlePath}`));
|
|
243
|
+
if (imported.ok) console.log("Imported via Gentle AI (provider-owned authority).");
|
|
244
|
+
else {
|
|
245
|
+
console.error(`Import failed (${imported.code}; mutation=${imported.mutationOutcome}).`);
|
|
246
|
+
for (const line of imported.diagnostics ?? []) console.error(` ${line}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return { ...imported, exitCode };
|
|
250
|
+
}
|
|
251
|
+
throw new Error(`Unknown reviews action "${action}". Use list, show, verify, export, or import.`);
|
|
142
252
|
} catch (error) {
|
|
143
253
|
const exitCode = REVIEW_EXIT_CODES.ERROR;
|
|
144
254
|
const message = String(error?.message ?? error);
|
|
@@ -91,24 +91,94 @@ function parseNameStatus(text) {
|
|
|
91
91
|
function fingerprintPayload(s) {
|
|
92
92
|
return {
|
|
93
93
|
mode: s.mode, headSha: s.headSha, base: s.base ?? null, commit: s.commit ?? null,
|
|
94
|
-
files: s.files.map((f) =>
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
files: s.files.map((f) => {
|
|
95
|
+
const entry = {
|
|
96
|
+
path: f.path, sourcePath: f.sourcePath ?? null, status: f.status,
|
|
97
|
+
hash: f.hash, changedLines: f.changedLines
|
|
98
|
+
};
|
|
99
|
+
if (f.mode != null) entry.mode = f.mode;
|
|
100
|
+
return entry;
|
|
101
|
+
}),
|
|
97
102
|
excluded: s.excluded.map((e) => ({ path: e.path, reason: e.reason }))
|
|
98
103
|
};
|
|
99
104
|
}
|
|
100
105
|
|
|
106
|
+
function parseRawDiff(text) {
|
|
107
|
+
return text.split("\n").filter((l) => l.startsWith(":")).map((line) => {
|
|
108
|
+
const columns = line.slice(1).split("\t");
|
|
109
|
+
const parts = columns[0].trim().split(/\s+/);
|
|
110
|
+
const [oldMode, newMode, oldHash, newHash, status] = parts;
|
|
111
|
+
const pathParts = columns.slice(1).map(unquotePath);
|
|
112
|
+
if (pathParts.length >= 2) {
|
|
113
|
+
return {
|
|
114
|
+
oldMode, newMode, oldHash, newHash, status,
|
|
115
|
+
sourcePath: pathParts[0], path: pathParts[1]
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
oldMode, newMode, oldHash, newHash, status,
|
|
120
|
+
sourcePath: null, path: pathParts[0]
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function resolveObjectIdLength(cwd, execFileImpl) {
|
|
126
|
+
try {
|
|
127
|
+
const format = (await git(cwd, ["rev-parse", "--show-object-format"], execFileImpl)).trim();
|
|
128
|
+
return format === "sha256" ? 64 : 40;
|
|
129
|
+
} catch {
|
|
130
|
+
return 40;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function assertFullObjectId(hash, expectedLength, { path, role = "blob" } = {}) {
|
|
135
|
+
if (hash == null || hash === "" || /^0+$/.test(hash)) {
|
|
136
|
+
throw new ReviewSnapshotError(
|
|
137
|
+
`Missing ${role} object ID for staged path "${path}".`,
|
|
138
|
+
{
|
|
139
|
+
code: REVIEW_SNAPSHOT_ERROR_CODES.TRUNCATED_OBJECT_ID,
|
|
140
|
+
details: { path, role, hash, expectedLength }
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (!new RegExp(`^[a-f0-9]{${expectedLength}}$`, "i").test(hash)) {
|
|
145
|
+
throw new ReviewSnapshotError(
|
|
146
|
+
`Truncated or invalid ${role} object ID for "${path}" `
|
|
147
|
+
+ `(got length ${String(hash).length}, expected ${expectedLength}).`,
|
|
148
|
+
{
|
|
149
|
+
code: REVIEW_SNAPSHOT_ERROR_CODES.TRUNCATED_OBJECT_ID,
|
|
150
|
+
details: { path, role, hash, expectedLength }
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return hash.toLowerCase();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function blobIsBinary(cwd, blobHash, execFileImpl) {
|
|
158
|
+
if (!blobHash || /^0+$/.test(blobHash)) return false;
|
|
159
|
+
try {
|
|
160
|
+
const { stdout } = await execFileImpl("git", ["cat-file", "-p", blobHash], {
|
|
161
|
+
cwd, encoding: null, maxBuffer: 8 * 1024 * 1024
|
|
162
|
+
});
|
|
163
|
+
return isBinaryContent(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout ?? ""));
|
|
164
|
+
} catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
101
169
|
/** Bounded Git review snapshot via argv-only git (no shell, no repo writes). */
|
|
102
170
|
export async function resolveReviewSnapshot({
|
|
103
|
-
cwd, base = null, commit = null, includePrivate = false, privateConfirmed = false,
|
|
171
|
+
cwd, base = null, commit = null, staged = false, includePrivate = false, privateConfirmed = false,
|
|
104
172
|
execFileImpl = defaultExecFile
|
|
105
173
|
} = {}) {
|
|
106
|
-
const mode = resolveReviewScopeMode({ base, commit });
|
|
174
|
+
const mode = resolveReviewScopeMode({ base, commit, staged });
|
|
107
175
|
await assertGitRepo(cwd, execFileImpl);
|
|
108
176
|
const headSha = (await git(cwd, ["rev-parse", "HEAD"], execFileImpl)).trim();
|
|
109
177
|
let rawEntries = [];
|
|
110
178
|
let numstat = new Map();
|
|
111
179
|
let diffBytes = 0;
|
|
180
|
+
let rawIdentity = new Map();
|
|
181
|
+
let stagedObjectIdLength = null;
|
|
112
182
|
|
|
113
183
|
if (mode === REVIEW_SCOPE_MODES.WORKING_TREE) {
|
|
114
184
|
rawEntries = parsePorcelain(await git(cwd, ["status", "--porcelain=v1", "-uall"], execFileImpl));
|
|
@@ -118,6 +188,17 @@ export async function resolveReviewSnapshot({
|
|
|
118
188
|
]);
|
|
119
189
|
diffBytes = Buffer.byteLength(await git(cwd, ["diff"], execFileImpl), "utf8")
|
|
120
190
|
+ Buffer.byteLength(await git(cwd, ["diff", "--cached"], execFileImpl), "utf8");
|
|
191
|
+
} else if (mode === REVIEW_SCOPE_MODES.STAGED) {
|
|
192
|
+
stagedObjectIdLength = await resolveObjectIdLength(cwd, execFileImpl);
|
|
193
|
+
rawEntries = parseNameStatus(await git(cwd, ["diff", "--cached", "--name-status"], execFileImpl));
|
|
194
|
+
numstat = parseNumstat(await git(cwd, ["diff", "--cached", "--numstat"], execFileImpl));
|
|
195
|
+
diffBytes = Buffer.byteLength(await git(cwd, ["diff", "--cached"], execFileImpl), "utf8");
|
|
196
|
+
const rawText = await git(cwd, [
|
|
197
|
+
"-c", "core.abbrev=no", "diff", "--cached", "--raw", "--full-index"
|
|
198
|
+
], execFileImpl);
|
|
199
|
+
for (const entry of parseRawDiff(rawText)) {
|
|
200
|
+
rawIdentity.set(entry.path, entry);
|
|
201
|
+
}
|
|
121
202
|
} else if (mode === REVIEW_SCOPE_MODES.BASE) {
|
|
122
203
|
const range = `${base}...HEAD`;
|
|
123
204
|
await git(cwd, ["rev-parse", "--verify", base], execFileImpl);
|
|
@@ -127,10 +208,10 @@ export async function resolveReviewSnapshot({
|
|
|
127
208
|
} else {
|
|
128
209
|
await git(cwd, ["rev-parse", "--verify", `${commit}^{commit}`], execFileImpl);
|
|
129
210
|
rawEntries = parseNameStatus(
|
|
130
|
-
await git(cwd, ["diff-tree", "--no-commit-id", "--name-status", "-r", commit], execFileImpl)
|
|
211
|
+
await git(cwd, ["diff-tree", "--no-commit-id", "--name-status", "-r", "--root", commit], execFileImpl)
|
|
131
212
|
);
|
|
132
213
|
numstat = parseNumstat(
|
|
133
|
-
await git(cwd, ["diff-tree", "--no-commit-id", "--numstat", "-r", commit], execFileImpl)
|
|
214
|
+
await git(cwd, ["diff-tree", "--no-commit-id", "--numstat", "-r", "--root", commit], execFileImpl)
|
|
134
215
|
);
|
|
135
216
|
diffBytes = Buffer.byteLength(await git(cwd, ["show", "--format=", "--patch", commit], execFileImpl), "utf8");
|
|
136
217
|
}
|
|
@@ -149,6 +230,7 @@ export async function resolveReviewSnapshot({
|
|
|
149
230
|
}
|
|
150
231
|
|
|
151
232
|
let hash;
|
|
233
|
+
let modeBits = null;
|
|
152
234
|
let changedLines = numstat.get(path) ?? 0;
|
|
153
235
|
let bytes = 0;
|
|
154
236
|
const deleted = /D/.test(entry.status);
|
|
@@ -168,14 +250,37 @@ export async function resolveReviewSnapshot({
|
|
|
168
250
|
bytes = buffer.length;
|
|
169
251
|
if (!numstat.has(path)) changedLines = buffer.toString("utf8").split(/\r?\n/).length;
|
|
170
252
|
if (entry.status === "??") diffBytes += bytes;
|
|
253
|
+
} else if (mode === REVIEW_SCOPE_MODES.STAGED) {
|
|
254
|
+
const identity = rawIdentity.get(path);
|
|
255
|
+
if (!identity) {
|
|
256
|
+
throw new ReviewSnapshotError(
|
|
257
|
+
`Missing staged raw identity for "${path}".`,
|
|
258
|
+
{
|
|
259
|
+
code: REVIEW_SNAPSHOT_ERROR_CODES.TRUNCATED_OBJECT_ID,
|
|
260
|
+
details: { path }
|
|
261
|
+
}
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
const blobHash = deleted ? identity.oldHash : identity.newHash;
|
|
265
|
+
modeBits = deleted ? identity.oldMode : identity.newMode;
|
|
266
|
+
if (modeBits === "000000") modeBits = identity.oldMode;
|
|
267
|
+
const fullHash = assertFullObjectId(blobHash, stagedObjectIdLength, { path });
|
|
268
|
+
if (await blobIsBinary(cwd, fullHash, execFileImpl)) {
|
|
269
|
+
excluded.push({ path, reason: "binary" });
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
hash = fullHash;
|
|
171
273
|
} else {
|
|
172
|
-
|
|
274
|
+
const blobRef = mode === REVIEW_SCOPE_MODES.COMMIT ? `${commit}:${path}` : `HEAD:${path}`;
|
|
275
|
+
try { hash = (await git(cwd, ["rev-parse", blobRef], execFileImpl)).trim(); }
|
|
173
276
|
catch { hash = createHash("sha256").update(`${entry.status}:${path}`).digest("hex"); }
|
|
174
277
|
}
|
|
175
278
|
|
|
176
|
-
|
|
279
|
+
const file = {
|
|
177
280
|
path, sourcePath, status: entry.status.trim(), hash, changedLines, bytes
|
|
178
|
-
}
|
|
281
|
+
};
|
|
282
|
+
if (modeBits != null) file.mode = modeBits;
|
|
283
|
+
files.push(file);
|
|
179
284
|
}
|
|
180
285
|
|
|
181
286
|
requirePrivateConsent({ includePrivate, privateConfirmed, privatePaths: privateCandidates });
|
|
@@ -185,7 +290,8 @@ export async function resolveReviewSnapshot({
|
|
|
185
290
|
assertWithinReviewLimits({ fileCount: files.length, changedLines, diffBytes });
|
|
186
291
|
|
|
187
292
|
const snapshot = {
|
|
188
|
-
version:
|
|
293
|
+
version: mode === REVIEW_SCOPE_MODES.STAGED ? 2 : 1,
|
|
294
|
+
mode, cwd, headSha, base: base ?? null, commit: commit ?? null,
|
|
189
295
|
files, excluded, totals: { fileCount: files.length, changedLines, diffBytes }, fingerprint: null
|
|
190
296
|
};
|
|
191
297
|
snapshot.fingerprint = canonicalFingerprint(fingerprintPayload(snapshot));
|
|
@@ -199,6 +305,7 @@ export function fingerprintReviewSnapshot(snapshot) {
|
|
|
199
305
|
export async function detectReviewSnapshotDrift(previous, options = {}) {
|
|
200
306
|
const next = await resolveReviewSnapshot({
|
|
201
307
|
cwd: previous.cwd, base: previous.base, commit: previous.commit,
|
|
308
|
+
staged: previous.mode === REVIEW_SCOPE_MODES.STAGED,
|
|
202
309
|
includePrivate: options.includePrivate ?? false,
|
|
203
310
|
privateConfirmed: options.privateConfirmed ?? false,
|
|
204
311
|
execFileImpl: options.execFileImpl
|
|
@@ -210,3 +317,27 @@ export async function detectReviewSnapshotDrift(previous, options = {}) {
|
|
|
210
317
|
next
|
|
211
318
|
};
|
|
212
319
|
}
|
|
320
|
+
|
|
321
|
+
/** Fail-closed staged candidate check against a receipt snapshot. */
|
|
322
|
+
export async function verifyStagedReviewReceipt(receipt, {
|
|
323
|
+
cwd = null, includePrivate = false, privateConfirmed = false, execFileImpl = defaultExecFile
|
|
324
|
+
} = {}) {
|
|
325
|
+
const snapshot = receipt?.snapshot;
|
|
326
|
+
if (!snapshot || snapshot.mode !== REVIEW_SCOPE_MODES.STAGED) {
|
|
327
|
+
throw new ReviewSnapshotError("Staged verification requires a staged review receipt.", {
|
|
328
|
+
code: REVIEW_SNAPSHOT_ERROR_CODES.INVALID_SCOPE,
|
|
329
|
+
details: { mode: snapshot?.mode ?? null }
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
const drift = await detectReviewSnapshotDrift(
|
|
333
|
+
{ ...snapshot, cwd: cwd ?? snapshot.cwd },
|
|
334
|
+
{ includePrivate, privateConfirmed, execFileImpl }
|
|
335
|
+
);
|
|
336
|
+
return {
|
|
337
|
+
ok: !drift.stale,
|
|
338
|
+
stale: drift.stale,
|
|
339
|
+
previousFingerprint: drift.previousFingerprint,
|
|
340
|
+
nextFingerprint: drift.nextFingerprint,
|
|
341
|
+
headSha: drift.next.headSha
|
|
342
|
+
};
|
|
343
|
+
}
|
|
@@ -101,6 +101,8 @@ export async function buildScopedReviewPatch(snapshot, { execFileImpl = defaultE
|
|
|
101
101
|
raw = await gitDiff(
|
|
102
102
|
cwd, ["show", "--format=", "--patch", snapshot.commit, "--", ...admitted], execFileImpl
|
|
103
103
|
);
|
|
104
|
+
} else if (snapshot.mode === REVIEW_SCOPE_MODES.STAGED) {
|
|
105
|
+
raw = await gitDiff(cwd, ["diff", "--cached", "--", ...admitted], execFileImpl);
|
|
104
106
|
} else {
|
|
105
107
|
raw = [
|
|
106
108
|
await gitDiff(cwd, ["diff", "--", ...admitted], execFileImpl),
|