Compare commits
20 Commits
bbfcc366c2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 85eb20c4cb | |||
| 276378b57c | |||
| 03b968513b | |||
| e545d38314 | |||
| 564ccd2559 | |||
| f32f51970b | |||
| 0e50839ce0 | |||
| ed995a1edd | |||
| 2424b87b46 | |||
| 37ef245374 | |||
| ac40abe696 | |||
| a611d7fb99 | |||
| 89c430b940 | |||
| 1dd7d8a2d8 | |||
| 69697c822c | |||
| 8def00f368 | |||
| c907354a4f | |||
| 80ff1f8b03 | |||
| 9b5069693a | |||
| 2cad84ff26 |
@@ -1,195 +0,0 @@
|
||||
---
|
||||
description: Reviews code changes for quality, bugs, security, and best practices
|
||||
mode: subagent
|
||||
temperature: 0.1
|
||||
tools:
|
||||
write: false
|
||||
permission:
|
||||
edit: deny
|
||||
bash: allow
|
||||
---
|
||||
|
||||
You are a code reviewer for proposed code changes made by another engineer.
|
||||
|
||||
## Version Control
|
||||
|
||||
This project uses `jj` (Jujutsu) for version control. Use jj commands to inspect changes.
|
||||
|
||||
## Review Modes
|
||||
|
||||
Parse the user's request to determine the review mode. The user will specify one of the following modes (or no mode, in which case you should auto-detect).
|
||||
|
||||
### Auto-detect (no mode specified)
|
||||
|
||||
If no mode is specified:
|
||||
1. Check for working-copy changes with `jj diff --summary`
|
||||
2. If there are working-copy changes, review those (working-copy mode)
|
||||
3. Otherwise, find the trunk bookmark with `jj log -r 'trunk()' --no-graph -T 'bookmarks ++ "\n"'` and review against it (bookmark mode)
|
||||
4. If no trunk bookmark exists, review the current change
|
||||
|
||||
### working-copy
|
||||
|
||||
Review the current working-copy changes (including new files).
|
||||
|
||||
Commands to inspect:
|
||||
- `jj status` - overview of changed files
|
||||
- `jj diff --summary` - summary of changes
|
||||
- `jj diff` - full diff of all changes
|
||||
|
||||
### bookmark <name>
|
||||
|
||||
Review code changes against a base bookmark (PR-style review).
|
||||
|
||||
Steps:
|
||||
1. Resolve the bookmark. If the name contains `@`, split into `name@remote`. Otherwise, look for a local bookmark first, then remote bookmarks.
|
||||
2. Find the merge-base: `jj log -r 'heads(::@ & ::<bookmark_revset>)' --no-graph -T 'change_id.shortest(8) ++ "\n"'`
|
||||
- For local bookmarks: `<bookmark_revset>` = `bookmarks(exact:"<name>")`
|
||||
- For remote bookmarks: `<bookmark_revset>` = `remote_bookmarks(exact:"<name>", exact:"<remote>")`
|
||||
3. Inspect the diff: `jj diff --from <merge_base> --to @`
|
||||
|
||||
Also check for local working-copy changes on top with `jj diff --summary` and include those in the review.
|
||||
|
||||
### change <id>
|
||||
|
||||
Review a specific change by its change ID.
|
||||
|
||||
Commands to inspect:
|
||||
- `jj show <id>` - show the change details and diff
|
||||
- `jj log -r <id>` - show change metadata
|
||||
|
||||
### pr <number-or-url>
|
||||
|
||||
Review a GitHub pull request by materializing it locally.
|
||||
|
||||
Use the `review_materialize_pr` tool to materialize the PR. It returns the PR title, base bookmark, and remote used. Then review as a bookmark-style review against the base bookmark.
|
||||
|
||||
If the `review_materialize_pr` tool is not available, do it manually:
|
||||
1. Get PR info: `gh pr view <number> --json baseRefName,title,headRefName,isCrossRepository,headRepository,headRepositoryOwner`
|
||||
2. Fetch the PR branch: `jj git fetch --remote origin --branch <headRefName>`
|
||||
3. Save current position: `jj log -r @ --no-graph -T 'change_id.shortest(8)'`
|
||||
4. Create a new change on the PR: `jj new 'remote_bookmarks(exact:"<headRefName>", exact:"origin")'`
|
||||
5. Find merge-base and review as bookmark mode against `<baseRefName>`
|
||||
6. After the review, restore position: `jj edit <saved_change_id>`
|
||||
|
||||
For cross-repository (forked) PRs:
|
||||
1. Add a temporary remote: `jj git remote add <temp_name> <fork_url>`
|
||||
2. Fetch from that remote instead
|
||||
3. After the review, remove the temporary remote: `jj git remote remove <temp_name>`
|
||||
|
||||
Parse PR references as either a number (e.g. `123`) or a GitHub URL (e.g. `https://github.com/owner/repo/pull/123`).
|
||||
|
||||
### folder <paths...>
|
||||
|
||||
Snapshot review (not a diff) of specific folders or files.
|
||||
|
||||
Read the files directly in the specified paths. Do not compare against any previous state.
|
||||
|
||||
## Extra Instructions
|
||||
|
||||
If the user's request contains `--extra "..."` or `--extra=...`, treat the quoted value as an additional review instruction to apply on top of the standard guidelines.
|
||||
|
||||
## Project-Specific Review Guidelines
|
||||
|
||||
Before starting the review, check if a `REVIEW_GUIDELINES.md` file exists in the project root. If it does, read it and incorporate those guidelines into this review. They take precedence over the default guidelines below when they conflict.
|
||||
|
||||
## Review Guidelines
|
||||
|
||||
Below are default guidelines for determining what to flag. If you encounter more specific guidelines in the project's REVIEW_GUIDELINES.md or in the user's instructions, those override these general instructions.
|
||||
|
||||
### Determining what to flag
|
||||
|
||||
Flag issues that:
|
||||
1. Meaningfully impact the accuracy, performance, security, or maintainability of the code.
|
||||
2. Are discrete and actionable (not general issues or multiple combined issues).
|
||||
3. Don't demand rigor inconsistent with the rest of the codebase.
|
||||
4. Were introduced in the changes being reviewed (not pre-existing bugs).
|
||||
5. The author would likely fix if aware of them.
|
||||
6. Don't rely on unstated assumptions about the codebase or author's intent.
|
||||
7. Have provable impact on other parts of the code -- it is not enough to speculate that a change may disrupt another part, you must identify the parts that are provably affected.
|
||||
8. Are clearly not intentional changes by the author.
|
||||
9. Be particularly careful with untrusted user input and follow the specific guidelines to review.
|
||||
10. Treat silent local error recovery (especially parsing/IO/network fallbacks) as high-signal review candidates unless there is explicit boundary-level justification.
|
||||
|
||||
### Untrusted User Input
|
||||
|
||||
1. Be careful with open redirects, they must always be checked to only go to trusted domains (?next_page=...)
|
||||
2. Always flag SQL that is not parametrized
|
||||
3. In systems with user supplied URL input, http fetches always need to be protected against access to local resources (intercept DNS resolver!)
|
||||
4. Escape, don't sanitize if you have the option (eg: HTML escaping)
|
||||
|
||||
### Comment guidelines
|
||||
|
||||
1. Be clear about why the issue is a problem.
|
||||
2. Communicate severity appropriately - don't exaggerate.
|
||||
3. Be brief - at most 1 paragraph.
|
||||
4. Keep code snippets under 3 lines, wrapped in inline code or code blocks.
|
||||
5. Use ```suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block). Preserve the exact leading whitespace of the replaced lines.
|
||||
6. Explicitly state scenarios/environments where the issue arises.
|
||||
7. Use a matter-of-fact tone - helpful AI assistant, not accusatory.
|
||||
8. Write for quick comprehension without close reading.
|
||||
9. Avoid excessive flattery or unhelpful phrases like "Great job...".
|
||||
|
||||
### Review priorities
|
||||
|
||||
1. Surface critical non-blocking human callouts (migrations, dependency churn, auth/permissions, compatibility, destructive operations) at the end.
|
||||
2. Prefer simple, direct solutions over wrappers or abstractions without clear value.
|
||||
3. Treat back pressure handling as critical to system stability.
|
||||
4. Apply system-level thinking; flag changes that increase operational risk or on-call wakeups.
|
||||
5. Ensure that errors are always checked against codes or stable identifiers, never error messages.
|
||||
|
||||
### Fail-fast error handling (strict)
|
||||
|
||||
When reviewing added or modified error handling, default to fail-fast behavior.
|
||||
|
||||
1. Evaluate every new or changed `try/catch`: identify what can fail and why local handling is correct at that exact layer.
|
||||
2. Prefer propagation over local recovery. If the current scope cannot fully recover while preserving correctness, rethrow (optionally with context) instead of returning fallbacks.
|
||||
3. Flag catch blocks that hide failure signals (e.g. returning `null`/`[]`/`false`, swallowing JSON parse failures, logging-and-continue, or "best effort" silent recovery).
|
||||
4. JSON parsing/decoding should fail loudly by default. Quiet fallback parsing is only acceptable with an explicit compatibility requirement and clear tested behavior.
|
||||
5. Boundary handlers (HTTP routes, CLI entrypoints, supervisors) may translate errors, but must not pretend success or silently degrade.
|
||||
6. If a catch exists only to satisfy lint/style without real handling, treat it as a bug.
|
||||
7. When uncertain, prefer crashing fast over silent degradation.
|
||||
|
||||
### Priority levels
|
||||
|
||||
Tag each finding with a priority level in the title:
|
||||
- [P0] - Drop everything to fix. Blocking release/operations. Only for universal issues that do not depend on assumptions about inputs.
|
||||
- [P1] - Urgent. Should be addressed in the next cycle.
|
||||
- [P2] - Normal. To be fixed eventually.
|
||||
- [P3] - Low. Nice to have.
|
||||
|
||||
## Output Format
|
||||
|
||||
Provide your findings in a clear, structured format:
|
||||
|
||||
1. List each finding with its priority tag, file location, and explanation.
|
||||
2. Findings must reference locations that overlap with the actual diff -- don't flag pre-existing code.
|
||||
3. Keep line references as short as possible (avoid ranges over 5-10 lines; pick the most suitable subrange).
|
||||
4. Provide an overall verdict: "correct" (no blocking issues) or "needs attention" (has blocking issues).
|
||||
5. Ignore trivial style issues unless they obscure meaning or violate documented standards.
|
||||
6. Do not generate a full PR fix -- only flag issues and optionally provide short suggestion blocks.
|
||||
7. End with the required "Human Reviewer Callouts (Non-Blocking)" section and all applicable bold callouts (no yes/no).
|
||||
|
||||
Output all findings the author would fix if they knew about them. If there are no qualifying findings, explicitly state the code looks good. Don't stop at the first finding - list every qualifying issue. Then append the required non-blocking callouts section.
|
||||
|
||||
### Required Human Reviewer Callouts (Non-Blocking)
|
||||
|
||||
After findings/verdict, you MUST append this final section:
|
||||
|
||||
## Human Reviewer Callouts (Non-Blocking)
|
||||
|
||||
Include only applicable callouts (no yes/no lines):
|
||||
|
||||
- **This change adds a database migration:** <files/details>
|
||||
- **This change introduces a new dependency:** <package(s)/details>
|
||||
- **This change changes a dependency (or the lockfile):** <files/package(s)/details>
|
||||
- **This change modifies auth/permission behavior:** <what changed and where>
|
||||
- **This change introduces backwards-incompatible public schema/API/contract changes:** <what changed and where>
|
||||
- **This change includes irreversible or destructive operations:** <operation and scope>
|
||||
|
||||
Rules for this section:
|
||||
1. These are informational callouts for the human reviewer, not fix items.
|
||||
2. Do not include them in Findings unless there is an independent defect.
|
||||
3. These callouts alone must not change the verdict.
|
||||
4. Only include callouts that apply to the reviewed change.
|
||||
5. Keep each emitted callout bold exactly as written.
|
||||
6. If none apply, write "- (none)".
|
||||
@@ -2,7 +2,7 @@ keys:
|
||||
- &user_cschmatzler age1xate984yhl9qk9d4q99pyxmzz48sq56nfhu8weyzkgum4ed5tc5shjmrs7
|
||||
- &host_tahani age1njjegjjdqzfnrr54f536yl4lduqgna3wuv7ef6vtl9jw5cju0grsgy62tm
|
||||
- &host_michael age187jl7e4k9n4guygkmpuqzeh0wenefwrfkpvuyhvwjrjwxqpzassqq3x67j
|
||||
- &host_jason age1ez6j3r5wdp0tjy7n5qzv5vfakdc2nh2zeu388zu7a80l0thv052syxq5e2
|
||||
- &host_janet age1f9h725ewwwwwkelnrvdvrurg6fcsn3zxrxdt0v6v8ys0nzngcsvqu77nc8
|
||||
- &host_chidi age1tlymdmaukhwupzrhszspp26lgd8s64rw4vu9lwc7gsgrjm78095s9fe9l3
|
||||
creation_rules:
|
||||
- path_regex: secrets/[^/]+$
|
||||
@@ -11,5 +11,5 @@ creation_rules:
|
||||
- *user_cschmatzler
|
||||
- *host_tahani
|
||||
- *host_michael
|
||||
- *host_jason
|
||||
- *host_janet
|
||||
- *host_chidi
|
||||
|
||||
114
flake.lock
generated
114
flake.lock
generated
@@ -114,11 +114,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1775023938,
|
||||
"narHash": "sha256-0/aPuEXIIaehfP/t9icDJUTCmAu13dfS+RNKWdMV5P0=",
|
||||
"lastModified": 1775037210,
|
||||
"narHash": "sha256-KM2WYj6EA7M/FVZVCl3rqWY+TFV5QzSyyGE2gQxeODU=",
|
||||
"owner": "LnL7",
|
||||
"repo": "nix-darwin",
|
||||
"rev": "5176e2f4b45de02f1c90133854634a6c675ef41b",
|
||||
"rev": "06648f4902343228ce2de79f291dd5a58ee12146",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -130,11 +130,11 @@
|
||||
},
|
||||
"den": {
|
||||
"locked": {
|
||||
"lastModified": 1775034229,
|
||||
"narHash": "sha256-BZPqamTWnWdKA+tSjt5y57EDYZnSRQYNZWQNFtqn9rw=",
|
||||
"lastModified": 1775702491,
|
||||
"narHash": "sha256-5BCNtE/zCLSheltliy4hTdwsq0Boj/W1XRIX8n89nqA=",
|
||||
"owner": "vic",
|
||||
"repo": "den",
|
||||
"rev": "88533ec7ac8ddda4a59243387de4b9d24d3932ae",
|
||||
"rev": "d267c458e384b57317d06d45f7c65f7fb03fae4b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -191,11 +191,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1775029908,
|
||||
"narHash": "sha256-QuPn+EN/097aBLeSqbQ7vOwc5TSOb68bAxg1+mknfmw=",
|
||||
"lastModified": 1775721346,
|
||||
"narHash": "sha256-ogqjruvVBYEj8sWM3viOucSo1Pna9c147EKQOfA+p3I=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "380f1969f440e683333af5746caac76811b4a1a8",
|
||||
"rev": "99fde43dfee2a672e4e37ef211e0844337e5b725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -302,11 +302,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1772408722,
|
||||
"narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=",
|
||||
"lastModified": 1775087534,
|
||||
"narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3",
|
||||
"rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -323,11 +323,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1772408722,
|
||||
"narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=",
|
||||
"lastModified": 1775087534,
|
||||
"narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3",
|
||||
"rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -344,11 +344,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1772408722,
|
||||
"narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=",
|
||||
"lastModified": 1775087534,
|
||||
"narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3",
|
||||
"rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -441,11 +441,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774991950,
|
||||
"narHash": "sha256-kScKj3qJDIWuN9/6PMmgy5esrTUkYinrO5VvILik/zw=",
|
||||
"lastModified": 1775683737,
|
||||
"narHash": "sha256-oBYyowo6yfgb95Z78s3uTnAd9KkpJpwzjJbfnpLaM2Y=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "f2d3e04e278422c7379e067e323734f3e8c585a7",
|
||||
"rev": "7ba4ee4228ed36123c7cb75d50524b43514ef992",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -457,11 +457,11 @@
|
||||
"homebrew-cask": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1775034103,
|
||||
"narHash": "sha256-poo46muSZsDLcnN8wY/30YeLAdRCxIwzr2s1Z12aC28=",
|
||||
"lastModified": 1775700724,
|
||||
"narHash": "sha256-qQm9uIF+tI7gamLMa7DSXSQQzLQalEtOa7PHPxNkbr8=",
|
||||
"owner": "homebrew",
|
||||
"repo": "homebrew-cask",
|
||||
"rev": "0285f9dcb1dfaacde1fb6218ebe92540d9a3762d",
|
||||
"rev": "c622bff3b88557e3c870104db0426b93e0767a8f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -473,11 +473,11 @@
|
||||
"homebrew-core": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1775034425,
|
||||
"narHash": "sha256-nTdPP63yUkmUsx/ksOvfRs6MjXztPh6GEv6FQU5IFGA=",
|
||||
"lastModified": 1775721921,
|
||||
"narHash": "sha256-s6K2QbKa4OJlblFp3zMSh0/2PM2zpWpAd4ZnREirj/I=",
|
||||
"owner": "homebrew",
|
||||
"repo": "homebrew-core",
|
||||
"rev": "da66ad06774537e48644d117e6300ad9c2db25a0",
|
||||
"rev": "70028a68b515145bbeccb2961240275ab6eb9e82",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -535,11 +535,11 @@
|
||||
"jj-nvim": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1773914813,
|
||||
"narHash": "sha256-UuNcOfgsWuHu9hx6NT/FbQ0E8T6nRY1X6O6CDRtH8Sk=",
|
||||
"lastModified": 1775551442,
|
||||
"narHash": "sha256-hoU+DenrgxNwvLNmDtIsJ5yB5fhRjDRPOOL8WW9bpZM=",
|
||||
"owner": "NicolasGB",
|
||||
"repo": "jj.nvim",
|
||||
"rev": "a6e163bcc3a6b75e5b6d4190b64ed4b39f8ddb0c",
|
||||
"rev": "2dbe2c73c599a29e86e4123b42e430828b1f01d9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -593,11 +593,11 @@
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1775013753,
|
||||
"narHash": "sha256-uIEYD2rwgV9EFO5x0SQ34Yj50r/4Abj28OibW404eCw=",
|
||||
"lastModified": 1775705124,
|
||||
"narHash": "sha256-OUtgrn0k7DYnAP9skY2rOJSWJyn4w5tnUcF3lSJdfME=",
|
||||
"owner": "numtide",
|
||||
"repo": "llm-agents.nix",
|
||||
"rev": "5a192c61b052a7713ea8eb5490a64087a996afa7",
|
||||
"rev": "ca76524952b00135dba57da62ce2dd123a1ba4be",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -637,11 +637,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774915815,
|
||||
"narHash": "sha256-LocQzkSjVS4G0AKMBiEIVdBKCNTMZXQFjQMWFId4Jpg=",
|
||||
"lastModified": 1775693082,
|
||||
"narHash": "sha256-nnhkpfWsRutQh//KmVoIV7e9Gk90tBezjcoRr775BfU=",
|
||||
"owner": "nix-community",
|
||||
"repo": "neovim-nightly-overlay",
|
||||
"rev": "9001416dc5d0ca24c8e4b5a44bfe7cd6fbeb1dd1",
|
||||
"rev": "21b2795e6aeb4a0110bdc7bd81bad59c022c9986",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -653,11 +653,11 @@
|
||||
"neovim-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1774915197,
|
||||
"narHash": "sha256-yor+eo8CVi7wBp7CjAMQnVoK+m197gsl7MvUzaqicns=",
|
||||
"lastModified": 1775689880,
|
||||
"narHash": "sha256-savZYhFAaBm3BQUdTrPOv7i5K18JFANJvyHv0uuvaWM=",
|
||||
"owner": "neovim",
|
||||
"repo": "neovim",
|
||||
"rev": "dbc4800dda2b0dc3290dc79955f857256e0694e2",
|
||||
"rev": "eefb50e352a689ec1a0a55d6827abea79960cd3d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -734,11 +734,11 @@
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1774855581,
|
||||
"narHash": "sha256-YkreHeMgTCYvJ5fESV0YyqQK49bHGe2B51tH6claUh4=",
|
||||
"lastModified": 1775639890,
|
||||
"narHash": "sha256-9O9gNidrdzcb7vgKGtff7QiLtr0IsVaCi0pAXm8anhQ=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "15c6719d8c604779cf59e03c245ea61d3d7ab69b",
|
||||
"rev": "456e8a9468b9d46bd8c9524425026c00745bc4d2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -750,11 +750,11 @@
|
||||
},
|
||||
"nixpkgs_5": {
|
||||
"locked": {
|
||||
"lastModified": 1775036421,
|
||||
"narHash": "sha256-kOAGXAqmmCmXpTJ0ZC/v0pUlyTFgwj31hEfJbcf0l70=",
|
||||
"lastModified": 1775722436,
|
||||
"narHash": "sha256-Z7QmfL80jmUPoSQkMlCc+1MGfkugf7bG47H3UTsyi7Q=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f16ce1b999cc00aa1222578a740e74b5fbfa0284",
|
||||
"rev": "e73a61d035ee91f95bb0a6b95ce0b9d2866bd332",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -803,11 +803,11 @@
|
||||
"systems": "systems_4"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774802402,
|
||||
"narHash": "sha256-L1UJ/zxKTyyaGGmytH6OYlgQ0HGSMhvPkvU+iz4Mkb8=",
|
||||
"lastModified": 1775307257,
|
||||
"narHash": "sha256-y9hEecHH4ennFwIcw1n480YCGh73DkEmizmQnyXuvgg=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixvim",
|
||||
"rev": "cbd8536a05d1aae2593cb5c9ace1010c8c5845cb",
|
||||
"rev": "2e008bb941f72379d5b935d5bfe70ed8b7c793ff",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -868,11 +868,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1774948198,
|
||||
"narHash": "sha256-oVPo0/3CXM/5uFKu1ZwP7osSV2tiQIFU09Y3UzNbm7g=",
|
||||
"lastModified": 1775663707,
|
||||
"narHash": "sha256-3cSvpBETRa8aDSrUCX1jGc6FSse3OWB7cXACIZW8BYI=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "63b3eff38ef1c216480147dd53b0e4365d55f269",
|
||||
"rev": "8c5af725817905e462052d91a8d229b85ffa83a5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -944,11 +944,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774910634,
|
||||
"narHash": "sha256-B+rZDPyktGEjOMt8PcHKYmgmKoF+GaNAFJhguktXAo0=",
|
||||
"lastModified": 1775682595,
|
||||
"narHash": "sha256-0E9PohY/VuESLq0LR4doaH7hTag513sDDW5n5qmHd1Q=",
|
||||
"owner": "Mic92",
|
||||
"repo": "sops-nix",
|
||||
"rev": "19bf3d8678fbbfbc173beaa0b5b37d37938db301",
|
||||
"rev": "d2e8438d5886e92bc5e7c40c035ab6cae0c41f76",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -1040,11 +1040,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1773297127,
|
||||
"narHash": "sha256-6E/yhXP7Oy/NbXtf1ktzmU8SdVqJQ09HC/48ebEGBpk=",
|
||||
"lastModified": 1775636079,
|
||||
"narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "71b125cd05fbfd78cab3e070b73544abe24c5016",
|
||||
"rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
---
|
||||
description: Autonomous deep worker — explores thoroughly, acts decisively, finishes the job
|
||||
mode: primary
|
||||
model: openai/gpt-5.4
|
||||
temperature: 0.3
|
||||
color: "#D97706"
|
||||
reasoningEffort: xhigh
|
||||
---
|
||||
You are an autonomous deep worker for software engineering.
|
||||
|
||||
Build context by examining the codebase first. Do not assume. Think through the nuances of the code you encounter. Complete tasks end-to-end within the current turn. Persevere when tool calls fail. Only end your turn when the problem is solved and verified.
|
||||
|
||||
When blocked: try a different approach, decompose the problem, challenge assumptions, explore how others solved it. Asking the user is the last resort after exhausting alternatives.
|
||||
|
||||
## Do Not Ask — Just Do
|
||||
|
||||
FORBIDDEN:
|
||||
- Asking permission ("Should I proceed?", "Would you like me to...?") — JUST DO IT
|
||||
- "Do you want me to run tests?" — RUN THEM
|
||||
- "I noticed Y, should I fix it?" — FIX IT
|
||||
- Stopping after partial implementation — finish or don't start
|
||||
- Answering a question then stopping — questions imply action, DO THE ACTION
|
||||
- "I'll do X" then ending turn — you committed to X, DO X NOW
|
||||
- Explaining findings without acting on them — ACT immediately
|
||||
|
||||
CORRECT:
|
||||
- Keep going until COMPLETELY done
|
||||
- Run verification without asking
|
||||
- Make decisions; course-correct on concrete failure
|
||||
- Note assumptions in your final message, not as questions mid-work
|
||||
|
||||
## Intent Extraction
|
||||
|
||||
Every message has surface form and true intent. Extract true intent BEFORE doing anything:
|
||||
|
||||
- "Did you do X?" (and you didn't) → Acknowledge, DO X immediately
|
||||
- "How does X work?" → Explore, then implement/fix
|
||||
- "Can you look into Y?" → Investigate AND resolve
|
||||
- "What's the best way to do Z?" → Decide, then implement
|
||||
- "Why is A broken?" → Diagnose, then fix
|
||||
|
||||
A message is pure question ONLY when the user explicitly says "just explain" or "don't change anything". Default: message implies action.
|
||||
|
||||
## Task Classification
|
||||
|
||||
Classify before acting:
|
||||
|
||||
- **Trivial**: Single file, known location, <10 lines — use tools directly, no exploration needed
|
||||
- **Explicit**: Specific file/line given, clear instruction — execute directly
|
||||
- **Exploratory**: "How does X work?", "Find Y" — fire parallel searches, then act on findings
|
||||
- **Open-ended**: "Improve", "Refactor", "Add feature" — full execution loop required
|
||||
- **Ambiguous**: Unclear scope, multiple interpretations — explore first (search, read, grep), ask only if exploration fails
|
||||
|
||||
Default bias: explore before asking. Exhaust tools before asking a clarifying question.
|
||||
|
||||
## Execution
|
||||
|
||||
1. **EXPLORE**: Search the codebase in parallel — fire multiple reads and searches simultaneously
|
||||
2. **PLAN**: Identify files to modify, specific changes, dependencies
|
||||
3. **EXECUTE**: Make the changes
|
||||
4. **VERIFY**: Check diagnostics on all modified files, run tests, run build
|
||||
|
||||
If verification fails, return to step 1. After 3 failed approaches, stop edits, revert to last working state, and explain what you tried.
|
||||
|
||||
## Verification Is Mandatory
|
||||
|
||||
Before ending your turn, you MUST have:
|
||||
- All requested functionality fully implemented
|
||||
- Diagnostics clean on all modified files
|
||||
- Build passing (if applicable)
|
||||
- Tests passing (or pre-existing failures documented)
|
||||
- Evidence for each verification step — "it should work" is not evidence
|
||||
|
||||
## Progress
|
||||
|
||||
Report what you're doing every ~30 seconds. One or two sentences with a concrete detail — a file path, a pattern found, a decision made.
|
||||
|
||||
## Self-Check Before Ending
|
||||
|
||||
1. Did the user's message imply action you haven't taken?
|
||||
2. Did you commit to something ("I'll do X") without doing it?
|
||||
3. Did you offer to do something instead of doing it?
|
||||
4. Did you answer a question and stop when work was implied?
|
||||
|
||||
If any of these are true, you are not done. Continue working.
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
description: Add AI session summary to GitHub PR or GitLab MR description
|
||||
---
|
||||
|
||||
Update the PR/MR description with an AI session export summary.
|
||||
|
||||
First, invoke the skill tool to load the session-export skill:
|
||||
|
||||
```
|
||||
skill({ name: 'session-export' })
|
||||
```
|
||||
|
||||
Then follow the skill instructions to export the session summary.
|
||||
|
||||
<user-request>
|
||||
$ARGUMENTS
|
||||
</user-request>
|
||||
150
modules/_opencode/command/supermemory-init.md
Normal file
150
modules/_opencode/command/supermemory-init.md
Normal file
@@ -0,0 +1,150 @@
|
||||
---
|
||||
description: Initialize Supermemory with comprehensive codebase knowledge
|
||||
---
|
||||
|
||||
# Initializing Supermemory
|
||||
|
||||
You are initializing persistent memory for this codebase. This is not just data collection - you're building context that will make you significantly more effective across all future sessions.
|
||||
|
||||
## Understanding Context
|
||||
|
||||
You are a **stateful** coding agent. Users expect to work with you over extended periods - potentially the entire lifecycle of a project. Your memory is how you get better over time and maintain continuity.
|
||||
|
||||
## What to Remember
|
||||
|
||||
### 1. Procedures (Rules & Workflows)
|
||||
Explicit rules that should always be followed:
|
||||
- "Never commit directly to main - always use feature branches"
|
||||
- "Always run lint before tests"
|
||||
- "Use conventional commits format"
|
||||
|
||||
### 2. Preferences (Style & Conventions)
|
||||
Project and user coding style:
|
||||
- "Prefer functional components over class components"
|
||||
- "Use early returns instead of nested conditionals"
|
||||
- "Always add JSDoc to exported functions"
|
||||
|
||||
### 3. Architecture & Context
|
||||
How the codebase works and why:
|
||||
- "Auth system was refactored in v2.0 - old patterns deprecated"
|
||||
- "The monorepo used to have 3 modules before consolidation"
|
||||
- "This pagination bug was fixed before - similar to PR #234"
|
||||
|
||||
## Memory Scopes
|
||||
|
||||
**Project-scoped** (\`scope: "project"\`):
|
||||
- Build/test/lint commands
|
||||
- Architecture and key directories
|
||||
- Team conventions specific to this codebase
|
||||
- Technology stack and framework choices
|
||||
- Known issues and their solutions
|
||||
|
||||
**User-scoped** (\`scope: "user"\`):
|
||||
- Personal coding preferences across all projects
|
||||
- Communication style preferences
|
||||
- General workflow habits
|
||||
|
||||
## Research Approach
|
||||
|
||||
This is a **deep research** initialization. Take your time and be thorough (~50+ tool calls). The goal is to genuinely understand the project, not just collect surface-level facts.
|
||||
|
||||
**What to uncover:**
|
||||
- Tech stack and dependencies (explicit and implicit)
|
||||
- Project structure and architecture
|
||||
- Build/test/deploy commands and workflows
|
||||
- Contributors & team dynamics (who works on what?)
|
||||
- Commit conventions and branching strategy
|
||||
- Code evolution (major refactors, architecture changes)
|
||||
- Pain points (areas with lots of bug fixes)
|
||||
- Implicit conventions not documented anywhere
|
||||
|
||||
## Research Techniques
|
||||
|
||||
### File-based
|
||||
- README.md, CONTRIBUTING.md, AGENTS.md, CLAUDE.md
|
||||
- Package manifests (package.json, Cargo.toml, pyproject.toml, go.mod)
|
||||
- Config files (.eslintrc, tsconfig.json, .prettierrc)
|
||||
- CI/CD configs (.github/workflows/)
|
||||
|
||||
### Git-based
|
||||
- \`git log --oneline -20\` - Recent history
|
||||
- \`git branch -a\` - Branching strategy
|
||||
- \`git log --format="%s" -50\` - Commit conventions
|
||||
- \`git shortlog -sn --all | head -10\` - Main contributors
|
||||
|
||||
### Explore Agent
|
||||
Fire parallel explore queries for broad understanding:
|
||||
\`\`\`
|
||||
Task(explore, "What is the tech stack and key dependencies?")
|
||||
Task(explore, "What is the project structure? Key directories?")
|
||||
Task(explore, "How do you build, test, and run this project?")
|
||||
Task(explore, "What are the main architectural patterns?")
|
||||
Task(explore, "What conventions or patterns are used?")
|
||||
\`\`\`
|
||||
|
||||
## How to Do Thorough Research
|
||||
|
||||
**Don't just collect data - analyze and cross-reference.**
|
||||
|
||||
Bad (shallow):
|
||||
- Run commands, copy output
|
||||
- List facts without understanding
|
||||
|
||||
Good (thorough):
|
||||
- Cross-reference findings (if inconsistent, dig deeper)
|
||||
- Resolve ambiguities (don't leave questions unanswered)
|
||||
- Read actual file content, not just names
|
||||
- Look for patterns (what do commits tell you about workflow?)
|
||||
- Think like a new team member - what would you want to know?
|
||||
|
||||
## Saving Memories
|
||||
|
||||
Use the \`supermemory\` tool for each distinct insight:
|
||||
|
||||
\`\`\`
|
||||
supermemory(mode: "add", content: "...", type: "...", scope: "project")
|
||||
\`\`\`
|
||||
|
||||
**Types:**
|
||||
- \`project-config\` - tech stack, commands, tooling
|
||||
- \`architecture\` - codebase structure, key components, data flow
|
||||
- \`learned-pattern\` - conventions specific to this codebase
|
||||
- \`error-solution\` - known issues and their fixes
|
||||
- \`preference\` - coding style preferences (use with user scope)
|
||||
|
||||
**Guidelines:**
|
||||
- Save each distinct insight as a separate memory
|
||||
- Be concise but include enough context to be useful
|
||||
- Include the "why" not just the "what" when relevant
|
||||
- Update memories incrementally as you research (don't wait until the end)
|
||||
|
||||
**Good memories:**
|
||||
- "Uses Bun runtime and package manager. Commands: bun install, bun run dev, bun test"
|
||||
- "API routes in src/routes/, handlers in src/handlers/. Hono framework."
|
||||
- "Auth uses Redis sessions, not JWT. Implementation in src/lib/auth.ts"
|
||||
- "Never use \`any\` type - strict TypeScript. Use \`unknown\` and narrow."
|
||||
- "Database migrations must be backward compatible - we do rolling deploys"
|
||||
|
||||
## Upfront Questions
|
||||
|
||||
Before diving in, ask:
|
||||
1. "Any specific rules I should always follow?"
|
||||
2. "Preferences for how I communicate? (terse/detailed)"
|
||||
|
||||
## Reflection Phase
|
||||
|
||||
Before finishing, reflect:
|
||||
1. **Completeness**: Did you cover commands, architecture, conventions, gotchas?
|
||||
2. **Quality**: Are memories concise and searchable?
|
||||
3. **Scope**: Did you correctly separate project vs user knowledge?
|
||||
|
||||
Then ask: "I've initialized memory with X insights. Want me to continue refining, or is this good?"
|
||||
|
||||
## Your Task
|
||||
|
||||
1. Ask upfront questions (research depth, rules, preferences)
|
||||
2. Check existing memories: \`supermemory(mode: "list", scope: "project")\`
|
||||
3. Research based on chosen depth
|
||||
4. Save memories incrementally as you discover insights
|
||||
5. Reflect and verify completeness
|
||||
6. Summarize what was learned and ask if user wants refinement
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { Plugin } from "@opencode-ai/plugin";
|
||||
|
||||
export const DirenvPlugin: Plugin = async ({ $ }) => {
|
||||
return {
|
||||
"shell.env": async (input, output) => {
|
||||
try {
|
||||
const exported = await $`direnv export json`
|
||||
.cwd(input.cwd)
|
||||
.quiet()
|
||||
.json();
|
||||
|
||||
Object.assign(output.env, exported);
|
||||
} catch (error) {
|
||||
console.warn("[direnv] failed to export env:", error);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,3 +1,9 @@
|
||||
// Substantially modified from:
|
||||
// https://github.com/mitsuhiko/agent-stuff/blob/80e1e96/pi-extensions/review.ts
|
||||
// Copyright Armin Ronacher and contributors
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
import type {
|
||||
TuiPlugin,
|
||||
TuiPluginModule,
|
||||
@@ -8,6 +14,17 @@ import path from "node:path"
|
||||
|
||||
type BookmarkRef = { name: string; remote?: string }
|
||||
type Change = { changeId: string; title: string }
|
||||
type JjRemote = { name: string; url: string }
|
||||
type PullRequestListItem = {
|
||||
prNumber: number
|
||||
title: string
|
||||
updatedAt: string
|
||||
reviewRequested: boolean
|
||||
author?: string
|
||||
baseRefName?: string
|
||||
headRefName?: string
|
||||
isManualEntry?: boolean
|
||||
}
|
||||
|
||||
type ReviewTarget =
|
||||
| { type: "workingCopy" }
|
||||
@@ -25,6 +42,9 @@ type ReviewTarget =
|
||||
type ReviewSelectorValue = ReviewTarget["type"] | "toggleCustomInstructions"
|
||||
|
||||
const CUSTOM_INSTRUCTIONS_KEY = "review.customInstructions"
|
||||
const MIN_CHANGE_REVIEW_OPTIONS = 10
|
||||
const RECENT_PULL_REQUEST_LIMIT = 5
|
||||
const PULL_REQUEST_MAX_AGE_DAYS = 7
|
||||
|
||||
const WORKING_COPY_PROMPT =
|
||||
"Review the current working-copy changes (including new files) and provide prioritized findings."
|
||||
@@ -156,6 +176,13 @@ Provide your findings in a clear, structured format:
|
||||
|
||||
Output all findings the author would fix if they knew about them. If there are no qualifying findings, explicitly state the code looks good. Don't stop at the first finding - list every qualifying issue. Then append the required non-blocking callouts section.`
|
||||
|
||||
function normalizeCustomInstructions(
|
||||
value: string | undefined,
|
||||
): string | undefined {
|
||||
const normalized = value?.trim()
|
||||
return normalized ? normalized : undefined
|
||||
}
|
||||
|
||||
function bookmarkLabel(b: BookmarkRef): string {
|
||||
return b.remote ? `${b.name}@${b.remote}` : b.name
|
||||
}
|
||||
@@ -205,19 +232,139 @@ function parseChanges(stdout: string): Change[] {
|
||||
|
||||
function parsePrRef(ref: string): number | null {
|
||||
const trimmed = ref.trim()
|
||||
const num = parseInt(trimmed, 10)
|
||||
if (!isNaN(num) && num > 0) return num
|
||||
const urlMatch = trimmed.match(/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)/)
|
||||
if (urlMatch) return parseInt(urlMatch[1], 10)
|
||||
if (/^\d+$/.test(trimmed)) {
|
||||
const num = Number(trimmed)
|
||||
return Number.isSafeInteger(num) && num > 0 ? num : null
|
||||
}
|
||||
|
||||
const urlMatch = trimmed.match(
|
||||
/^(?:https?:\/\/)?github\.com\/[^/]+\/[^/]+\/pull\/(\d+)(?:[/?#].*)?$/i,
|
||||
)
|
||||
if (urlMatch) {
|
||||
const num = Number(urlMatch[1])
|
||||
return Number.isSafeInteger(num) && num > 0 ? num : null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function formatRelativeTime(value: string): string | null {
|
||||
const timestamp = Date.parse(value)
|
||||
if (!Number.isFinite(timestamp)) return null
|
||||
|
||||
const deltaMs = Date.now() - timestamp
|
||||
const future = deltaMs < 0
|
||||
const absoluteSeconds = Math.round(Math.abs(deltaMs) / 1000)
|
||||
|
||||
if (absoluteSeconds < 60) return future ? "in <1m" : "just now"
|
||||
|
||||
const units = [
|
||||
{ label: "y", seconds: 60 * 60 * 24 * 365 },
|
||||
{ label: "mo", seconds: 60 * 60 * 24 * 30 },
|
||||
{ label: "d", seconds: 60 * 60 * 24 },
|
||||
{ label: "h", seconds: 60 * 60 },
|
||||
{ label: "m", seconds: 60 },
|
||||
]
|
||||
|
||||
for (const unit of units) {
|
||||
if (absoluteSeconds >= unit.seconds) {
|
||||
const count = Math.floor(absoluteSeconds / unit.seconds)
|
||||
return future ? `in ${count}${unit.label}` : `${count}${unit.label} ago`
|
||||
}
|
||||
}
|
||||
|
||||
return future ? "soon" : "just now"
|
||||
}
|
||||
|
||||
function getPullRequestUpdatedSinceDate(days: number): string {
|
||||
const timestamp = Date.now() - days * 24 * 60 * 60 * 1000
|
||||
return new Date(timestamp).toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
function parsePullRequests(stdout: string): PullRequestListItem[] {
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(stdout)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsed)) return []
|
||||
|
||||
return parsed.flatMap((entry) => {
|
||||
if (!entry || typeof entry !== "object") return []
|
||||
|
||||
const prNumber =
|
||||
typeof entry.number === "number" && Number.isSafeInteger(entry.number)
|
||||
? entry.number
|
||||
: null
|
||||
const title = typeof entry.title === "string" ? entry.title.trim() : ""
|
||||
const updatedAt =
|
||||
typeof entry.updatedAt === "string" ? entry.updatedAt.trim() : ""
|
||||
if (!prNumber || !title || !updatedAt) return []
|
||||
|
||||
return [
|
||||
{
|
||||
prNumber,
|
||||
title,
|
||||
updatedAt,
|
||||
reviewRequested: entry.reviewRequested === true,
|
||||
author:
|
||||
entry.author &&
|
||||
typeof entry.author === "object" &&
|
||||
typeof entry.author.login === "string"
|
||||
? entry.author.login
|
||||
: undefined,
|
||||
baseRefName:
|
||||
typeof entry.baseRefName === "string"
|
||||
? entry.baseRefName.trim() || undefined
|
||||
: undefined,
|
||||
headRefName:
|
||||
typeof entry.headRefName === "string"
|
||||
? entry.headRefName.trim() || undefined
|
||||
: undefined,
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
function dedupePullRequests(
|
||||
pullRequests: PullRequestListItem[],
|
||||
): PullRequestListItem[] {
|
||||
const seen = new Set<number>()
|
||||
const result: PullRequestListItem[] = []
|
||||
|
||||
for (const pullRequest of pullRequests) {
|
||||
if (seen.has(pullRequest.prNumber)) continue
|
||||
seen.add(pullRequest.prNumber)
|
||||
result.push(pullRequest)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function buildPullRequestOptionDescription(pr: PullRequestListItem): string {
|
||||
if (pr.isManualEntry) return "(enter PR number or URL)"
|
||||
|
||||
const parts = [pr.reviewRequested ? "review requested" : "recent"]
|
||||
const relativeTime = formatRelativeTime(pr.updatedAt)
|
||||
if (relativeTime) parts.push(`updated ${relativeTime}`)
|
||||
if (pr.author) parts.push(`@${pr.author}`)
|
||||
if (pr.baseRefName && pr.headRefName) {
|
||||
parts.push(`${pr.headRefName} → ${pr.baseRefName}`)
|
||||
}
|
||||
|
||||
return `(${parts.join(" · ")})`
|
||||
}
|
||||
|
||||
function normalizeRemoteUrl(value: string): string {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/^git@github\.com:/, "https://github.com/")
|
||||
.replace(/^ssh:\/\/git@github\.com\//, "https://github.com/")
|
||||
.replace(/^git@([^:]+):/, "https://$1/")
|
||||
.replace(/^ssh:\/\/git@([^/]+)\//, "https://$1/")
|
||||
.replace(/^(https?:\/\/)[^/@]+@/i, "$1")
|
||||
.replace(/\.git$/, "")
|
||||
.replace(/\/+$/, "")
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
@@ -228,18 +375,14 @@ function sanitizeRemoteName(value: string): string {
|
||||
)
|
||||
}
|
||||
|
||||
function getRepositoryUrl(value: string): string | null {
|
||||
const match = normalizeRemoteUrl(value).match(/^https?:\/\/[^/]+\/[^/]+\/[^/]+/)
|
||||
if (!match) return null
|
||||
return match[0]
|
||||
}
|
||||
|
||||
const plugin: TuiPlugin = async (api) => {
|
||||
const cwd = api.state.path.directory
|
||||
let reviewCustomInstructions = normalizeCustomInstructions(
|
||||
api.kv.get<string | undefined>(CUSTOM_INSTRUCTIONS_KEY, undefined),
|
||||
)
|
||||
|
||||
function setReviewCustomInstructions(value?: string): void {
|
||||
reviewCustomInstructions = normalizeCustomInstructions(value)
|
||||
api.kv.set(CUSTOM_INSTRUCTIONS_KEY, reviewCustomInstructions)
|
||||
}
|
||||
|
||||
// -- shell helpers -------------------------------------------------------
|
||||
|
||||
async function exec(
|
||||
cmd: string,
|
||||
@@ -276,72 +419,33 @@ const plugin: TuiPlugin = async (api) => {
|
||||
return { stdout: r.stdout, ok: r.exitCode === 0, stderr: r.stderr }
|
||||
}
|
||||
|
||||
// -- jj helpers ----------------------------------------------------------
|
||||
|
||||
async function isJjRepo(): Promise<boolean> {
|
||||
return (await jj("root")).ok
|
||||
}
|
||||
|
||||
const reviewCustomInstructionsKey = `${CUSTOM_INSTRUCTIONS_KEY}-${cwd}`
|
||||
let reviewCustomInstructions = normalizeCustomInstructions(
|
||||
api.kv.get<string | undefined>(reviewCustomInstructionsKey, undefined),
|
||||
)
|
||||
|
||||
function setReviewCustomInstructions(value?: string): void {
|
||||
reviewCustomInstructions = normalizeCustomInstructions(value)
|
||||
api.kv.set(reviewCustomInstructionsKey, reviewCustomInstructions)
|
||||
}
|
||||
|
||||
async function hasWorkingCopyChanges(): Promise<boolean> {
|
||||
const r = await jj("diff", "--summary")
|
||||
return r.ok && r.stdout.trim().length > 0
|
||||
}
|
||||
|
||||
async function getBookmarks(): Promise<BookmarkRef[]> {
|
||||
const r = await jj(
|
||||
"bookmark",
|
||||
"list",
|
||||
"--all-remotes",
|
||||
"-T",
|
||||
'name ++ "\\t" ++ remote ++ "\\n"',
|
||||
)
|
||||
if (!r.ok) return []
|
||||
return parseBookmarks(r.stdout)
|
||||
}
|
||||
|
||||
async function getCurrentBookmarks(): Promise<BookmarkRef[]> {
|
||||
const headRevset = (await hasWorkingCopyChanges()) ? "@" : "@-"
|
||||
const r = await jj(
|
||||
"bookmark",
|
||||
"list",
|
||||
"--all-remotes",
|
||||
"-r",
|
||||
headRevset,
|
||||
"-T",
|
||||
'name ++ "\\t" ++ remote ++ "\\n"',
|
||||
)
|
||||
if (!r.ok) return []
|
||||
return parseBookmarks(r.stdout)
|
||||
}
|
||||
|
||||
async function getDefaultBookmark(): Promise<BookmarkRef | null> {
|
||||
const trunkR = await jj(
|
||||
"bookmark",
|
||||
"list",
|
||||
"--all-remotes",
|
||||
"-r",
|
||||
"trunk()",
|
||||
"-T",
|
||||
'name ++ "\\t" ++ remote ++ "\\n"',
|
||||
)
|
||||
if (trunkR.ok) {
|
||||
const bookmarks = parseBookmarks(trunkR.stdout)
|
||||
if (bookmarks.length > 0) return bookmarks[0]
|
||||
}
|
||||
const all = await getBookmarks()
|
||||
return (
|
||||
all.find((b) => !b.remote && b.name === "main") ??
|
||||
all.find((b) => !b.remote && b.name === "master") ??
|
||||
all[0] ??
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
async function getRecentChanges(limit = 20): Promise<Change[]> {
|
||||
const effectiveLimit = Math.max(limit, MIN_CHANGE_REVIEW_OPTIONS)
|
||||
const r = await jj(
|
||||
"log",
|
||||
"-r",
|
||||
"all()",
|
||||
"-n",
|
||||
String(limit),
|
||||
String(effectiveLimit),
|
||||
"--no-graph",
|
||||
"-T",
|
||||
'change_id.shortest(8) ++ "\\t" ++ description.first_line() ++ "\\n"',
|
||||
@@ -350,6 +454,53 @@ const plugin: TuiPlugin = async (api) => {
|
||||
return parseChanges(r.stdout)
|
||||
}
|
||||
|
||||
async function getPullRequests(
|
||||
args: string[],
|
||||
reviewRequested: boolean,
|
||||
): Promise<PullRequestListItem[]> {
|
||||
const response = await gh(
|
||||
"pr",
|
||||
"list",
|
||||
...args,
|
||||
"--json",
|
||||
"number,title,updatedAt,author,baseRefName,headRefName",
|
||||
)
|
||||
if (!response.ok) return []
|
||||
|
||||
return parsePullRequests(response.stdout).map((pr) => ({
|
||||
...pr,
|
||||
reviewRequested,
|
||||
}))
|
||||
}
|
||||
|
||||
async function getSelectablePullRequests(): Promise<PullRequestListItem[]> {
|
||||
const updatedSince = getPullRequestUpdatedSinceDate(
|
||||
PULL_REQUEST_MAX_AGE_DAYS,
|
||||
)
|
||||
const [reviewRequested, recent] = await Promise.all([
|
||||
getPullRequests(
|
||||
[
|
||||
"--search",
|
||||
`review-requested:@me updated:>=${updatedSince} sort:updated-desc`,
|
||||
"--limit",
|
||||
"50",
|
||||
],
|
||||
true,
|
||||
),
|
||||
getPullRequests(
|
||||
[
|
||||
"--search",
|
||||
`updated:>=${updatedSince} sort:updated-desc`,
|
||||
"--limit",
|
||||
String(RECENT_PULL_REQUEST_LIMIT),
|
||||
],
|
||||
false,
|
||||
),
|
||||
])
|
||||
|
||||
return dedupePullRequests([...reviewRequested, ...recent])
|
||||
}
|
||||
|
||||
async function getMergeBase(
|
||||
bookmark: string,
|
||||
remote?: string,
|
||||
@@ -372,8 +523,6 @@ const plugin: TuiPlugin = async (api) => {
|
||||
return lines.length === 1 ? lines[0].trim() : null
|
||||
}
|
||||
|
||||
// -- PR materialization --------------------------------------------------
|
||||
|
||||
async function materializePr(prNumber: number): Promise<
|
||||
| {
|
||||
ok: true
|
||||
@@ -393,24 +542,19 @@ const plugin: TuiPlugin = async (api) => {
|
||||
}
|
||||
}
|
||||
|
||||
const savedR = await jj(
|
||||
"log",
|
||||
"-r",
|
||||
"@",
|
||||
"--no-graph",
|
||||
"-T",
|
||||
"change_id.shortest(8)",
|
||||
)
|
||||
const savedChangeId = savedR.stdout.trim()
|
||||
const savedChangeId = await getSingleChangeId("@")
|
||||
if (!savedChangeId) {
|
||||
return { ok: false, error: "Failed to determine the current change" }
|
||||
}
|
||||
|
||||
const prR = await gh(
|
||||
const prResponse = await gh(
|
||||
"pr",
|
||||
"view",
|
||||
String(prNumber),
|
||||
"--json",
|
||||
"baseRefName,title,headRefName,isCrossRepository,headRepository,headRepositoryOwner",
|
||||
"baseRefName,title,headRefName,isCrossRepository,headRepository,headRepositoryOwner,url",
|
||||
)
|
||||
if (!prR.ok) {
|
||||
if (!prResponse.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `Could not find PR #${prNumber}. Check gh auth and that the PR exists.`,
|
||||
@@ -422,54 +566,36 @@ const plugin: TuiPlugin = async (api) => {
|
||||
title: string
|
||||
headRefName: string
|
||||
isCrossRepository: boolean
|
||||
url: string
|
||||
headRepository?: { name: string; url: string }
|
||||
headRepositoryOwner?: { login: string }
|
||||
}
|
||||
try {
|
||||
prInfo = JSON.parse(prR.stdout)
|
||||
prInfo = JSON.parse(prResponse.stdout)
|
||||
} catch {
|
||||
return { ok: false, error: "Failed to parse PR info" }
|
||||
}
|
||||
|
||||
const remotesR = await jj("git", "remote", "list")
|
||||
const remotes = remotesR.stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const [name, ...urlParts] = line.split(/\s+/)
|
||||
return { name, url: urlParts.join(" ") }
|
||||
})
|
||||
.filter((r) => r.name && r.url)
|
||||
|
||||
const defaultRemote =
|
||||
remotes.find((r) => r.name === "origin") ?? remotes[0]
|
||||
const remotes = await getJjRemotes()
|
||||
const defaultRemote = getDefaultRemote(remotes)
|
||||
if (!defaultRemote) {
|
||||
return { ok: false, error: "No jj remotes configured" }
|
||||
}
|
||||
|
||||
const baseRepoUrl = getRepositoryUrl(prInfo.url)
|
||||
const baseRemote = baseRepoUrl
|
||||
? remotes.find((remote) => getRepositoryUrl(remote.url) === baseRepoUrl)
|
||||
: undefined
|
||||
|
||||
let remoteName = defaultRemote.name
|
||||
let addedTempRemote = false
|
||||
|
||||
if (prInfo.isCrossRepository) {
|
||||
const repoSlug =
|
||||
prInfo.headRepositoryOwner?.login && prInfo.headRepository?.name
|
||||
? `${prInfo.headRepositoryOwner.login}/${prInfo.headRepository.name}`.toLowerCase()
|
||||
: undefined
|
||||
const forkUrl = prInfo.headRepository?.url
|
||||
|
||||
const existingRemote = remotes.find((r) => {
|
||||
if (
|
||||
forkUrl &&
|
||||
normalizeRemoteUrl(r.url) === normalizeRemoteUrl(forkUrl)
|
||||
)
|
||||
return true
|
||||
return repoSlug
|
||||
? normalizeRemoteUrl(r.url).includes(
|
||||
`github.com/${repoSlug}`,
|
||||
)
|
||||
: false
|
||||
})
|
||||
const forkRepoUrl = forkUrl ? getRepositoryUrl(forkUrl) : null
|
||||
const existingRemote = forkRepoUrl
|
||||
? remotes.find((remote) => getRepositoryUrl(remote.url) === forkRepoUrl)
|
||||
: undefined
|
||||
|
||||
if (existingRemote) {
|
||||
remoteName = existingRemote.name
|
||||
@@ -483,21 +609,23 @@ const plugin: TuiPlugin = async (api) => {
|
||||
while (names.has(remoteName)) {
|
||||
remoteName = `${baseName}-${suffix++}`
|
||||
}
|
||||
const addR = await jj(
|
||||
const addRemoteResult = await jj(
|
||||
"git",
|
||||
"remote",
|
||||
"add",
|
||||
remoteName,
|
||||
forkUrl,
|
||||
)
|
||||
if (!addR.ok) return { ok: false, error: "Failed to add PR remote" }
|
||||
if (!addRemoteResult.ok) {
|
||||
return { ok: false, error: "Failed to add PR remote" }
|
||||
}
|
||||
addedTempRemote = true
|
||||
} else {
|
||||
return { ok: false, error: "PR fork URL is unavailable" }
|
||||
}
|
||||
}
|
||||
|
||||
const fetchR = await jj(
|
||||
const fetchHeadResult = await jj(
|
||||
"git",
|
||||
"fetch",
|
||||
"--remote",
|
||||
@@ -505,15 +633,15 @@ const plugin: TuiPlugin = async (api) => {
|
||||
"--branch",
|
||||
prInfo.headRefName,
|
||||
)
|
||||
if (!fetchR.ok) {
|
||||
if (!fetchHeadResult.ok) {
|
||||
if (addedTempRemote)
|
||||
await jj("git", "remote", "remove", remoteName)
|
||||
return { ok: false, error: "Failed to fetch PR branch" }
|
||||
}
|
||||
|
||||
const revset = `remote_bookmarks(exact:${JSON.stringify(prInfo.headRefName)}, exact:${JSON.stringify(remoteName)})`
|
||||
const newR = await jj("new", revset)
|
||||
if (!newR.ok) {
|
||||
const createChangeResult = await jj("new", revset)
|
||||
if (!createChangeResult.ok) {
|
||||
if (addedTempRemote)
|
||||
await jj("git", "remote", "remove", remoteName)
|
||||
return { ok: false, error: "Failed to create change on PR branch" }
|
||||
@@ -521,27 +649,17 @@ const plugin: TuiPlugin = async (api) => {
|
||||
|
||||
if (addedTempRemote) await jj("git", "remote", "remove", remoteName)
|
||||
|
||||
// Resolve base bookmark remote
|
||||
const baseRef = await resolveBookmarkRef(prInfo.baseRefName)
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
title: prInfo.title,
|
||||
baseBookmark: prInfo.baseRefName,
|
||||
baseRemote: baseRef?.remote,
|
||||
baseRemote: baseRemote?.name,
|
||||
headBookmark: prInfo.headRefName,
|
||||
remote: remoteName,
|
||||
savedChangeId,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCustomInstructions(
|
||||
value: string | undefined,
|
||||
): string | undefined {
|
||||
const normalized = value?.trim()
|
||||
return normalized ? normalized : undefined
|
||||
}
|
||||
|
||||
function parseNonEmptyLines(stdout: string): string[] {
|
||||
return stdout
|
||||
.trim()
|
||||
@@ -554,19 +672,6 @@ const plugin: TuiPlugin = async (api) => {
|
||||
return left.name === right.name && left.remote === right.remote
|
||||
}
|
||||
|
||||
function parseBookmarkReference(value: string): BookmarkRef {
|
||||
const trimmed = value.trim()
|
||||
const separatorIndex = trimmed.lastIndexOf("@")
|
||||
if (separatorIndex <= 0 || separatorIndex === trimmed.length - 1) {
|
||||
return { name: trimmed }
|
||||
}
|
||||
|
||||
return {
|
||||
name: trimmed.slice(0, separatorIndex),
|
||||
remote: trimmed.slice(separatorIndex + 1),
|
||||
}
|
||||
}
|
||||
|
||||
function dedupeBookmarkRefs(bookmarks: BookmarkRef[]): BookmarkRef[] {
|
||||
const seen = new Set<string>()
|
||||
const result: BookmarkRef[] = []
|
||||
@@ -625,7 +730,7 @@ const plugin: TuiPlugin = async (api) => {
|
||||
return revisions.length === 1 ? revisions[0] : null
|
||||
}
|
||||
|
||||
async function getJjRemotes(): Promise<Array<{ name: string; url: string }>> {
|
||||
async function getJjRemotes(): Promise<JjRemote[]> {
|
||||
const r = await jj("git", "remote", "list")
|
||||
if (!r.ok) return []
|
||||
|
||||
@@ -637,10 +742,16 @@ const plugin: TuiPlugin = async (api) => {
|
||||
.filter((remote) => remote.name && remote.url)
|
||||
}
|
||||
|
||||
function getDefaultRemote(remotes: JjRemote[]): JjRemote | null {
|
||||
return (
|
||||
remotes.find((remote) => remote.name === "origin") ??
|
||||
remotes[0] ??
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
async function getDefaultRemoteName(): Promise<string | null> {
|
||||
const remotes = await getJjRemotes()
|
||||
if (remotes.length === 0) return null
|
||||
return remotes.find((remote) => remote.name === "origin")?.name ?? remotes[0].name
|
||||
return getDefaultRemote(await getJjRemotes())?.name ?? null
|
||||
}
|
||||
|
||||
function preferBookmarkRef(
|
||||
@@ -759,8 +870,6 @@ const plugin: TuiPlugin = async (api) => {
|
||||
}
|
||||
}
|
||||
|
||||
// -- prompt building -----------------------------------------------------
|
||||
|
||||
async function buildTargetReviewPrompt(
|
||||
target: ReviewTarget,
|
||||
options?: { includeLocalChanges?: boolean },
|
||||
@@ -841,15 +950,15 @@ const plugin: TuiPlugin = async (api) => {
|
||||
}
|
||||
|
||||
async function buildReviewPrompt(target: ReviewTarget): Promise<string> {
|
||||
const prompt = await buildTargetReviewPrompt(target)
|
||||
const prompt = await buildTargetReviewPrompt(target, {
|
||||
includeLocalChanges:
|
||||
target.type !== "workingCopy" && (await hasWorkingCopyChanges()),
|
||||
})
|
||||
const projectGuidelines = await loadProjectReviewGuidelines()
|
||||
const sharedInstructions = normalizeCustomInstructions(
|
||||
reviewCustomInstructions,
|
||||
)
|
||||
let fullPrompt = `${REVIEW_RUBRIC}\n\n---\n\nPlease perform a code review with the following focus:\n\n${prompt}`
|
||||
|
||||
if (sharedInstructions) {
|
||||
fullPrompt += `\n\nShared custom review instructions (applies to all reviews):\n\n${sharedInstructions}`
|
||||
if (reviewCustomInstructions) {
|
||||
fullPrompt += `\n\nCustom review instructions for this working directory (applies to all review modes here):\n\n${reviewCustomInstructions}`
|
||||
}
|
||||
|
||||
if (projectGuidelines) {
|
||||
@@ -910,17 +1019,13 @@ const plugin: TuiPlugin = async (api) => {
|
||||
}
|
||||
}
|
||||
|
||||
// -- review execution ----------------------------------------------------
|
||||
|
||||
async function startReview(target: ReviewTarget): Promise<void> {
|
||||
async function startReview(target: ReviewTarget): Promise<boolean> {
|
||||
const prompt = await buildReviewPrompt(target)
|
||||
const hint = getUserFacingHint(target)
|
||||
const cleared = await api.client.tui.clearPrompt()
|
||||
const appended = await api.client.tui.appendPrompt({
|
||||
text: prompt,
|
||||
})
|
||||
// `prompt.submit` is ignored unless the prompt input is focused.
|
||||
// When this runs from a dialog, focus returns on the next tick.
|
||||
await sleep(50)
|
||||
const submitted = await api.client.tui.submitPrompt()
|
||||
|
||||
@@ -929,16 +1034,16 @@ const plugin: TuiPlugin = async (api) => {
|
||||
message: "Failed to start review prompt automatically",
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
api.ui.toast({
|
||||
message: `Starting review: ${hint}`,
|
||||
variant: "info",
|
||||
})
|
||||
}
|
||||
|
||||
// -- dialogs -------------------------------------------------------------
|
||||
return true
|
||||
}
|
||||
|
||||
async function showReviewSelector(): Promise<void> {
|
||||
const smartDefault = await getSmartDefault()
|
||||
@@ -972,8 +1077,8 @@ const plugin: TuiPlugin = async (api) => {
|
||||
: "Add custom review instructions",
|
||||
value: "toggleCustomInstructions",
|
||||
description: reviewCustomInstructions
|
||||
? "(currently set)"
|
||||
: "(applies to all review modes)",
|
||||
? "(set for this directory)"
|
||||
: "(this directory, all review modes)",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -996,7 +1101,7 @@ const plugin: TuiPlugin = async (api) => {
|
||||
void showChangeSelector()
|
||||
break
|
||||
case "pullRequest":
|
||||
void showPrInput()
|
||||
void showPrSelector()
|
||||
break
|
||||
case "folder":
|
||||
showFolderInput()
|
||||
@@ -1005,7 +1110,8 @@ const plugin: TuiPlugin = async (api) => {
|
||||
if (reviewCustomInstructions) {
|
||||
setReviewCustomInstructions(undefined)
|
||||
api.ui.toast({
|
||||
message: "Custom review instructions removed",
|
||||
message:
|
||||
"Custom review instructions removed for this directory",
|
||||
variant: "info",
|
||||
})
|
||||
void showReviewSelector()
|
||||
@@ -1040,7 +1146,8 @@ const plugin: TuiPlugin = async (api) => {
|
||||
|
||||
setReviewCustomInstructions(next)
|
||||
api.ui.toast({
|
||||
message: "Custom review instructions saved",
|
||||
message:
|
||||
"Custom review instructions saved for this directory",
|
||||
variant: "success",
|
||||
})
|
||||
void showReviewSelector()
|
||||
@@ -1150,16 +1257,7 @@ const plugin: TuiPlugin = async (api) => {
|
||||
)
|
||||
}
|
||||
|
||||
async function showPrInput(): Promise<void> {
|
||||
if (await hasWorkingCopyChanges()) {
|
||||
api.ui.toast({
|
||||
message:
|
||||
"Cannot materialize PR: you have local jj changes. Please snapshot or discard them first.",
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
function showPrManualInput(): void {
|
||||
api.ui.dialog.replace(
|
||||
() =>
|
||||
api.ui.DialogPrompt({
|
||||
@@ -1179,6 +1277,69 @@ const plugin: TuiPlugin = async (api) => {
|
||||
api.ui.dialog.clear()
|
||||
void handlePrReview(prNumber)
|
||||
},
|
||||
onCancel: () => {
|
||||
api.ui.dialog.clear()
|
||||
void showPrSelector()
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function showPrSelector(): Promise<void> {
|
||||
if (await hasWorkingCopyChanges()) {
|
||||
api.ui.toast({
|
||||
message:
|
||||
"Cannot materialize PR: you have local jj changes. Please snapshot or discard them first.",
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
api.ui.toast({ message: "Loading pull requests...", variant: "info" })
|
||||
|
||||
const pullRequests = await getSelectablePullRequests()
|
||||
const options: TuiDialogSelectOption<PullRequestListItem>[] = [
|
||||
{
|
||||
title: "Enter a PR number or URL",
|
||||
value: {
|
||||
prNumber: -1,
|
||||
title: "Manual entry",
|
||||
updatedAt: "",
|
||||
reviewRequested: false,
|
||||
isManualEntry: true,
|
||||
},
|
||||
description: "(override the list)",
|
||||
},
|
||||
...pullRequests.map((pr) => ({
|
||||
title: `#${pr.prNumber} ${pr.title}`,
|
||||
value: pr,
|
||||
description: buildPullRequestOptionDescription(pr),
|
||||
})),
|
||||
]
|
||||
|
||||
if (pullRequests.length === 0) {
|
||||
api.ui.toast({
|
||||
message:
|
||||
"No pull requests found from GitHub; you can still enter a PR number or URL.",
|
||||
variant: "info",
|
||||
})
|
||||
}
|
||||
|
||||
api.ui.dialog.replace(
|
||||
() =>
|
||||
api.ui.DialogSelect({
|
||||
title: "Select pull request to review",
|
||||
placeholder: "Filter pull requests...",
|
||||
options,
|
||||
onSelect: (option) => {
|
||||
api.ui.dialog.clear()
|
||||
if (option.value.isManualEntry) {
|
||||
showPrManualInput()
|
||||
return
|
||||
}
|
||||
|
||||
void handlePrReview(option.value.prNumber)
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1206,13 +1367,22 @@ const plugin: TuiPlugin = async (api) => {
|
||||
variant: "info",
|
||||
})
|
||||
|
||||
await startReview({
|
||||
const started = await startReview({
|
||||
type: "pullRequest",
|
||||
prNumber,
|
||||
baseBookmark: result.baseBookmark,
|
||||
baseRemote: result.baseRemote,
|
||||
title: result.title,
|
||||
})
|
||||
if (started) return
|
||||
|
||||
const restored = await jj("edit", result.savedChangeId)
|
||||
api.ui.toast({
|
||||
message: restored.ok
|
||||
? "Restored the previous change after the review prompt failed"
|
||||
: `Review prompt failed and restoring the previous change also failed (${result.savedChangeId})`,
|
||||
variant: restored.ok ? "info" : "error",
|
||||
})
|
||||
}
|
||||
|
||||
function showFolderInput(): void {
|
||||
@@ -1240,12 +1410,8 @@ const plugin: TuiPlugin = async (api) => {
|
||||
)
|
||||
}
|
||||
|
||||
// -- jj repo check -------------------------------------------------------
|
||||
|
||||
const inJjRepo = await isJjRepo()
|
||||
|
||||
// -- command registration ------------------------------------------------
|
||||
|
||||
api.command.register(() =>
|
||||
inJjRepo
|
||||
? [
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
---
|
||||
name: librarian
|
||||
description: Multi-repository codebase exploration. Research library internals, find code patterns, understand architecture, compare implementations across GitHub/npm/PyPI/crates. Use when needing deep understanding of how libraries work, finding implementations across open source, or exploring remote repository structure.
|
||||
references:
|
||||
- references/tool-routing.md
|
||||
- references/opensrc-api.md
|
||||
- references/opensrc-examples.md
|
||||
- references/linking.md
|
||||
- references/diagrams.md
|
||||
---
|
||||
|
||||
# Librarian Skill
|
||||
|
||||
Deep codebase exploration across remote repositories.
|
||||
|
||||
## How to Use This Skill
|
||||
|
||||
### Reference Structure
|
||||
|
||||
| File | Purpose | When to Read |
|
||||
|------|---------|--------------|
|
||||
| `tool-routing.md` | Tool selection decision trees | **Always read first** |
|
||||
| `opensrc-api.md` | API reference, types | Writing opensrc code |
|
||||
| `opensrc-examples.md` | JavaScript patterns, workflows | Implementation examples |
|
||||
| `linking.md` | GitHub URL patterns | Formatting responses |
|
||||
| `diagrams.md` | Mermaid patterns | Visualizing architecture |
|
||||
|
||||
### Reading Order
|
||||
|
||||
1. **Start** with `tool-routing.md` → choose tool strategy
|
||||
2. **If using opensrc:**
|
||||
- Read `opensrc-api.md` for API details
|
||||
- Read `opensrc-examples.md` for patterns
|
||||
3. **Before responding:** `linking.md` + `diagrams.md` for output formatting
|
||||
|
||||
## Tool Arsenal
|
||||
|
||||
| Tool | Best For | Limitations |
|
||||
|------|----------|-------------|
|
||||
| **grep_app** | Find patterns across ALL public GitHub | Literal search only |
|
||||
| **context7** | Library docs, API examples, usage | Known libraries only |
|
||||
| **opensrc** | Fetch full source for deep exploration | Must fetch before read |
|
||||
|
||||
## Quick Decision Trees
|
||||
|
||||
### "How does X work?"
|
||||
|
||||
```
|
||||
Known library?
|
||||
├─ Yes → context7.resolve-library-id → context7.query-docs
|
||||
│ └─ Need internals? → opensrc.fetch → read source
|
||||
└─ No → grep_app search → opensrc.fetch top result
|
||||
```
|
||||
|
||||
### "Find pattern X"
|
||||
|
||||
```
|
||||
Specific repo?
|
||||
├─ Yes → opensrc.fetch → opensrc.grep → read matches
|
||||
└─ No → grep_app (broad) → opensrc.fetch interesting repos
|
||||
```
|
||||
|
||||
### "Explore repo structure"
|
||||
|
||||
```
|
||||
1. opensrc.fetch(target)
|
||||
2. opensrc.tree(source.name) → quick overview
|
||||
3. opensrc.files(source.name, "**/*.ts") → detailed listing
|
||||
4. Read: README, package.json, src/index.*
|
||||
5. Create architecture diagram (see diagrams.md)
|
||||
```
|
||||
|
||||
### "Compare X vs Y"
|
||||
|
||||
```
|
||||
1. opensrc.fetch(["X", "Y"])
|
||||
2. Use source.name from results for subsequent calls
|
||||
3. opensrc.grep(pattern, { sources: [nameX, nameY] })
|
||||
4. Read comparable files, synthesize differences
|
||||
```
|
||||
|
||||
## Critical: Source Naming Convention
|
||||
|
||||
**After fetching, always use `source.name` for subsequent calls:**
|
||||
|
||||
```javascript
|
||||
const [{ source }] = await opensrc.fetch("vercel/ai");
|
||||
const files = await opensrc.files(source.name, "**/*.ts");
|
||||
```
|
||||
|
||||
| Type | Fetch Spec | Source Name |
|
||||
|------|------------|-------------|
|
||||
| npm | `"zod"` | `"zod"` |
|
||||
| npm scoped | `"@tanstack/react-query"` | `"@tanstack/react-query"` |
|
||||
| pypi | `"pypi:requests"` | `"requests"` |
|
||||
| crates | `"crates:serde"` | `"serde"` |
|
||||
| GitHub | `"vercel/ai"` | `"github.com/vercel/ai"` |
|
||||
| GitLab | `"gitlab:org/repo"` | `"gitlab.com/org/repo"` |
|
||||
|
||||
## When NOT to Use opensrc
|
||||
|
||||
| Scenario | Use Instead |
|
||||
|----------|-------------|
|
||||
| Simple library API questions | context7 |
|
||||
| Finding examples across many repos | grep_app |
|
||||
| Very large monorepos (>10GB) | Clone locally |
|
||||
| Private repositories | Direct access |
|
||||
|
||||
## Output Guidelines
|
||||
|
||||
1. **Comprehensive final message** - only last message returns to main agent
|
||||
2. **Parallel tool calls** - maximize efficiency
|
||||
3. **Link every file reference** - see `linking.md`
|
||||
4. **Diagram complex relationships** - see `diagrams.md`
|
||||
5. **Never mention tool names** - say "I'll search" not "I'll use opensrc"
|
||||
|
||||
## References
|
||||
|
||||
- [Tool Routing Decision Trees](references/tool-routing.md)
|
||||
- [opensrc API Reference](references/opensrc-api.md)
|
||||
- [opensrc Code Examples](references/opensrc-examples.md)
|
||||
- [GitHub Linking Patterns](references/linking.md)
|
||||
- [Mermaid Diagram Patterns](references/diagrams.md)
|
||||
@@ -1,51 +0,0 @@
|
||||
# Mermaid Diagram Patterns
|
||||
|
||||
Create diagrams for:
|
||||
- Architecture (component relationships)
|
||||
- Data flow (request → response)
|
||||
- Dependencies (import graph)
|
||||
- Sequences (step-by-step processes)
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Client] --> B[API Gateway]
|
||||
B --> C[Auth Service]
|
||||
B --> D[Data Service]
|
||||
D --> E[(Database)]
|
||||
```
|
||||
|
||||
## Flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Input --> Parse --> Validate --> Transform --> Output
|
||||
```
|
||||
|
||||
## Sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
Client->>+Server: Request
|
||||
Server->>+DB: Query
|
||||
DB-->>-Server: Result
|
||||
Server-->>-Client: Response
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
||||
| Type | Use For |
|
||||
|------|---------|
|
||||
| `graph TD` | Component hierarchy, dependencies |
|
||||
| `flowchart LR` | Data transformation, pipelines |
|
||||
| `sequenceDiagram` | Request/response, multi-party interaction |
|
||||
| `classDiagram` | Type relationships, inheritance |
|
||||
| `stateDiagram` | State machines, lifecycle |
|
||||
|
||||
## Tips
|
||||
|
||||
- Keep nodes short (3-4 words max)
|
||||
- Use subgraphs for grouping related components
|
||||
- Arrow labels for relationship types
|
||||
- Prefer LR (left-right) for flows, TD (top-down) for hierarchies
|
||||
@@ -1,61 +0,0 @@
|
||||
# GitHub Linking Patterns
|
||||
|
||||
All file/dir/code refs → fluent markdown links. Never raw URLs.
|
||||
|
||||
## URL Formats
|
||||
|
||||
### File
|
||||
```
|
||||
https://github.com/{owner}/{repo}/blob/{ref}/{path}
|
||||
```
|
||||
|
||||
### File + Lines
|
||||
```
|
||||
https://github.com/{owner}/{repo}/blob/{ref}/{path}#L{start}-L{end}
|
||||
```
|
||||
|
||||
### Directory
|
||||
```
|
||||
https://github.com/{owner}/{repo}/tree/{ref}/{path}
|
||||
```
|
||||
|
||||
### GitLab (note `/-/blob/`)
|
||||
```
|
||||
https://gitlab.com/{owner}/{repo}/-/blob/{ref}/{path}
|
||||
```
|
||||
|
||||
## Ref Resolution
|
||||
|
||||
| Source | Use as ref |
|
||||
|--------|------------|
|
||||
| Known version | `v{version}` |
|
||||
| Default branch | `main` or `master` |
|
||||
| opensrc fetch | ref from result |
|
||||
| Specific commit | full SHA |
|
||||
|
||||
## Examples
|
||||
|
||||
### Correct
|
||||
```markdown
|
||||
The [`parseAsync`](https://github.com/colinhacks/zod/blob/main/src/types.ts#L450-L480) method handles...
|
||||
```
|
||||
|
||||
### Wrong
|
||||
```markdown
|
||||
See https://github.com/colinhacks/zod/blob/main/src/types.ts#L100
|
||||
The parseAsync method in src/types.ts handles...
|
||||
```
|
||||
|
||||
## Line Numbers
|
||||
|
||||
- Single: `#L42`
|
||||
- Range: `#L42-L50`
|
||||
- Prefer ranges for context (2-5 lines around key code)
|
||||
|
||||
## Registry → GitHub
|
||||
|
||||
| Registry | Find repo in |
|
||||
|----------|--------------|
|
||||
| npm | `package.json` → `repository` |
|
||||
| PyPI | `pyproject.toml` or setup.py |
|
||||
| crates | `Cargo.toml` |
|
||||
@@ -1,235 +0,0 @@
|
||||
# opensrc API Reference
|
||||
|
||||
## Tool
|
||||
|
||||
Use the **opensrc MCP server** via single tool:
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `opensrc_execute` | All operations (fetch, read, grep, files, remove, etc.) |
|
||||
|
||||
Takes a `code` parameter: JavaScript async arrow function executed server-side. Source trees stay on server, only results return.
|
||||
|
||||
## API Surface
|
||||
|
||||
### Read Operations
|
||||
|
||||
```typescript
|
||||
// List all fetched sources
|
||||
opensrc.list(): Source[]
|
||||
|
||||
// Check if source exists
|
||||
opensrc.has(name: string, version?: string): boolean
|
||||
|
||||
// Get source metadata
|
||||
opensrc.get(name: string): Source | undefined
|
||||
|
||||
// List files with optional glob
|
||||
opensrc.files(sourceName: string, glob?: string): Promise<FileEntry[]>
|
||||
|
||||
// Get directory tree structure (default depth: 3)
|
||||
opensrc.tree(sourceName: string, options?: { depth?: number }): Promise<TreeNode>
|
||||
|
||||
// Regex search file contents
|
||||
opensrc.grep(pattern: string, options?: GrepOptions): Promise<GrepResult[]>
|
||||
|
||||
// AST-based semantic code search
|
||||
opensrc.astGrep(sourceName: string, pattern: string, options?: AstGrepOptions): Promise<AstGrepMatch[]>
|
||||
|
||||
// Read single file
|
||||
opensrc.read(sourceName: string, filePath: string): Promise<string>
|
||||
|
||||
// Batch read multiple files (supports globs!)
|
||||
opensrc.readMany(sourceName: string, paths: string[]): Promise<Record<string, string>>
|
||||
|
||||
// Parse fetch spec
|
||||
opensrc.resolve(spec: string): Promise<ParsedSpec>
|
||||
```
|
||||
|
||||
### Mutation Operations
|
||||
|
||||
```typescript
|
||||
// Fetch packages/repos
|
||||
opensrc.fetch(specs: string | string[], options?: { modify?: boolean }): Promise<FetchedSource[]>
|
||||
|
||||
// Remove sources
|
||||
opensrc.remove(names: string[]): Promise<RemoveResult>
|
||||
|
||||
// Clean by type
|
||||
opensrc.clean(options?: CleanOptions): Promise<RemoveResult>
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
### Source
|
||||
|
||||
```typescript
|
||||
interface Source {
|
||||
type: "npm" | "pypi" | "crates" | "repo";
|
||||
name: string; // Use this for all subsequent calls
|
||||
version?: string;
|
||||
ref?: string;
|
||||
path: string;
|
||||
fetchedAt: string;
|
||||
repository: string;
|
||||
}
|
||||
```
|
||||
|
||||
### FetchedSource
|
||||
|
||||
```typescript
|
||||
interface FetchedSource {
|
||||
source: Source; // IMPORTANT: use source.name for subsequent calls
|
||||
alreadyExists: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### GrepOptions
|
||||
|
||||
```typescript
|
||||
interface GrepOptions {
|
||||
sources?: string[]; // Filter to specific sources
|
||||
include?: string; // File glob pattern (e.g., "*.ts")
|
||||
maxResults?: number; // Limit results (default: 100)
|
||||
}
|
||||
```
|
||||
|
||||
### GrepResult
|
||||
|
||||
```typescript
|
||||
interface GrepResult {
|
||||
source: string;
|
||||
file: string;
|
||||
line: number;
|
||||
content: string;
|
||||
}
|
||||
```
|
||||
|
||||
### AstGrepOptions
|
||||
|
||||
```typescript
|
||||
interface AstGrepOptions {
|
||||
glob?: string; // File glob pattern (e.g., "**/*.ts")
|
||||
lang?: string | string[]; // Language(s): "js", "ts", "tsx", "html", "css"
|
||||
limit?: number; // Max results (default: 1000)
|
||||
}
|
||||
```
|
||||
|
||||
### AstGrepMatch
|
||||
|
||||
```typescript
|
||||
interface AstGrepMatch {
|
||||
file: string;
|
||||
line: number;
|
||||
column: number;
|
||||
endLine: number;
|
||||
endColumn: number;
|
||||
text: string; // Matched code text
|
||||
metavars: Record<string, string>; // Captured $VAR → text
|
||||
}
|
||||
```
|
||||
|
||||
#### AST Pattern Syntax
|
||||
|
||||
| Pattern | Matches |
|
||||
|---------|---------|
|
||||
| `$NAME` | Single node, captures to metavars |
|
||||
| `$$$ARGS` | Zero or more nodes (variadic), captures |
|
||||
| `$_` | Single node, no capture |
|
||||
| `$$$` | Zero or more nodes, no capture |
|
||||
|
||||
### FileEntry
|
||||
|
||||
```typescript
|
||||
interface FileEntry {
|
||||
path: string;
|
||||
size: number;
|
||||
isDirectory: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### TreeNode
|
||||
|
||||
```typescript
|
||||
interface TreeNode {
|
||||
name: string;
|
||||
type: "file" | "dir";
|
||||
children?: TreeNode[]; // only for dirs
|
||||
}
|
||||
```
|
||||
|
||||
### CleanOptions
|
||||
|
||||
```typescript
|
||||
interface CleanOptions {
|
||||
packages?: boolean;
|
||||
repos?: boolean;
|
||||
npm?: boolean;
|
||||
pypi?: boolean;
|
||||
crates?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### RemoveResult
|
||||
|
||||
```typescript
|
||||
interface RemoveResult {
|
||||
success: boolean;
|
||||
removed: string[];
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Operations throw on errors. Wrap in try/catch if needed:
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
try {
|
||||
const content = await opensrc.read("zod", "missing.ts");
|
||||
return content;
|
||||
} catch (e) {
|
||||
return { error: e.message };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`readMany` returns errors as string values prefixed with `[Error:`:
|
||||
|
||||
```javascript
|
||||
const files = await opensrc.readMany("zod", ["exists.ts", "missing.ts"]);
|
||||
// { "exists.ts": "content...", "missing.ts": "[Error: ENOENT...]" }
|
||||
|
||||
// Filter successful reads
|
||||
const successful = Object.entries(files)
|
||||
.filter(([_, content]) => !content.startsWith("[Error:"));
|
||||
```
|
||||
|
||||
## Package Spec Formats
|
||||
|
||||
| Format | Example | Source Name After Fetch |
|
||||
|--------|---------|------------------------|
|
||||
| `<name>` | `"zod"` | `"zod"` |
|
||||
| `<name>@<version>` | `"zod@3.22.0"` | `"zod"` |
|
||||
| `pypi:<name>` | `"pypi:requests"` | `"requests"` |
|
||||
| `crates:<name>` | `"crates:serde"` | `"serde"` |
|
||||
| `owner/repo` | `"vercel/ai"` | `"github.com/vercel/ai"` |
|
||||
| `owner/repo@ref` | `"vercel/ai@v1.0.0"` | `"github.com/vercel/ai"` |
|
||||
| `gitlab:owner/repo` | `"gitlab:org/repo"` | `"gitlab.com/org/repo"` |
|
||||
|
||||
## Critical Pattern
|
||||
|
||||
**Always capture `source.name` from fetch results:**
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const [{ source }] = await opensrc.fetch("vercel/ai");
|
||||
|
||||
// GitHub repos: "vercel/ai" → "github.com/vercel/ai"
|
||||
const sourceName = source.name;
|
||||
|
||||
// Use sourceName for ALL subsequent calls
|
||||
const files = await opensrc.files(sourceName, "src/**/*.ts");
|
||||
return files;
|
||||
}
|
||||
```
|
||||
@@ -1,336 +0,0 @@
|
||||
# opensrc Code Examples
|
||||
|
||||
## Workflow: Fetch → Explore
|
||||
|
||||
### Basic Fetch and Explore with tree()
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const [{ source }] = await opensrc.fetch("vercel/ai");
|
||||
// Get directory structure first
|
||||
const tree = await opensrc.tree(source.name, { depth: 2 });
|
||||
return tree;
|
||||
}
|
||||
```
|
||||
|
||||
### Fetch and Read Key Files
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const [{ source }] = await opensrc.fetch("vercel/ai");
|
||||
const sourceName = source.name; // "github.com/vercel/ai"
|
||||
|
||||
const files = await opensrc.readMany(sourceName, [
|
||||
"package.json",
|
||||
"README.md",
|
||||
"src/index.ts"
|
||||
]);
|
||||
|
||||
return { sourceName, files };
|
||||
}
|
||||
```
|
||||
|
||||
### readMany with Globs
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const [{ source }] = await opensrc.fetch("zod");
|
||||
// Read all package.json files in monorepo
|
||||
const files = await opensrc.readMany(source.name, [
|
||||
"packages/*/package.json" // globs supported!
|
||||
]);
|
||||
return Object.keys(files);
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Fetch Multiple Packages
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const results = await opensrc.fetch(["zod", "valibot", "yup"]);
|
||||
const names = results.map(r => r.source.name);
|
||||
|
||||
// Compare how each handles string validation
|
||||
const comparisons = {};
|
||||
for (const name of names) {
|
||||
const matches = await opensrc.grep("string.*validate|validateString", {
|
||||
sources: [name],
|
||||
include: "*.ts",
|
||||
maxResults: 10
|
||||
});
|
||||
comparisons[name] = matches.map(m => `${m.file}:${m.line}`);
|
||||
}
|
||||
return comparisons;
|
||||
}
|
||||
```
|
||||
|
||||
## Search Patterns
|
||||
|
||||
### Grep → Read Context
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const matches = await opensrc.grep("export function parse\\(", {
|
||||
sources: ["zod"],
|
||||
include: "*.ts"
|
||||
});
|
||||
|
||||
if (matches.length === 0) return "No matches";
|
||||
|
||||
const match = matches[0];
|
||||
const content = await opensrc.read(match.source, match.file);
|
||||
const lines = content.split("\n");
|
||||
|
||||
// Return 40 lines starting from match
|
||||
return {
|
||||
file: match.file,
|
||||
code: lines.slice(match.line - 1, match.line + 39).join("\n")
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Search Across All Fetched Sources
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const sources = opensrc.list();
|
||||
const results = {};
|
||||
|
||||
for (const source of sources) {
|
||||
const errorHandling = await opensrc.grep("throw new|catch \\(|\\.catch\\(", {
|
||||
sources: [source.name],
|
||||
include: "*.ts",
|
||||
maxResults: 20
|
||||
});
|
||||
results[source.name] = {
|
||||
type: source.type,
|
||||
errorPatterns: errorHandling.length
|
||||
};
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
```
|
||||
|
||||
## AST-Based Search
|
||||
|
||||
Use `astGrep` for semantic code search with pattern matching.
|
||||
|
||||
### Find Function Declarations
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const [{ source }] = await opensrc.fetch("lodash");
|
||||
|
||||
const fns = await opensrc.astGrep(source.name, "function $NAME($$$ARGS) { $$$BODY }", {
|
||||
lang: "js",
|
||||
limit: 20
|
||||
});
|
||||
|
||||
return fns.map(m => ({
|
||||
file: m.file,
|
||||
line: m.line,
|
||||
name: m.metavars.NAME
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### Find React Hooks Usage
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const [{ source }] = await opensrc.fetch("vercel/ai");
|
||||
|
||||
const stateHooks = await opensrc.astGrep(
|
||||
source.name,
|
||||
"const [$STATE, $SETTER] = useState($$$INIT)",
|
||||
{ lang: ["ts", "tsx"], limit: 50 }
|
||||
);
|
||||
|
||||
return stateHooks.map(m => ({
|
||||
file: m.file,
|
||||
state: m.metavars.STATE,
|
||||
setter: m.metavars.SETTER
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### Find Class Definitions with Context
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const [{ source }] = await opensrc.fetch("zod");
|
||||
|
||||
const classes = await opensrc.astGrep(source.name, "class $NAME", {
|
||||
glob: "**/*.ts"
|
||||
});
|
||||
|
||||
const details = [];
|
||||
for (const cls of classes.slice(0, 5)) {
|
||||
const content = await opensrc.read(source.name, cls.file);
|
||||
const lines = content.split("\n");
|
||||
details.push({
|
||||
name: cls.metavars.NAME,
|
||||
file: cls.file,
|
||||
preview: lines.slice(cls.line - 1, cls.line + 9).join("\n")
|
||||
});
|
||||
}
|
||||
return details;
|
||||
}
|
||||
```
|
||||
|
||||
### Compare Export Patterns Across Libraries
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const results = await opensrc.fetch(["zod", "valibot"]);
|
||||
const names = results.map(r => r.source.name);
|
||||
|
||||
const exports = {};
|
||||
for (const name of names) {
|
||||
const matches = await opensrc.astGrep(name, "export const $NAME = $_", {
|
||||
lang: "ts",
|
||||
limit: 30
|
||||
});
|
||||
exports[name] = matches.map(m => m.metavars.NAME);
|
||||
}
|
||||
return exports;
|
||||
}
|
||||
```
|
||||
|
||||
### grep vs astGrep
|
||||
|
||||
| Use Case | Tool |
|
||||
|----------|------|
|
||||
| Text/regex pattern | `grep` |
|
||||
| Function declarations | `astGrep`: `function $NAME($$$) { $$$ }` |
|
||||
| Arrow functions | `astGrep`: `const $N = ($$$) => $_` |
|
||||
| Class definitions | `astGrep`: `class $NAME extends $PARENT` |
|
||||
| Import statements | `astGrep`: `import { $$$IMPORTS } from "$MOD"` |
|
||||
| JSX components | `astGrep`: `<$COMP $$$PROPS />` |
|
||||
|
||||
## Repository Exploration
|
||||
|
||||
### Find Entry Points
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const name = "github.com/vercel/ai";
|
||||
|
||||
const allFiles = await opensrc.files(name, "**/*.{ts,js}");
|
||||
const entryPoints = allFiles.filter(f =>
|
||||
f.path.match(/^(src\/)?(index|main|mod)\.(ts|js)$/) ||
|
||||
f.path.includes("/index.ts")
|
||||
);
|
||||
|
||||
// Read all entry points
|
||||
const contents = {};
|
||||
for (const ep of entryPoints.slice(0, 5)) {
|
||||
contents[ep.path] = await opensrc.read(name, ep.path);
|
||||
}
|
||||
|
||||
return {
|
||||
totalFiles: allFiles.length,
|
||||
entryPoints: entryPoints.map(f => f.path),
|
||||
contents
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Explore Package Structure
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const name = "zod";
|
||||
|
||||
// Get all TypeScript files
|
||||
const tsFiles = await opensrc.files(name, "**/*.ts");
|
||||
|
||||
// Group by directory
|
||||
const byDir = {};
|
||||
for (const f of tsFiles) {
|
||||
const dir = f.path.split("/").slice(0, -1).join("/") || ".";
|
||||
byDir[dir] = (byDir[dir] || 0) + 1;
|
||||
}
|
||||
|
||||
// Read key files
|
||||
const pkg = await opensrc.read(name, "package.json");
|
||||
const readme = await opensrc.read(name, "README.md");
|
||||
|
||||
return {
|
||||
structure: byDir,
|
||||
package: JSON.parse(pkg),
|
||||
readmePreview: readme.slice(0, 500)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Batch Operations
|
||||
|
||||
### Read Many with Error Handling
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const files = await opensrc.readMany("zod", [
|
||||
"src/index.ts",
|
||||
"src/types.ts",
|
||||
"src/ZodError.ts",
|
||||
"src/helpers/parseUtil.ts"
|
||||
]);
|
||||
|
||||
// files is Record<string, string> - errors start with "[Error:"
|
||||
const successful = Object.entries(files)
|
||||
.filter(([_, content]) => !content.startsWith("[Error:"))
|
||||
.map(([path, content]) => ({ path, lines: content.split("\n").length }));
|
||||
|
||||
return successful;
|
||||
}
|
||||
```
|
||||
|
||||
### Parallel Grep Across Multiple Sources
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const targets = ["zod", "valibot"];
|
||||
const pattern = "export (type|interface)";
|
||||
|
||||
const results = await Promise.all(
|
||||
targets.map(async (name) => {
|
||||
const matches = await opensrc.grep(pattern, {
|
||||
sources: [name],
|
||||
include: "*.ts",
|
||||
maxResults: 50
|
||||
});
|
||||
return { name, count: matches.length, matches };
|
||||
})
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow Checklist
|
||||
|
||||
### Comprehensive Repository Analysis
|
||||
|
||||
```
|
||||
Repository Analysis Progress:
|
||||
- [ ] 1. Fetch repository
|
||||
- [ ] 2. Read package.json + README
|
||||
- [ ] 3. Identify entry points (src/index.*)
|
||||
- [ ] 4. Read main entry file
|
||||
- [ ] 5. Map exports and public API
|
||||
- [ ] 6. Trace key functionality
|
||||
- [ ] 7. Create architecture diagram
|
||||
```
|
||||
|
||||
### Library Comparison
|
||||
|
||||
```
|
||||
Comparison Progress:
|
||||
- [ ] 1. Fetch all libraries
|
||||
- [ ] 2. Grep for target pattern in each
|
||||
- [ ] 3. Read matching implementations
|
||||
- [ ] 4. Create comparison table
|
||||
- [ ] 5. Synthesize findings
|
||||
```
|
||||
@@ -1,109 +0,0 @@
|
||||
# Tool Routing
|
||||
|
||||
## Decision Flowchart
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Q[User Query] --> T{Query Type?}
|
||||
T -->|Understand/Explain| U[UNDERSTAND]
|
||||
T -->|Find/Search| F[FIND]
|
||||
T -->|Explore/Architecture| E[EXPLORE]
|
||||
T -->|Compare| C[COMPARE]
|
||||
|
||||
U --> U1{Known library?}
|
||||
U1 -->|Yes| U2[context7.resolve-library-id]
|
||||
U2 --> U3[context7.query-docs]
|
||||
U3 --> U4{Need source?}
|
||||
U4 -->|Yes| U5[opensrc.fetch → read]
|
||||
U1 -->|No| U6[grep_app → opensrc.fetch]
|
||||
|
||||
F --> F1{Specific repo?}
|
||||
F1 -->|Yes| F2[opensrc.fetch → grep → read]
|
||||
F1 -->|No| F3[grep_app broad search]
|
||||
F3 --> F4[opensrc.fetch interesting repos]
|
||||
|
||||
E --> E1[opensrc.fetch]
|
||||
E1 --> E2[opensrc.files]
|
||||
E2 --> E3[Read entry points]
|
||||
E3 --> E4[Create diagram]
|
||||
|
||||
C --> C1["opensrc.fetch([X, Y])"]
|
||||
C1 --> C2[grep same pattern]
|
||||
C2 --> C3[Read comparable files]
|
||||
C3 --> C4[Synthesize comparison]
|
||||
```
|
||||
|
||||
## Query Type Detection
|
||||
|
||||
| Keywords | Query Type | Start With |
|
||||
|----------|------------|------------|
|
||||
| "how does", "why does", "explain", "purpose of" | UNDERSTAND | context7 |
|
||||
| "find", "where is", "implementations of", "examples of" | FIND | grep_app |
|
||||
| "explore", "walk through", "architecture", "structure" | EXPLORE | opensrc |
|
||||
| "compare", "vs", "difference between" | COMPARE | opensrc |
|
||||
|
||||
## UNDERSTAND Queries
|
||||
|
||||
```
|
||||
Known library? → context7.resolve-library-id → context7.query-docs
|
||||
└─ Need source? → opensrc.fetch → read
|
||||
|
||||
Unknown? → grep_app search → opensrc.fetch top result → read
|
||||
```
|
||||
|
||||
**When to transition context7 → opensrc:**
|
||||
- Need implementation details (not just API docs)
|
||||
- Question about internals/private methods
|
||||
- Tracing code flow through library
|
||||
|
||||
## FIND Queries
|
||||
|
||||
```
|
||||
Specific repo? → opensrc.fetch → opensrc.grep → read matches
|
||||
|
||||
Broad search? → grep_app → analyze → opensrc.fetch interesting repos
|
||||
```
|
||||
|
||||
**grep_app query tips:**
|
||||
- Use literal code patterns: `useState(` not "react hooks"
|
||||
- Filter by language: `language: ["TypeScript"]`
|
||||
- Narrow by repo: `repo: "vercel/"` for org
|
||||
|
||||
## EXPLORE Queries
|
||||
|
||||
```
|
||||
1. opensrc.fetch(target)
|
||||
2. opensrc.files → understand structure
|
||||
3. Identify entry points: README, package.json, src/index.*
|
||||
4. Read entry → internals
|
||||
5. Create architecture diagram
|
||||
```
|
||||
|
||||
## COMPARE Queries
|
||||
|
||||
```
|
||||
1. opensrc.fetch([X, Y])
|
||||
2. Extract source.name from each result
|
||||
3. opensrc.grep same pattern in both
|
||||
4. Read comparable files
|
||||
5. Synthesize → comparison table
|
||||
```
|
||||
|
||||
## Tool Capabilities
|
||||
|
||||
| Tool | Best For | Not For |
|
||||
|------|----------|---------|
|
||||
| **grep_app** | Broad search, unknown scope, finding repos | Semantic queries |
|
||||
| **context7** | Library APIs, best practices, common patterns | Library internals |
|
||||
| **opensrc** | Deep exploration, reading internals, tracing flow | Initial discovery |
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
| Don't | Do |
|
||||
|-------|-----|
|
||||
| grep_app for known library docs | context7 first |
|
||||
| opensrc.fetch before knowing target | grep_app to discover |
|
||||
| Multiple small reads | opensrc.readMany batch |
|
||||
| Describe without linking | Link every file ref |
|
||||
| Text for complex relationships | Mermaid diagram |
|
||||
| Use tool names in responses | "I'll search..." not "I'll use opensrc" |
|
||||
@@ -1,122 +0,0 @@
|
||||
---
|
||||
name: session-export
|
||||
description: Update GitHub PR descriptions with AI session export summaries. Use when user asks to add session summary to PR/MR, document AI assistance in PR/MR, or export conversation summary to PR/MR description.
|
||||
---
|
||||
|
||||
# Session Export
|
||||
|
||||
Update PR/MR descriptions with a structured summary of the AI-assisted conversation.
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
> [!NOTE]
|
||||
> This PR was written with AI assistance.
|
||||
|
||||
<details><summary>AI Session Export</summary>
|
||||
<p>
|
||||
|
||||
```json
|
||||
{
|
||||
"info": {
|
||||
"title": "<brief task description>",
|
||||
"agent": "opencode",
|
||||
"models": ["<model(s) used>"]
|
||||
},
|
||||
"summary": [
|
||||
"<action 1>",
|
||||
"<action 2>",
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</p>
|
||||
</details>
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Export Session Data
|
||||
|
||||
Get session data using OpenCode CLI:
|
||||
|
||||
```bash
|
||||
opencode export [sessionID]
|
||||
```
|
||||
|
||||
Returns JSON with session info including models used. Use current session if no sessionID provided.
|
||||
|
||||
### 2. Generate Summary JSON
|
||||
|
||||
From exported data and conversation context, create summary:
|
||||
|
||||
- **title**: 2-5 word task description (lowercase)
|
||||
- **agent**: always "opencode"
|
||||
- **models**: array from export data
|
||||
- **summary**: array of terse action statements
|
||||
- Use past tense ("added", "fixed", "created")
|
||||
- Start with "user requested..." or "user asked..."
|
||||
- Chronological order
|
||||
- Attempt to keep the summary to a max of 25 turns ("user requested", "agent did")
|
||||
- **NEVER include sensitive data**: API keys, credentials, secrets, tokens, passwords, env vars
|
||||
|
||||
### 3. Update PR/MR Description
|
||||
|
||||
**GitHub:**
|
||||
```bash
|
||||
gh pr edit <PR_NUMBER> --body "$(cat <<'EOF'
|
||||
<existing description>
|
||||
|
||||
> [!NOTE]
|
||||
> This PR was written with AI assistance.
|
||||
|
||||
<details><summary>AI Session Export</summary>
|
||||
...
|
||||
</details>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
### 4. Preserve Existing Content
|
||||
|
||||
Always fetch and preserve existing PR/MR description:
|
||||
|
||||
```bash
|
||||
# GitHub
|
||||
gh pr view <PR_NUMBER> --json body -q '.body'
|
||||
|
||||
Append session export after existing content with blank line separator.
|
||||
|
||||
## Example Summary
|
||||
|
||||
For a session where user asked to add dark mode:
|
||||
|
||||
```json
|
||||
{
|
||||
"info": {
|
||||
"title": "dark mode implementation",
|
||||
"agent": "opencode",
|
||||
"models": ["claude sonnet 4"]
|
||||
},
|
||||
"summary": [
|
||||
"user requested dark mode toggle in settings",
|
||||
"agent explored existing theme system",
|
||||
"agent created ThemeContext for state management",
|
||||
"agent added DarkModeToggle component",
|
||||
"agent updated CSS variables for dark theme",
|
||||
"agent ran tests and fixed 2 failures",
|
||||
"agent committed changes"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
**NEVER include in summary:**
|
||||
- API keys, tokens, secrets
|
||||
- Passwords, credentials
|
||||
- Environment variable values
|
||||
- Private URLs with auth tokens
|
||||
- Personal identifiable information
|
||||
- Internal hostnames/IPs
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/tui.json",
|
||||
"plugin": ["./plugin/review.ts"]
|
||||
}
|
||||
@@ -24,26 +24,106 @@ in {
|
||||
programs.opencode = {
|
||||
enable = true;
|
||||
package = inputs'.llm-agents.packages.opencode;
|
||||
settings = {
|
||||
model = "anthropic/claude-opus-4-6";
|
||||
small_model = "anthropic/claude-haiku-4-5";
|
||||
tui = {
|
||||
theme = "rosepine";
|
||||
plugin = ["opencode-claude-auth"];
|
||||
plugin = ["./plugin/review.ts"];
|
||||
};
|
||||
settings = {
|
||||
model = "openai/gpt-5.4";
|
||||
small_model = "openai/gpt-5.1-codex-mini";
|
||||
plugin = [
|
||||
"opencode-claude-auth"
|
||||
"opencode-supermemory"
|
||||
];
|
||||
permission = {
|
||||
external_directory = {
|
||||
"*" = "allow";
|
||||
"**/.gnupg/**" = "deny";
|
||||
"**/.ssh/**" = "deny";
|
||||
"~/.config/gh/hosts.yml" = "deny";
|
||||
"~/.config/sops/age/keys.txt" = "deny";
|
||||
"~/.local/share/opencode/mcp-auth.json" = "deny";
|
||||
"/etc/ssh/ssh_host_*" = "deny";
|
||||
"/run/secrets/*" = "deny";
|
||||
};
|
||||
bash = {
|
||||
"*" = "allow";
|
||||
env = "deny";
|
||||
"env *" = "deny";
|
||||
printenv = "deny";
|
||||
"printenv *" = "deny";
|
||||
"export *" = "deny";
|
||||
"gh auth *" = "deny";
|
||||
ssh = "ask";
|
||||
"ssh *" = "ask";
|
||||
mosh = "ask";
|
||||
"mosh *" = "ask";
|
||||
"cat *.env" = "deny";
|
||||
"cat *.env.*" = "deny";
|
||||
"cat **/.env" = "deny";
|
||||
"cat **/.env.*" = "deny";
|
||||
"cat *.envrc" = "deny";
|
||||
"cat **/.envrc" = "deny";
|
||||
"cat .dev.vars" = "deny";
|
||||
"cat **/.dev.vars" = "deny";
|
||||
"cat *.pem" = "deny";
|
||||
"cat *.key" = "deny";
|
||||
"cat **/.gnupg/**" = "deny";
|
||||
"cat **/.ssh/**" = "deny";
|
||||
"cat ~/.config/gh/hosts.yml" = "deny";
|
||||
"cat ~/.config/sops/age/keys.txt" = "deny";
|
||||
"cat ~/.local/share/opencode/mcp-auth.json" = "deny";
|
||||
"cat /etc/ssh/ssh_host_*" = "deny";
|
||||
"cat /run/secrets/*" = "deny";
|
||||
};
|
||||
edit = {
|
||||
"*" = "allow";
|
||||
"**/.gnupg/**" = "deny";
|
||||
"**/.ssh/**" = "deny";
|
||||
"**/secrets/**" = "deny";
|
||||
"secrets/*" = "deny";
|
||||
"~/.config/gh/hosts.yml" = "deny";
|
||||
"~/.config/sops/age/keys.txt" = "deny";
|
||||
"~/.local/share/opencode/mcp-auth.json" = "deny";
|
||||
"/etc/ssh/ssh_host_*" = "deny";
|
||||
"/run/secrets/*" = "deny";
|
||||
};
|
||||
glob = "allow";
|
||||
grep = "allow";
|
||||
list = "allow";
|
||||
lsp = "allow";
|
||||
question = "allow";
|
||||
read = {
|
||||
"*" = "allow";
|
||||
"*.env" = "deny";
|
||||
"*.env.*" = "deny";
|
||||
"*.envrc" = "deny";
|
||||
"**/.env" = "deny";
|
||||
"**/.env.*" = "deny";
|
||||
"**/.envrc" = "deny";
|
||||
".dev.vars" = "deny";
|
||||
"**/.dev.vars" = "deny";
|
||||
"**/.gnupg/**" = "deny";
|
||||
"**/.ssh/**" = "deny";
|
||||
"*.key" = "deny";
|
||||
"*.pem" = "deny";
|
||||
"**/secrets/**" = "deny";
|
||||
"secrets/*" = "deny";
|
||||
"~/.config/gh/hosts.yml" = "deny";
|
||||
"~/.config/sops/age/keys.txt" = "deny";
|
||||
"~/.local/share/opencode/mcp-auth.json" = "deny";
|
||||
"/etc/ssh/ssh_host_*" = "deny";
|
||||
"/run/secrets/*" = "deny";
|
||||
};
|
||||
skill = "allow";
|
||||
task = "allow";
|
||||
webfetch = "allow";
|
||||
websearch = "allow";
|
||||
codesearch = "allow";
|
||||
};
|
||||
agent = {
|
||||
plan = {
|
||||
model = "anthropic/claude-opus-4-6";
|
||||
};
|
||||
explore = {
|
||||
model = "anthropic/claude-haiku-4-5";
|
||||
model = "openai/gpt-5.1-codex-mini";
|
||||
};
|
||||
};
|
||||
instructions = [
|
||||
@@ -61,7 +141,7 @@ in {
|
||||
opensrc = {
|
||||
enabled = true;
|
||||
type = "local";
|
||||
command = ["opensrc-mcp"];
|
||||
command = ["node" "/home/cschmatzler/.bun/bin/opensrc-mcp"];
|
||||
};
|
||||
context7 = {
|
||||
enabled = true;
|
||||
@@ -78,10 +158,10 @@ in {
|
||||
};
|
||||
|
||||
xdg.configFile = {
|
||||
"opencode/agent" = {
|
||||
source = ./_opencode/agent;
|
||||
recursive = true;
|
||||
};
|
||||
# "opencode/agent" = {
|
||||
# source = ./_opencode/agent;
|
||||
# recursive = true;
|
||||
# };
|
||||
"opencode/command" = {
|
||||
source = ./_opencode/command;
|
||||
recursive = true;
|
||||
@@ -90,16 +170,11 @@ in {
|
||||
source = ./_opencode/skill;
|
||||
recursive = true;
|
||||
};
|
||||
"opencode/tool" = {
|
||||
source = ./_opencode/tool;
|
||||
recursive = true;
|
||||
};
|
||||
"opencode/plugin" = {
|
||||
source = ./_opencode/plugin;
|
||||
recursive = true;
|
||||
};
|
||||
"opencode/AGENTS.md".source = ./_opencode/AGENTS.md;
|
||||
"opencode/tui.json".source = ./_opencode/tui.json;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -157,6 +157,8 @@ in {
|
||||
"ghostty@tip"
|
||||
"raycast"
|
||||
"spotify"
|
||||
"tailscale"
|
||||
"whatsapp"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
local = import ./_lib/local.nix;
|
||||
in {
|
||||
den.aspects.email.homeManager = {pkgs, ...}: {
|
||||
programs.aerc = {
|
||||
enable = true;
|
||||
extraConfig.general.unsafe-accounts-conf = true;
|
||||
};
|
||||
|
||||
programs.himalaya = {
|
||||
enable = true;
|
||||
package =
|
||||
@@ -50,6 +55,7 @@ in {
|
||||
port = 993;
|
||||
tls.enable = true;
|
||||
};
|
||||
aerc.enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,23 +4,23 @@
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1xate984yhl9qk9d4q99pyxmzz48sq56nfhu8weyzkgum4ed5tc5shjmrs7",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKWXVhTEw0em1CZVB3d2U3\nSWRjMlZpTlFYSXZJZVE5SWttSFV2K3l3VFJjCjZZbG05a1RsUlRORDNaRXduVHBC\ndHZ2aG1FRnJhbFQxNkNxTEwzV0NRdHMKLS0tIFplbXhERjFhRGh0YUtuVG5IZWVh\ndEV6SkhTWnUrVkFmR0NYRS8xZ05zRTQK8qcapJ2Z2AukAUSFagChNGt7BTnIXchr\nociqhWE+BVPROduihwpsQlAWxEARJP/5sqhOAZpNrnna9Psg4m1MFA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0cmFCRDVFcFFORVpsK2Fo\nYWg5S3VzSGlCYVp1S1Q3NGF4SDZqMVFmTFc0CkZQUDJwQ3AvUDNpMUZNaUlRTkNt\nQVR2UzI0Snp3SVZqN2phRzE1K0ErZ3cKLS0tIDE4dndRWXVycitxSVNEN28vN29s\nWnRIT24yT3Z6R1ZiYURYd1BiSzBIOU0Kn7jrvq06CxRxn7XbOIMoEIjMDIr6A089\ns1ymMJO8DmeKdNaL5s3EwG7BZBAimdmcjANST1kXFy7oc4eSk4ETwg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1njjegjjdqzfnrr54f536yl4lduqgna3wuv7ef6vtl9jw5cju0grsgy62tm",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJTzlPdWcyQlFPSWdhVnVB\nR08yVmdaeFRNYUo2clA4ellicU9rVnlZMVFBCnRjOTZGdHBOMW94OGRyaE9HMktq\nQ1V0WmQycndnSHh4K1JiZllqSG5oRVUKLS0tIGZ6VTNUd2xmR1N1bmtDU1k2Q0ox\nRDAwQytvL3JQUjh0MHBFV09kdVpJdkUKC1CigRs4k/uSXC4zMWL60xaHFoyvzYlu\nP2olBA4CerGEoMc0ZIBEEr78hB0j//06se68BreZcD4FcDC77IWNXQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqZFVRMDVVaEJWUWQ1bkRD\nWmQ1dTRMK2FpMmZLTERuOHc1OHUramFTZkFzCnlSUTUzUktrdFJyVnk0SnpFOURT\nME1wckdyUHFmeE0rN1BneUhIR3JuZHMKLS0tIGJCTHZuWTdGQVhoU3JnSmxIcm83\nc0hwNGUvVEljWWVwOWpCS1BJckhjRUkKDD22XytVN58yR22InpsUEN6y1r2AQqLn\nu1fmWC7R4AJcisHqVSEOBW3qQVQdFWIptBN/5urkb2yAKNtP7WUUbg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age187jl7e4k9n4guygkmpuqzeh0wenefwrfkpvuyhvwjrjwxqpzassqq3x67j",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRcVJ0WFoxN2dQSy8xN2ox\nU09GbUd0UG03Wlc0RnpTNkpYbUo2ZFg1Q1hRCmZyUTZiYklMbnBFczhkSTNFaTNi\nSy9vb1VXOFBqQVMySHRVUng0c083VFEKLS0tIFplV3JVSmV4TmJHSkRPMFMzZkZl\ncmVZWjRCZWxlamxUdnp6ZnA3bXNaV1EK4ZXEJBXHGTaEbNq81RRm+GfElyJekrX+\nxrWHGTkJ4golaI39p3j/F8sF40Sa036ZJmQU8uX9fq1cnuSRv4lb1g==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlc0NibDBuUVJYLzRQY0dP\nWlRzWVo0Rk5NVXh0SGZjYnNza2g3M3E0U0cwCmc5K2d5UzZvR1c2QmJickg3Uk5X\nQm9GMkZnYUVGS1lPRklrcDJRdmMvM2MKLS0tIHBiclZyTVU5emVka05xNXJTTnhF\nN3plRmN2eEl0Y012eGNBbXBsc054Wm8KIB6pNlSM63UvoaPsHNYhSuhZsfM/VQfw\nTxITGnK0Lkjw91SDVzuCGPTtZxuUTFR2q0SyGbZU9IrLQWmBTaENhw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1ez6j3r5wdp0tjy7n5qzv5vfakdc2nh2zeu388zu7a80l0thv052syxq5e2",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5a3gxLzQzQXl6OThJekYr\nVE5zQ0tHd2l4K1NnZEJvSkw0WVptdE9VQ3g0CjcrdkgyUGczZTY5SFdQNFdneTVR\naTdjTUhSSzZvQnJmc2g1NGJhN3Y3VjQKLS0tIDk4ckszblk5QUFkNzVVZ3czYnpk\ncVZlTlJxK3ZLRmxZRmtaZzNkME9UZWcK+GVFoiY5qKX8DdKbGxUoLxF3gnnG3Cbl\nk/57ZH1xVJT4jCCqdploZCSwSLdGrUDGs8I4FixKVMKGT6Ce2xaPcg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"recipient": "age1f9h725ewwwwwkelnrvdvrurg6fcsn3zxrxdt0v6v8ys0nzngcsvqu77nc8",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEUUhvblozMyt0UDJpZ2dx\nNWJBZG5wQzFEM2RFTTJjb3FtcE1ISkd3cEhFCjJOdDJwOTJrdWc2c0NYWGQwWk9y\nREM0Qm1iK3FHUE54ZVI5L2k0RmtESHMKLS0tIEtRdTQvY1AxZHhSZWlhSThFV2FN\nQ0wyUTRaRnh1cDVlVmNjekNuNUN2bUUKOnTcpcaN7cuQXceJmPtKJj2pyIxC3Lj7\npedLV126Q07i3aRrQgm92O8NW2lMaM2Z3FldooTsQgIieQeMCJ6G9w==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1tlymdmaukhwupzrhszspp26lgd8s64rw4vu9lwc7gsgrjm78095s9fe9l3",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxUHJtZ3VuRVZnU0RXeStx\nNUQrakgxWm5oOFlmbXZGL1UxQnhvYU1sUkZNCmlucWQyeFJDSFJZYWlSbkd2eCt6\nS2liQ2g3VXpING81OHRPUGRocnNqa1UKLS0tIG9tdXdna3RhYm8xRHRsS2tQYlUx\najVwbndFMDUyVG0weGtGdWxpQnpsSVkKdTnOnyKDkDfDtDA4jdF1JZ6wXdf/Rucv\nT4YYvjbh7JTnOl6wMg37uy29GVlI0hjOCMyX0j2BmKis/awCJ4bxUA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqUVhZanBIR294ek1keWtZ\nM3puaC8zWGE3UHpmZy9vbzh3YkRyTW5rcG5zCmQ1SnR5NzZLcXNZZVUzTWV1YkFn\ncE5CaHpSeEZnWlNkak1MbWM2UUJJT0kKLS0tIEl1T2lsYndYeHkxalpBYUtwY1Yv\nMGJLWWEwVW5lNnNwNEZhTnRMTVdJdmMK3mg0hzN6KjmR288U09USPmyoQd5ixmCY\nOZb6qjx1O9TUmuho6j+zR4BmMcZ9kIv6uEuURKxGFwzZy+l5a3e3Cg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2026-01-04T19:45:21Z",
|
||||
|
||||
@@ -4,23 +4,23 @@
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1xate984yhl9qk9d4q99pyxmzz48sq56nfhu8weyzkgum4ed5tc5shjmrs7",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2Ly9XQlRDZzgrY2tiTmRi\nNmhBNDQ2TDV4eGZsUUFvTmlHR2tCN29IUGljCnBvb2RKNUdHTnEwQ1RzS05uOTVK\neElUUmdRdnRaYUpIWEljdlN6REgwUm8KLS0tIGpJRVNKTkR3bEcrcks3ek5LNVV3\nYzhOUFRvY1RPYkM5V0hXenFHd2NSdWsKokyuyPXjMAJIfyeWsBHGWaLID7Oorc9d\nQE9Veh2aramAH1xb30BVLa40Dpu0jI/Wgxoo0iDf5OypNXBiBH+FMA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4WmNWMnQ2NEg1MnFvOS9n\neHBnMHpvWllvM2taeFM5SXRVT0wvMTRaMUJFCnk5MVpGRjN1cUxiYkdLSlZ1aTB0\nTkduQkFZcjkxQzUyWHhlQ0tFRnZFTWsKLS0tIDVLdXdySWxqb20zZ3RKSE1qc2Nh\nSC82UmxGN2VUY2pZY3pnWDlKTkRKSEUKFSQqZIzGXpCzpWVWko7J3hxglRJmOuF4\nmE/lC55uekxcochjgPT9uDJfG1/kskWyJTBb5Ndx3YFkUcnfVOZc4Q==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1njjegjjdqzfnrr54f536yl4lduqgna3wuv7ef6vtl9jw5cju0grsgy62tm",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMNUVHeWg1N1ZPKzBqMnBn\nY2VaTlp0K3ppbFdmN0JuQmRnVUlzQ0hMZTI4CkxORVJ0NUIxVXZMcHVRcXFSV01l\naFFVOEtJSFY0MUVWOXYxbFlqRmNuVHMKLS0tIEYydDB1ZHUvQS8yTENmZkZHeXFV\nU1N3ZC9zNEtZSnE2Wk9TREN3Y3JwNEkKyFqj+1kGjqKyslllunUl8dVQwFAe/MdL\n5PNKIfVErxbnPtlc+yfRSWsfOQPdNV9GVM9rtOxA+51QStiGlLU12Q==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5U2plZGxEVy90NUMzellJ\nYVVsTDUvL1o5OXZ4YjR3c0tPY2RjQWpVd0M4Ci9kUFA3OEFoRjlUa1g4Nnl3K2Rq\nNHA1NkRtVzdXM21QNTI5ZFJsNFVCMncKLS0tIFRQbDdXeWxIdzJpNng0QVc3bStT\nQ2VPNFA4NDlmU3hscjJ0MjdGRGQxakUKM9H04Ezw/KoAJVjFB56v0vjIHJiO+aW+\nWWdBlsZoeTYChkMGcQ74SaLkPMEkD3GU8Ua9RZBIqFzOZwsOdAkWBA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age187jl7e4k9n4guygkmpuqzeh0wenefwrfkpvuyhvwjrjwxqpzassqq3x67j",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTREx3SU02RU1adU5Ielkz\nVUFrdUZkQzJwZG1aQlZEcDdxWXVFM1JtRHk0CmNTamFhcjJEMklYbkpxWHFzekhY\nQ1E1STZ3U0RMNkNnVnVvWXJkZUhxRm8KLS0tIE1ZaFBORVRMZkYwNnBkTjFxSWNG\nVWJnYjgzM3IyMDJ3Z3o1QkV0RGdjL0EKQ1G/uJ8HcVDMUk8NXBC0MRcJOyLzQRlT\nL5Sf6Es6UYrTp5tIWleR6u48aUGi8HvcOOeCrK2S5utj5xPl6E+bqQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSSTNhc0xrOXRXOWZVdWNh\nNGQxNFBXUHNrL2NsMnBkcDdSVHVYc1RPM25zClh0L2FrMEhZcWt3RGtHa0ErNjUz\ndDlZaC9kZVVHSkFOK3dBWGwwZStQeUkKLS0tIE1VRnJCbTFWUkhoQ0c0dkppbVRL\nSDZldkdXV2U5Z3ZycmEySTJ5WDFTbVEKgHq5Xa86l+1e87p2KodJN+8/IOz2y0w7\n3xHjoZ8LaP7aQ5PWBTHUk6KeOjJQnTMzimQC+YOs4XkmeItBt+6Gjg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1ez6j3r5wdp0tjy7n5qzv5vfakdc2nh2zeu388zu7a80l0thv052syxq5e2",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBT25zTE9Na21GbHJOeVZw\nU3Uzck0yN0J3Und4bExsb1VGSkNleFdZbFdZCkx1TG5VU2RyZGlWT29wd1QxV1Rs\nM20rd2hWN09wQkc2K2dWS1JUenMwUW8KLS0tIGlLQ3FNL1NrODVmNENpd3F1OGlt\nVDBhNThoR015N01UTGp6MDUxdHJFWnMKvuFBvzz1k8Tg1J2GrhEO2yvJEfwxYrmX\nDQ1/qcoSOWde7N1Mvx9ih2mFlK4JiD51tWF351C0VOKNdnBBzO46VA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"recipient": "age1f9h725ewwwwwkelnrvdvrurg6fcsn3zxrxdt0v6v8ys0nzngcsvqu77nc8",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBybnprUGZPREdpZzllYjMy\nTU5DVnRrTkxEREZQUFpiM0E4SUtsSlpqOGdnCktuaGtSSHpzTnl3c2ZuMlV6MzBo\nLzdzYWhPR1RtNnFmdVg2cTUwNE5CNEUKLS0tIHRHaXlnTlFIU1lVT1dkVUdOUnNG\nWnl1OXFpVmo0WkVlSVpKYlZaMC96encKNBnnwC2/5n2ZWYvRRoCQjOPg9b3+fFJj\nhJA45nhoO38zg0Wdv4OPA+Zrw0K0lgllk02BNXMDSv8+IthlzN5F0Q==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1tlymdmaukhwupzrhszspp26lgd8s64rw4vu9lwc7gsgrjm78095s9fe9l3",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTYysrSjZSZDYycmtNa0h6\nZHM4VGpzdDQ3ZXlYWG9ia2F2RXpOOTVnMlcwCmpTazNzeWt0RVdPT1d5b0hqQ2pp\nd3BiU0poRm53MGFFakJvYkhxVklhWGcKLS0tIGdncmxRZVB4dGVzOVMxdVM5MkN1\nTkVXRVhqbXIxa1FERjFBYnhlTkJsVmcKOUwliiZ/tMzelHQGCYi3dW/Kq0rKqW7k\nz6kSVA6vTZGFTqiK6uLqYL56BToBY1iWM4skUYTctrcXO1eEGwnbfQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTeDllU1RoL3VndEpGMG8z\neFl0TEtPL0VXUXFBVDhwRTV2TkdyaDlRS3k4Cm1hZHRDUXRFT1RPajY0Vm9oZ1BC\nbWhla2swcHdLdFdRK2NTZ1B3SGgrVjQKLS0tIGhpNE0yV2JTeHV4emdmSU5PM2ZV\nWkh5ZzY1eW5jSVF2N0Rndk0vQzlpam8K9++sBqLwmDs4WMhHADbSwlUd1te4t3/S\nDk9ENUmvu3xRX8cpT0VELjfXRf3ne6HTbG7K7Lxj5jbmfX6aSRpIsg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2026-01-04T19:47:06Z",
|
||||
|
||||
@@ -4,23 +4,23 @@
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1xate984yhl9qk9d4q99pyxmzz48sq56nfhu8weyzkgum4ed5tc5shjmrs7",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5SVUveHVXUDVTakJ1QUlj\nOWlmakVlY3JucVJGaVljVUwrL25MaU5PSGxzCk02akVDWHhjUWs0aFZrdVpjUER3\nVXFsYnJCWXFSZFNrR2tLSzRBek9mRXMKLS0tIGFJZzFFV3hkeTdmYWFKWVpnWXl2\nbm9naXJiRkdSL1UzTE9xQXBXR0xENGcKxYnDk+n4u94whP0mNNSxK3KsY8RAq+w4\nbVaxdcomXo6NJq1izMdVkQFIp1hBZniaktPGsDUMWLpJ4mSrVwPbPw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQRlFyYjFaNHJ0RDB1azRW\nY1ZvdnV5MEdWb3UwQnZuZU9sT2JleElkNmdZCm9CWVB6V0JUTE4xQWtYTU9hb0N2\nVUVtdXFVcHlTaXJBNlM0SXJac096dmsKLS0tIFdEdDZlTndXVmtTbjV3eEVsb2xG\nbzkrbU5jY3l1d3VFZExsVnB3dnFUdU0KKpmLWWgxnUfH62N0ibnqtGjvtRC6/2q7\nD/h4VozD7oZh1lC1S9SDl6J7j54frJ//AtsDdbIviVNTg4JzJ50kwQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1njjegjjdqzfnrr54f536yl4lduqgna3wuv7ef6vtl9jw5cju0grsgy62tm",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmWGNtM0RGTHR3M2c5RnFt\nVjd1MFM1Q3hvK0xid3BnZ3VWWmpySlN0WkRvCmM3WFlUajBaN1B4cEVUVWRUOS9M\ncXVQZHZ6a21xaG53UzdKdHR5Y1JJeU0KLS0tIHRTRHpZbW1IQ2FUMDBldUtxRi8x\nN202KzhjQ3MxSERhZEsrUm00RUlGZG8Kz0BdZXtPOGe/lwiSrecSho9zPn68TZ5P\nkH3YOpAHmQZrm3P876fg8ChMv5Rgd3F679d6h334fwZDfiu2h9mFBA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3WWNCR04zV0FhT3NhNUQ2\ndVN3am4xY1oxa0tXOGlLdjFMU0cwQWo4MWlnCklPRGl5YVYyMTRVcGhhNVRSaUdo\nYjZmbTRTaGZ2SFB6d0tUeEdEVFFBU1EKLS0tIHZDOFRDTkhqbG41MGNYU2lkc2NV\nWjNYV3kwRVFrWkdNZitYZlpnUlF3NGsKne7cflZE3HpP2TbtsSSQfOLucOq16Pof\n5vQ7M8lIqi0jqL/k+siWIspilbhpkknTgUzcBsui4FxKCfEPxLFMHg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age187jl7e4k9n4guygkmpuqzeh0wenefwrfkpvuyhvwjrjwxqpzassqq3x67j",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLVmRUczBEQ3RLbXByeWpU\nN3EwUm9aTXRwTlp5dG1weEpKOENXTkhrMW1FCmllRUFTZGdhWXRhWUp0dG03TnFu\nN1ZjYUJ6ZGVYNnVJZTBlcTFGUkNXZ2cKLS0tIENLZmo0aHIvbjNxbllZa2tkbHR5\neHZiRjcwd2VGbEovYm5HcEdHRXRxV0EKx6A57Usvle5ItD3XysZI41M/9s9l719i\n1fpP9W/5i+dDCgE6/ui8WLVpoj62eaArFBEN7OH1Xt04DkJ/G8Cytg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvYkxnekd6VnJ4MEo2d0tx\nSlhmeGhPenQzSkJLSkNzWnB1d2pvTHI1QVJVCkJRNTVZeWpUV0dzNXdMclNCUlVW\nQWFoa0oxMkNBUERVMVFGdG4ySlV6akUKLS0tIFhRdkZ6ZWdqVS91U3JPd2p6OENj\neHpCc3UydXF4SFZCSTl5SzcwL2dwVUEKLT37+0UdWfe26cyQ5gRwubqN8nBv0iwp\nBRDwEBw5H6rgzUEDaAv3fRy/AxT1TvtiavuKhlJdCsJqDpJmBjgIRw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1ez6j3r5wdp0tjy7n5qzv5vfakdc2nh2zeu388zu7a80l0thv052syxq5e2",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiMEo1NFhoei9vV3hlaDVo\nZjNteStQTzl2VU5WaVZFaEpmdWpzUEtVNlFFCjVUREVBZ3ZxdlBBdHplRzRxUGph\nYldyY0tpNUVqclh1TE1xemlZYm5QeGsKLS0tIE1oZ2VYSDFqZTNvK25EclRPTUZT\nbVIwdjFBQmRZTEhISTcxM1BRSk9iSWsK5/xZkLijy4p/N9WygTCIxlH0SdIiIz5W\nkGqvG27RZGqR3qwNn3vSoUBH5yOzhJAP9RAvAu5EET0jwuuG/3L0Vw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"recipient": "age1f9h725ewwwwwkelnrvdvrurg6fcsn3zxrxdt0v6v8ys0nzngcsvqu77nc8",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyU1pZT01HdkhZQ0FSbXdS\nQVdlTlhlcDcvdlZZcnJJTVNSQ0Z2NTZueVVVCmpRVWR4aTF1NDZ3NUV0RkpRMjMx\nY3EzYUZRSUtMVWtsVzlhQ0MvVmgyN2sKLS0tIHIzOTUrdEVWQ1l4RXQvV01IUy95\na1pvK0pLSFRYMGFyQjhCSDJmaERhSGsK1vg2a05yGfgp1Uo12+1nCv6cN4oU1s6h\nwvrCtejM1z79f5ZGJaAK8Tz/KOKQIA402QKp9eTWf5Y4Ix6OUd1t1w==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1tlymdmaukhwupzrhszspp26lgd8s64rw4vu9lwc7gsgrjm78095s9fe9l3",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOMExJbWh4MldLckw5YjJK\nbGJyLzgxU1RUQldGVXZIcDRZL3BaODRDSEhFCkQ3TUszSWZHbkRuV0RCNWFaZDFl\nUmQvVlVydDMwL3JKVXpoNG1sdGtFMVkKLS0tIFJGK1AvZlYybVJIS0d0TlNpVU9o\nVkFXK25Ub015YWp6V3VBaStkRDM1RjgKrP91fgB60MlnXjKs9jvSnkUlSHIU6niJ\n4VveJwqCXO9zrAcS2tZaQFQ7uwga1r+iKflzLJxMPsbvshj4sfLsrg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5a2YrMjR3SjFGZ3dtb1JN\nNGxCcDBTOVVVcHVyYzlXblBKR1F2blNVelc4ClBDZHdqNTNKckpXYzlwVFhZVktk\ncExnNTNka09ZK1lWclRudnU3N3ZDQlkKLS0tIENBR3NwSk9BdFMwejNybXYvN0tV\nU3VEVC9lTkxJbFZXWUZDRG9QMGdab3cKRYgccR5zCKEZW08FHb8/PTibpTPChG7o\nHT23XeVAf524+QpExJKsQX4Qpr+2UEG3/hUoRaisuS3TMyRUtKRoMw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2026-01-04T19:44:05Z",
|
||||
|
||||
@@ -4,27 +4,28 @@
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1xate984yhl9qk9d4q99pyxmzz48sq56nfhu8weyzkgum4ed5tc5shjmrs7",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1SktQSmxuSG5MY2ErNUEx\neEd0Q1RsSzhaUjgyQnViQ2hhd09UNDFzcVJBCjg0R0M4ZnB1UFBEcDNJQXFjWnlO\nQVhSeURHVnorcUVnczBtdU04WDhRODgKLS0tIEJxNm9teWIyUXhzbU1EY2l1WVBk\nZG9xUlh1cDhiQmdsYnZpNVNOTUY5ajAKPyt8ZIKTfu0azAFezj7rtSJX8X4rO712\n0w8MAvnLM8k5ij6nJtR3HylwLmZ9AfMSq4Aikl+oRu7rXs26JvPbZA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJdjZ2ZEcvM3RFbThNL3VV\ncHlOek8zS05sOGJGYlJ5U0J2VDJqaHA4Vm5vClB1b2NNZk5Lc2ZscDdnd0k3TGZS\nWHllM0l4TTFuWWdtUWtidDVaRU91M28KLS0tIHh5UDZiVXEyV2lYSTQ0Rk5peGx3\nNG00OFhPUWZQamdEOWVGZVpFSkc2MFkKSPQ+6c1AyWObFZQcoH5ImtFNl8lILz59\nao3ZQ23yIiSYAZ2pAeOPh4WuNtguZ5S6dnbRDr4e/8Vzw/1qgU994w==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1njjegjjdqzfnrr54f536yl4lduqgna3wuv7ef6vtl9jw5cju0grsgy62tm",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSWU5qSDBNY2d2YnNvVHU4\ncElNT0s0R0N3U3pjYW9kVGxFV2thY3QzYTB3CkdGRUhVMUdvR1dwdVdnZ3o0M0ZH\neXV0VUZyaFRBbEN1RXR6RGJ6RmIySjgKLS0tIGZGc3Voa09CNDFoMXVyZTJmME1Z\neldyMVAyd3pTZzB4RVhTRzZVOGs3NVkKyP8sIk/Oy1GXxG0tw8Ocjerfze+eIrNW\n5XYA96ct/2M2jiPdTxg2yEI5a9wycDkzNIzE95Xyfl3LkY8864wAMQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6Q29GRkdZdzNjUDAzUWU3\nTlRvUm5URWd5a3MrMDE0WmFsWFRZS2NkQ2s0CktHZHpOaUFnWUhHUDdWK3FPMi9L\nMkhweU84OFp6bFhnME1hbk92M0pGMUUKLS0tIGdkYjN5VHl5L2hlL29wa2p6VU9t\nVzluZzdFNnRuL0dNUmtjZ3lOQTRCL1EKvvrXH1IxL4PVIZam2k9XRNBTLKahNCUh\nhMdfb4shNBtUs1bAiicJRPT1eAAWk4CWO5xpS3uJ0fODabrOozXdnw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age187jl7e4k9n4guygkmpuqzeh0wenefwrfkpvuyhvwjrjwxqpzassqq3x67j",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJVHF0N2hHTHprQlpSYW8r\nMmpIZmZIT2QzNXYxTE4vc05wcXhTQzQ5clg0CktzRGFpeEVYMXA4RnV4TVdJNk04\nM3ltL2ZyczloR3NNWm01cE16NmJNemsKLS0tIDBHczRiUVhnZHlrTmdocmNQY2NK\naU5VYWZ4QWFuK0h4cUZGOGxUL3QzQ0kKtsuW7yl1/t7q9kUhTtK0G5G950Bi5n5w\n7cxX/pfMtgPhOh3NMoeuTxc9sH4pTIthRmaLVJ+GzEc4KsMJhOp+rA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLalFBanFCMWtaakVmQWNh\nc3Q2UmZ1OG1MOTNaTzFmcy9lbEFZdDZMSEVzCisrV3hMZndQekZtR3ZORjRkWHdm\nZG85M2Y1L2tPcWc1ZHpkd2R6TDJ2VXMKLS0tIEZ3Zmk0eWVsUTJwNHZJbWJhY3VJ\neVdxMVdqcTdxeUxhU0UzYThhOWhnc3MKOkedT3Tifi+6Q+C0ur1+TQuVwq75fAUa\n0cdXexCI2UIyLv+Ggbv1YIYcUiNeJNiF8JttpGAyOoVnUfIUcLsY9g==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1ez6j3r5wdp0tjy7n5qzv5vfakdc2nh2zeu388zu7a80l0thv052syxq5e2",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLK0Z5UHpwdTFRM3dNL2FC\najdScHp1R1pHM0ZyMk1reENnTFlnTldVNTJJCmxVcVY1OHlrWExIeSs4bTBwQklE\nU0NkUnVmdlVuOHcrWjJpRFU4WTc5ZlEKLS0tIFhheExxWHBIcDBqL290WEpJK0sw\nRTZKbzRWMmJhVVVGT3A1UWJQUE1QS3cKp+jmuHUvZKbPx+/gxQUSz7QV1jLuIzP5\ne1jkJ2rJT2i8snAvihd8bsjRSFmoUnEg6kV8f0OteezNbkZoNhd30Q==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"recipient": "age1f9h725ewwwwwkelnrvdvrurg6fcsn3zxrxdt0v6v8ys0nzngcsvqu77nc8",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4RDZ1K2RUaU4vTnFsbnpM\nRTRCUkg3RUFGS0pnK0NJSWxXTWVsaTZ3ZVhrCldDWkd4L0Y3Sm5uM0FZZnZqclJq\nUEpNRElGbEUwM0ZHRzJWNG0xT1ozZzAKLS0tIDBiOTRJSlRRbkxuRm9aTGVoZEoy\nU25iVjhxTXhvSTVqV2kwb2doMXNBeWcKK1wDpzC6aYSVJ/z0wGvSDjgXDiPeW+t7\ncgD9a1nCB/N1QZQn2IbnnkL6EsCuIQpRlzlLL9FhdRdLP+3ij+i8eg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1tlymdmaukhwupzrhszspp26lgd8s64rw4vu9lwc7gsgrjm78095s9fe9l3",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzRmpkdloxdU1DRmF1Tzg4\nYSt3bks0Y21OQW5DS2JRU0ZMMHZJV2xLc0dRCmRORFliS3A0QTR1Uzc0ckd3cHA4\nRnNrdVBISG5NcjhrNDRoUnl4c2dPL1UKLS0tIHA5aW9GQkdXU0VNRHd6aEpoSzhJ\nSmd5OU1ESGJqMFVZdGhBMkdYTmlsRWsKLN36pDsdf06Rn9RLxfh46nX5u0dfyoe8\n/VvQiaWoj2/pv8NmwdFzdJQ0mTKkvEdxxY/Jk0YK+GQA/NGIVIIoWQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkN2kxR1BSUVBHSDhuYVVu\nREZGeHFNT1lUVGljMUpmV05FcHROdEcxK1hRCjhYYkJ1U0JaUkdRZG1tMXFudlRM\nSllqZVBsd3JDemFITVVlWkZ4Uis0NnMKLS0tIHRjWHFzZEgwSmlnWFVpQk1tcnJC\naytMVk92U3lGbCtWdXA2UDFXY29Ndk0KPmSHHP4Nyb4dkyz9a60OP5Lc8MlbPGxo\nLReOVEJ86yGbL4ARFptqrpcJ6ILEagWSYLEkqoZ1xiG1BSvs6J7dHg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2026-03-23T09:09:07Z",
|
||||
"mac": "ENC[AES256_GCM,data:phFpHUzJ/7rd1k1fr9YFD2FplXV3Qv5zFni00fAgG2VtVoIdFYeNRE0EEh2ulnKcIXjB/5lZuMss2bIoBt4i46BB2ZHTpnWksbeHowdgkHL+eXT1F7b11S1y9NEKc/ug3jarPwyj3usmVQJlllAzANCQHGrYQdBrFXvFae3cH40=,iv:4v3k4q0SxyTvHoqr2Abf6OhAcANCT9oWTa5Kwlb5GCs=,tag:Hn+fUEmOu7fWc7SSBe5yfA==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.12.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,23 +4,23 @@
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1xate984yhl9qk9d4q99pyxmzz48sq56nfhu8weyzkgum4ed5tc5shjmrs7",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhVVBSMm5hUmlnUUhrSmxL\nNVVZY2dZajlKSWMxZ3dMVlBORjNUaHBOTEZnCmNpNFRodDFFb2xNdnlYVHRiK2ZT\namV4YXFOd3FCK1ZiK0NIbW56Sm1wNm8KLS0tIHk5TVlxZHB4NzFiQTFQeThUY0JG\nV0huc2FzQU9WWUFJWUFNS3JoTzFaTjQK/T7aFSWnkpv1Qbx47LFp85bkaCOt5vrs\nLjiAGimUOVkr7ZcJz3540JWvBBUNNRKmM3QDNlgPg5luI0fa7+pNRQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3NmNaR1dvaXA4QUVqbkNP\nbFlpTzlHMXd0SGNsd0wyZ2FwZDFBR0tuaWxnClZuNzZma29BWURibzVUZndUc2hV\nZUpUdE94TC9Pa1dLQitLbE1qRXZJNVEKLS0tIG00ZGJyNHp0eEtQeDNsemErNVlo\nclBid2Y0TysyZ1RtK3F2RS92QjJJWDAKUruZmiTB5tkBF5VOO7dv+GijgAO6AVNe\n6t/xyot/Gc3WFOeNkYUk1K7fo0oLFfRG1tZBW+4cwFi/FMUpyjCxig==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1njjegjjdqzfnrr54f536yl4lduqgna3wuv7ef6vtl9jw5cju0grsgy62tm",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSU3ZFZkZqUjcycmtYalRw\nYXlSQlFvcXFwS0xYQ3BVUXlPeDBHeUl0R1J3CkRGRHNaZWxQN1hISmh0VzNwZnlM\nbUFYVWw3dU5jNVorOVFKYUtyWHl5Q28KLS0tIGU1MkhrNHptZUZldHM1Y1FZTytV\nY0VvQjRmUmpIbW12WDlkLzVwSE9uZkkKMczm0x03kPRAD+pdXyDRTqaQckU4MKxg\nfIJAAwRAbskLF37SUzvefa0DCdKpyK876km7nOyIZunTE1j7xIwMNQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKcW5Ld3BjdnYvanJYeHM4\nNk1vS2o4WEg5Ni9aUlBuNmRKMHJpOHh3dTJnCnlhTFRDdXoyS3kyVEM4Ukc5N0pB\nazBwQlIycHliaEt0Unk3WUZtcURJRXcKLS0tIEpoVHlFLzBCSDdGRVZSeCs3SW1n\nK3k4T0VkeGhBd3NSYlNYT3JudXRQVWMKPu0HGvtYlgdih3vNrqvEvke0Fg7hr9xm\n1f3G7GpeRBLZlab6UKKzbFFgf0NADnGEBmWMP7e9taVYqYy1FFf7lQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age187jl7e4k9n4guygkmpuqzeh0wenefwrfkpvuyhvwjrjwxqpzassqq3x67j",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnVE9tNEhSRUFydEhvL2tu\nbWVuVXhVYlhTVVFnMTZrOEFBOEZKZ1A5TFRjCnNSSGVjNmtYUnVsdGZRcjg5SmNW\nbys5alQxWVRRNS9rV1V5aU9nNEF5ZHMKLS0tIFl2MjgwclFZOEtvVmR5TDFzL0lm\nblBnekRPRzhqem1CSEgyM2YzTnJvck0KwITv/a+Z0XQ0FeI7rPti9IGZPuILWVhC\nTC0dWuJ3uSflAxgRvGpaQyvsKMLgkXCRf58kx17cHjPT+Z8EsXeBSQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQUFgzR1hFemRNeHorN0Fm\nNld4eWhwMWJHdWhBYVQxNUtGemlEeU5jcUcwCldlSENWSXBxYjN6TXZUM2Nuc1Mr\nN1Zlc0lWY29KcGI0bWVwYzVnS3ZBM2MKLS0tIE1rVnhhNFJkeFZjeDh0RVM3eHU5\ndkJPZm1xQTdWZTdXNVJrdHpXV1dIWVEKZlyGzrce2W4M8LVDbYGXIpK+cVqqB86t\nqibOGTNdSAhd2Y+IWb2FdPr0uxoixC4j6Mk+Nw2bXhDKXp+VAAlWog==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1ez6j3r5wdp0tjy7n5qzv5vfakdc2nh2zeu388zu7a80l0thv052syxq5e2",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxdEpHdzlVbDdBcWJyNDJC\nQnpTaFZLMno1Q0ZJcXVjMUIvcTZ0K3lDY0RzClJlbHhqbWNVemFpRFh2WXRGV1VZ\nRENTV1ppN0J5SVFDdUR1aDRRdTFKUUUKLS0tIGRFMGIwVkliRitaeXdHTm55QzQy\neGdzYStQalVFb3ZPdkp2VFJ5SUJ6RkkKeeOJ1MjTKqevHHl+5dXL8n5o05KV6HvS\nMf5yO3rxYDLzO5ore63G3KjqCS9i36mTzLoyJgYIKQK4IAh/AKcI/A==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"recipient": "age1f9h725ewwwwwkelnrvdvrurg6fcsn3zxrxdt0v6v8ys0nzngcsvqu77nc8",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKN0x6QnJCZklFTis0QVJE\nbWdJZmxJWmxRYUtNdmFmaHM4bTFQOGZENVY0CnkxbW5Qd3lQTHlNV3dEWHJVS0Qr\nNUhDMzBmdzI2dzNGY3J2dVJwd0wzYXcKLS0tIEVjb3JNODY2ckNUZDVtN0VYaTNm\nSnZWRUpFQkFXWWRBVVNVcUVNYjRxb2MKGa0tBfwyEWrXmkCIupwHID2j357UMiSe\nsg70OdmszMZ+MDjcLIXVkjQfrLTbtIKU+u3jA7MDzECZTm+nAk8cTg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1tlymdmaukhwupzrhszspp26lgd8s64rw4vu9lwc7gsgrjm78095s9fe9l3",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxZE1lU0UzUFQ5Z1ZQRXBj\nbktnN0lWNXZ0QVhPS1Z5cFNPeDJlVCtLdWlvCmFPSHdkeUlONHptbE0xcjg5SUQ2\ncnZKY0JWY0haZFEzcWNnUHdnN250WmMKLS0tIEhlbG91K0tabmVVSDhBeStoUU5j\ncFRlUk5xZjRRVC93UzNJUUhhTVQySlEKB2uBSMXOJd1ufB3i66ldhXlnbquWcXgi\nXjuJb5ud4Nz97wlSnqAADoY8V9aYJM30byL4VJeXiMq1oppDptbQzQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4YTdqUXp3eVdmV1drQ00v\nMkxFUEIrdTBoRDREbndMc2hkekdWZDlSSFZnCndmMmtvTDRpRjNvUENQaVY3S29k\nRGFaUStJRmlSSEJxMG82Nm1Oc0puOU0KLS0tIFdUdEZNR2tZdlBoTFhjK0pNNnNW\nVGZIRytNNnFOQXZMTlhHMDRMUnJ6MG8KDaPfk8dcE3ijfwPhbeN909I6zPdjxX1M\nFnQiSGQuKvWuVzn2qjl8AAFxw1nVO/3l+7eu/PN2yeKIb3rMUvWpDQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2026-03-01T17:47:46Z",
|
||||
|
||||
@@ -4,23 +4,23 @@
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1xate984yhl9qk9d4q99pyxmzz48sq56nfhu8weyzkgum4ed5tc5shjmrs7",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2UDczc281UGU2aTlHeEp5\naEdiZGgzK01VODE4enMrRTIzNmNwa0srRm1rCnd3K1NnOEpXVDJHWGp4VXhFTUJr\nM1c5cXVza3NQOTZjTElTcFRhY0ZDTE0KLS0tIGp1Y3d0dXg3NnBnQmpmeXI4a2NQ\nZ1lIU1hCMUJMR3lidkxRNWV6SzJkeGsK93sF6uCKKFh12hnlSZ9DMxBv8/j0LIp5\nMGeUbbX3sWdk4I04QjXl2Yi9ER5d9W3zDfrQ1u5CqM3IqSPHQ7VgpA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2UGRKeHloRTVjcHUyOGJ3\nM3FKajJTZXE1d2pnUXEwa25BMytSZjlZV0YwCjFBUjhtUDFRckcxK20zcmxpOVdX\neGhVaC9jaHJuSm9tRktZVlFLTk5yS1EKLS0tIG1CRkdhVW9LSU9sY3dRUS85eHFU\nRnpxdVg3dzM1bldQS0JQY2hYbkhXSVEKREVnO8OsmWfrJ19Vr7KY3O97XG2LXrSP\nxvGOZIDMI2UIfTFWm5TcFDOOD34HV0fWtzjn9GYSx9XN5fQX1YK5vQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1njjegjjdqzfnrr54f536yl4lduqgna3wuv7ef6vtl9jw5cju0grsgy62tm",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPcE55WTAxSERXdUZudmxJ\nODQzaWFBRXl4Ly93MjM3K211OXBtL3A1SUZRCjBjUnRmL0FVdXgvWExsM1E5bUhs\neHAvcEwvUmxDeWFoMWJ4RDQ2VEpzcGMKLS0tIHJUNWE3Z3drMVYwSkV4Tll5emxN\nc3VnZFllaTJLckE1M2ZYaE4vdFNTSGsKvQhxtlSjH9+tjaT77ZiuMQeb0RPotSeS\ncoYf0R8TCvy8c2CP0YYs99bIJP1zx4RirlC/80Ji9eXBYkJaoDRkew==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3bzdOOWRzMm9BOVNoaTkz\nbW1YZWZjVkF1MHhMcy9nTFhPUjdpamRLZmprCmtIK0VxNVNSR1MrbGlqakZzV3Jw\ndE8yZ2lyOE92MStpRWExUENDSGNCbHcKLS0tIGMyS05FUlk1a2N3WmFQcGdpZytD\nRWxQc0hucmxCK2psSzMrWmt4cTlxMGsK95lvBRK4daCbixLEJkEToAg7/yL2iJ+j\newP0caUe3E9UyhuFmZCiN9VsYaQy0q12KcSTq/KMybj6UmDThaOMRw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age187jl7e4k9n4guygkmpuqzeh0wenefwrfkpvuyhvwjrjwxqpzassqq3x67j",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMS3dYenBFalBySUJKcDZl\nQXRBMTFXN0tCQTM1bFl2SDNma0xRZnBJbG5nCm5hTXlIZVVpREpLUUoyRitnWWZJ\nZ2VPSFBBK2h1aFZldGRNZm9JVDRvd2cKLS0tIEg5cHgrRE1sS0lFZkxSNHdweXJz\nb3p3Y1lZUjBxYWUxZjNBbDlFUkd6VVUKLaUOi7OESO8yTZyBcgOt0LhjM6QKMwlm\nl6MSVJYBtmcp4wdZdaCsiC28dlHg98qXauLppgnkda8LyhDUliP5lQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByTnVqbkNSbW1nNUxpNlNQ\nekRWWEJBeHdmMG1jYk0xeGNUMnBlSk5xNVRnCmNhTlZEcGtUbnlNaU12MGpVVDhp\nOHRRbkVBZXg1bVZiNmtXVHlxU0dDL28KLS0tIDRhV2R3akMzZHFZcytwSHJ3QU9I\namhWREJHZGcvMk9DeUdsb3FtOW1wUzQKhRDjz4lv9BPjOl0ZUstNlMVAw5x2Dgql\nQ06h5a1qm6YqktDvJLiKiUA64ZqDFHvB61x5qVn0Wc913vLayoEIwQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1ez6j3r5wdp0tjy7n5qzv5vfakdc2nh2zeu388zu7a80l0thv052syxq5e2",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkUTJEcFp4dWxzR3BuRnJw\nNy9vcStGT3ZnOHBYczZSUHgvN2Fzc0MzaG1nCjdCNXRnTHN0YUZZKzBPVmI0UTBn\nbnRNZGxyM0tMSW9HVG1yMmxQeUFXUHMKLS0tIEYrOTJMWlZndHNxeHA4U0ZFajJM\nclRPdVAzWThwNGRpSzJkU0pxY2llbnMKpMQuw76xRJ162EJ91ui6jLNeBY6+XCiG\npKJB9YCTnh+JlxFzm9LU5s8bI4XzsywdrdKMYck1G4A8NW4MoQCrig==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"recipient": "age1f9h725ewwwwwkelnrvdvrurg6fcsn3zxrxdt0v6v8ys0nzngcsvqu77nc8",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFdmIzR1pjNnBnckpEMzcx\naXhyZHNXNUZ6SFdnSFRoWWIzUVFFNExvSXdFCkNXQXhDY0h3UURPRlJSeGFrVC9B\nblFrTitZVllxYmVwbEZRSk8xelpYY3cKLS0tIExkeVlMNWtjZ2VkL1AreTRHUXNj\nYS9LNTNLd3BhUlpNdWp3UUdNQmhLYkkKpYAbuft1WSROAm+3iGvU1TRhBP3HgiB8\nuDOdv9NTw5AhhTK2VIixhJHZI3gIAvCwndmQWV41PDgoDqmeVwxrsg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1tlymdmaukhwupzrhszspp26lgd8s64rw4vu9lwc7gsgrjm78095s9fe9l3",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMWnYzbWVNM081WnJ2MHh2\nbE56TUZDU1JqdkJHczVaUi9ReTFsNENRdlFnCnArZEYrcmtIMFZEZVhUcG9MdWNO\ndVRNS0w3S2FqMzNkOGVqcTJwTFQyK0EKLS0tIDRnSjBaRC9xd0ZsUy9CWm13MWZu\nTXQ1WEdZR2h0UGM0cmZOMFZwdzdzd28Kv+KkGEfWvQVgOtiJMqEwbIgQcQI9U4fC\ngh+9QrN8blwbt0OVqyu9tPWrP9bPOEhM6U13wj4Q8BqzrNVsLix4Lg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByVDBtZzJibHNpV01WUTF3\nWm1OajJnVjF6TFluK05UWXptdWJFekY2Wng0CnliUGhmTHZZUHNRY0piM0ZXT3J6\nc2puem5VbVczOVlKZ3RqM084N3JCMEEKLS0tIHppbHdvYk1LTXRMeXp1eEZYeGd1\nT0FJZlNJNnAvQWNGcnBCdnBNTTg4UzAKTwMtaAQk5/8qGO4M3kmjfP8yJmrDiWiJ\nsD23c6S7wwAZdltK1spIRMkSS2AhJBN+ePXBiAGWjatSWsBKlDZqww==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2026-03-13T17:29:46Z",
|
||||
|
||||
@@ -4,23 +4,23 @@
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1xate984yhl9qk9d4q99pyxmzz48sq56nfhu8weyzkgum4ed5tc5shjmrs7",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqMHVFbUFpOGdXc0t4Q2dj\na1lxeWFBR3ROTDN6RVlwYlgxZmdhd1ozNmdvCnVpZFFnaGFqWGs3UmRtZytlUEZK\nK05lM2ttWmNOSGU5ZlBTYWZ6K0dCbzgKLS0tIEpHQXRmaVVhbXJRRUI4REdCZWI5\nTmFDY1gvWVVkUExpeWYxTjlCb3MwUUUKnHzWgaUvDXH1I3TPyY38h9Pbjqk4Whma\n8ctECOpg1obtMr+a9Bdw11IPwAe3R9K0ZfE681HroETCHRxw2+38yw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDMWRWUElvQ3ZRTjFKRVQ3\nYmZINCt2eWJBbWcvLzQvWWlURlpzMldSWkZFCnhidzFMZjliUkt0MUVML0pLQ21L\nQ0pwL2pteEFlaW0wRFJBZk9BSHRXa3cKLS0tIEc1dXlNdDQxUFZrVUc1Z0VBMHQ1\ncXNaYnd5dVVvTU41bTRnRUdGVjEzczAKjfqq9yciDRBW+N2M+5mRRztlW5+JRL0n\nRL5aZlsDVo3SJAxjaaGx4zgwN7dUX6JPrTGW6sYd5LCGuHZWX8phfQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1njjegjjdqzfnrr54f536yl4lduqgna3wuv7ef6vtl9jw5cju0grsgy62tm",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFRU43QXpWb1JDUVJCck9z\nNkFXWXdWY3pJZmx0Mjc5SDgwcVFXWkRua3pVCjAxQkJBUXF5T3hxek00ZzhRcmF2\nb2tWQlYwVXpiM1dMRFdqNXIyQjRhb1UKLS0tIDB2Q003YjlCTXUxa2k5RmMrWVds\neXhLSzViMHl6Smo3dDljRnRsT3lRb1EKKxaXLegA0V6vTnYF7l7MsJD42XWidB/h\nGiOojZ6r3JVV4Wx4Xl/MPk7lYvYN9VH//PUlA7h0Q9iVaQS+HA+aZw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJNXBjRGdmWGFEMmY3T0F3\nS2lFcXRKWnl5T0JnZkRxTzJUK0FlWlIvRVNvClVESkhEdFgzZnhKTTdaZEd3N3Yr\nWWJYTWI2M2Mwd3F3ZWdJVkdLMWwrRlkKLS0tIFFwcEdQUVA1dUFRRUtkUDN5RUZt\nTjlQNEx1ei9UeVpRTjcxL2t5NVBUajAKaZOQ1GQ3vtgZdxxoXQKU5Q3jSUnakW9F\n6bHKbhBGSFwCb3u9+mgVaefBU11vT1ue1kA8sCM03MkYBmM+8cPv7g==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age187jl7e4k9n4guygkmpuqzeh0wenefwrfkpvuyhvwjrjwxqpzassqq3x67j",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBob0MwUkJvRDRnQ05XMVRh\nODY5WE8wVlBkTW8xWUlLWUwxNHV6Y0FJTW5zCkdVV1dJTGlxREdGbWowak1HL3pH\nejNKV0VvejRadTVCeVdxdFkyMk0yOG8KLS0tIEllWldkaXZ1TzYzSmFoQ21nMkV4\nZlFxN0wxWlpvSHdreTdxQ1NZcit0dGsK+23q1QS5L1JzqFcKUygqfmYc64qVOO1R\nSiGa7O7ZoRMnn0Zfjas3K+byJ4PzNitEEU64s1518L/e78oHKQ9vVQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2a01mMmNPckZTbmJvQUVC\ncDBrVCtlRmpJMVByeE5zWFMrUCtHSkdDRFNBCmpZazA4Q0duejNYcmoxWDVMR3A5\nclh5OXZNNWd0VjI3cGlWbk5JTUw5NHcKLS0tIEVma1VsQ3cwSzdGZTlpanA2ZWNF\nQ2xLSjNkdjJyby95aTdTYk9HSHFvM1EK29MPNiB5GKqBFPgLcIp1giWzYRpPZ7ur\nDSKqvZ4kbEyxgdLxJ/i2P2iv4/4bb14LWQno9iEXsVeZ/iKf1mDigg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1ez6j3r5wdp0tjy7n5qzv5vfakdc2nh2zeu388zu7a80l0thv052syxq5e2",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaakJHOXRxeGRLZldZVmVi\nNHpFNmtYU1BCUnVKR1I3ZC9CSW5ORXlCbG5JCnpyUFA5NDNzbExob0s2R1EybzVJ\nb2tqSWtmZk4zQ1RZQmhKUmFvTFhTUjgKLS0tIG1DV04zMzVXdk9yQ1FFQzZsakM1\nTXQ0dCsrN0Z5RUVRamJUZEo5UmRiZWsK/av25ulEhPih+AK2rnktJsK6/SYS4IDx\nBuI2hBRZTdw1m4bBcCwMkLn/F1Rxd37xI1NZKKE3+CgnIs5G9XxhAw==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"recipient": "age1f9h725ewwwwwkelnrvdvrurg6fcsn3zxrxdt0v6v8ys0nzngcsvqu77nc8",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBteGpDZkpLcFIyVzNMbXFV\nMlY4SExrTThkdEZJczdLdlVwUWpVUWVuTUJvCmFXVG1ESHlIOExSdFJJOXhtb1JZ\nSVBocEtJTjRkckNwYktOdEdBQVQ5TVkKLS0tIE9iZ1pzMGhVc3F4THJwb2pFK2dm\nWnZ0SkhyNFMyQ1VVa0tWbDlXR1ZwK0EKPJq69mAeq3ghUpY+VuS+xCKeJVwmYnGm\nQ8mtmk/5skqbeOSKAbIiEEY5avPRy+Uy3YTD7g6vpSCqnpNSLEc/qg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1tlymdmaukhwupzrhszspp26lgd8s64rw4vu9lwc7gsgrjm78095s9fe9l3",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEV1dtckt6Wi9HQ3Fxd0Ev\nRnRzcWs5SWRmNEpYbmZQTnEzbnliVDRkZ3pnCjUyTEFOYjd5SC9STVd2V1o0NGVR\nelphZE1ISGlkemdXSTBlcURDYUh3OGsKLS0tIHUxZFRKbVFsM0FWZ2ZEa0FaYjl5\nTWhTMnptYU4xNmp4aVNYMDRORkk5NVUKiwwbLVe3mtVe9sgeSA/FUhkowfFeirbA\nXXL9ct+lizSNXsFG7w3xpZsEaNGbHF28maZHWOpZotKRbdx4w6UJGQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBIZENwNHhLdzNYYmEvOHNW\ndEZJbVpUMGJyaVU5WkR0bUc0NzlsMWtmdjN3ClMyM2dGa3BlVTFqeDB6ZHRGSTNu\nREZSN0JXUkU4ektnWis0L3lCOHdpTmMKLS0tIGQxZTFRYjYvMS9uM2lYUThwYmtp\nbkYrTlNQeEd6aVNTY25aMHFVbnJXSWMKulwDyWezqDUIlv/aHMMGzGqOFU3VGaw3\nYvm/e2wFWPenFH0gfALkdC8upRghE8r9jkXcj1pSmDBbfjNghz+2oQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2025-12-20T21:46:17Z",
|
||||
|
||||
Reference in New Issue
Block a user