| @@ -2,7 +2,10 @@ node_modules/ | ||
| *.log | ||
| .DS_Store | ||
| - | # Local redaction-decision cache (hashes + actions only, never secrets). | |
| - | # Kept out of git because it's per-machine resolution state, not shareable | |
| - | # lineage. The shareable companion - .treetrace/tree.json - IS committed. | |
| .treetrace/redactions.json | ||
| + | ||
| + | /.treetrace/ | |
| + | /PROMPT_TREE.md | |
| + | /TREETRACE_REPORT.md | |
| + | ||
| + | _ops/ |
| @@ -1,21 +1,661 @@ | ||
| - | MIT License | |
| - | ||
| - | Copyright (c) 2026 Zion Boggan | |
| - | ||
| - | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| - | of this software and associated documentation files (the "Software"), to deal | |
| - | in the Software without restriction, including without limitation the rights | |
| - | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| - | copies of the Software, and to permit persons to whom the Software is | |
| - | furnished to do so, subject to the following conditions: | |
| - | ||
| - | The above copyright notice and this permission notice shall be included in all | |
| - | copies or substantial portions of the Software. | |
| - | ||
| - | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| - | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| - | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| - | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| - | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| - | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| - | SOFTWARE. | |
| + | GNU AFFERO GENERAL PUBLIC LICENSE | |
| + | Version 3, 19 November 2007 | |
| + | ||
| + | Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> | |
| + | Everyone is permitted to copy and distribute verbatim copies | |
| + | of this license document, but changing it is not allowed. | |
| + | ||
| + | Preamble | |
| + | ||
| + | The GNU Affero General Public License is a free, copyleft license for | |
| + | software and other kinds of works, specifically designed to ensure | |
| + | cooperation with the community in the case of network server software. | |
| + | ||
| + | The licenses for most software and other practical works are designed | |
| + | to take away your freedom to share and change the works. By contrast, | |
| + | our General Public Licenses are intended to guarantee your freedom to | |
| + | share and change all versions of a program--to make sure it remains free | |
| + | software for all its users. | |
| + | ||
| + | When we speak of free software, we are referring to freedom, not | |
| + | price. Our General Public Licenses are designed to make sure that you | |
| + | have the freedom to distribute copies of free software (and charge for | |
| + | them if you wish), that you receive source code or can get it if you | |
| + | want it, that you can change the software or use pieces of it in new | |
| + | free programs, and that you know you can do these things. | |
| + | ||
| + | Developers that use our General Public Licenses protect your rights | |
| + | with two steps: (1) assert copyright on the software, and (2) offer | |
| + | you this License which gives you legal permission to copy, distribute | |
| + | and/or modify the software. | |
| + | ||
| + | A secondary benefit of defending all users' freedom is that | |
| + | improvements made in alternate versions of the program, if they | |
| + | receive widespread use, become available for other developers to | |
| + | incorporate. Many developers of free software are heartened and | |
| + | encouraged by the resulting cooperation. However, in the case of | |
| + | software used on network servers, this result may fail to come about. | |
| + | The GNU General Public License permits making a modified version and | |
| + | letting the public access it on a server without ever releasing its | |
| + | source code to the public. | |
| + | ||
| + | The GNU Affero General Public License is designed specifically to | |
| + | ensure that, in such cases, the modified source code becomes available | |
| + | to the community. It requires the operator of a network server to | |
| + | provide the source code of the modified version running there to the | |
| + | users of that server. Therefore, public use of a modified version, on | |
| + | a publicly accessible server, gives the public access to the source | |
| + | code of the modified version. | |
| + | ||
| + | An older license, called the Affero General Public License and | |
| + | published by Affero, was designed to accomplish similar goals. This is | |
| + | a different license, not a version of the Affero GPL, but Affero has | |
| + | released a new version of the Affero GPL which permits relicensing under | |
| + | this license. | |
| + | ||
| + | The precise terms and conditions for copying, distribution and | |
| + | modification follow. | |
| + | ||
| + | TERMS AND CONDITIONS | |
| + | ||
| + | 0. Definitions. | |
| + | ||
| + | "This License" refers to version 3 of the GNU Affero General Public License. | |
| + | ||
| + | "Copyright" also means copyright-like laws that apply to other kinds of | |
| + | works, such as semiconductor masks. | |
| + | ||
| + | "The Program" refers to any copyrightable work licensed under this | |
| + | License. Each licensee is addressed as "you". "Licensees" and | |
| + | "recipients" may be individuals or organizations. | |
| + | ||
| + | To "modify" a work means to copy from or adapt all or part of the work | |
| + | in a fashion requiring copyright permission, other than the making of an | |
| + | exact copy. The resulting work is called a "modified version" of the | |
| + | earlier work or a work "based on" the earlier work. | |
| + | ||
| + | A "covered work" means either the unmodified Program or a work based | |
| + | on the Program. | |
| + | ||
| + | To "propagate" a work means to do anything with it that, without | |
| + | permission, would make you directly or secondarily liable for | |
| + | infringement under applicable copyright law, except executing it on a | |
| + | computer or modifying a private copy. Propagation includes copying, | |
| + | distribution (with or without modification), making available to the | |
| + | public, and in some countries other activities as well. | |
| + | ||
| + | To "convey" a work means any kind of propagation that enables other | |
| + | parties to make or receive copies. Mere interaction with a user through | |
| + | a computer network, with no transfer of a copy, is not conveying. | |
| + | ||
| + | An interactive user interface displays "Appropriate Legal Notices" | |
| + | to the extent that it includes a convenient and prominently visible | |
| + | feature that (1) displays an appropriate copyright notice, and (2) | |
| + | tells the user that there is no warranty for the work (except to the | |
| + | extent that warranties are provided), that licensees may convey the | |
| + | work under this License, and how to view a copy of this License. If | |
| + | the interface presents a list of user commands or options, such as a | |
| + | menu, a prominent item in the list meets this criterion. | |
| + | ||
| + | 1. Source Code. | |
| + | ||
| + | The "source code" for a work means the preferred form of the work | |
| + | for making modifications to it. "Object code" means any non-source | |
| + | form of a work. | |
| + | ||
| + | A "Standard Interface" means an interface that either is an official | |
| + | standard defined by a recognized standards body, or, in the case of | |
| + | interfaces specified for a particular programming language, one that | |
| + | is widely used among developers working in that language. | |
| + | ||
| + | The "System Libraries" of an executable work include anything, other | |
| + | than the work as a whole, that (a) is included in the normal form of | |
| + | packaging a Major Component, but which is not part of that Major | |
| + | Component, and (b) serves only to enable use of the work with that | |
| + | Major Component, or to implement a Standard Interface for which an | |
| + | implementation is available to the public in source code form. A | |
| + | "Major Component", in this context, means a major essential component | |
| + | (kernel, window system, and so on) of the specific operating system | |
| + | (if any) on which the executable work runs, or a compiler used to | |
| + | produce the work, or an object code interpreter used to run it. | |
| + | ||
| + | The "Corresponding Source" for a work in object code form means all | |
| + | the source code needed to generate, install, and (for an executable | |
| + | work) run the object code and to modify the work, including scripts to | |
| + | control those activities. However, it does not include the work's | |
| + | System Libraries, or general-purpose tools or generally available free | |
| + | programs which are used unmodified in performing those activities but | |
| + | which are not part of the work. For example, Corresponding Source | |
| + | includes interface definition files associated with source files for | |
| + | the work, and the source code for shared libraries and dynamically | |
| + | linked subprograms that the work is specifically designed to require, | |
| + | such as by intimate data communication or control flow between those | |
| + | subprograms and other parts of the work. | |
| + | ||
| + | The Corresponding Source need not include anything that users | |
| + | can regenerate automatically from other parts of the Corresponding | |
| + | Source. | |
| + | ||
| + | The Corresponding Source for a work in source code form is that | |
| + | same work. | |
| + | ||
| + | 2. Basic Permissions. | |
| + | ||
| + | All rights granted under this License are granted for the term of | |
| + | copyright on the Program, and are irrevocable provided the stated | |
| + | conditions are met. This License explicitly affirms your unlimited | |
| + | permission to run the unmodified Program. The output from running a | |
| + | covered work is covered by this License only if the output, given its | |
| + | content, constitutes a covered work. This License acknowledges your | |
| + | rights of fair use or other equivalent, as provided by copyright law. | |
| + | ||
| + | You may make, run and propagate covered works that you do not | |
| + | convey, without conditions so long as your license otherwise remains | |
| + | in force. You may convey covered works to others for the sole purpose | |
| + | of having them make modifications exclusively for you, or provide you | |
| + | with facilities for running those works, provided that you comply with | |
| + | the terms of this License in conveying all material for which you do | |
| + | not control copyright. Those thus making or running the covered works | |
| + | for you must do so exclusively on your behalf, under your direction | |
| + | and control, on terms that prohibit them from making any copies of | |
| + | your copyrighted material outside their relationship with you. | |
| + | ||
| + | Conveying under any other circumstances is permitted solely under | |
| + | the conditions stated below. Sublicensing is not allowed; section 10 | |
| + | makes it unnecessary. | |
| + | ||
| + | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. | |
| + | ||
| + | No covered work shall be deemed part of an effective technological | |
| + | measure under any applicable law fulfilling obligations under article | |
| + | 11 of the WIPO copyright treaty adopted on 20 December 1996, or | |
| + | similar laws prohibiting or restricting circumvention of such | |
| + | measures. | |
| + | ||
| + | When you convey a covered work, you waive any legal power to forbid | |
| + | circumvention of technological measures to the extent such circumvention | |
| + | is effected by exercising rights under this License with respect to | |
| + | the covered work, and you disclaim any intention to limit operation or | |
| + | modification of the work as a means of enforcing, against the work's | |
| + | users, your or third parties' legal rights to forbid circumvention of | |
| + | technological measures. | |
| + | ||
| + | 4. Conveying Verbatim Copies. | |
| + | ||
| + | You may convey verbatim copies of the Program's source code as you | |
| + | receive it, in any medium, provided that you conspicuously and | |
| + | appropriately publish on each copy an appropriate copyright notice; | |
| + | keep intact all notices stating that this License and any | |
| + | non-permissive terms added in accord with section 7 apply to the code; | |
| + | keep intact all notices of the absence of any warranty; and give all | |
| + | recipients a copy of this License along with the Program. | |
| + | ||
| + | You may charge any price or no price for each copy that you convey, | |
| + | and you may offer support or warranty protection for a fee. | |
| + | ||
| + | 5. Conveying Modified Source Versions. | |
| + | ||
| + | You may convey a work based on the Program, or the modifications to | |
| + | produce it from the Program, in the form of source code under the | |
| + | terms of section 4, provided that you also meet all of these conditions: | |
| + | ||
| + | a) The work must carry prominent notices stating that you modified | |
| + | it, and giving a relevant date. | |
| + | ||
| + | b) The work must carry prominent notices stating that it is | |
| + | released under this License and any conditions added under section | |
| + | 7. This requirement modifies the requirement in section 4 to | |
| + | "keep intact all notices". | |
| + | ||
| + | c) You must license the entire work, as a whole, under this | |
| + | License to anyone who comes into possession of a copy. This | |
| + | License will therefore apply, along with any applicable section 7 | |
| + | additional terms, to the whole of the work, and all its parts, | |
| + | regardless of how they are packaged. This License gives no | |
| + | permission to license the work in any other way, but it does not | |
| + | invalidate such permission if you have separately received it. | |
| + | ||
| + | d) If the work has interactive user interfaces, each must display | |
| + | Appropriate Legal Notices; however, if the Program has interactive | |
| + | interfaces that do not display Appropriate Legal Notices, your | |
| + | work need not make them do so. | |
| + | ||
| + | A compilation of a covered work with other separate and independent | |
| + | works, which are not by their nature extensions of the covered work, | |
| + | and which are not combined with it such as to form a larger program, | |
| + | in or on a volume of a storage or distribution medium, is called an | |
| + | "aggregate" if the compilation and its resulting copyright are not | |
| + | used to limit the access or legal rights of the compilation's users | |
| + | beyond what the individual works permit. Inclusion of a covered work | |
| + | in an aggregate does not cause this License to apply to the other | |
| + | parts of the aggregate. | |
| + | ||
| + | 6. Conveying Non-Source Forms. | |
| + | ||
| + | You may convey a covered work in object code form under the terms | |
| + | of sections 4 and 5, provided that you also convey the | |
| + | machine-readable Corresponding Source under the terms of this License, | |
| + | in one of these ways: | |
| + | ||
| + | a) Convey the object code in, or embodied in, a physical product | |
| + | (including a physical distribution medium), accompanied by the | |
| + | Corresponding Source fixed on a durable physical medium | |
| + | customarily used for software interchange. | |
| + | ||
| + | b) Convey the object code in, or embodied in, a physical product | |
| + | (including a physical distribution medium), accompanied by a | |
| + | written offer, valid for at least three years and valid for as | |
| + | long as you offer spare parts or customer support for that product | |
| + | model, to give anyone who possesses the object code either (1) a | |
| + | copy of the Corresponding Source for all the software in the | |
| + | product that is covered by this License, on a durable physical | |
| + | medium customarily used for software interchange, for a price no | |
| + | more than your reasonable cost of physically performing this | |
| + | conveying of source, or (2) access to copy the | |
| + | Corresponding Source from a network server at no charge. | |
| + | ||
| + | c) Convey individual copies of the object code with a copy of the | |
| + | written offer to provide the Corresponding Source. This | |
| + | alternative is allowed only occasionally and noncommercially, and | |
| + | only if you received the object code with such an offer, in accord | |
| + | with subsection 6b. | |
| + | ||
| + | d) Convey the object code by offering access from a designated | |
| + | place (gratis or for a charge), and offer equivalent access to the | |
| + | Corresponding Source in the same way through the same place at no | |
| + | further charge. You need not require recipients to copy the | |
| + | Corresponding Source along with the object code. If the place to | |
| + | copy the object code is a network server, the Corresponding Source | |
| + | may be on a different server (operated by you or a third party) | |
| + | that supports equivalent copying facilities, provided you maintain | |
| + | clear directions next to the object code saying where to find the | |
| + | Corresponding Source. Regardless of what server hosts the | |
| + | Corresponding Source, you remain obligated to ensure that it is | |
| + | available for as long as needed to satisfy these requirements. | |
| + | ||
| + | e) Convey the object code using peer-to-peer transmission, provided | |
| + | you inform other peers where the object code and Corresponding | |
| + | Source of the work are being offered to the general public at no | |
| + | charge under subsection 6d. | |
| + | ||
| + | A separable portion of the object code, whose source code is excluded | |
| + | from the Corresponding Source as a System Library, need not be | |
| + | included in conveying the object code work. | |
| + | ||
| + | A "User Product" is either (1) a "consumer product", which means any | |
| + | tangible personal property which is normally used for personal, family, | |
| + | or household purposes, or (2) anything designed or sold for incorporation | |
| + | into a dwelling. In determining whether a product is a consumer product, | |
| + | doubtful cases shall be resolved in favor of coverage. For a particular | |
| + | product received by a particular user, "normally used" refers to a | |
| + | typical or common use of that class of product, regardless of the status | |
| + | of the particular user or of the way in which the particular user | |
| + | actually uses, or expects or is expected to use, the product. A product | |
| + | is a consumer product regardless of whether the product has substantial | |
| + | commercial, industrial or non-consumer uses, unless such uses represent | |
| + | the only significant mode of use of the product. | |
| + | ||
| + | "Installation Information" for a User Product means any methods, | |
| + | procedures, authorization keys, or other information required to install | |
| + | and execute modified versions of a covered work in that User Product from | |
| + | a modified version of its Corresponding Source. The information must | |
| + | suffice to ensure that the continued functioning of the modified object | |
| + | code is in no case prevented or interfered with solely because | |
| + | modification has been made. | |
| + | ||
| + | If you convey an object code work under this section in, or with, or | |
| + | specifically for use in, a User Product, and the conveying occurs as | |
| + | part of a transaction in which the right of possession and use of the | |
| + | User Product is transferred to the recipient in perpetuity or for a | |
| + | fixed term (regardless of how the transaction is characterized), the | |
| + | Corresponding Source conveyed under this section must be accompanied | |
| + | by the Installation Information. But this requirement does not apply | |
| + | if neither you nor any third party retains the ability to install | |
| + | modified object code on the User Product (for example, the work has | |
| + | been installed in ROM). | |
| + | ||
| + | The requirement to provide Installation Information does not include a | |
| + | requirement to continue to provide support service, warranty, or updates | |
| + | for a work that has been modified or installed by the recipient, or for | |
| + | the User Product in which it has been modified or installed. Access to a | |
| + | network may be denied when the modification itself materially and | |
| + | adversely affects the operation of the network or violates the rules and | |
| + | protocols for communication across the network. | |
| + | ||
| + | Corresponding Source conveyed, and Installation Information provided, | |
| + | in accord with this section must be in a format that is publicly | |
| + | documented (and with an implementation available to the public in | |
| + | source code form), and must require no special password or key for | |
| + | unpacking, reading or copying. | |
| + | ||
| + | 7. Additional Terms. | |
| + | ||
| + | "Additional permissions" are terms that supplement the terms of this | |
| + | License by making exceptions from one or more of its conditions. | |
| + | Additional permissions that are applicable to the entire Program shall | |
| + | be treated as though they were included in this License, to the extent | |
| + | that they are valid under applicable law. If additional permissions | |
| + | apply only to part of the Program, that part may be used separately | |
| + | under those permissions, but the entire Program remains governed by | |
| + | this License without regard to the additional permissions. | |
| + | ||
| + | When you convey a copy of a covered work, you may at your option | |
| + | remove any additional permissions from that copy, or from any part of | |
| + | it. (Additional permissions may be written to require their own | |
| + | removal in certain cases when you modify the work.) You may place | |
| + | additional permissions on material, added by you to a covered work, | |
| + | for which you have or can give appropriate copyright permission. | |
| + | ||
| + | Notwithstanding any other provision of this License, for material you | |
| + | add to a covered work, you may (if authorized by the copyright holders of | |
| + | that material) supplement the terms of this License with terms: | |
| + | ||
| + | a) Disclaiming warranty or limiting liability differently from the | |
| + | terms of sections 15 and 16 of this License; or | |
| + | ||
| + | b) Requiring preservation of specified reasonable legal notices or | |
| + | author attributions in that material or in the Appropriate Legal | |
| + | Notices displayed by works containing it; or | |
| + | ||
| + | c) Prohibiting misrepresentation of the origin of that material, or | |
| + | requiring that modified versions of such material be marked in | |
| + | reasonable ways as different from the original version; or | |
| + | ||
| + | d) Limiting the use for publicity purposes of names of licensors or | |
| + | authors of the material; or | |
| + | ||
| + | e) Declining to grant rights under trademark law for use of some | |
| + | trade names, trademarks, or service marks; or | |
| + | ||
| + | f) Requiring indemnification of licensors and authors of that | |
| + | material by anyone who conveys the material (or modified versions of | |
| + | it) with contractual assumptions of liability to the recipient, for | |
| + | any liability that these contractual assumptions directly impose on | |
| + | those licensors and authors. | |
| + | ||
| + | All other non-permissive additional terms are considered "further | |
| + | restrictions" within the meaning of section 10. If the Program as you | |
| + | received it, or any part of it, contains a notice stating that it is | |
| + | governed by this License along with a term that is a further | |
| + | restriction, you may remove that term. If a license document contains | |
| + | a further restriction but permits relicensing or conveying under this | |
| + | License, you may add to a covered work material governed by the terms | |
| + | of that license document, provided that the further restriction does | |
| + | not survive such relicensing or conveying. | |
| + | ||
| + | If you add terms to a covered work in accord with this section, you | |
| + | must place, in the relevant source files, a statement of the | |
| + | additional terms that apply to those files, or a notice indicating | |
| + | where to find the applicable terms. | |
| + | ||
| + | Additional terms, permissive or non-permissive, may be stated in the | |
| + | form of a separately written license, or stated as exceptions; | |
| + | the above requirements apply either way. | |
| + | ||
| + | 8. Termination. | |
| + | ||
| + | You may not propagate or modify a covered work except as expressly | |
| + | provided under this License. Any attempt otherwise to propagate or | |
| + | modify it is void, and will automatically terminate your rights under | |
| + | this License (including any patent licenses granted under the third | |
| + | paragraph of section 11). | |
| + | ||
| + | However, if you cease all violation of this License, then your | |
| + | license from a particular copyright holder is reinstated (a) | |
| + | provisionally, unless and until the copyright holder explicitly and | |
| + | finally terminates your license, and (b) permanently, if the copyright | |
| + | holder fails to notify you of the violation by some reasonable means | |
| + | prior to 60 days after the cessation. | |
| + | ||
| + | Moreover, your license from a particular copyright holder is | |
| + | reinstated permanently if the copyright holder notifies you of the | |
| + | violation by some reasonable means, this is the first time you have | |
| + | received notice of violation of this License (for any work) from that | |
| + | copyright holder, and you cure the violation prior to 30 days after | |
| + | your receipt of the notice. | |
| + | ||
| + | Termination of your rights under this section does not terminate the | |
| + | licenses of parties who have received copies or rights from you under | |
| + | this License. If your rights have been terminated and not permanently | |
| + | reinstated, you do not qualify to receive new licenses for the same | |
| + | material under section 10. | |
| + | ||
| + | 9. Acceptance Not Required for Having Copies. | |
| + | ||
| + | You are not required to accept this License in order to receive or | |
| + | run a copy of the Program. Ancillary propagation of a covered work | |
| + | occurring solely as a consequence of using peer-to-peer transmission | |
| + | to receive a copy likewise does not require acceptance. However, | |
| + | nothing other than this License grants you permission to propagate or | |
| + | modify any covered work. These actions infringe copyright if you do | |
| + | not accept this License. Therefore, by modifying or propagating a | |
| + | covered work, you indicate your acceptance of this License to do so. | |
| + | ||
| + | 10. Automatic Licensing of Downstream Recipients. | |
| + | ||
| + | Each time you convey a covered work, the recipient automatically | |
| + | receives a license from the original licensors, to run, modify and | |
| + | propagate that work, subject to this License. You are not responsible | |
| + | for enforcing compliance by third parties with this License. | |
| + | ||
| + | An "entity transaction" is a transaction transferring control of an | |
| + | organization, or substantially all assets of one, or subdividing an | |
| + | organization, or merging organizations. If propagation of a covered | |
| + | work results from an entity transaction, each party to that | |
| + | transaction who receives a copy of the work also receives whatever | |
| + | licenses to the work the party's predecessor in interest had or could | |
| + | give under the previous paragraph, plus a right to possession of the | |
| + | Corresponding Source of the work from the predecessor in interest, if | |
| + | the predecessor has it or can get it with reasonable efforts. | |
| + | ||
| + | You may not impose any further restrictions on the exercise of the | |
| + | rights granted or affirmed under this License. For example, you may | |
| + | not impose a license fee, royalty, or other charge for exercise of | |
| + | rights granted under this License, and you may not initiate litigation | |
| + | (including a cross-claim or counterclaim in a lawsuit) alleging that | |
| + | any patent claim is infringed by making, using, selling, offering for | |
| + | sale, or importing the Program or any portion of it. | |
| + | ||
| + | 11. Patents. | |
| + | ||
| + | A "contributor" is a copyright holder who authorizes use under this | |
| + | License of the Program or a work on which the Program is based. The | |
| + | work thus licensed is called the contributor's "contributor version". | |
| + | ||
| + | A contributor's "essential patent claims" are all patent claims | |
| + | owned or controlled by the contributor, whether already acquired or | |
| + | hereafter acquired, that would be infringed by some manner, permitted | |
| + | by this License, of making, using, or selling its contributor version, | |
| + | but do not include claims that would be infringed only as a | |
| + | consequence of further modification of the contributor version. For | |
| + | purposes of this definition, "control" includes the right to grant | |
| + | patent sublicenses in a manner consistent with the requirements of | |
| + | this License. | |
| + | ||
| + | Each contributor grants you a non-exclusive, worldwide, royalty-free | |
| + | patent license under the contributor's essential patent claims, to | |
| + | make, use, sell, offer for sale, import and otherwise run, modify and | |
| + | propagate the contents of its contributor version. | |
| + | ||
| + | In the following three paragraphs, a "patent license" is any express | |
| + | agreement or commitment, however denominated, not to enforce a patent | |
| + | (such as an express permission to practice a patent or covenant not to | |
| + | sue for patent infringement). To "grant" such a patent license to a | |
| + | party means to make such an agreement or commitment not to enforce a | |
| + | patent against the party. | |
| + | ||
| + | If you convey a covered work, knowingly relying on a patent license, | |
| + | and the Corresponding Source of the work is not available for anyone | |
| + | to copy, free of charge and under the terms of this License, through a | |
| + | publicly available network server or other readily accessible means, | |
| + | then you must either (1) cause the Corresponding Source to be so | |
| + | available, or (2) arrange to deprive yourself of the benefit of the | |
| + | patent license for this particular work, or (3) arrange, in a manner | |
| + | consistent with the requirements of this License, to extend the patent | |
| + | license to downstream recipients. "Knowingly relying" means you have | |
| + | actual knowledge that, but for the patent license, your conveying the | |
| + | covered work in a country, or your recipient's use of the covered work | |
| + | in a country, would infringe one or more identifiable patents in that | |
| + | country that you have reason to believe are valid. | |
| + | ||
| + | If, pursuant to or in connection with a single transaction or | |
| + | arrangement, you convey, or propagate by procuring conveyance of, a | |
| + | covered work, and grant a patent license to some of the parties | |
| + | receiving the covered work authorizing them to use, propagate, modify | |
| + | or convey a specific copy of the covered work, then the patent license | |
| + | you grant is automatically extended to all recipients of the covered | |
| + | work and works based on it. | |
| + | ||
| + | A patent license is "discriminatory" if it does not include within | |
| + | the scope of its coverage, prohibits the exercise of, or is | |
| + | conditioned on the non-exercise of one or more of the rights that are | |
| + | specifically granted under this License. You may not convey a covered | |
| + | work if you are a party to an arrangement with a third party that is | |
| + | in the business of distributing software, under which you make payment | |
| + | to the third party based on the extent of your activity of conveying | |
| + | the work, and under which the third party grants, to any of the | |
| + | parties who would receive the covered work from you, a discriminatory | |
| + | patent license (a) in connection with copies of the covered work | |
| + | conveyed by you (or copies made from those copies), or (b) primarily | |
| + | for and in connection with specific products or compilations that | |
| + | contain the covered work, unless you entered into that arrangement, | |
| + | or that patent license was granted, prior to 28 March 2007. | |
| + | ||
| + | Nothing in this License shall be construed as excluding or limiting | |
| + | any implied license or other defenses to infringement that may | |
| + | otherwise be available to you under applicable patent law. | |
| + | ||
| + | 12. No Surrender of Others' Freedom. | |
| + | ||
| + | If conditions are imposed on you (whether by court order, agreement or | |
| + | otherwise) that contradict the conditions of this License, they do not | |
| + | excuse you from the conditions of this License. If you cannot convey a | |
| + | covered work so as to satisfy simultaneously your obligations under this | |
| + | License and any other pertinent obligations, then as a consequence you may | |
| + | not convey it at all. For example, if you agree to terms that obligate you | |
| + | to collect a royalty for further conveying from those to whom you convey | |
| + | the Program, the only way you could satisfy both those terms and this | |
| + | License would be to refrain entirely from conveying the Program. | |
| + | ||
| + | 13. Remote Network Interaction; Use with the GNU General Public License. | |
| + | ||
| + | Notwithstanding any other provision of this License, if you modify the | |
| + | Program, your modified version must prominently offer all users | |
| + | interacting with it remotely through a computer network (if your version | |
| + | supports such interaction) an opportunity to receive the Corresponding | |
| + | Source of your version by providing access to the Corresponding Source | |
| + | from a network server at no charge, through some standard or customary | |
| + | means of facilitating copying of software. This Corresponding Source | |
| + | shall include the Corresponding Source for any work covered by version 3 | |
| + | of the GNU General Public License that is incorporated pursuant to the | |
| + | following paragraph. | |
| + | ||
| + | Notwithstanding any other provision of this License, you have | |
| + | permission to link or combine any covered work with a work licensed | |
| + | under version 3 of the GNU General Public License into a single | |
| + | combined work, and to convey the resulting work. The terms of this | |
| + | License will continue to apply to the part which is the covered work, | |
| + | but the work with which it is combined will remain governed by version | |
| + | 3 of the GNU General Public License. | |
| + | ||
| + | 14. Revised Versions of this License. | |
| + | ||
| + | The Free Software Foundation may publish revised and/or new versions of | |
| + | the GNU Affero General Public License from time to time. Such new versions | |
| + | will be similar in spirit to the present version, but may differ in detail to | |
| + | address new problems or concerns. | |
| + | ||
| + | Each version is given a distinguishing version number. If the | |
| + | Program specifies that a certain numbered version of the GNU Affero General | |
| + | Public License "or any later version" applies to it, you have the | |
| + | option of following the terms and conditions either of that numbered | |
| + | version or of any later version published by the Free Software | |
| + | Foundation. If the Program does not specify a version number of the | |
| + | GNU Affero General Public License, you may choose any version ever published | |
| + | by the Free Software Foundation. | |
| + | ||
| + | If the Program specifies that a proxy can decide which future | |
| + | versions of the GNU Affero General Public License can be used, that proxy's | |
| + | public statement of acceptance of a version permanently authorizes you | |
| + | to choose that version for the Program. | |
| + | ||
| + | Later license versions may give you additional or different | |
| + | permissions. However, no additional obligations are imposed on any | |
| + | author or copyright holder as a result of your choosing to follow a | |
| + | later version. | |
| + | ||
| + | 15. Disclaimer of Warranty. | |
| + | ||
| + | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY | |
| + | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT | |
| + | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY | |
| + | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, | |
| + | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |
| + | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM | |
| + | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF | |
| + | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. | |
| + | ||
| + | 16. Limitation of Liability. | |
| + | ||
| + | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING | |
| + | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS | |
| + | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY | |
| + | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE | |
| + | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF | |
| + | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD | |
| + | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), | |
| + | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF | |
| + | SUCH DAMAGES. | |
| + | ||
| + | 17. Interpretation of Sections 15 and 16. | |
| + | ||
| + | If the disclaimer of warranty and limitation of liability provided | |
| + | above cannot be given local legal effect according to their terms, | |
| + | reviewing courts shall apply local law that most closely approximates | |
| + | an absolute waiver of all civil liability in connection with the | |
| + | Program, unless a warranty or assumption of liability accompanies a | |
| + | copy of the Program in return for a fee. | |
| + | ||
| + | END OF TERMS AND CONDITIONS | |
| + | ||
| + | How to Apply These Terms to Your New Programs | |
| + | ||
| + | If you develop a new program, and you want it to be of the greatest | |
| + | possible use to the public, the best way to achieve this is to make it | |
| + | free software which everyone can redistribute and change under these terms. | |
| + | ||
| + | To do so, attach the following notices to the program. It is safest | |
| + | to attach them to the start of each source file to most effectively | |
| + | state the exclusion of warranty; and each file should have at least | |
| + | the "copyright" line and a pointer to where the full notice is found. | |
| + | ||
| + | <one line to give the program's name and a brief idea of what it does.> | |
| + | Copyright (C) <year> <name of author> | |
| + | ||
| + | This program is free software: you can redistribute it and/or modify | |
| + | it under the terms of the GNU Affero General Public License as published by | |
| + | the Free Software Foundation, either version 3 of the License, or | |
| + | (at your option) any later version. | |
| + | ||
| + | This program is distributed in the hope that it will be useful, | |
| + | but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| + | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| + | GNU Affero General Public License for more details. | |
| + | ||
| + | You should have received a copy of the GNU Affero General Public License | |
| + | along with this program. If not, see <https://www.gnu.org/licenses/>. | |
| + | ||
| + | Also add information on how to contact you by electronic and paper mail. | |
| + | ||
| + | If your software can interact with users remotely through a computer | |
| + | network, you should also make sure that it provides a way for users to | |
| + | get its source. For example, if your program is a web application, its | |
| + | interface could display a "Source" link that leads users to an archive | |
| + | of the code. There are many ways you could offer source, and different | |
| + | solutions will be better for different programs; see section 13 for the | |
| + | specific requirements. | |
| + | ||
| + | You should also get your employer (if you work as a programmer) or school, | |
| + | if any, to sign a "copyright disclaimer" for the program, if necessary. | |
| + | For more information on this, and how to apply and follow the GNU AGPL, see | |
| + | <https://www.gnu.org/licenses/>. |
| @@ -54,20 +54,20 @@ TreeTrace is the local-first layer between raw chat logs, runtime traces, and co | ||
| ## Usage | ||
| - | ```bash | |
| - | npx treetrace # trace this project and write all artifacts | |
| - | npx treetrace --report # write all artifacts and print the human report | |
| - | npx treetrace --handoff # print an agent-ready continuation brief | |
| - | npx treetrace --file session.jsonl # import specific transcript(s) | |
| - | npx treetrace --stdin < chat.txt # parse pasted User:/Assistant: transcript text | |
| - | npx treetrace --failures # write and print .treetrace/failures.json | |
| - | npx treetrace --lessons # write and print .treetrace/lessons.md | |
| - | npx treetrace --evals # write and print .treetrace/evals.jsonl | |
| - | npx treetrace --memory # write and print .treetrace/agent-memory.md | |
| - | npx treetrace --titles-only # compact human tree, no full prompt details | |
| - | npx treetrace --redact-auto # redact every detected secret without prompting | |
| - | npx treetrace --since 2026-06-01 | |
| - | ``` | |
| + | | Command | What it does | | |
| + | |---------|--------------| | |
| + | | `npx treetrace` | Trace this project and write all artifacts | | |
| + | | `npx treetrace --report` | Write all artifacts and print the human report | | |
| + | | `npx treetrace --handoff` | Print an agent ready continuation brief | | |
| + | | `npx treetrace --file session.jsonl` | Import specific transcripts | | |
| + | | `npx treetrace --stdin < chat.txt` | Parse a pasted `User:` / `Assistant:` transcript | | |
| + | | `npx treetrace --failures` | Write and print `.treetrace/failures.json` | | |
| + | | `npx treetrace --lessons` | Write and print `.treetrace/lessons.md` | | |
| + | | `npx treetrace --evals` | Write and print `.treetrace/evals.jsonl` | | |
| + | | `npx treetrace --memory` | Write and print `.treetrace/agent-memory.md` | | |
| + | | `npx treetrace --titles-only` | Compact human tree, no full prompt details | | |
| + | | `npx treetrace --redact-auto` | Redact every detected secret without prompting | | |
| + | | `npx treetrace --since 2026-06-01` | Limit to sessions on or after a date | | |
| For a Termius, Codex CLI, Claude Code, or SSH session where you want the report in the terminal window, use: | ||
| @@ -151,8 +151,12 @@ The strongest identity is: | ||
| ## License | ||
| - | MIT (c) Zion Boggan | |
| + | GNU Affero General Public License v3.0 only (AGPL-3.0-only). | |
| + | ||
| + | Copyright 2026 Zion Boggan. | |
| + | ||
| + | You may use, study, share, and modify TreeTrace under the terms of the AGPL version 3. If you run a modified version as a network service, you must offer its users the corresponding source. See [LICENSE](LICENSE). | |
| --- | ||
| - | This repository ships its own [PROMPT_TREE.md](PROMPT_TREE.md), but the Markdown tree is now one artifact among several. The main product is structured, local, eval-ready knowledge about how agents fail and how humans correct them. | |
| + | See [examples/](examples/) for a full set of generated artifacts. The Markdown tree is one artifact among several: the main product is structured, local, eval-ready knowledge about how agents fail and how humans correct them. |
| @@ -9,7 +9,7 @@ inputs: | ||
| source: | ||
| description: > | ||
| Path to a transcript file (.jsonl or plain text) committed or produced in | ||
| - | CI. Session logs live on dev machines, not in CI - typical usage is | |
| + | CI. Session logs live on dev machines, not in CI, so typical usage is | |
| regenerating PROMPT_TREE.md from a committed .treetrace/tree.json or an | ||
| uploaded transcript artifact. | ||
| required: false | ||
| @@ -30,14 +30,13 @@ runs: | ||
| npx --yes treetrace --file "${{ inputs.source }}" --redact-auto --quiet | ||
| elif [ -f .treetrace/tree.json ]; then | ||
| echo "::notice::Using committed .treetrace/tree.json" | ||
| - | # re-render markdown from committed lineage (no transcripts in CI) | |
| node -e " | ||
| const { readFileSync, writeFileSync } = require('fs'); | ||
| const data = JSON.parse(readFileSync('.treetrace/tree.json','utf8')); | ||
| - | console.log('lineage present:', data.nodes.length, 'nodes - TREETRACE_REPORT.md and PROMPT_TREE.md should be committed alongside it'); | |
| + | console.log('lineage present:', data.nodes.length, 'nodes. TREETRACE_REPORT.md and PROMPT_TREE.md should be committed alongside it'); | |
| " | ||
| else | ||
| - | echo "::warning::No source transcript or .treetrace/tree.json found - nothing to do." | |
| + | echo "::warning::No source transcript or .treetrace/tree.json found, nothing to do." | |
| fi | ||
| - name: Comment on PR | ||
| if: ${{ inputs.comment-pr == 'true' && github.event_name == 'pull_request' }} |
| @@ -4,27 +4,18 @@ Generated TreeTrace outputs from the synthetic weather-dashboard fixture. | ||
| ## Weather Dashboard | ||
| - | - [weather-dashboard/PROMPT_TREE.md](weather-dashboard/PROMPT_TREE.md) - human-readable lineage | |
| - | - [weather-dashboard/TREETRACE_REPORT.md](weather-dashboard/TREETRACE_REPORT.md) - combined human-readable report | |
| - | - [weather-dashboard/tree.json](weather-dashboard/tree.json) - canonical v0.2 machine-readable lineage | |
| - | - [weather-dashboard/.treetrace/failures.json](weather-dashboard/.treetrace/failures.json) - failure signals and correction chains | |
| - | - [weather-dashboard/.treetrace/lessons.md](weather-dashboard/.treetrace/lessons.md) - lessons for future agents | |
| - | - [weather-dashboard/.treetrace/evals.jsonl](weather-dashboard/.treetrace/evals.jsonl) - eval candidates | |
| - | - [weather-dashboard/.treetrace/agent-memory.md](weather-dashboard/.treetrace/agent-memory.md) - compact memory pack | |
| + | - [weather-dashboard/PROMPT_TREE.md](weather-dashboard/PROMPT_TREE.md): human-readable lineage | |
| + | - [weather-dashboard/TREETRACE_REPORT.md](weather-dashboard/TREETRACE_REPORT.md): combined human-readable report | |
| + | - [weather-dashboard/tree.json](weather-dashboard/tree.json): canonical v0.2 machine-readable lineage | |
| + | - [weather-dashboard/.treetrace/failures.json](weather-dashboard/.treetrace/failures.json): failure signals and correction chains | |
| + | - [weather-dashboard/.treetrace/lessons.md](weather-dashboard/.treetrace/lessons.md): lessons for future agents | |
| + | - [weather-dashboard/.treetrace/evals.jsonl](weather-dashboard/.treetrace/evals.jsonl): eval candidates | |
| + | - [weather-dashboard/.treetrace/agent-memory.md](weather-dashboard/.treetrace/agent-memory.md): compact memory pack | |
| - | The root-level example files mirror the same analysis artifacts for quick inspection: | |
| - | ||
| - | - [failures.json](failures.json) | |
| - | - [lessons.md](lessons.md) | |
| - | - [evals.jsonl](evals.jsonl) | |
| - | - [agent-memory.md](agent-memory.md) | |
| - | ||
| - | Generated with: | |
| + | Reproduce with: | |
| ```bash | ||
| node bin/treetrace.js --file test/fixtures/synthetic-session.jsonl --dir examples/weather-dashboard --redact-auto --quiet | ||
| ``` | ||
| - | ## Dogfooding | |
| - | ||
| - | TreeTrace ships its own [PROMPT_TREE.md](../PROMPT_TREE.md), but the pivot makes that Markdown tree one artifact among several. The structured outputs are the main product: lineage JSON, failure analysis, eval candidates, and agent memory. | |
| + | The Markdown tree is one artifact among several. The structured outputs are the main product: lineage JSON, failure analysis, eval candidates, and agent memory. |
| @@ -2,7 +2,7 @@ | ||
| "schemaVersion": "0.2", | ||
| "project": { | ||
| "name": "weather-dashboard", | ||
| - | "generatedAt": "2026-06-12T02:37:21.277Z" | |
| + | "generatedAt": "2026-06-12T04:12:12.307Z" | |
| }, | ||
| "summary": { | ||
| "totalFailureSignals": 1, |
| @@ -7,7 +7,7 @@ | ||
| }, | ||
| "project": { | ||
| "name": "weather-dashboard", | ||
| - | "generatedAt": "2026-06-12T02:37:21.277Z", | |
| + | "generatedAt": "2026-06-12T04:12:12.307Z", | |
| "sourceType": "claude-code-jsonl" | ||
| }, | ||
| "stats": { |
| @@ -1,8 +1,8 @@ | ||
| - | # 🌳 Prompt Tree - weather-dashboard | |
| + | # 🌳 Prompt Tree: weather-dashboard | |
| > **4 prompts** · **1 session** · **1 day** · 1 correction · 1 scope change · 2 tool calls · 1 file touched | ||
| > | ||
| - | > The prompt lineage that built this project - extracted from real sessions, curated and redacted by the author, generated by [treetrace](https://github.com/REPLACE-ME-ORG/treetrace). | |
| + | > The prompt lineage that built this project, extracted from real sessions, curated and redacted by the author, generated by [treetrace](https://github.com/REPLACE-ME-ORG/treetrace). | |
| ## Goal | ||
| @@ -37,7 +37,7 @@ | ||
| ## Reusable Prompt Pack | ||
| - | A distilled, replayable version of the accepted path - paste into a fresh agent to rebuild something like this: | |
| + | A distilled, replayable version of the accepted path. Paste into a fresh agent to rebuild something like this: | |
| ```text | ||
| 1. Build a weather dashboard web app that shows the forecast for Memphis using the NWS API. Keep it a single static page. | ||
| @@ -48,4 +48,4 @@ A distilled, replayable version of the accepted path - paste into a fresh agen | ||
| --- | ||
| - | *Generated by [treetrace](https://github.com/REPLACE-ME-ORG/treetrace) · 4 prompts across 1 session · assistant-model · machine-readable lineage in `.treetrace/tree.json` ([schema](https://github.com/REPLACE-ME-ORG/treetrace/blob/main/SCHEMA.md))* | |
| + | *Generated by [treetrace](https://github.com/REPLACE-ME-ORG/treetrace) · 4 prompts across 1 session · machine-readable lineage in `.treetrace/tree.json` ([schema](https://github.com/REPLACE-ME-ORG/treetrace/blob/main/SCHEMA.md))* |
| @@ -1,6 +1,6 @@ | ||
| # TreeTrace Report - weather-dashboard | ||
| - | Generated: 2026-06-12T02:37:21.277Z | |
| + | Generated: 2026-06-12T04:12:12.307Z | |
| This is the human-readable rollup. Keep the split `.treetrace/` artifacts for agents, CI, eval harnesses, and other tools. | ||
| @@ -62,7 +62,7 @@ Most recent accepted direction: Actually wait - also add a settings panel so t | ||
| #### Constraints learned the hard way | ||
| - | These corrections were issued during the build - do not repeat the mistakes they fixed: | |
| + | These corrections were issued during the build. Do not repeat the mistakes they fixed: | |
| - No, scrap the radar map, it is too heavy. Keep the page lightweight, just the forecast cards. | ||
| @@ -112,7 +112,7 @@ Source nodes: node_002, node_003, node_004 | ||
| > **4 prompts** · **1 session** · **1 day** · 1 correction · 1 scope change · 2 tool calls · 1 file touched | ||
| > | ||
| - | > The prompt lineage that built this project - extracted from real sessions, curated and redacted by the author, generated by [treetrace](https://github.com/REPLACE-ME-ORG/treetrace). | |
| + | > The prompt lineage that built this project, extracted from real sessions, curated and redacted by the author, generated by [treetrace](https://github.com/REPLACE-ME-ORG/treetrace). | |
| #### Goal | ||
| @@ -147,7 +147,7 @@ Source nodes: node_002, node_003, node_004 | ||
| #### Reusable Prompt Pack | ||
| - | A distilled, replayable version of the accepted path - paste into a fresh agent to rebuild something like this: | |
| + | A distilled, replayable version of the accepted path. Paste into a fresh agent to rebuild something like this: | |
| ```text | ||
| 1. Build a weather dashboard web app that shows the forecast for Memphis using the NWS API. Keep it a single static page. | ||
| @@ -158,7 +158,7 @@ A distilled, replayable version of the accepted path - paste into a fresh agen | ||
| --- | ||
| - | *Generated by [treetrace](https://github.com/REPLACE-ME-ORG/treetrace) · 4 prompts across 1 session · assistant-model · machine-readable lineage in `.treetrace/tree.json` ([schema](https://github.com/REPLACE-ME-ORG/treetrace/blob/main/SCHEMA.md))* | |
| + | *Generated by [treetrace](https://github.com/REPLACE-ME-ORG/treetrace) · 4 prompts across 1 session · machine-readable lineage in `.treetrace/tree.json` ([schema](https://github.com/REPLACE-ME-ORG/treetrace/blob/main/SCHEMA.md))* | |
| --- | ||
| @@ -7,7 +7,7 @@ | ||
| }, | ||
| "project": { | ||
| "name": "weather-dashboard", | ||
| - | "generatedAt": "2026-06-12T02:37:21.277Z", | |
| + | "generatedAt": "2026-06-12T04:12:12.307Z", | |
| "sourceType": "claude-code-jsonl" | ||
| }, | ||
| "stats": { |
| @@ -15,7 +15,7 @@ | ||
| "regression", | ||
| "memory" | ||
| ], | ||
| - | "license": "MIT", | |
| + | "license": "AGPL-3.0-only", | |
| "author": "Zion Boggan", | ||
| "type": "module", | ||
| "bin": { |
| @@ -59,7 +59,6 @@ export async function main(argv) { | ||
| const projectName = detectProjectName(projectDir); | ||
| const log = opts.quiet ? () => {} : (msg) => process.stderr.write(`${msg}\n`); | ||
| - | // ---- gather sessions ---- | |
| let sessions = []; | ||
| if (opts.stdin) { | ||
| const text = readFileSync(0, 'utf8'); | ||
| @@ -99,14 +98,12 @@ export async function main(argv) { | ||
| sessions = sessions.filter((s) => !s.lastTs || s.lastTs >= opts.since); | ||
| } | ||
| - | // ---- extract + build ---- | |
| const nodes = classifyPrompts(sessions); | ||
| if (!nodes.length) { | ||
| - | throw new Error('no human prompts found in these sessions - nothing to trace.'); | |
| + | throw new Error('no human prompts found in these sessions, nothing to trace.'); | |
| } | ||
| const tree = buildTree(sessions, nodes); | ||
| - | // ---- redaction gate ---- | |
| const ttDir = join(projectDir, '.treetrace'); | ||
| const decisionsPath = join(ttDir, 'redactions.json'); | ||
| const priorDecisions = existsSync(decisionsPath) | ||
| @@ -136,7 +133,6 @@ export async function main(argv) { | ||
| } | ||
| analyzeTree(tree); | ||
| - | // ---- render ---- | |
| const generatedAt = new Date().toISOString(); | ||
| const renderOpts = { projectName, titlesOnly: opts.titlesOnly, version: VERSION, generatedAt }; | ||
| @@ -181,13 +177,12 @@ export async function main(argv) { | ||
| mkdirSync(ttDir, { recursive: true }); | ||
| writeFileSync(join(ttDir, 'tree.json'), jsonText); | ||
| for (const artifact of Object.values(artifacts)) writeFileSync(artifact.path, artifact.text); | ||
| - | // decisions file stores only hashes + actions - safe to keep, never secrets | |
| + | ||
| writeFileSync(decisionsPath, JSON.stringify(decisions, null, 2)); | ||
| if (opts.json) process.stdout.write(jsonText + '\n'); | ||
| if (opts.report) process.stdout.write(report); | ||
| - | // ---- terminal summary ---- | |
| log(''); | ||
| log(summaryLine(tree.stats, projectName)); | ||
| log(renderTerminalSummary(tree, renderOpts).trimEnd()); | ||
| @@ -240,7 +235,7 @@ function assertClean(rendered, decisions, label) { | ||
| if (leaks.length) { | ||
| throw new Error( | ||
| `shadow scan found ${plural(leaks.length, 'unresolved secret')} in the rendered ${label} ` + | ||
| - | `(${[...new Set(leaks.map((l) => l.ruleId))].join(', ')}) - refusing to write. ` + | |
| + | `(${[...new Set(leaks.map((l) => l.ruleId))].join(', ')}). Refusing to write. ` + | |
| `This is a bug worth reporting; as a workaround run interactively to resolve hits.` | ||
| ); | ||
| } | ||
| @@ -255,7 +250,7 @@ function summaryLine(stats, projectName) { | ||
| if (stats.corrections) bits.push(`${stats.corrections} ${c.yellow('↩')} corrections`); | ||
| if (stats.abandonedBranches) bits.push(`${stats.abandonedBranches} ${c.red('✗')} abandoned`); | ||
| if (stats.toolUses) bits.push(`${stats.toolUses.toLocaleString()} tool calls`); | ||
| - | return `${c.cyan('🌳')} ${c.bold(projectName)} - ${bits.join(' · ')}`; | |
| + | return `${c.cyan('🌳')} ${c.bold(projectName)} · ${bits.join(' · ')}`; | |
| } | ||
| const PREVIEW_LIMIT = 30; | ||
| @@ -276,7 +271,7 @@ function previewTree(tree, log) { | ||
| log(`${' '.repeat(depth + 1)}${icon} ${title}`); | ||
| return true; | ||
| }; | ||
| - | // flat for linear chains, indent only at forks (matches the md renderer) | |
| + | ||
| const walk = (node, depth) => { | ||
| let cur = node; | ||
| for (;;) { | ||
| @@ -303,7 +298,7 @@ function detectProjectName(dir) { | ||
| const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')); | ||
| if (pkg.name) return pkg.name; | ||
| } catch { | ||
| - | /* no package.json - fall through */ | |
| + | ||
| } | ||
| return basename(dir); | ||
| } |
| @@ -1,8 +1,2 @@ | ||
| - | // Single source of truth for the public repo URL. | |
| - | // | |
| - | // NOTE (2026-06-12): the launch home is not yet decided - the personal GitHub | |
| - | // account is suspended, so treetrace will be published under a NEW GitHub | |
| - | // organization. Until that org exists, this is a placeholder. Change it here | |
| - | // (or set TREETRACE_REPO_URL) and every export/doc reference updates. | |
| export const REPO_URL = | ||
| process.env.TREETRACE_REPO_URL || 'https://github.com/REPLACE-ME-ORG/treetrace'; |
| @@ -2,9 +2,6 @@ import { readdirSync, statSync, existsSync } from 'node:fs'; | ||
| import { homedir } from 'node:os'; | ||
| import { join, resolve, sep } from 'node:path'; | ||
| - | // Claude Code stores sessions under ~/.claude/projects/<munged-cwd>/<sessionId>.jsonl | |
| - | // where <munged-cwd> is the absolute project path with every non [A-Za-z0-9-] | |
| - | // character replaced by "-" (case preserved). e.g. /home/dev/weatherapp -> -home-dev-weatherapp | |
| export function mungePath(absPath) { | ||
| return absPath.replace(/[^A-Za-z0-9-]/g, '-'); | ||
| } | ||
| @@ -15,22 +12,13 @@ export function claudeProjectsRoot() { | ||
| : join(homedir(), '.claude', 'projects'); | ||
| } | ||
| - | /** | |
| - | * Find Claude Code session files relevant to a project directory. | |
| - | * | |
| - | * A session "belongs" to the project if it was started from the project dir | |
| - | * itself OR any directory beneath it (Claude Code keys storage by exact cwd, | |
| - | * so a repo worked on from two subdirs produces two storage dirs). | |
| - | * | |
| - | * Returns [{ path, sessionId, sizeBytes, mtimeMs, storageDir }] sorted by mtime. | |
| - | */ | |
| export function discoverSessions(projectDir) { | ||
| const root = claudeProjectsRoot(); | ||
| if (!existsSync(root)) return []; | ||
| const abs = resolve(projectDir); | ||
| const exact = mungePath(abs); | ||
| - | const prefix = mungePath(abs + sep); // children share this prefix | |
| + | const prefix = mungePath(abs + sep); | |
| const sessions = []; | ||
| for (const entry of readdirSync(root, { withFileTypes: true })) { | ||
| @@ -38,8 +26,7 @@ export function discoverSessions(projectDir) { | ||
| if (entry.name !== exact && !entry.name.startsWith(prefix)) continue; | ||
| const dir = join(root, entry.name); | ||
| for (const f of readdirSync(dir, { withFileTypes: true })) { | ||
| - | // top-level session files only; subdirectories hold subagent/sidechain | |
| - | // transcripts which are agent-to-agent, not human lineage | |
| + | ||
| if (!f.isFile() || !f.name.endsWith('.jsonl')) continue; | ||
| const path = join(dir, f.name); | ||
| let st; |
| @@ -1,13 +1,5 @@ | ||
| import { truncate } from './util.js'; | ||
| - | /** | |
| - | * Classify candidate human prompts into lineage roles and fold noise. | |
| - | * | |
| - | * Deterministic by design: the same transcript always produces the same tree. | |
| - | * An optional --llm pass (the user's own model) may later retitle nodes, but | |
| - | * classification never depends on it. | |
| - | */ | |
| - | ||
| const KIND = { | ||
| ROOT: 'root', | ||
| DIRECTION: 'direction', | ||
| @@ -17,13 +9,11 @@ const KIND = { | ||
| QUESTION: 'question', | ||
| }; | ||
| - | // Strong correction signals: explicit negation/undo - these outrank scope. | |
| const CORRECTION_STRONG_OPENERS = | ||
| /^(no[,.\s]|no$|not |don'?t |stop\b|wrong\b|undo\b|revert\b|nope\b|that'?s (not|wrong)|why did you)/i; | ||
| const CORRECTION_ANYWHERE = | ||
| /(didn'?t work|doesn'?t work|not working|still (failing|broken|wrong|not)|that broke|you (missed|forgot|skipped|ignored)|redo (this|that|it)|go back|that'?s incorrect|not what i (asked|meant|wanted)|undo (this|that)|roll(?: |-)?back)/i; | ||
| - | // Soft correction signals: conversational pivots - only count when nothing | |
| - | // stronger (like an additive scope change) explains the message. | |
| + | ||
| const CORRECTION_SOFT_OPENERS = /^(wait\b|actually[,\s]|hold on\b|hmm[,\s]|instead[,\s])/i; | ||
| const SCOPE_ANYWHERE = | ||
| @@ -35,16 +25,11 @@ const CHECKPOINT_ANYWHERE = | ||
| const QUESTION_ONLY = | ||
| /^(what|how|why|where|when|which|who|is|are|can|could|should|would|will|do|does|did)\b[^]*\?\s*$/i; | ||
| - | // Short acknowledgements that nudge the agent along but carry no direction. | |
| - | // NB: bare numerals / "option B" are NOT here - they select from an offered | |
| - | // menu and steer the project (corpus ground truth), so they become nodes. | |
| const CONTINUATION_RE = | ||
| /^(y|yes|yep|yeah|ok|okay|k|sure|continue|cont|go|go ahead|do it|proceed|next|sounds good|looks good|lgtm|perfect|nice|good|great|approved?|yes please|please do|carry on|keep going|resume|finish|all good|that works|works|👍|do that)[.! ]*$/i; | ||
| - | // Menu selections: tiny text, real steering - titled specially. | |
| const SELECTION_RE = /^(?:option\s+)?([0-9]{1,2}|[a-d])[.)! ]*$/i; | ||
| - | // Explicit self-described throwaways ("Test message. Ignore this.") | |
| const IGNORE_RE = /\bignore this\b/i; | ||
| const MAX_NUDGE_WORDS = 4; | ||
| @@ -59,9 +44,6 @@ export function classifyPrompts(sessions) { | ||
| const text = prompt.text; | ||
| const words = text.split(/\s+/).filter(Boolean); | ||
| - | // The same human message can appear twice in a transcript (queued | |
| - | // resend, bridge echo, draft-then-full edit). Collapse consecutive | |
| - | // duplicates, including prefix-duplicates - keep the longer text. | |
| if (prevNode && isDupOf(prevNode.text, text)) { | ||
| if (text.length > prevNode.text.length) { | ||
| prevNode.text = text; | ||
| @@ -72,16 +54,13 @@ export function classifyPrompts(sessions) { | ||
| continue; | ||
| } | ||
| - | // Re-armed recurring prompts (/loop restarts, repeated dispatches with | |
| - | // small wording drift) collapse into one node with a re-run counter. | |
| if (prevNode && isRerunOf(prevNode.text, text)) { | ||
| prevNode.reruns = (prevNode.reruns || 0) + 1; | ||
| - | prevNode.text = text; // latest wording wins | |
| + | prevNode.text = text; | |
| prevNode.title = makeTitle(text); | ||
| continue; | ||
| } | ||
| - | // Fold pure nudges into the previous node instead of creating noise nodes. | |
| if ( | ||
| prevNode && | ||
| words.length <= MAX_NUDGE_WORDS && | ||
| @@ -91,7 +70,6 @@ export function classifyPrompts(sessions) { | ||
| continue; | ||
| } | ||
| - | // Self-described throwaways never become lineage. | |
| if (words.length <= 6 && IGNORE_RE.test(text)) continue; | ||
| const selection = rootAssigned && SELECTION_RE.exec(text); | ||
| @@ -109,7 +87,7 @@ export function classifyPrompts(sessions) { | ||
| afterInterruption: prompt.afterInterruption, | ||
| chars: text.length, | ||
| } : { | ||
| - | id: null, // assigned by tree builder | |
| + | id: null, | |
| uuid: prompt.uuid, | ||
| parentUuid: prompt.parentUuid, | ||
| sessionId: session.sessionId, | ||
| @@ -117,7 +95,7 @@ export function classifyPrompts(sessions) { | ||
| text, | ||
| title: makeTitle(text), | ||
| kind: classifyOne(text, prompt, rootAssigned), | ||
| - | status: 'accepted', // tree builder may demote to abandoned | |
| + | status: 'accepted', | |
| nudges: 0, | ||
| afterInterruption: prompt.afterInterruption, | ||
| chars: text.length, | ||
| @@ -130,27 +108,22 @@ export function classifyPrompts(sessions) { | ||
| return nodes; | ||
| } | ||
| - | // Two consecutive messages are duplicates if one is (nearly) a prefix of the | |
| - | // other after whitespace normalization - covers truncated draft echoes. | |
| function isDupOf(a, b) { | ||
| const na = a.replace(/\s+/g, ' ').trim(); | ||
| const nb = b.replace(/\s+/g, ' ').trim(); | ||
| if (na === nb) return true; | ||
| const [short, long] = na.length <= nb.length ? [na, nb] : [nb, na]; | ||
| - | if (short.length < 24) return false; // too short to call a prefix-dup safely | |
| - | // tolerate a few trailing chars of divergence (cut-off mid-word) | |
| + | if (short.length < 24) return false; | |
| + | ||
| return long.startsWith(short.slice(0, short.length - 4)); | ||
| } | ||
| - | // Same recurring instruction re-issued with wording drift: identical opening | |
| - | // (command name / first words) plus high common-prefix overlap. | |
| function isRerunOf(a, b) { | ||
| const na = a.replace(/\s+/g, ' ').trim(); | ||
| const nb = b.replace(/\s+/g, ' ').trim(); | ||
| if (na.length < 40 || nb.length < 40) return false; | ||
| if (na.slice(0, 24) !== nb.slice(0, 24)) return false; | ||
| - | // Command re-arms (/loop, /dispatch …): same command + same opening counts | |
| - | // as a re-issue even when the long arg body drifts. | |
| + | ||
| if (na.startsWith('/') && na.slice(0, 32) === nb.slice(0, 32)) return true; | ||
| const limit = Math.min(na.length, nb.length); | ||
| let common = 0; | ||
| @@ -168,7 +141,6 @@ function classifyOne(text, prompt, rootAssigned) { | ||
| return KIND.DIRECTION; | ||
| } | ||
| - | // First sentence-ish fragment, cleaned, for node titles. | |
| export function makeTitle(text) { | ||
| const firstLine = text.split(/\r?\n/).find((l) => l.trim()) || text; | ||
| const sentence = firstLine.split(/(?<=[.!?])\s+/)[0] || firstLine; |
| @@ -1,14 +1,6 @@ | ||
| import { truncate } from './util.js'; | ||
| import { analyzeTree } from './analyze.js'; | ||
| - | /** | |
| - | * --handoff: an agent-ready context pack, printed to stdout (and gated by the | |
| - | * same redaction pipeline as every other export). | |
| - | * | |
| - | * Not a "replay" - a briefing: goal, where things stand, accepted decisions, | |
| - | * known dead ends, and standing constraints, so the next agent (or model) | |
| - | * starts with the lineage instead of an empty context. | |
| - | */ | |
| export function renderHandoff(tree, opts = {}) { | ||
| const { projectName } = opts; | ||
| const { nodes, stats } = tree; | ||
| @@ -20,7 +12,7 @@ export function renderHandoff(tree, opts = {}) { | ||
| const lastCheckpoint = [...accepted].reverse().find((n) => n.kind === 'checkpoint'); | ||
| const lastAccepted = accepted.at(-1); | ||
| - | lines.push(`# Handoff brief - ${projectName}`); | |
| + | lines.push(`# Handoff brief: ${projectName}`); | |
| lines.push(''); | ||
| lines.push( | ||
| `You are taking over an AI-assisted project. This brief was distilled from the real prompt lineage (${stats.promptCount} prompts, ${stats.sessionCount} sessions). Read it fully before acting.` | ||
| @@ -57,7 +49,7 @@ export function renderHandoff(tree, opts = {}) { | ||
| if (corrections.length) { | ||
| lines.push('## Constraints learned the hard way'); | ||
| lines.push(''); | ||
| - | lines.push('These corrections were issued during the build - do not repeat the mistakes they fixed:'); | |
| + | lines.push('These corrections were issued during the build. Do not repeat the mistakes they fixed:'); | |
| lines.push(''); | ||
| corrections.forEach((n) => lines.push(`- ${truncate(n.text.replace(/\s+/g, ' '), 300)}`)); | ||
| lines.push(''); | ||
| @@ -69,7 +61,7 @@ export function renderHandoff(tree, opts = {}) { | ||
| if (abandoned.length) { | ||
| lines.push('## Known dead ends'); | ||
| lines.push(''); | ||
| - | lines.push('These approaches were tried and abandoned - avoid unless told otherwise:'); | |
| + | lines.push('These approaches were tried and abandoned. Avoid unless told otherwise:'); | |
| lines.push(''); | ||
| abandoned.forEach((n) => lines.push(`- ${truncate(n.text.replace(/\s+/g, ' '), 300)}`)); | ||
| lines.push(''); |
| @@ -1,23 +1,6 @@ | ||
| import { createReadStream } from 'node:fs'; | ||
| import { createInterface } from 'node:readline'; | ||
| - | /** | |
| - | * Streaming parser for Claude Code session JSONL files. | |
| - | * | |
| - | * Built against a 579-file / ~195k-line corpus census (format versions | |
| - | * 2.1.133-2.1.173). Key realities encoded here: | |
| - | * - Records form a DAG per file; chains pass THROUGH system/attachment | |
| - | * nodes, so all addressable node types must be indexed. | |
| - | * - One API assistant message = N jsonl records sharing message.id, with | |
| - | * usage repeated on every split - merge or token stats inflate 2-4×. | |
| - | * - Compaction restarts the chain (parentUuid:null) but provides | |
| - | * logicalParentUuid to stitch through. | |
| - | * - userType is 'external' on every record including agent-authored ones - | |
| - | * never a human discriminator. Sidechains live in separate files. | |
| - | * - The last `last-prompt` record's leafUuid is the live branch tip. | |
| - | * - Session files reach 200MB+ (multi-MB base64 lines): stream, never buffer. | |
| - | */ | |
| - | ||
| const DAG_TYPES = new Set(['user', 'assistant', 'system', 'attachment']); | ||
| export async function parseSessionFile(path, sessionMeta = {}) { | ||
| @@ -31,10 +14,10 @@ export async function parseSessionFile(path, sessionMeta = {}) { | ||
| gitBranch: null, | ||
| firstTs: null, | ||
| lastTs: null, | ||
| - | prompts: [], // candidate human prompts (full text retained) | |
| - | index: new Map(), // uuid -> { parentUuid, type, ts } for all DAG records | |
| - | leafUuid: null, // last addressable record seen (fallback branch tip) | |
| - | activeLeafUuid: null, // from last `last-prompt` record (authoritative) | |
| + | prompts: [], | |
| + | index: new Map(), | |
| + | leafUuid: null, | |
| + | activeLeafUuid: null, | |
| stats: { | ||
| userLines: 0, | ||
| assistantLines: 0, | ||
| @@ -46,7 +29,7 @@ export async function parseSessionFile(path, sessionMeta = {}) { | ||
| interruptions: 0, | ||
| }, | ||
| isContinuation: false, | ||
| - | _usageByMsgId: new Map(), // assistant split merge: last record's usage wins | |
| + | _usageByMsgId: new Map(), | |
| _pendingInterruption: false, | ||
| }; | ||
| @@ -54,22 +37,21 @@ export async function parseSessionFile(path, sessionMeta = {}) { | ||
| const rl = createInterface({ input: stream, crlfDelay: Infinity }); | ||
| for await (const line of rl) { | ||
| - | if (!line || line.charCodeAt(0) !== 123 /* '{' */) continue; | |
| + | if (!line || line.charCodeAt(0) !== 123 ) continue; | |
| let rec; | ||
| try { | ||
| rec = JSON.parse(line); | ||
| } catch { | ||
| - | continue; // truncated/corrupt line (live files mutate mid-scan) | |
| + | continue; | |
| } | ||
| try { | ||
| ingestRecord(session, rec); | ||
| } catch { | ||
| - | continue; // unknown shape - tolerate, never crash | |
| + | continue; | |
| } | ||
| } | ||
| rl.close(); | ||
| - | // fold merged assistant usage into totals | |
| for (const usage of session._usageByMsgId.values()) { | ||
| session.stats.inputTokens += usage.input_tokens || 0; | ||
| session.stats.outputTokens += usage.output_tokens || 0; | ||
| @@ -92,7 +74,7 @@ function ingestRecord(session, rec) { | ||
| break; | ||
| case 'system': | ||
| indexDagNode(session, rec, { | ||
| - | // compaction boundary restarts parentUuid; stitch the logical chain | |
| + | ||
| parentOverride: | ||
| rec.subtype === 'compact_boundary' && rec.logicalParentUuid | ||
| ? rec.logicalParentUuid | ||
| @@ -100,23 +82,22 @@ function ingestRecord(session, rec) { | ||
| }); | ||
| break; | ||
| case 'attachment': | ||
| - | indexDagNode(session, rec); // chains pass through attachments | |
| + | indexDagNode(session, rec); | |
| break; | ||
| - | case 'summary': // legacy (<2.1.133) | |
| + | case 'summary': | |
| if (rec.summary && !session.title) session.title = rec.summary; | ||
| break; | ||
| - | case 'ai-title': // last occurrence wins | |
| + | case 'ai-title': | |
| if (rec.aiTitle || rec.title) session.title = rec.aiTitle || rec.title; | ||
| break; | ||
| - | case 'custom-title': // user-set, beats ai-title | |
| + | case 'custom-title': | |
| if (rec.customTitle) session.customTitle = rec.customTitle; | ||
| break; | ||
| - | case 'last-prompt': // last occurrence's leafUuid = live branch tip | |
| + | case 'last-prompt': | |
| if (rec.leafUuid) session.activeLeafUuid = rec.leafUuid; | ||
| break; | ||
| default: | ||
| - | // mode, permission-mode, bridge-session, queue-operation, | |
| - | // file-history-snapshot, unknown future types - not lineage material | |
| + | ||
| break; | ||
| } | ||
| @@ -141,17 +122,14 @@ function indexDagNode(session, rec, { parentOverride } = {}) { | ||
| } | ||
| function ingestUser(session, rec) { | ||
| - | // Sidechain traffic is agent-authored even when it mimics human voice. | |
| - | // (Sidechains live in separate files; belt-and-suspenders for inline ones.) | |
| + | ||
| if (rec.isSidechain || rec.agentId) return; | ||
| indexDagNode(session, rec); | ||
| session.stats.userLines++; | ||
| - | // Tool plumbing: results echo back as user records (~90% of user lines), | |
| - | // marked by toolUseResult / sourceToolAssistantUUID even for string content. | |
| if (rec.toolUseResult !== undefined || rec.sourceToolAssistantUUID !== undefined) return; | ||
| - | if (rec.isMeta) return; // caveats, skill-body injections | |
| + | if (rec.isMeta) return; | |
| if (rec.isCompactSummary) { | ||
| session.isContinuation = true; | ||
| return; | ||
| @@ -178,14 +156,12 @@ function ingestUser(session, rec) { | ||
| return; | ||
| } | ||
| if (classification === 'command') { | ||
| - | // Slash-command wrappers are noise - unless the human packed real intent | |
| - | // into the args (e.g. `/loop <multi-line work focus>`). | |
| + | ||
| const invocation = extractCommandInvocation(trimmed); | ||
| if (!invocation) return; | ||
| trimmed = invocation; | ||
| } | ||
| - | // Image-only records are often screenshot feedback - meaningful, keep. | |
| if (!trimmed && hasImage) trimmed = '[image-only prompt: screenshot/annotated feedback]'; | ||
| if (!trimmed) return; | ||
| @@ -210,9 +186,7 @@ function ingestAssistant(session, rec) { | ||
| const synthetic = msg.model === '<synthetic>' || rec.isApiErrorMessage; | ||
| if (msg.model && !synthetic) session.stats.models.add(msg.model); | ||
| - | // One API message = N split records sharing message.id, usage repeated on | |
| - | // each (main sessions) or present only on the last (subagent files): | |
| - | // keep the latest non-empty usage per id, sum after parsing. | |
| + | ||
| if (msg.usage && !synthetic && (msg.usage.input_tokens || msg.usage.output_tokens)) { | ||
| session._usageByMsgId.set(msg.id || rec.uuid, msg.usage); | ||
| } | ||
| @@ -249,7 +223,7 @@ function flattenUserContent(content) { | ||
| } else if (block.type === 'image') { | ||
| images++; | ||
| } else { | ||
| - | others++; // documents, future block types | |
| + | others++; | |
| } | ||
| } | ||
| return { | ||
| @@ -286,8 +260,6 @@ export function classifySpecialUserText(text) { | ||
| return 'prompt'; | ||
| } | ||
| - | // `/loop de-swamp & polish ...` - wrapper noise, but non-empty <command-args> | |
| - | // is the human's actual instruction. Returns reconstructed text or null. | |
| export function extractCommandInvocation(text) { | ||
| const name = text.match(/<command-name>([^<]*)<\/command-name>/)?.[1]?.trim(); | ||
| const args = text.match(/<command-args>([\s\S]*?)<\/command-args>/)?.[1]?.trim(); | ||
| @@ -295,11 +267,6 @@ export function extractCommandInvocation(text) { | ||
| return `${name || '(command)'} ${args}`; | ||
| } | ||
| - | /** | |
| - | * Fallback importer: plain text / markdown transcripts (pasted exports from | |
| - | * ChatGPT, Claude.ai, etc.). Recognizes common turn markers; returns a | |
| - | * session-shaped object with prompts only. | |
| - | */ | |
| export function parsePlainTranscript(text, label = 'pasted-transcript') { | ||
| const lines = text.split(/\r?\n/); | ||
| const markers = |
| @@ -1,22 +1,8 @@ | ||
| import { createInterface } from 'node:readline/promises'; | ||
| import { sha256, shannonEntropy, truncate, c } from './util.js'; | ||
| - | /** | |
| - | * Secret/PII scanner + export gate. | |
| - | * | |
| - | * Philosophy: NOTHING leaves un-reviewed. In a TTY the user resolves every | |
| - | * unique hit (redact / keep / edit). Outside a TTY every hit is redacted | |
| - | * automatically - the tool fails closed, never open. After rendering, the | |
| - | * final artifact is shadow-scanned again; an unresolved high/medium hit at | |
| - | * that stage aborts the write. | |
| - | * | |
| - | * Rules are curated for precision (gitleaks-style provider formats) plus a | |
| - | * high-entropy fallback. False negatives are existential for a privacy tool, | |
| - | * false positives merely cost a keystroke - when in doubt, flag. | |
| - | */ | |
| - | ||
| export const RULES = [ | ||
| - | // ---- high: unambiguous secret formats ---- | |
| + | ||
| { id: 'private-key-block', severity: 'high', re: /-----BEGIN [A-Z ]*PRIVATE KEY( BLOCK)?-----[\s\S]*?(-----END [A-Z ]*PRIVATE KEY( BLOCK)?-----|$)/g }, | ||
| { id: 'aws-access-key', severity: 'high', re: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g }, | ||
| { id: 'github-token', severity: 'high', re: /\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g }, | ||
| @@ -35,13 +21,11 @@ export const RULES = [ | ||
| { id: 'discord-webhook', severity: 'high', re: /https:\/\/(?:ptb\.|canary\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[A-Za-z0-9_-]+/g }, | ||
| { id: 'jwt', severity: 'high', re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}\b/g }, | ||
| - | // ---- medium: context-dependent assignments ---- | |
| { id: 'wireguard-key', severity: 'medium', re: /\b(PrivateKey|PresharedKey)\s*=\s*[A-Za-z0-9+/]{42,44}=?/g }, | ||
| { id: 'url-basic-auth', severity: 'medium', re: /[a-z][a-z0-9+.-]*:\/\/[^/\s:@'"`]{2,}:[^/\s@'"`]{2,}@[^\s'"`]+/gi }, | ||
| { id: 'bearer-header', severity: 'medium', re: /\bBearer\s+[A-Za-z0-9._+/=-]{20,}\b/g }, | ||
| { id: 'secret-assignment', severity: 'medium', re: /\b(password|passwd|pwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret)\b\s*[:=]\s*(?!(?:['"]?\s*)?(?:\$\{|<|%|\*{3}|\.{3}|REDACTED|xxx+|placeholder|changeme|example|your[-_]))(?:"[^"\r\n]{8,}"|'[^'\r\n]{8,}'|[^\s'"`,;]{8,})/gi }, | ||
| - | // ---- soft: PII and context the user may want to keep ---- | |
| { id: 'email', severity: 'soft', re: /\b[A-Za-z0-9._%+-]+@(?!(?:users\.noreply\.github\.com|example\.(?:com|org)))[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g }, | ||
| { id: 'ipv4', severity: 'soft', re: /\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\b(?!\.\d)/g }, | ||
| { id: 'home-dir-username', severity: 'soft', re: /(?:\/(?:home|Users)\/|C:\\Users\\)([A-Za-z][A-Za-z0-9._-]{2,30})\b/g }, | ||
| @@ -85,8 +69,6 @@ export function scanText(text) { | ||
| } | ||
| } | ||
| - | // High-entropy fallback: long mixed-charset tokens that no provider rule | |
| - | // caught. Pure hex (git SHAs, digests) and uuids excluded - too noisy. | |
| const seenSpans = findings.map((f) => [f.index, f.index + f.match.length]); | ||
| ENTROPY_CANDIDATE_RE.lastIndex = 0; | ||
| let m; | ||
| @@ -139,13 +121,9 @@ export function maskFor(finding) { | ||
| return `[REDACTED:${finding.ruleId}]`; | ||
| } | ||
| - | /** | |
| - | * Resolve findings into decisions, keyed by sha256(match). | |
| - | * decision = { action: 'redact'|'keep', replacement, ruleId } | |
| - | */ | |
| export async function resolveFindings(findings, priorDecisions, { interactive, autoRedact }) { | ||
| const decisions = { ...priorDecisions }; | ||
| - | const unique = new Map(); // hash -> { finding, count } | |
| + | const unique = new Map(); | |
| for (const f of findings) { | ||
| const h = sha256(f.match); | ||
| if (!unique.has(h)) unique.set(h, { finding: f, count: 0 }); | ||
| @@ -164,7 +142,7 @@ export async function resolveFindings(findings, priorDecisions, { interactive, a | ||
| const rl = createInterface({ input: process.stdin, output: process.stderr }); | ||
| process.stderr.write( | ||
| - | `\n${c.bold(`${unresolved.length} potential secret${unresolved.length === 1 ? '' : 's'} found`)} - nothing is exported until each is resolved.\n\n` | |
| + | `\n${c.bold(`${unresolved.length} potential secret${unresolved.length === 1 ? '' : 's'} found`)}. Nothing is exported until each is resolved.\n\n` | |
| ); | ||
| let i = 0; | ||
| for (const [h, { finding, count }] of unresolved) { | ||
| @@ -175,6 +153,7 @@ export async function resolveFindings(findings, priorDecisions, { interactive, a | ||
| : c.gray(finding.severity); | ||
| process.stderr.write( | ||
| `${c.dim(`[${i}/${unresolved.length}]`)} ${sev} ${c.bold(finding.ruleId)} ×${count}\n ${c.cyan(truncate(finding.match, 72))}\n` | ||
| + | ||
| ); | ||
| let answer; | ||
| for (;;) { | ||
| @@ -196,13 +175,8 @@ export async function resolveFindings(findings, priorDecisions, { interactive, a | ||
| return { decisions, asked: unresolved.length }; | ||
| } | ||
| - | /** | |
| - | * Apply redaction decisions to text. Decisions are keyed by sha256(match) and | |
| - | * deliberately never store the secret itself (the persisted decision file must | |
| - | * be safe to commit); the raw strings come from this run's findings. | |
| - | */ | |
| export function applyDecisions(text, findings, decisions) { | ||
| - | const toRedact = new Map(); // original -> replacement | |
| + | const toRedact = new Map(); | |
| for (const f of findings) { | ||
| const d = decisions[sha256(f.match)]; | ||
| if (d && d.action === 'redact') { | ||
| @@ -210,7 +184,7 @@ export function applyDecisions(text, findings, decisions) { | ||
| } | ||
| } | ||
| let out = text; | ||
| - | // Longest matches first so substrings of larger secrets don't pre-empt them. | |
| + | ||
| for (const [original, replacement] of [...toRedact.entries()].sort( | ||
| (a, b) => b[0].length - a[0].length | ||
| )) { | ||
| @@ -219,10 +193,6 @@ export function applyDecisions(text, findings, decisions) { | ||
| return out; | ||
| } | ||
| - | /** | |
| - | * Shadow scan: run after rendering. Any high/medium finding that is not an | |
| - | * explicit "keep" means the gate failed - abort, never write. | |
| - | */ | |
| export function shadowScan(renderedText, decisions) { | ||
| const leaks = []; | ||
| for (const f of scanText(renderedText)) { |
| @@ -1,11 +1,6 @@ | ||
| import { REPO_URL } from './config.js'; | ||
| import { analyzeTree } from './analyze.js'; | ||
| - | /** | |
| - | * Machine-readable export: TreeTrace lineage schema v0.2. | |
| - | * Documented in SCHEMA.md with a mapping to the Agent Trace RFC. | |
| - | */ | |
| - | ||
| const RELATIONSHIP_BY_KIND = { | ||
| direction: 'refines', | ||
| correction: 'corrects', | ||
| @@ -73,8 +68,7 @@ export function renderJson(tree, opts = {}) { | ||
| failureSignals: n.failureSignals || [], | ||
| evalCandidate: Boolean(n.evalCandidate), | ||
| lessonIds: n.lessonIds || [], | ||
| - | // source linkage for audit: the original record uuid inside the local | |
| - | // session transcript (raw transcripts themselves are never exported) | |
| + | ||
| sourceEventIds: n.uuid ? [n.uuid] : [], | ||
| })), | ||
| edges: nodes |
| @@ -17,16 +17,15 @@ export function renderMarkdown(tree, opts = {}) { | ||
| const { stats, roots, nodes, sessions } = tree; | ||
| const lines = []; | ||
| - | lines.push(`# 🌳 Prompt Tree - ${projectName}`); | |
| + | lines.push(`# 🌳 Prompt Tree: ${projectName}`); | |
| lines.push(''); | ||
| lines.push(`> ${banner(stats)}`); | ||
| lines.push('>'); | ||
| lines.push( | ||
| - | `> The prompt lineage that built this project - extracted from real sessions, curated and redacted by the author, generated by [treetrace](${REPO_URL}).` | |
| + | `> The prompt lineage that built this project, extracted from real sessions, curated and redacted by the author, generated by [treetrace](${REPO_URL}).` | |
| ); | ||
| lines.push(''); | ||
| - | // Goal | |
| const root = nodes.find((n) => n.kind === 'root') || nodes[0]; | ||
| if (root) { | ||
| lines.push('## Goal'); | ||
| @@ -35,7 +34,6 @@ export function renderMarkdown(tree, opts = {}) { | ||
| lines.push(''); | ||
| } | ||
| - | // The Path | |
| lines.push('## The Path'); | ||
| lines.push(''); | ||
| lines.push( | ||
| @@ -45,7 +43,6 @@ export function renderMarkdown(tree, opts = {}) { | ||
| for (const r of roots) renderNode(r, 0, lines, { titlesOnly }); | ||
| lines.push(''); | ||
| - | // Sessions timeline | |
| const active = sessions.filter((s) => s.prompts.length); | ||
| if (active.length > 1) { | ||
| lines.push('## Sessions'); | ||
| @@ -54,15 +51,14 @@ export function renderMarkdown(tree, opts = {}) { | ||
| lines.push('|---|------|---------|---------|'); | ||
| active.forEach((s, i) => { | ||
| lines.push( | ||
| - | `| ${i + 1} | ${formatDay(s.firstTs) || '-'} | ${s.prompts.length} | ${mdEscapePipe( | |
| - | s.title || s.sessionId || '-' | |
| + | `| ${i + 1} | ${formatDay(s.firstTs) || ''} | ${s.prompts.length} | ${mdEscapePipe( | |
| + | s.title || s.sessionId || '' | |
| )} |` | ||
| ); | ||
| }); | ||
| lines.push(''); | ||
| } | ||
| - | // Corrections & dead ends - the honest part, and the interesting part. | |
| const corrections = nodes.filter((n) => n.kind === 'correction'); | ||
| const abandoned = nodes.filter( | ||
| (n) => n.status === 'abandoned' && (!n.parent || n.parent.status !== 'abandoned') | ||
| @@ -88,11 +84,10 @@ export function renderMarkdown(tree, opts = {}) { | ||
| } | ||
| } | ||
| - | // Reusable prompt pack | |
| lines.push('## Reusable Prompt Pack'); | ||
| lines.push(''); | ||
| lines.push( | ||
| - | 'A distilled, replayable version of the accepted path - paste into a fresh agent to rebuild something like this:' | |
| + | 'A distilled, replayable version of the accepted path. Paste into a fresh agent to rebuild something like this:' | |
| ); | ||
| lines.push(''); | ||
| lines.push('```text'); | ||
| @@ -106,7 +101,7 @@ export function renderMarkdown(tree, opts = {}) { | ||
| `*Generated by [treetrace](${REPO_URL}) · ${plural(stats.promptCount, 'prompt')} across ${plural( | ||
| stats.sessionCount, | ||
| 'session' | ||
| - | )}${stats.models.length ? ` · ${stats.models.join(', ')}` : ''} · machine-readable lineage in \`.treetrace/tree.json\` ([schema](${REPO_URL}/blob/main/SCHEMA.md))*` | |
| + | )} · machine-readable lineage in \`.treetrace/tree.json\` ([schema](${REPO_URL}/blob/main/SCHEMA.md))*` | |
| ); | ||
| lines.push(''); | ||
| @@ -128,8 +123,6 @@ function banner(stats) { | ||
| return parts.join(' · '); | ||
| } | ||
| - | // Real sessions are mostly linear: render single-child chains flat and indent | |
| - | // only at genuine forks, otherwise long projects become an unreadable staircase. | |
| function renderNode(node, depth, lines, opts) { | ||
| let cur = node; | ||
| for (;;) { | ||
| @@ -178,11 +171,6 @@ function clip(text, max) { | ||
| return `${text.slice(0, max).trimEnd()}\n\n*[...trimmed, ${text.length - max} more chars]*`; | ||
| } | ||
| - | /** | |
| - | * The distilled, replayable instruction list: the accepted spine of the tree. | |
| - | * Corrections merge into their parent as constraints; abandoned branches are | |
| - | * dropped; questions and checkpoints are skipped. | |
| - | */ | |
| export function promptPack(nodes) { | ||
| const accepted = nodes.filter( | ||
| (n) => |
| @@ -1,19 +1,9 @@ | ||
| import { daySpan } from './util.js'; | ||
| - | /** | |
| - | * Build the lineage tree from classified prompt nodes + session topology. | |
| - | * | |
| - | * Claude Code records form a DAG via parentUuid: rewinds and forks create | |
| - | * real branches. The "main path" of a session is the ancestor chain of its | |
| - | * final record; prompts off that chain were abandoned (rewound away). | |
| - | */ | |
| export function buildTree(sessions, nodes) { | ||
| const byUuid = new Map(); | ||
| for (const node of nodes) if (node.uuid) byUuid.set(node.uuid, node); | ||
| - | // Per-session main-path sets (uuids of records that "made it" to the end). | |
| - | // The last `last-prompt` record's leafUuid is the authoritative live-branch | |
| - | // tip; the last addressable record is the fallback. | |
| const mainPaths = new Map(); | ||
| for (const session of sessions) { | ||
| const main = new Set(); | ||
| @@ -29,7 +19,6 @@ export function buildTree(sessions, nodes) { | ||
| mainPaths.set(session.sessionId, main); | ||
| } | ||
| - | // Parent resolution: walk up the record chain to the nearest prompt node. | |
| const sessionById = new Map(sessions.map((s) => [s.sessionId, s])); | ||
| for (const node of nodes) { | ||
| node.parent = null; | ||
| @@ -48,9 +37,6 @@ export function buildTree(sessions, nodes) { | ||
| } | ||
| } | ||
| - | // Session ordering by first activity, then chain sessions together: | |
| - | // the first parentless node of a session hangs off the previous session's | |
| - | // last main-path node. | |
| const ordered = [...sessions].sort((a, b) => | ||
| String(a.firstTs || '').localeCompare(String(b.firstTs || '')) | ||
| ); | ||
| @@ -66,7 +52,7 @@ export function buildTree(sessions, nodes) { | ||
| if (!sNodes.length) continue; | ||
| for (const node of sNodes) { | ||
| if (!node.parent && node !== sNodes[0]) { | ||
| - | // orphan mid-session (uuid chain broken) - chain linearly | |
| + | ||
| node.parent = sNodes[sNodes.indexOf(node) - 1]; | ||
| } | ||
| } | ||
| @@ -81,11 +67,6 @@ export function buildTree(sessions, nodes) { | ||
| prevTail = tail || sNodes[sNodes.length - 1]; | ||
| } | ||
| - | // Status: a prompt is abandoned only if it sits on a dead side-branch of a | |
| - | // REAL fork - i.e. walking up its record chain reaches a node that IS on the | |
| - | // session's main path while the prompt itself is not. parentUuid chains can | |
| - | // reset mid-file (bridge events, compaction); a broken chain is not a fork, | |
| - | // so prompts above a break stay accepted. | |
| for (const node of nodes) { | ||
| if (!node.uuid) continue; | ||
| const main = mainPaths.get(node.sessionId); | ||
| @@ -102,7 +83,6 @@ export function buildTree(sessions, nodes) { | ||
| } | ||
| } | ||
| - | // ids + children | |
| nodes.forEach((n, i) => { | ||
| n.id = `node_${String(i + 1).padStart(3, '0')}`; | ||
| n.children = []; |
| @@ -49,7 +49,6 @@ export function formatDay(ts) { | ||
| return d.toISOString().slice(0, 10); | ||
| } | ||
| - | // Span of calendar days covered by a set of timestamps, e.g. "9 days" / "1 day". | |
| export function daySpan(timestamps) { | ||
| const valid = timestamps.map((t) => new Date(t).getTime()).filter(Number.isFinite); | ||
| if (!valid.length) return null; | ||
| @@ -70,8 +69,6 @@ export function shannonEntropy(s) { | ||
| return entropy; | ||
| } | ||
| - | // Escape text destined for a Markdown document so it cannot break out of its | |
| - | // surrounding structure (tables, emphasis). Conservative: only what's needed. | |
| export function mdEscapePipe(s) { | ||
| return String(s).replace(/\|/g, '\\|').replace(/\r?\n/g, ' '); | ||
| } |
| @@ -143,7 +143,7 @@ test('renderers: markdown, json, handoff are consistent and footer-credited', as | ||
| const { tree } = await fixtureTree(); | ||
| analyzeTree(tree); | ||
| const md = renderMarkdown(tree, { projectName: 'demo' }); | ||
| - | assert.ok(md.startsWith('# 🌳 Prompt Tree - demo')); | |
| + | assert.ok(md.startsWith('# 🌳 Prompt Tree: demo')); | |
| assert.ok(md.includes('## Goal')); | ||
| assert.ok(md.includes('## Reusable Prompt Pack')); | ||
| assert.ok(md.includes('generated by [treetrace]') || md.includes('Generated by [treetrace]')); |