@crewhaus/tool-plan 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +72 -2
- package/dist/index.d.ts +64 -0
- package/dist/index.js +319 -18
- package/package.json +12 -8
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
CrewHaus Factory
|
|
2
|
+
Copyright 2026 Max Meier
|
|
3
|
+
|
|
4
|
+
This product includes software developed by Max Meier
|
|
5
|
+
(operating as StudioMax — https://studiomax.io) and contributors.
|
|
6
|
+
|
|
7
|
+
The CrewHaus name and logo are trademarks of Max Meier. See
|
|
8
|
+
https://github.com/crewhaus/factory/blob/main/TRADEMARK.md for the
|
|
9
|
+
use-of-marks policy.
|
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ import { defaultCatalog } from "@crewhaus/tool-catalog";
|
|
|
12
12
|
const bundle = createPlanTools({
|
|
13
13
|
specName: "support-bot",
|
|
14
14
|
appendEvent: (e) => eventLog.append(e), // plan_update / goal_update / action_proof
|
|
15
|
+
proofMode: "ladder", // continuity.proof — "ladder" (default) | "require" | "off"
|
|
15
16
|
});
|
|
16
17
|
for (const tool of bundle.all) defaultCatalog.register(tool);
|
|
17
18
|
```
|
|
@@ -24,7 +25,7 @@ for (const tool of bundle.all) defaultCatalog.register(tool);
|
|
|
24
25
|
| `FocusWrite` | destructive (audit-and-allow, **no** justification — it's the hot loop) | Replace the focus body (capped at `focusMaxChars`) |
|
|
25
26
|
| `PlanRead` | readOnly | Numbered steps with ladder statuses; `claimed` rendered as unverified |
|
|
26
27
|
| `PlanUpdate` | destructive (audit-and-allow) | `create` / `add_step` / `set_step_status` (up to `claimed`) / `set_active` |
|
|
27
|
-
| `PlanComplete` | destructive | The **proven** transition: requires `evidence: [{toolUseId}]`, machine-checked against session event logs (child sessions included) |
|
|
28
|
+
| `PlanComplete` | destructive | The **proven** transition: requires `evidence: [{toolUseId}]`, machine-checked against session event logs (child sessions included); recorded unchecked under `continuity.proof: off` |
|
|
28
29
|
| `GoalWrite` | destructive | Create a goal (`goals.yaml`) |
|
|
29
30
|
| `GoalUpdate` | destructive | Update a goal; `proven` requires evidence like a step |
|
|
30
31
|
| `GoalList` | readOnly | List goals with progress |
|
|
@@ -35,6 +36,72 @@ rejects with `no verified evidence for tu_…: run the action first, then
|
|
|
35
36
|
complete the step with its toolUseId.` — the *bias toward action*: narration
|
|
36
37
|
can never produce a ✓.
|
|
37
38
|
|
|
39
|
+
## Proof modes (`continuity.proof`)
|
|
40
|
+
|
|
41
|
+
`createPlanTools({ proofMode })` takes the spec's `continuity.proof`, which
|
|
42
|
+
memory-service passes through. The modes are enforced since 0.8.0
|
|
43
|
+
(design/0.8.0-provability-release.md §6.3); before that, `require` and `off`
|
|
44
|
+
ran the ladder and printed a boot note.
|
|
45
|
+
|
|
46
|
+
| Mode | `claimed` | `proven` | Completion below `proven` |
|
|
47
|
+
| --- | --- | --- | --- |
|
|
48
|
+
| `ladder` (default) | free | evidence verified against the session event log | — (the pre-0.8.0 proof behaviour and tool descriptions) |
|
|
49
|
+
| `require` | free — honest progress | verified, exactly as `ladder` | refused, with an error that teaches the proof |
|
|
50
|
+
| `off` | free | the cited toolUseIds are **recorded, not resolved** | — |
|
|
51
|
+
|
|
52
|
+
**`require`.** Nothing below `proven` completes anything. Plans have no
|
|
53
|
+
close operation: a plan is complete exactly when every step is proven, so a
|
|
54
|
+
claimed step leaves it open. Once nothing is left open or in progress, the
|
|
55
|
+
`PlanUpdate` and `PlanComplete` replies name the steps that still need proof
|
|
56
|
+
(`plan-0001 is not complete: step 3 is claimed, not proven — prove it with
|
|
57
|
+
PlanComplete({step, evidence: [{toolUseId}]}) first.`). The one way the
|
|
58
|
+
store could mark something achieved below `proven` is a goal's progress
|
|
59
|
+
(`current`, read as 0 when unset) reaching its `target`, which is how the
|
|
60
|
+
Thredz mirror completes a goal. So `GoalWrite` refuses a goal created at its
|
|
61
|
+
target (a `target: 0` goal included), and `GoalUpdate` refuses progress that
|
|
62
|
+
reaches it unless the same call proves the goal (`status: "proven"` with
|
|
63
|
+
evidence). `GoalUpdate`'s check runs inside the store lock on the record as
|
|
64
|
+
written, so two sessions cannot each pass it on a stale read. The tool
|
|
65
|
+
descriptions under `require` say that only a proven step or goal is done.
|
|
66
|
+
|
|
67
|
+
Two kinds of `proven` record count as below `proven` under `require` until a
|
|
68
|
+
verified proof replaces them: one proven by assertion in an earlier `off`
|
|
69
|
+
run, and one with no frozen proof at all. The store never writes the
|
|
70
|
+
second, so it is a hand edit, or a step line that a multi-line plan title
|
|
71
|
+
smuggled into a plan file before titles were kept to one line. Every surface
|
|
72
|
+
counts them the way the tools do (continuity-store's `countsAsProven`): such
|
|
73
|
+
a goal stays among the open goals in the `<current_plan>` tail and in
|
|
74
|
+
`handoff.md`, such a step of the active plan is named in the handoff's Next
|
|
75
|
+
actions as `Prove: …`, another plan holding one stays among the handoff's
|
|
76
|
+
open plans with the step left out of its proven count, and the Thredz goal
|
|
77
|
+
mirror does not complete the remote goal.
|
|
78
|
+
`createPlanTools` passes its `proofMode` to the store it builds, so that
|
|
79
|
+
store's handoff agrees with the tools.
|
|
80
|
+
|
|
81
|
+
**`off`** is for harnesses that keep **no session event log**, so there is
|
|
82
|
+
nothing to resolve a toolUseId against. `PlanComplete` and
|
|
83
|
+
`GoalUpdate({status: "proven"})` still require cited evidence, but record it
|
|
84
|
+
as unverified refs (never as frozen proofs) and pin no session for
|
|
85
|
+
retention. Each cited id must be one token (letters, digits, `_ . : -`, at
|
|
86
|
+
most 256 characters), and a record holds at most 32, so a citation cannot
|
|
87
|
+
inject a forged line into a render or inflate a state file. `PlanRead`,
|
|
88
|
+
`GoalList`, the `<current_plan>` tail and `handoff.md` show
|
|
89
|
+
`[proven (unverified)]`, never `[proven]`. A step already proven with
|
|
90
|
+
verified evidence keeps its ✓: an unverified proof never downgrades it.
|
|
91
|
+
|
|
92
|
+
**One line per record, in every mode** (0.8.0, the default `ladder`
|
|
93
|
+
included). A plan or goal title, and a goal unit, is stored as one line:
|
|
94
|
+
every whitespace run, newlines included, collapses to one space and the ends
|
|
95
|
+
are trimmed, as step text always was. A title over 512 characters
|
|
96
|
+
(`MAX_TITLE_CHARS`) or a unit over 64 (`MAX_UNIT_CHARS`) is refused with
|
|
97
|
+
nothing written. Before 0.8.0 a title was only trimmed and a unit kept as
|
|
98
|
+
given, so a multi-line title could carry a forged `[proven]` step line into
|
|
99
|
+
a plan file. The default mode can therefore refuse a `PlanUpdate`,
|
|
100
|
+
`GoalWrite` or `GoalUpdate` it accepted before (an oversized title or unit,
|
|
101
|
+
or a `GoalUpdate` title that is only whitespace, as a new title already
|
|
102
|
+
could not be), and a multi-line title now reads back on one line. See
|
|
103
|
+
continuity-store's "One line per record".
|
|
104
|
+
|
|
38
105
|
## Event emission (decoupled)
|
|
39
106
|
|
|
40
107
|
Mutations emit the additive event-log kinds `plan_update`, `goal_update`, and
|
|
@@ -42,7 +109,10 @@ Mutations emit the additive event-log kinds `plan_update`, `goal_update`, and
|
|
|
42
109
|
`appendEvent` callback — this package never imports runtime wiring; emitters
|
|
43
110
|
or memory-service decide where events land (session JSONL, trace bus, both).
|
|
44
111
|
Rejected proof attempts emit `action_proof` too (`missing` / `error_result`),
|
|
45
|
-
so the audit trail records proof pressure, not just wins
|
|
112
|
+
so the audit trail records proof pressure, not just wins; under
|
|
113
|
+
`proofMode: "off"` the verdict is `unverified` — recorded, never checked —
|
|
114
|
+
and a goal proven that way says `verification: "unverified"` on its
|
|
115
|
+
`goal_update`.
|
|
46
116
|
|
|
47
117
|
## Pillar 3 notes
|
|
48
118
|
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,52 @@
|
|
|
31
31
|
* Rejected proof attempts emit `action_proof` too (verdict `missing` /
|
|
32
32
|
* `error_result`): the audit trail records proof pressure, not just wins.
|
|
33
33
|
*
|
|
34
|
+
* Proof modes (`continuity.proof`, design §2.4; enforced from 0.8.0,
|
|
35
|
+
* design/0.8.0-provability-release.md §6.3) — `createPlanTools({proofMode})`:
|
|
36
|
+
*
|
|
37
|
+
* ladder (default) the proof behaviour above, unchanged from before
|
|
38
|
+
* 0.8.0: `claimed` is free, `proven` needs verified evidence, and
|
|
39
|
+
* the tool descriptions are the pre-0.8.0 strings.
|
|
40
|
+
* require the ladder, plus: nothing below `proven` COMPLETES anything.
|
|
41
|
+
* `claimed` stays settable — it is honest progress — but the
|
|
42
|
+
* tools refuse every operation that would mark a goal achieved
|
|
43
|
+
* while it is below `proven`. The store has exactly one such
|
|
44
|
+
* path: a goal whose progress (`current`, 0 when unset) reaches
|
|
45
|
+
* its `target` (the Thredz mirror completes a goal on
|
|
46
|
+
* `currentValue >= targetValue`), so GoalWrite/GoalUpdate refuse
|
|
47
|
+
* that unless the same call proves the goal — GoalUpdate through
|
|
48
|
+
* a store guard evaluated under the store lock, so concurrent
|
|
49
|
+
* sessions cannot each pass a stale check. Plans have no close
|
|
50
|
+
* operation: a plan is complete exactly when every step is
|
|
51
|
+
* proven, which only PlanComplete's verified path reaches — so a
|
|
52
|
+
* claimed step leaves the plan open, and the tools say which
|
|
53
|
+
* steps still need proof. A record proven by assertion under an
|
|
54
|
+
* earlier `off` run counts as below `proven` here, and so does a
|
|
55
|
+
* `proven` record with no frozen proof (the store never writes one;
|
|
56
|
+
* see `verifiedProven`). The `<current_plan>` tail, handoff.md and
|
|
57
|
+
* the Thredz goal mirror count such a record the same way
|
|
58
|
+
* (continuity-store's `countsAsProven`): open, and not completed.
|
|
59
|
+
* off no verification, for harnesses that keep no session event log
|
|
60
|
+
* to resolve evidence against. PlanComplete / GoalUpdate `proven`
|
|
61
|
+
* still demand cited toolUseIds (one token each, see the store's
|
|
62
|
+
* `UNVERIFIED_TOOL_USE_ID_REGEX`), record them as UNVERIFIED refs
|
|
63
|
+
* (never frozen proofs) and pin no session for retention.
|
|
64
|
+
* PlanComplete emits `action_proof` with `verdict: "unverified"`;
|
|
65
|
+
* GoalUpdate marks its `goal_update` `verification: "unverified"`
|
|
66
|
+
* (`action_proof` is keyed by plan step). Every tool render says
|
|
67
|
+
* `[proven (unverified)]`, never `[proven]`.
|
|
68
|
+
*
|
|
69
|
+
* In EVERY mode, the ladder included (0.8.0, continuity-store's "one line
|
|
70
|
+
* per record"): a plan or goal title, and a goal unit, is stored as one line
|
|
71
|
+
* — every whitespace run, newlines included, collapses to one space and the
|
|
72
|
+
* ends are trimmed, as step text always was — and a title over 512
|
|
73
|
+
* characters (`MAX_TITLE_CHARS`) or a unit over 64 (`MAX_UNIT_CHARS`) is
|
|
74
|
+
* refused with nothing written. Before 0.8.0 the store only trimmed a title
|
|
75
|
+
* and kept a unit as given, so a multi-line title could carry a forged
|
|
76
|
+
* `[proven]` step line into a plan file, which defeated `require`. These caps, and a GoalUpdate title
|
|
77
|
+
* that is only whitespace (refused as GoalWrite's and PlanUpdate's always
|
|
78
|
+
* were), are the input the default mode now refuses that it used to accept.
|
|
79
|
+
*
|
|
34
80
|
* Pillar 3: plan/focus/ledger content is user-authored working state
|
|
35
81
|
* (origin "user" semantics, like tool-memory writes) — recalled wiki/fact
|
|
36
82
|
* content is what gets the new `memory` TrustOrigin, not this. All tools are
|
|
@@ -70,6 +116,22 @@ export type AppendContinuityEvent = (event: ContinuityEvent) => void | Promise<v
|
|
|
70
116
|
* → tail re-render), so a drift in either literal fails tests.
|
|
71
117
|
*/
|
|
72
118
|
export declare const PLAN_DIRTY_STATE_KEY = "plan.dirty";
|
|
119
|
+
/**
|
|
120
|
+
* `continuity.proof` (design §2.4, enforced from design/0.8.0 §6.3) — how the
|
|
121
|
+
* tools treat the `proven` rung. See the module docblock for each mode.
|
|
122
|
+
*/
|
|
123
|
+
export type ProofMode = "ladder" | "require" | "off";
|
|
124
|
+
export declare const PROOF_MODES: readonly ProofMode[];
|
|
125
|
+
/** The lowered default, and the only behaviour before 0.8.0. */
|
|
126
|
+
export declare const DEFAULT_PROOF_MODE: ProofMode;
|
|
127
|
+
/**
|
|
128
|
+
* Thrown under `proofMode: "require"` by an operation that would complete
|
|
129
|
+
* something below `proven`. The message teaches the proven path — the
|
|
130
|
+
* model's next call should be the proof, not a retry of the same claim.
|
|
131
|
+
*/
|
|
132
|
+
export declare class ProofRequiredError extends Error {
|
|
133
|
+
readonly name = "ProofRequiredError";
|
|
134
|
+
}
|
|
73
135
|
export type CreatePlanToolsOptions = {
|
|
74
136
|
readonly specName: string;
|
|
75
137
|
readonly rootDir?: string;
|
|
@@ -84,6 +146,8 @@ export type CreatePlanToolsOptions = {
|
|
|
84
146
|
/** Event-log append seam — see `AppendContinuityEvent`. */
|
|
85
147
|
readonly appendEvent?: AppendContinuityEvent;
|
|
86
148
|
readonly now?: () => Date;
|
|
149
|
+
/** `continuity.proof` — default {@link DEFAULT_PROOF_MODE} (`"ladder"`). */
|
|
150
|
+
readonly proofMode?: ProofMode;
|
|
87
151
|
};
|
|
88
152
|
export type PlanToolBundle = {
|
|
89
153
|
readonly focusRead: RegisteredTool;
|
package/dist/index.js
CHANGED
|
@@ -31,12 +31,58 @@
|
|
|
31
31
|
* Rejected proof attempts emit `action_proof` too (verdict `missing` /
|
|
32
32
|
* `error_result`): the audit trail records proof pressure, not just wins.
|
|
33
33
|
*
|
|
34
|
+
* Proof modes (`continuity.proof`, design §2.4; enforced from 0.8.0,
|
|
35
|
+
* design/0.8.0-provability-release.md §6.3) — `createPlanTools({proofMode})`:
|
|
36
|
+
*
|
|
37
|
+
* ladder (default) the proof behaviour above, unchanged from before
|
|
38
|
+
* 0.8.0: `claimed` is free, `proven` needs verified evidence, and
|
|
39
|
+
* the tool descriptions are the pre-0.8.0 strings.
|
|
40
|
+
* require the ladder, plus: nothing below `proven` COMPLETES anything.
|
|
41
|
+
* `claimed` stays settable — it is honest progress — but the
|
|
42
|
+
* tools refuse every operation that would mark a goal achieved
|
|
43
|
+
* while it is below `proven`. The store has exactly one such
|
|
44
|
+
* path: a goal whose progress (`current`, 0 when unset) reaches
|
|
45
|
+
* its `target` (the Thredz mirror completes a goal on
|
|
46
|
+
* `currentValue >= targetValue`), so GoalWrite/GoalUpdate refuse
|
|
47
|
+
* that unless the same call proves the goal — GoalUpdate through
|
|
48
|
+
* a store guard evaluated under the store lock, so concurrent
|
|
49
|
+
* sessions cannot each pass a stale check. Plans have no close
|
|
50
|
+
* operation: a plan is complete exactly when every step is
|
|
51
|
+
* proven, which only PlanComplete's verified path reaches — so a
|
|
52
|
+
* claimed step leaves the plan open, and the tools say which
|
|
53
|
+
* steps still need proof. A record proven by assertion under an
|
|
54
|
+
* earlier `off` run counts as below `proven` here, and so does a
|
|
55
|
+
* `proven` record with no frozen proof (the store never writes one;
|
|
56
|
+
* see `verifiedProven`). The `<current_plan>` tail, handoff.md and
|
|
57
|
+
* the Thredz goal mirror count such a record the same way
|
|
58
|
+
* (continuity-store's `countsAsProven`): open, and not completed.
|
|
59
|
+
* off no verification, for harnesses that keep no session event log
|
|
60
|
+
* to resolve evidence against. PlanComplete / GoalUpdate `proven`
|
|
61
|
+
* still demand cited toolUseIds (one token each, see the store's
|
|
62
|
+
* `UNVERIFIED_TOOL_USE_ID_REGEX`), record them as UNVERIFIED refs
|
|
63
|
+
* (never frozen proofs) and pin no session for retention.
|
|
64
|
+
* PlanComplete emits `action_proof` with `verdict: "unverified"`;
|
|
65
|
+
* GoalUpdate marks its `goal_update` `verification: "unverified"`
|
|
66
|
+
* (`action_proof` is keyed by plan step). Every tool render says
|
|
67
|
+
* `[proven (unverified)]`, never `[proven]`.
|
|
68
|
+
*
|
|
69
|
+
* In EVERY mode, the ladder included (0.8.0, continuity-store's "one line
|
|
70
|
+
* per record"): a plan or goal title, and a goal unit, is stored as one line
|
|
71
|
+
* — every whitespace run, newlines included, collapses to one space and the
|
|
72
|
+
* ends are trimmed, as step text always was — and a title over 512
|
|
73
|
+
* characters (`MAX_TITLE_CHARS`) or a unit over 64 (`MAX_UNIT_CHARS`) is
|
|
74
|
+
* refused with nothing written. Before 0.8.0 the store only trimmed a title
|
|
75
|
+
* and kept a unit as given, so a multi-line title could carry a forged
|
|
76
|
+
* `[proven]` step line into a plan file, which defeated `require`. These caps, and a GoalUpdate title
|
|
77
|
+
* that is only whitespace (refused as GoalWrite's and PlanUpdate's always
|
|
78
|
+
* were), are the input the default mode now refuses that it used to accept.
|
|
79
|
+
*
|
|
34
80
|
* Pillar 3: plan/focus/ledger content is user-authored working state
|
|
35
81
|
* (origin "user" semantics, like tool-memory writes) — recalled wiki/fact
|
|
36
82
|
* content is what gets the new `memory` TrustOrigin, not this. All tools are
|
|
37
83
|
* `scope: "internal"` (process-local files only).
|
|
38
84
|
*/
|
|
39
|
-
import { EvidenceError, createContinuityStore,
|
|
85
|
+
import { EvidenceError, createContinuityStore, isProvenByAssertion, isProvenVerified, renderLadderStatus, renderUnverifiedRef, } from "@crewhaus/continuity-store";
|
|
40
86
|
import { formatAgentIdentity } from "@crewhaus/run-context";
|
|
41
87
|
import { buildTool } from "@crewhaus/tool-builder";
|
|
42
88
|
import { z } from "zod";
|
|
@@ -51,6 +97,17 @@ import { z } from "zod";
|
|
|
51
97
|
* → tail re-render), so a drift in either literal fails tests.
|
|
52
98
|
*/
|
|
53
99
|
export const PLAN_DIRTY_STATE_KEY = "plan.dirty";
|
|
100
|
+
export const PROOF_MODES = ["ladder", "require", "off"];
|
|
101
|
+
/** The lowered default, and the only behaviour before 0.8.0. */
|
|
102
|
+
export const DEFAULT_PROOF_MODE = "ladder";
|
|
103
|
+
/**
|
|
104
|
+
* Thrown under `proofMode: "require"` by an operation that would complete
|
|
105
|
+
* something below `proven`. The message teaches the proven path — the
|
|
106
|
+
* model's next call should be the proof, not a retry of the same claim.
|
|
107
|
+
*/
|
|
108
|
+
export class ProofRequiredError extends Error {
|
|
109
|
+
name = "ProofRequiredError";
|
|
110
|
+
}
|
|
54
111
|
// ---------------------------------------------------------------------------
|
|
55
112
|
// schemas
|
|
56
113
|
// ---------------------------------------------------------------------------
|
|
@@ -151,22 +208,152 @@ function renderPlan(plan) {
|
|
|
151
208
|
lines.push(" (no steps yet — add some with PlanUpdate {action: 'add_step'})");
|
|
152
209
|
}
|
|
153
210
|
for (const step of plan.steps) {
|
|
154
|
-
const proofSuffix = step
|
|
155
|
-
? ` —
|
|
156
|
-
: ""
|
|
157
|
-
|
|
211
|
+
const proofSuffix = isProvenByAssertion(step)
|
|
212
|
+
? ` — cited, not verified: ${(step.unverified ?? []).map((r) => r.toolUseId).join(", ")}`
|
|
213
|
+
: step.status === "proven" && step.proofs.length > 0
|
|
214
|
+
? ` — proof: ${step.proofs.map((p) => p.toolUseId).join(", ")}`
|
|
215
|
+
: "";
|
|
216
|
+
lines.push(` ${step.index}. ${renderLadderStatus(step)} ${step.text}${proofSuffix}`);
|
|
158
217
|
}
|
|
159
218
|
return lines.join("\n");
|
|
160
219
|
}
|
|
161
|
-
function
|
|
162
|
-
|
|
220
|
+
function progressOf(goal) {
|
|
221
|
+
return goal.target !== undefined
|
|
163
222
|
? ` (${goal.current ?? 0}/${goal.target}${goal.unit !== undefined ? ` ${goal.unit}` : ""})`
|
|
164
223
|
: "";
|
|
165
|
-
|
|
224
|
+
}
|
|
225
|
+
function renderGoal(goal) {
|
|
226
|
+
return `- ${goal.id} ${renderLadderStatus(goal)} ${goal.title}${progressOf(goal)}`;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The `require`-mode reading of "achieved": progress at or past the target,
|
|
230
|
+
* with an unset `current` read as 0. That is how every surface shows a goal
|
|
231
|
+
* (`progressOf`, the handoff, the `<current_plan>` tail all print
|
|
232
|
+
* `current ?? 0`), and it is the Thredz goal mirror's completion rule
|
|
233
|
+
* (`currentValue >= targetValue`, `currentValue` defaulting to 0). So a
|
|
234
|
+
* `target: 0` goal is achieved the moment it exists.
|
|
235
|
+
*/
|
|
236
|
+
function atTarget(goal) {
|
|
237
|
+
return goal.target !== undefined && (goal.current ?? 0) >= goal.target;
|
|
238
|
+
}
|
|
239
|
+
/** The `require` label for a `proven` record with no frozen proof. */
|
|
240
|
+
const NO_PROOF_LABEL = "marked proven with no proof on record";
|
|
241
|
+
/**
|
|
242
|
+
* `proven` with machine-checked evidence on record (the store's
|
|
243
|
+
* `isProvenVerified`). Under `require`, two kinds of `proven` record are
|
|
244
|
+
* below this, and complete nothing until a verified proof replaces them: one
|
|
245
|
+
* proven by assertion (an earlier `off` run), and one with no frozen proof at
|
|
246
|
+
* all. The store never writes the second — every verified transition freezes
|
|
247
|
+
* at least one proof — so it was edited into a file by hand, or read back
|
|
248
|
+
* from a step line that a multi-line plan title smuggled into a plan file
|
|
249
|
+
* before the store kept titles to one line. Nobody checked either.
|
|
250
|
+
*/
|
|
251
|
+
function verifiedProven(record) {
|
|
252
|
+
return isProvenVerified(record);
|
|
253
|
+
}
|
|
254
|
+
/** How a below-proven record names itself in a `require` refusal. */
|
|
255
|
+
function belowProvenLabel(record) {
|
|
256
|
+
if (isProvenByAssertion(record))
|
|
257
|
+
return "proven (unverified)";
|
|
258
|
+
return record.status === "proven" ? NO_PROOF_LABEL : record.status;
|
|
259
|
+
}
|
|
260
|
+
/** `a`, `a or b`, `a, b or c`. */
|
|
261
|
+
function orList(items) {
|
|
262
|
+
return items.length > 2
|
|
263
|
+
? `${items.slice(0, -1).join(", ")} or ${items[items.length - 1]}`
|
|
264
|
+
: items.join(" or ");
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Under `require`, the note a plan earns once nothing is left open or in
|
|
268
|
+
* progress while some step is still below proven: claimed, proven by
|
|
269
|
+
* assertion under an earlier `off` run, or marked proven with no proof on
|
|
270
|
+
* record (see {@link verifiedProven}). That is the moment a model is
|
|
271
|
+
* likeliest to believe the plan is done, and it is not: each such step still
|
|
272
|
+
* needs a verified proof. Null while the plan still has unstarted work (the
|
|
273
|
+
* claim is then plainly not the end) or once every step is proven.
|
|
274
|
+
*/
|
|
275
|
+
function incompletePlanNote(plan) {
|
|
276
|
+
if (plan.steps.some((s) => s.status === "open" || s.status === "in_progress"))
|
|
277
|
+
return null;
|
|
278
|
+
const below = plan.steps.filter((s) => !verifiedProven(s));
|
|
279
|
+
if (below.length === 0)
|
|
280
|
+
return null;
|
|
281
|
+
const present = new Set(below.map(belowProvenLabel));
|
|
282
|
+
const labels = orList(["claimed", "proven (unverified)", NO_PROOF_LABEL].filter((l) => present.has(l)));
|
|
283
|
+
const which = below.length === 1
|
|
284
|
+
? `step ${below[0]?.index} is ${labels}, not proven — prove it`
|
|
285
|
+
: `steps ${below.map((s) => s.index).join(", ")} are ${labels}, not proven — prove each`;
|
|
286
|
+
return `${plan.id} is not complete: ${which} with PlanComplete({step, evidence: [{toolUseId}]}) first.`;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* The `require` refusal for a GoalWrite that would create a goal already at
|
|
290
|
+
* its target, or null when the goal starts below it.
|
|
291
|
+
*/
|
|
292
|
+
function goalWriteRefusal(input) {
|
|
293
|
+
if (input.target === undefined || !atTarget(input))
|
|
294
|
+
return null;
|
|
295
|
+
const progress = `${input.current ?? 0}/${input.target}${input.unit !== undefined ? ` ${input.unit}` : ""}`;
|
|
296
|
+
const head = `a new goal cannot start at its target (${progress}) under continuity.proof: require — that would mark it achieved without proof.`;
|
|
297
|
+
if (input.target > 0) {
|
|
298
|
+
return `${head} Create it without current (or below the target), then prove it with GoalUpdate({goalId, current: ${input.current}, status: "proven", evidence: [{toolUseId}]}).`;
|
|
299
|
+
}
|
|
300
|
+
// A target of 0 or below is met before any work: progress counts up from
|
|
301
|
+
// `current`, which reads as 0 when unset. Leaving out `current` cannot help.
|
|
302
|
+
return `${head} Progress counts up from current (0 when unset), so a target of ${input.target} is met before any work. Count up toward a positive target instead (for example, items done out of a total), or create the goal without a target and set it in the GoalUpdate that proves it: GoalUpdate({goalId, target: ${input.target}, status: "proven", evidence: [{toolUseId}]}).`;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* The `require` refusal for a GoalUpdate whose RESULT (`next`, the record
|
|
306
|
+
* as written) sits at its target below a verified proof, or null. `input`
|
|
307
|
+
* supplies the fields the reply tells the model to resend with its proof.
|
|
308
|
+
*/
|
|
309
|
+
function goalUpdateRefusal(goalId, input, next) {
|
|
310
|
+
if (!atTarget(next) || verifiedProven(next))
|
|
311
|
+
return null;
|
|
312
|
+
const fields = [
|
|
313
|
+
...(input.current !== undefined ? [`current: ${input.current}`] : []),
|
|
314
|
+
...(input.target !== undefined ? [`target: ${input.target}`] : []),
|
|
315
|
+
].join(", ");
|
|
316
|
+
const why = isProvenByAssertion(next)
|
|
317
|
+
? `${goalId} is proven (unverified), which under continuity.proof: require is not proven — reaching its target${progressOf(next)} would mark it achieved. Prove it again in the same call, with verified evidence`
|
|
318
|
+
: `${goalId} is ${belowProvenLabel(next)}, not proven — reaching its target${progressOf(next)} would mark it achieved. Prove it in the same call`;
|
|
319
|
+
return `${why}: GoalUpdate({goalId: "${goalId}", ${fields}, status: "proven", evidence: [{toolUseId}]}), citing the call that measured it.`;
|
|
166
320
|
}
|
|
167
321
|
function renderRequirement(req) {
|
|
168
322
|
return `- ${req.id} [${req.status}] ${JSON.stringify(req.text)} (user, ${req.source.sessionId}, turn ${req.source.turn})`;
|
|
169
323
|
}
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// per-mode descriptions — the ladder strings are the pre-0.8.0 bytes
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
const PLAN_READ_DESCRIPTION = {
|
|
328
|
+
ladder: "Read a plan's numbered steps with their proof-ladder statuses. 'claimed' steps are rendered as unverified — only 'proven' steps have machine-checked evidence. Omit planId for the active plan.",
|
|
329
|
+
require: "Read a plan's numbered steps with their proof-ladder statuses. 'claimed' steps are rendered as unverified — only 'proven' steps have machine-checked evidence, and a plan is complete only when every step is proven. Omit planId for the active plan.",
|
|
330
|
+
off: "Read a plan's numbered steps with their proof-ladder statuses. Proof checking is off in this harness: 'claimed' and 'proven (unverified)' steps are both the model's own account, and nothing is machine-checked. Omit planId for the active plan.",
|
|
331
|
+
};
|
|
332
|
+
const PLAN_UPDATE_DESCRIPTION = {
|
|
333
|
+
ladder: "Mutate the persisted plan: create a plan, add a step, move a step's ladder status (open → in_progress → claimed), or set the active plan. 'claimed' records progress WITHOUT verification — the 'proven' transition goes through PlanComplete with evidence.",
|
|
334
|
+
require: "Mutate the persisted plan: create a plan, add a step, move a step's ladder status (open → in_progress → claimed), or set the active plan. 'claimed' records progress WITHOUT verification and completes nothing — a step is done only once PlanComplete proves it with evidence.",
|
|
335
|
+
off: "Mutate the persisted plan: create a plan, add a step, move a step's ladder status (open → in_progress → claimed), or set the active plan. 'claimed' records progress; the 'proven' transition goes through PlanComplete, which in this harness records the cited evidence without checking it.",
|
|
336
|
+
};
|
|
337
|
+
const PLAN_COMPLETE_DESCRIPTION = {
|
|
338
|
+
ladder: "The proven transition (machine-checked): mark a plan step proven by citing the toolUseId(s) of the calls that did the work. Each id is resolved against the session event logs (child sessions included); missing or errored results are rejected. Do the work first, then complete the step.",
|
|
339
|
+
require: "The proven transition (machine-checked): mark a plan step proven by citing the toolUseId(s) of the calls that did the work. Each id is resolved against the session event logs (child sessions included); missing or errored results are rejected. This is the ONLY way a step completes — 'claimed' never counts as done. Do the work first, then complete the step.",
|
|
340
|
+
off: "Mark a plan step proven by citing the toolUseId(s) of the calls that did the work. Proof checking is off in this harness: the ids are RECORDED as unverified refs, not resolved against an event log, and the step reads 'proven (unverified)'. Cite only calls you actually ran. Do the work first, then complete the step.",
|
|
341
|
+
};
|
|
342
|
+
const GOAL_WRITE_DESCRIPTION = {
|
|
343
|
+
ladder: "Create a persisted goal ({title, target?, current?, unit?}) in goals.yaml. Goals carry the same open → in_progress → claimed → proven ladder as plan steps.",
|
|
344
|
+
require: "Create a persisted goal ({title, target?, current?, unit?}) in goals.yaml. Goals carry the same open → in_progress → claimed → proven ladder as plan steps, and reach their target only as proven — create a goal below its target, then prove it with GoalUpdate.",
|
|
345
|
+
off: "Create a persisted goal ({title, target?, current?, unit?}) in goals.yaml. Goals carry the same open → in_progress → claimed → proven ladder as plan steps.",
|
|
346
|
+
};
|
|
347
|
+
const GOAL_UPDATE_DESCRIPTION = {
|
|
348
|
+
ladder: "Update a goal: title, target/current progress, or ladder status. 'claimed' is free; 'proven' requires evidence [{toolUseId}] verified against the session event logs, exactly like PlanComplete.",
|
|
349
|
+
require: "Update a goal: title, target/current progress, or ladder status. 'claimed' is free; 'proven' requires evidence [{toolUseId}] verified against the session event logs, exactly like PlanComplete. A goal reaches its target only as proven: set current to the target in the same call as status 'proven' with evidence.",
|
|
350
|
+
off: "Update a goal: title, target/current progress, or ladder status. 'claimed' is free; 'proven' requires evidence [{toolUseId}], which this harness records WITHOUT checking it (the goal reads 'proven (unverified)').",
|
|
351
|
+
};
|
|
352
|
+
const GOAL_LIST_DESCRIPTION = {
|
|
353
|
+
ladder: "List persisted goals with ladder statuses and progress. 'claimed' goals are rendered as unverified.",
|
|
354
|
+
require: "List persisted goals with ladder statuses and progress. 'claimed' goals are rendered as unverified.",
|
|
355
|
+
off: "List persisted goals with ladder statuses and progress. 'claimed' and 'proven (unverified)' goals are both unchecked.",
|
|
356
|
+
};
|
|
170
357
|
/** Thrown when a plan-scoped call has neither a planId nor an active plan. */
|
|
171
358
|
export class NoActivePlanError extends Error {
|
|
172
359
|
name = "NoActivePlanError";
|
|
@@ -180,6 +367,13 @@ export class NoActivePlanError extends Error {
|
|
|
180
367
|
* `for (const t of bundle.all) defaultCatalog.register(t)`).
|
|
181
368
|
*/
|
|
182
369
|
export function createPlanTools(opts) {
|
|
370
|
+
const proofMode = opts.proofMode ?? DEFAULT_PROOF_MODE;
|
|
371
|
+
// A hand-built fragment can carry any string; a typo'd mode must not
|
|
372
|
+
// quietly become the ladder (the spec schema rejects it at compile time —
|
|
373
|
+
// this is the same refusal for callers that skip the compiler).
|
|
374
|
+
if (!PROOF_MODES.includes(proofMode)) {
|
|
375
|
+
throw new Error(`createPlanTools: proofMode "${String(proofMode)}" is not one of ${PROOF_MODES.join(" | ")}.`);
|
|
376
|
+
}
|
|
183
377
|
const store = opts.store ??
|
|
184
378
|
createContinuityStore({
|
|
185
379
|
specName: opts.specName,
|
|
@@ -188,6 +382,8 @@ export function createPlanTools(opts) {
|
|
|
188
382
|
...(opts.sessionRootDir !== undefined ? { sessionRootDir: opts.sessionRootDir } : {}),
|
|
189
383
|
...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
|
|
190
384
|
...(opts.now !== undefined ? { now: opts.now } : {}),
|
|
385
|
+
// The store's handoff.md counts `proven` as these tools do (§6.3).
|
|
386
|
+
proofMode,
|
|
191
387
|
});
|
|
192
388
|
async function emit(event) {
|
|
193
389
|
if (opts.appendEvent !== undefined)
|
|
@@ -264,7 +460,7 @@ export function createPlanTools(opts) {
|
|
|
264
460
|
});
|
|
265
461
|
const planRead = buildTool({
|
|
266
462
|
name: "PlanRead",
|
|
267
|
-
description:
|
|
463
|
+
description: PLAN_READ_DESCRIPTION[proofMode],
|
|
268
464
|
inputSchema: planReadSchema,
|
|
269
465
|
readOnly: true,
|
|
270
466
|
execute: async (input) => {
|
|
@@ -289,7 +485,7 @@ export function createPlanTools(opts) {
|
|
|
289
485
|
});
|
|
290
486
|
const planUpdate = buildTool({
|
|
291
487
|
name: "PlanUpdate",
|
|
292
|
-
description:
|
|
488
|
+
description: PLAN_UPDATE_DESCRIPTION[proofMode],
|
|
293
489
|
inputSchema: planUpdateSchema,
|
|
294
490
|
destructive: true, // audit-and-allow (§7.4): a judge call per routine plan update would drown the loop
|
|
295
491
|
execute: async (input, ctx) => {
|
|
@@ -318,7 +514,7 @@ export function createPlanTools(opts) {
|
|
|
318
514
|
}
|
|
319
515
|
case "set_step_status": {
|
|
320
516
|
const planId = await resolvePlanId(input.planId);
|
|
321
|
-
await store.setStepStatus(planId, input.step, input.status);
|
|
517
|
+
const plan = await store.setStepStatus(planId, input.step, input.status);
|
|
322
518
|
await emit({
|
|
323
519
|
kind: "plan_update",
|
|
324
520
|
payload: {
|
|
@@ -330,6 +526,12 @@ export function createPlanTools(opts) {
|
|
|
330
526
|
},
|
|
331
527
|
});
|
|
332
528
|
markPlanDirty(ctx);
|
|
529
|
+
if (proofMode === "require" && input.status === "claimed") {
|
|
530
|
+
// `claimed` stays settable under require — it is honest progress
|
|
531
|
+
// — but the reply says plainly that it completes nothing.
|
|
532
|
+
const note = incompletePlanNote(plan);
|
|
533
|
+
return `${planId} step ${input.step} → claimed (unverified — under continuity.proof: require it completes nothing; prove it with PlanComplete({step: ${input.step}, evidence: [{toolUseId}]}))${note !== null ? `\n${note}` : ""}`;
|
|
534
|
+
}
|
|
333
535
|
return `${planId} step ${input.step} → ${input.status}${input.status === "claimed" ? " (unverified — prove it with PlanComplete)" : ""}`;
|
|
334
536
|
}
|
|
335
537
|
case "set_active": {
|
|
@@ -346,7 +548,7 @@ export function createPlanTools(opts) {
|
|
|
346
548
|
});
|
|
347
549
|
const planComplete = buildTool({
|
|
348
550
|
name: "PlanComplete",
|
|
349
|
-
description:
|
|
551
|
+
description: PLAN_COMPLETE_DESCRIPTION[proofMode],
|
|
350
552
|
inputSchema: planCompleteSchema,
|
|
351
553
|
destructive: true,
|
|
352
554
|
execute: async (input, ctx) => {
|
|
@@ -360,6 +562,48 @@ export function createPlanTools(opts) {
|
|
|
360
562
|
? { sessionId }
|
|
361
563
|
: {}),
|
|
362
564
|
}));
|
|
565
|
+
if (proofMode === "off") {
|
|
566
|
+
// design/0.8.0 §6.3 — no verification: the step is proven by
|
|
567
|
+
// assertion, the citations are recorded unresolved, no session is
|
|
568
|
+
// pinned, and the audit trail says `unverified`, never `verified`.
|
|
569
|
+
const plan = await store.proveStep(planId, input.step, refs, {
|
|
570
|
+
verification: "unverified",
|
|
571
|
+
});
|
|
572
|
+
const step = plan.steps.find((s) => s.index === input.step);
|
|
573
|
+
if (step === undefined || !isProvenByAssertion(step)) {
|
|
574
|
+
// The store kept an earlier machine-checked proof rather than
|
|
575
|
+
// downgrade it — nothing new was recorded, so nothing is emitted.
|
|
576
|
+
return `${planId} step ${input.step} is already proven with verified evidence — nothing recorded.`;
|
|
577
|
+
}
|
|
578
|
+
const cited = new Set(refs.map((r) => r.toolUseId));
|
|
579
|
+
const recorded = (step.unverified ?? []).filter((r) => cited.has(r.toolUseId));
|
|
580
|
+
for (const ref of recorded) {
|
|
581
|
+
await emit({
|
|
582
|
+
kind: "action_proof",
|
|
583
|
+
payload: {
|
|
584
|
+
planId,
|
|
585
|
+
step: input.step,
|
|
586
|
+
toolUseId: ref.toolUseId,
|
|
587
|
+
verdict: "unverified",
|
|
588
|
+
...identityOf(ctx),
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
await emit({
|
|
593
|
+
kind: "plan_update",
|
|
594
|
+
payload: {
|
|
595
|
+
planId,
|
|
596
|
+
action: "prove_step",
|
|
597
|
+
step: input.step,
|
|
598
|
+
status: "proven",
|
|
599
|
+
...identityOf(ctx),
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
markPlanDirty(ctx);
|
|
603
|
+
return `${planId} step ${input.step} proven (unverified) — continuity.proof is off, so the evidence was recorded, not checked: ${recorded
|
|
604
|
+
.map(renderUnverifiedRef)
|
|
605
|
+
.join(", ")}`;
|
|
606
|
+
}
|
|
363
607
|
try {
|
|
364
608
|
const plan = await store.proveStep(planId, input.step, refs);
|
|
365
609
|
const step = plan.steps.find((s) => s.index === input.step);
|
|
@@ -388,9 +632,14 @@ export function createPlanTools(opts) {
|
|
|
388
632
|
},
|
|
389
633
|
});
|
|
390
634
|
markPlanDirty(ctx);
|
|
391
|
-
|
|
635
|
+
const reply = `${planId} step ${input.step} proven — evidence: ${refs
|
|
392
636
|
.map((r) => r.toolUseId)
|
|
393
637
|
.join(", ")}`;
|
|
638
|
+
// Under require, proving the last open step of a plan whose other
|
|
639
|
+
// steps are claimed (or proven only by assertion) does not finish
|
|
640
|
+
// it; say which steps still need a verified proof.
|
|
641
|
+
const note = proofMode === "require" ? incompletePlanNote(plan) : null;
|
|
642
|
+
return note !== null ? `${reply}\n${note}` : reply;
|
|
394
643
|
}
|
|
395
644
|
catch (err) {
|
|
396
645
|
if (err instanceof EvidenceError) {
|
|
@@ -412,10 +661,15 @@ export function createPlanTools(opts) {
|
|
|
412
661
|
});
|
|
413
662
|
const goalWrite = buildTool({
|
|
414
663
|
name: "GoalWrite",
|
|
415
|
-
description:
|
|
664
|
+
description: GOAL_WRITE_DESCRIPTION[proofMode],
|
|
416
665
|
inputSchema: goalWriteSchema,
|
|
417
666
|
destructive: true,
|
|
418
667
|
execute: async (input, ctx) => {
|
|
668
|
+
// A new goal's record comes from the input alone, so checking the
|
|
669
|
+
// input is checking the record (no lock needed, unlike GoalUpdate).
|
|
670
|
+
const refusal = proofMode === "require" ? goalWriteRefusal(input) : null;
|
|
671
|
+
if (refusal !== null)
|
|
672
|
+
throw new ProofRequiredError(refusal);
|
|
419
673
|
const goal = await store.writeGoal({
|
|
420
674
|
title: input.title,
|
|
421
675
|
...(input.target !== undefined ? { target: input.target } : {}),
|
|
@@ -432,7 +686,7 @@ export function createPlanTools(opts) {
|
|
|
432
686
|
});
|
|
433
687
|
const goalUpdate = buildTool({
|
|
434
688
|
name: "GoalUpdate",
|
|
435
|
-
description:
|
|
689
|
+
description: GOAL_UPDATE_DESCRIPTION[proofMode],
|
|
436
690
|
inputSchema: goalUpdateSchema,
|
|
437
691
|
destructive: true,
|
|
438
692
|
execute: async (input, ctx) => {
|
|
@@ -445,20 +699,67 @@ export function createPlanTools(opts) {
|
|
|
445
699
|
? { sessionId }
|
|
446
700
|
: {}),
|
|
447
701
|
}));
|
|
448
|
-
|
|
702
|
+
// `require`: the store's one completion path below proven is progress
|
|
703
|
+
// reaching the target (the Thredz mirror completes the goal on it), so
|
|
704
|
+
// an update that moves progress is refused when the goal would end up
|
|
705
|
+
// at its target without a verified proof. Only progress moves are
|
|
706
|
+
// policed: a title edit on a goal already sitting at its target
|
|
707
|
+
// completes nothing new.
|
|
708
|
+
const guarded = proofMode === "require" && (input.current !== undefined || input.target !== undefined);
|
|
709
|
+
const refuseUnproven = (next) => {
|
|
710
|
+
const why = goalUpdateRefusal(input.goalId, input, next);
|
|
711
|
+
if (why !== null)
|
|
712
|
+
throw new ProofRequiredError(why);
|
|
713
|
+
};
|
|
714
|
+
if (guarded) {
|
|
715
|
+
// Early refusal on a snapshot, before any evidence is resolved; it
|
|
716
|
+
// holds whatever store wrapper is in use. The authoritative check is
|
|
717
|
+
// the store guard below, which sees the record as written under the
|
|
718
|
+
// lock, so two sessions cannot each pass on a stale read. An unknown
|
|
719
|
+
// id falls through to the store's own "no goal" error.
|
|
720
|
+
const existing = (await store.listGoals()).find((g) => g.id === input.goalId);
|
|
721
|
+
// A call that sets `proven` resolves its evidence (require is the
|
|
722
|
+
// ladder there), so it lands verified — with a frozen proof,
|
|
723
|
+
// replacing any assertion — or not at all; the snapshot has nothing
|
|
724
|
+
// to refuse in it.
|
|
725
|
+
if (existing !== undefined && input.status !== "proven") {
|
|
726
|
+
refuseUnproven({
|
|
727
|
+
status: input.status ?? existing.status,
|
|
728
|
+
unverified: existing.unverified,
|
|
729
|
+
proofs: existing.proofs,
|
|
730
|
+
target: input.target ?? existing.target,
|
|
731
|
+
current: input.current ?? existing.current,
|
|
732
|
+
unit: input.unit ?? existing.unit,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const patch = {
|
|
449
737
|
...(input.title !== undefined ? { title: input.title } : {}),
|
|
450
738
|
...(input.status !== undefined ? { status: input.status } : {}),
|
|
451
739
|
...(input.target !== undefined ? { target: input.target } : {}),
|
|
452
740
|
...(input.current !== undefined ? { current: input.current } : {}),
|
|
453
741
|
...(input.unit !== undefined ? { unit: input.unit } : {}),
|
|
454
742
|
...(evidence !== undefined ? { evidence } : {}),
|
|
455
|
-
}
|
|
743
|
+
};
|
|
744
|
+
// `off` records a proven goal's evidence unresolved (design/0.8.0
|
|
745
|
+
// §6.3); `require` hands the store its at-target guard; the ladder
|
|
746
|
+
// makes the exact pre-0.8.0 call.
|
|
747
|
+
const goal = proofMode === "off"
|
|
748
|
+
? await store.updateGoal(input.goalId, patch, { verification: "unverified" })
|
|
749
|
+
: guarded
|
|
750
|
+
? await store.updateGoal(input.goalId, patch, { guard: (next) => refuseUnproven(next) })
|
|
751
|
+
: await store.updateGoal(input.goalId, patch);
|
|
752
|
+
// `action_proof` is keyed by plan step, so a goal proven by assertion
|
|
753
|
+
// is marked on its own `goal_update` instead: the log must not show it
|
|
754
|
+
// the way it shows a verified goal proof.
|
|
755
|
+
const asserted = input.status === "proven" && isProvenByAssertion(goal);
|
|
456
756
|
await emit({
|
|
457
757
|
kind: "goal_update",
|
|
458
758
|
payload: {
|
|
459
759
|
goalId: goal.id,
|
|
460
760
|
action: "update",
|
|
461
761
|
...(input.status !== undefined ? { status: input.status } : {}),
|
|
762
|
+
...(asserted ? { verification: "unverified" } : {}),
|
|
462
763
|
...identityOf(ctx),
|
|
463
764
|
},
|
|
464
765
|
});
|
|
@@ -468,7 +769,7 @@ export function createPlanTools(opts) {
|
|
|
468
769
|
});
|
|
469
770
|
const goalList = buildTool({
|
|
470
771
|
name: "GoalList",
|
|
471
|
-
description:
|
|
772
|
+
description: GOAL_LIST_DESCRIPTION[proofMode],
|
|
472
773
|
inputSchema: goalListSchema,
|
|
473
774
|
readOnly: true,
|
|
474
775
|
execute: async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/tool-plan",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "v0.3.0 Goal 1 — Focus*/Plan*/Goal*/MemoryClear RegisteredTools over @crewhaus/continuity-store: the claimed→proven proof ladder as a tool surface, with plan_update/goal_update/action_proof event emission via an injected append seam.",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,18 +8,19 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js"
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
12
13
|
}
|
|
13
14
|
},
|
|
14
15
|
"scripts": {
|
|
15
16
|
"test": "bun test src"
|
|
16
17
|
},
|
|
17
18
|
"dependencies": {
|
|
18
|
-
"@crewhaus/continuity-store": "0.
|
|
19
|
-
"@crewhaus/event-log": "0.
|
|
20
|
-
"@crewhaus/run-context": "0.
|
|
21
|
-
"@crewhaus/tool-builder": "0.
|
|
22
|
-
"@crewhaus/tool-catalog": "0.
|
|
19
|
+
"@crewhaus/continuity-store": "0.8.0",
|
|
20
|
+
"@crewhaus/event-log": "0.8.0",
|
|
21
|
+
"@crewhaus/run-context": "0.8.0",
|
|
22
|
+
"@crewhaus/tool-builder": "0.8.0",
|
|
23
|
+
"@crewhaus/tool-catalog": "0.8.0",
|
|
23
24
|
"zod": "^3.23.8"
|
|
24
25
|
},
|
|
25
26
|
"license": "Apache-2.0",
|
|
@@ -40,5 +41,8 @@
|
|
|
40
41
|
"publishConfig": {
|
|
41
42
|
"access": "public"
|
|
42
43
|
},
|
|
43
|
-
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
44
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"],
|
|
45
|
+
"engines": {
|
|
46
|
+
"bun": ">=1.3.14"
|
|
47
|
+
}
|
|
44
48
|
}
|