@primitive.ai/prim 0.1.0-alpha.66 → 0.1.0-alpha.67

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.
@@ -422,10 +422,12 @@ function refreshToken(options = {}) {
422
422
  }
423
423
  var HttpError = class extends Error {
424
424
  status;
425
- constructor(status, message) {
425
+ body;
426
+ constructor(status, message, body) {
426
427
  super(message);
427
428
  this.name = "HttpError";
428
429
  this.status = status;
430
+ this.body = body;
429
431
  }
430
432
  };
431
433
  var _cachedCredential;
@@ -486,7 +488,9 @@ async function request(method, path, body, options) {
486
488
  throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
487
489
  }
488
490
  const errorBody = await response.json().catch(() => null);
489
- throw new HttpError(response.status, errorBody?.error ?? `HTTP ${response.status}`);
491
+ const errorRecord = typeof errorBody === "object" && errorBody !== null && !Array.isArray(errorBody) ? errorBody : void 0;
492
+ const message = typeof errorRecord?.error === "string" ? errorRecord.error : `HTTP ${response.status}`;
493
+ throw new HttpError(response.status, message, errorBody);
490
494
  }
491
495
  return response.json();
492
496
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getClient
3
- } from "./chunk-CPX3BUOC.js";
3
+ } from "./chunk-RTLFE6EZ.js";
4
4
 
5
5
  // src/decisions/feedback.ts
6
6
  var FEEDBACK_PROTOCOL_VERSION = 1;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getSiteUrl
3
- } from "./chunk-CPX3BUOC.js";
3
+ } from "./chunk-RTLFE6EZ.js";
4
4
 
5
5
  // src/journal.ts
6
6
  import {
@@ -9,7 +9,7 @@ import {
9
9
  HttpError,
10
10
  getClient,
11
11
  withFileLock
12
- } from "./chunk-CPX3BUOC.js";
12
+ } from "./chunk-RTLFE6EZ.js";
13
13
 
14
14
  // src/commands/skill.ts
15
15
  import { randomUUID } from "crypto";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getClient,
3
3
  getSiteUrl
4
- } from "./chunk-CPX3BUOC.js";
4
+ } from "./chunk-RTLFE6EZ.js";
5
5
  import {
6
6
  daemonRequest
7
7
  } from "./chunk-UTKQTZHL.js";
@@ -394,10 +394,12 @@ function refreshToken(options = {}) {
394
394
  }
395
395
  var HttpError = class extends Error {
396
396
  status;
397
- constructor(status, message) {
397
+ body;
398
+ constructor(status, message, body) {
398
399
  super(message);
399
400
  this.name = "HttpError";
400
401
  this.status = status;
402
+ this.body = body;
401
403
  }
402
404
  };
403
405
  var _cachedCredential;
@@ -458,7 +460,9 @@ async function request(method, path, body, options) {
458
460
  throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
459
461
  }
460
462
  const errorBody = await response.json().catch(() => null);
461
- throw new HttpError(response.status, errorBody?.error ?? `HTTP ${response.status}`);
463
+ const errorRecord = typeof errorBody === "object" && errorBody !== null && !Array.isArray(errorBody) ? errorBody : void 0;
464
+ const message = typeof errorRecord?.error === "string" ? errorRecord.error : `HTTP ${response.status}`;
465
+ throw new HttpError(response.status, message, errorBody);
462
466
  }
463
467
  return response.json();
464
468
  }
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  appendMove,
8
8
  resolveOrg
9
- } from "../chunk-HIMWM526.js";
9
+ } from "../chunk-UN5O5SYG.js";
10
10
  import {
11
11
  getOrCreateWorkspaceId
12
12
  } from "../chunk-UVIWZWM6.js";
@@ -16,7 +16,7 @@ import {
16
16
  repoSyncId,
17
17
  resolveRepositoryContext
18
18
  } from "../chunk-TDRXHBEX.js";
19
- import "../chunk-CPX3BUOC.js";
19
+ import "../chunk-RTLFE6EZ.js";
20
20
 
21
21
  // src/hooks/post-commit.ts
22
22
  import { execFileSync, spawn } from "child_process";
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  appendMove,
8
8
  resolveOrg
9
- } from "../chunk-HIMWM526.js";
9
+ } from "../chunk-UN5O5SYG.js";
10
10
  import {
11
11
  getOrCreateWorkspaceId
12
12
  } from "../chunk-UVIWZWM6.js";
@@ -16,7 +16,7 @@ import {
16
16
  repoSyncId,
17
17
  resolveRepositoryContext
18
18
  } from "../chunk-TDRXHBEX.js";
19
- import "../chunk-CPX3BUOC.js";
19
+ import "../chunk-RTLFE6EZ.js";
20
20
 
21
21
  // src/hooks/post-rewrite.ts
22
22
  import { execFileSync, spawn } from "child_process";
@@ -16,7 +16,7 @@ import {
16
16
  import {
17
17
  appendMove,
18
18
  resolveOrg
19
- } from "../chunk-HIMWM526.js";
19
+ } from "../chunk-UN5O5SYG.js";
20
20
  import {
21
21
  getOrCreateWorkspaceId
22
22
  } from "../chunk-UVIWZWM6.js";
@@ -33,7 +33,7 @@ import {
33
33
  } from "../chunk-TDRXHBEX.js";
34
34
  import {
35
35
  getClient
36
- } from "../chunk-CPX3BUOC.js";
36
+ } from "../chunk-RTLFE6EZ.js";
37
37
  import {
38
38
  normalizeEnvelope,
39
39
  parseAgent
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  checkAffectedDecisions,
4
4
  formatDecisionsWarning
5
- } from "../chunk-WS3MCNFD.js";
6
- import "../chunk-CPX3BUOC.js";
5
+ } from "../chunk-XUAP572Z.js";
6
+ import "../chunk-RTLFE6EZ.js";
7
7
  import "../chunk-UTKQTZHL.js";
8
8
 
9
9
  // src/hooks/pre-commit.ts
@@ -25,7 +25,7 @@ import {
25
25
  } from "../chunk-TDRXHBEX.js";
26
26
  import {
27
27
  getClient
28
- } from "../chunk-CPX3BUOC.js";
28
+ } from "../chunk-RTLFE6EZ.js";
29
29
  import {
30
30
  normalizeEnvelope,
31
31
  parseAgent
@@ -8,7 +8,7 @@ import {
8
8
  acknowledgeDecisionFeedback,
9
9
  leaseDecisionFeedback,
10
10
  renderFeedback
11
- } from "../chunk-KHZNYEUV.js";
11
+ } from "../chunk-U6L2RU67.js";
12
12
  import {
13
13
  enrichHookPayloadWithFileRefs,
14
14
  preserveHookFileMetadata
@@ -21,7 +21,7 @@ import {
21
21
  import {
22
22
  appendMove,
23
23
  resolveOrg
24
- } from "../chunk-HIMWM526.js";
24
+ } from "../chunk-UN5O5SYG.js";
25
25
  import {
26
26
  getOrCreateWorkspaceId
27
27
  } from "../chunk-UVIWZWM6.js";
@@ -36,7 +36,7 @@ import {
36
36
  repoSyncId,
37
37
  resolveRepositoryContext
38
38
  } from "../chunk-TDRXHBEX.js";
39
- import "../chunk-CPX3BUOC.js";
39
+ import "../chunk-RTLFE6EZ.js";
40
40
  import {
41
41
  normalizeEnvelope,
42
42
  parseAgent
@@ -5,7 +5,7 @@ import {
5
5
  ensureEffectivePostRewriteHook,
6
6
  hasUsableCodexGuidance,
7
7
  refreshClaudePlugins
8
- } from "../chunk-DR66XM5L.js";
8
+ } from "../chunk-XPVNH3J6.js";
9
9
  import {
10
10
  buildHookOutput,
11
11
  handoffHookOutput
@@ -15,7 +15,7 @@ import {
15
15
  acknowledgeDecisionFeedback,
16
16
  leaseDecisionFeedback,
17
17
  renderFeedback
18
- } from "../chunk-KHZNYEUV.js";
18
+ } from "../chunk-U6L2RU67.js";
19
19
  import {
20
20
  getOrCreateWorkspaceId
21
21
  } from "../chunk-UVIWZWM6.js";
@@ -32,7 +32,7 @@ import {
32
32
  import {
33
33
  getSiteUrl,
34
34
  isSessionEnded
35
- } from "../chunk-CPX3BUOC.js";
35
+ } from "../chunk-RTLFE6EZ.js";
36
36
  import {
37
37
  daemonRequest
38
38
  } from "../chunk-UTKQTZHL.js";
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  uninstallPostRewriteHookAtPath,
18
18
  uninstallProjectPostCommitHook,
19
19
  uninstallProjectPostRewriteHook
20
- } from "./chunk-DR66XM5L.js";
20
+ } from "./chunk-XPVNH3J6.js";
21
21
  import {
22
22
  bold,
23
23
  boundedHealthError,
@@ -32,10 +32,10 @@ import {
32
32
  checkAffectedDecisions,
33
33
  daemonOrDirectGet,
34
34
  formatDecisionsWarning
35
- } from "./chunk-WS3MCNFD.js";
35
+ } from "./chunk-XUAP572Z.js";
36
36
  import {
37
37
  fetchFeedbackCapability
38
- } from "./chunk-KHZNYEUV.js";
38
+ } from "./chunk-U6L2RU67.js";
39
39
  import {
40
40
  JOURNAL_DIR,
41
41
  SESSIONS_DIR,
@@ -44,7 +44,7 @@ import {
44
44
  listFlushing,
45
45
  pendingJournalStats,
46
46
  readMovesFromPath
47
- } from "./chunk-HIMWM526.js";
47
+ } from "./chunk-UN5O5SYG.js";
48
48
  import {
49
49
  inspectWorkspaceId
50
50
  } from "./chunk-UVIWZWM6.js";
@@ -80,7 +80,7 @@ import {
80
80
  resolveAuthCredential,
81
81
  setStoredToken,
82
82
  withFileLock
83
- } from "./chunk-CPX3BUOC.js";
83
+ } from "./chunk-RTLFE6EZ.js";
84
84
  import {
85
85
  daemonIsLive,
86
86
  daemonRequest
@@ -3394,6 +3394,537 @@ function formatRelateJson(result) {
3394
3394
  return JSON.stringify(result.outcome, null, 2);
3395
3395
  }
3396
3396
 
3397
+ // src/decisions/repairs.ts
3398
+ var REPAIRS_TIMEOUT_MS = 1e4;
3399
+ var MAX_REPAIR_LIST_PAGES = 100;
3400
+ var MAX_REPAIR_DECISIONS_FOR_CONFIRM = 25;
3401
+ var MAX_REPAIRS_PER_PAGE = 10;
3402
+ var MAX_CURSOR_LENGTH = 4096;
3403
+ var MAX_INTENT_LENGTH = 1e3;
3404
+ var MAX_SIGNALS = 8;
3405
+ var MAX_SIGNAL_LENGTH = 160;
3406
+ var SHA_RE = /^[0-9a-f]{40}$/u;
3407
+ var SHA_INPUT_RE = /^[0-9a-f]{40}$/iu;
3408
+ var REVIEW_TOKEN_RE = /^[0-9a-f]{64}$/u;
3409
+ var REVIEW_TOKEN_INPUT_RE = /^[0-9a-f]{64}$/iu;
3410
+ var REPO_FULL_NAME_RE = /^[a-z0-9_.-]+\/[a-z0-9_.-]+$/u;
3411
+ var REVIEWED_CONFIRM_WIRE_ACTION = "confirm_reviewed_v2";
3412
+ var LEGACY_REPAIR_BODY_ERROR = "Body requires id, action (confirm or reject), and expectedProposedSha";
3413
+ var LEGACY_CONFIRM_PROTOCOL_ERROR = "Reviewed confirmation protocol required; legacy action confirm is not supported";
3414
+ var defaultDeps6 = { getClient };
3415
+ var RepairProposalNotFoundError = class extends Error {
3416
+ constructor(proposalId) {
3417
+ super(`Commit repair proposal not found: ${oneLine(proposalId)}`);
3418
+ this.name = "RepairProposalNotFoundError";
3419
+ }
3420
+ };
3421
+ var RepairAuthorizationError = class extends Error {
3422
+ constructor() {
3423
+ super(
3424
+ "Not authorized to review commit repairs; active organization membership and current repository authorization are required"
3425
+ );
3426
+ this.name = "RepairAuthorizationError";
3427
+ }
3428
+ };
3429
+ var RepairListContractError = class extends Error {
3430
+ constructor(detail) {
3431
+ super(`Commit repair response violated the reviewed CLI contract: ${oneLine(detail)}`);
3432
+ this.name = "RepairListContractError";
3433
+ }
3434
+ };
3435
+ var RepairEndpointVersionError = class extends Error {
3436
+ constructor() {
3437
+ super(
3438
+ "The selected server must be upgraded for the reviewed commit-repair contract; nothing was confirmed or queued (client/server version skew)"
3439
+ );
3440
+ this.name = "RepairEndpointVersionError";
3441
+ }
3442
+ };
3443
+ var RepairResolutionInputError = class extends Error {
3444
+ constructor(detail) {
3445
+ super(`Invalid commit repair resolution: ${oneLine(detail)}`);
3446
+ this.name = "RepairResolutionInputError";
3447
+ }
3448
+ };
3449
+ function contractError(path, detail) {
3450
+ throw new RepairListContractError(`${path} ${detail}`);
3451
+ }
3452
+ function asRecord(value, path) {
3453
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
3454
+ contractError(path, "must be an object");
3455
+ }
3456
+ return value;
3457
+ }
3458
+ function assertKeys(record, path, required, optional = []) {
3459
+ const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
3460
+ const missing = required.find((key) => !Object.hasOwn(record, key));
3461
+ if (missing !== void 0) contractError(path, `is missing ${missing}`);
3462
+ const extra = Object.keys(record).find((key) => !allowed.has(key));
3463
+ if (extra !== void 0) contractError(path, `contains unexpected field ${extra}`);
3464
+ }
3465
+ function asString(value, path, options) {
3466
+ if (typeof value !== "string") contractError(path, "must be a string");
3467
+ if (options.nonempty && value.length === 0) contractError(path, "must not be empty");
3468
+ if (value.length > options.max) contractError(path, "exceeds its length bound");
3469
+ return value;
3470
+ }
3471
+ function asTimestamp(value, path) {
3472
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
3473
+ contractError(path, "must be a non-negative safe integer timestamp");
3474
+ }
3475
+ return value;
3476
+ }
3477
+ function asCount(value, path) {
3478
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
3479
+ contractError(path, "must be a non-negative safe integer");
3480
+ }
3481
+ return value;
3482
+ }
3483
+ function asBoolean(value, path) {
3484
+ if (typeof value !== "boolean") contractError(path, "must be a boolean");
3485
+ return value;
3486
+ }
3487
+ function asSha(value, path) {
3488
+ const sha = asString(value, path, { max: 40, nonempty: true });
3489
+ if (!SHA_RE.test(sha)) contractError(path, "must be a canonical lowercase Git commit SHA");
3490
+ return sha;
3491
+ }
3492
+ function optionalString(record, key, path, max) {
3493
+ if (!Object.hasOwn(record, key)) return void 0;
3494
+ return asString(record[key], `${path}.${key}`, { max, nonempty: true });
3495
+ }
3496
+ function parseProposal(value, path) {
3497
+ const record = asRecord(value, path);
3498
+ assertKeys(
3499
+ record,
3500
+ path,
3501
+ ["_id", "repoFullName", "deadCommitSha", "status", "signals", "lastEvaluatedAt"],
3502
+ ["proposedSha", "proposedTier", "nextEvaluateAt"]
3503
+ );
3504
+ const repoFullName = asString(record.repoFullName, `${path}.repoFullName`, {
3505
+ max: 200,
3506
+ nonempty: true
3507
+ });
3508
+ if (!REPO_FULL_NAME_RE.test(repoFullName)) {
3509
+ contractError(`${path}.repoFullName`, "must be canonical owner/repository syntax");
3510
+ }
3511
+ const status = record.status;
3512
+ if (status !== "proposed" && status !== "confirmed" && status !== "verification_failed") {
3513
+ contractError(`${path}.status`, "is not a review-visible proposal status");
3514
+ }
3515
+ if (!Array.isArray(record.signals) || record.signals.length > MAX_SIGNALS) {
3516
+ contractError(`${path}.signals`, "must be a bounded array");
3517
+ }
3518
+ const signals = record.signals.map(
3519
+ (signal, index) => asString(signal, `${path}.signals[${String(index)}]`, {
3520
+ max: MAX_SIGNAL_LENGTH,
3521
+ nonempty: true
3522
+ })
3523
+ );
3524
+ const proposedSha = Object.hasOwn(record, "proposedSha") ? asSha(record.proposedSha, `${path}.proposedSha`) : void 0;
3525
+ let proposedTier;
3526
+ if (Object.hasOwn(record, "proposedTier")) {
3527
+ if (record.proposedTier !== "exact_patch_body" && record.proposedTier !== "exact_file_set") {
3528
+ contractError(`${path}.proposedTier`, "is not a supported match tier");
3529
+ }
3530
+ proposedTier = record.proposedTier;
3531
+ }
3532
+ const nextEvaluateAt = Object.hasOwn(record, "nextEvaluateAt") ? asTimestamp(record.nextEvaluateAt, `${path}.nextEvaluateAt`) : void 0;
3533
+ return {
3534
+ _id: asString(record._id, `${path}._id`, { max: 256, nonempty: true }),
3535
+ repoFullName,
3536
+ deadCommitSha: asSha(record.deadCommitSha, `${path}.deadCommitSha`),
3537
+ status,
3538
+ proposedSha,
3539
+ proposedTier,
3540
+ signals,
3541
+ lastEvaluatedAt: asTimestamp(record.lastEvaluatedAt, `${path}.lastEvaluatedAt`),
3542
+ nextEvaluateAt
3543
+ };
3544
+ }
3545
+ function parseDecision(value, path) {
3546
+ const record = asRecord(value, path);
3547
+ assertKeys(record, path, ["_id", "intent", "status", "intentTruncated"], ["shortId"]);
3548
+ const status = record.status;
3549
+ if (status !== "provisional" && status !== "abandoned") {
3550
+ contractError(`${path}.status`, "is not a repairable decision status");
3551
+ }
3552
+ return {
3553
+ _id: asString(record._id, `${path}._id`, { max: 256, nonempty: true }),
3554
+ shortId: optionalString(record, "shortId", path, 128),
3555
+ intent: asString(record.intent, `${path}.intent`, { max: MAX_INTENT_LENGTH }),
3556
+ status,
3557
+ intentTruncated: asBoolean(record.intentTruncated, `${path}.intentTruncated`)
3558
+ };
3559
+ }
3560
+ function parseListItem(value, path) {
3561
+ const record = asRecord(value, path);
3562
+ assertKeys(record, path, [
3563
+ "proposal",
3564
+ "decisions",
3565
+ "affectedDecisionCount",
3566
+ "decisionsTruncated",
3567
+ "reviewToken"
3568
+ ]);
3569
+ if (!Array.isArray(record.decisions) || record.decisions.length > MAX_REPAIR_DECISIONS_FOR_CONFIRM) {
3570
+ contractError(`${path}.decisions`, "must be a bounded array");
3571
+ }
3572
+ const decisions = record.decisions.map(
3573
+ (decision, index) => parseDecision(decision, `${path}.decisions[${String(index)}]`)
3574
+ );
3575
+ const proposal = parseProposal(record.proposal, `${path}.proposal`);
3576
+ const decisionsTruncated = asBoolean(record.decisionsTruncated, `${path}.decisionsTruncated`);
3577
+ const affectedDecisionCount = record.affectedDecisionCount === null ? null : asCount(record.affectedDecisionCount, `${path}.affectedDecisionCount`);
3578
+ let reviewToken;
3579
+ if (record.reviewToken === null) {
3580
+ reviewToken = null;
3581
+ } else {
3582
+ reviewToken = asString(record.reviewToken, `${path}.reviewToken`, {
3583
+ max: 64,
3584
+ nonempty: true
3585
+ });
3586
+ if (!REVIEW_TOKEN_RE.test(reviewToken)) {
3587
+ contractError(`${path}.reviewToken`, "must be a canonical lowercase SHA-256 token");
3588
+ }
3589
+ }
3590
+ if (!decisionsTruncated && affectedDecisionCount === null) {
3591
+ contractError(path, "cannot claim a complete decision list with an unknown count");
3592
+ }
3593
+ if (!decisionsTruncated && affectedDecisionCount !== decisions.length) {
3594
+ contractError(path, "has a complete decision count that does not match its projection");
3595
+ }
3596
+ if (decisionsTruncated && affectedDecisionCount !== null && affectedDecisionCount <= decisions.length) {
3597
+ contractError(path, "claims truncation without a larger affected-decision count");
3598
+ }
3599
+ if (decisionsTruncated && reviewToken !== null) {
3600
+ contractError(path, "must not expose a review token for a truncated decision set");
3601
+ }
3602
+ if (reviewToken !== null && (proposal.proposedSha === void 0 || proposal.proposedTier === void 0 || proposal.proposedSha === proposal.deadCommitSha)) {
3603
+ contractError(path, "must not expose a review token without a valid replacement candidate");
3604
+ }
3605
+ return {
3606
+ proposal,
3607
+ decisions,
3608
+ affectedDecisionCount,
3609
+ decisionsTruncated,
3610
+ reviewToken
3611
+ };
3612
+ }
3613
+ function parseListPage(value, pageNumber) {
3614
+ const path = `page ${String(pageNumber)}`;
3615
+ const record = asRecord(value, path);
3616
+ assertKeys(record, path, ["repairs", "isDone", "nextCursor", "truncated"]);
3617
+ if (!Array.isArray(record.repairs) || record.repairs.length > MAX_REPAIRS_PER_PAGE) {
3618
+ contractError(`${path}.repairs`, "must be a bounded array");
3619
+ }
3620
+ const isDone = asBoolean(record.isDone, `${path}.isDone`);
3621
+ const truncated = asBoolean(record.truncated, `${path}.truncated`);
3622
+ if (truncated === isDone) contractError(path, "has inconsistent isDone/truncated flags");
3623
+ let nextCursor;
3624
+ if (record.nextCursor === null) {
3625
+ nextCursor = null;
3626
+ } else {
3627
+ nextCursor = asString(record.nextCursor, `${path}.nextCursor`, {
3628
+ max: MAX_CURSOR_LENGTH,
3629
+ nonempty: true
3630
+ });
3631
+ }
3632
+ if (isDone && nextCursor !== null) contractError(path, "is done but includes a next cursor");
3633
+ if (!isDone && nextCursor === null) contractError(path, "is incomplete without a next cursor");
3634
+ return {
3635
+ repairs: record.repairs.map(
3636
+ (item, index) => parseListItem(item, `${path}.repairs[${String(index)}]`)
3637
+ ),
3638
+ isDone,
3639
+ nextCursor,
3640
+ truncated
3641
+ };
3642
+ }
3643
+ function effectivePageLimit(deps) {
3644
+ if (deps.maxPages === void 0) return MAX_REPAIR_LIST_PAGES;
3645
+ if (!Number.isSafeInteger(deps.maxPages) || deps.maxPages < 1) {
3646
+ throw new RepairListContractError("configured page limit must be a positive safe integer");
3647
+ }
3648
+ return Math.min(deps.maxPages, MAX_REPAIR_LIST_PAGES);
3649
+ }
3650
+ function listPath(cursor) {
3651
+ if (cursor === null) return "/api/cli/decisions/repairs";
3652
+ const search = new URLSearchParams({ cursor });
3653
+ return `/api/cli/decisions/repairs?${search.toString()}`;
3654
+ }
3655
+ async function fetchRepairsWithClient(client, deps) {
3656
+ const repairs = [];
3657
+ const seenCursors = /* @__PURE__ */ new Set();
3658
+ const seenProposalIds = /* @__PURE__ */ new Set();
3659
+ const pageLimit = effectivePageLimit(deps);
3660
+ let cursor = null;
3661
+ for (let pageNumber = 1; pageNumber <= pageLimit; pageNumber += 1) {
3662
+ let rawPage;
3663
+ try {
3664
+ rawPage = await client.get(listPath(cursor), {
3665
+ signal: AbortSignal.timeout(REPAIRS_TIMEOUT_MS)
3666
+ });
3667
+ } catch (error) {
3668
+ if (error instanceof HttpError && error.status === 403) {
3669
+ throw new RepairAuthorizationError();
3670
+ }
3671
+ if (error instanceof HttpError && error.status === 404) {
3672
+ throw new RepairEndpointVersionError();
3673
+ }
3674
+ throw error;
3675
+ }
3676
+ const page = parseListPage(rawPage, pageNumber);
3677
+ for (const item of page.repairs) {
3678
+ if (seenProposalIds.has(item.proposal._id)) {
3679
+ contractError(`page ${String(pageNumber)}`, "repeats a proposal from an earlier page");
3680
+ }
3681
+ seenProposalIds.add(item.proposal._id);
3682
+ repairs.push(item);
3683
+ }
3684
+ if (page.isDone) {
3685
+ return { repairs, isDone: true, nextCursor: null, truncated: false };
3686
+ }
3687
+ const nextCursor = page.nextCursor;
3688
+ if (nextCursor === null)
3689
+ contractError(`page ${String(pageNumber)}`, "has no continuation cursor");
3690
+ if (nextCursor === cursor || seenCursors.has(nextCursor)) {
3691
+ contractError(`page ${String(pageNumber)}`, "has a repeated or non-advancing cursor");
3692
+ }
3693
+ seenCursors.add(nextCursor);
3694
+ cursor = nextCursor;
3695
+ }
3696
+ throw new RepairListContractError(
3697
+ `pagination did not finish within ${String(pageLimit)} page(s); no partial result was emitted`
3698
+ );
3699
+ }
3700
+ async function fetchRepairs(deps = defaultDeps6) {
3701
+ return fetchRepairsWithClient(deps.getClient(), deps);
3702
+ }
3703
+ function normalizedResolutionInput(proposalId, expectedProposedSha) {
3704
+ if (proposalId.length === 0 || proposalId.length > 256) {
3705
+ throw new RepairResolutionInputError("proposal ID must be a non-empty bounded string");
3706
+ }
3707
+ if (!SHA_INPUT_RE.test(expectedProposedSha)) {
3708
+ throw new RepairResolutionInputError("expected proposed SHA must be a 40-character Git SHA");
3709
+ }
3710
+ return { proposalId, expectedProposedSha: expectedProposedSha.toLowerCase() };
3711
+ }
3712
+ function parseResolutionOutcome(value, action) {
3713
+ const record = asRecord(value, "resolution response");
3714
+ const allowed = action === "confirm" ? ["confirmed", "already_applied", "disabled", "no_op", "stale"] : ["rejected", "already_applied", "no_op", "stale"];
3715
+ if (record.status === "backoff") {
3716
+ assertKeys(record, "resolution response", ["status", "retryAt"]);
3717
+ if (action !== "confirm") {
3718
+ contractError("resolution response.status", "backoff is impossible for reject");
3719
+ }
3720
+ return {
3721
+ status: "backoff",
3722
+ retryAt: asTimestamp(record.retryAt, "resolution response.retryAt")
3723
+ };
3724
+ }
3725
+ assertKeys(record, "resolution response", ["status"]);
3726
+ if (typeof record.status !== "string" || !allowed.includes(record.status)) {
3727
+ contractError("resolution response.status", "is not a supported outcome");
3728
+ }
3729
+ return { status: record.status };
3730
+ }
3731
+ function normalizedReviewToken(value) {
3732
+ if (value === void 0 || !REVIEW_TOKEN_INPUT_RE.test(value)) {
3733
+ throw new RepairResolutionInputError(
3734
+ "confirm requires --review-token with the 64-character token from the reviewed list"
3735
+ );
3736
+ }
3737
+ return value.toLowerCase();
3738
+ }
3739
+ function parseConflictOutcome(value, action) {
3740
+ const record = asRecord(value, "resolution conflict response");
3741
+ assertKeys(record, "resolution conflict response", ["status"]);
3742
+ if (action !== "confirm") {
3743
+ contractError(
3744
+ "resolution conflict response.status",
3745
+ "a review conflict is impossible for reject"
3746
+ );
3747
+ }
3748
+ if (record.status !== "review_too_large" && record.status !== "stale_review") {
3749
+ contractError("resolution conflict response.status", "is not a supported 409 outcome");
3750
+ }
3751
+ return { status: record.status };
3752
+ }
3753
+ async function resolveRepair(proposalId, expectedProposedSha, action, expectedReviewToken, deps = defaultDeps6) {
3754
+ const normalized = normalizedResolutionInput(proposalId, expectedProposedSha);
3755
+ const reviewToken = action === "confirm" ? normalizedReviewToken(expectedReviewToken) : void 0;
3756
+ if (action === "reject" && expectedReviewToken !== void 0) {
3757
+ throw new RepairResolutionInputError("--review-token is valid only with confirm");
3758
+ }
3759
+ const client = deps.getClient();
3760
+ const wireAction = action === "confirm" ? REVIEWED_CONFIRM_WIRE_ACTION : "reject";
3761
+ try {
3762
+ const rawOutcome = await client.post(
3763
+ "/api/cli/decisions/repairs/resolve",
3764
+ {
3765
+ id: normalized.proposalId,
3766
+ action: wireAction,
3767
+ expectedProposedSha: normalized.expectedProposedSha,
3768
+ ...reviewToken === void 0 ? {} : { expectedReviewToken: reviewToken }
3769
+ },
3770
+ { signal: AbortSignal.timeout(REPAIRS_TIMEOUT_MS) }
3771
+ );
3772
+ return {
3773
+ proposalId: normalized.proposalId,
3774
+ action,
3775
+ outcome: parseResolutionOutcome(rawOutcome, action)
3776
+ };
3777
+ } catch (error) {
3778
+ if (error instanceof HttpError && error.status === 404 && error.message === "Repair proposal not found") {
3779
+ throw new RepairProposalNotFoundError(normalized.proposalId);
3780
+ }
3781
+ if (error instanceof HttpError && error.status === 404) {
3782
+ throw new RepairEndpointVersionError();
3783
+ }
3784
+ if (error instanceof HttpError && error.status === 403) {
3785
+ throw new RepairAuthorizationError();
3786
+ }
3787
+ if (action === "confirm" && error instanceof HttpError && error.status === 400 && (error.message === LEGACY_REPAIR_BODY_ERROR || error.message === LEGACY_CONFIRM_PROTOCOL_ERROR)) {
3788
+ throw new RepairEndpointVersionError();
3789
+ }
3790
+ if (error instanceof HttpError && error.status === 409) {
3791
+ return {
3792
+ proposalId: normalized.proposalId,
3793
+ action,
3794
+ outcome: parseConflictOutcome(error.body, action)
3795
+ };
3796
+ }
3797
+ throw error;
3798
+ }
3799
+ }
3800
+ function shortSha(sha) {
3801
+ return sha === void 0 ? "(missing)" : sha.slice(0, 12);
3802
+ }
3803
+ function tierLabel(tier) {
3804
+ if (tier === "exact_patch_body") return "exact patch body";
3805
+ if (tier === "exact_file_set") return "exact file set";
3806
+ return "match tier missing";
3807
+ }
3808
+ function oneLine(value) {
3809
+ return stripControlChars(value.replace(/\s+/gu, " ")).trim();
3810
+ }
3811
+ function timestampLabel(value) {
3812
+ const timestamp = new Date(value);
3813
+ return Number.isNaN(timestamp.getTime()) ? String(value) : `${timestamp.toISOString()} (${String(value)})`;
3814
+ }
3815
+ function reviewLabel(item) {
3816
+ if (item.affectedDecisionCount === null) {
3817
+ return `showing ${String(item.decisions.length)}; total unknown; confirmation disabled`;
3818
+ }
3819
+ if (item.decisionsTruncated) {
3820
+ return `showing ${String(item.decisions.length)} of ${String(item.affectedDecisionCount)}; confirmation disabled`;
3821
+ }
3822
+ if (item.affectedDecisionCount > MAX_REPAIR_DECISIONS_FOR_CONFIRM) {
3823
+ return `${String(item.affectedDecisionCount)} affected; exceeds CLI review limit ${String(MAX_REPAIR_DECISIONS_FOR_CONFIRM)}; confirmation disabled`;
3824
+ }
3825
+ if (item.affectedDecisionCount === 0) {
3826
+ return "0 affected; confirmation disabled";
3827
+ }
3828
+ return `${String(item.affectedDecisionCount)} affected; complete`;
3829
+ }
3830
+ function canRenderConfirm(item) {
3831
+ return item.proposal.proposedSha !== void 0 && !item.decisionsTruncated && item.affectedDecisionCount !== null && item.affectedDecisionCount > 0 && item.affectedDecisionCount <= MAX_REPAIR_DECISIONS_FOR_CONFIRM && item.reviewToken !== null;
3832
+ }
3833
+ function shellArg(value) {
3834
+ if (/^[a-zA-Z0-9_./:@%+=,-]+$/u.test(value)) return value;
3835
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
3836
+ }
3837
+ function proposalReviewLabel(item) {
3838
+ if (item.proposal.status === "confirmed") {
3839
+ return canRenderConfirm(item) ? "already confirmed; a guarded application retry may be requested when eligible" : "already confirmed; a guarded retry is currently unavailable";
3840
+ }
3841
+ if (item.proposal.status === "verification_failed") {
3842
+ return canRenderConfirm(item) ? "prior verification failed; eligible for a fresh confirmation" : "prior verification failed; confirmation is currently unavailable";
3843
+ }
3844
+ return canRenderConfirm(item) ? "awaiting operator review" : "awaiting review; confirmation is currently unavailable";
3845
+ }
3846
+ function formatRepairsHuman(result) {
3847
+ if (result.repairs.length === 0) {
3848
+ return "[prim] decision repairs \xB7 0 review-visible proposals \xB7 complete paginated scan";
3849
+ }
3850
+ const lines = [
3851
+ `[prim] decision repairs \xB7 ${String(result.repairs.length)} review-visible proposal(s) \xB7 complete paginated scan`
3852
+ ];
3853
+ for (const item of result.repairs) {
3854
+ const { proposal, decisions } = item;
3855
+ const renderedProposalId = oneLine(proposal._id);
3856
+ lines.push(
3857
+ ` ${renderedProposalId} [${proposal.status}] \xB7 ${oneLine(proposal.repoFullName)} \xB7 ${shortSha(proposal.deadCommitSha)} -> ${shortSha(proposal.proposedSha)} \xB7 ${tierLabel(proposal.proposedTier)}`,
3858
+ ` review state: ${proposalReviewLabel(item)}`
3859
+ );
3860
+ if (proposal.signals.length > 0) {
3861
+ lines.push(` signals: ${proposal.signals.map(oneLine).join("; ")}`);
3862
+ }
3863
+ if (proposal.nextEvaluateAt !== void 0) {
3864
+ lines.push(` next proposal evaluation: ${timestampLabel(proposal.nextEvaluateAt)}`);
3865
+ }
3866
+ lines.push(` decisions (${reviewLabel(item)}):`);
3867
+ for (const decision of decisions) {
3868
+ const id = renderIdentifier({
3869
+ shortId: decision.shortId === void 0 ? void 0 : oneLine(decision.shortId),
3870
+ id: oneLine(decision._id)
3871
+ });
3872
+ const truncation = decision.intentTruncated ? " [intent truncated]" : "";
3873
+ lines.push(` - ${id} [${decision.status}] ${oneLine(decision.intent)}${truncation}`);
3874
+ }
3875
+ if (canRenderConfirm(item) && proposal.status === "proposed") {
3876
+ lines.push(
3877
+ ` confirm: prim decisions repairs confirm ${shellArg(renderedProposalId)} ${proposal.proposedSha} --review-token ${item.reviewToken}`
3878
+ );
3879
+ }
3880
+ if (canRenderConfirm(item) && proposal.status === "verification_failed") {
3881
+ lines.push(
3882
+ ` retry verification: prim decisions repairs confirm ${shellArg(renderedProposalId)} ${proposal.proposedSha} --review-token ${item.reviewToken}`
3883
+ );
3884
+ }
3885
+ if (canRenderConfirm(item) && proposal.status === "confirmed") {
3886
+ lines.push(
3887
+ ` guarded retry confirmation: prim decisions repairs confirm ${shellArg(renderedProposalId)} ${proposal.proposedSha} --review-token ${item.reviewToken}`
3888
+ );
3889
+ }
3890
+ if (proposal.proposedSha !== void 0) {
3891
+ lines.push(
3892
+ ` reject: prim decisions repairs reject ${shellArg(renderedProposalId)} ${proposal.proposedSha}`
3893
+ );
3894
+ }
3895
+ }
3896
+ return lines.join("\n");
3897
+ }
3898
+ function formatRepairsJson(result) {
3899
+ return JSON.stringify(result, null, 2);
3900
+ }
3901
+ function formatRepairResolutionHuman(result) {
3902
+ const proposalId = oneLine(result.proposalId);
3903
+ switch (result.outcome.status) {
3904
+ case "confirmed":
3905
+ return `[prim] repair ${proposalId} confirmed; fresh landing verification queued. No Decision was changed by this response.`;
3906
+ case "rejected":
3907
+ return `[prim] repair ${proposalId} rejected.`;
3908
+ case "already_applied":
3909
+ return `[prim] repair ${proposalId} was already applied; nothing to change.`;
3910
+ case "backoff":
3911
+ return `[prim] repair ${proposalId} is in application retry backoff; nothing changed. Retry at ${timestampLabel(result.outcome.retryAt)}.`;
3912
+ case "disabled":
3913
+ return `[prim] repair ${proposalId} was not confirmed because repair application is disabled; nothing changed.`;
3914
+ case "review_too_large":
3915
+ return `[prim] repair ${proposalId} was refused because its complete review exceeds the safe bound; nothing changed.`;
3916
+ case "stale_review":
3917
+ return `[prim] repair ${proposalId} changed after review; re-list and review the current decision set before confirming with its new token. Nothing changed.`;
3918
+ case "stale":
3919
+ return `[prim] repair ${proposalId} has a different proposed SHA; refresh the repair list before acting. Nothing changed.`;
3920
+ case "no_op":
3921
+ return `[prim] repair ${proposalId} cannot be ${result.action === "confirm" ? "confirmed" : "rejected"} in its current state; nothing changed.`;
3922
+ }
3923
+ }
3924
+ function formatRepairResolutionJson(result) {
3925
+ return JSON.stringify(result.outcome, null, 2);
3926
+ }
3927
+
3397
3928
  // src/decisions/show.ts
3398
3929
  var NOT_FOUND_RE4 = /not found/i;
3399
3930
  function colorStatus(status) {
@@ -3406,14 +3937,14 @@ function colorStatus(status) {
3406
3937
  return color(status, "gray");
3407
3938
  }
3408
3939
  var SHOW_TIMEOUT_MS = 1e4;
3409
- var defaultDeps6 = { getClient };
3940
+ var defaultDeps7 = { getClient };
3410
3941
  var DecisionNotFoundError = class extends Error {
3411
3942
  constructor(idOrShortId) {
3412
3943
  super(`Decision not found: ${idOrShortId}`);
3413
3944
  this.name = "DecisionNotFoundError";
3414
3945
  }
3415
3946
  };
3416
- async function fetchShow(idOrShortId, deps = defaultDeps6) {
3947
+ async function fetchShow(idOrShortId, deps = defaultDeps7) {
3417
3948
  const params = new URLSearchParams({ id: idOrShortId });
3418
3949
  const client = deps.getClient();
3419
3950
  try {
@@ -3547,7 +4078,33 @@ async function askConfirmation(question, output = process.stdout) {
3547
4078
 
3548
4079
  // src/commands/decisions.ts
3549
4080
  var EXIT_NOT_FOUND = 4;
4081
+ var EXIT_FAILURE = 1;
3550
4082
  var EXIT_USAGE = 2;
4083
+ function handleRepairCommandError(error) {
4084
+ if (error instanceof RepairProposalNotFoundError) {
4085
+ console.error(`[prim] ${error.message}`);
4086
+ process.exitCode = EXIT_NOT_FOUND;
4087
+ return true;
4088
+ }
4089
+ if (error instanceof RepairResolutionInputError) {
4090
+ console.error(`[prim] ${error.message}`);
4091
+ process.exitCode = EXIT_USAGE;
4092
+ return true;
4093
+ }
4094
+ if (error instanceof RepairAuthorizationError || error instanceof HttpError && error.status === 403) {
4095
+ console.error(
4096
+ `[prim] ${error instanceof RepairAuthorizationError ? error.message : "Not authorized to review commit repairs; active organization membership and current repository authorization are required"}`
4097
+ );
4098
+ process.exitCode = EXIT_FAILURE;
4099
+ return true;
4100
+ }
4101
+ if (error instanceof RepairEndpointVersionError || error instanceof RepairListContractError) {
4102
+ console.error(`[prim] ${error.message}`);
4103
+ process.exitCode = EXIT_FAILURE;
4104
+ return true;
4105
+ }
4106
+ return false;
4107
+ }
3551
4108
  var CREATE_INACTIVE_PROMPT = "[prim] Decision ingestion is disabled here. Create this one Decision without enabling passive ingestion?";
3552
4109
  var CREATE_INACTIVE_APPROVED = "[prim] one-time Decision creation approved; passive ingestion remains disabled here";
3553
4110
  var CREATE_INACTIVE_REJECTED = "[prim] decision not created: Decision ingestion is disabled here; rerun with prim's --yes to approve this one Decision, or run `prim enable` in a Git project";
@@ -3626,6 +4183,46 @@ function registerDecisionsCommands(program2) {
3626
4183
  throw err;
3627
4184
  }
3628
4185
  });
4186
+ const repairs = decisions.command("repairs").description("Review human-gated commit rewrite repair proposals").action(runRepairsList);
4187
+ async function runRepairsList() {
4188
+ try {
4189
+ const result = await fetchRepairs();
4190
+ console.error(formatRepairsHuman(result));
4191
+ console.log(formatRepairsJson(result));
4192
+ } catch (error) {
4193
+ if (handleRepairCommandError(error)) return;
4194
+ throw error;
4195
+ }
4196
+ }
4197
+ repairs.command("list").description("List review-visible commit rewrite repair proposals").action(runRepairsList);
4198
+ const registerRepairResolution = (action) => {
4199
+ const resolution = repairs.command(`${action} <proposalId> <proposedSha>`).description(
4200
+ action === "confirm" ? "Confirm the exact reviewed decision set and queue fresh landing verification" : "Reject a proposal and remember its proposed SHA"
4201
+ );
4202
+ if (action === "confirm") {
4203
+ resolution.option(
4204
+ "--review-token <token>",
4205
+ "64-character token printed with the complete reviewed decision set (required)"
4206
+ );
4207
+ }
4208
+ resolution.action(
4209
+ async (proposalId, proposedSha, options) => {
4210
+ try {
4211
+ const result = await resolveRepair(proposalId, proposedSha, action, options.reviewToken);
4212
+ console.error(formatRepairResolutionHuman(result));
4213
+ console.log(formatRepairResolutionJson(result));
4214
+ if (result.outcome.status === "review_too_large" || result.outcome.status === "stale_review") {
4215
+ process.exitCode = EXIT_USAGE;
4216
+ }
4217
+ } catch (error) {
4218
+ if (handleRepairCommandError(error)) return;
4219
+ throw error;
4220
+ }
4221
+ }
4222
+ );
4223
+ };
4224
+ registerRepairResolution("confirm");
4225
+ registerRepairResolution("reject");
3629
4226
  decisions.command("create").description("Record a decision directly with its explicit user or agent origin").requiredOption("--intent <text>", "What was decided (required)").addOption(
3630
4227
  new Option2(
3631
4228
  "--attribution <origin>",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitive.ai/prim",
3
- "version": "0.1.0-alpha.66",
3
+ "version": "0.1.0-alpha.67",
4
4
  "description": "CLI for Primitive's decision graph — passive decision capture, conflict gate, and team presence",
5
5
  "type": "module",
6
6
  "license": "MIT",