@arnilo/prism-coding-agent 0.0.96 → 0.1.1

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.
Files changed (81) hide show
  1. package/CHANGELOG.md +139 -3
  2. package/README.md +48 -19
  3. package/dist/ask-user-decision.d.ts +160 -0
  4. package/dist/ask-user-decision.js +495 -0
  5. package/dist/atomic-write.d.ts +3 -0
  6. package/dist/atomic-write.js +24 -0
  7. package/dist/checks.js +5 -0
  8. package/dist/coding-checkpoint.js +6 -15
  9. package/dist/delete.d.ts +29 -0
  10. package/dist/delete.js +119 -0
  11. package/dist/edit-diff.js +1 -4
  12. package/dist/edit.d.ts +5 -1
  13. package/dist/edit.js +20 -9
  14. package/dist/effects.d.ts +33 -0
  15. package/dist/effects.js +89 -0
  16. package/dist/execution-policy.d.ts +8 -3
  17. package/dist/execution-policy.js +5 -2
  18. package/dist/file-mutation-queue.js +1 -2
  19. package/dist/forge/github.d.ts +2 -0
  20. package/dist/forge/github.js +554 -0
  21. package/dist/forge/index.d.ts +3 -0
  22. package/dist/forge/index.js +3 -0
  23. package/dist/forge/types.d.ts +150 -0
  24. package/dist/forge/types.js +19 -0
  25. package/dist/git-aware-repository.d.ts +25 -0
  26. package/dist/git-aware-repository.js +268 -0
  27. package/dist/git-exec.js +1 -1
  28. package/dist/git-tools.d.ts +4 -1
  29. package/dist/git-tools.js +15 -7
  30. package/dist/git.d.ts +3 -3
  31. package/dist/git.js +14 -14
  32. package/dist/glob-match.d.ts +6 -0
  33. package/dist/glob-match.js +81 -0
  34. package/dist/glob.d.ts +14 -0
  35. package/dist/glob.js +147 -0
  36. package/dist/goal-verify.d.ts +66 -0
  37. package/dist/goal-verify.js +280 -0
  38. package/dist/index.d.ts +63 -30
  39. package/dist/index.js +40 -16
  40. package/dist/language/client.d.ts +44 -0
  41. package/dist/language/client.js +290 -0
  42. package/dist/language/framing.d.ts +23 -0
  43. package/dist/language/framing.js +112 -0
  44. package/dist/language/index.d.ts +4 -0
  45. package/dist/language/index.js +4 -0
  46. package/dist/language/intelligence.d.ts +10 -0
  47. package/dist/language/intelligence.js +526 -0
  48. package/dist/language/types.d.ts +106 -0
  49. package/dist/language/types.js +21 -0
  50. package/dist/lifecycle.d.ts +75 -0
  51. package/dist/lifecycle.js +102 -0
  52. package/dist/limits.d.ts +41 -0
  53. package/dist/limits.js +41 -0
  54. package/dist/list.js +6 -10
  55. package/dist/move.d.ts +24 -0
  56. package/dist/move.js +150 -0
  57. package/dist/mutation-path.d.ts +7 -0
  58. package/dist/mutation-path.js +51 -0
  59. package/dist/output-accumulator.d.ts +8 -0
  60. package/dist/output-accumulator.js +45 -1
  61. package/dist/path-utils.js +1 -1
  62. package/dist/process/index.d.ts +3 -0
  63. package/dist/process/index.js +3 -0
  64. package/dist/process/sessions.d.ts +2 -0
  65. package/dist/process/sessions.js +592 -0
  66. package/dist/process/types.d.ts +146 -0
  67. package/dist/process/types.js +19 -0
  68. package/dist/read-path-set.d.ts +14 -0
  69. package/dist/read-path-set.js +26 -0
  70. package/dist/read.d.ts +3 -0
  71. package/dist/read.js +11 -17
  72. package/dist/repository.d.ts +54 -3
  73. package/dist/repository.js +144 -38
  74. package/dist/search.d.ts +1 -1
  75. package/dist/search.js +91 -27
  76. package/dist/shell.d.ts +3 -0
  77. package/dist/shell.js +23 -8
  78. package/dist/truncate.js +1 -1
  79. package/dist/write.d.ts +5 -1
  80. package/dist/write.js +19 -6
  81. 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 {
@@ -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";