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

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/dist/index.js CHANGED
@@ -17,25 +17,18 @@ import {
17
17
  uninstallPostRewriteHookAtPath,
18
18
  uninstallProjectPostCommitHook,
19
19
  uninstallProjectPostRewriteHook
20
- } from "./chunk-DR66XM5L.js";
20
+ } from "./chunk-XPVNH3J6.js";
21
21
  import {
22
- bold,
23
- boundedHealthError,
24
- color,
25
- colorForArea,
26
- dim,
27
- requireDurableIngestAcknowledgement,
28
- stripAnsi,
29
- stripControlChars
30
- } from "./chunk-JQFN6LBO.js";
22
+ requireDurableIngestAcknowledgement
23
+ } from "./chunk-XGADQPCI.js";
31
24
  import {
32
25
  checkAffectedDecisions,
33
26
  daemonOrDirectGet,
34
27
  formatDecisionsWarning
35
- } from "./chunk-WS3MCNFD.js";
28
+ } from "./chunk-XUAP572Z.js";
36
29
  import {
37
30
  fetchFeedbackCapability
38
- } from "./chunk-KHZNYEUV.js";
31
+ } from "./chunk-U6L2RU67.js";
39
32
  import {
40
33
  JOURNAL_DIR,
41
34
  SESSIONS_DIR,
@@ -44,18 +37,27 @@ import {
44
37
  listFlushing,
45
38
  pendingJournalStats,
46
39
  readMovesFromPath
47
- } from "./chunk-HIMWM526.js";
40
+ } from "./chunk-UN5O5SYG.js";
48
41
  import {
49
42
  inspectWorkspaceId
50
43
  } from "./chunk-UVIWZWM6.js";
51
44
  import {
52
45
  binFile,
46
+ bold,
47
+ boundedHealthError,
48
+ color,
49
+ colorForArea,
53
50
  commandMatchesBin,
54
51
  detachedHookShimCommand,
52
+ dim,
53
+ formatStatusline,
54
+ formatTeammates,
55
55
  packageVersion,
56
56
  pinnedHookCommand,
57
+ stripAnsi,
58
+ stripControlChars,
57
59
  warmBinCache
58
- } from "./chunk-CZ24W5QU.js";
60
+ } from "./chunk-K3AYW6VZ.js";
59
61
  import {
60
62
  canonicalGitRoot,
61
63
  canonicalRepositoryPath,
@@ -80,7 +82,7 @@ import {
80
82
  resolveAuthCredential,
81
83
  setStoredToken,
82
84
  withFileLock
83
- } from "./chunk-CPX3BUOC.js";
85
+ } from "./chunk-RTLFE6EZ.js";
84
86
  import {
85
87
  daemonIsLive,
86
88
  daemonRequest
@@ -2131,53 +2133,6 @@ async function runLatestDaemonBootstrap(ensureCurrent, options = {}) {
2131
2133
  }
2132
2134
  }
2133
2135
 
2134
- // src/lib/presence.ts
2135
- var DECISION_ORIGIN = "https://app.getprimitive.ai";
2136
- function validDecisionUrl(value) {
2137
- if (typeof value !== "string" || stripControlChars(value) !== value) {
2138
- return void 0;
2139
- }
2140
- try {
2141
- const url = new URL(value);
2142
- if (url.origin !== DECISION_ORIGIN || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "" || !/^\/decisions\/[^/]+$/u.test(url.pathname)) {
2143
- return void 0;
2144
- }
2145
- return url.href;
2146
- } catch {
2147
- return void 0;
2148
- }
2149
- }
2150
- function decisionLink(label, decisionUrl) {
2151
- const url = validDecisionUrl(decisionUrl);
2152
- return url ? `\x1B]8;;${url}\x07\x1B[34;4m${label}\x1B[0m\x1B]8;;\x07` : label;
2153
- }
2154
- function formatLabeled(labels, cap) {
2155
- if (labels === void 0) {
2156
- return "\u2014";
2157
- }
2158
- if (labels.length === 0) {
2159
- return "just you";
2160
- }
2161
- if (labels.length <= cap) {
2162
- return labels.join(", ");
2163
- }
2164
- return `${labels.slice(0, cap).join(", ")} +${String(labels.length - cap)}`;
2165
- }
2166
- function formatTeammates(names, cap) {
2167
- return formatLabeled(names?.map(stripControlChars), cap);
2168
- }
2169
- function formatTeammatesWithArea(teammates, cap) {
2170
- return formatLabeled(
2171
- teammates?.map((t) => {
2172
- const name = stripControlChars(t.name);
2173
- const area = stripControlChars(t.area ?? "").trim();
2174
- const label = area ? `${name} - ${area}` : name;
2175
- return decisionLink(label, t.decisionUrl);
2176
- }),
2177
- cap
2178
- );
2179
- }
2180
-
2181
2136
  // src/commands/daemon.ts
2182
2137
  var DAEMON_BIN = "prim-daemon-server";
2183
2138
  var CONFIG_DIR = join5(homedir5(), ".config", "prim");
@@ -3077,7 +3032,7 @@ async function fetchRecent(args, deps = defaultDeps2) {
3077
3032
  "decisions_recent",
3078
3033
  `/api/cli/decisions/recent?${params.toString()}`,
3079
3034
  client,
3080
- RECENT_TIMEOUT_MS
3035
+ deps.timeoutMs ?? RECENT_TIMEOUT_MS
3081
3036
  );
3082
3037
  if (args.author !== void 0 && res.author === void 0 && res.unavailable === void 0) {
3083
3038
  return {
@@ -3394,6 +3349,537 @@ function formatRelateJson(result) {
3394
3349
  return JSON.stringify(result.outcome, null, 2);
3395
3350
  }
3396
3351
 
3352
+ // src/decisions/repairs.ts
3353
+ var REPAIRS_TIMEOUT_MS = 1e4;
3354
+ var MAX_REPAIR_LIST_PAGES = 100;
3355
+ var MAX_REPAIR_DECISIONS_FOR_CONFIRM = 25;
3356
+ var MAX_REPAIRS_PER_PAGE = 10;
3357
+ var MAX_CURSOR_LENGTH = 4096;
3358
+ var MAX_INTENT_LENGTH = 1e3;
3359
+ var MAX_SIGNALS = 8;
3360
+ var MAX_SIGNAL_LENGTH = 160;
3361
+ var SHA_RE = /^[0-9a-f]{40}$/u;
3362
+ var SHA_INPUT_RE = /^[0-9a-f]{40}$/iu;
3363
+ var REVIEW_TOKEN_RE = /^[0-9a-f]{64}$/u;
3364
+ var REVIEW_TOKEN_INPUT_RE = /^[0-9a-f]{64}$/iu;
3365
+ var REPO_FULL_NAME_RE = /^[a-z0-9_.-]+\/[a-z0-9_.-]+$/u;
3366
+ var REVIEWED_CONFIRM_WIRE_ACTION = "confirm_reviewed_v2";
3367
+ var LEGACY_REPAIR_BODY_ERROR = "Body requires id, action (confirm or reject), and expectedProposedSha";
3368
+ var LEGACY_CONFIRM_PROTOCOL_ERROR = "Reviewed confirmation protocol required; legacy action confirm is not supported";
3369
+ var defaultDeps6 = { getClient };
3370
+ var RepairProposalNotFoundError = class extends Error {
3371
+ constructor(proposalId) {
3372
+ super(`Commit repair proposal not found: ${oneLine(proposalId)}`);
3373
+ this.name = "RepairProposalNotFoundError";
3374
+ }
3375
+ };
3376
+ var RepairAuthorizationError = class extends Error {
3377
+ constructor() {
3378
+ super(
3379
+ "Not authorized to review commit repairs; active organization membership and current repository authorization are required"
3380
+ );
3381
+ this.name = "RepairAuthorizationError";
3382
+ }
3383
+ };
3384
+ var RepairListContractError = class extends Error {
3385
+ constructor(detail) {
3386
+ super(`Commit repair response violated the reviewed CLI contract: ${oneLine(detail)}`);
3387
+ this.name = "RepairListContractError";
3388
+ }
3389
+ };
3390
+ var RepairEndpointVersionError = class extends Error {
3391
+ constructor() {
3392
+ super(
3393
+ "The selected server must be upgraded for the reviewed commit-repair contract; nothing was confirmed or queued (client/server version skew)"
3394
+ );
3395
+ this.name = "RepairEndpointVersionError";
3396
+ }
3397
+ };
3398
+ var RepairResolutionInputError = class extends Error {
3399
+ constructor(detail) {
3400
+ super(`Invalid commit repair resolution: ${oneLine(detail)}`);
3401
+ this.name = "RepairResolutionInputError";
3402
+ }
3403
+ };
3404
+ function contractError(path, detail) {
3405
+ throw new RepairListContractError(`${path} ${detail}`);
3406
+ }
3407
+ function asRecord(value, path) {
3408
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
3409
+ contractError(path, "must be an object");
3410
+ }
3411
+ return value;
3412
+ }
3413
+ function assertKeys(record, path, required, optional = []) {
3414
+ const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
3415
+ const missing = required.find((key) => !Object.hasOwn(record, key));
3416
+ if (missing !== void 0) contractError(path, `is missing ${missing}`);
3417
+ const extra = Object.keys(record).find((key) => !allowed.has(key));
3418
+ if (extra !== void 0) contractError(path, `contains unexpected field ${extra}`);
3419
+ }
3420
+ function asString(value, path, options) {
3421
+ if (typeof value !== "string") contractError(path, "must be a string");
3422
+ if (options.nonempty && value.length === 0) contractError(path, "must not be empty");
3423
+ if (value.length > options.max) contractError(path, "exceeds its length bound");
3424
+ return value;
3425
+ }
3426
+ function asTimestamp(value, path) {
3427
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
3428
+ contractError(path, "must be a non-negative safe integer timestamp");
3429
+ }
3430
+ return value;
3431
+ }
3432
+ function asCount(value, path) {
3433
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
3434
+ contractError(path, "must be a non-negative safe integer");
3435
+ }
3436
+ return value;
3437
+ }
3438
+ function asBoolean(value, path) {
3439
+ if (typeof value !== "boolean") contractError(path, "must be a boolean");
3440
+ return value;
3441
+ }
3442
+ function asSha(value, path) {
3443
+ const sha = asString(value, path, { max: 40, nonempty: true });
3444
+ if (!SHA_RE.test(sha)) contractError(path, "must be a canonical lowercase Git commit SHA");
3445
+ return sha;
3446
+ }
3447
+ function optionalString(record, key, path, max) {
3448
+ if (!Object.hasOwn(record, key)) return void 0;
3449
+ return asString(record[key], `${path}.${key}`, { max, nonempty: true });
3450
+ }
3451
+ function parseProposal(value, path) {
3452
+ const record = asRecord(value, path);
3453
+ assertKeys(
3454
+ record,
3455
+ path,
3456
+ ["_id", "repoFullName", "deadCommitSha", "status", "signals", "lastEvaluatedAt"],
3457
+ ["proposedSha", "proposedTier", "nextEvaluateAt"]
3458
+ );
3459
+ const repoFullName = asString(record.repoFullName, `${path}.repoFullName`, {
3460
+ max: 200,
3461
+ nonempty: true
3462
+ });
3463
+ if (!REPO_FULL_NAME_RE.test(repoFullName)) {
3464
+ contractError(`${path}.repoFullName`, "must be canonical owner/repository syntax");
3465
+ }
3466
+ const status = record.status;
3467
+ if (status !== "proposed" && status !== "confirmed" && status !== "verification_failed") {
3468
+ contractError(`${path}.status`, "is not a review-visible proposal status");
3469
+ }
3470
+ if (!Array.isArray(record.signals) || record.signals.length > MAX_SIGNALS) {
3471
+ contractError(`${path}.signals`, "must be a bounded array");
3472
+ }
3473
+ const signals = record.signals.map(
3474
+ (signal, index) => asString(signal, `${path}.signals[${String(index)}]`, {
3475
+ max: MAX_SIGNAL_LENGTH,
3476
+ nonempty: true
3477
+ })
3478
+ );
3479
+ const proposedSha = Object.hasOwn(record, "proposedSha") ? asSha(record.proposedSha, `${path}.proposedSha`) : void 0;
3480
+ let proposedTier;
3481
+ if (Object.hasOwn(record, "proposedTier")) {
3482
+ if (record.proposedTier !== "exact_patch_body" && record.proposedTier !== "exact_file_set") {
3483
+ contractError(`${path}.proposedTier`, "is not a supported match tier");
3484
+ }
3485
+ proposedTier = record.proposedTier;
3486
+ }
3487
+ const nextEvaluateAt = Object.hasOwn(record, "nextEvaluateAt") ? asTimestamp(record.nextEvaluateAt, `${path}.nextEvaluateAt`) : void 0;
3488
+ return {
3489
+ _id: asString(record._id, `${path}._id`, { max: 256, nonempty: true }),
3490
+ repoFullName,
3491
+ deadCommitSha: asSha(record.deadCommitSha, `${path}.deadCommitSha`),
3492
+ status,
3493
+ proposedSha,
3494
+ proposedTier,
3495
+ signals,
3496
+ lastEvaluatedAt: asTimestamp(record.lastEvaluatedAt, `${path}.lastEvaluatedAt`),
3497
+ nextEvaluateAt
3498
+ };
3499
+ }
3500
+ function parseDecision(value, path) {
3501
+ const record = asRecord(value, path);
3502
+ assertKeys(record, path, ["_id", "intent", "status", "intentTruncated"], ["shortId"]);
3503
+ const status = record.status;
3504
+ if (status !== "provisional" && status !== "abandoned") {
3505
+ contractError(`${path}.status`, "is not a repairable decision status");
3506
+ }
3507
+ return {
3508
+ _id: asString(record._id, `${path}._id`, { max: 256, nonempty: true }),
3509
+ shortId: optionalString(record, "shortId", path, 128),
3510
+ intent: asString(record.intent, `${path}.intent`, { max: MAX_INTENT_LENGTH }),
3511
+ status,
3512
+ intentTruncated: asBoolean(record.intentTruncated, `${path}.intentTruncated`)
3513
+ };
3514
+ }
3515
+ function parseListItem(value, path) {
3516
+ const record = asRecord(value, path);
3517
+ assertKeys(record, path, [
3518
+ "proposal",
3519
+ "decisions",
3520
+ "affectedDecisionCount",
3521
+ "decisionsTruncated",
3522
+ "reviewToken"
3523
+ ]);
3524
+ if (!Array.isArray(record.decisions) || record.decisions.length > MAX_REPAIR_DECISIONS_FOR_CONFIRM) {
3525
+ contractError(`${path}.decisions`, "must be a bounded array");
3526
+ }
3527
+ const decisions = record.decisions.map(
3528
+ (decision, index) => parseDecision(decision, `${path}.decisions[${String(index)}]`)
3529
+ );
3530
+ const proposal = parseProposal(record.proposal, `${path}.proposal`);
3531
+ const decisionsTruncated = asBoolean(record.decisionsTruncated, `${path}.decisionsTruncated`);
3532
+ const affectedDecisionCount = record.affectedDecisionCount === null ? null : asCount(record.affectedDecisionCount, `${path}.affectedDecisionCount`);
3533
+ let reviewToken;
3534
+ if (record.reviewToken === null) {
3535
+ reviewToken = null;
3536
+ } else {
3537
+ reviewToken = asString(record.reviewToken, `${path}.reviewToken`, {
3538
+ max: 64,
3539
+ nonempty: true
3540
+ });
3541
+ if (!REVIEW_TOKEN_RE.test(reviewToken)) {
3542
+ contractError(`${path}.reviewToken`, "must be a canonical lowercase SHA-256 token");
3543
+ }
3544
+ }
3545
+ if (!decisionsTruncated && affectedDecisionCount === null) {
3546
+ contractError(path, "cannot claim a complete decision list with an unknown count");
3547
+ }
3548
+ if (!decisionsTruncated && affectedDecisionCount !== decisions.length) {
3549
+ contractError(path, "has a complete decision count that does not match its projection");
3550
+ }
3551
+ if (decisionsTruncated && affectedDecisionCount !== null && affectedDecisionCount <= decisions.length) {
3552
+ contractError(path, "claims truncation without a larger affected-decision count");
3553
+ }
3554
+ if (decisionsTruncated && reviewToken !== null) {
3555
+ contractError(path, "must not expose a review token for a truncated decision set");
3556
+ }
3557
+ if (reviewToken !== null && (proposal.proposedSha === void 0 || proposal.proposedTier === void 0 || proposal.proposedSha === proposal.deadCommitSha)) {
3558
+ contractError(path, "must not expose a review token without a valid replacement candidate");
3559
+ }
3560
+ return {
3561
+ proposal,
3562
+ decisions,
3563
+ affectedDecisionCount,
3564
+ decisionsTruncated,
3565
+ reviewToken
3566
+ };
3567
+ }
3568
+ function parseListPage(value, pageNumber) {
3569
+ const path = `page ${String(pageNumber)}`;
3570
+ const record = asRecord(value, path);
3571
+ assertKeys(record, path, ["repairs", "isDone", "nextCursor", "truncated"]);
3572
+ if (!Array.isArray(record.repairs) || record.repairs.length > MAX_REPAIRS_PER_PAGE) {
3573
+ contractError(`${path}.repairs`, "must be a bounded array");
3574
+ }
3575
+ const isDone = asBoolean(record.isDone, `${path}.isDone`);
3576
+ const truncated = asBoolean(record.truncated, `${path}.truncated`);
3577
+ if (truncated === isDone) contractError(path, "has inconsistent isDone/truncated flags");
3578
+ let nextCursor;
3579
+ if (record.nextCursor === null) {
3580
+ nextCursor = null;
3581
+ } else {
3582
+ nextCursor = asString(record.nextCursor, `${path}.nextCursor`, {
3583
+ max: MAX_CURSOR_LENGTH,
3584
+ nonempty: true
3585
+ });
3586
+ }
3587
+ if (isDone && nextCursor !== null) contractError(path, "is done but includes a next cursor");
3588
+ if (!isDone && nextCursor === null) contractError(path, "is incomplete without a next cursor");
3589
+ return {
3590
+ repairs: record.repairs.map(
3591
+ (item, index) => parseListItem(item, `${path}.repairs[${String(index)}]`)
3592
+ ),
3593
+ isDone,
3594
+ nextCursor,
3595
+ truncated
3596
+ };
3597
+ }
3598
+ function effectivePageLimit(deps) {
3599
+ if (deps.maxPages === void 0) return MAX_REPAIR_LIST_PAGES;
3600
+ if (!Number.isSafeInteger(deps.maxPages) || deps.maxPages < 1) {
3601
+ throw new RepairListContractError("configured page limit must be a positive safe integer");
3602
+ }
3603
+ return Math.min(deps.maxPages, MAX_REPAIR_LIST_PAGES);
3604
+ }
3605
+ function listPath(cursor) {
3606
+ if (cursor === null) return "/api/cli/decisions/repairs";
3607
+ const search = new URLSearchParams({ cursor });
3608
+ return `/api/cli/decisions/repairs?${search.toString()}`;
3609
+ }
3610
+ async function fetchRepairsWithClient(client, deps) {
3611
+ const repairs = [];
3612
+ const seenCursors = /* @__PURE__ */ new Set();
3613
+ const seenProposalIds = /* @__PURE__ */ new Set();
3614
+ const pageLimit = effectivePageLimit(deps);
3615
+ let cursor = null;
3616
+ for (let pageNumber = 1; pageNumber <= pageLimit; pageNumber += 1) {
3617
+ let rawPage;
3618
+ try {
3619
+ rawPage = await client.get(listPath(cursor), {
3620
+ signal: AbortSignal.timeout(REPAIRS_TIMEOUT_MS)
3621
+ });
3622
+ } catch (error) {
3623
+ if (error instanceof HttpError && error.status === 403) {
3624
+ throw new RepairAuthorizationError();
3625
+ }
3626
+ if (error instanceof HttpError && error.status === 404) {
3627
+ throw new RepairEndpointVersionError();
3628
+ }
3629
+ throw error;
3630
+ }
3631
+ const page = parseListPage(rawPage, pageNumber);
3632
+ for (const item of page.repairs) {
3633
+ if (seenProposalIds.has(item.proposal._id)) {
3634
+ contractError(`page ${String(pageNumber)}`, "repeats a proposal from an earlier page");
3635
+ }
3636
+ seenProposalIds.add(item.proposal._id);
3637
+ repairs.push(item);
3638
+ }
3639
+ if (page.isDone) {
3640
+ return { repairs, isDone: true, nextCursor: null, truncated: false };
3641
+ }
3642
+ const nextCursor = page.nextCursor;
3643
+ if (nextCursor === null)
3644
+ contractError(`page ${String(pageNumber)}`, "has no continuation cursor");
3645
+ if (nextCursor === cursor || seenCursors.has(nextCursor)) {
3646
+ contractError(`page ${String(pageNumber)}`, "has a repeated or non-advancing cursor");
3647
+ }
3648
+ seenCursors.add(nextCursor);
3649
+ cursor = nextCursor;
3650
+ }
3651
+ throw new RepairListContractError(
3652
+ `pagination did not finish within ${String(pageLimit)} page(s); no partial result was emitted`
3653
+ );
3654
+ }
3655
+ async function fetchRepairs(deps = defaultDeps6) {
3656
+ return fetchRepairsWithClient(deps.getClient(), deps);
3657
+ }
3658
+ function normalizedResolutionInput(proposalId, expectedProposedSha) {
3659
+ if (proposalId.length === 0 || proposalId.length > 256) {
3660
+ throw new RepairResolutionInputError("proposal ID must be a non-empty bounded string");
3661
+ }
3662
+ if (!SHA_INPUT_RE.test(expectedProposedSha)) {
3663
+ throw new RepairResolutionInputError("expected proposed SHA must be a 40-character Git SHA");
3664
+ }
3665
+ return { proposalId, expectedProposedSha: expectedProposedSha.toLowerCase() };
3666
+ }
3667
+ function parseResolutionOutcome(value, action) {
3668
+ const record = asRecord(value, "resolution response");
3669
+ const allowed = action === "confirm" ? ["confirmed", "already_applied", "disabled", "no_op", "stale"] : ["rejected", "already_applied", "no_op", "stale"];
3670
+ if (record.status === "backoff") {
3671
+ assertKeys(record, "resolution response", ["status", "retryAt"]);
3672
+ if (action !== "confirm") {
3673
+ contractError("resolution response.status", "backoff is impossible for reject");
3674
+ }
3675
+ return {
3676
+ status: "backoff",
3677
+ retryAt: asTimestamp(record.retryAt, "resolution response.retryAt")
3678
+ };
3679
+ }
3680
+ assertKeys(record, "resolution response", ["status"]);
3681
+ if (typeof record.status !== "string" || !allowed.includes(record.status)) {
3682
+ contractError("resolution response.status", "is not a supported outcome");
3683
+ }
3684
+ return { status: record.status };
3685
+ }
3686
+ function normalizedReviewToken(value) {
3687
+ if (value === void 0 || !REVIEW_TOKEN_INPUT_RE.test(value)) {
3688
+ throw new RepairResolutionInputError(
3689
+ "confirm requires --review-token with the 64-character token from the reviewed list"
3690
+ );
3691
+ }
3692
+ return value.toLowerCase();
3693
+ }
3694
+ function parseConflictOutcome(value, action) {
3695
+ const record = asRecord(value, "resolution conflict response");
3696
+ assertKeys(record, "resolution conflict response", ["status"]);
3697
+ if (action !== "confirm") {
3698
+ contractError(
3699
+ "resolution conflict response.status",
3700
+ "a review conflict is impossible for reject"
3701
+ );
3702
+ }
3703
+ if (record.status !== "review_too_large" && record.status !== "stale_review") {
3704
+ contractError("resolution conflict response.status", "is not a supported 409 outcome");
3705
+ }
3706
+ return { status: record.status };
3707
+ }
3708
+ async function resolveRepair(proposalId, expectedProposedSha, action, expectedReviewToken, deps = defaultDeps6) {
3709
+ const normalized = normalizedResolutionInput(proposalId, expectedProposedSha);
3710
+ const reviewToken = action === "confirm" ? normalizedReviewToken(expectedReviewToken) : void 0;
3711
+ if (action === "reject" && expectedReviewToken !== void 0) {
3712
+ throw new RepairResolutionInputError("--review-token is valid only with confirm");
3713
+ }
3714
+ const client = deps.getClient();
3715
+ const wireAction = action === "confirm" ? REVIEWED_CONFIRM_WIRE_ACTION : "reject";
3716
+ try {
3717
+ const rawOutcome = await client.post(
3718
+ "/api/cli/decisions/repairs/resolve",
3719
+ {
3720
+ id: normalized.proposalId,
3721
+ action: wireAction,
3722
+ expectedProposedSha: normalized.expectedProposedSha,
3723
+ ...reviewToken === void 0 ? {} : { expectedReviewToken: reviewToken }
3724
+ },
3725
+ { signal: AbortSignal.timeout(REPAIRS_TIMEOUT_MS) }
3726
+ );
3727
+ return {
3728
+ proposalId: normalized.proposalId,
3729
+ action,
3730
+ outcome: parseResolutionOutcome(rawOutcome, action)
3731
+ };
3732
+ } catch (error) {
3733
+ if (error instanceof HttpError && error.status === 404 && error.message === "Repair proposal not found") {
3734
+ throw new RepairProposalNotFoundError(normalized.proposalId);
3735
+ }
3736
+ if (error instanceof HttpError && error.status === 404) {
3737
+ throw new RepairEndpointVersionError();
3738
+ }
3739
+ if (error instanceof HttpError && error.status === 403) {
3740
+ throw new RepairAuthorizationError();
3741
+ }
3742
+ if (action === "confirm" && error instanceof HttpError && error.status === 400 && (error.message === LEGACY_REPAIR_BODY_ERROR || error.message === LEGACY_CONFIRM_PROTOCOL_ERROR)) {
3743
+ throw new RepairEndpointVersionError();
3744
+ }
3745
+ if (error instanceof HttpError && error.status === 409) {
3746
+ return {
3747
+ proposalId: normalized.proposalId,
3748
+ action,
3749
+ outcome: parseConflictOutcome(error.body, action)
3750
+ };
3751
+ }
3752
+ throw error;
3753
+ }
3754
+ }
3755
+ function shortSha(sha) {
3756
+ return sha === void 0 ? "(missing)" : sha.slice(0, 12);
3757
+ }
3758
+ function tierLabel(tier) {
3759
+ if (tier === "exact_patch_body") return "exact patch body";
3760
+ if (tier === "exact_file_set") return "exact file set";
3761
+ return "match tier missing";
3762
+ }
3763
+ function oneLine(value) {
3764
+ return stripControlChars(value.replace(/\s+/gu, " ")).trim();
3765
+ }
3766
+ function timestampLabel(value) {
3767
+ const timestamp = new Date(value);
3768
+ return Number.isNaN(timestamp.getTime()) ? String(value) : `${timestamp.toISOString()} (${String(value)})`;
3769
+ }
3770
+ function reviewLabel(item) {
3771
+ if (item.affectedDecisionCount === null) {
3772
+ return `showing ${String(item.decisions.length)}; total unknown; confirmation disabled`;
3773
+ }
3774
+ if (item.decisionsTruncated) {
3775
+ return `showing ${String(item.decisions.length)} of ${String(item.affectedDecisionCount)}; confirmation disabled`;
3776
+ }
3777
+ if (item.affectedDecisionCount > MAX_REPAIR_DECISIONS_FOR_CONFIRM) {
3778
+ return `${String(item.affectedDecisionCount)} affected; exceeds CLI review limit ${String(MAX_REPAIR_DECISIONS_FOR_CONFIRM)}; confirmation disabled`;
3779
+ }
3780
+ if (item.affectedDecisionCount === 0) {
3781
+ return "0 affected; confirmation disabled";
3782
+ }
3783
+ return `${String(item.affectedDecisionCount)} affected; complete`;
3784
+ }
3785
+ function canRenderConfirm(item) {
3786
+ return item.proposal.proposedSha !== void 0 && !item.decisionsTruncated && item.affectedDecisionCount !== null && item.affectedDecisionCount > 0 && item.affectedDecisionCount <= MAX_REPAIR_DECISIONS_FOR_CONFIRM && item.reviewToken !== null;
3787
+ }
3788
+ function shellArg(value) {
3789
+ if (/^[a-zA-Z0-9_./:@%+=,-]+$/u.test(value)) return value;
3790
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
3791
+ }
3792
+ function proposalReviewLabel(item) {
3793
+ if (item.proposal.status === "confirmed") {
3794
+ return canRenderConfirm(item) ? "already confirmed; a guarded application retry may be requested when eligible" : "already confirmed; a guarded retry is currently unavailable";
3795
+ }
3796
+ if (item.proposal.status === "verification_failed") {
3797
+ return canRenderConfirm(item) ? "prior verification failed; eligible for a fresh confirmation" : "prior verification failed; confirmation is currently unavailable";
3798
+ }
3799
+ return canRenderConfirm(item) ? "awaiting operator review" : "awaiting review; confirmation is currently unavailable";
3800
+ }
3801
+ function formatRepairsHuman(result) {
3802
+ if (result.repairs.length === 0) {
3803
+ return "[prim] decision repairs \xB7 0 review-visible proposals \xB7 complete paginated scan";
3804
+ }
3805
+ const lines = [
3806
+ `[prim] decision repairs \xB7 ${String(result.repairs.length)} review-visible proposal(s) \xB7 complete paginated scan`
3807
+ ];
3808
+ for (const item of result.repairs) {
3809
+ const { proposal, decisions } = item;
3810
+ const renderedProposalId = oneLine(proposal._id);
3811
+ lines.push(
3812
+ ` ${renderedProposalId} [${proposal.status}] \xB7 ${oneLine(proposal.repoFullName)} \xB7 ${shortSha(proposal.deadCommitSha)} -> ${shortSha(proposal.proposedSha)} \xB7 ${tierLabel(proposal.proposedTier)}`,
3813
+ ` review state: ${proposalReviewLabel(item)}`
3814
+ );
3815
+ if (proposal.signals.length > 0) {
3816
+ lines.push(` signals: ${proposal.signals.map(oneLine).join("; ")}`);
3817
+ }
3818
+ if (proposal.nextEvaluateAt !== void 0) {
3819
+ lines.push(` next proposal evaluation: ${timestampLabel(proposal.nextEvaluateAt)}`);
3820
+ }
3821
+ lines.push(` decisions (${reviewLabel(item)}):`);
3822
+ for (const decision of decisions) {
3823
+ const id = renderIdentifier({
3824
+ shortId: decision.shortId === void 0 ? void 0 : oneLine(decision.shortId),
3825
+ id: oneLine(decision._id)
3826
+ });
3827
+ const truncation = decision.intentTruncated ? " [intent truncated]" : "";
3828
+ lines.push(` - ${id} [${decision.status}] ${oneLine(decision.intent)}${truncation}`);
3829
+ }
3830
+ if (canRenderConfirm(item) && proposal.status === "proposed") {
3831
+ lines.push(
3832
+ ` confirm: prim decisions repairs confirm ${shellArg(renderedProposalId)} ${proposal.proposedSha} --review-token ${item.reviewToken}`
3833
+ );
3834
+ }
3835
+ if (canRenderConfirm(item) && proposal.status === "verification_failed") {
3836
+ lines.push(
3837
+ ` retry verification: prim decisions repairs confirm ${shellArg(renderedProposalId)} ${proposal.proposedSha} --review-token ${item.reviewToken}`
3838
+ );
3839
+ }
3840
+ if (canRenderConfirm(item) && proposal.status === "confirmed") {
3841
+ lines.push(
3842
+ ` guarded retry confirmation: prim decisions repairs confirm ${shellArg(renderedProposalId)} ${proposal.proposedSha} --review-token ${item.reviewToken}`
3843
+ );
3844
+ }
3845
+ if (proposal.proposedSha !== void 0) {
3846
+ lines.push(
3847
+ ` reject: prim decisions repairs reject ${shellArg(renderedProposalId)} ${proposal.proposedSha}`
3848
+ );
3849
+ }
3850
+ }
3851
+ return lines.join("\n");
3852
+ }
3853
+ function formatRepairsJson(result) {
3854
+ return JSON.stringify(result, null, 2);
3855
+ }
3856
+ function formatRepairResolutionHuman(result) {
3857
+ const proposalId = oneLine(result.proposalId);
3858
+ switch (result.outcome.status) {
3859
+ case "confirmed":
3860
+ return `[prim] repair ${proposalId} confirmed; fresh landing verification queued. No Decision was changed by this response.`;
3861
+ case "rejected":
3862
+ return `[prim] repair ${proposalId} rejected.`;
3863
+ case "already_applied":
3864
+ return `[prim] repair ${proposalId} was already applied; nothing to change.`;
3865
+ case "backoff":
3866
+ return `[prim] repair ${proposalId} is in application retry backoff; nothing changed. Retry at ${timestampLabel(result.outcome.retryAt)}.`;
3867
+ case "disabled":
3868
+ return `[prim] repair ${proposalId} was not confirmed because repair application is disabled; nothing changed.`;
3869
+ case "review_too_large":
3870
+ return `[prim] repair ${proposalId} was refused because its complete review exceeds the safe bound; nothing changed.`;
3871
+ case "stale_review":
3872
+ return `[prim] repair ${proposalId} changed after review; re-list and review the current decision set before confirming with its new token. Nothing changed.`;
3873
+ case "stale":
3874
+ return `[prim] repair ${proposalId} has a different proposed SHA; refresh the repair list before acting. Nothing changed.`;
3875
+ case "no_op":
3876
+ return `[prim] repair ${proposalId} cannot be ${result.action === "confirm" ? "confirmed" : "rejected"} in its current state; nothing changed.`;
3877
+ }
3878
+ }
3879
+ function formatRepairResolutionJson(result) {
3880
+ return JSON.stringify(result.outcome, null, 2);
3881
+ }
3882
+
3397
3883
  // src/decisions/show.ts
3398
3884
  var NOT_FOUND_RE4 = /not found/i;
3399
3885
  function colorStatus(status) {
@@ -3406,14 +3892,14 @@ function colorStatus(status) {
3406
3892
  return color(status, "gray");
3407
3893
  }
3408
3894
  var SHOW_TIMEOUT_MS = 1e4;
3409
- var defaultDeps6 = { getClient };
3895
+ var defaultDeps7 = { getClient };
3410
3896
  var DecisionNotFoundError = class extends Error {
3411
3897
  constructor(idOrShortId) {
3412
3898
  super(`Decision not found: ${idOrShortId}`);
3413
3899
  this.name = "DecisionNotFoundError";
3414
3900
  }
3415
3901
  };
3416
- async function fetchShow(idOrShortId, deps = defaultDeps6) {
3902
+ async function fetchShow(idOrShortId, deps = defaultDeps7) {
3417
3903
  const params = new URLSearchParams({ id: idOrShortId });
3418
3904
  const client = deps.getClient();
3419
3905
  try {
@@ -3547,7 +4033,33 @@ async function askConfirmation(question, output = process.stdout) {
3547
4033
 
3548
4034
  // src/commands/decisions.ts
3549
4035
  var EXIT_NOT_FOUND = 4;
4036
+ var EXIT_FAILURE = 1;
3550
4037
  var EXIT_USAGE = 2;
4038
+ function handleRepairCommandError(error) {
4039
+ if (error instanceof RepairProposalNotFoundError) {
4040
+ console.error(`[prim] ${error.message}`);
4041
+ process.exitCode = EXIT_NOT_FOUND;
4042
+ return true;
4043
+ }
4044
+ if (error instanceof RepairResolutionInputError) {
4045
+ console.error(`[prim] ${error.message}`);
4046
+ process.exitCode = EXIT_USAGE;
4047
+ return true;
4048
+ }
4049
+ if (error instanceof RepairAuthorizationError || error instanceof HttpError && error.status === 403) {
4050
+ console.error(
4051
+ `[prim] ${error instanceof RepairAuthorizationError ? error.message : "Not authorized to review commit repairs; active organization membership and current repository authorization are required"}`
4052
+ );
4053
+ process.exitCode = EXIT_FAILURE;
4054
+ return true;
4055
+ }
4056
+ if (error instanceof RepairEndpointVersionError || error instanceof RepairListContractError) {
4057
+ console.error(`[prim] ${error.message}`);
4058
+ process.exitCode = EXIT_FAILURE;
4059
+ return true;
4060
+ }
4061
+ return false;
4062
+ }
3551
4063
  var CREATE_INACTIVE_PROMPT = "[prim] Decision ingestion is disabled here. Create this one Decision without enabling passive ingestion?";
3552
4064
  var CREATE_INACTIVE_APPROVED = "[prim] one-time Decision creation approved; passive ingestion remains disabled here";
3553
4065
  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 +4138,46 @@ function registerDecisionsCommands(program2) {
3626
4138
  throw err;
3627
4139
  }
3628
4140
  });
4141
+ const repairs = decisions.command("repairs").description("Review human-gated commit rewrite repair proposals").action(runRepairsList);
4142
+ async function runRepairsList() {
4143
+ try {
4144
+ const result = await fetchRepairs();
4145
+ console.error(formatRepairsHuman(result));
4146
+ console.log(formatRepairsJson(result));
4147
+ } catch (error) {
4148
+ if (handleRepairCommandError(error)) return;
4149
+ throw error;
4150
+ }
4151
+ }
4152
+ repairs.command("list").description("List review-visible commit rewrite repair proposals").action(runRepairsList);
4153
+ const registerRepairResolution = (action) => {
4154
+ const resolution = repairs.command(`${action} <proposalId> <proposedSha>`).description(
4155
+ action === "confirm" ? "Confirm the exact reviewed decision set and queue fresh landing verification" : "Reject a proposal and remember its proposed SHA"
4156
+ );
4157
+ if (action === "confirm") {
4158
+ resolution.option(
4159
+ "--review-token <token>",
4160
+ "64-character token printed with the complete reviewed decision set (required)"
4161
+ );
4162
+ }
4163
+ resolution.action(
4164
+ async (proposalId, proposedSha, options) => {
4165
+ try {
4166
+ const result = await resolveRepair(proposalId, proposedSha, action, options.reviewToken);
4167
+ console.error(formatRepairResolutionHuman(result));
4168
+ console.log(formatRepairResolutionJson(result));
4169
+ if (result.outcome.status === "review_too_large" || result.outcome.status === "stale_review") {
4170
+ process.exitCode = EXIT_USAGE;
4171
+ }
4172
+ } catch (error) {
4173
+ if (handleRepairCommandError(error)) return;
4174
+ throw error;
4175
+ }
4176
+ }
4177
+ );
4178
+ };
4179
+ registerRepairResolution("confirm");
4180
+ registerRepairResolution("reject");
3629
4181
  decisions.command("create").description("Record a decision directly with its explicit user or agent origin").requiredOption("--intent <text>", "What was decided (required)").addOption(
3630
4182
  new Option2(
3631
4183
  "--attribution <origin>",
@@ -5647,48 +6199,6 @@ function registerSetupCommand(program2, dependencies = {}) {
5647
6199
  import { readFileSync as readFileSync9 } from "fs";
5648
6200
  import { dirname as dirname6, resolve as resolve3 } from "path";
5649
6201
  import { fileURLToPath } from "url";
5650
-
5651
- // src/lib/statusline-render.ts
5652
- var STATUSLINE_NAME_CAP = 3;
5653
- function formatStatusline(version, snapshot, resolveIngestionStatus) {
5654
- if (!snapshot) {
5655
- return `primitive ${version} (daemon: down)`;
5656
- }
5657
- if (snapshot.healthy === false) {
5658
- if (snapshot.needsReauth) {
5659
- return `primitive ${version} (daemon: paused \xB7 run \`prim auth login\`)`;
5660
- }
5661
- if (snapshot.ingestion?.healthy === false) {
5662
- const pending = snapshot.ingestion.pendingCount;
5663
- const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
5664
- return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""})`;
5665
- }
5666
- if (snapshot.heartbeat?.healthy === false) {
5667
- return `primitive ${version} (daemon: degraded \xB7 presence: unavailable)`;
5668
- }
5669
- return `primitive ${version} (daemon: starting)`;
5670
- }
5671
- const ingestionStatus = resolveIngestionStatus();
5672
- if (snapshot.envMismatch) {
5673
- return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: other env)`;
5674
- }
5675
- if (snapshot.presenceStale) {
5676
- return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: stale)`;
5677
- }
5678
- let team;
5679
- if (snapshot.onlineTeammates !== void 0) {
5680
- team = `team: ${formatTeammatesWithArea(snapshot.onlineTeammates, STATUSLINE_NAME_CAP)}`;
5681
- } else if (snapshot.onlineNames !== void 0) {
5682
- team = `team: ${formatTeammates(snapshot.onlineNames, STATUSLINE_NAME_CAP)}`;
5683
- } else if (typeof snapshot.onlineCount === "number") {
5684
- team = `team: ${String(snapshot.onlineCount)} online`;
5685
- } else {
5686
- team = "team: \u2014";
5687
- }
5688
- return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 ${team})`;
5689
- }
5690
-
5691
- // src/commands/statusline.ts
5692
6202
  var STATUSLINE_TIMEOUT_MS = 200;
5693
6203
  function readPackageVersion() {
5694
6204
  try {