@arnilo/prism-coding-agent 0.0.96 → 0.1.0
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 +139 -3
- package/README.md +48 -19
- package/dist/ask-user-decision.d.ts +160 -0
- package/dist/ask-user-decision.js +495 -0
- package/dist/atomic-write.d.ts +3 -0
- package/dist/atomic-write.js +24 -0
- package/dist/checks.js +5 -0
- package/dist/coding-checkpoint.js +6 -15
- package/dist/delete.d.ts +29 -0
- package/dist/delete.js +119 -0
- package/dist/edit-diff.js +1 -4
- package/dist/edit.d.ts +5 -1
- package/dist/edit.js +20 -9
- package/dist/effects.d.ts +33 -0
- package/dist/effects.js +89 -0
- package/dist/execution-policy.d.ts +8 -3
- package/dist/execution-policy.js +5 -2
- package/dist/file-mutation-queue.js +1 -2
- package/dist/forge/github.d.ts +2 -0
- package/dist/forge/github.js +554 -0
- package/dist/forge/index.d.ts +3 -0
- package/dist/forge/index.js +3 -0
- package/dist/forge/types.d.ts +150 -0
- package/dist/forge/types.js +19 -0
- package/dist/git-aware-repository.d.ts +25 -0
- package/dist/git-aware-repository.js +268 -0
- package/dist/git-exec.js +1 -1
- package/dist/git-tools.d.ts +4 -1
- package/dist/git-tools.js +15 -7
- package/dist/git.d.ts +3 -3
- package/dist/git.js +14 -14
- package/dist/glob-match.d.ts +6 -0
- package/dist/glob-match.js +81 -0
- package/dist/glob.d.ts +14 -0
- package/dist/glob.js +147 -0
- package/dist/goal-verify.d.ts +66 -0
- package/dist/goal-verify.js +280 -0
- package/dist/index.d.ts +63 -30
- package/dist/index.js +40 -16
- package/dist/language/client.d.ts +44 -0
- package/dist/language/client.js +290 -0
- package/dist/language/framing.d.ts +23 -0
- package/dist/language/framing.js +112 -0
- package/dist/language/index.d.ts +4 -0
- package/dist/language/index.js +4 -0
- package/dist/language/intelligence.d.ts +10 -0
- package/dist/language/intelligence.js +526 -0
- package/dist/language/types.d.ts +106 -0
- package/dist/language/types.js +21 -0
- package/dist/lifecycle.d.ts +75 -0
- package/dist/lifecycle.js +102 -0
- package/dist/limits.d.ts +41 -0
- package/dist/limits.js +41 -0
- package/dist/list.js +6 -10
- package/dist/move.d.ts +24 -0
- package/dist/move.js +150 -0
- package/dist/mutation-path.d.ts +7 -0
- package/dist/mutation-path.js +51 -0
- package/dist/output-accumulator.d.ts +8 -0
- package/dist/output-accumulator.js +45 -1
- package/dist/path-utils.js +1 -1
- package/dist/process/index.d.ts +3 -0
- package/dist/process/index.js +3 -0
- package/dist/process/sessions.d.ts +2 -0
- package/dist/process/sessions.js +592 -0
- package/dist/process/types.d.ts +146 -0
- package/dist/process/types.js +19 -0
- package/dist/read-path-set.d.ts +14 -0
- package/dist/read-path-set.js +26 -0
- package/dist/read.d.ts +3 -0
- package/dist/read.js +11 -17
- package/dist/repository.d.ts +54 -3
- package/dist/repository.js +144 -38
- package/dist/search.d.ts +1 -1
- package/dist/search.js +91 -27
- package/dist/shell.d.ts +3 -0
- package/dist/shell.js +23 -8
- package/dist/truncate.js +1 -1
- package/dist/write.d.ts +5 -1
- package/dist/write.js +19 -6
- package/package.json +6 -4
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { AgentIdentity, ExecutionPolicy, OwnershipScope, ToolEffectStore } from "@arnilo/prism";
|
|
2
|
+
import type { BoundGitRunner, CreateGitRunnerOptions } from "../git-exec.js";
|
|
3
|
+
/** Read-only context for one GitHub issue, bounded by payload caps. */
|
|
4
|
+
export interface ForgeIssueContext {
|
|
5
|
+
readonly number: number;
|
|
6
|
+
readonly title: string;
|
|
7
|
+
readonly state: "open" | "closed";
|
|
8
|
+
readonly body: string;
|
|
9
|
+
readonly labels: readonly string[];
|
|
10
|
+
readonly author: string;
|
|
11
|
+
readonly updatedAt: string;
|
|
12
|
+
readonly url: string;
|
|
13
|
+
}
|
|
14
|
+
/** Pull-request state as seen through the forge. */
|
|
15
|
+
export interface ForgePullRequest {
|
|
16
|
+
readonly number: number;
|
|
17
|
+
readonly state: "open" | "closed";
|
|
18
|
+
readonly merged: boolean;
|
|
19
|
+
readonly head: string;
|
|
20
|
+
readonly base: string;
|
|
21
|
+
readonly title: string;
|
|
22
|
+
readonly body: string;
|
|
23
|
+
readonly url: string;
|
|
24
|
+
}
|
|
25
|
+
/** One check run or commit status, normalized. */
|
|
26
|
+
export interface ForgeCheck {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly status: "queued" | "in_progress" | "completed";
|
|
29
|
+
readonly conclusion?: string;
|
|
30
|
+
readonly detailsUrl?: string;
|
|
31
|
+
}
|
|
32
|
+
/** Bounded handoff reconciliation: push/PR/check state, never auto-merged. */
|
|
33
|
+
export interface ForgeHandoffReport {
|
|
34
|
+
readonly base: string;
|
|
35
|
+
readonly head: string;
|
|
36
|
+
/** Whether the head ref exists on the remote. */
|
|
37
|
+
readonly pushed: boolean;
|
|
38
|
+
readonly aheadBy: number;
|
|
39
|
+
readonly behindBy: number;
|
|
40
|
+
/** No commits ahead and no divergence: nothing to push. */
|
|
41
|
+
readonly alreadyUpToDate: boolean;
|
|
42
|
+
readonly alreadyMerged: boolean;
|
|
43
|
+
readonly pullRequest?: ForgePullRequest;
|
|
44
|
+
readonly checks: readonly ForgeCheck[];
|
|
45
|
+
/** Bounded commit list (sha + subject), present only when pushed. */
|
|
46
|
+
readonly commits: readonly {
|
|
47
|
+
sha: string;
|
|
48
|
+
subject: string;
|
|
49
|
+
}[];
|
|
50
|
+
/** Bounded changed paths, present only when pushed. */
|
|
51
|
+
readonly changedPaths: readonly string[];
|
|
52
|
+
readonly diffstat: string;
|
|
53
|
+
readonly warnings: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
export type ForgeErrorCode = "ERR_PRISM_FORGE_AUTH" | "ERR_PRISM_FORGE_API" | "ERR_PRISM_FORGE_STALE" | "ERR_PRISM_FORGE_RATE_LIMIT" | "ERR_PRISM_FORGE_LIMIT" | "ERR_PRISM_FORGE_OWNERSHIP";
|
|
56
|
+
export declare class ForgeError extends Error {
|
|
57
|
+
readonly code: ForgeErrorCode;
|
|
58
|
+
constructor(code: ForgeErrorCode, message: string);
|
|
59
|
+
}
|
|
60
|
+
export interface ForgeLimits {
|
|
61
|
+
readonly pagesPerOperation?: number;
|
|
62
|
+
readonly payloadBytes?: number;
|
|
63
|
+
readonly commentsPerReview?: number;
|
|
64
|
+
readonly requestConcurrency?: number;
|
|
65
|
+
readonly requestTimeoutMs?: number;
|
|
66
|
+
}
|
|
67
|
+
export interface ResolvedForgeLimits {
|
|
68
|
+
readonly pagesPerOperation: number;
|
|
69
|
+
readonly payloadBytes: number;
|
|
70
|
+
readonly commentsPerReview: number;
|
|
71
|
+
readonly requestConcurrency: number;
|
|
72
|
+
readonly requestTimeoutMs: number;
|
|
73
|
+
}
|
|
74
|
+
export declare function resolveForgeLimits(options?: ForgeLimits): ResolvedForgeLimits;
|
|
75
|
+
export interface ForgeOperations {
|
|
76
|
+
issueContext(input: {
|
|
77
|
+
number: number;
|
|
78
|
+
}): Promise<ForgeIssueContext>;
|
|
79
|
+
push(input: {
|
|
80
|
+
refspec?: string;
|
|
81
|
+
}): Promise<{
|
|
82
|
+
remoteRef: string;
|
|
83
|
+
}>;
|
|
84
|
+
createPullRequest(input: {
|
|
85
|
+
head: string;
|
|
86
|
+
base: string;
|
|
87
|
+
title: string;
|
|
88
|
+
body: string;
|
|
89
|
+
}): Promise<ForgePullRequest>;
|
|
90
|
+
updatePullRequest(input: {
|
|
91
|
+
number: number;
|
|
92
|
+
title?: string;
|
|
93
|
+
body?: string;
|
|
94
|
+
state?: "open" | "closed";
|
|
95
|
+
}): Promise<ForgePullRequest>;
|
|
96
|
+
createReviewComment(input: {
|
|
97
|
+
number: number;
|
|
98
|
+
path: string;
|
|
99
|
+
line: number;
|
|
100
|
+
body: string;
|
|
101
|
+
}): Promise<{
|
|
102
|
+
id: number;
|
|
103
|
+
}>;
|
|
104
|
+
checks(input: {
|
|
105
|
+
ref: string;
|
|
106
|
+
}): Promise<readonly ForgeCheck[]>;
|
|
107
|
+
reconcileHandoff(input: {
|
|
108
|
+
base: string;
|
|
109
|
+
head: string;
|
|
110
|
+
}): Promise<ForgeHandoffReport>;
|
|
111
|
+
}
|
|
112
|
+
/** Structural mirror of the core `CredentialResolverSource` (not barrel-exported). */
|
|
113
|
+
export interface ForgeCredential {
|
|
114
|
+
readonly type: "bearer" | "api_key" | "basic" | "custom";
|
|
115
|
+
readonly value: string;
|
|
116
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
117
|
+
}
|
|
118
|
+
export interface ForgeCredentialResolver {
|
|
119
|
+
resolve(request: {
|
|
120
|
+
readonly name: string;
|
|
121
|
+
readonly provider?: string;
|
|
122
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
123
|
+
}): Promise<ForgeCredential | undefined> | ForgeCredential | undefined;
|
|
124
|
+
}
|
|
125
|
+
export interface ForgeCredentialResolverSource {
|
|
126
|
+
readonly name: string;
|
|
127
|
+
readonly resolver: ForgeCredentialResolver;
|
|
128
|
+
}
|
|
129
|
+
export interface CreateGitHubForgeOptions {
|
|
130
|
+
/** Credential resolver — resolved with provider "github" per call. */
|
|
131
|
+
readonly credentials: ForgeCredentialResolverSource;
|
|
132
|
+
/** "owner/repo", bound per instance. */
|
|
133
|
+
readonly repository: string;
|
|
134
|
+
/** Local checkout the adapter pushes from. */
|
|
135
|
+
readonly cwd: string;
|
|
136
|
+
/** Git runner reused for authenticated push. */
|
|
137
|
+
readonly git: CreateGitRunnerOptions | BoundGitRunner;
|
|
138
|
+
/** Mutations are gated through this policy before any request. */
|
|
139
|
+
readonly policy?: ExecutionPolicy;
|
|
140
|
+
/** REQUIRED: idempotency + unknown-outcome recovery for mutations. */
|
|
141
|
+
readonly effectStore: ToolEffectStore;
|
|
142
|
+
/** Durable context for effect keys; required for mutations. */
|
|
143
|
+
readonly identity?: AgentIdentity;
|
|
144
|
+
readonly ownership?: OwnershipScope;
|
|
145
|
+
readonly sessionId?: string;
|
|
146
|
+
readonly runId?: string;
|
|
147
|
+
readonly limits?: ForgeLimits;
|
|
148
|
+
/** Host-injectable fetch (e.g. routed through an egress proxy); defaults to globalThis.fetch. */
|
|
149
|
+
readonly fetch?: typeof fetch;
|
|
150
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DEFAULT_MAX_FORGE_COMMENTS_PER_REVIEW, DEFAULT_MAX_FORGE_PAGES_PER_OPERATION, DEFAULT_MAX_FORGE_PAYLOAD_BYTES, DEFAULT_MAX_FORGE_REQUEST_CONCURRENCY, DEFAULT_MAX_FORGE_REQUEST_TIMEOUT_MS, HARD_MAX_FORGE_COMMENTS_PER_REVIEW, HARD_MAX_FORGE_PAGES_PER_OPERATION, HARD_MAX_FORGE_PAYLOAD_BYTES, HARD_MAX_FORGE_REQUEST_CONCURRENCY, HARD_MAX_FORGE_REQUEST_TIMEOUT_MS, validateCodingLimit, } from "../limits.js";
|
|
2
|
+
export class ForgeError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "ForgeError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function resolveForgeLimits(options) {
|
|
11
|
+
return {
|
|
12
|
+
pagesPerOperation: validateCodingLimit("forge.pagesPerOperation", options?.pagesPerOperation ?? DEFAULT_MAX_FORGE_PAGES_PER_OPERATION, HARD_MAX_FORGE_PAGES_PER_OPERATION),
|
|
13
|
+
payloadBytes: validateCodingLimit("forge.payloadBytes", options?.payloadBytes ?? DEFAULT_MAX_FORGE_PAYLOAD_BYTES, HARD_MAX_FORGE_PAYLOAD_BYTES),
|
|
14
|
+
commentsPerReview: validateCodingLimit("forge.commentsPerReview", options?.commentsPerReview ?? DEFAULT_MAX_FORGE_COMMENTS_PER_REVIEW, HARD_MAX_FORGE_COMMENTS_PER_REVIEW),
|
|
15
|
+
requestConcurrency: validateCodingLimit("forge.requestConcurrency", options?.requestConcurrency ?? DEFAULT_MAX_FORGE_REQUEST_CONCURRENCY, HARD_MAX_FORGE_REQUEST_CONCURRENCY),
|
|
16
|
+
requestTimeoutMs: validateCodingLimit("forge.requestTimeoutMs", options?.requestTimeoutMs ?? DEFAULT_MAX_FORGE_REQUEST_TIMEOUT_MS, HARD_MAX_FORGE_REQUEST_TIMEOUT_MS),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git-aware repository enumeration over `git ls-files`.
|
|
3
|
+
*
|
|
4
|
+
* Detection: `git rev-parse --is-inside-work-tree` (cached per instance).
|
|
5
|
+
* Non-Git / detection failure → native fallback. Post-detection Git failure → fail closed.
|
|
6
|
+
* No hand-rolled `.gitignore` parser; argv is fixed host-side only.
|
|
7
|
+
*/
|
|
8
|
+
import { type BoundGitRunner, type CreateGitRunnerOptions } from "./git-exec.js";
|
|
9
|
+
import { type RepositoryLimitOptions, type RepositoryOperations } from "./repository.js";
|
|
10
|
+
export interface GitAwareRepositoryOptions {
|
|
11
|
+
readonly git?: CreateGitRunnerOptions | BoundGitRunner;
|
|
12
|
+
readonly fallback?: RepositoryOperations;
|
|
13
|
+
/** Host-config only, never model-settable. Default false: ignored paths stay excluded. */
|
|
14
|
+
readonly includeIgnored?: boolean;
|
|
15
|
+
readonly limits?: RepositoryLimitOptions;
|
|
16
|
+
/** Override freeze default/hard `ls-files` stdout cap. */
|
|
17
|
+
readonly maxLsFilesOutputBytes?: number;
|
|
18
|
+
}
|
|
19
|
+
/** Parse NUL-delimited `git ls-files -z` stdout. */
|
|
20
|
+
export declare function parseGitLsFilesZ(buffer: Buffer): string[];
|
|
21
|
+
/**
|
|
22
|
+
* Repository operations that prefer Git tracked/unignored enumeration.
|
|
23
|
+
* Outside a Git work tree (or when detection fails), delegates to `fallback`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createGitAwareRepositoryOperations(cwd: string, options?: GitAwareRepositoryOptions): RepositoryOperations;
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git-aware repository enumeration over `git ls-files`.
|
|
3
|
+
*
|
|
4
|
+
* Detection: `git rev-parse --is-inside-work-tree` (cached per instance).
|
|
5
|
+
* Non-Git / detection failure → native fallback. Post-detection Git failure → fail closed.
|
|
6
|
+
* No hand-rolled `.gitignore` parser; argv is fixed host-side only.
|
|
7
|
+
*/
|
|
8
|
+
import { lstat } from "node:fs/promises";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { createBoundGitRunner, gitText, GitError } from "./git-exec.js";
|
|
11
|
+
import { DEFAULT_MAX_LS_FILES_OUTPUT_BYTES, HARD_MAX_LS_FILES_OUTPUT_BYTES, validateCodingLimit } from "./limits.js";
|
|
12
|
+
import { createLocalRepositoryOperations, RepositoryError, toRepoRelative, } from "./repository.js";
|
|
13
|
+
function isBoundGitRunner(value) {
|
|
14
|
+
return typeof value.exec === "function" && typeof value.gitPath === "string";
|
|
15
|
+
}
|
|
16
|
+
function shouldSkipName(name, includeHidden, exclude) {
|
|
17
|
+
if (name === "." || name === "..")
|
|
18
|
+
return true;
|
|
19
|
+
if (exclude.has(name))
|
|
20
|
+
return true;
|
|
21
|
+
if (!includeHidden && name.startsWith("."))
|
|
22
|
+
return true;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
function pathHasSkippedComponent(relativePath, includeHidden, exclude) {
|
|
26
|
+
for (const part of relativePath.split("/")) {
|
|
27
|
+
if (shouldSkipName(part, includeHidden, exclude))
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
/** Parse NUL-delimited `git ls-files -z` stdout. */
|
|
33
|
+
export function parseGitLsFilesZ(buffer) {
|
|
34
|
+
const out = [];
|
|
35
|
+
let start = 0;
|
|
36
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
37
|
+
if (buffer[i] === 0) {
|
|
38
|
+
if (i > start)
|
|
39
|
+
out.push(buffer.subarray(start, i).toString("utf8"));
|
|
40
|
+
start = i + 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (start < buffer.length)
|
|
44
|
+
out.push(buffer.subarray(start).toString("utf8"));
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function depthFromStart(relativePath, startRel) {
|
|
48
|
+
const rel = startRel === "."
|
|
49
|
+
? relativePath
|
|
50
|
+
: relativePath === startRel
|
|
51
|
+
? ""
|
|
52
|
+
: relativePath.startsWith(`${startRel}/`)
|
|
53
|
+
? relativePath.slice(startRel.length + 1)
|
|
54
|
+
: relativePath;
|
|
55
|
+
if (rel === "" || rel === ".")
|
|
56
|
+
return 0;
|
|
57
|
+
return rel.split("/").length - 1;
|
|
58
|
+
}
|
|
59
|
+
function isUnderStart(relativePath, startRel) {
|
|
60
|
+
if (startRel === "." || startRel === "")
|
|
61
|
+
return true;
|
|
62
|
+
return relativePath === startRel || relativePath.startsWith(`${startRel}/`);
|
|
63
|
+
}
|
|
64
|
+
async function* walkGitFiles(rootReal, startAbsolute, limits, files) {
|
|
65
|
+
const startRel = toRepoRelative(rootReal, startAbsolute);
|
|
66
|
+
const dirSeen = new Set();
|
|
67
|
+
const planned = [];
|
|
68
|
+
for (const relativePath of files) {
|
|
69
|
+
if (limits.signal?.aborted)
|
|
70
|
+
throw new RepositoryError("Operation aborted");
|
|
71
|
+
if (limits.deadlineAt !== undefined && Date.now() >= limits.deadlineAt) {
|
|
72
|
+
throw new RepositoryError("Repository operation exceeded time limit");
|
|
73
|
+
}
|
|
74
|
+
if (!relativePath || relativePath.includes("\0"))
|
|
75
|
+
continue;
|
|
76
|
+
if (relativePath === ".git" || relativePath.startsWith(".git/"))
|
|
77
|
+
continue;
|
|
78
|
+
if (!isUnderStart(relativePath, startRel))
|
|
79
|
+
continue;
|
|
80
|
+
if (pathHasSkippedComponent(relativePath, limits.includeHidden, limits.exclude))
|
|
81
|
+
continue;
|
|
82
|
+
const fileDepth = depthFromStart(relativePath, startRel);
|
|
83
|
+
if (fileDepth > limits.maxDepth)
|
|
84
|
+
continue;
|
|
85
|
+
// Synthesize parent directories within the start scope (native walker emits dirs too).
|
|
86
|
+
const parts = relativePath.split("/");
|
|
87
|
+
let acc = "";
|
|
88
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
89
|
+
acc = acc ? `${acc}/${parts[i]}` : parts[i];
|
|
90
|
+
if (!isUnderStart(acc, startRel))
|
|
91
|
+
continue;
|
|
92
|
+
if (pathHasSkippedComponent(acc, limits.includeHidden, limits.exclude))
|
|
93
|
+
break;
|
|
94
|
+
const dirDepth = depthFromStart(acc, startRel);
|
|
95
|
+
if (dirDepth > limits.maxDepth)
|
|
96
|
+
break;
|
|
97
|
+
if (!dirSeen.has(acc)) {
|
|
98
|
+
dirSeen.add(acc);
|
|
99
|
+
planned.push({ path: acc, kind: "directory", depth: dirDepth });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
planned.push({ path: relativePath, kind: "file", depth: fileDepth });
|
|
103
|
+
}
|
|
104
|
+
planned.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
105
|
+
let scannedEntries = 0;
|
|
106
|
+
let scannedFiles = 0;
|
|
107
|
+
for (const item of planned) {
|
|
108
|
+
if (limits.signal?.aborted)
|
|
109
|
+
throw new RepositoryError("Operation aborted");
|
|
110
|
+
if (limits.deadlineAt !== undefined && Date.now() >= limits.deadlineAt) {
|
|
111
|
+
throw new RepositoryError("Repository operation exceeded time limit");
|
|
112
|
+
}
|
|
113
|
+
if (scannedEntries >= limits.maxEntries) {
|
|
114
|
+
yield { type: "limit", truncatedBy: "entries" };
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const absolutePath = join(rootReal, item.path);
|
|
118
|
+
const rootResolved = resolve(rootReal);
|
|
119
|
+
const absResolved = resolve(absolutePath);
|
|
120
|
+
if (absResolved !== rootResolved && !absResolved.startsWith(rootResolved + "/"))
|
|
121
|
+
continue;
|
|
122
|
+
let kind = item.kind;
|
|
123
|
+
let size;
|
|
124
|
+
try {
|
|
125
|
+
const st = await lstat(absolutePath);
|
|
126
|
+
if (st.isSymbolicLink())
|
|
127
|
+
kind = "symlink";
|
|
128
|
+
else if (st.isDirectory())
|
|
129
|
+
kind = "directory";
|
|
130
|
+
else if (st.isFile()) {
|
|
131
|
+
kind = "file";
|
|
132
|
+
size = st.size;
|
|
133
|
+
}
|
|
134
|
+
else
|
|
135
|
+
kind = "other";
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Missing after ls-files (race) — skip.
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (kind === "file") {
|
|
142
|
+
if (scannedFiles >= limits.maxFiles) {
|
|
143
|
+
yield { type: "limit", truncatedBy: "files" };
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
scannedFiles++;
|
|
147
|
+
}
|
|
148
|
+
scannedEntries++;
|
|
149
|
+
const entry = size === undefined ? { path: item.path, kind } : { path: item.path, kind, size };
|
|
150
|
+
yield { type: "entry", entry, absolutePath, depth: item.depth };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Repository operations that prefer Git tracked/unignored enumeration.
|
|
155
|
+
* Outside a Git work tree (or when detection fails), delegates to `fallback`.
|
|
156
|
+
*/
|
|
157
|
+
export function createGitAwareRepositoryOperations(cwd, options) {
|
|
158
|
+
const fallback = options?.fallback ?? createLocalRepositoryOperations(options?.limits);
|
|
159
|
+
const includeIgnored = options?.includeIgnored === true;
|
|
160
|
+
const maxLsBytes = validateCodingLimit("maxLsFilesOutputBytes", options?.maxLsFilesOutputBytes ?? DEFAULT_MAX_LS_FILES_OUTPUT_BYTES, HARD_MAX_LS_FILES_OUTPUT_BYTES);
|
|
161
|
+
let detected;
|
|
162
|
+
let runnerPromise;
|
|
163
|
+
let gitOps;
|
|
164
|
+
function getRunner() {
|
|
165
|
+
if (!runnerPromise) {
|
|
166
|
+
const git = options?.git;
|
|
167
|
+
runnerPromise =
|
|
168
|
+
git && isBoundGitRunner(git)
|
|
169
|
+
? Promise.resolve(git)
|
|
170
|
+
: createBoundGitRunner({
|
|
171
|
+
...git,
|
|
172
|
+
maxOutputBytes: maxLsBytes,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return runnerPromise;
|
|
176
|
+
}
|
|
177
|
+
async function detect(signal) {
|
|
178
|
+
if (detected !== undefined)
|
|
179
|
+
return detected;
|
|
180
|
+
try {
|
|
181
|
+
const result = await (await getRunner()).exec({
|
|
182
|
+
args: ["rev-parse", "--is-inside-work-tree"],
|
|
183
|
+
cwd,
|
|
184
|
+
signal,
|
|
185
|
+
maxOutputBytes: 64,
|
|
186
|
+
});
|
|
187
|
+
detected = result.exitCode === 0 && gitText(result).trim() === "true";
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
detected = false;
|
|
191
|
+
}
|
|
192
|
+
return detected;
|
|
193
|
+
}
|
|
194
|
+
async function listGitPaths(signal) {
|
|
195
|
+
const runner = await getRunner();
|
|
196
|
+
// Fixed argv only — never model-supplied flags (Task 0 freeze).
|
|
197
|
+
const primary = await runner.exec({
|
|
198
|
+
args: ["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
|
|
199
|
+
cwd,
|
|
200
|
+
signal,
|
|
201
|
+
maxOutputBytes: maxLsBytes,
|
|
202
|
+
});
|
|
203
|
+
if (primary.timedOut)
|
|
204
|
+
throw new RepositoryError("Repository operation exceeded time limit");
|
|
205
|
+
if (primary.aborted)
|
|
206
|
+
throw new RepositoryError("Operation aborted");
|
|
207
|
+
if (primary.exitCode !== 0) {
|
|
208
|
+
throw new RepositoryError(`git ls-files failed (exit ${primary.exitCode})`);
|
|
209
|
+
}
|
|
210
|
+
const paths = new Set(parseGitLsFilesZ(primary.stdout));
|
|
211
|
+
if (includeIgnored) {
|
|
212
|
+
// Second invocation only when host opts into ignored paths (≤ 2 total per freeze).
|
|
213
|
+
const ignored = await runner.exec({
|
|
214
|
+
args: ["ls-files", "-o", "-i", "--exclude-standard", "-z"],
|
|
215
|
+
cwd,
|
|
216
|
+
signal,
|
|
217
|
+
maxOutputBytes: maxLsBytes,
|
|
218
|
+
});
|
|
219
|
+
if (ignored.timedOut)
|
|
220
|
+
throw new RepositoryError("Repository operation exceeded time limit");
|
|
221
|
+
if (ignored.aborted)
|
|
222
|
+
throw new RepositoryError("Operation aborted");
|
|
223
|
+
if (ignored.exitCode !== 0) {
|
|
224
|
+
throw new RepositoryError(`git ls-files (ignored) failed (exit ${ignored.exitCode})`);
|
|
225
|
+
}
|
|
226
|
+
for (const p of parseGitLsFilesZ(ignored.stdout))
|
|
227
|
+
paths.add(p);
|
|
228
|
+
}
|
|
229
|
+
return [...paths].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
230
|
+
}
|
|
231
|
+
function getGitOps() {
|
|
232
|
+
if (!gitOps) {
|
|
233
|
+
const walk = async function* (rootReal, startAbsolute, limits) {
|
|
234
|
+
let files;
|
|
235
|
+
try {
|
|
236
|
+
files = await listGitPaths(limits.signal);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
if (error instanceof RepositoryError)
|
|
240
|
+
throw error;
|
|
241
|
+
if (error instanceof GitError) {
|
|
242
|
+
if (/abort/i.test(error.message))
|
|
243
|
+
throw new RepositoryError("Operation aborted");
|
|
244
|
+
if (/time|exceeded/i.test(error.message)) {
|
|
245
|
+
throw new RepositoryError("Repository operation exceeded time limit");
|
|
246
|
+
}
|
|
247
|
+
throw new RepositoryError(error.message);
|
|
248
|
+
}
|
|
249
|
+
throw new RepositoryError(error instanceof Error ? error.message : String(error));
|
|
250
|
+
}
|
|
251
|
+
yield* walkGitFiles(rootReal, startAbsolute, limits, files);
|
|
252
|
+
};
|
|
253
|
+
gitOps = createLocalRepositoryOperations(options?.limits, walk);
|
|
254
|
+
}
|
|
255
|
+
return gitOps;
|
|
256
|
+
}
|
|
257
|
+
async function route(signal, gitCall, nativeCall) {
|
|
258
|
+
if (!(await detect(signal)))
|
|
259
|
+
return nativeCall();
|
|
260
|
+
return gitCall();
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
list: (request) => route(request.signal, () => getGitOps().list(request), () => fallback.list(request)),
|
|
264
|
+
search: (request) => route(request.signal, () => getGitOps().search(request), () => fallback.search(request)),
|
|
265
|
+
glob: (request) => route(request.signal, () => getGitOps().glob(request), () => fallback.glob(request)),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=git-aware-repository.js.map
|
package/dist/git-exec.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* runner (for example a sandbox `execFile` adapter) without changing tool code.
|
|
7
7
|
*/
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
|
-
import { access } from "node:fs/promises";
|
|
10
9
|
import { constants as fsConstants } from "node:fs";
|
|
10
|
+
import { access } from "node:fs/promises";
|
|
11
11
|
import { isAbsolute } from "node:path";
|
|
12
12
|
import { DEFAULT_GIT_TIMEOUT_MS, DEFAULT_MAX_GIT_OUTPUT_BYTES, HARD_GIT_TIMEOUT_MS, HARD_MAX_GIT_OUTPUT_BYTES, validateCodingLimit, } from "./limits.js";
|
|
13
13
|
export class GitError extends Error {
|
package/dist/git-tools.d.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* Shell is never used internally; all Git invocations go through typed arg arrays.
|
|
6
6
|
*/
|
|
7
7
|
import type { ExecutionPolicy, ToolDefinition } from "@arnilo/prism";
|
|
8
|
-
import { type ArtifactWriter, type CreateGitOperationsOptions, type GitOperations } from "./git.js";
|
|
9
8
|
import { type CodingCheckToolOptions, type NamedCheckDefinition } from "./checks.js";
|
|
9
|
+
import type { CodingLifecycleEvent } from "./lifecycle.js";
|
|
10
|
+
import { type ArtifactWriter, type CreateGitOperationsOptions, type GitOperations } from "./git.js";
|
|
10
11
|
export interface GitToolsOptions {
|
|
11
12
|
readonly executionPolicy?: ExecutionPolicy;
|
|
12
13
|
readonly gitPath?: string;
|
|
@@ -19,6 +20,8 @@ export interface GitToolsOptions {
|
|
|
19
20
|
/** Optional named checks included by `createGitTools` when provided. */
|
|
20
21
|
readonly checks?: Readonly<Record<string, NamedCheckDefinition>>;
|
|
21
22
|
readonly checkOptions?: Omit<CodingCheckToolOptions, "checks" | "executionPolicy">;
|
|
23
|
+
/** Optional consumer-gated lifecycle listener (worktree_changed / permission_denied). */
|
|
24
|
+
readonly onEvent?: (event: CodingLifecycleEvent) => void;
|
|
22
25
|
}
|
|
23
26
|
export declare function createGitStatusTool(cwd: string, options?: GitToolsOptions): ToolDefinition;
|
|
24
27
|
export declare function createGitDiffTool(cwd: string, options?: GitToolsOptions): ToolDefinition;
|
package/dist/git-tools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { createCodingCheckTool } from "./checks.js";
|
|
2
|
+
import { classifyGitApplyEffect, classifyGitBranchEffect, classifyGitWorktreeEffect, CODING_LOCAL_EFFECT, CODING_OBSERVATION_EFFECT, CODING_UNSUPPORTED_EFFECT, } from "./effects.js";
|
|
1
3
|
import { enforceExecutionPolicy } from "./execution-policy.js";
|
|
2
4
|
import { createGitOperations, GitError, } from "./git.js";
|
|
3
|
-
import { createCodingCheckTool } from "./checks.js";
|
|
4
5
|
function errorResult(toolName, toolCallId, message) {
|
|
5
6
|
return {
|
|
6
7
|
toolCallId,
|
|
@@ -39,6 +40,7 @@ export function createGitStatusTool(cwd, options) {
|
|
|
39
40
|
const getOps = opsFactory(cwd, options);
|
|
40
41
|
return {
|
|
41
42
|
name: "git_status",
|
|
43
|
+
effect: CODING_OBSERVATION_EFFECT,
|
|
42
44
|
description: "Return structured Git status (porcelain v2) including branch metadata and dirty-state. Does not follow shell; paths are repository-relative.",
|
|
43
45
|
parameters: {
|
|
44
46
|
type: "object",
|
|
@@ -84,6 +86,7 @@ export function createGitDiffTool(cwd, options) {
|
|
|
84
86
|
const getOps = opsFactory(cwd, options);
|
|
85
87
|
return {
|
|
86
88
|
name: "git_diff",
|
|
89
|
+
effect: CODING_OBSERVATION_EFFECT,
|
|
87
90
|
description: "Return a bounded unified diff (--no-ext-diff --no-textconv). Large diffs truncate inline and may spill through the host artifact writer.",
|
|
88
91
|
parameters: {
|
|
89
92
|
type: "object",
|
|
@@ -138,6 +141,7 @@ export function createGitBranchTool(cwd, options) {
|
|
|
138
141
|
const getOps = opsFactory(cwd, options);
|
|
139
142
|
return {
|
|
140
143
|
name: "git_branch",
|
|
144
|
+
effect: classifyGitBranchEffect,
|
|
141
145
|
description: "Validate, list, create, or switch branches. Switch refuses a dirty worktree unless createCheckpoint=true.",
|
|
142
146
|
exclusive: true,
|
|
143
147
|
parameters: {
|
|
@@ -208,6 +212,7 @@ export function createGitWorktreeTool(cwd, options) {
|
|
|
208
212
|
const getOps = opsFactory(cwd, options);
|
|
209
213
|
return {
|
|
210
214
|
name: "git_worktree",
|
|
215
|
+
effect: classifyGitWorktreeEffect,
|
|
211
216
|
description: "List, add, or remove Git worktrees within finite caps. Prefer disposable worktrees for mutating transactions.",
|
|
212
217
|
exclusive: true,
|
|
213
218
|
parameters: {
|
|
@@ -238,7 +243,7 @@ export function createGitWorktreeTool(cwd, options) {
|
|
|
238
243
|
paths: path ? [path] : [cwd],
|
|
239
244
|
risk: action === "list" ? "low" : "high",
|
|
240
245
|
metadata: { branch, force, sessionId: context.sessionId, runId: context.runId },
|
|
241
|
-
}, toolCallId, "git_worktree");
|
|
246
|
+
}, toolCallId, "git_worktree", (denied) => options?.onEvent?.({ type: "permission_denied", ...denied }));
|
|
242
247
|
if (!policy.allowed)
|
|
243
248
|
return policy.result;
|
|
244
249
|
try {
|
|
@@ -249,9 +254,11 @@ export function createGitWorktreeTool(cwd, options) {
|
|
|
249
254
|
force,
|
|
250
255
|
signal: context.signal,
|
|
251
256
|
});
|
|
257
|
+
if (action !== "list") {
|
|
258
|
+
options?.onEvent?.({ type: "worktree_changed", action, path: result.path ?? path ?? "", toolCallId });
|
|
259
|
+
}
|
|
252
260
|
const text = action === "list"
|
|
253
|
-
? result.worktrees.map((w) => `${w.path}\t${w.branch ?? ""}\t${w.head ?? ""}`).join("\n") ||
|
|
254
|
-
"(no worktrees)"
|
|
261
|
+
? result.worktrees.map((w) => `${w.path}\t${w.branch ?? ""}\t${w.head ?? ""}`).join("\n") || "(no worktrees)"
|
|
255
262
|
: `ok action=${action} path=${result.path ?? ""}`;
|
|
256
263
|
return {
|
|
257
264
|
toolCallId,
|
|
@@ -270,6 +277,7 @@ export function createGitApplyTool(cwd, options) {
|
|
|
270
277
|
const getOps = opsFactory(cwd, options);
|
|
271
278
|
return {
|
|
272
279
|
name: "git_apply",
|
|
280
|
+
effect: classifyGitApplyEffect,
|
|
273
281
|
description: "Check, apply, or reverse a unified patch. Always runs apply --check first for apply/reverse. Dirty trees require createCheckpoint=true; failures restore the checkpoint or clean tree.",
|
|
274
282
|
exclusive: true,
|
|
275
283
|
parameters: {
|
|
@@ -337,6 +345,7 @@ export function createGitCommitTool(cwd, options) {
|
|
|
337
345
|
const getOps = opsFactory(cwd, options);
|
|
338
346
|
return {
|
|
339
347
|
name: "git_commit",
|
|
348
|
+
effect: CODING_LOCAL_EFFECT,
|
|
340
349
|
description: "Stage and commit explicit paths only (never `git add -A`). Refuses dirty unrelated worktrees unless createCheckpoint=true. Uses --no-verify and a temp message file; never pushes.",
|
|
341
350
|
exclusive: true,
|
|
342
351
|
parameters: {
|
|
@@ -396,6 +405,7 @@ export function createGitPrHandoffTool(cwd, options) {
|
|
|
396
405
|
const getOps = opsFactory(cwd, options);
|
|
397
406
|
return {
|
|
398
407
|
name: "git_pr_handoff",
|
|
408
|
+
effect: CODING_UNSUPPORTED_EFFECT,
|
|
399
409
|
description: "Build a bounded host-owned PR handoff payload (base/head/commits/paths/diffstat/checks/artifact). Never pushes, authenticates, or opens a PR.",
|
|
400
410
|
exclusive: true,
|
|
401
411
|
parameters: {
|
|
@@ -430,9 +440,7 @@ export function createGitPrHandoffTool(cwd, options) {
|
|
|
430
440
|
const base = typeof args.base === "string" ? args.base : "";
|
|
431
441
|
const head = typeof args.head === "string" ? args.head : undefined;
|
|
432
442
|
const includeBundle = args.includeBundle === true;
|
|
433
|
-
const checks = Array.isArray(args.checks)
|
|
434
|
-
? args.checks
|
|
435
|
-
: undefined;
|
|
443
|
+
const checks = Array.isArray(args.checks) ? args.checks : undefined;
|
|
436
444
|
const policy = await enforceExecutionPolicy(options?.executionPolicy, {
|
|
437
445
|
kind: "git",
|
|
438
446
|
operation: "pr_handoff",
|
package/dist/git.d.ts
CHANGED
|
@@ -133,7 +133,7 @@ export interface CreateGitOperationsOptions extends CreateGitRunnerOptions, GitL
|
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
135
|
export declare function createGitOperations(options: CreateGitOperationsOptions): Promise<GitOperations>;
|
|
136
|
+
export type { BoundGitRunner, CreateGitRunnerOptions, GitExecRequest, GitExecResult, GitRunner } from "./git-exec.js";
|
|
137
|
+
export { createBoundGitRunner, GitError, runGitCli, SAFE_GIT_CONFIG_ARGS, SAFE_GIT_ENV, } from "./git-exec.js";
|
|
138
|
+
export type { GitStatusBranch, GitStatusEntry, GitStatusEntryKind, GitStatusResult } from "./git-status.js";
|
|
136
139
|
export { parsePorcelainV2 } from "./git-status.js";
|
|
137
|
-
export type { GitStatusResult, GitStatusEntry, GitStatusBranch, GitStatusEntryKind } from "./git-status.js";
|
|
138
|
-
export { GitError, SAFE_GIT_ENV, SAFE_GIT_CONFIG_ARGS, createBoundGitRunner, runGitCli, } from "./git-exec.js";
|
|
139
|
-
export type { GitRunner, GitExecRequest, GitExecResult, BoundGitRunner, CreateGitRunnerOptions } from "./git-exec.js";
|