ctxr-cli 0.23.0 → 0.25.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.
Files changed (197) hide show
  1. package/README.md +65 -5
  2. package/dist/commands/graph-diff.d.ts +33 -0
  3. package/dist/commands/graph-diff.d.ts.map +1 -0
  4. package/dist/commands/graph-diff.js +52 -0
  5. package/dist/commands/graph-diff.js.map +1 -0
  6. package/dist/commands/land.d.ts +50 -0
  7. package/dist/commands/land.d.ts.map +1 -0
  8. package/dist/commands/land.js +414 -0
  9. package/dist/commands/land.js.map +1 -0
  10. package/dist/commands/publish-check.d.ts +6 -0
  11. package/dist/commands/publish-check.d.ts.map +1 -1
  12. package/dist/commands/publish-check.js +78 -3
  13. package/dist/commands/publish-check.js.map +1 -1
  14. package/dist/commands/publish-gather.d.ts.map +1 -1
  15. package/dist/commands/publish-gather.js +4 -1
  16. package/dist/commands/publish-gather.js.map +1 -1
  17. package/dist/commands/publish-new.d.ts +5 -0
  18. package/dist/commands/publish-new.d.ts.map +1 -1
  19. package/dist/commands/publish-new.js +30 -10
  20. package/dist/commands/publish-new.js.map +1 -1
  21. package/dist/commands/publish-stamp.d.ts +32 -0
  22. package/dist/commands/publish-stamp.d.ts.map +1 -0
  23. package/dist/commands/publish-stamp.js +78 -0
  24. package/dist/commands/publish-stamp.js.map +1 -0
  25. package/dist/commands/publish-urls.d.ts +13 -0
  26. package/dist/commands/publish-urls.d.ts.map +1 -1
  27. package/dist/commands/publish-urls.js +23 -6
  28. package/dist/commands/publish-urls.js.map +1 -1
  29. package/dist/commands/review-list.d.ts +34 -0
  30. package/dist/commands/review-list.d.ts.map +1 -0
  31. package/dist/commands/review-list.js +49 -0
  32. package/dist/commands/review-list.js.map +1 -0
  33. package/dist/commands/serve.d.ts +3 -6
  34. package/dist/commands/serve.d.ts.map +1 -1
  35. package/dist/commands/serve.js +106 -9
  36. package/dist/commands/serve.js.map +1 -1
  37. package/dist/config/schema.d.ts +40 -0
  38. package/dist/config/schema.d.ts.map +1 -1
  39. package/dist/config/schema.js +39 -0
  40. package/dist/config/schema.js.map +1 -1
  41. package/dist/core/approval/decision.d.ts +73 -0
  42. package/dist/core/approval/decision.d.ts.map +1 -0
  43. package/dist/core/approval/decision.js +166 -0
  44. package/dist/core/approval/decision.js.map +1 -0
  45. package/dist/core/approval/file-diff.d.ts +32 -0
  46. package/dist/core/approval/file-diff.d.ts.map +1 -0
  47. package/dist/core/approval/file-diff.js +83 -0
  48. package/dist/core/approval/file-diff.js.map +1 -0
  49. package/dist/core/approval/identity.d.ts +34 -0
  50. package/dist/core/approval/identity.d.ts.map +1 -0
  51. package/dist/core/approval/identity.js +105 -0
  52. package/dist/core/approval/identity.js.map +1 -0
  53. package/dist/core/approval/landing-record.d.ts +17 -0
  54. package/dist/core/approval/landing-record.d.ts.map +1 -0
  55. package/dist/core/approval/landing-record.js +30 -0
  56. package/dist/core/approval/landing-record.js.map +1 -0
  57. package/dist/core/approval/resolver.d.ts +15 -0
  58. package/dist/core/approval/resolver.d.ts.map +1 -0
  59. package/dist/core/approval/resolver.js +41 -0
  60. package/dist/core/approval/resolver.js.map +1 -0
  61. package/dist/core/approval/sessions.d.ts +36 -0
  62. package/dist/core/approval/sessions.d.ts.map +1 -0
  63. package/dist/core/approval/sessions.js +76 -0
  64. package/dist/core/approval/sessions.js.map +1 -0
  65. package/dist/core/browse/approve.d.ts +45 -0
  66. package/dist/core/browse/approve.d.ts.map +1 -0
  67. package/dist/core/browse/approve.js +239 -0
  68. package/dist/core/browse/approve.js.map +1 -0
  69. package/dist/core/browse/diagram-assets.d.ts +15 -0
  70. package/dist/core/browse/diagram-assets.d.ts.map +1 -0
  71. package/dist/core/browse/diagram-assets.js +35 -0
  72. package/dist/core/browse/diagram-assets.js.map +1 -0
  73. package/dist/core/browse/page-url.d.ts +22 -0
  74. package/dist/core/browse/page-url.d.ts.map +1 -1
  75. package/dist/core/browse/page-url.js +31 -0
  76. package/dist/core/browse/page-url.js.map +1 -1
  77. package/dist/core/browse/render.d.ts +10 -1
  78. package/dist/core/browse/render.d.ts.map +1 -1
  79. package/dist/core/browse/render.js +23 -1
  80. package/dist/core/browse/render.js.map +1 -1
  81. package/dist/core/browse/review.d.ts +21 -0
  82. package/dist/core/browse/review.d.ts.map +1 -0
  83. package/dist/core/browse/review.js +52 -0
  84. package/dist/core/browse/review.js.map +1 -0
  85. package/dist/core/browse/templates.d.ts +4 -1
  86. package/dist/core/browse/templates.d.ts.map +1 -1
  87. package/dist/core/browse/templates.js +18 -2
  88. package/dist/core/browse/templates.js.map +1 -1
  89. package/dist/core/checks/organize-checks.d.ts +17 -0
  90. package/dist/core/checks/organize-checks.d.ts.map +1 -1
  91. package/dist/core/checks/organize-checks.js +116 -0
  92. package/dist/core/checks/organize-checks.js.map +1 -1
  93. package/dist/core/errors.d.ts +67 -0
  94. package/dist/core/errors.d.ts.map +1 -1
  95. package/dist/core/errors.js +160 -0
  96. package/dist/core/errors.js.map +1 -1
  97. package/dist/core/forge/credentials.d.ts +69 -0
  98. package/dist/core/forge/credentials.d.ts.map +1 -0
  99. package/dist/core/forge/credentials.js +197 -0
  100. package/dist/core/forge/credentials.js.map +1 -0
  101. package/dist/core/forge/github.d.ts +73 -0
  102. package/dist/core/forge/github.d.ts.map +1 -0
  103. package/dist/core/forge/github.js +101 -0
  104. package/dist/core/forge/github.js.map +1 -0
  105. package/dist/core/git/exec.d.ts +25 -0
  106. package/dist/core/git/exec.d.ts.map +1 -1
  107. package/dist/core/git/exec.js +100 -2
  108. package/dist/core/git/exec.js.map +1 -1
  109. package/dist/core/graph/diff-block.d.ts +6 -0
  110. package/dist/core/graph/diff-block.d.ts.map +1 -0
  111. package/dist/core/graph/diff-block.js +63 -0
  112. package/dist/core/graph/diff-block.js.map +1 -0
  113. package/dist/core/graph/diff-document.d.ts +38 -0
  114. package/dist/core/graph/diff-document.d.ts.map +1 -0
  115. package/dist/core/graph/diff-document.js +117 -0
  116. package/dist/core/graph/diff-document.js.map +1 -0
  117. package/dist/core/graph/diff.d.ts +123 -0
  118. package/dist/core/graph/diff.d.ts.map +1 -0
  119. package/dist/core/graph/diff.js +253 -0
  120. package/dist/core/graph/diff.js.map +1 -0
  121. package/dist/core/graph/tree.d.ts +24 -0
  122. package/dist/core/graph/tree.d.ts.map +1 -0
  123. package/dist/core/graph/tree.js +45 -0
  124. package/dist/core/graph/tree.js.map +1 -0
  125. package/dist/core/notes/list.d.ts +10 -0
  126. package/dist/core/notes/list.d.ts.map +1 -1
  127. package/dist/core/notes/list.js +35 -12
  128. package/dist/core/notes/list.js.map +1 -1
  129. package/dist/core/publish/filing.d.ts +12 -12
  130. package/dist/core/publish/filing.d.ts.map +1 -1
  131. package/dist/core/publish/filing.js +20 -18
  132. package/dist/core/publish/filing.js.map +1 -1
  133. package/dist/core/publish/lifespan.d.ts +37 -0
  134. package/dist/core/publish/lifespan.d.ts.map +1 -0
  135. package/dist/core/publish/lifespan.js +59 -0
  136. package/dist/core/publish/lifespan.js.map +1 -0
  137. package/dist/core/publish/represents.d.ts +72 -0
  138. package/dist/core/publish/represents.d.ts.map +1 -0
  139. package/dist/core/publish/represents.js +83 -0
  140. package/dist/core/publish/represents.js.map +1 -0
  141. package/dist/run.d.ts.map +1 -1
  142. package/dist/run.js +60 -4
  143. package/dist/run.js.map +1 -1
  144. package/dist/version.d.ts +1 -1
  145. package/dist/version.js +1 -1
  146. package/package.json +2 -1
  147. package/templates/approve/approve-button.md +1 -0
  148. package/templates/approve/author-heading.md +1 -0
  149. package/templates/approve/author-note.md +1 -0
  150. package/templates/approve/comment-label.md +1 -0
  151. package/templates/approve/comment-required.md +1 -0
  152. package/templates/approve/commits-heading.md +1 -0
  153. package/templates/approve/decision-heading.md +1 -0
  154. package/templates/approve/decision-note.md +1 -0
  155. package/templates/approve/diff-binary.md +1 -0
  156. package/templates/approve/diff-empty.md +1 -0
  157. package/templates/approve/diff-heading.md +1 -0
  158. package/templates/approve/facts-heading.md +1 -0
  159. package/templates/approve/forge-unreachable.md +1 -0
  160. package/templates/approve/github-app-manifest.json +15 -0
  161. package/templates/approve/head-missing.md +1 -0
  162. package/templates/approve/index-empty.md +1 -0
  163. package/templates/approve/index-heading.md +1 -0
  164. package/templates/approve/index-intro.md +1 -0
  165. package/templates/approve/landed.md +1 -0
  166. package/templates/approve/moved-head.md +1 -0
  167. package/templates/approve/no-pull.md +1 -0
  168. package/templates/approve/not-open.md +1 -0
  169. package/templates/approve/pages-heading.md +1 -0
  170. package/templates/approve/pages-note.md +1 -0
  171. package/templates/approve/record-failed.md +1 -0
  172. package/templates/approve/request-changes-button.md +1 -0
  173. package/templates/approve/state-line.md +1 -0
  174. package/templates/conventions/baseline-conventions.md +6 -0
  175. package/templates/graph/knowledge-changes/archived-item.md +1 -0
  176. package/templates/graph/knowledge-changes/archived.md +1 -0
  177. package/templates/graph/knowledge-changes/bare-route.md +2 -0
  178. package/templates/graph/knowledge-changes/block.md +10 -0
  179. package/templates/graph/knowledge-changes/counts.md +1 -0
  180. package/templates/graph/knowledge-changes/dangling-item.md +1 -0
  181. package/templates/graph/knowledge-changes/dangling.md +1 -0
  182. package/templates/graph/knowledge-changes/empty.md +1 -0
  183. package/templates/graph/knowledge-changes/reason-ambiguous.md +1 -0
  184. package/templates/graph/knowledge-changes/reason-not_found.md +1 -0
  185. package/templates/graph/knowledge-changes/review.md +4 -0
  186. package/templates/graph/review/moved-head.md +1 -0
  187. package/templates/land/resolver.md +12 -0
  188. package/templates/serve/diagrams.mjs +136 -0
  189. package/templates/serve/shell.html +1 -1
  190. package/templates/serve/style.css +90 -0
  191. package/templates/skills/ctxr-land.md +37 -31
  192. package/templates/skills/ctxr-organize-audit.md +19 -0
  193. package/templates/skills/ctxr-publish.md +100 -45
  194. package/templates/skills/ctxr-second-opinion/run.mjs +28 -17
  195. package/templates/skills/ctxr-second-opinion.md +19 -14
  196. package/templates/skills/ctxr-session-lifecycle.md +9 -0
  197. package/templates/skills/ctxr-submit.md +28 -17
package/README.md CHANGED
@@ -220,13 +220,15 @@ Each item is validated and applied independently, so one bad path refuses that i
220
220
 
221
221
  **5. Submit.** `ctxr-submit`: re-scan git state (never replay a plan from an earlier snapshot), run the capture pass, stage surgically with `git add <paths>` — never `-A`, and derived cache paths never stage — then `ctxr doctor` over the whole store, not `--staged`: a session's job is to leave the *store* healthy, not merely to pass one commit's gate. Then commit, rename the generated branch to something meaningful before it reaches the forge, `git push -u origin <branch>`, and `gh pr create`. If `gh` has no reachable GitHub remote, the push still succeeds on its own and you get manual pull-request instructions instead of a retry loop.
222
222
 
223
- **6. Land.** `ctxr-land`, after review: name the pull request explicitly rather than inferring it from whichever checkout you're standing in, read its state and mergeability with `gh pr view`, then wait for an explicit go — plan consent is not fire consent. `gh pr merge --squash`, then re-read state to confirm the forge reports `MERGED`; a transport error can arrive after a merge already succeeded, so the exit code alone is never enough. Finally fast-forward the canonical clone (`git fetch origin && git merge --ff-only`) and reclaim the worktree (`git worktree remove`, `git branch -d`) — nothing sweeps up afterward, so a worktree left here is left indefinitely.
223
+ **6. Land.** `ctxr-land`, after review: name the pull request explicitly rather than inferring it from whichever checkout you're standing in, read its state with `gh pr view` (and its decision with `ctxr review list`, when the store [approves changes from a page](#approving-changes-from-a-page)), then wait for an explicit go — plan consent is not fire consent — unless a decider already approved it on the page. Then `ctxr land <pr>` (with `--confirm` when the consent is your go): it catches the branch up with the default branch, merges, re-reads the forge to confirm the merge rather than trusting an exit code, fast-forwards the canonical clone, and removes the worktree and its branch — nothing sweeps up afterward, so a worktree left here is left indefinitely.
224
224
 
225
225
  `ctxr-session-lifecycle` covers what surrounds both halves: when to re-scan, the conflict playbook, and sequencing several pull requests.
226
226
 
227
- ### Why steps 5 and 6 aren't CLI commands
227
+ ### Why step 5 isn't a CLI command, and step 6 is
228
228
 
229
- They're ordinary `git` and `gh`. The CLI owns only what git cannot do — creating the worktree, applying validated note writes, running the health gates — and the sequencing, the confirmation gates, and the judgment live in the skills, where you can read and change them. That's why `ctxr session` has exactly three subcommands: `start`, `capture`, and `list`.
229
+ Submitting is ordinary `git` and `gh`. The CLI owns only what git cannot do — creating the worktree, applying validated note writes, running the health gates — and the sequencing, the confirmation gates, and the judgment live in the skills, where you can read and change them. That's why `ctxr session` has exactly three subcommands: `start`, `capture`, and `list`.
230
+
231
+ Landing was the same until a person could approve a change from a page. Then the thing that acts on an approval has no agent in it, and its one job — merge only a head someone approved — is a check that has to be enforced, not an instruction an agent follows. So `ctxr land` is code: it never resolves content, and the decision of *when* to land stays in `ctxr-land` and on the approval page.
230
232
 
231
233
  Two mechanical backstops hold the line underneath all of it: the **pre-commit** hook runs `doctor --staged`, and the **pre-push** hook refuses a push to the default branch (`CONTEXTURE_ALLOW_DEFAULT_BRANCH_PUSH=1` is the emergency override, for emergencies). Both are version-controlled git hooks, so they hold whichever harness, or human, does the writing.
232
234
 
@@ -309,10 +311,15 @@ The store's own `.contexture/guidance/mission.md` uses the same write path, but
309
311
  ctxr publish gather --note <path> # or --under <prefix>, or --entity <name>
310
312
  # -> "Page filing: <derived path>/<page-name>"
311
313
  ctxr publish new <derived path>/comparison # scaffolds the page folder and its sibling README
314
+ ctxr publish stamp .contexture/publish/<derived path>/comparison # records the hash of the note the README leads with
312
315
  ctxr publish check .contexture/publish/<derived path>/comparison/index.html
316
+
317
+ ctxr publish new --disposable agent-week # -> .contexture/publish/disposable/<today>-agent-week
313
318
  ```
314
319
 
315
- `gather` resolves a subject to its note set — never a hand-picked list, which is how an unintended note slips onto a page unnoticed — and derives where the page belongs from where the subject's own notes live, at whatever depth the store carries them to. So the published-pages navigation reads like the store rather than like one long list, and a subject's second page collects beside its first under one folder naming the subject; `gather` reports the page that move would displace, and moves nothing. `check` runs the structural gates: no external references, viewport meta, a print rule, provenance, the sibling README, script syntax, and that the page is not filed flat at the publish root. Both `new` and `check` end by naming the address `ctxr serve` answers for the page — the preview route while the page is still in a session worktree, together with the published-pages address it takes once that work lands, so the page gets looked at while changing it is still cheap. What a page may *contain*, given who will read it, is a judgment `ctxr-publish` walks you through and no check can make.
320
+ A page has one of two lifespans, read from where it is filed. A **permanent** page is the rendered form of one note that keeps changing: its README leads with a wikilink to that note (a folder's hub note, for a page drawn from a folder), `ctxr publish stamp` records the note's hash beside the link, and `check` fails the page — and `ctxr lint` lists it — once the note changes, until somebody reviews the page, removes the marker and stamps again. A **disposable** page is built to understand something once; it lives under `disposable/<YYYY-MM-DD>-<name>`, dated by the command, is never revised after it lands, and `ctxr lint` lists it as a removal candidate after 30 days — removal is the operator's call, made through a reviewed pull request.
321
+
322
+ `gather` resolves a subject to its note set — never a hand-picked list, which is how an unintended note slips onto a page unnoticed — and derives where the page belongs from where the subject's own notes live, at whatever depth the store carries them to. So the published-pages navigation reads like the store rather than like one long list, and a subject's second page collects beside its first under one folder naming the subject; `gather` reports the page that move would displace, and moves nothing. `check` runs the structural gates: no external references, viewport meta, a print rule, provenance, the sibling README, script syntax, that the page is not filed flat at the publish root, and — by lifespan — a disposable page's dated shape or a permanent page's represented note and its recorded hash. Both `new` and `check` end by naming the address `ctxr serve` answers for the page — the preview route while the page is still in a session worktree, together with the published-pages address it takes once that work lands, so the page gets looked at while changing it is still cheap. What a page may *contain*, given who will read it, is a judgment `ctxr-publish` walks you through and no check can make.
316
323
 
317
324
  → `ctxr-publish`, plus the vendored `frontend-design` and `eli5` skills for the craft contexture supplies none of.
318
325
 
@@ -322,12 +329,63 @@ ctxr publish check .contexture/publish/<derived path>/comparison/index.html
322
329
  ctxr serve
323
330
  ```
324
331
 
325
- Renders notes, catalog sections, the graph document, and published pages as cross-linked HTML. It binds loopback (`127.0.0.1`) by default. `--host` widens that, but note what it does not change: `serve` applies **no per-requester filtering at any bind address**. Widening it exposes the whole store to whatever can reach that address, so only do it behind a front end you've arranged yourself.
332
+ Renders notes, catalog sections, the graph document, and published pages as cross-linked HTML. It binds loopback (`127.0.0.1`) by default. `--host` widens that, but note what it does not change: `serve` applies **no per-requester filtering at any bind address**. Widening it exposes the whole store to whatever can reach that address, so only do it behind a front end you've arranged yourself. The one exception is the approval page below, which verifies who is asking before it shows or records anything.
326
333
 
327
334
  A published page's navigation label follows its own declared `<title>`, falling back to its folder name when it declares none. A folder group is labelled with the declared name of the taxonomy layer whose path it matches — so a store's own words for its top-level folders reach the sidebar — and with the bare directory name otherwise; either way a group sits where its directory name sorts, not where its label does. The header's light/dark/system links choose a display theme, and the ☰ control shows and hides the navigation — both persist across pages via cookies, and neither requires client-side script. A published page itself is still served byte-verbatim, so neither the theme nor the navigation reaches into it; `ctxr publish new`'s scaffold instead follows the viewer's own system preference on its own.
328
335
 
329
336
  The **Preview** area, directly below the published pages, holds the pages your open session worktrees are carrying. A page is previewable from the moment its files exist on disk — before any commit, before any push, before a PR is opened or merged — because `serve` reads the worktree rather than asking git or a forge anything. Pages group by the worktree holding them at `/preview/<worktree>/<page>/index.html`, and a preview is byte-identical to what `/publish/<page>/index.html` will serve once the page lands, so what you read is what ships. Both routes address files rather than directories, so a page's address always names its index file — which is the address `ctxr publish new` and `ctxr publish check` report, so you never have to work out which of the two applies. For a whole change set, `ctxr publish urls --staged` (or `--since <ref>`) reports every page added, modified, moved or removed with both addresses, which is what `ctxr-submit` puts in a pull request's **Pages** section; set `serve.previews: none` in `contexture.yaml` if previews never resolve for your reviewers. A session worktree carrying no page appears nowhere, and nothing else inside one — notes, catalog, graph, config — becomes reachable.
330
337
 
338
+ ## Approving changes from a page
339
+
340
+ People who own a store's content often have no forge account. With change approval declared, `ctxr serve` gives them a page instead: `/approve/` lists every session with an open pull request, and each one's page shows the author's account of the change (its title, description and commit messages, labelled as the author's), the knowledge changes computed from the commits, the preview of every changed page, and every changed line. Two buttons — **Approve** and **Request changes** — record the decision on the pull request, as a review by the store's own forge identity naming the person who decided and the commit they saw. Nobody needs a GitHub account to leave an entry in the audit trail.
341
+
342
+ ```yaml
343
+ serve:
344
+ base_url: https://ctx.example.com
345
+ change_approval:
346
+ identity: # the signed assertion your access proxy forwards
347
+ header: Cf-Access-Jwt-Assertion
348
+ issuer: https://<team>.cloudflareaccess.com
349
+ audience: <application aud tag>
350
+ jwks_url: https://<team>.cloudflareaccess.com/cdn-cgi/access/certs
351
+ deciders: ["@example.com"] # exact emails or @domain entries
352
+ recording_login: ctx-approvals # the forge identity decisions are recorded as
353
+ resolver: ["claude", "-p"] # optional: who resolves a conflict
354
+ ```
355
+
356
+ Identity comes from the proxy in front of `serve` (Cloudflare Access, Google IAP, oauth2-proxy): every `/approve/` request must carry its signed assertion, verified against the key set, issuer, audience and expiry, or it gets `401`. A request that reaches the port around the proxy carries none.
357
+
358
+ Then the system does the rest. Run `ctxr land --pending` on a schedule, with a forge token for the recording identity that can push and merge: it lands every pull request whose current head carries an approval — rebasing it onto the default branch when it is behind, merging, syncing the canonical clone, and removing the worktree. A clean rebase keeps the approval only when every commit's patch is unchanged, and the carry-over is recorded on the pull request. A rebase that conflicts goes to the configured resolver; its result is checked (clean, rebased, `ctxr doctor` passes), pushed, and handed back to the page for a new decision, because nobody has read an agent's merge of two sides. With no resolver, the change is marked blocked. Requested changes and blocked landings reach the next agent session through `ctxr review list`.
359
+
360
+ Outside the store, set up the recording identity (below) and, where your plan allows it, a branch protection rule requiring one approving review.
361
+
362
+ ### Recording identity
363
+
364
+ Decisions and landings are recorded on the pull request by one forge identity, the `recording_login`. It must not be the identity your agents open pull requests with: the forge refuses an approval from a pull request's own author. There are two ways to provide it.
365
+
366
+ **A GitHub App (recommended).** No account to look after, no seat, and no token to rotate: the App's private key does not expire, and contexture mints a one-hour installation token from it whenever it needs one.
367
+
368
+ 1. Create the App at <https://github.com/settings/apps/new> (or the organization's equivalent). Leave the webhook inactive and the callback URL empty, and request no user authorization. Under **Repository permissions** set **Contents**, **Issues** and **Pull requests** to *Read and write*; **Metadata** is read-only by default. Nothing else. [`templates/approve/github-app-manifest.json`](templates/approve/github-app-manifest.json) holds the same settings for GitHub's manifest flow.
369
+ 2. Generate a private key on the App's page, and keep the `.pem` outside every repository, readable only by the user that runs `serve` (`chmod 600`).
370
+ 3. Install the App on the store's repository, selecting only that repository.
371
+ 4. Set `recording_login` to the App's slug followed by `[bot]`, e.g. `ctx-a-approvals[bot]`.
372
+ 5. Give `ctxr serve` and the scheduled `ctxr land --pending` these, and nothing else needs a forge credential:
373
+
374
+ ```sh
375
+ CONTEXTURE_GITHUB_APP_ID=<the App ID> # or its client ID
376
+ CONTEXTURE_GITHUB_APP_PRIVATE_KEY_FILE=/run/secrets/ctx-approvals.pem # or CONTEXTURE_GITHUB_APP_PRIVATE_KEY=<the PEM text>
377
+ ```
378
+
379
+ contexture finds the installation from the store's github.com `origin` (or from `CONTEXTURE_GITHUB_REPOSITORY=owner/repo` when `origin` is not a github.com URL), and `ctxr land` fetches and pushes as the App too, handing git the token for that one command only. Every envelope reports `credential: github-app` and never the credential itself.
380
+
381
+ **One App per key holder.** Whoever holds the key can act on every repository the App is installed on, so share an App only across stores *you* run. To serve several of your stores under different owners, make the App installable on any account and install it on each owner with only that store's repository selected. A store someone else runs uses their own App.
382
+
383
+ **Where the key may live.** Only in the environments of `ctxr serve` and the land runner — never anywhere an agent runs commands, such as a harness gateway. An agent that can read the key can act on every store the App is installed on. If a key leaks, delete it on the App's page; that revokes it at once.
384
+
385
+ **A token for an account.** Without the App variables, `serve`, `land` and `review list` use `gh`'s own credential (`GH_TOKEN` or its credential file), so a dedicated machine account with a fine-grained token (Contents, Issues and Pull requests read and write on the store's repository) also works. It needs a rotation whenever the token expires.
386
+
387
+ GitHub Free cannot protect a private repository on a personal account, so such a store has no required-review rule; `ctxr land`'s consent check is what gates the merge either way. Where branch protection is available, require one approving review and leave **"Require approval of the most recent reviewable push"** off: the runner pushes a rebase and then approves the carry-over as the same identity.
388
+
331
389
  ## Keeping a store current
332
390
 
333
391
  ```sh
@@ -385,6 +443,8 @@ To pin a value against a future default change, declare it. Nothing rewrites wha
385
443
  | `rollup gather \| write \| stale` | Entity synthesis: enumerate, write, find what's out of date |
386
444
  | `publish gather \| new \| check` | Resolve a subject, scaffold a page, run the structural gates |
387
445
  | `session start \| capture \| list` | Session worktrees, and applying an approved capture proposal |
446
+ | `land <pr> [--confirm] \| --pending` | Land a session pull request that carries consent: catch up, merge, confirm, sync, reclaim |
447
+ | `review list` | Each open session pull request's decision from the approval page |
388
448
  | `adapters generate` | Regenerate harness outputs (`ctxr update` does this too) |
389
449
  | `serve` | Read the store in a browser |
390
450
  | `update` | Bring contexture-owned files up to the installed version |
@@ -0,0 +1,33 @@
1
+ import { type ServedAtRoute } from '../core/browse/page-url.js';
2
+ import type { CommandOutcome, CommandRequires } from '../core/command.js';
3
+ import { type GraphDelta } from '../core/graph/diff.js';
4
+ import type { Store } from '../core/store.js';
5
+ export declare const requires: CommandRequires;
6
+ /** review-the-knowledge-diff (D9): an enum rather than a boolean, so a further form has room and `--json` stays the envelope switch. */
7
+ export declare const GRAPH_DIFF_FORMATS: readonly ['text', 'markdown'];
8
+ export type GraphDiffFormat = (typeof GRAPH_DIFF_FORMATS)[number];
9
+ export interface GraphDiffFlags {
10
+ since: string | undefined;
11
+ until: string | undefined;
12
+ format: string | undefined;
13
+ }
14
+ export interface GraphDiffData extends GraphDelta {
15
+ base_url_declared: boolean;
16
+ /** The review view of this session worktree, carrying the head id; null outside a worktree or with previews off. */
17
+ review: ServedAtRoute | null;
18
+ }
19
+ /**
20
+ * The flag checks, ahead of any store or git read, so a malformed invocation
21
+ * is refused with the usage code however the store is doing. Returns the
22
+ * format to render.
23
+ */
24
+ export declare function checkFlags(flags: GraphDiffFlags, jsonMode: boolean): GraphDiffFormat;
25
+ /**
26
+ * context-retrieval spec (review-the-knowledge-diff): the graph delta between
27
+ * the merge base of `--since` and the head, read from git's object store. It
28
+ * reports and gates nothing — no findings, and exit 0 whenever both sides
29
+ * could be read (D10). `--format markdown` makes the human output the
30
+ * knowledge-changes block and nothing else, so it can be pasted unchanged.
31
+ */
32
+ export declare function execute(store: Store, flags: GraphDiffFlags, jsonMode?: boolean): Promise<CommandOutcome<GraphDiffData>>;
33
+ //# sourceMappingURL=graph-diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-diff.d.ts","sourceRoot":"","sources":["../../src/commands/graph-diff.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAM1E,OAAO,EAAmC,KAAK,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACzF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAE9C,eAAO,MAAM,QAAQ,EAAE,eAAuC,CAAC;AAE/D,wIAAwI;AACxI,eAAO,MAAM,kBAAkB,YAAI,MAAM,EAAE,UAAU,CAAU,CAAC;AAChE,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAElE,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,iBAAiB,EAAE,OAAO,CAAC;IAC3B,oHAAoH;IACpH,MAAM,EAAE,aAAa,GAAG,IAAI,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,GAAG,eAAe,CAMpF;AAED;;;;;;GAMG;AACH,wBAAsB,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,QAAQ,UAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC,CAqB3H"}
@@ -0,0 +1,52 @@
1
+ import { reviewServedAt } from '../core/browse/page-url.js';
2
+ import { GraphDiffFormatError, GraphDiffSinceRequiredError } from '../core/errors.js';
3
+ import { ExitCode } from '../core/exit-codes.js';
4
+ import { createExecFileGitRunner } from '../core/git/exec.js';
5
+ import { renderKnowledgeChangesBlock, reviewLines } from '../core/graph/diff-block.js';
6
+ import { renderDiffDocument } from '../core/graph/diff-document.js';
7
+ import { computeGraphDelta, resolveRange } from '../core/graph/diff.js';
8
+ export const requires = { store: 'required' };
9
+ /** review-the-knowledge-diff (D9): an enum rather than a boolean, so a further form has room and `--json` stays the envelope switch. */
10
+ export const GRAPH_DIFF_FORMATS = ['text', 'markdown'];
11
+ /**
12
+ * The flag checks, ahead of any store or git read, so a malformed invocation
13
+ * is refused with the usage code however the store is doing. Returns the
14
+ * format to render.
15
+ */
16
+ export function checkFlags(flags, jsonMode) {
17
+ if (flags.since === undefined)
18
+ throw new GraphDiffSinceRequiredError();
19
+ const format = flags.format ?? 'text';
20
+ if (!GRAPH_DIFF_FORMATS.includes(format))
21
+ throw new GraphDiffFormatError(format, GRAPH_DIFF_FORMATS, false);
22
+ if (format === 'markdown' && jsonMode)
23
+ throw new GraphDiffFormatError(format, GRAPH_DIFF_FORMATS, true);
24
+ return format;
25
+ }
26
+ /**
27
+ * context-retrieval spec (review-the-knowledge-diff): the graph delta between
28
+ * the merge base of `--since` and the head, read from git's object store. It
29
+ * reports and gates nothing — no findings, and exit 0 whenever both sides
30
+ * could be read (D10). `--format markdown` makes the human output the
31
+ * knowledge-changes block and nothing else, so it can be pasted unchanged.
32
+ */
33
+ export async function execute(store, flags, jsonMode = false) {
34
+ const format = checkFlags(flags, jsonMode);
35
+ const git = createExecFileGitRunner();
36
+ const range = await resolveRange(git, store.root, flags.since, flags.until);
37
+ const delta = await computeGraphDelta(git, store.root, range, store.config);
38
+ const review = reviewServedAt(store, delta.head);
39
+ const baseUrlDeclared = store.config.serve?.base_url !== undefined;
40
+ const humanSummary = format === 'markdown'
41
+ ? renderKnowledgeChangesBlock(delta, review).replace(/\n$/, '')
42
+ : [renderDiffDocument(delta).replace(/\n$/, ''), ...reviewLines(review)].join('\n');
43
+ return {
44
+ exitCode: ExitCode.Ok,
45
+ data: { ...delta, base_url_declared: baseUrlDeclared, review },
46
+ findings: [],
47
+ humanSummary,
48
+ storeRoot: store.root,
49
+ schemaVersion: store.config.schema_version,
50
+ };
51
+ }
52
+ //# sourceMappingURL=graph-diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-diff.js","sourceRoot":"","sources":["../../src/commands/graph-diff.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAsB,MAAM,4BAA4B,CAAC;AAEhF,OAAO,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AACtF,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,2BAA2B,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AACvF,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAmB,MAAM,uBAAuB,CAAC;AAGzF,MAAM,CAAC,MAAM,QAAQ,GAAoB,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;AAE/D,wIAAwI;AACxI,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,UAAU,CAAU,CAAC;AAehE;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,KAAqB,EAAE,QAAiB;IACjE,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;QAAE,MAAM,IAAI,2BAA2B,EAAE,CAAC;IACvE,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,MAAM,CAAC;IACtC,IAAI,CAAE,kBAAwC,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,MAAM,IAAI,oBAAoB,CAAC,MAAM,EAAE,kBAAkB,EAAE,KAAK,CAAC,CAAC;IACnI,IAAI,MAAM,KAAK,UAAU,IAAI,QAAQ;QAAE,MAAM,IAAI,oBAAoB,CAAC,MAAM,EAAE,kBAAkB,EAAE,IAAI,CAAC,CAAC;IACxG,OAAO,MAAyB,CAAC;AACnC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,KAAY,EAAE,KAAqB,EAAE,QAAQ,GAAG,KAAK;IACjF,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAC3C,MAAM,GAAG,GAAG,uBAAuB,EAAE,CAAC;IACtC,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,KAAM,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IAC7E,MAAM,KAAK,GAAG,MAAM,iBAAiB,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC5E,MAAM,MAAM,GAAG,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IACjD,MAAM,eAAe,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,QAAQ,KAAK,SAAS,CAAC;IAEnE,MAAM,YAAY,GAChB,MAAM,KAAK,UAAU;QACnB,CAAC,CAAC,2BAA2B,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;QAC/D,CAAC,CAAC,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAExF,OAAO;QACL,QAAQ,EAAE,QAAQ,CAAC,EAAE;QACrB,IAAI,EAAE,EAAE,GAAG,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,EAAE;QAC9D,QAAQ,EAAE,EAAE;QACZ,YAAY;QACZ,SAAS,EAAE,KAAK,CAAC,IAAI;QACrB,aAAa,EAAE,KAAK,CAAC,MAAM,CAAC,cAAc;KAC3C,CAAC;AACJ,CAAC"}
@@ -0,0 +1,50 @@
1
+ import type { CommandOutcome, CommandRequires } from '../core/command.js';
2
+ import type { RunEnv } from '../core/env.js';
3
+ import { type CredentialKind } from '../core/forge/credentials.js';
4
+ import { type ForgeClient } from '../core/forge/github.js';
5
+ import { type Store } from '../core/store.js';
6
+ export declare const requires: CommandRequires;
7
+ export interface LandFlags {
8
+ pr: number | undefined;
9
+ pending: boolean;
10
+ confirm: boolean;
11
+ }
12
+ export type LandOutcome = 'merged' | 'no-consent' | 'needs-reapproval' | 'blocked' | 'merge-refused' | 'lease-refused';
13
+ export type CatchUp = 'not-needed' | 'clean' | 'carried-over' | 'patches-changed' | 'conflict' | 'conflict-resolved';
14
+ export interface LandResult {
15
+ pull_request: number;
16
+ worktree: string | null;
17
+ outcome: LandOutcome;
18
+ /** Where the consent came from: a decision recorded from the approval page, or `--confirm`. */
19
+ consent: 'recorded' | 'confirm' | null;
20
+ /** The decision state, when consent was looked for and not found. */
21
+ state: string | null;
22
+ reason: string | null;
23
+ catch_up: CatchUp | null;
24
+ merged_commit: string | null;
25
+ canonical_sync: {
26
+ synced: boolean;
27
+ reason: string | null;
28
+ } | null;
29
+ reclaim: {
30
+ removed: boolean;
31
+ reason: string | null;
32
+ } | null;
33
+ /** Records posted on the pull request, and records this credential could not post (not the recording login). */
34
+ records_posted: string[];
35
+ records_reported_only: string[];
36
+ }
37
+ export interface LandData {
38
+ /** Which kind of forge credential the command used; never the credential. */
39
+ credential: CredentialKind;
40
+ results: LandResult[];
41
+ }
42
+ /**
43
+ * write-lifecycle spec ("Landing merges only a head that carries consent"):
44
+ * one pull request, or with `--pending` every session pull request whose
45
+ * current head carries an approval. design.md D1: this is code because the
46
+ * runner acting on a page approval has no agent in it, and the consent check is
47
+ * the one thing that must be enforced rather than instructed.
48
+ */
49
+ export declare function execute(env: RunEnv, store: Store, flags: LandFlags, forge?: ForgeClient): Promise<CommandOutcome<LandData>>;
50
+ //# sourceMappingURL=land.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"land.d.ts","sourceRoot":"","sources":["../../src/commands/land.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE1E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAG7C,OAAO,EAAiC,KAAK,cAAc,EAAyB,MAAM,8BAA8B,CAAC;AACzH,OAAO,EAA0C,KAAK,WAAW,EAAsC,MAAM,yBAAyB,CAAC;AAGvI,OAAO,EAAa,KAAK,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAKzD,eAAO,MAAM,QAAQ,EAAE,eAAuC,CAAC;AAE/D,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,YAAY,GAAG,kBAAkB,GAAG,SAAS,GAAG,eAAe,GAAG,eAAe,CAAC;AACvH,MAAM,MAAM,OAAO,GAAG,YAAY,GAAG,OAAO,GAAG,cAAc,GAAG,iBAAiB,GAAG,UAAU,GAAG,mBAAmB,CAAC;AAErH,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,WAAW,CAAC;IACrB,+FAA+F;IAC/F,OAAO,EAAE,UAAU,GAAG,SAAS,GAAG,IAAI,CAAC;IACvC,qEAAqE;IACrE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IAClE,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5D,gHAAgH;IAChH,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,qBAAqB,EAAE,MAAM,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,QAAQ;IACvB,6EAA6E;IAC7E,UAAU,EAAE,cAAc,CAAC;IAC3B,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB;AAuWD;;;;;;GAMG;AACH,wBAAsB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAkDjI"}