@crewhaus/tool-plan 0.7.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 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, renderStatus, } from "@crewhaus/continuity-store";
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.status === "proven" && step.proofs.length > 0
155
- ? ` — proof: ${step.proofs.map((p) => p.toolUseId).join(", ")}`
156
- : "";
157
- lines.push(` ${step.index}. ${renderStatus(step.status)} ${step.text}${proofSuffix}`);
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 renderGoal(goal) {
162
- const progress = goal.target !== undefined
220
+ function progressOf(goal) {
221
+ return goal.target !== undefined
163
222
  ? ` (${goal.current ?? 0}/${goal.target}${goal.unit !== undefined ? ` ${goal.unit}` : ""})`
164
223
  : "";
165
- return `- ${goal.id} ${renderStatus(goal.status)} ${goal.title}${progress}`;
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: "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.",
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: "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.",
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: "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.",
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
- return `${planId} step ${input.step} proven — evidence: ${refs
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: "Create a persisted goal ({title, target?, current?, unit?}) in goals.yaml. Goals carry the same open → in_progress → claimed → proven ladder as plan steps.",
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: "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.",
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
- const goal = await store.updateGoal(input.goalId, {
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: "List persisted goals with ladder statuses and progress. 'claimed' goals are rendered as unverified.",
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.7.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.7.0",
19
- "@crewhaus/event-log": "0.7.0",
20
- "@crewhaus/run-context": "0.7.0",
21
- "@crewhaus/tool-builder": "0.7.0",
22
- "@crewhaus/tool-catalog": "0.7.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
  }