@primitive.ai/prim 0.1.0-alpha.65 → 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.
- package/dist/{chunk-5CKL5EYQ.js → chunk-CPSE3Y3T.js} +62 -0
- package/dist/{chunk-LNU44OJV.js → chunk-CZ24W5QU.js} +2 -1
- package/dist/{chunk-CPX3BUOC.js → chunk-RTLFE6EZ.js} +6 -2
- package/dist/{chunk-KHZNYEUV.js → chunk-U6L2RU67.js} +1 -1
- package/dist/{chunk-HIMWM526.js → chunk-UN5O5SYG.js} +1 -1
- package/dist/{chunk-CPHJUQNE.js → chunk-XPVNH3J6.js} +229 -89
- package/dist/{chunk-WS3MCNFD.js → chunk-XUAP572Z.js} +1 -1
- package/dist/daemon/server.js +6 -2
- package/dist/hooks/post-commit.js +3 -3
- package/dist/hooks/post-rewrite.js +194 -0
- package/dist/hooks/post-tool-use.js +7 -7
- package/dist/hooks/pre-commit.js +2 -2
- package/dist/hooks/pre-tool-use.js +2 -2
- package/dist/hooks/prim-hook.js +17 -17
- package/dist/hooks/session-start.js +9 -4
- package/dist/index.js +699 -62
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
bindRepository,
|
|
4
|
+
compareSemver,
|
|
5
|
+
ensureEffectivePostCommitHook,
|
|
6
|
+
ensureEffectivePostRewriteHook,
|
|
7
|
+
ensurePostCommitHookAtPath,
|
|
8
|
+
ensurePostRewriteHookAtPath,
|
|
9
|
+
inspectEffectivePostCommitHook,
|
|
10
|
+
inspectEffectivePostRewriteHook,
|
|
11
|
+
postCommitHookBlock,
|
|
12
|
+
postRewriteHookBlock,
|
|
13
|
+
printJson,
|
|
14
|
+
registerSkillCommands,
|
|
15
|
+
resolveRepositoryBinding,
|
|
16
|
+
uninstallPostCommitHookAtPath,
|
|
17
|
+
uninstallPostRewriteHookAtPath,
|
|
18
|
+
uninstallProjectPostCommitHook,
|
|
19
|
+
uninstallProjectPostRewriteHook
|
|
20
|
+
} from "./chunk-XPVNH3J6.js";
|
|
2
21
|
import {
|
|
3
22
|
bold,
|
|
4
23
|
boundedHealthError,
|
|
@@ -13,7 +32,10 @@ import {
|
|
|
13
32
|
checkAffectedDecisions,
|
|
14
33
|
daemonOrDirectGet,
|
|
15
34
|
formatDecisionsWarning
|
|
16
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-XUAP572Z.js";
|
|
36
|
+
import {
|
|
37
|
+
fetchFeedbackCapability
|
|
38
|
+
} from "./chunk-U6L2RU67.js";
|
|
17
39
|
import {
|
|
18
40
|
JOURNAL_DIR,
|
|
19
41
|
SESSIONS_DIR,
|
|
@@ -22,23 +44,7 @@ import {
|
|
|
22
44
|
listFlushing,
|
|
23
45
|
pendingJournalStats,
|
|
24
46
|
readMovesFromPath
|
|
25
|
-
} from "./chunk-
|
|
26
|
-
import {
|
|
27
|
-
bindRepository,
|
|
28
|
-
compareSemver,
|
|
29
|
-
ensureEffectivePostCommitHook,
|
|
30
|
-
ensurePostCommitHookAtPath,
|
|
31
|
-
inspectEffectivePostCommitHook,
|
|
32
|
-
postCommitHookBlock,
|
|
33
|
-
printJson,
|
|
34
|
-
registerSkillCommands,
|
|
35
|
-
resolveRepositoryBinding,
|
|
36
|
-
uninstallPostCommitHookAtPath,
|
|
37
|
-
uninstallProjectPostCommitHook
|
|
38
|
-
} from "./chunk-CPHJUQNE.js";
|
|
39
|
-
import {
|
|
40
|
-
fetchFeedbackCapability
|
|
41
|
-
} from "./chunk-KHZNYEUV.js";
|
|
47
|
+
} from "./chunk-UN5O5SYG.js";
|
|
42
48
|
import {
|
|
43
49
|
inspectWorkspaceId
|
|
44
50
|
} from "./chunk-UVIWZWM6.js";
|
|
@@ -49,7 +55,7 @@ import {
|
|
|
49
55
|
packageVersion,
|
|
50
56
|
pinnedHookCommand,
|
|
51
57
|
warmBinCache
|
|
52
|
-
} from "./chunk-
|
|
58
|
+
} from "./chunk-CZ24W5QU.js";
|
|
53
59
|
import {
|
|
54
60
|
canonicalGitRoot,
|
|
55
61
|
canonicalRepositoryPath,
|
|
@@ -74,7 +80,7 @@ import {
|
|
|
74
80
|
resolveAuthCredential,
|
|
75
81
|
setStoredToken,
|
|
76
82
|
withFileLock
|
|
77
|
-
} from "./chunk-
|
|
83
|
+
} from "./chunk-RTLFE6EZ.js";
|
|
78
84
|
import {
|
|
79
85
|
daemonIsLive,
|
|
80
86
|
daemonRequest
|
|
@@ -101,8 +107,16 @@ async function applyActivation(active) {
|
|
|
101
107
|
try {
|
|
102
108
|
let binding;
|
|
103
109
|
let postCommitHook;
|
|
110
|
+
let postRewriteHook;
|
|
104
111
|
if (active) {
|
|
105
112
|
postCommitHook = ensureEffectivePostCommitHook(root).path;
|
|
113
|
+
try {
|
|
114
|
+
postRewriteHook = ensureEffectivePostRewriteHook(root).path;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
117
|
+
process.stderr.write(`[prim] post-rewrite hook coverage is degraded: ${detail}
|
|
118
|
+
`);
|
|
119
|
+
}
|
|
106
120
|
phase = "repository binding";
|
|
107
121
|
binding = await bindRepository(root);
|
|
108
122
|
phase = "local activation";
|
|
@@ -132,7 +146,8 @@ async function applyActivation(active) {
|
|
|
132
146
|
repositoryFullName: binding.repositoryFullName,
|
|
133
147
|
...binding.status === "connected" ? { repoSyncId: binding.repoSyncId } : {}
|
|
134
148
|
} : {},
|
|
135
|
-
...postCommitHook ? { postCommitHook } : {}
|
|
149
|
+
...postCommitHook ? { postCommitHook } : {},
|
|
150
|
+
...postRewriteHook ? { postRewriteHook } : {}
|
|
136
151
|
});
|
|
137
152
|
} catch (err) {
|
|
138
153
|
const detail = err instanceof Error ? err.message : String(err);
|
|
@@ -3379,6 +3394,537 @@ function formatRelateJson(result) {
|
|
|
3379
3394
|
return JSON.stringify(result.outcome, null, 2);
|
|
3380
3395
|
}
|
|
3381
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
|
+
|
|
3382
3928
|
// src/decisions/show.ts
|
|
3383
3929
|
var NOT_FOUND_RE4 = /not found/i;
|
|
3384
3930
|
function colorStatus(status) {
|
|
@@ -3391,14 +3937,14 @@ function colorStatus(status) {
|
|
|
3391
3937
|
return color(status, "gray");
|
|
3392
3938
|
}
|
|
3393
3939
|
var SHOW_TIMEOUT_MS = 1e4;
|
|
3394
|
-
var
|
|
3940
|
+
var defaultDeps7 = { getClient };
|
|
3395
3941
|
var DecisionNotFoundError = class extends Error {
|
|
3396
3942
|
constructor(idOrShortId) {
|
|
3397
3943
|
super(`Decision not found: ${idOrShortId}`);
|
|
3398
3944
|
this.name = "DecisionNotFoundError";
|
|
3399
3945
|
}
|
|
3400
3946
|
};
|
|
3401
|
-
async function fetchShow(idOrShortId, deps =
|
|
3947
|
+
async function fetchShow(idOrShortId, deps = defaultDeps7) {
|
|
3402
3948
|
const params = new URLSearchParams({ id: idOrShortId });
|
|
3403
3949
|
const client = deps.getClient();
|
|
3404
3950
|
try {
|
|
@@ -3532,7 +4078,33 @@ async function askConfirmation(question, output = process.stdout) {
|
|
|
3532
4078
|
|
|
3533
4079
|
// src/commands/decisions.ts
|
|
3534
4080
|
var EXIT_NOT_FOUND = 4;
|
|
4081
|
+
var EXIT_FAILURE = 1;
|
|
3535
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
|
+
}
|
|
3536
4108
|
var CREATE_INACTIVE_PROMPT = "[prim] Decision ingestion is disabled here. Create this one Decision without enabling passive ingestion?";
|
|
3537
4109
|
var CREATE_INACTIVE_APPROVED = "[prim] one-time Decision creation approved; passive ingestion remains disabled here";
|
|
3538
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";
|
|
@@ -3611,6 +4183,46 @@ function registerDecisionsCommands(program2) {
|
|
|
3611
4183
|
throw err;
|
|
3612
4184
|
}
|
|
3613
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");
|
|
3614
4226
|
decisions.command("create").description("Record a decision directly with its explicit user or agent origin").requiredOption("--intent <text>", "What was decided (required)").addOption(
|
|
3615
4227
|
new Option2(
|
|
3616
4228
|
"--attribution <origin>",
|
|
@@ -4004,27 +4616,27 @@ async function checkRepositoryBinding() {
|
|
|
4004
4616
|
};
|
|
4005
4617
|
}
|
|
4006
4618
|
}
|
|
4007
|
-
function
|
|
4619
|
+
function classifyManagedHook(hookName, inspection) {
|
|
4008
4620
|
if (inspection.covered) {
|
|
4009
4621
|
return {
|
|
4010
|
-
name:
|
|
4622
|
+
name: hookName,
|
|
4011
4623
|
status: "ok",
|
|
4012
4624
|
detail: `effective and executable \xB7 ${inspection.kind} \xB7 ${inspection.hookPath}`
|
|
4013
4625
|
};
|
|
4014
4626
|
}
|
|
4015
4627
|
return {
|
|
4016
|
-
name:
|
|
4628
|
+
name: hookName,
|
|
4017
4629
|
status: "fail",
|
|
4018
4630
|
detail: `${inspection.reason ?? "uncovered"} \xB7 ${inspection.hookPath}`
|
|
4019
4631
|
};
|
|
4020
4632
|
}
|
|
4021
|
-
function
|
|
4633
|
+
function checkManagedHook(hookName, inspect) {
|
|
4022
4634
|
try {
|
|
4023
|
-
return
|
|
4635
|
+
return classifyManagedHook(hookName, inspect());
|
|
4024
4636
|
} catch (error) {
|
|
4025
4637
|
const detail = error instanceof Error ? error.message : String(error);
|
|
4026
4638
|
return {
|
|
4027
|
-
name:
|
|
4639
|
+
name: hookName,
|
|
4028
4640
|
status: /not a git repository/iu.test(detail) ? "warn" : "fail",
|
|
4029
4641
|
detail: detail.slice(0, 120)
|
|
4030
4642
|
};
|
|
@@ -4150,7 +4762,8 @@ async function collectChecks() {
|
|
|
4150
4762
|
checkFeedbackHooks(),
|
|
4151
4763
|
checkWorkspaceIdentity(),
|
|
4152
4764
|
await checkRepositoryBinding(),
|
|
4153
|
-
|
|
4765
|
+
checkManagedHook("post-commit", inspectEffectivePostCommitHook),
|
|
4766
|
+
checkManagedHook("post-rewrite", inspectEffectivePostRewriteHook),
|
|
4154
4767
|
...backend,
|
|
4155
4768
|
await checkFeedbackCapability()
|
|
4156
4769
|
];
|
|
@@ -4534,7 +5147,8 @@ import { dirname as dirname4, join as join7, resolve as resolve2 } from "path";
|
|
|
4534
5147
|
import { Option as Option3 } from "commander";
|
|
4535
5148
|
var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
|
|
4536
5149
|
var POST_COMMIT = { hookName: "post-commit", binName: "prim-post-commit" };
|
|
4537
|
-
var
|
|
5150
|
+
var POST_REWRITE = { hookName: "post-rewrite", binName: "prim-post-rewrite" };
|
|
5151
|
+
var HOOKS = [PRE_COMMIT, POST_COMMIT, POST_REWRITE];
|
|
4538
5152
|
var GIT_TIMEOUT_MS = 1e3;
|
|
4539
5153
|
function blockMarkers(spec) {
|
|
4540
5154
|
return {
|
|
@@ -4563,6 +5177,7 @@ ${hookShim(spec.binName)}
|
|
|
4563
5177
|
}
|
|
4564
5178
|
function huskyBlock(spec) {
|
|
4565
5179
|
if (spec.hookName === POST_COMMIT.hookName) return postCommitHookBlock();
|
|
5180
|
+
if (spec.hookName === POST_REWRITE.hookName) return postRewriteHookBlock();
|
|
4566
5181
|
const { start, end } = blockMarkers(spec);
|
|
4567
5182
|
return `${start}
|
|
4568
5183
|
${hookShim(spec.binName)}
|
|
@@ -4570,6 +5185,7 @@ ${end}`;
|
|
|
4570
5185
|
}
|
|
4571
5186
|
function gatedBlock(spec) {
|
|
4572
5187
|
if (spec.hookName === POST_COMMIT.hookName) return postCommitHookBlock();
|
|
5188
|
+
if (spec.hookName === POST_REWRITE.hookName) return postRewriteHookBlock();
|
|
4573
5189
|
const { start, end } = blockMarkers(spec);
|
|
4574
5190
|
return `${start}
|
|
4575
5191
|
${gatedShim(spec.binName)}
|
|
@@ -4707,7 +5323,6 @@ var PASSTHROUGH_HOOKS = [
|
|
|
4707
5323
|
"post-checkout",
|
|
4708
5324
|
"post-merge",
|
|
4709
5325
|
"pre-push",
|
|
4710
|
-
"post-rewrite",
|
|
4711
5326
|
"pre-auto-gc",
|
|
4712
5327
|
"push-to-checkout",
|
|
4713
5328
|
"sendemail-validate",
|
|
@@ -4717,11 +5332,13 @@ var PASSTHROUGH_HOOKS = [
|
|
|
4717
5332
|
// with a query protocol, not a lifecycle event, so a bare exec stub is wrong.)
|
|
4718
5333
|
];
|
|
4719
5334
|
function globalHookScript(spec) {
|
|
4720
|
-
const chainExit = spec.hookName ===
|
|
4721
|
-
const
|
|
4722
|
-
const
|
|
5335
|
+
const chainExit = spec.hookName === PRE_COMMIT.hookName ? "|| exit $?" : "|| true";
|
|
5336
|
+
const managedBlock = spec.hookName === POST_COMMIT.hookName ? postCommitHookBlock() : spec.hookName === POST_REWRITE.hookName ? postRewriteHookBlock() : void 0;
|
|
5337
|
+
const invocation = managedBlock ?? gatedShim(spec.binName);
|
|
5338
|
+
const managedRepoGuard = managedBlock ? ` && ! grep -Fq '${blockMarkers(spec).start}' "$repo_hook" 2>/dev/null` : "";
|
|
5339
|
+
const beforeComments = managedBlock ? `${invocation}
|
|
4723
5340
|
` : "";
|
|
4724
|
-
const afterComments =
|
|
5341
|
+
const afterComments = managedBlock ? "" : `${invocation}
|
|
4725
5342
|
`;
|
|
4726
5343
|
return `#!/bin/sh
|
|
4727
5344
|
${beforeComments}# prim global ${spec.hookName} hook (core.hooksPath) \u2014 managed by prim; do not edit.
|
|
@@ -4731,7 +5348,7 @@ ${beforeComments}# prim global ${spec.hookName} hook (core.hooksPath) \u2014 man
|
|
|
4731
5348
|
# hook regardless, so inactive repos are unaffected.
|
|
4732
5349
|
${afterComments}common_dir=$(git rev-parse --git-common-dir 2>/dev/null) || exit 0
|
|
4733
5350
|
repo_hook="$common_dir/hooks/${spec.hookName}"
|
|
4734
|
-
if [ -x "$repo_hook" ] && ! grep -q '${PRIM_MANAGED_MARK}' "$repo_hook" 2>/dev/null; then
|
|
5351
|
+
if [ -x "$repo_hook" ] && ! grep -q '${PRIM_MANAGED_MARK}' "$repo_hook" 2>/dev/null${managedRepoGuard}; then
|
|
4735
5352
|
"$repo_hook" "$@" ${chainExit}
|
|
4736
5353
|
fi
|
|
4737
5354
|
exit 0
|
|
@@ -4757,6 +5374,8 @@ function writeOwnHooks() {
|
|
|
4757
5374
|
const path = resolve2(PRIM_GIT_HOOKS_DIR, spec.hookName);
|
|
4758
5375
|
if (spec.hookName === POST_COMMIT.hookName) {
|
|
4759
5376
|
ensurePostCommitHookAtPath(path, globalHookScript(spec));
|
|
5377
|
+
} else if (spec.hookName === POST_REWRITE.hookName) {
|
|
5378
|
+
writeFileSync4(path, globalHookScript(spec), { mode: 493 });
|
|
4760
5379
|
} else {
|
|
4761
5380
|
writeFileSync4(path, globalHookScript(spec), { mode: 493 });
|
|
4762
5381
|
}
|
|
@@ -4770,6 +5389,10 @@ function appendPrimBlock(hookPath, spec) {
|
|
|
4770
5389
|
ensurePostCommitHookAtPath(hookPath);
|
|
4771
5390
|
return;
|
|
4772
5391
|
}
|
|
5392
|
+
if (spec.hookName === POST_REWRITE.hookName) {
|
|
5393
|
+
ensurePostRewriteHookAtPath(hookPath);
|
|
5394
|
+
return;
|
|
5395
|
+
}
|
|
4773
5396
|
mergePrimBlock(hookPath, gatedBlock(spec), spec.binName);
|
|
4774
5397
|
}
|
|
4775
5398
|
function stripPrimBlock(hookPath, spec) {
|
|
@@ -4777,6 +5400,10 @@ function stripPrimBlock(hookPath, spec) {
|
|
|
4777
5400
|
uninstallPostCommitHookAtPath(hookPath);
|
|
4778
5401
|
return;
|
|
4779
5402
|
}
|
|
5403
|
+
if (spec.hookName === POST_REWRITE.hookName) {
|
|
5404
|
+
uninstallPostRewriteHookAtPath(hookPath);
|
|
5405
|
+
return;
|
|
5406
|
+
}
|
|
4780
5407
|
if (!existsSync7(hookPath)) return;
|
|
4781
5408
|
const existing = readFileSync6(hookPath, "utf-8");
|
|
4782
5409
|
const primCreated = existing.includes(PRIM_CREATED_MARK);
|
|
@@ -4856,22 +5483,29 @@ function uninstallGlobalHooks() {
|
|
|
4856
5483
|
console.log("No prim global git hooks found.");
|
|
4857
5484
|
}
|
|
4858
5485
|
function installHooks(gitRoot, target) {
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
installToDotGit(gitRoot, spec);
|
|
4864
|
-
}
|
|
5486
|
+
if (target === "husky") {
|
|
5487
|
+
installToHusky(gitRoot, PRE_COMMIT);
|
|
5488
|
+
} else {
|
|
5489
|
+
installToDotGit(gitRoot, PRE_COMMIT);
|
|
4865
5490
|
}
|
|
4866
5491
|
const postCommit = ensureEffectivePostCommitHook(gitRoot);
|
|
4867
5492
|
console.log(
|
|
4868
5493
|
`${postCommit.changed ? "Installed" : "Refreshed"} effective post-commit hook at ${postCommit.path}.`
|
|
4869
5494
|
);
|
|
5495
|
+
try {
|
|
5496
|
+
const postRewrite = ensureEffectivePostRewriteHook(gitRoot);
|
|
5497
|
+
console.log(
|
|
5498
|
+
`${postRewrite.changed ? "Installed" : "Refreshed"} effective post-rewrite hook at ${postRewrite.path}.`
|
|
5499
|
+
);
|
|
5500
|
+
} catch (error) {
|
|
5501
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
5502
|
+
console.error(`[prim] post-rewrite hook coverage is degraded: ${detail}`);
|
|
5503
|
+
}
|
|
4870
5504
|
}
|
|
4871
5505
|
function registerHooksCommands(program2) {
|
|
4872
5506
|
const hooks = program2.command("hooks").description("Manage git hooks");
|
|
4873
5507
|
hooks.command("install").description(
|
|
4874
|
-
"Install the prim git hooks \u2014 pre-commit + post-commit (auto-detects Husky; use --target to override)"
|
|
5508
|
+
"Install the prim git hooks \u2014 pre-commit + post-commit + post-rewrite (auto-detects Husky; use --target to override)"
|
|
4875
5509
|
).addOption(
|
|
4876
5510
|
new Option3("--target <where>", "install destination; bypasses Husky detection").choices([
|
|
4877
5511
|
"husky",
|
|
@@ -4929,27 +5563,30 @@ function registerHooksCommands(program2) {
|
|
|
4929
5563
|
}
|
|
4930
5564
|
const gitRoot = getGitRoot();
|
|
4931
5565
|
const hooksDir = projectHooksDir(gitRoot);
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
}
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
5566
|
+
const preCommitPath = resolve2(hooksDir, PRE_COMMIT.hookName);
|
|
5567
|
+
if (!existsSync7(preCommitPath)) {
|
|
5568
|
+
console.log(`No ${PRE_COMMIT.hookName} hook found.`);
|
|
5569
|
+
} else if (containsPrimHook(readFileSync6(preCommitPath, "utf-8"), PRE_COMMIT.binName)) {
|
|
5570
|
+
unlinkSync2(preCommitPath);
|
|
5571
|
+
console.log(`Removed ${PRE_COMMIT.hookName} hook at ${preCommitPath}`);
|
|
5572
|
+
} else {
|
|
5573
|
+
console.log(
|
|
5574
|
+
`Left ${PRE_COMMIT.hookName} hook at ${preCommitPath} untouched (not a prim hook).`
|
|
5575
|
+
);
|
|
5576
|
+
}
|
|
5577
|
+
for (const [hookName, uninstall] of [
|
|
5578
|
+
[POST_COMMIT.hookName, uninstallProjectPostCommitHook],
|
|
5579
|
+
[POST_REWRITE.hookName, uninstallProjectPostRewriteHook]
|
|
5580
|
+
]) {
|
|
5581
|
+
const result = uninstall(gitRoot);
|
|
5582
|
+
if (!result.changed) {
|
|
5583
|
+
console.log(`No Prim ${hookName} block found at ${result.path}.`);
|
|
5584
|
+
} else if (result.removedFile) {
|
|
5585
|
+
console.log(`Removed Prim-created ${hookName} hook at ${result.path}.`);
|
|
4941
5586
|
} else {
|
|
4942
|
-
console.log(`
|
|
5587
|
+
console.log(`Removed the Prim ${hookName} block from ${result.path}.`);
|
|
4943
5588
|
}
|
|
4944
5589
|
}
|
|
4945
|
-
const postCommit = uninstallProjectPostCommitHook(gitRoot);
|
|
4946
|
-
if (!postCommit.changed) {
|
|
4947
|
-
console.log(`No Prim post-commit block found at ${postCommit.path}.`);
|
|
4948
|
-
} else if (postCommit.removedFile) {
|
|
4949
|
-
console.log(`Removed Prim-created post-commit hook at ${postCommit.path}.`);
|
|
4950
|
-
} else {
|
|
4951
|
-
console.log(`Removed the Prim post-commit block from ${postCommit.path}.`);
|
|
4952
|
-
}
|
|
4953
5590
|
});
|
|
4954
5591
|
}
|
|
4955
5592
|
|