Keep all prompt records private and remove repository-published prompt ledgers

Hop-State: A_06FN8RB08RV0PTBDNTH4738
Hop-Proposal: R_06FN8R7MN60MJK1X7ZRDG5G
Hop-Task: T_06FN8PDXR44H4NMQ41P9P10
Hop-Attempt: AT_06FN8PDXR7HFSVKEKH6BQV8
This commit is contained in:
2026-07-11 20:31:35 -07:00
committed by Hop
parent 4364e1d8a4
commit f7cd6df597
33 changed files with 40 additions and 486 deletions
+3 -6
View File
@@ -1,11 +1,8 @@
.DS_Store
# Hop's local state and isolated workspaces.
# Hop's database and disposable workspaces stay local. Its immutable prompt
# records are intentionally versioned with the repository.
/.hop/*
!/.hop/records/
!/.hop/records/**
# Hop's local state, prompt records, and isolated workspaces can contain private
# user requests and machine paths. Never publish them through Git.
/.hop/
# Local build and verification artifacts.
/hop
@@ -1,16 +0,0 @@
{
"id": "P_06FN3MBF9ABQ6NTST1B7GZ0",
"task_id": "T_06FN3MBF98GWD4NA5PA1RWG",
"attempt_id": "AT_06FN3MBF98FBDSE1BZDP5DG",
"state_id": "P_06FN3MBF9ABQ6NTST1B7GZ0",
"prompt": "what? wtf? i need it in the right folder how do i do that",
"agent_name": "codex",
"status": "completed",
"created_at": "2026-07-11T15:35:07.850559Z",
"metadata": {
"source_tree": "012d4872472b5c8621c6ef3cedd50509e09e7aa6",
"git_commit": "a70773826f67e30e57b7c0a0419e3b601674632f",
"attempt_head": "R_06FN5TJRZC3APECRM9VM2A8",
"attempt_head_kind": "proposal"
}
}
@@ -1,16 +0,0 @@
{
"id": "P_06FN4EVBXYRV3MQJE7FXJ9R",
"task_id": "T_06FN3MBF98GWD4NA5PA1RWG",
"attempt_id": "AT_06FN3MBF98FBDSE1BZDP5DG",
"state_id": "P_06FN4EVBXYRV3MQJE7FXJ9R",
"prompt": "so.. i can have multiple agents working on the same codebase at the same time and it should be fine?",
"agent_name": "codex",
"status": "completed",
"created_at": "2026-07-11T17:30:53.807892Z",
"metadata": {
"source_tree": "22e1b6bfde927afbe0622a552cfe6703fed85f3f",
"git_commit": "dcad0b49f3e5cde9cffd3ea23f50088e6ba4c617",
"attempt_head": "R_06FN5TJRZC3APECRM9VM2A8",
"attempt_head_kind": "proposal"
}
}
@@ -1,16 +0,0 @@
{
"id": "P_06FN4K2V1S84CQ0XFJDQRQG",
"task_id": "T_06FN3MBF98GWD4NA5PA1RWG",
"attempt_id": "AT_06FN3MBF98FBDSE1BZDP5DG",
"state_id": "P_06FN4K2V1S84CQ0XFJDQRQG",
"prompt": "hop is broken. \"I updated the reference to v=3, but Hop blocked landing because another recent task also changed hop-home.css, creating an overlap. That stylesheet conflict needs to be resolved before I can safely apply the cache-busting fix.\" this is fucking horse shit. the agent should intelligently resolve the conflict",
"agent_name": "codex",
"status": "completed",
"created_at": "2026-07-11T17:49:23.598284Z",
"metadata": {
"source_tree": "22e1b6bfde927afbe0622a552cfe6703fed85f3f",
"git_commit": "94d2ac053cd45d514d586764836ee46eb26c2af2",
"attempt_head": "R_06FN5TJRZC3APECRM9VM2A8",
"attempt_head_kind": "proposal"
}
}
@@ -1,16 +0,0 @@
{
"id": "P_06FN4ZBB60PW23320GTM4T8",
"task_id": "T_06FN3MBF98GWD4NA5PA1RWG",
"attempt_id": "AT_06FN3MBF98FBDSE1BZDP5DG",
"state_id": "P_06FN4ZBB60PW23320GTM4T8",
"prompt": "now i want to publish this as a real product people can use. how will people install it? by cloning a git repo and running some commands? can you update the readme with clear installation/getting started instructions, and create the wiki pages as well",
"agent_name": "codex",
"status": "completed",
"created_at": "2026-07-11T18:42:58.992885Z",
"metadata": {
"source_tree": "8e3074490c46530db90e20242bd16000aee239d0",
"git_commit": "56fe10e4deb1d42626d0a5dfc4f1446987338019",
"attempt_head": "R_06FN5TJRZC3APECRM9VM2A8",
"attempt_head_kind": "proposal"
}
}
@@ -1,16 +0,0 @@
{
"id": "P_06FN4ZY663SBZ8CDXP9YPF8",
"task_id": "T_06FN3MBF98GWD4NA5PA1RWG",
"attempt_id": "AT_06FN3MBF98FBDSE1BZDP5DG",
"state_id": "P_06FN4ZY663SBZ8CDXP9YPF8",
"prompt": "wait? should we create a packaged release? HELLO IM ASKING YOU WHATS THE BEST IMPLEMENTATION FOR A POLISHED PRODUCT NOT TO JUST WORK WITH HOWEVER IT IS NOW JSUT BECAUSE",
"agent_name": "codex",
"status": "completed",
"created_at": "2026-07-11T18:45:33.360226Z",
"metadata": {
"source_tree": "8e3074490c46530db90e20242bd16000aee239d0",
"git_commit": "422e59ec24e57ddb0d3df46199c4f16bf428371e",
"attempt_head": "R_06FN5TJRZC3APECRM9VM2A8",
"attempt_head_kind": "proposal"
}
}
@@ -1,16 +0,0 @@
{
"id": "P_06FN5172P0R8W5XZ1CHK8N0",
"task_id": "T_06FN3MBF98GWD4NA5PA1RWG",
"attempt_id": "AT_06FN3MBF98FBDSE1BZDP5DG",
"state_id": "P_06FN5172P0R8W5XZ1CHK8N0",
"prompt": "i wont be hosting this on github, ill be hosting it on githop.xyz which is a custom gitea. the site isnt live yet but its done",
"agent_name": "codex",
"status": "completed",
"created_at": "2026-07-11T18:51:08.33613Z",
"metadata": {
"source_tree": "7879aa3466a9b33dcebbe8c1c9873334976577e5",
"git_commit": "f434be463cc2940449970408924e653161dc9ac8",
"attempt_head": "R_06FN5TJRZC3APECRM9VM2A8",
"attempt_head_kind": "proposal"
}
}
@@ -1,16 +0,0 @@
{
"id": "P_06FN572J3TK7PBAZVM7HSVG",
"task_id": "T_06FN3MBF98GWD4NA5PA1RWG",
"attempt_id": "AT_06FN3MBF98FBDSE1BZDP5DG",
"state_id": "P_06FN572J3TK7PBAZVM7HSVG",
"prompt": "can you publish it under GnosysLabsLLC/Hop .. i have added my SSH key to githop so you should be able to",
"agent_name": "codex",
"status": "completed",
"created_at": "2026-07-11T19:16:44.190798Z",
"metadata": {
"source_tree": "a91a4bd37315fa30673005970ce750755e9582be",
"git_commit": "e13e291812530b2dee72d0caec1e5974a87fbf47",
"attempt_head": "R_06FN5TJRZC3APECRM9VM2A8",
"attempt_head_kind": "proposal"
}
}
@@ -1,16 +0,0 @@
{
"id": "P_06FN57YDR2V5WBGFMNBYN48",
"task_id": "T_06FN3MBF98GWD4NA5PA1RWG",
"attempt_id": "AT_06FN3MBF98FBDSE1BZDP5DG",
"state_id": "P_06FN57YDR2V5WBGFMNBYN48",
"prompt": "sorry actually its https://githop.xyz/org/GnosysLabs/Hop",
"agent_name": "codex",
"status": "completed",
"created_at": "2026-07-11T19:20:32.448864Z",
"metadata": {
"source_tree": "a91a4bd37315fa30673005970ce750755e9582be",
"git_commit": "020f15acdb2ae92f5fb80e431485a6ff8ea8025a",
"attempt_head": "R_06FN5TJRZC3APECRM9VM2A8",
"attempt_head_kind": "proposal"
}
}
@@ -1,16 +0,0 @@
{
"id": "P_06FN58K0XX5MSYB34619NX0",
"task_id": "T_06FN3MBF98GWD4NA5PA1RWG",
"attempt_id": "AT_06FN3MBF98FBDSE1BZDP5DG",
"state_id": "P_06FN58K0XX5MSYB34619NX0",
"prompt": "i turned off the proxy? does that make it work",
"agent_name": "codex",
"status": "completed",
"created_at": "2026-07-11T19:23:21.19964Z",
"metadata": {
"source_tree": "a91a4bd37315fa30673005970ce750755e9582be",
"git_commit": "ad1055c4f4e79f0a0efa9e39c2d6dd8ca5c1b219",
"attempt_head": "R_06FN5TJRZC3APECRM9VM2A8",
"attempt_head_kind": "proposal"
}
}
@@ -1,16 +0,0 @@
{
"id": "P_06FN58Y686QH957QKXDC820",
"task_id": "T_06FN3MBF98GWD4NA5PA1RWG",
"attempt_id": "AT_06FN3MBF98FBDSE1BZDP5DG",
"state_id": "P_06FN58Y686QH957QKXDC820",
"prompt": "why would you have to put port 22? thats not how it works with github. wtf?",
"agent_name": "codex",
"status": "completed",
"created_at": "2026-07-11T19:24:52.673537Z",
"metadata": {
"source_tree": "a91a4bd37315fa30673005970ce750755e9582be",
"git_commit": "092fec93f02614e919b8a75392044f86703d2293",
"attempt_head": "R_06FN5TJRZC3APECRM9VM2A8",
"attempt_head_kind": "proposal"
}
}
@@ -1,16 +0,0 @@
{
"id": "P_06FN596K5H32W6J8X4NNS4R",
"task_id": "T_06FN3MBF98GWD4NA5PA1RWG",
"attempt_id": "AT_06FN3MBF98FBDSE1BZDP5DG",
"state_id": "P_06FN596K5H32W6J8X4NNS4R",
"prompt": "no thats gucking dumb i want it to work with the base url? so wtf do i need to do? go into the vps and make it fucking work",
"agent_name": "codex",
"status": "completed",
"created_at": "2026-07-11T19:26:01.516159Z",
"metadata": {
"source_tree": "a91a4bd37315fa30673005970ce750755e9582be",
"git_commit": "c88206ebec78fd2abff1b971188a520ba39dedfa",
"attempt_head": "R_06FN5TJRZC3APECRM9VM2A8",
"attempt_head_kind": "proposal"
}
}
@@ -1,16 +0,0 @@
{
"id": "P_06FN5E0MF7G4PQ12VZTMB9G",
"task_id": "T_06FN3MBF98GWD4NA5PA1RWG",
"attempt_id": "AT_06FN3MBF98FBDSE1BZDP5DG",
"state_id": "P_06FN5E0MF7G4PQ12VZTMB9G",
"prompt": "idk what this is, im going to disable the actions thing on our instance we dont have the server resources for i\n\nAttachment: /var/folders/58/b6yn15tx3jb7_6lw9m67f89m0000gn/T/TemporaryItems/NSIRD_screencaptureui_4Rixx8/Screenshot 2026-07-11 at 12.45.33PM.png",
"agent_name": "codex",
"status": "completed",
"created_at": "2026-07-11T19:47:03.417913Z",
"metadata": {
"source_tree": "52869fbc4277ef5d2db2794c6d7867d6c23c423c",
"git_commit": "c77bf19172e2fed26b577183282c27ce341c80d4",
"attempt_head": "R_06FN5TJRZC3APECRM9VM2A8",
"attempt_head_kind": "proposal"
}
}
File diff suppressed because one or more lines are too long
@@ -1,17 +0,0 @@
{
"id": "P_06FN5JNW8825596P3JQACJG",
"task_id": "T_06FN3MBF98GWD4NA5PA1RWG",
"attempt_id": "AT_06FN5J8NBKM5ES4ZMD5Y1CR",
"state_id": "P_06FN5JNW8825596P3JQACJG",
"prompt": "\"Hop correctly detected that this long-running tasks original workspace was based on an older accepted tree. It has created a reconciliation workspace; Im preserving the current product code and applying only the README/wiki cleanup there, then Ill revalidate and land it automatically.\" WHY DOES THIS HAPPEN WITH EVERY SINGLE FUCKING REQUEST?",
"agent_name": "codex",
"status": "accepted",
"response_summary": "Reconcile concise README with current accepted product",
"created_at": "2026-07-11T20:07:26.01873Z",
"metadata": {
"source_tree": "2c8e849cbf49c422470f5857fda59a1aaf9ac2eb",
"git_commit": "240d7927e029adfc01c0a83622a8720963a3cbec",
"attempt_head": "R_06FN5JXX64P7BMT8Q98HHE8",
"attempt_head_kind": "proposal"
}
}
@@ -1,17 +0,0 @@
{
"id": "P_06FN5M2868H466RPTMYQ3B0",
"task_id": "T_06FN3MBF98GWD4NA5PA1RWG",
"attempt_id": "AT_06FN3MBF98FBDSE1BZDP5DG",
"state_id": "P_06FN5M2868H466RPTMYQ3B0",
"prompt": "ok please fix it",
"agent_name": "codex",
"status": "completed",
"response_summary": "Roll completed agent sessions onto the latest accepted state",
"created_at": "2026-07-11T20:13:29.522241Z",
"metadata": {
"source_tree": "4dae249394e559de9d1d86b76b5bcb1b551cc109",
"git_commit": "4e5aa7120134cb7a017685e50c3ec8d1534ad816",
"attempt_head": "R_06FN5TJRZC3APECRM9VM2A8",
"attempt_head_kind": "proposal"
}
}
@@ -1,17 +0,0 @@
{
"id": "P_06FN63779989R5PFV1QF8TR",
"task_id": "T_06FN637799RW5Q7WH9H6PJR",
"attempt_id": "AT_06FN63779BJRBK1VFQ94GRR",
"state_id": "P_06FN63779989R5PFV1QF8TR",
"prompt": "does our installer install the skill in .agents so it works with any agent? and, why is the readme so specific to codex? it should work with any agent",
"agent_name": "codex",
"status": "accepted",
"response_summary": "Install portable agent-neutral Hop skill and documentation",
"created_at": "2026-07-11T21:19:42.41027Z",
"metadata": {
"source_tree": "fcc3f3c78f5bbf6fae43651a99019e833bd88530",
"git_commit": "2f00b2dd3f8bc5e4c261563a9ac063d1716f0d47",
"attempt_head": "R_06FN69VET1STZNKPWK913K8",
"attempt_head_kind": "proposal"
}
}
@@ -1,17 +0,0 @@
{
"id": "P_06FN6BNGRJKMV6THWMBFVM8",
"task_id": "T_06FN6BNGRHYXR3WCQBDNVNR",
"attempt_id": "AT_06FN6BNGRKX5FS7MJ6VN8Z8",
"state_id": "P_06FN6BNGRJKMV6THWMBFVM8",
"prompt": "we will do MIT license, and, do I just post the token here or what?",
"agent_name": "codex",
"status": "accepted",
"response_summary": "License Hop under MIT",
"created_at": "2026-07-11T21:56:36.676853Z",
"metadata": {
"source_tree": "8423d5eeffbb7d7e3b416139417c06a552beca7d",
"git_commit": "68d8bf2f7d4e064ba21fb6dcd4fb72b01fd8722f",
"attempt_head": "R_06FN6C531M6TX6KCGEMG4E0",
"attempt_head_kind": "proposal"
}
}
@@ -1,17 +0,0 @@
{
"id": "P_06FN6CFVXS0Q22MHZRR17PR",
"task_id": "T_06FN6CFVXVVAP1KZAJ2X62R",
"attempt_id": "AT_06FN6CFVXTRZ8ZDG28QR5Z0",
"state_id": "P_06FN6CFVXS0Q22MHZRR17PR",
"prompt": "before we do that, i think we should set up hop to push after every commit, my logic is taking the most steps out of the users need to do manually as possible, so having it auto push would alliviate manual work",
"agent_name": "codex",
"status": "accepted",
"response_summary": "Automatically push every accepted Hop transition",
"created_at": "2026-07-11T22:00:12.526502Z",
"metadata": {
"source_tree": "996e9d600f0b31af0db77ff6794370758544a417",
"git_commit": "9a3a6d152b93df531cc21753e5564045db4b75b2",
"attempt_head": "R_06FN6FDTK9G17RXFFT82ZYR",
"attempt_head_kind": "proposal"
}
}
@@ -1,16 +0,0 @@
{
"id": "P_06FN6GZKFM9KAYSX7A6TXP8",
"task_id": "T_06FN6GZKFMRV2HP5N7XMNSR",
"attempt_id": "AT_06FN6GZKFP22ZCFRXGB7V00",
"state_id": "P_06FN6GZKFM9KAYSX7A6TXP8",
"prompt": "lets finish what we were doing then investigate that, i have added the key to the keychain",
"agent_name": "codex",
"status": "accepted",
"created_at": "2026-07-11T22:19:50.013492Z",
"metadata": {
"source_tree": "3b5d48444b5e5290839774d383a7498d0190e708",
"git_commit": "7139b9e1a7e52e8748c130dc366472c9f10cb04e",
"attempt_head": "R_06FN6KJWAY5XWVMCVQHCXKR",
"attempt_head_kind": "proposal"
}
}
@@ -1,17 +0,0 @@
{
"id": "P_06FN6JQ4DBZHXABYYCA61E8",
"task_id": "T_06FN6GZKFMRV2HP5N7XMNSR",
"attempt_id": "AT_06FN6GZKFP22ZCFRXGB7V00",
"state_id": "P_06FN6JQ4DBZHXABYYCA61E8",
"prompt": "why is it creating tokens? is there something in hop encouraging that behavior? i dont like that",
"agent_name": "codex",
"status": "accepted",
"response_summary": "Require user-provisioned release credentials",
"created_at": "2026-07-11T22:27:24.906795Z",
"metadata": {
"source_tree": "3b5d48444b5e5290839774d383a7498d0190e708",
"git_commit": "531775df8be5e87e57832473fc6a7ad8342b9b91",
"attempt_head": "R_06FN6KJWAY5XWVMCVQHCXKR",
"attempt_head_kind": "proposal"
}
}
@@ -1,17 +0,0 @@
{
"id": "P_06FN72497T1AVJAQC7X7860",
"task_id": "T_06FN6ZVNS25H7V4BRG743PG",
"attempt_id": "AT_06FN6ZVNS0W32KK9YQCRG88",
"state_id": "P_06FN72497T1AVJAQC7X7860",
"prompt": "we dont need to show internal agent instructions at all. also why do all the agent responses say \"Roll completed agent sessions onto the latest accepted state\"",
"agent_name": "codex",
"status": "proposed",
"response_summary": "Publish only user prompts with their own proposal outcomes",
"created_at": "2026-07-11T23:34:44.798223Z",
"metadata": {
"source_tree": "a7f12a311a193987f0691cd61e7d7efd313a61d0",
"git_commit": "61fef5fbb2288dfeb5fc02675644f2780b9305d4",
"attempt_head": "C_06FN72ADQRWFSQ8TP9Q9SY0",
"attempt_head_kind": "checkpoint"
}
}
@@ -1,17 +0,0 @@
{
"id": "P_06FN83DN7R0B9X5TEV3YDC0",
"task_id": "T_06FN83DN7SMZB9WJRZNZMTR",
"attempt_id": "AT_06FN83DN7VHVF5BVW6DEJ8R",
"state_id": "P_06FN83DN7R0B9X5TEV3YDC0",
"prompt": "# Files mentioned by the user:\n\n## Screenshot 2026-07-11 at 6.58.29PM.png: /var/folders/58/b6yn15tx3jb7_6lw9m67f89m0000gn/T/TemporaryItems/NSIRD_screencaptureui_vyA0oe/Screenshot 2026-07-11 at 6.58.29PM.png\n\n## My request for Codex:\nonce again, there are prompts here that arent mine!",
"agent_name": "codex",
"status": "proposed",
"response_summary": "Purge internal reconciliation prompts from portable records",
"created_at": "2026-07-12T02:00:12.350322Z",
"metadata": {
"source_tree": "27e94b6d19eb7e4107b127e3cec0ca41718053dc",
"git_commit": "74ba06de917236754f6146aca0e2d6e10438e455",
"attempt_head": "C_06FN83MVSDWQH772HR663SR",
"attempt_head_kind": "checkpoint"
}
}
@@ -1,17 +0,0 @@
{
"id": "P_06FN892CVDG36AN981MGF00",
"task_id": "T_06FN892CVE210TZR2ZQQ9JG",
"attempt_id": "AT_06FN892CVDGHFG3F889Q3YR",
"state_id": "P_06FN892CVDG36AN981MGF00",
"prompt": "implement that",
"agent_name": "codex",
"status": "proposed",
"response_summary": "Attribute accepted commits to the prompting user's configured Git identity",
"created_at": "2026-07-12T02:24:52.955741Z",
"metadata": {
"source_tree": "5895f21f33fd3d6d04b764c84dfeb6e0c4738c1f",
"git_commit": "787acb2330d8662320477d022da37ee4029697ac",
"attempt_head": "C_06FN89NDDXYFZK3XRRBQPVR",
"attempt_head_kind": "checkpoint"
}
}
@@ -1,17 +0,0 @@
{
"id": "P_06FN8AWMR1EMWPB5787YS60",
"task_id": "T_06FN8AWMR0HY1RJY8AN2A3R",
"attempt_id": "AT_06FN8AWMR0WHBQ7ZMDZ0NWG",
"state_id": "P_06FN8AWMR1EMWPB5787YS60",
"prompt": "HOP IS SUPPOSED TO INTELLIGENTLY RECONCILE THIS!",
"agent_name": "codex",
"status": "proposed",
"response_summary": "Reconcile compatible remote advancement before publishing accepted states",
"created_at": "2026-07-12T02:32:50.112254Z",
"metadata": {
"source_tree": "b24e0fc4185ee2df0b827cbcddf7712e8f14ebff",
"git_commit": "59b8ca7c05edbdd50535a61ae9ca732516ce7f31",
"attempt_head": "C_06FN8BEYCESTJ4ZDR0XRNK0",
"attempt_head_kind": "checkpoint"
}
}
@@ -1,17 +0,0 @@
{
"id": "P_06FN8C4PMD47GH10ZFQY2VR",
"task_id": "T_06FN8C4PMFJRT8APV4G03Y8",
"attempt_id": "AT_06FN8C4PMFRJEE4B7KKQ740",
"state_id": "P_06FN8C4PMD47GH10ZFQY2VR",
"prompt": "\"I couldnt safely start the repository change because Hop prompt capture timed out. Per the repository workflow, I stopped before inspecting or modifying files. No changes were made.\" what the fuck is this bullshit",
"agent_name": "codex",
"status": "proposed",
"response_summary": "Make prompt capture fast and resilient",
"created_at": "2026-07-12T02:38:18.275042Z",
"metadata": {
"source_tree": "7f3f889e96a317c157eedf4f5285af835865f6fb",
"git_commit": "ff62e89596a85ff3073ea348919b8d2b7125069f",
"attempt_head": "C_06FN8CQ0ZNZ60MRZ818MB9G",
"attempt_head_kind": "checkpoint"
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"prompt_ids": [
"P_06FN3SFFTFGNMTA6219GNX0",
"P_06FN5WHD61PV5ZABVFXDXS8",
"P_06FN6RMF4VH728C9Q6GTQNR",
"P_06FN6VQSN9P8R8NGASJQG7R",
"P_06FN6XEKNYR6BP9GQF78MVG",
"P_06FN6ZVNS3EYMSG7D7J17E8"
]
}
+2 -2
View File
@@ -218,7 +218,7 @@ func RunCLIWithInput(args []string, stdin io.Reader, stdout, stderr io.Writer) i
case "export":
fs := flag.NewFlagSet("export", flag.ContinueOnError)
fs.SetOutput(stderr)
output := fs.String("output", "", "write the portable prompt ledger beneath this directory")
output := fs.String("output", "", "write a private local prompt export beneath this directory")
if err := fs.Parse(commandArgs); err != nil || len(fs.Args()) != 0 {
if err == nil {
fmt.Fprintln(stderr, "usage: hop export [--output PATH]")
@@ -235,7 +235,7 @@ func RunCLIWithInput(args []string, stdin io.Reader, stdout, stderr io.Writer) i
if root == "" {
root = service.Root
}
fmt.Fprintf(stdout, "Exported %d prompt records to %s\n", len(ledger.Prompts), filepath.Join(root, ".hop", "records", "prompts"))
fmt.Fprintf(stdout, "Exported %d private local prompt records to %s\n", len(ledger.Prompts), filepath.Join(root, ".hop", "records", "prompts"))
}
case "check":
stateID, argv, ok := splitCommand(commandArgs)
+13 -12
View File
@@ -109,8 +109,8 @@ func EnsureRepository(path string) (*Repository, error) {
return NewGitStore().Ensure(context.Background(), path)
}
// OpenRepository opens the repository containing path and keeps Hop's local
// runtime private while leaving its portable record ledger versionable.
// OpenRepository opens the repository containing path and keeps all Hop state,
// including prompt exports, private to the local machine.
func OpenRepository(path string) (*Repository, error) {
return NewGitStore().Open(context.Background(), path)
}
@@ -254,9 +254,8 @@ func (r *Repository) GitDir() string { return r.gitDir }
// worktrees.
func (r *Repository) CommonGitDir() string { return r.commonGitDir }
// EnsureHopExcluded keeps the SQLite cache and disposable workspaces private
// without excluding .hop/records. The records directory is intentionally
// versionable so a repository can carry Hop's portable causal history.
// EnsureHopExcluded keeps the entire .hop directory private. Prompt exports can
// contain user requests and machine paths, so they must never enter snapshots.
func (r *Repository) EnsureHopExcluded() error {
infoDir := filepath.Join(r.commonGitDir, "info")
if err := os.MkdirAll(infoDir, 0o755); err != nil {
@@ -269,18 +268,20 @@ func (r *Repository) EnsureHopExcluded() error {
}
lines := make([]string, 0)
for _, line := range strings.Split(string(contents), "\n") {
if strings.TrimSuffix(line, "\r") == ".hop/" {
continue // Migrate the legacy blanket exclusion.
normalized := strings.TrimSuffix(line, "\r")
switch normalized {
case ".hop/", ".hop/*", "!.hop/records/", "!.hop/records/**",
"# Hop local runtime; .hop/records is intentionally versioned.",
"# Hop private local runtime and prompt records.":
continue
}
lines = append(lines, line)
}
updated := strings.Join(lines, "\n")
if !strings.Contains(updated, ".hop/*\n") {
if updated != "" && !strings.HasSuffix(updated, "\n") {
updated += "\n"
}
updated += "# Hop local runtime; .hop/records is intentionally versioned.\n.hop/*\n!.hop/records/\n!.hop/records/**\n"
if updated != "" && !strings.HasSuffix(updated, "\n") {
updated += "\n"
}
updated += "# Hop private local runtime and prompt records.\n.hop/\n"
if updated == string(contents) {
return nil
}
+5 -7
View File
@@ -10,9 +10,8 @@ import (
"time"
)
// PortablePromptLedger is the versioned, repository-safe subset of Hop's
// local state. It deliberately excludes SQLite, workspace paths, check output,
// and every other machine-local runtime detail.
// PortablePromptLedger is an explicit local export of prompt state. It is kept
// beneath the ignored .hop directory because prompts and metadata can be private.
type PortablePromptLedger struct {
SchemaVersion int `json:"schema_version"`
GeneratedAt time.Time `json:"generated_at"`
@@ -52,9 +51,8 @@ type suppressedPromptManifest struct {
PromptIDs []string `json:"prompt_ids"`
}
// ExportPromptLedger writes immutable prompt files beneath
// .hop/records/prompts. Passing attemptIDs restricts the export to those
// attempts, which keeps concurrent proposals from writing the same files.
// ExportPromptLedger writes local prompt files beneath .hop/records/prompts.
// Passing attemptIDs restricts the export to those attempts.
func (s *Service) ExportPromptLedger(ctx context.Context, destinationRoot string, attemptIDs ...string) (PortablePromptLedger, error) {
return s.exportPromptLedger(ctx, destinationRoot, promptExportOptions{AttemptIDs: attemptIDs, PublishableOnly: true})
}
@@ -199,7 +197,7 @@ func (s *Service) exportPromptLedger(ctx context.Context, destinationRoot string
}
if err := os.Rename(temporaryPath, outputPath); err != nil {
_ = os.Remove(temporaryPath)
return PortablePromptLedger{}, fmt.Errorf("publish prompt record %s: %w", record.ID, err)
return PortablePromptLedger{}, fmt.Errorf("write prompt record %s: %w", record.ID, err)
}
}
return ledger, nil
+1 -8
View File
@@ -34,9 +34,7 @@ func InitProject(ctx context.Context, path string) (*Service, State, error) {
return nil, State{}, err
}
for _, trackedPath := range trackedHopPaths {
if !strings.HasPrefix(filepath.ToSlash(trackedPath), ".hop/records/") {
return nil, State{}, fmt.Errorf("cannot initialize Hop: local .hop runtime path is already tracked (for example %s)", trackedPath)
}
return nil, State{}, fmt.Errorf("cannot initialize Hop: private .hop path is already tracked (for example %s)", trackedPath)
}
hopDir := filepath.Join(root, ".hop")
if err := os.MkdirAll(filepath.Join(hopDir, "workspaces"), 0o755); err != nil {
@@ -600,11 +598,6 @@ func (s *Service) Propose(ctx context.Context, stateID, summary string) (Proposa
return ProposalResult{}, fmt.Errorf("reconciliation must pass hop check on the resolved tree before proposing")
}
}
if _, err := s.exportPromptLedger(ctx, attempt.Workspace, promptExportOptions{
AttemptIDs: []string{attempt.ID}, Status: "proposed", ResponseSummary: summary,
}); err != nil {
return ProposalResult{}, err
}
commit, tree, err := workspaceRepo.Snapshot(ctx, "hop: proposal\n")
if err != nil {
return ProposalResult{}, err
+15 -29
View File
@@ -73,7 +73,7 @@ func TestInitRefusesAUserTrackedHopDirectory(t *testing.T) {
writeTestFile(t, filepath.Join(root, ".hop", "user-owned.txt"), "do not overwrite\n")
runGitTest(t, root, "add", "-f", ".hop/user-owned.txt")
_, _, err := InitProject(context.Background(), root)
if err == nil || !strings.Contains(err.Error(), "local .hop runtime path is already tracked") {
if err == nil || !strings.Contains(err.Error(), "private .hop path is already tracked") {
t.Fatalf("InitProject error = %v, want tracked local-runtime refusal", err)
}
contents, readErr := os.ReadFile(filepath.Join(root, ".hop", "user-owned.txt"))
@@ -82,24 +82,15 @@ func TestInitRefusesAUserTrackedHopDirectory(t *testing.T) {
}
}
func TestInitAllowsTrackedPortableHopRecords(t *testing.T) {
func TestInitRefusesTrackedPromptRecords(t *testing.T) {
root := t.TempDir()
runGitTest(t, root, "init", "--quiet")
writeTestFile(t, filepath.Join(root, ".hop", "records", "prompts.json"), `{"schema_version":1,"prompts":[]}`+"\n")
runGitTest(t, root, "add", "-f", ".hop/records/prompts.json")
service, _, err := InitProject(context.Background(), root)
if err != nil {
t.Fatalf("InitProject error = %v", err)
}
t.Cleanup(func() { _ = service.Close() })
exclude, err := os.ReadFile(filepath.Join(root, ".git", "info", "exclude"))
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(exclude), "\n.hop/\n") || !strings.Contains(string(exclude), "!.hop/records/**") {
t.Fatalf("portable ledger is not allowed by exclude file:\n%s", exclude)
_, _, err := InitProject(context.Background(), root)
if err == nil || !strings.Contains(err.Error(), "private .hop path is already tracked") {
t.Fatalf("InitProject error = %v, want tracked private-path refusal", err)
}
}
@@ -228,20 +219,23 @@ func TestConcurrentInitInExistingGitRepository(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if count := strings.Count(string(exclude), ".hop/*\n"); count != 1 {
t.Fatalf("concurrent initialization wrote .hop/* exclusion %d times", count)
if count := strings.Count(string(exclude), ".hop/\n"); count != 1 {
t.Fatalf("concurrent initialization wrote .hop/ exclusion %d times", count)
}
if strings.Contains(string(exclude), "!.hop/records") {
t.Fatalf("concurrent initialization exposed prompt records:\n%s", exclude)
}
}
func TestProposeExportsPortablePromptLedger(t *testing.T) {
func TestProposeDoesNotPublishPromptLedger(t *testing.T) {
ctx := context.Background()
service, _ := newTestProject(t, map[string]string{"base.txt": "base\n"})
result, err := service.CreatePrompt(ctx, "Publish a portable prompt ledger", "", "codex")
result, err := service.CreatePrompt(ctx, "Keep this prompt private", "", "codex")
if err != nil {
t.Fatal(err)
}
writeTestFile(t, filepath.Join(result.Workspace, "feature.txt"), "done\n")
proposal, err := service.Propose(ctx, result.Prompt.ID, "Publish the ledger")
proposal, err := service.Propose(ctx, result.Prompt.ID, "Land without publishing the prompt")
if err != nil {
t.Fatal(err)
}
@@ -251,16 +245,8 @@ func TestProposeExportsPortablePromptLedger(t *testing.T) {
t.Fatal(err)
}
t.Cleanup(func() { _ = service.Repo.RemoveWorktree(ctx, materialized, true) })
contents, err := os.ReadFile(filepath.Join(materialized, ".hop", "records", "prompts", result.Prompt.ID+".json"))
if err != nil {
t.Fatal(err)
}
var record PortablePromptRecord
if err := json.Unmarshal(contents, &record); err != nil {
t.Fatal(err)
}
if record.ID != result.Prompt.ID || record.Prompt != "Publish a portable prompt ledger" || record.Status != "proposed" || record.ResponseSummary != "Publish the ledger" {
t.Fatalf("unexpected portable prompt record: %#v", record)
if _, err := os.Stat(filepath.Join(materialized, ".hop")); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("proposal published private .hop content: %v", err)
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ release.
|---|---|
| `hop accept PROPOSAL [-- COMMAND...]` | Accept internally without changing visible files |
| `hop sync` | Materialize the current accepted tree from a safe accepted ancestor |
| `hop export [--output PATH]` | Write immutable, repository-safe prompt records to `.hop/records/prompts/` |
| `hop export [--output PATH]` | Write a private local prompt export to ignored `.hop/records/prompts/` |
| `hop push` | Retry publishing the current accepted commit to its inferred upstream |
| `hop undo` | Create a forward-only acceptance that restores the previous accepted tree |
| `hop doctor [--repair]` | Validate database/object/ref consistency |