Zion Boggan zionboggan.com ↗

Relicense under AGPL-3.0 and prepare for public launch

- Relicense MIT to AGPL-3.0-only (LICENSE, package.json, README)
- Strip every inline code comment from shipped source
- Remove em dashes from all output and docs (intentional transcript-parsing regexes in parse.js retained)
- Drop renderer model-name footer and "Generated with" authorship tells
- Remove the root dogfood that quoted private content and named build models
- Neutralize synthetic fixture model names; regenerate the example set
- Harden .gitignore against accidental dogfood commits
9b38d4c   Zion Boggan committed on Jun 12, 2026 (1 week ago)
.gitignore +6 -3
@@ -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/
LICENSE +661 -21
@@ -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/>.
README.md +20 -16
@@ -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.
action.yml +3 -4
@@ -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' }}
examples/README.md +9 -18
@@ -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.
examples/weather-dashboard/.treetrace/failures.json +1 -1
@@ -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,
examples/weather-dashboard/.treetrace/tree.json +1 -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": {
examples/weather-dashboard/PROMPT_TREE.md +4 -4
@@ -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))*
examples/weather-dashboard/TREETRACE_REPORT.md +5 -5
@@ -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))*
---
examples/weather-dashboard/tree.json +1 -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": {
package.json +1 -1
@@ -15,7 +15,7 @@
"regression",
"memory"
],
- "license": "MIT",
+ "license": "AGPL-3.0-only",
"author": "Zion Boggan",
"type": "module",
"bin": {
src/cli.js +6 -11
@@ -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);
}
src/config.js +0 -6
@@ -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';
src/discover.js +2 -15
@@ -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;
src/extract.js +7 -35
@@ -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;
src/handoff.js +3 -11
@@ -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('');
src/parse.js +20 -53
@@ -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 =
src/redact.js +6 -36
@@ -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)) {
src/render-json.js +1 -7
@@ -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
src/render-md.js +6 -18
@@ -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) =>
src/tree.js +1 -21
@@ -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 = [];
src/util.js +0 -3
@@ -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, ' ');
}
test/treetrace.test.js +1 -1
@@ -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]'));