Repository navigation
✨ Read tracked files, changes, diffs and committed text (#940) - #944
Conversation
`<Git.Files>`, `<Git.Changes>`, `<Git.Diff>` and `<Git.File>` answer four read-only questions about the checkout a Repository selected, each binding JSON through a required ordinary `as`. A page is bounded by the caller's own limit — 100 records or 4096 code points by default, and whatever larger safe integer is asked for — and `next` holds the complete arguments of the read that continues it. A REPL entry retains each page, so a reopened entry answers from its record with no `git` at all and the entry after it inherits the bound values. They reach the same `GitQuery` seam the legacy questions do, at the same implementation slot the four transitions install at, and confer none of their evidence: nothing a read returns authorizes a mutation, a publication, a pull request or an Agent, and a `cursor` is continuation data rather than permission. A read changes nothing in the repository it reads, and the repository's own configuration cannot make it: each observation runs against isolated Git administration and a contained read-only view of the authenticated object database, with a private copy of the correct per-checkout index. Two defects the evidence caught and this fixes: a split index resolves its shared data beside the index *file* rather than the Git directory, and a linked worktree keeps its own shared index under its own administration; and copying an index must carry the source's modification time, because that time is what Git's racily-clean rule compares against — without it a working-tree edit made in the same second as the commit that recorded it was reported as no change at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four corrections both #940 reviewers reproduced against 94496999. **A directory is a directory under either spelling.** `<Git.File path="dir/" />` answered with the blob at `dir/inside.txt` under a path claiming to be `dir/`. The argument Git receives is a pathspec rather than a lookup: `dir` matches the directory entry and `dir/` matches the things inside it, and the read took the first record without checking whose path it was. It now resolves the entry at exactly the path asked for; a path that matched records but is not one of them is the directory it is, and refuses. **Trusted admission holds a saved route to the history that made it.** A fully shaped observation whose checkout named a root nothing selected was admitted, and its completed terminal reused with no Git at all — the body that checks the live registry is never entered for a completed terminal. Admission now requires the selection and the checkout to be ones a preceding placement in that same history made, before completed or unfinished reuse, decided from parsed historical facts with no canonicalization and no `git`. A cold reuse whose checkout has since been deleted still answers, and a history holding no observation record is unaffected. **One entry's reads and mutations share one native turn.** The reads keyed it `observation <commonDirectory>` while the transitions and the publication keyed it `git <commonDirectory>`, so sharing the serializer shared nothing: a same-entry `<Git.Add>` reached native Git while a `<Git.Diff>` still owned its multi-command read. The key is now named once, `nativeGitTurn()`, and all three callers take it — held through joined teardown and the acknowledged append. Cross-entry independence is untouched: that is a mutation lease, not this queue. **The journey's two missing checkpoint-2 reads and its per-phase immutability evidence** are completed in `sidekick-journey-completion.md` through the real terminal and ACP Sidekick, with paired before/after fingerprints around every phase. The original journey's proven portions stand. Its host count is corrected to 95 of 447 with 8 `config` calls, and its unverifiable tree metadata is withdrawn for a tree object this repository holds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| return unreadable(view, "its tracked files"); | ||
| } | ||
| // One path per entry: an unmerged path has an index entry per stage, and a | ||
| // listing that repeated it would report a file three times. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // listing that repeated it would report a file three times. |
| return result.stdout; | ||
| }, | ||
|
|
||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| *files(_invocation: GitFilesInvocation): Operation<GitFilesResult> { | ||
| throw new GitObservationProviderError("<Git.Files>"); | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| *changes(_invocation: GitChangesInvocation): Operation<GitChangesResult> { | ||
| throw new GitObservationProviderError("<Git.Changes>"); | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| *diff(_invocation: GitDiffInvocation): Operation<GitDiffResult> { | ||
| throw new GitObservationProviderError("<Git.Diff>"); | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| git(["commit", "-m", "sentinels"], root, home); | ||
| // The fixture's own commands ran with these settings in place, so whatever | ||
| // they fired is the baseline. It is removed here, before any read, so what | ||
| // the marker holds afterwards is what a read did. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // the marker holds afterwards is what a read did. |
| required: ["value"], | ||
| additionalProperties: false, | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| const working = options.cwd ?? place.link; | ||
| yield* API.Env.around( | ||
| { | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| ); | ||
| // What a runtime entrypoint installs beside the provider: `API.Files` has | ||
| // no host default, and a document that writes `<File>` must reach the | ||
| // caller's own filesystem exactly as the REPL leaves it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // caller's own filesystem exactly as the REPL leaves it. |
| yield* Elicitation.around({ | ||
| *elicit(): Operation<unknown> { | ||
| reached.resolve(); | ||
| // Nobody answers, so this is where the entry is when it is left. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Nobody answers, so this is where the entry is when it is left. |
PR #944: ✨ Read tracked files, changes, diffs and committed text (#940)38 files, +8202 / -105 Scope🔴 PR has 8307 lines changed. Split into focused PRs. 🟡 8307 lines changed. PRs under 400 receive more thorough review. 🟡 38 files changed. Are all changes related? 🟡 Changes span 7 directories. StructuralOxlint structural signals:
Slop
Static AnalysisOxlint: 40 diagnostics across 14 files (11 rules) no-unused-vars (9): packages/git/tests/support/observations.ts, packages/git/src/deno/composition/observations.ts, packages/git/src/deno/run-composition/provider.ts (+3) CorrectnessNo extraneous code patterns detected. |
Integrates the approved #939 head 0217795, which PR943 publishes and CI run 39135245943 reports green, into this already-published branch. An additive merge rather than a rebase: cfb997c is published, both original reviewers PASSed it at that exact SHA, and rewriting it would discard that. What arrives is one commit — `<Fail>` marks its own throw as an authored stop, and the settling boundary in `execute.ts` uses that mark instead of inferring intent from an undescribed failure. It names this story's refusals among the things that must keep escaping, which is why the merge is worth evidence rather than a formality: a Git protocol refusal, an admission rejection and a secret-gate fatal all reach that boundary, and none of them is a document deciding to stop. Nothing overlapped. The parent touches `packages/core/src/components/Fail.ts`, `packages/core/src/execute.ts` and `packages/cli/src/repl/model.ts`; this story touches none of them, and its own 35 files are byte-identical to the reviewed head. The three incoming files are byte-identical to the approved parent, so the merged tree adds no third version of either side. Evidence on the merged tree, each passing on its first run: F1 18 passed, F2 9, F3 15 including RGO1–RGO5, F4 13, with 0 failed throughout. The parent's own boundary suites — `fail-component` and `output-error-mode` — also pass here, 15 passed, which is what rules out this story's refusals being swallowed as authored stops. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`<Git.Files>`, `<Git.Changes>`, `<Git.Diff>` and `<Git.File>` answer four read-only questions about the checkout a Repository selected, each binding JSON through a required ordinary `as`. A page is bounded by the caller's own limit — 100 records or 4096 code points by default, and whatever larger safe integer is asked for — and `next` holds the complete arguments of the read that continues it. A REPL entry retains each page, so a reopened entry answers from its record with no `git` at all and the entry after it inherits the bound values. They reach the same `GitQuery` seam the legacy questions do, at the same implementation slot the four transitions install at, and confer none of their evidence: nothing a read returns authorizes a mutation, a publication, a pull request or an Agent, and a `cursor` is continuation data rather than permission. A read changes nothing in the repository it reads, and the repository's own configuration cannot make it: each observation runs against isolated Git administration and a contained read-only view of the authenticated object database, with a private copy of the correct per-checkout index. Two defects the evidence caught and this fixes: a split index resolves its shared data beside the index *file* rather than the Git directory, and a linked worktree keeps its own shared index under its own administration; and copying an index must carry the source's modification time, because that time is what Git's racily-clean rule compares against — without it a working-tree edit made in the same second as the commit that recorded it was reported as no change at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four corrections both #940 reviewers reproduced against 94496999. **A directory is a directory under either spelling.** `<Git.File path="dir/" />` answered with the blob at `dir/inside.txt` under a path claiming to be `dir/`. The argument Git receives is a pathspec rather than a lookup: `dir` matches the directory entry and `dir/` matches the things inside it, and the read took the first record without checking whose path it was. It now resolves the entry at exactly the path asked for; a path that matched records but is not one of them is the directory it is, and refuses. **Trusted admission holds a saved route to the history that made it.** A fully shaped observation whose checkout named a root nothing selected was admitted, and its completed terminal reused with no Git at all — the body that checks the live registry is never entered for a completed terminal. Admission now requires the selection and the checkout to be ones a preceding placement in that same history made, before completed or unfinished reuse, decided from parsed historical facts with no canonicalization and no `git`. A cold reuse whose checkout has since been deleted still answers, and a history holding no observation record is unaffected. **One entry's reads and mutations share one native turn.** The reads keyed it `observation <commonDirectory>` while the transitions and the publication keyed it `git <commonDirectory>`, so sharing the serializer shared nothing: a same-entry `<Git.Add>` reached native Git while a `<Git.Diff>` still owned its multi-command read. The key is now named once, `nativeGitTurn()`, and all three callers take it — held through joined teardown and the acknowledged append. Cross-entry independence is untouched: that is a mutation lease, not this queue. **The journey's two missing checkpoint-2 reads and its per-phase immutability evidence** are completed in `sidekick-journey-completion.md` through the real terminal and ACP Sidekick, with paired before/after fingerprints around every phase. The original journey's proven portions stand. Its host count is corrected to 95 of 447 with 8 `config` calls, and its unverifiable tree metadata is withdrawn for a tree object this repository holds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cfb997c to
368aa6d
Compare
| */ | ||
|
|
||
| // The read-only Git questions, and the four operations pre-bound for callers | ||
| // The read-only Git questions, and the eight operations pre-bound for callers |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // The read-only Git questions, and the eight operations pre-bound for callers |
| export type { GitObjectFormat, GitQueryApi } from "./src/git.ts"; | ||
|
|
||
| // What the four selected-checkout reads ask for and answer with, and the | ||
| // continuation that carries one page to the next. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // continuation that carries one page to the next. |
| return Object.freeze({ text: "", length, nextOffset: null }); | ||
| } | ||
| // Subtraction rather than `offset + limit`, which a safe-integer limit can | ||
| // carry past what integer arithmetic represents. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // carry past what integer arithmetic represents. |
| return undefined; | ||
| } | ||
| // The continuation stays on the commit this read resolved. A next page naming | ||
| // another commit would read a different file under the same question. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // another commit would read a different file under the same question. |
| // whose shared bytes sat anywhere else would be an index it cannot open. Both | ||
| // directories are searched because a linked worktree keeps its own shared | ||
| // index under its own administration while the primary keeps one in the | ||
| // common directory. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // common directory. |
| *changes(_invocation: GitChangesInvocation): Operation<GitChangesResult> { | ||
| throw new GitObservationProviderError("<Git.Changes>"); | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| *diff(_invocation: GitDiffInvocation): Operation<GitDiffResult> { | ||
| throw new GitObservationProviderError("<Git.Diff>"); | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| export function* admitReplRepositoryHistory(retained: readonly DurableEvent[]): Operation<void> { | ||
| // The placements this history has selected *so far*. An observation names the | ||
| // checkout it read, and nothing outside this snapshot may authorize one: the | ||
| // route has to be a placement a preceding record in the same history made. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // route has to be a placement a preceding record in the same history made. |
| required: ["value"], | ||
| additionalProperties: false, | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| const working = options.cwd ?? place.link; | ||
| yield* API.Env.around( | ||
| { | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
After #943 merged, GitHub rebased this branch onto the new main, so the remote held 368aa6d while the signed integration held 3d10513. Both carried the same tree. This merges the remote head into the signed branch, which keeps every published object reachable, makes the remote an ancestor again, and leaves a normal fast-forward as the way to publish. The merge changes nothing: the tree is still 9fd3794, the tree both sides already had and the one F1-F4 and the parent-boundary suites passed on. Nothing was rebased, force pushed or reset, the deleted parent branch stays deleted, and the pull request keeps main as its base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eight jobs failed on 368aa6d, not the one the first connector page showed. Four distinct causes, all this story's own. **The vocabulary grew and one count did not.** ORC2 asserts that the Plugin's registration array and the syntax catalog describe the same names, and it still expected twelve. The four reads make sixteen. The list it compares against gets the four names, and `OPERATIONS` gets them too: that suite's own prose says every element needing a provider reports an absent one, and leaving the reads out would have made the sentence false rather than the gate smaller. **Three cases need a runtime that operates repositories.** GO9's first case clones into a managed root and adds a worktree, which this repository holds under a kernel-released advisory lock taken through the Deno runtime; on Node and Bun the document fails on the lock before a read happens. ROB5's cancelled append, held-read mutation and foreign mutation lease need the same lock, a real `git` child to exclude, and a Deno-backed Files provider to unwind. They move to `git-observations-deno.test.ts` and `retained-repository-observations-deno.test.ts` and join `DENO_ONLY_REPOSITORY_PROVIDER`, which is where this repository records such a decision for review. Only those four cases move: every provider-neutral case — the sibling checkouts, the refused foreign and edited selections, ROB1 through ROB4, the interrupted child, the fatal append — stays where it was and keeps running on all three runtimes. **Prose used a banned word.** Three JSDoc sentences said "claim" where they meant presented, expect, and what teardown has to show. **Thirteen files were unformatted and one export was dead.** `oxfmt --check` and the dead-code gate are the second half of the same lint job, which never reached them before — the banned words failed first. The formatter's output stands as it produced it, and `settled` in the REPL observation support had no consumer. Evidence on this tree: the whole `pnpm lint` gate passes, which is oxlint, the prose pass, the architecture check, `oxfmt --check` and the dead-code gate together. F1 18/57, F2 9/33, F3 15/52, F4 13/61, ORC with the two new files and the exclusions gate 13/32, all 0 failed. Node shards 4/10 and 6/10 — the two that failed — run 37 of 37 and 38 of 38 files green, and they are where `git-observations.test.ts`, `retained-repository-observations.test.ts` and `run-composition.test.ts` land. Bun 5/5 runs 75 of 75 green. `tsc --project tsconfig.node.json` is clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| return result.stdout; | ||
| }, | ||
|
|
||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| *files(_invocation: GitFilesInvocation): Operation<GitFilesResult> { | ||
| throw new GitObservationProviderError("<Git.Files>"); | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| *changes(_invocation: GitChangesInvocation): Operation<GitChangesResult> { | ||
| throw new GitObservationProviderError("<Git.Changes>"); | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| *diff(_invocation: GitDiffInvocation): Operation<GitDiffResult> { | ||
| throw new GitObservationProviderError("<Git.Diff>"); | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| required: ["value"], | ||
| additionalProperties: false, | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| const working = options.cwd ?? place.link; | ||
| yield* API.Env.around( | ||
| { | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| if (name !== replPlacementRecordName({ kind: record.kind, fingerprint })) { | ||
| throw misnamed(REPL_REPOSITORY_PLACEMENT); | ||
| } | ||
| // A refused selection placed nothing, so it authorizes no route. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // A refused selection placed nothing, so it authorizes no route. |
| yield* Elicitation.around({ | ||
| *elicit(): Operation<unknown> { | ||
| reached.resolve(); | ||
| // Nobody answers, so this is where the entry is when it is left. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Nobody answers, so this is where the entry is when it is left. |
The registry is sixteen names now, not twelve, and `<Git.Files>`, `<Git.Changes>`, `<Git.Diff>` and `<Git.File>` join the half a selected checkout answers for. Every count, every catalog sentence and the binding suite's A1 row say sixteen. `publish()` takes the shared `nativeGitTurn` that arrived with those reads, so one entry still serializes its native work through one turn. They also settle the question this delivery kept answering about `<PullRequest>` from the other side: a REPL entry retains every page those four answer with, as `GitQuery` middleware under the component, and declares none of their names. Retaining an operation is not what makes a body take its operation as an argument — owning a boundary that has to enclose the public chain is, and that is still `<Git.Push />` and `<Issue>` alone.
Why
Documents could select and change a repository, but lacked components for inspecting its files and changes. Authors can now read the selected checkout directly and retain the answers in REPL entries.
Closes #940. Builds on #939, merged in #943. This PR now targets
mainat0116500f2385bb9ab37cf277103e6df2b141f0b4; the approved parent changes are already included there.What changes
Git.Fileslists tracked paths, with an optional literal prefix.Git.Changesreturns changed paths and statuses for unstaged, staged or specified commit comparisons.Git.Diffreturns unified diff text for those comparisons, with optional path filtering.Git.Filereads exact text from a specified commit and keeps subsequent windows pinned to its resolved commit ID.For example, inspect the staged patch for one directory:
Each self-closing read requires
asand binds JSON. Results default to 100 records or 4096 code points; callers can increase the limit. A completenextrequest continues the result. If a live listing or diff changes between pages, continuation refuses with restart guidance.Reads leave repository state unchanged. Saved REPL results reopen without Git, and later entries inherit their bound JSON through #939.
Validation
Final signed head:
d49c0c4b138b05ea3185d7c060340f2195feb525.The real terminal/ACP Sidekick journey and its bounded completion are historical evidence from the
7490f014runtime, not a fresh journey at this head. The observation behavior is unchanged; final-head tests prove cold reopen, cross-entry inheritance and the corrected parent result/failure boundary. Both original reviewers accepted this finite combination.Risks and limitations
The native provider is Deno-specific; a host without a selected-read provider refuses. These reads inspect tracked paths and committed text, excluding unrelated untracked files.
Git.Filerefuses directory paths. Earlier local Bun module-loading failures and a bounded shard timeout remain archived; they are not counted as local passes.Agent review evidence
Both original agents independently PASS the identical full head
d49c0c4b138b05ea3185d7c060340f2195feb525:stack-20261017-architect-940)architect-review-d49c0c4b.md3721062af89bae4455f2bdc0d5683230706af6d5bd38891b86aa4f2d05672086stack-20261017-planner-940)planner-review-d49c0c4b.mdd2fafc5550a5cb292a00c5ad889bdc99aa7f12adc2f310a6c5e9b9d81de451f2Reports and full evidence are retained centrally under
control/.development/stack/940/. Agent verdicts and the saved CI results are separate evidence; these are not GitHub review approvals.