@phyzess/pappra 0.0.0-stage → 0.1.0-beta.2

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 (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -2
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/assets/integrations/skills/pappra/SKILL.md +68 -0
  5. package/assets/integrations/skills/pappra/references/hosts.md +30 -0
  6. package/assets/integrations/skills/pappra/references/lifecycle.md +37 -0
  7. package/assets/integrations/skills/pappra/references/operations.md +14 -0
  8. package/assets/manifest.json +4 -0
  9. package/dist/app/collaboration.d.ts +170 -0
  10. package/dist/app/collaboration.js +1650 -0
  11. package/dist/app/doctor.d.ts +9 -0
  12. package/dist/app/doctor.js +1030 -0
  13. package/dist/app/init.d.ts +143 -0
  14. package/dist/app/init.js +1111 -0
  15. package/dist/cli/commands/code.d.ts +16 -0
  16. package/dist/cli/commands/code.js +127 -0
  17. package/dist/cli/commands/collaboration.d.ts +13 -0
  18. package/dist/cli/commands/collaboration.js +186 -0
  19. package/dist/cli/commands/continuity.d.ts +29 -0
  20. package/dist/cli/commands/continuity.js +133 -0
  21. package/dist/cli/commands/docs.d.ts +11 -0
  22. package/dist/cli/commands/docs.js +133 -0
  23. package/dist/cli/commands/doctor.d.ts +14 -0
  24. package/dist/cli/commands/doctor.js +57 -0
  25. package/dist/cli/commands/host.d.ts +13 -0
  26. package/dist/cli/commands/host.js +125 -0
  27. package/dist/cli/commands/init.d.ts +51 -0
  28. package/dist/cli/commands/init.js +668 -0
  29. package/dist/cli/main.d.ts +2 -0
  30. package/dist/cli/main.js +10 -0
  31. package/dist/cli/output.d.ts +4 -0
  32. package/dist/cli/output.js +1 -0
  33. package/dist/cli/run.d.ts +2 -0
  34. package/dist/cli/run.js +597 -0
  35. package/dist/code/budget.d.ts +14 -0
  36. package/dist/code/budget.js +39 -0
  37. package/dist/code/exec.d.ts +23 -0
  38. package/dist/code/exec.js +133 -0
  39. package/dist/code/identity.d.ts +13 -0
  40. package/dist/code/identity.js +214 -0
  41. package/dist/code/index.d.ts +6 -0
  42. package/dist/code/index.js +3 -0
  43. package/dist/code/providers/codegraph.d.ts +59 -0
  44. package/dist/code/providers/codegraph.js +567 -0
  45. package/dist/code/registration.d.ts +9 -0
  46. package/dist/code/registration.js +293 -0
  47. package/dist/code/service.d.ts +53 -0
  48. package/dist/code/service.js +633 -0
  49. package/dist/code/text.d.ts +3 -0
  50. package/dist/code/text.js +202 -0
  51. package/dist/collab/admission.d.ts +450 -0
  52. package/dist/collab/admission.js +797 -0
  53. package/dist/collab/canonical.d.ts +26 -0
  54. package/dist/collab/canonical.js +136 -0
  55. package/dist/collab/contracts.d.ts +73 -0
  56. package/dist/collab/contracts.js +76 -0
  57. package/dist/collab/envelope.d.ts +52 -0
  58. package/dist/collab/envelope.js +241 -0
  59. package/dist/collab/fingerprint.d.ts +7 -0
  60. package/dist/collab/fingerprint.js +58 -0
  61. package/dist/collab/handoff.d.ts +350 -0
  62. package/dist/collab/handoff.js +513 -0
  63. package/dist/collab/index.d.ts +9 -0
  64. package/dist/collab/index.js +8 -0
  65. package/dist/collab/lifecycle.d.ts +449 -0
  66. package/dist/collab/lifecycle.js +534 -0
  67. package/dist/collab/messages.d.ts +148 -0
  68. package/dist/collab/messages.js +583 -0
  69. package/dist/collab/notify.d.ts +47 -0
  70. package/dist/collab/notify.js +140 -0
  71. package/dist/collab/preflight.d.ts +31 -0
  72. package/dist/collab/preflight.js +271 -0
  73. package/dist/collab/resources.d.ts +119 -0
  74. package/dist/collab/resources.js +451 -0
  75. package/dist/collab/store.d.ts +143 -0
  76. package/dist/collab/store.js +606 -0
  77. package/dist/collab/types.d.ts +354 -0
  78. package/dist/collab/types.js +1 -0
  79. package/dist/collab/validation.d.ts +10 -0
  80. package/dist/collab/validation.js +303 -0
  81. package/dist/continuity/context.d.ts +24 -0
  82. package/dist/continuity/context.js +179 -0
  83. package/dist/continuity/index.d.ts +3 -0
  84. package/dist/continuity/index.js +1 -0
  85. package/dist/continuity/paths.d.ts +37 -0
  86. package/dist/continuity/paths.js +258 -0
  87. package/dist/continuity/service.d.ts +59 -0
  88. package/dist/continuity/service.js +1258 -0
  89. package/dist/hosts/claude/native.d.ts +79 -0
  90. package/dist/hosts/claude/native.js +853 -0
  91. package/dist/hosts/codex/native.d.ts +345 -0
  92. package/dist/hosts/codex/native.js +1528 -0
  93. package/dist/hosts/codex/protocol.d.ts +51 -0
  94. package/dist/hosts/codex/protocol.js +42 -0
  95. package/dist/hosts/detect.d.ts +28 -0
  96. package/dist/hosts/detect.js +73 -0
  97. package/dist/hosts/index.d.ts +10 -0
  98. package/dist/hosts/index.js +10 -0
  99. package/dist/hosts/integration/bundle.d.ts +16 -0
  100. package/dist/hosts/integration/bundle.js +81 -0
  101. package/dist/hosts/integration/hook-entry.d.ts +1 -0
  102. package/dist/hosts/integration/hook-entry.js +68 -0
  103. package/dist/hosts/integration/hook.d.ts +32 -0
  104. package/dist/hosts/integration/hook.js +781 -0
  105. package/dist/hosts/integration/pi-extension-runtime.d.ts +18 -0
  106. package/dist/hosts/integration/pi-extension-runtime.js +89 -0
  107. package/dist/hosts/integration/pi-template.d.ts +3 -0
  108. package/dist/hosts/integration/pi-template.js +5 -0
  109. package/dist/hosts/integration/service.d.ts +41 -0
  110. package/dist/hosts/integration/service.js +1381 -0
  111. package/dist/hosts/integration/support.d.ts +36 -0
  112. package/dist/hosts/integration/support.js +41 -0
  113. package/dist/hosts/integration/templates.d.ts +22 -0
  114. package/dist/hosts/integration/templates.js +70 -0
  115. package/dist/hosts/pi/protocol.d.ts +22 -0
  116. package/dist/hosts/pi/protocol.js +28 -0
  117. package/dist/hosts/pi/query-result.d.ts +22 -0
  118. package/dist/hosts/pi/query-result.js +710 -0
  119. package/dist/hosts/pi/rpc.d.ts +937 -0
  120. package/dist/hosts/pi/rpc.js +1621 -0
  121. package/dist/hosts/pilot.d.ts +485 -0
  122. package/dist/hosts/pilot.js +593 -0
  123. package/dist/index.d.ts +5 -0
  124. package/dist/index.js +4 -0
  125. package/dist/infra/fs.d.ts +27 -0
  126. package/dist/infra/fs.js +130 -0
  127. package/dist/infra/git.d.ts +60 -0
  128. package/dist/infra/git.js +358 -0
  129. package/dist/infra/owned-process-group.d.ts +173 -0
  130. package/dist/infra/owned-process-group.js +571 -0
  131. package/dist/install/apply.d.ts +65 -0
  132. package/dist/install/apply.js +663 -0
  133. package/dist/install/index.d.ts +5 -0
  134. package/dist/install/index.js +5 -0
  135. package/dist/install/ownership.d.ts +85 -0
  136. package/dist/install/ownership.js +430 -0
  137. package/dist/install/plan.d.ts +39 -0
  138. package/dist/install/plan.js +97 -0
  139. package/dist/install/rollback.d.ts +18 -0
  140. package/dist/install/rollback.js +544 -0
  141. package/dist/install/runtime.d.ts +76 -0
  142. package/dist/install/runtime.js +1145 -0
  143. package/dist/knowledge/catalog.d.ts +47 -0
  144. package/dist/knowledge/catalog.js +308 -0
  145. package/dist/knowledge/context.d.ts +28 -0
  146. package/dist/knowledge/context.js +61 -0
  147. package/dist/knowledge/index.d.ts +3 -0
  148. package/dist/knowledge/index.js +1 -0
  149. package/dist/knowledge/journal.d.ts +16 -0
  150. package/dist/knowledge/journal.js +109 -0
  151. package/dist/knowledge/lifecycle.d.ts +8 -0
  152. package/dist/knowledge/lifecycle.js +1001 -0
  153. package/dist/knowledge/paths.d.ts +37 -0
  154. package/dist/knowledge/paths.js +236 -0
  155. package/dist/knowledge/refs.d.ts +36 -0
  156. package/dist/knowledge/refs.js +267 -0
  157. package/dist/knowledge/service.d.ts +21 -0
  158. package/dist/knowledge/service.js +393 -0
  159. package/dist/knowledge/types.d.ts +171 -0
  160. package/dist/knowledge/types.js +1 -0
  161. package/dist/knowledge/write.d.ts +8 -0
  162. package/dist/knowledge/write.js +1223 -0
  163. package/dist/package-info.d.ts +6 -0
  164. package/dist/package-info.js +19 -0
  165. package/dist/schemas/code.d.ts +377 -0
  166. package/dist/schemas/code.js +145 -0
  167. package/dist/schemas/collaboration.d.ts +115 -0
  168. package/dist/schemas/collaboration.js +106 -0
  169. package/dist/schemas/continuity.d.ts +315 -0
  170. package/dist/schemas/continuity.js +199 -0
  171. package/dist/schemas/diagnostics.d.ts +28 -0
  172. package/dist/schemas/diagnostics.js +27 -0
  173. package/dist/schemas/doctor.d.ts +157 -0
  174. package/dist/schemas/doctor.js +75 -0
  175. package/dist/schemas/document.d.ts +644 -0
  176. package/dist/schemas/document.js +296 -0
  177. package/dist/schemas/errors.d.ts +177 -0
  178. package/dist/schemas/errors.js +176 -0
  179. package/dist/schemas/evidence.d.ts +24 -0
  180. package/dist/schemas/evidence.js +16 -0
  181. package/dist/schemas/host.d.ts +436 -0
  182. package/dist/schemas/host.js +201 -0
  183. package/dist/schemas/index.d.ts +15 -0
  184. package/dist/schemas/index.js +15 -0
  185. package/dist/schemas/init.d.ts +121 -0
  186. package/dist/schemas/init.js +94 -0
  187. package/dist/schemas/install.d.ts +213 -0
  188. package/dist/schemas/install.js +139 -0
  189. package/dist/schemas/machine.d.ts +43 -0
  190. package/dist/schemas/machine.js +96 -0
  191. package/dist/schemas/outcome.d.ts +23 -0
  192. package/dist/schemas/outcome.js +17 -0
  193. package/dist/schemas/paths.d.ts +19 -0
  194. package/dist/schemas/paths.js +63 -0
  195. package/dist/schemas/workspace.d.ts +120 -0
  196. package/dist/schemas/workspace.js +201 -0
  197. package/dist/workspace/config-file.d.ts +36 -0
  198. package/dist/workspace/config-file.js +237 -0
  199. package/dist/workspace/config.d.ts +28 -0
  200. package/dist/workspace/config.js +79 -0
  201. package/dist/workspace/discover.d.ts +34 -0
  202. package/dist/workspace/discover.js +195 -0
  203. package/dist/workspace/documents.d.ts +21 -0
  204. package/dist/workspace/documents.js +395 -0
  205. package/dist/workspace/ignore.d.ts +10 -0
  206. package/dist/workspace/ignore.js +48 -0
  207. package/dist/workspace/index.d.ts +7 -0
  208. package/dist/workspace/index.js +3 -0
  209. package/dist/workspace/repositories.d.ts +24 -0
  210. package/dist/workspace/repositories.js +411 -0
  211. package/dist/workspace/resolve.d.ts +24 -0
  212. package/dist/workspace/resolve.js +268 -0
  213. package/dist/workspace/types.d.ts +71 -0
  214. package/dist/workspace/types.js +1 -0
  215. package/dist/workspace/validation.d.ts +18 -0
  216. package/dist/workspace/validation.js +197 -0
  217. package/package.json +105 -4
  218. package/skills/pappra/SKILL.md +68 -0
  219. package/skills/pappra/references/hosts.md +30 -0
  220. package/skills/pappra/references/lifecycle.md +37 -0
  221. package/skills/pappra/references/operations.md +14 -0
@@ -0,0 +1,130 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { open, lstat, realpath, stat } from 'node:fs/promises';
3
+ import { constants } from 'node:fs';
4
+ import { errorMessage, failed, succeeded } from '../schemas/outcome.js';
5
+ import { diagnosticCodes } from '../schemas/errors.js';
6
+ export const DEFAULT_CONFIG_MAX_BYTES = 1024 * 1024;
7
+ export function sha256Digest(bytes) {
8
+ return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
9
+ }
10
+ function filesystemFailure(path, error) {
11
+ const code = typeof error === 'object' &&
12
+ error !== null &&
13
+ 'code' in error &&
14
+ typeof error.code === 'string'
15
+ ? error.code
16
+ : null;
17
+ if (code === 'ENOENT') {
18
+ return failed(diagnosticCodes.fileNotFound, `File not found: ${path}`, {
19
+ nextAction: 'Create the file or correct the path.',
20
+ });
21
+ }
22
+ return failed(diagnosticCodes.fileUnreadable, `Cannot read ${path}: ${errorMessage(error)}`, { nextAction: 'Check file permissions and the path.' });
23
+ }
24
+ /**
25
+ * Reads at most `maxBytes + 1` bytes from a regular file. The implementation
26
+ * never loads a larger file into memory just to report that it is too large.
27
+ */
28
+ export async function readFileBounded(path, maxBytes = DEFAULT_CONFIG_MAX_BYTES) {
29
+ // Check the type before opening: opening a FIFO for reading can block until
30
+ // a writer appears. O_NONBLOCK also covers a file replaced after this stat.
31
+ let pathStats;
32
+ try {
33
+ pathStats = await stat(path);
34
+ }
35
+ catch (error) {
36
+ return filesystemFailure(path, error);
37
+ }
38
+ if (!pathStats.isFile()) {
39
+ return failed(diagnosticCodes.fileNotRegular, `Not a regular file: ${path}`, { nextAction: 'Replace it with a regular file.' });
40
+ }
41
+ let handle;
42
+ try {
43
+ handle = await open(path, constants.O_RDONLY |
44
+ (process.platform === 'win32' ? 0 : constants.O_NONBLOCK));
45
+ }
46
+ catch (error) {
47
+ return filesystemFailure(path, error);
48
+ }
49
+ try {
50
+ const stats = await handle.stat();
51
+ if (!stats.isFile()) {
52
+ return failed(diagnosticCodes.fileNotRegular, `Not a regular file: ${path}`, { nextAction: 'Replace it with a regular file.' });
53
+ }
54
+ const buffer = Buffer.allocUnsafe(maxBytes + 1);
55
+ let total = 0;
56
+ while (total < buffer.length) {
57
+ const { bytesRead } = await handle.read(buffer, total, buffer.length - total, null);
58
+ if (bytesRead === 0)
59
+ break;
60
+ total += bytesRead;
61
+ }
62
+ if (total > maxBytes) {
63
+ return failed(diagnosticCodes.fileTooLarge, `File exceeds the ${maxBytes}-byte limit: ${path}`, {
64
+ nextAction: 'Reduce the file size; Pappra does not truncate configuration.',
65
+ });
66
+ }
67
+ const bytes = buffer.subarray(0, total);
68
+ return succeeded({
69
+ bytes,
70
+ digest: sha256Digest(bytes),
71
+ size: total,
72
+ });
73
+ }
74
+ catch (error) {
75
+ return filesystemFailure(path, error);
76
+ }
77
+ finally {
78
+ await handle.close().catch(() => undefined);
79
+ }
80
+ }
81
+ export async function readTextFileBounded(path, maxBytes = DEFAULT_CONFIG_MAX_BYTES) {
82
+ const read = await readFileBounded(path, maxBytes);
83
+ if (!read.ok)
84
+ return read;
85
+ let text;
86
+ try {
87
+ text = new TextDecoder('utf-8', {
88
+ fatal: true,
89
+ ignoreBOM: true,
90
+ }).decode(read.value.bytes);
91
+ }
92
+ catch (error) {
93
+ return failed(diagnosticCodes.fileInvalidEncoding, `File is not valid UTF-8: ${path} (${errorMessage(error)})`, { nextAction: 'Re-save the file as UTF-8 without invalid bytes.' });
94
+ }
95
+ return succeeded({ ...read.value, text });
96
+ }
97
+ /** Observes one path segment without following a final symlink. */
98
+ export async function observePath(path) {
99
+ try {
100
+ const stats = await lstat(path);
101
+ const kind = stats.isSymbolicLink()
102
+ ? 'symlink'
103
+ : stats.isDirectory()
104
+ ? 'directory'
105
+ : stats.isFile()
106
+ ? 'file'
107
+ : 'other';
108
+ return { kind, path, stats };
109
+ }
110
+ catch (error) {
111
+ const code = typeof error === 'object' &&
112
+ error !== null &&
113
+ 'code' in error &&
114
+ typeof error.code === 'string'
115
+ ? error.code
116
+ : null;
117
+ if (code === 'ENOENT' || code === 'ENOTDIR') {
118
+ return { kind: 'missing', path, stats: null };
119
+ }
120
+ throw error;
121
+ }
122
+ }
123
+ export async function resolveRealpath(path) {
124
+ try {
125
+ return await realpath(path);
126
+ }
127
+ catch {
128
+ return null;
129
+ }
130
+ }
@@ -0,0 +1,60 @@
1
+ import type { EvidenceRef } from '../schemas/evidence.js';
2
+ import type { Outcome } from '../schemas/outcome.js';
3
+ export declare const DEFAULT_GIT_TIMEOUT_MS = 5000;
4
+ export declare const DEFAULT_GIT_MAX_OUTPUT_BYTES: number;
5
+ export interface RunGitOptions {
6
+ readonly cwd: string;
7
+ readonly args: readonly string[];
8
+ readonly timeoutMs?: number;
9
+ readonly maxOutputBytes?: number;
10
+ /** Exit codes that still produce an observable result. Defaults to [0]. */
11
+ readonly acceptedExitCodes?: readonly number[];
12
+ /** Bytes written to git stdin, for commands such as check-ignore --stdin. */
13
+ readonly input?: string;
14
+ readonly signal?: AbortSignal;
15
+ }
16
+ export interface GitCommandResult {
17
+ readonly args: readonly string[];
18
+ readonly stdout: string;
19
+ readonly stdoutBytes: number;
20
+ readonly stderr: string;
21
+ readonly exitCode: number;
22
+ readonly durationMs: number;
23
+ }
24
+ /**
25
+ * Runs one git command with an argv boundary, a timeout, and bounded output.
26
+ * Discovery never fetches, checks out, or runs hooks here; every call is a
27
+ * read-only query.
28
+ */
29
+ export declare function runGit(options: RunGitOptions): Promise<Outcome<GitCommandResult>>;
30
+ export type GitRepositoryKind = 'regular' | 'linked-worktree' | 'submodule' | 'bare';
31
+ export interface GitRepositoryObservation {
32
+ readonly kind: GitRepositoryKind;
33
+ readonly root: string;
34
+ readonly gitDir: string;
35
+ readonly commonDir: string;
36
+ readonly superproject: string | null;
37
+ readonly head: string | null;
38
+ readonly branch: string | null;
39
+ readonly detached: boolean;
40
+ readonly dirty: boolean | null;
41
+ readonly limitations: readonly string[];
42
+ readonly evidence: readonly EvidenceRef[];
43
+ }
44
+ export interface InspectGitRepositoryOptions {
45
+ readonly timeoutMs?: number;
46
+ readonly maxOutputBytes?: number;
47
+ readonly signal?: AbortSignal;
48
+ }
49
+ /**
50
+ * Observes a repository without modifying the index, refs, or working tree.
51
+ * The caller supplies the expected checkout root; a subdirectory of a larger
52
+ * repository is rejected instead of being silently classified as the root.
53
+ */
54
+ export declare function inspectGitRepository(candidateRoot: string, options?: InspectGitRepositoryOptions): Promise<Outcome<GitRepositoryObservation>>;
55
+ /**
56
+ * Returns the absolute paths ignored by git for the supplied candidates.
57
+ * `check-ignore` is read-only; a failure is reported rather than treated as
58
+ * "nothing is ignored".
59
+ */
60
+ export declare function filterGitIgnored(repositoryRoot: string, candidates: readonly string[], options?: InspectGitRepositoryOptions): Promise<Outcome<readonly string[]>>;
@@ -0,0 +1,358 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { relative, resolve, sep } from 'node:path';
3
+ import { diagnosticCodes } from '../schemas/errors.js';
4
+ import { errorMessage, failed, succeeded } from '../schemas/outcome.js';
5
+ import { samePath } from '../schemas/paths.js';
6
+ import { sha256Digest, resolveRealpath } from './fs.js';
7
+ export const DEFAULT_GIT_TIMEOUT_MS = 5_000;
8
+ export const DEFAULT_GIT_MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
9
+ function sanitizedGitEnvironment() {
10
+ const environment = { ...process.env };
11
+ for (const key of [
12
+ 'GIT_DIR',
13
+ 'GIT_WORK_TREE',
14
+ 'GIT_COMMON_DIR',
15
+ 'GIT_INDEX_FILE',
16
+ 'GIT_OBJECT_DIRECTORY',
17
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES',
18
+ 'GIT_NAMESPACE',
19
+ 'GIT_PREFIX',
20
+ 'GIT_CEILING_DIRECTORIES',
21
+ 'GIT_CONFIG_COUNT',
22
+ 'GIT_CONFIG_PARAMETERS',
23
+ ]) {
24
+ delete environment[key];
25
+ }
26
+ environment.GIT_OPTIONAL_LOCKS = '0';
27
+ environment.GIT_TERMINAL_PROMPT = '0';
28
+ environment.GIT_PAGER = 'cat';
29
+ environment.LC_ALL = 'C';
30
+ return environment;
31
+ }
32
+ /**
33
+ * Runs one git command with an argv boundary, a timeout, and bounded output.
34
+ * Discovery never fetches, checks out, or runs hooks here; every call is a
35
+ * read-only query.
36
+ */
37
+ export function runGit(options) {
38
+ if (options.signal?.aborted === true) {
39
+ return Promise.resolve(failed(diagnosticCodes.gitAborted, 'Git observation was cancelled.', {
40
+ nextAction: 'Retry discovery when cancellation is no longer requested.',
41
+ }));
42
+ }
43
+ const cwd = resolve(options.cwd);
44
+ const timeoutMs = options.timeoutMs ?? DEFAULT_GIT_TIMEOUT_MS;
45
+ const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_GIT_MAX_OUTPUT_BYTES;
46
+ const acceptedExitCodes = options.acceptedExitCodes ?? [0];
47
+ const startedAt = performance.now();
48
+ return new Promise((resolveResult) => {
49
+ let settled = false;
50
+ let timedOut = false;
51
+ let aborted = false;
52
+ let outputExceeded = false;
53
+ const stdoutChunks = [];
54
+ const stderrChunks = [];
55
+ let outputBytes = 0;
56
+ let timer = null;
57
+ let child;
58
+ const settle = (result) => {
59
+ if (settled)
60
+ return;
61
+ settled = true;
62
+ if (timer !== null)
63
+ clearTimeout(timer);
64
+ options.signal?.removeEventListener('abort', abort);
65
+ resolveResult(result);
66
+ };
67
+ const abort = () => {
68
+ aborted = true;
69
+ child.kill('SIGKILL');
70
+ };
71
+ try {
72
+ child = spawn('git', ['-c', 'core.fsmonitor=false', ...options.args], {
73
+ cwd,
74
+ shell: false,
75
+ env: sanitizedGitEnvironment(),
76
+ stdio: [
77
+ options.input === undefined ? 'ignore' : 'pipe',
78
+ 'pipe',
79
+ 'pipe',
80
+ ],
81
+ });
82
+ }
83
+ catch (error) {
84
+ settle(failed(diagnosticCodes.gitUnavailable, `Cannot start git: ${errorMessage(error)}`, { nextAction: 'Install git and ensure it is on PATH.' }));
85
+ return;
86
+ }
87
+ timer = setTimeout(() => {
88
+ timedOut = true;
89
+ child.kill('SIGKILL');
90
+ }, timeoutMs);
91
+ options.signal?.addEventListener('abort', abort, { once: true });
92
+ child.stdout?.on('data', (chunk) => {
93
+ outputBytes += chunk.byteLength;
94
+ if (outputBytes > maxOutputBytes) {
95
+ outputExceeded = true;
96
+ child.kill('SIGKILL');
97
+ return;
98
+ }
99
+ stdoutChunks.push(chunk);
100
+ });
101
+ child.stderr?.on('data', (chunk) => {
102
+ outputBytes += chunk.byteLength;
103
+ if (outputBytes > maxOutputBytes) {
104
+ outputExceeded = true;
105
+ child.kill('SIGKILL');
106
+ return;
107
+ }
108
+ stderrChunks.push(chunk);
109
+ });
110
+ child.on('error', (error) => {
111
+ settle(failed(diagnosticCodes.gitUnavailable, `Cannot run git ${options.args.join(' ')}: ${errorMessage(error)}`, { nextAction: 'Install git and ensure it is on PATH.' }));
112
+ });
113
+ child.on('close', (exitCode, signal) => {
114
+ const durationMs = Math.round(performance.now() - startedAt);
115
+ const stdout = Buffer.concat(stdoutChunks).toString('utf8');
116
+ const stderr = Buffer.concat(stderrChunks).toString('utf8');
117
+ if (timedOut) {
118
+ settle(failed(diagnosticCodes.gitTimeout, `git ${options.args[0] ?? ''} timed out after ${timeoutMs}ms in ${cwd}.`, {
119
+ retryable: true,
120
+ nextAction: 'Retry with a healthy repository or a larger timeout.',
121
+ }));
122
+ return;
123
+ }
124
+ if (aborted) {
125
+ settle(failed(diagnosticCodes.gitAborted, 'Git observation was cancelled.', {
126
+ nextAction: 'Retry discovery when cancellation is no longer requested.',
127
+ }));
128
+ return;
129
+ }
130
+ if (outputExceeded) {
131
+ settle(failed(diagnosticCodes.gitOutputLimit, `git ${options.args[0] ?? ''} exceeded the ${maxOutputBytes}-byte output limit in ${cwd}.`, {
132
+ nextAction: 'Narrow the repository scope; output was not truncated into a success.',
133
+ }));
134
+ return;
135
+ }
136
+ if (exitCode === null) {
137
+ settle(failed(diagnosticCodes.gitCommandFailed, `git ${options.args[0] ?? ''} ended without an exit code (signal ${signal ?? 'unknown'}).`, {
138
+ retryable: true,
139
+ nextAction: 'Retry and inspect the repository state.',
140
+ }));
141
+ return;
142
+ }
143
+ if (!acceptedExitCodes.includes(exitCode)) {
144
+ const detail = stderr.trim().split('\n')[0]?.slice(0, 500) ?? '';
145
+ settle(failed(diagnosticCodes.gitCommandFailed, `git ${options.args.join(' ')} exited ${exitCode} in ${cwd}${detail === '' ? '' : `: ${detail}`}`, { nextAction: 'Inspect the repository and git installation.' }));
146
+ return;
147
+ }
148
+ settle(succeeded({
149
+ args: options.args,
150
+ stdout,
151
+ stdoutBytes: Buffer.byteLength(stdout, 'utf8'),
152
+ stderr,
153
+ exitCode,
154
+ durationMs,
155
+ }));
156
+ });
157
+ if (options.input !== undefined && child.stdin !== null) {
158
+ child.stdin.on('error', () => undefined);
159
+ child.stdin.end(options.input);
160
+ }
161
+ });
162
+ }
163
+ function gitEvidence(ref, stdout) {
164
+ return {
165
+ kind: 'git',
166
+ ref,
167
+ digest: sha256Digest(stdout),
168
+ observedAt: new Date().toISOString(),
169
+ scope: 'live-owned',
170
+ };
171
+ }
172
+ function splitLines(value) {
173
+ return value
174
+ .split('\n')
175
+ .map((line) => line.trimEnd())
176
+ .filter((line) => line.length > 0);
177
+ }
178
+ /**
179
+ * Observes a repository without modifying the index, refs, or working tree.
180
+ * The caller supplies the expected checkout root; a subdirectory of a larger
181
+ * repository is rejected instead of being silently classified as the root.
182
+ */
183
+ export async function inspectGitRepository(candidateRoot, options = {}) {
184
+ const root = (await resolveRealpath(candidateRoot)) ?? resolve(candidateRoot);
185
+ const evidence = [];
186
+ const limitations = [];
187
+ const run = (args) => runGit({
188
+ cwd: root,
189
+ args,
190
+ ...(options.timeoutMs === undefined
191
+ ? {}
192
+ : { timeoutMs: options.timeoutMs }),
193
+ ...(options.maxOutputBytes === undefined
194
+ ? {}
195
+ : { maxOutputBytes: options.maxOutputBytes }),
196
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
197
+ });
198
+ const optional = (args) => runGit({
199
+ cwd: root,
200
+ args,
201
+ acceptedExitCodes: [0, 1],
202
+ ...(options.timeoutMs === undefined
203
+ ? {}
204
+ : { timeoutMs: options.timeoutMs }),
205
+ ...(options.maxOutputBytes === undefined
206
+ ? {}
207
+ : { maxOutputBytes: options.maxOutputBytes }),
208
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
209
+ });
210
+ const bareResult = await run(['rev-parse', '--is-bare-repository']);
211
+ if (!bareResult.ok)
212
+ return bareResult;
213
+ evidence.push(gitEvidence('git:rev-parse --is-bare-repository', bareResult.value.stdout));
214
+ const isBare = bareResult.value.stdout.trim() === 'true';
215
+ const identity = await run([
216
+ 'rev-parse',
217
+ '--path-format=absolute',
218
+ ...(isBare ? [] : ['--show-toplevel']),
219
+ '--absolute-git-dir',
220
+ '--git-common-dir',
221
+ ]);
222
+ if (!identity.ok)
223
+ return identity;
224
+ evidence.push(gitEvidence('git:rev-parse identity', identity.value.stdout));
225
+ const lines = splitLines(identity.value.stdout);
226
+ const expectedLines = isBare ? 2 : 3;
227
+ if (lines.length < expectedLines) {
228
+ return failed(diagnosticCodes.gitCommandFailed, `git returned an incomplete repository identity for ${root}.`, {
229
+ nextAction: 'Inspect the repository metadata; no configuration was changed.',
230
+ });
231
+ }
232
+ const observedRoot = isBare ? root : (lines[0] ?? '');
233
+ const gitDir = lines[isBare ? 0 : 1] ?? '';
234
+ const commonDir = lines[isBare ? 1 : 2] ?? '';
235
+ if (!isBare) {
236
+ const realRoot = root;
237
+ if (!samePath(resolve(observedRoot), realRoot)) {
238
+ return failed(diagnosticCodes.repositoryNotGit, `${root} is not a repository root; git resolves it to ${observedRoot}.`, {
239
+ nextAction: 'Declare the actual repository root, or discover it as part of a scan.',
240
+ });
241
+ }
242
+ }
243
+ const superprojectResult = await run([
244
+ 'rev-parse',
245
+ '--show-superproject-working-tree',
246
+ ]);
247
+ let superproject = null;
248
+ if (superprojectResult.ok) {
249
+ const value = superprojectResult.value.stdout.trim();
250
+ superproject = value === '' ? null : value;
251
+ }
252
+ else {
253
+ limitations.push('superproject state unavailable');
254
+ }
255
+ const headResult = await optional([
256
+ 'rev-parse',
257
+ '--verify',
258
+ '--quiet',
259
+ 'HEAD',
260
+ ]);
261
+ let head = null;
262
+ if (headResult.ok) {
263
+ const value = headResult.value.stdout.trim();
264
+ head = value === '' ? null : value;
265
+ }
266
+ else {
267
+ limitations.push('HEAD unavailable');
268
+ }
269
+ const branchResult = await optional([
270
+ 'symbolic-ref',
271
+ '--short',
272
+ '-q',
273
+ 'HEAD',
274
+ ]);
275
+ let branch = null;
276
+ if (branchResult.ok) {
277
+ const value = branchResult.value.stdout.trim();
278
+ branch = value === '' ? null : value;
279
+ }
280
+ else {
281
+ limitations.push('branch unavailable');
282
+ }
283
+ let dirty = null;
284
+ const statusResult = await run([
285
+ 'status',
286
+ '--porcelain=v1',
287
+ '-z',
288
+ '--untracked-files=normal',
289
+ ]);
290
+ if (statusResult.ok) {
291
+ dirty = statusResult.value.stdout.length > 0;
292
+ }
293
+ else {
294
+ limitations.push(`working tree state unavailable: ${statusResult.message}`);
295
+ }
296
+ const kind = isBare
297
+ ? 'bare'
298
+ : !samePath(gitDir, commonDir)
299
+ ? 'linked-worktree'
300
+ : superproject === null
301
+ ? 'regular'
302
+ : 'submodule';
303
+ return succeeded({
304
+ kind,
305
+ root: isBare ? root : resolve(observedRoot),
306
+ gitDir: resolve(gitDir),
307
+ commonDir: resolve(commonDir),
308
+ superproject,
309
+ head,
310
+ branch,
311
+ detached: head !== null && branch === null,
312
+ dirty,
313
+ limitations,
314
+ evidence,
315
+ });
316
+ }
317
+ /**
318
+ * Returns the absolute paths ignored by git for the supplied candidates.
319
+ * `check-ignore` is read-only; a failure is reported rather than treated as
320
+ * "nothing is ignored".
321
+ */
322
+ export async function filterGitIgnored(repositoryRoot, candidates, options = {}) {
323
+ if (candidates.length === 0)
324
+ return succeeded([]);
325
+ const root = resolve(repositoryRoot);
326
+ const relativePaths = [];
327
+ for (const candidate of candidates) {
328
+ const path = relative(root, candidate);
329
+ if (path === '' || path === '..' || path.startsWith(`..${sep}`))
330
+ continue;
331
+ relativePaths.push(path.split('\\').join('/'));
332
+ }
333
+ if (relativePaths.length === 0)
334
+ return succeeded([]);
335
+ const input = `${relativePaths.join('\0')}\0`;
336
+ const result = await runGit({
337
+ cwd: root,
338
+ args: ['check-ignore', '--stdin', '-z'],
339
+ input,
340
+ acceptedExitCodes: [0, 1],
341
+ ...(options.timeoutMs === undefined
342
+ ? {}
343
+ : { timeoutMs: options.timeoutMs }),
344
+ ...(options.maxOutputBytes === undefined
345
+ ? {}
346
+ : { maxOutputBytes: options.maxOutputBytes }),
347
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
348
+ });
349
+ if (!result.ok)
350
+ return result;
351
+ const ignored = [];
352
+ for (const item of result.value.stdout.split('\0')) {
353
+ if (item === '')
354
+ continue;
355
+ ignored.push(resolve(root, ...item.split('/')));
356
+ }
357
+ return succeeded(ignored);
358
+ }