@arnilo/prism-coding-agent 0.2.4 → 0.2.6
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/CHANGELOG.md +16 -0
- package/README.md +10 -0
- package/dist/coding-checkpoint.js +4 -0
- package/dist/diagnostics.d.ts +83 -0
- package/dist/diagnostics.js +179 -0
- package/dist/git.d.ts +27 -6
- package/dist/git.js +58 -1
- package/dist/index.d.ts +13 -4
- package/dist/index.js +13 -2
- package/dist/language/client.d.ts +25 -0
- package/dist/language/client.js +54 -0
- package/dist/language/framing.d.ts +9 -1
- package/dist/language/framing.js +89 -17
- package/dist/language/index.d.ts +1 -1
- package/dist/language/intelligence.js +58 -0
- package/dist/language/types.d.ts +32 -0
- package/dist/limits.d.ts +59 -0
- package/dist/limits.js +59 -0
- package/dist/process/index.d.ts +4 -1
- package/dist/process/index.js +1 -0
- package/dist/process/recovery.d.ts +174 -0
- package/dist/process/recovery.js +320 -0
- package/dist/process/sessions.js +714 -25
- package/dist/process/types.d.ts +128 -4
- package/dist/process/types.js +7 -1
- package/dist/repository/glob.d.ts +4 -0
- package/dist/repository/glob.js +143 -0
- package/dist/repository/indexed-search.d.ts +121 -0
- package/dist/repository/indexed-search.js +313 -0
- package/dist/repository/list.d.ts +3 -0
- package/dist/repository/list.js +119 -0
- package/dist/repository/operations.d.ts +5 -0
- package/dist/repository/operations.js +14 -0
- package/dist/repository/path.d.ts +18 -0
- package/dist/repository/path.js +91 -0
- package/dist/repository/search.d.ts +9 -0
- package/dist/repository/search.js +284 -0
- package/dist/repository/types.d.ts +138 -0
- package/dist/repository/types.js +31 -0
- package/dist/repository/walk.d.ts +22 -0
- package/dist/repository/walk.js +99 -0
- package/dist/repository.d.ts +11 -172
- package/dist/repository.js +11 -748
- package/dist/review.d.ts +150 -0
- package/dist/review.js +222 -0
- package/dist/search.d.ts +3 -1
- package/dist/search.js +42 -7
- package/dist/workspace-lifecycle.d.ts +153 -0
- package/dist/workspace-lifecycle.js +629 -0
- package/package.json +3 -3
package/dist/review.d.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded patch-review manifest binding review decisions to the exact
|
|
3
|
+
* repository/worktree/base/head identity plus patch digest and artifact
|
|
4
|
+
* revision. Pure helpers compose over the server ArtifactService (attach /
|
|
5
|
+
* approve / reject) — no second approval engine, no raw patch body persisted.
|
|
6
|
+
*/
|
|
7
|
+
export type CodingPatchReviewState = "pending" | "accepted" | "rejected" | "superseded";
|
|
8
|
+
export type ReviewDiagnosticSeverity = "error" | "warning" | "info" | "hint";
|
|
9
|
+
export interface CodingPatchReviewCheckSummary {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly exitCode: number;
|
|
12
|
+
readonly summary: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CodingPatchReviewDiagnosticSummary {
|
|
15
|
+
readonly file: string;
|
|
16
|
+
readonly severity: ReviewDiagnosticSeverity;
|
|
17
|
+
readonly count: number;
|
|
18
|
+
readonly generation: number;
|
|
19
|
+
}
|
|
20
|
+
export interface CodingPatchReviewDiffstatEntry {
|
|
21
|
+
readonly file: string;
|
|
22
|
+
readonly additions: number;
|
|
23
|
+
readonly deletions: number;
|
|
24
|
+
}
|
|
25
|
+
export interface CodingPatchReviewIdentity {
|
|
26
|
+
readonly repositoryId: string;
|
|
27
|
+
/** Credential-free remote fingerprint (GitOperations.fingerprint). */
|
|
28
|
+
readonly remoteFingerprint: string;
|
|
29
|
+
readonly defaultBranch: string;
|
|
30
|
+
/** Relative worktree path inside the approved worktree roots; optional. */
|
|
31
|
+
readonly worktreePath?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface CodingPatchReview {
|
|
34
|
+
readonly schemaVersion: 1;
|
|
35
|
+
readonly reviewId: string;
|
|
36
|
+
readonly state: CodingPatchReviewState;
|
|
37
|
+
readonly threadId: string;
|
|
38
|
+
readonly artifactId: string;
|
|
39
|
+
readonly identity: CodingPatchReviewIdentity;
|
|
40
|
+
readonly base: string;
|
|
41
|
+
readonly head: string;
|
|
42
|
+
readonly patch: {
|
|
43
|
+
readonly kind: "patch" | "bundle" | "diff" | "other";
|
|
44
|
+
readonly uri: string;
|
|
45
|
+
readonly sha256: string;
|
|
46
|
+
readonly bytes: number;
|
|
47
|
+
};
|
|
48
|
+
readonly changedPaths: readonly string[];
|
|
49
|
+
readonly diffstat: readonly CodingPatchReviewDiffstatEntry[];
|
|
50
|
+
readonly checks: readonly CodingPatchReviewCheckSummary[];
|
|
51
|
+
readonly diagnostics: readonly CodingPatchReviewDiagnosticSummary[];
|
|
52
|
+
/** SHA-256 over the canonical manifest JSON — the acceptance binding. */
|
|
53
|
+
readonly digest: string;
|
|
54
|
+
readonly createdAt: string;
|
|
55
|
+
}
|
|
56
|
+
export interface CreateCodingPatchReviewInput {
|
|
57
|
+
readonly threadId: string;
|
|
58
|
+
readonly artifactId: string;
|
|
59
|
+
readonly identity: CodingPatchReviewIdentity;
|
|
60
|
+
readonly base: string;
|
|
61
|
+
readonly head: string;
|
|
62
|
+
readonly patch: CodingPatchReview["patch"];
|
|
63
|
+
readonly changedPaths?: readonly string[];
|
|
64
|
+
readonly diffstat?: readonly CodingPatchReviewDiffstatEntry[];
|
|
65
|
+
readonly checks?: readonly CodingPatchReviewCheckSummary[];
|
|
66
|
+
readonly diagnostics?: readonly CodingPatchReviewDiagnosticSummary[];
|
|
67
|
+
readonly limits?: CodingReviewLimits;
|
|
68
|
+
/** Explicit review id; defaults to a deterministic id from the digest. */
|
|
69
|
+
readonly reviewId?: string;
|
|
70
|
+
/** Explicit ISO-8601 timestamp (deterministic manifests); defaults to now. */
|
|
71
|
+
readonly createdAt?: string;
|
|
72
|
+
}
|
|
73
|
+
export interface CodingReviewLimits {
|
|
74
|
+
readonly maxRevisions?: number;
|
|
75
|
+
readonly maxDiagnostics?: number;
|
|
76
|
+
readonly maxManifestBytes?: number;
|
|
77
|
+
}
|
|
78
|
+
export interface ResolvedCodingReviewLimits {
|
|
79
|
+
readonly maxRevisions: number;
|
|
80
|
+
readonly maxDiagnostics: number;
|
|
81
|
+
readonly maxManifestBytes: number;
|
|
82
|
+
}
|
|
83
|
+
export declare function resolveCodingReviewLimits(options?: CodingReviewLimits): ResolvedCodingReviewLimits;
|
|
84
|
+
/** Structural subset of the server ArtifactAttachInput (thread/uri/hash/preview). */
|
|
85
|
+
export interface CodingReviewArtifactInput {
|
|
86
|
+
readonly threadId: string;
|
|
87
|
+
readonly id: string;
|
|
88
|
+
readonly uri: string;
|
|
89
|
+
readonly mime: string;
|
|
90
|
+
readonly hash: string;
|
|
91
|
+
readonly size?: number;
|
|
92
|
+
readonly title?: string;
|
|
93
|
+
readonly changeNote?: string;
|
|
94
|
+
readonly producerRunId?: string;
|
|
95
|
+
readonly preview?: Readonly<Record<string, unknown>>;
|
|
96
|
+
}
|
|
97
|
+
/** Structural subset of the server ArtifactRecord for acceptance checks. */
|
|
98
|
+
export interface CodingReviewArtifactRecord {
|
|
99
|
+
/** Server artifact id (ArtifactRecord.id); must equal review.artifactId. */
|
|
100
|
+
readonly id: string;
|
|
101
|
+
readonly threadId: string;
|
|
102
|
+
readonly revisions: readonly {
|
|
103
|
+
readonly version: number;
|
|
104
|
+
readonly hash: string;
|
|
105
|
+
readonly uri: string;
|
|
106
|
+
readonly preview?: Readonly<Record<string, unknown>>;
|
|
107
|
+
}[];
|
|
108
|
+
readonly approvals: readonly {
|
|
109
|
+
readonly version: number;
|
|
110
|
+
readonly state: "pending" | "approved" | "rejected";
|
|
111
|
+
readonly reviewer?: string;
|
|
112
|
+
readonly note?: string;
|
|
113
|
+
}[];
|
|
114
|
+
}
|
|
115
|
+
export type CodingPatchReviewErrorCode = "ERR_PRISM_REVIEW_INPUT" | "ERR_PRISM_REVIEW_LIMIT" | "ERR_PRISM_REVIEW_BINDING" | "ERR_PRISM_REVIEW_STATE" | "ERR_PRISM_REVIEW_OWNERSHIP";
|
|
116
|
+
export declare class CodingPatchReviewError extends Error {
|
|
117
|
+
readonly code: CodingPatchReviewErrorCode;
|
|
118
|
+
constructor(code: CodingPatchReviewErrorCode, message: string);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Build the bounded review manifest. Validates every identity field, caps
|
|
122
|
+
* changed paths / diffstat / check summaries / diagnostic summaries, computes
|
|
123
|
+
* the digest over the canonical manifest JSON, and returns the structural
|
|
124
|
+
* artifact input whose `preview.review` embeds the manifest (digest binding).
|
|
125
|
+
* Never embeds a raw patch body, command, env, or secret.
|
|
126
|
+
*/
|
|
127
|
+
export declare function createCodingPatchReviewManifest(input: CreateCodingPatchReviewInput): {
|
|
128
|
+
review: CodingPatchReview;
|
|
129
|
+
artifactInput: CodingReviewArtifactInput;
|
|
130
|
+
};
|
|
131
|
+
export interface AssertCodingPatchAcceptedInput {
|
|
132
|
+
/** Fresh manifest whose state is being derived; must match the artifact binding. */
|
|
133
|
+
readonly review: CodingPatchReview;
|
|
134
|
+
/** Server ArtifactRecord (structurally compatible with @arnilo/prism-server ArtifactRecord). */
|
|
135
|
+
readonly artifact: CodingReviewArtifactRecord;
|
|
136
|
+
}
|
|
137
|
+
export interface AssertCodingPatchAcceptedResult {
|
|
138
|
+
readonly state: CodingPatchReviewState;
|
|
139
|
+
readonly version: number;
|
|
140
|
+
readonly reviewer?: string;
|
|
141
|
+
readonly reason?: string;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Derive the review state from the artifact record. The binding is exact:
|
|
145
|
+
* review digest + patch digest + artifact revision + workspace identity must
|
|
146
|
+
* all match the recorded preview; any patch/repository/worktree/base/head
|
|
147
|
+
* change since approval surfaces as `superseded` — never a silent accept.
|
|
148
|
+
* Acceptance never applies, commits, pushes, or merges.
|
|
149
|
+
*/
|
|
150
|
+
export declare function assertCodingPatchAccepted(input: AssertCodingPatchAcceptedInput): AssertCodingPatchAcceptedResult;
|
package/dist/review.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded patch-review manifest binding review decisions to the exact
|
|
3
|
+
* repository/worktree/base/head identity plus patch digest and artifact
|
|
4
|
+
* revision. Pure helpers compose over the server ArtifactService (attach /
|
|
5
|
+
* approve / reject) — no second approval engine, no raw patch body persisted.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { DEFAULT_MAX_REVIEW_DIAGNOSTICS, DEFAULT_MAX_REVIEW_MANIFEST_BYTES, DEFAULT_MAX_REVIEW_REVISIONS, HARD_MAX_REVIEW_DIAGNOSTICS, HARD_MAX_REVIEW_MANIFEST_BYTES, HARD_MAX_REVIEW_REVISIONS, validateCodingLimit, } from "./limits.js";
|
|
9
|
+
export function resolveCodingReviewLimits(options) {
|
|
10
|
+
return {
|
|
11
|
+
maxRevisions: validateCodingLimit("maxRevisions", options?.maxRevisions ?? DEFAULT_MAX_REVIEW_REVISIONS, HARD_MAX_REVIEW_REVISIONS),
|
|
12
|
+
maxDiagnostics: validateCodingLimit("maxDiagnostics", options?.maxDiagnostics ?? DEFAULT_MAX_REVIEW_DIAGNOSTICS, HARD_MAX_REVIEW_DIAGNOSTICS),
|
|
13
|
+
maxManifestBytes: validateCodingLimit("maxManifestBytes", options?.maxManifestBytes ?? DEFAULT_MAX_REVIEW_MANIFEST_BYTES, HARD_MAX_REVIEW_MANIFEST_BYTES),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export class CodingPatchReviewError extends Error {
|
|
17
|
+
code;
|
|
18
|
+
constructor(code, message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "CodingPatchReviewError";
|
|
21
|
+
this.code = code;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
25
|
+
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
26
|
+
const REF_PATTERN = /^[^\s]{1,255}$/;
|
|
27
|
+
const CONTROL_PATTERN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g;
|
|
28
|
+
/**
|
|
29
|
+
* Build the bounded review manifest. Validates every identity field, caps
|
|
30
|
+
* changed paths / diffstat / check summaries / diagnostic summaries, computes
|
|
31
|
+
* the digest over the canonical manifest JSON, and returns the structural
|
|
32
|
+
* artifact input whose `preview.review` embeds the manifest (digest binding).
|
|
33
|
+
* Never embeds a raw patch body, command, env, or secret.
|
|
34
|
+
*/
|
|
35
|
+
export function createCodingPatchReviewManifest(input) {
|
|
36
|
+
const limits = resolveCodingReviewLimits(input.limits);
|
|
37
|
+
if (!ID_PATTERN.test(input.threadId)) {
|
|
38
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", "threadId must match [A-Za-z0-9][A-Za-z0-9._:-]*");
|
|
39
|
+
}
|
|
40
|
+
if (!ID_PATTERN.test(input.artifactId)) {
|
|
41
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", "artifactId must match [A-Za-z0-9][A-Za-z0-9._:-]*");
|
|
42
|
+
}
|
|
43
|
+
if (!ID_PATTERN.test(input.identity.repositoryId)) {
|
|
44
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", "repositoryId must match [A-Za-z0-9][A-Za-z0-9._:-]*");
|
|
45
|
+
}
|
|
46
|
+
if (!HEX64.test(input.identity.remoteFingerprint)) {
|
|
47
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", "remoteFingerprint must be a 64-hex sha256");
|
|
48
|
+
}
|
|
49
|
+
if (!REF_PATTERN.test(input.identity.defaultBranch) || input.identity.defaultBranch.includes("..")) {
|
|
50
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", "defaultBranch is invalid");
|
|
51
|
+
}
|
|
52
|
+
if (input.identity.worktreePath !== undefined) {
|
|
53
|
+
assertRelativePath(input.identity.worktreePath, "worktreePath");
|
|
54
|
+
}
|
|
55
|
+
if (!REF_PATTERN.test(input.base) || input.base.includes("..")) {
|
|
56
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", "base is invalid");
|
|
57
|
+
}
|
|
58
|
+
if (!REF_PATTERN.test(input.head) || input.head.includes("..")) {
|
|
59
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", "head is invalid");
|
|
60
|
+
}
|
|
61
|
+
if (!HEX64.test(input.patch.sha256)) {
|
|
62
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", "patch.sha256 must be a 64-hex sha256");
|
|
63
|
+
}
|
|
64
|
+
if (!Number.isSafeInteger(input.patch.bytes) || input.patch.bytes < 0) {
|
|
65
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", "patch.bytes must be a non-negative safe integer");
|
|
66
|
+
}
|
|
67
|
+
if (input.patch.uri.length > 2_048 || CONTROL_PATTERN.test(input.patch.uri)) {
|
|
68
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", "patch.uri exceeds 2048 bytes or contains control characters");
|
|
69
|
+
}
|
|
70
|
+
const changedPaths = (input.changedPaths ?? []).map((path) => assertRelativePath(path, "changedPaths"));
|
|
71
|
+
const diffstat = (input.diffstat ?? []).map((entry) => {
|
|
72
|
+
const file = assertRelativePath(entry.file, "diffstat");
|
|
73
|
+
if (!Number.isSafeInteger(entry.additions) || entry.additions < 0 || !Number.isSafeInteger(entry.deletions) || entry.deletions < 0) {
|
|
74
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", "diffstat counts must be non-negative safe integers");
|
|
75
|
+
}
|
|
76
|
+
return { file, additions: entry.additions, deletions: entry.deletions };
|
|
77
|
+
});
|
|
78
|
+
const checks = (input.checks ?? []).map((check) => {
|
|
79
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(check.name)) {
|
|
80
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", `check name is invalid: ${check.name}`);
|
|
81
|
+
}
|
|
82
|
+
if (!Number.isInteger(check.exitCode)) {
|
|
83
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", `check ${check.name} exitCode must be an integer`);
|
|
84
|
+
}
|
|
85
|
+
const summary = String(check.summary).replace(CONTROL_PATTERN, "");
|
|
86
|
+
if (Buffer.byteLength(summary, "utf8") > 8_192) {
|
|
87
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_LIMIT", `check ${check.name} summary exceeds 8192 bytes`);
|
|
88
|
+
}
|
|
89
|
+
return { name: check.name, exitCode: check.exitCode, summary };
|
|
90
|
+
});
|
|
91
|
+
const diagnostics = (input.diagnostics ?? []).map((diag) => {
|
|
92
|
+
const file = assertRelativePath(diag.file, "diagnostics");
|
|
93
|
+
if (diag.severity !== "error" && diag.severity !== "warning" && diag.severity !== "info" && diag.severity !== "hint") {
|
|
94
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", `diagnostic severity is invalid: ${String(diag.severity)}`);
|
|
95
|
+
}
|
|
96
|
+
if (!Number.isSafeInteger(diag.count) || diag.count < 0 || !Number.isSafeInteger(diag.generation) || diag.generation < 0) {
|
|
97
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", "diagnostic count/generation must be non-negative safe integers");
|
|
98
|
+
}
|
|
99
|
+
return { file, severity: diag.severity, count: diag.count, generation: diag.generation };
|
|
100
|
+
});
|
|
101
|
+
if (changedPaths.length > limits.maxRevisions * 250) {
|
|
102
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_LIMIT", `changedPaths exceed the bounded manifest budget (${limits.maxRevisions * 250})`);
|
|
103
|
+
}
|
|
104
|
+
if (checks.length > limits.maxRevisions) {
|
|
105
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_LIMIT", `checks exceed ${limits.maxRevisions}`);
|
|
106
|
+
}
|
|
107
|
+
if (diffstat.length > limits.maxRevisions * 250) {
|
|
108
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_LIMIT", `diffstat exceeds the bounded manifest budget (${limits.maxRevisions * 250})`);
|
|
109
|
+
}
|
|
110
|
+
if (diagnostics.length > limits.maxDiagnostics) {
|
|
111
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_LIMIT", `diagnostic summaries exceed ${limits.maxDiagnostics}`);
|
|
112
|
+
}
|
|
113
|
+
if (input.reviewId !== undefined && !ID_PATTERN.test(input.reviewId)) {
|
|
114
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", "reviewId must match [A-Za-z0-9][A-Za-z0-9._:-]*");
|
|
115
|
+
}
|
|
116
|
+
const body = {
|
|
117
|
+
schemaVersion: 1,
|
|
118
|
+
reviewId: "",
|
|
119
|
+
state: "pending",
|
|
120
|
+
threadId: input.threadId,
|
|
121
|
+
artifactId: input.artifactId,
|
|
122
|
+
identity: input.identity,
|
|
123
|
+
base: input.base,
|
|
124
|
+
head: input.head,
|
|
125
|
+
patch: input.patch,
|
|
126
|
+
changedPaths,
|
|
127
|
+
diffstat,
|
|
128
|
+
checks,
|
|
129
|
+
diagnostics,
|
|
130
|
+
};
|
|
131
|
+
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
132
|
+
if (typeof createdAt !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(createdAt)) {
|
|
133
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", "createdAt must be an ISO-8601 UTC timestamp");
|
|
134
|
+
}
|
|
135
|
+
const digest = sha256Hex(Buffer.from(JSON.stringify({ ...body, createdAt }), "utf8"));
|
|
136
|
+
const reviewId = input.reviewId ?? `review-${digest.slice(0, 24)}`;
|
|
137
|
+
const review = { ...body, reviewId, digest, createdAt };
|
|
138
|
+
const manifestJson = JSON.stringify(review);
|
|
139
|
+
if (Buffer.byteLength(manifestJson, "utf8") > limits.maxManifestBytes) {
|
|
140
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_LIMIT", `review manifest exceeds ${limits.maxManifestBytes} bytes`);
|
|
141
|
+
}
|
|
142
|
+
const artifactInput = {
|
|
143
|
+
threadId: input.threadId,
|
|
144
|
+
id: input.artifactId,
|
|
145
|
+
uri: input.patch.uri,
|
|
146
|
+
mime: `application/x-${input.patch.kind}`,
|
|
147
|
+
hash: input.patch.sha256,
|
|
148
|
+
size: input.patch.bytes,
|
|
149
|
+
title: `Coding patch review ${reviewId}`,
|
|
150
|
+
changeNote: `head ${input.head} over base ${input.base}`,
|
|
151
|
+
preview: {
|
|
152
|
+
review,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
return { review, artifactInput };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Derive the review state from the artifact record. The binding is exact:
|
|
159
|
+
* review digest + patch digest + artifact revision + workspace identity must
|
|
160
|
+
* all match the recorded preview; any patch/repository/worktree/base/head
|
|
161
|
+
* change since approval surfaces as `superseded` — never a silent accept.
|
|
162
|
+
* Acceptance never applies, commits, pushes, or merges.
|
|
163
|
+
*/
|
|
164
|
+
export function assertCodingPatchAccepted(input) {
|
|
165
|
+
const { review, artifact } = input;
|
|
166
|
+
if (artifact.threadId !== review.threadId || artifact.id !== review.artifactId) {
|
|
167
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_OWNERSHIP", "artifact does not belong to this review thread");
|
|
168
|
+
}
|
|
169
|
+
const bound = artifact.revisions.find((revision) => revision.hash === review.patch.sha256);
|
|
170
|
+
if (!bound) {
|
|
171
|
+
return { state: "superseded", version: 0, reason: "no artifact revision matches the patch digest" };
|
|
172
|
+
}
|
|
173
|
+
const latestVersion = artifact.revisions.reduce((max, revision) => Math.max(max, revision.version), 0);
|
|
174
|
+
if (bound.version < latestVersion) {
|
|
175
|
+
return { state: "superseded", version: bound.version, reason: "a newer patch revision supersedes this acceptance" };
|
|
176
|
+
}
|
|
177
|
+
const preview = bound.preview?.review;
|
|
178
|
+
if (!preview || typeof preview !== "object") {
|
|
179
|
+
return { state: "superseded", version: bound.version, reason: "artifact revision carries no review binding" };
|
|
180
|
+
}
|
|
181
|
+
if (preview.digest !== review.digest) {
|
|
182
|
+
return {
|
|
183
|
+
state: "superseded",
|
|
184
|
+
version: bound.version,
|
|
185
|
+
reason: "review digest changed after the decision (patch, identity, base, or head changed)",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (preview.identity?.repositoryId !== review.identity.repositoryId ||
|
|
189
|
+
preview.identity?.remoteFingerprint !== review.identity.remoteFingerprint ||
|
|
190
|
+
preview.identity?.defaultBranch !== review.identity.defaultBranch ||
|
|
191
|
+
preview.base !== review.base ||
|
|
192
|
+
preview.head !== review.head) {
|
|
193
|
+
return { state: "superseded", version: bound.version, reason: "repository/worktree/base/head identity changed after the decision" };
|
|
194
|
+
}
|
|
195
|
+
const decision = artifact.approvals.find((approval) => approval.version === bound.version);
|
|
196
|
+
if (!decision) {
|
|
197
|
+
return { state: "pending", version: bound.version, reason: "no decision recorded for the bound revision" };
|
|
198
|
+
}
|
|
199
|
+
if (decision.state === "rejected") {
|
|
200
|
+
return { state: "rejected", version: bound.version, reviewer: decision.reviewer, reason: decision.note };
|
|
201
|
+
}
|
|
202
|
+
if (decision.state !== "approved") {
|
|
203
|
+
return { state: "pending", version: bound.version, reason: "decision for the bound revision is not approved" };
|
|
204
|
+
}
|
|
205
|
+
return { state: "accepted", version: bound.version, reviewer: decision.reviewer };
|
|
206
|
+
}
|
|
207
|
+
function assertRelativePath(path, field) {
|
|
208
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
209
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", `${field} must be a non-empty string`);
|
|
210
|
+
}
|
|
211
|
+
if (path.includes("\0") || path.startsWith("/") || path.startsWith("\\") || /^[A-Za-z]:/.test(path)) {
|
|
212
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", `${field} must be workspace-relative: ${path}`);
|
|
213
|
+
}
|
|
214
|
+
if (path === ".." || path.startsWith("../") || path.split("/").includes("..")) {
|
|
215
|
+
throw new CodingPatchReviewError("ERR_PRISM_REVIEW_INPUT", `${field} escapes the workspace: ${path}`);
|
|
216
|
+
}
|
|
217
|
+
return path;
|
|
218
|
+
}
|
|
219
|
+
function sha256Hex(data) {
|
|
220
|
+
return createHash("sha256").update(data).digest("hex");
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=review.js.map
|
package/dist/search.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `repo_search` tool: bounded native literal repository text search.
|
|
3
3
|
*/
|
|
4
4
|
import type { ExecutionPolicy, ToolDefinition } from "@arnilo/prism";
|
|
5
|
-
import { type RepositoryLimitOptions, type RepositoryOperations } from "./repository.js";
|
|
5
|
+
import { type RepoSearchMode, type RepositoryLimitOptions, type RepositoryOperations } from "./repository.js";
|
|
6
6
|
export interface SearchToolOptions {
|
|
7
7
|
executionPolicy?: ExecutionPolicy;
|
|
8
8
|
operations?: RepositoryOperations;
|
|
@@ -10,5 +10,7 @@ export interface SearchToolOptions {
|
|
|
10
10
|
maxMatches?: number;
|
|
11
11
|
maxContextLines?: number;
|
|
12
12
|
exclude?: readonly string[];
|
|
13
|
+
/** Modes the tool exposes (default `["literal"]`); indexed modes require a host index via `operations`. */
|
|
14
|
+
modes?: readonly RepoSearchMode[];
|
|
13
15
|
}
|
|
14
16
|
export declare function createRepoSearchTool(cwd: string, options?: SearchToolOptions): ToolDefinition;
|
package/dist/search.js
CHANGED
|
@@ -3,6 +3,11 @@ import { enforceExecutionPolicy } from "./execution-policy.js";
|
|
|
3
3
|
import { HARD_MAX_SEARCH_CONTEXT_LINES, HARD_MAX_SEARCH_MATCHES, validateCodingLimit, validateCodingLimitAllowZero } from "./limits.js";
|
|
4
4
|
import { createLocalRepositoryOperations, RepositoryError, resolveRepositoryLimits, } from "./repository.js";
|
|
5
5
|
import { truncateLine } from "./truncate.js";
|
|
6
|
+
const ALL_MODES = ["literal", "indexed_literal", "semantic"];
|
|
7
|
+
const INDEX_MODE_LABEL = {
|
|
8
|
+
indexed_literal: "host-indexed literal match with relevance scores",
|
|
9
|
+
semantic: "host semantic search (requires a semantic-capable index backend)",
|
|
10
|
+
};
|
|
6
11
|
function errorResult(toolCallId, message) {
|
|
7
12
|
return {
|
|
8
13
|
toolCallId,
|
|
@@ -18,7 +23,8 @@ function formatMatch(match) {
|
|
|
18
23
|
lines.push(`${match.path}-${text}`);
|
|
19
24
|
}
|
|
20
25
|
const { text } = truncateLine(match.text, 500);
|
|
21
|
-
|
|
26
|
+
const score = match.score !== undefined ? ` [score ${match.score.toFixed(3)}]` : "";
|
|
27
|
+
lines.push(`${match.path}:${match.line}:${match.column}:${text}${score}`);
|
|
22
28
|
for (const after of match.after) {
|
|
23
29
|
const truncated = truncateLine(after, 500);
|
|
24
30
|
lines.push(`${match.path}+${truncated.text}`);
|
|
@@ -87,11 +93,35 @@ function buildSearchMetadata(result, outputMode) {
|
|
|
87
93
|
filesSkippedBinary: result.filesSkippedBinary,
|
|
88
94
|
filesSkippedOversize: result.filesSkippedOversize,
|
|
89
95
|
};
|
|
96
|
+
if (result.indexed !== undefined) {
|
|
97
|
+
base.untrusted_index = result.untrusted_index === true;
|
|
98
|
+
base.indexMode = result.indexed.mode;
|
|
99
|
+
base.indexState = result.indexed.state;
|
|
100
|
+
if (result.indexed.sourceRevision !== undefined)
|
|
101
|
+
base.indexRevision = result.indexed.sourceRevision;
|
|
102
|
+
if (result.indexed.updatedAt !== undefined)
|
|
103
|
+
base.indexUpdatedAt = result.indexed.updatedAt;
|
|
104
|
+
}
|
|
90
105
|
if (outputMode === "content") {
|
|
91
106
|
return { ...base, matches: result.matches };
|
|
92
107
|
}
|
|
93
108
|
return { ...base, fileCount: uniqueMatchPaths(result.matches).length };
|
|
94
109
|
}
|
|
110
|
+
function validateModes(modes) {
|
|
111
|
+
if (modes === undefined)
|
|
112
|
+
return ["literal"];
|
|
113
|
+
if (!Array.isArray(modes) || modes.length === 0)
|
|
114
|
+
return ["literal"];
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
for (const mode of modes) {
|
|
117
|
+
if (!ALL_MODES.includes(mode)) {
|
|
118
|
+
throw new Error(`unsupported search mode in options.modes: ${String(mode)}`);
|
|
119
|
+
}
|
|
120
|
+
seen.add(mode);
|
|
121
|
+
}
|
|
122
|
+
seen.add("literal"); // literal always stays available
|
|
123
|
+
return [...seen].sort((a, b) => ALL_MODES.indexOf(a) - ALL_MODES.indexOf(b));
|
|
124
|
+
}
|
|
95
125
|
export function createRepoSearchTool(cwd, options) {
|
|
96
126
|
const limits = resolveRepositoryLimits({
|
|
97
127
|
...options?.repository,
|
|
@@ -100,10 +130,15 @@ export function createRepoSearchTool(cwd, options) {
|
|
|
100
130
|
exclude: options?.exclude ?? options?.repository?.exclude,
|
|
101
131
|
});
|
|
102
132
|
const ops = options?.operations ?? createLocalRepositoryOperations(limits);
|
|
133
|
+
const modes = validateModes(options?.modes);
|
|
134
|
+
const modeDescriptions = modes.map((m) => (m === "literal" ? "literal substring match only" : INDEX_MODE_LABEL[m])).join("; ");
|
|
135
|
+
const indexedEnabled = modes.some((m) => m !== "literal");
|
|
103
136
|
return {
|
|
104
137
|
name: "repo_search",
|
|
105
138
|
effect: CODING_OBSERVATION_EFFECT,
|
|
106
|
-
description:
|
|
139
|
+
description: indexedEnabled
|
|
140
|
+
? `Search text files under the workspace. Modes: ${modeDescriptions}. Index results are untrusted host index output and may be stale; verify before mutation. Skips binary files and excluded basenames (default: ${limits.exclude.join(", ")}). Caps matches/scanned bytes/time.`
|
|
141
|
+
: `Search text files under the workspace using literal substring match. Use outputMode "files_with_matches" for paths only or "count" for totals without line bodies. Skips binary files, excluded basenames (default: ${limits.exclude.join(", ")}), and hidden names unless includeHidden is true. Does not follow symlinks. Caps matches/scanned bytes/time.`,
|
|
107
142
|
parameters: {
|
|
108
143
|
type: "object",
|
|
109
144
|
properties: {
|
|
@@ -114,8 +149,8 @@ export function createRepoSearchTool(cwd, options) {
|
|
|
114
149
|
},
|
|
115
150
|
mode: {
|
|
116
151
|
type: "string",
|
|
117
|
-
description:
|
|
118
|
-
enum: [
|
|
152
|
+
description: `Search mode: ${modeDescriptions}`,
|
|
153
|
+
enum: [...modes],
|
|
119
154
|
},
|
|
120
155
|
caseSensitive: {
|
|
121
156
|
type: "boolean",
|
|
@@ -153,10 +188,10 @@ export function createRepoSearchTool(cwd, options) {
|
|
|
153
188
|
if (args.mode === "regex") {
|
|
154
189
|
return errorResult(toolCallId, 'repo_search no longer supports mode "regex"; use literal substring search.');
|
|
155
190
|
}
|
|
156
|
-
if (args.mode !== undefined && args.mode
|
|
157
|
-
return errorResult(toolCallId, `unsupported search mode: ${String(args.mode)}`);
|
|
191
|
+
if (args.mode !== undefined && !modes.includes(args.mode)) {
|
|
192
|
+
return errorResult(toolCallId, `unsupported search mode: ${String(args.mode)} (enabled: ${modes.join(", ")})`);
|
|
158
193
|
}
|
|
159
|
-
const mode = "literal";
|
|
194
|
+
const mode = (args.mode ?? "literal");
|
|
160
195
|
let outputMode;
|
|
161
196
|
try {
|
|
162
197
|
outputMode = parseOutputMode(args.outputMode);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { ArtifactReference, GitOperations } from "./git.js";
|
|
2
|
+
/** Separate versioned namespace: never collides with coding checkpoint v1 keys. */
|
|
3
|
+
export declare const WORKSPACE_NAMESPACE: "prism.coding-agent.workspace.v1";
|
|
4
|
+
export declare const WORKSPACE_SCHEMA_VERSION: 1;
|
|
5
|
+
export declare const WORKSPACE_LOCK_REASON_PREFIX: "prism-workspace:";
|
|
6
|
+
/** Frozen workspace state machine: active | cleaning | closed | unknown. */
|
|
7
|
+
export type WorkspaceState = "active" | "cleaning" | "closed" | "unknown";
|
|
8
|
+
export declare const WORKSPACE_STATES: readonly WorkspaceState[];
|
|
9
|
+
export type WorkspaceRepositoryState = "active" | "removed" | "unknown";
|
|
10
|
+
export type WorkspaceErrorCode = "ERR_PRISM_WORKSPACE_UNKNOWN" | "ERR_PRISM_WORKSPACE_LIMIT" | "ERR_PRISM_WORKSPACE_OWNERSHIP" | "ERR_PRISM_WORKSPACE_FENCE" | "ERR_PRISM_WORKSPACE_DIRTY" | "ERR_PRISM_WORKSPACE_LOCKED" | "ERR_PRISM_WORKSPACE_MAIN" | "ERR_PRISM_WORKSPACE_PATH_ESCAPE" | "ERR_PRISM_WORKSPACE_FINGERPRINT";
|
|
11
|
+
export declare class WorkspaceError extends Error {
|
|
12
|
+
readonly code: WorkspaceErrorCode;
|
|
13
|
+
constructor(code: WorkspaceErrorCode, message: string);
|
|
14
|
+
}
|
|
15
|
+
/** Host-approved repository registration; `git` must be cwd-bound to `root`. */
|
|
16
|
+
export interface WorkspaceRepositoryRegistration {
|
|
17
|
+
readonly root: string;
|
|
18
|
+
readonly git: GitOperations;
|
|
19
|
+
}
|
|
20
|
+
/** Durable per-repository leg of a workspace record. */
|
|
21
|
+
export interface WorkspaceRepositoryRecord {
|
|
22
|
+
readonly repositoryId: string;
|
|
23
|
+
/** Canonical absolute root; the main worktree is immutable. */
|
|
24
|
+
readonly root: string;
|
|
25
|
+
/** Credential-free remote fingerprint (sha256 hex), never a URL. */
|
|
26
|
+
readonly remoteFingerprint: string;
|
|
27
|
+
readonly defaultBranch?: string;
|
|
28
|
+
readonly branch: string;
|
|
29
|
+
/** Branch base at create time. */
|
|
30
|
+
readonly base: string;
|
|
31
|
+
/** Verified head at create/last verify. */
|
|
32
|
+
readonly head: string;
|
|
33
|
+
readonly worktreeId: string;
|
|
34
|
+
readonly worktreePath: string;
|
|
35
|
+
readonly state: WorkspaceRepositoryState;
|
|
36
|
+
readonly createdAt: string;
|
|
37
|
+
}
|
|
38
|
+
/** Durable workspace record (schemaVersion 1, namespace WORKSPACE_NAMESPACE). */
|
|
39
|
+
export interface CodingWorkspaceRecord {
|
|
40
|
+
readonly schemaVersion: typeof WORKSPACE_SCHEMA_VERSION;
|
|
41
|
+
readonly workspaceId: string;
|
|
42
|
+
readonly taskId: string;
|
|
43
|
+
readonly ownerId: string;
|
|
44
|
+
readonly state: WorkspaceState;
|
|
45
|
+
readonly repositories: readonly WorkspaceRepositoryRecord[];
|
|
46
|
+
/** Artifact references only; never artifact contents or credentials. */
|
|
47
|
+
readonly artifactRefs: readonly ArtifactReference[];
|
|
48
|
+
/** Lease fencing token at last mutation; monotonic per record. */
|
|
49
|
+
readonly fencingToken: number;
|
|
50
|
+
readonly createdAt: string;
|
|
51
|
+
readonly updatedAt: string;
|
|
52
|
+
readonly cleanupAt?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface WorkspaceCreateRequest {
|
|
55
|
+
readonly taskId: string;
|
|
56
|
+
readonly repositories: readonly {
|
|
57
|
+
readonly repositoryId: string;
|
|
58
|
+
readonly branch: string;
|
|
59
|
+
}[];
|
|
60
|
+
readonly artifactRefs?: readonly ArtifactReference[];
|
|
61
|
+
readonly signal?: AbortSignal;
|
|
62
|
+
}
|
|
63
|
+
/** Host policy gates the documented cleanup refusals; all default to refuse. */
|
|
64
|
+
export interface WorkspaceCleanupPolicy {
|
|
65
|
+
/** Allow forced removal of dirty worktrees (potential data loss). */
|
|
66
|
+
readonly allowDirtyCleanup?: boolean;
|
|
67
|
+
/** Allow unlocking worktrees locked by an external actor. */
|
|
68
|
+
readonly allowLockedCleanup?: boolean;
|
|
69
|
+
/** Allow claiming a missing worktree as removed. */
|
|
70
|
+
readonly allowMissingCleanup?: boolean;
|
|
71
|
+
/** Allow unclaiming a path that exists but is not a registered worktree. */
|
|
72
|
+
readonly allowUnownedCleanup?: boolean;
|
|
73
|
+
/** Allow forced removal when the worktree head no longer matches the record. */
|
|
74
|
+
readonly allowMismatchedCleanup?: boolean;
|
|
75
|
+
}
|
|
76
|
+
export interface WorkspaceLimitOptions {
|
|
77
|
+
readonly maxRepositories?: number;
|
|
78
|
+
readonly maxWorktrees?: number;
|
|
79
|
+
readonly maxRecordBytes?: number;
|
|
80
|
+
readonly leaseTtlMs?: number;
|
|
81
|
+
readonly maxCleanupOperations?: number;
|
|
82
|
+
}
|
|
83
|
+
export interface ResolvedWorkspaceLimits {
|
|
84
|
+
readonly maxRepositories: number;
|
|
85
|
+
readonly maxWorktrees: number;
|
|
86
|
+
readonly maxRecordBytes: number;
|
|
87
|
+
readonly leaseTtlMs: number;
|
|
88
|
+
readonly maxCleanupOperations: number;
|
|
89
|
+
}
|
|
90
|
+
export declare function resolveWorkspaceLimits(options?: WorkspaceLimitOptions): ResolvedWorkspaceLimits;
|
|
91
|
+
export interface CreateCodingWorkspaceLifecycleOptions {
|
|
92
|
+
readonly checkpoints: import("@arnilo/prism").CheckpointStore;
|
|
93
|
+
readonly leases: import("@arnilo/prism").LeaseStore;
|
|
94
|
+
/** Replica/worker identity; part of the fencing trust boundary. */
|
|
95
|
+
readonly ownerId: string;
|
|
96
|
+
readonly ownership?: import("@arnilo/prism").OwnershipScope;
|
|
97
|
+
/** Host-approved repositories keyed by repositoryId. */
|
|
98
|
+
readonly repositories: Readonly<Record<string, WorkspaceRepositoryRegistration>>;
|
|
99
|
+
/** Host-approved linked-worktree destination roots (canonicalized). */
|
|
100
|
+
readonly worktreeRoots: readonly string[];
|
|
101
|
+
readonly policy?: WorkspaceCleanupPolicy;
|
|
102
|
+
readonly limits?: WorkspaceLimitOptions;
|
|
103
|
+
}
|
|
104
|
+
export interface CodingWorkspaceLifecycle {
|
|
105
|
+
/**
|
|
106
|
+
* Create linked worktrees for a task and persist the workspace record.
|
|
107
|
+
* Idempotent: an identical active record returns as-is (no Git mutation).
|
|
108
|
+
* Stale or conflicting workers fail with ERR_PRISM_WORKSPACE_FENCE.
|
|
109
|
+
*/
|
|
110
|
+
create(request: WorkspaceCreateRequest): Promise<CodingWorkspaceRecord>;
|
|
111
|
+
get(input: {
|
|
112
|
+
readonly taskId: string;
|
|
113
|
+
readonly signal?: AbortSignal;
|
|
114
|
+
}): Promise<CodingWorkspaceRecord | null>;
|
|
115
|
+
list(input?: {
|
|
116
|
+
readonly cursor?: string;
|
|
117
|
+
readonly limit?: number;
|
|
118
|
+
readonly signal?: AbortSignal;
|
|
119
|
+
}): Promise<{
|
|
120
|
+
readonly items: readonly CodingWorkspaceRecord[];
|
|
121
|
+
readonly nextCursor?: string;
|
|
122
|
+
}>;
|
|
123
|
+
/**
|
|
124
|
+
* Resume gate: revalidates repository/worktree identity and fingerprints
|
|
125
|
+
* (root containment, worktree presence, head, remote/default-branch) before
|
|
126
|
+
* tools, processes, index results, patches, or artifacts are reused.
|
|
127
|
+
*/
|
|
128
|
+
verify(input: {
|
|
129
|
+
readonly taskId: string;
|
|
130
|
+
readonly signal?: AbortSignal;
|
|
131
|
+
}): Promise<CodingWorkspaceRecord>;
|
|
132
|
+
attachArtifacts(input: {
|
|
133
|
+
readonly taskId: string;
|
|
134
|
+
readonly artifactRefs: readonly ArtifactReference[];
|
|
135
|
+
readonly signal?: AbortSignal;
|
|
136
|
+
}): Promise<CodingWorkspaceRecord>;
|
|
137
|
+
/**
|
|
138
|
+
* Remove owned linked worktrees and close the record. Refuses dirty, locked,
|
|
139
|
+
* unowned, missing, or mismatched trees unless the host policy allows the
|
|
140
|
+
* documented action; partial failure persists state `unknown` and remains
|
|
141
|
+
* reconcilable by retrying.
|
|
142
|
+
*/
|
|
143
|
+
cleanup(input: {
|
|
144
|
+
readonly taskId: string;
|
|
145
|
+
readonly signal?: AbortSignal;
|
|
146
|
+
}): Promise<CodingWorkspaceRecord>;
|
|
147
|
+
/** Delete the durable record (no Git mutation); false when absent. */
|
|
148
|
+
remove(input: {
|
|
149
|
+
readonly taskId: string;
|
|
150
|
+
readonly signal?: AbortSignal;
|
|
151
|
+
}): Promise<boolean>;
|
|
152
|
+
}
|
|
153
|
+
export declare function createCodingWorkspaceLifecycle(options: CreateCodingWorkspaceLifecycleOptions): CodingWorkspaceLifecycle;
|