@arnilo/prism-coding-agent 0.2.5 → 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/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/indexed-search.d.ts +121 -0
- package/dist/repository/indexed-search.js +313 -0
- package/dist/repository/types.d.ts +14 -2
- package/dist/repository.d.ts +1 -0
- package/dist/repository.js +1 -0
- 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
|
@@ -22,6 +22,7 @@ export interface RepositoryGlobResult {
|
|
|
22
22
|
readonly nextOffset?: number;
|
|
23
23
|
readonly offset: number;
|
|
24
24
|
}
|
|
25
|
+
export type RepoSearchMode = "literal" | "indexed_literal" | "semantic";
|
|
25
26
|
export type RepoSearchOutputMode = "content" | "files_with_matches" | "count";
|
|
26
27
|
export interface RepositorySearchMatch {
|
|
27
28
|
readonly path: string;
|
|
@@ -30,16 +31,27 @@ export interface RepositorySearchMatch {
|
|
|
30
31
|
readonly text: string;
|
|
31
32
|
readonly before: readonly string[];
|
|
32
33
|
readonly after: readonly string[];
|
|
34
|
+
/** Host index relevance in [0,1]; present only for indexed_literal/semantic results. */
|
|
35
|
+
readonly score?: number;
|
|
33
36
|
}
|
|
34
37
|
export interface RepositorySearchResult {
|
|
35
38
|
readonly matches: readonly RepositorySearchMatch[];
|
|
36
39
|
readonly truncated: boolean;
|
|
37
|
-
readonly truncatedBy: "matches" | "scan" | "file" | "entries" | "files" | "depth" | "time" | "abort" | "pattern" | null;
|
|
40
|
+
readonly truncatedBy: "matches" | "scan" | "file" | "entries" | "files" | "depth" | "time" | "abort" | "pattern" | "index" | null;
|
|
38
41
|
readonly scannedBytes: number;
|
|
39
42
|
readonly scannedFiles: number;
|
|
40
43
|
readonly scannedEntries: number;
|
|
41
44
|
readonly filesSkippedBinary: number;
|
|
42
45
|
readonly filesSkippedOversize: number;
|
|
46
|
+
/** Present only for indexed_literal/semantic queries: provenance and freshness. */
|
|
47
|
+
readonly indexed?: {
|
|
48
|
+
readonly mode: "indexed_literal" | "semantic";
|
|
49
|
+
readonly state: "empty" | "building" | "ready" | "stale" | "failed";
|
|
50
|
+
readonly sourceRevision?: string;
|
|
51
|
+
readonly updatedAt?: number;
|
|
52
|
+
};
|
|
53
|
+
/** True when match content came from a host index; index text is untrusted. */
|
|
54
|
+
readonly untrusted_index?: boolean;
|
|
43
55
|
}
|
|
44
56
|
export interface ResolvedRepositoryLimits {
|
|
45
57
|
readonly maxDepth: number;
|
|
@@ -88,7 +100,7 @@ export interface RepositorySearchRequest {
|
|
|
88
100
|
readonly root: string;
|
|
89
101
|
readonly query: string;
|
|
90
102
|
readonly path?: string;
|
|
91
|
-
readonly mode?:
|
|
103
|
+
readonly mode?: RepoSearchMode;
|
|
92
104
|
readonly outputMode?: RepoSearchOutputMode;
|
|
93
105
|
readonly caseSensitive?: boolean;
|
|
94
106
|
readonly includeHidden?: boolean;
|
package/dist/repository.d.ts
CHANGED
package/dist/repository.js
CHANGED
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);
|