@crewhaus/tool-plan 0.3.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/README.md +54 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +524 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# @crewhaus/tool-plan
|
|
2
|
+
|
|
3
|
+
v0.3.0 Goal 1 (design §2.4/§2.6): the continuity tool surface —
|
|
4
|
+
RegisteredTools over [`@crewhaus/continuity-store`](../continuity-store),
|
|
5
|
+
packaged like `tool-memory` (one factory bound to a spec name; the caller
|
|
6
|
+
registers the tools into the runtime catalog).
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { createPlanTools } from "@crewhaus/tool-plan";
|
|
10
|
+
import { defaultCatalog } from "@crewhaus/tool-catalog";
|
|
11
|
+
|
|
12
|
+
const bundle = createPlanTools({
|
|
13
|
+
specName: "support-bot",
|
|
14
|
+
appendEvent: (e) => eventLog.append(e), // plan_update / goal_update / action_proof
|
|
15
|
+
});
|
|
16
|
+
for (const tool of bundle.all) defaultCatalog.register(tool);
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## The tool table
|
|
20
|
+
|
|
21
|
+
| Tool | Flags | What it does |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `FocusRead` | readOnly | Focus body + active plan pointer + REQ ledger — check first, never re-ask answered questions |
|
|
24
|
+
| `FocusWrite` | destructive (audit-and-allow, **no** justification — it's the hot loop) | Replace the focus body (capped at `focusMaxChars`) |
|
|
25
|
+
| `PlanRead` | readOnly | Numbered steps with ladder statuses; `claimed` rendered as unverified |
|
|
26
|
+
| `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
|
+
| `GoalWrite` | destructive | Create a goal (`goals.yaml`) |
|
|
29
|
+
| `GoalUpdate` | destructive | Update a goal; `proven` requires evidence like a step |
|
|
30
|
+
| `GoalList` | readOnly | List goals with progress |
|
|
31
|
+
| `MemoryClear` | destructive + **requireJustification: true** | Clear focus/plans/goals/all — via trash, restorable, never hard-deleted |
|
|
32
|
+
|
|
33
|
+
The `proven` gate teaches instead of blocking silently: a bad citation
|
|
34
|
+
rejects with `no verified evidence for tu_…: run the action first, then
|
|
35
|
+
complete the step with its toolUseId.` — the *bias toward action*: narration
|
|
36
|
+
can never produce a ✓.
|
|
37
|
+
|
|
38
|
+
## Event emission (decoupled)
|
|
39
|
+
|
|
40
|
+
Mutations emit the additive event-log kinds `plan_update`, `goal_update`, and
|
|
41
|
+
`action_proof {planId, step, toolUseId, verdict}` through the **injected**
|
|
42
|
+
`appendEvent` callback — this package never imports runtime wiring; emitters
|
|
43
|
+
or memory-service decide where events land (session JSONL, trace bus, both).
|
|
44
|
+
Rejected proof attempts emit `action_proof` too (`missing` / `error_result`),
|
|
45
|
+
so the audit trail records proof pressure, not just wins.
|
|
46
|
+
|
|
47
|
+
## Pillar 3 notes
|
|
48
|
+
|
|
49
|
+
Plan/focus/ledger content is user-authored working state (origin `"user"`
|
|
50
|
+
semantics, like tool-memory writes). All tools are `scope: "internal"`
|
|
51
|
+
(process-local files). `MemoryClear` carries the intent gate
|
|
52
|
+
(`requireJustification: true`) because model-initiated forgetting is rare and
|
|
53
|
+
destructive-by-intent; routine Focus/Plan updates are audit-and-allow so a
|
|
54
|
+
judge call per update doesn't drown the loop (§7.4).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.3.0 Goal 1 — `tool-plan` (design §2.4/§2.6, PR 7). RegisteredTools over
|
|
3
|
+
* `@crewhaus/continuity-store`, packaged like `tool-memory`: one factory
|
|
4
|
+
* bound to a spec name, returning tools the caller registers into the
|
|
5
|
+
* runtime catalog.
|
|
6
|
+
*
|
|
7
|
+
* The tool table (product-DX §2.6 + proof ladder §2.4):
|
|
8
|
+
*
|
|
9
|
+
* FocusRead readOnly — current focus, active plan pointer, REQ ledger
|
|
10
|
+
* FocusWrite destructive — the hot loop: audit-and-allow, deliberately
|
|
11
|
+
* NO requireJustification (a judge call per
|
|
12
|
+
* routine focus update would drown the loop)
|
|
13
|
+
* PlanRead readOnly
|
|
14
|
+
* PlanUpdate destructive — create plans/steps, ladder moves UP TO
|
|
15
|
+
* `claimed`, active-plan pointer; audit-and-allow
|
|
16
|
+
* PlanComplete destructive — the `proven` transition: requires
|
|
17
|
+
* evidence [{toolUseId}], machine-checked
|
|
18
|
+
* against session event logs (child sessions
|
|
19
|
+
* included); rejects teach, never bluff
|
|
20
|
+
* GoalWrite destructive
|
|
21
|
+
* GoalUpdate destructive — `proven` requires evidence, like steps
|
|
22
|
+
* GoalList readOnly
|
|
23
|
+
* MemoryClear destructive + requireJustification — clears via trash
|
|
24
|
+
* (never hard-deletes); the intent gate is the
|
|
25
|
+
* brake on model-initiated forgetting
|
|
26
|
+
*
|
|
27
|
+
* Events: mutations emit the additive event-log kinds `plan_update`,
|
|
28
|
+
* `goal_update`, and `action_proof {planId, step, toolUseId, verdict}`
|
|
29
|
+
* through an INJECTED `appendEvent` seam — this package never imports
|
|
30
|
+
* runtime wiring, so emitters/memory-service decide where events land.
|
|
31
|
+
* Rejected proof attempts emit `action_proof` too (verdict `missing` /
|
|
32
|
+
* `error_result`): the audit trail records proof pressure, not just wins.
|
|
33
|
+
*
|
|
34
|
+
* Pillar 3: plan/focus/ledger content is user-authored working state
|
|
35
|
+
* (origin "user" semantics, like tool-memory writes) — recalled wiki/fact
|
|
36
|
+
* content is what gets the new `memory` TrustOrigin, not this. All tools are
|
|
37
|
+
* `scope: "internal"` (process-local files only).
|
|
38
|
+
*/
|
|
39
|
+
import { type ContinuityScope, type ContinuityStore } from "@crewhaus/continuity-store";
|
|
40
|
+
import type { ActionProofEventPayload, GoalUpdateEventPayload, PlanUpdateEventPayload } from "@crewhaus/event-log";
|
|
41
|
+
import type { RegisteredTool } from "@crewhaus/tool-catalog";
|
|
42
|
+
/** The three additive event kinds this package emits (event-log union). */
|
|
43
|
+
export type ContinuityEventKind = "plan_update" | "goal_update" | "action_proof";
|
|
44
|
+
export type ContinuityEvent = {
|
|
45
|
+
readonly kind: "plan_update";
|
|
46
|
+
readonly payload: PlanUpdateEventPayload;
|
|
47
|
+
} | {
|
|
48
|
+
readonly kind: "goal_update";
|
|
49
|
+
readonly payload: GoalUpdateEventPayload;
|
|
50
|
+
} | {
|
|
51
|
+
readonly kind: "action_proof";
|
|
52
|
+
readonly payload: ActionProofEventPayload;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* The injected append seam: typically wired to the session event log's
|
|
56
|
+
* `append` (and/or the trace bus) by the emitter or memory-service — kept as
|
|
57
|
+
* a callback so this package stays decoupled from runtime-core. Should not
|
|
58
|
+
* throw; a throwing sink fails the tool call loudly rather than dropping
|
|
59
|
+
* audit events silently.
|
|
60
|
+
*/
|
|
61
|
+
export type AppendContinuityEvent = (event: ContinuityEvent) => void | Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* v0.3.0 §2.5 seam — the per-run state-store key plan-mutating tools flip
|
|
64
|
+
* (through `ToolExecuteContext.bridge.runState`, when the runtime wires one)
|
|
65
|
+
* so runtime-core re-renders the volatile `<current_plan>` tail before the
|
|
66
|
+
* next model call. Deliberately a duplicated literal: this package never
|
|
67
|
+
* imports runtime wiring, and runtime-core exports the same literal as its
|
|
68
|
+
* own `PLAN_DIRTY_STATE_KEY`; the runtime-core continuity suite pins the
|
|
69
|
+
* end-to-end round trip (a real PlanUpdate execute → the loop's dirty-check
|
|
70
|
+
* → tail re-render), so a drift in either literal fails tests.
|
|
71
|
+
*/
|
|
72
|
+
export declare const PLAN_DIRTY_STATE_KEY = "plan.dirty";
|
|
73
|
+
export type CreatePlanToolsOptions = {
|
|
74
|
+
readonly specName: string;
|
|
75
|
+
readonly rootDir?: string;
|
|
76
|
+
readonly scope?: ContinuityScope;
|
|
77
|
+
/** Where session `.jsonl` logs live (proof verification). */
|
|
78
|
+
readonly sessionRootDir?: string;
|
|
79
|
+
/** Fallback session for evidence refs when the runtime context carries no
|
|
80
|
+
* sessionId (refs and `ctx.runContext.sessionId` take precedence). */
|
|
81
|
+
readonly sessionId?: string;
|
|
82
|
+
/** Inject a custom store implementation for tests. */
|
|
83
|
+
readonly store?: ContinuityStore;
|
|
84
|
+
/** Event-log append seam — see `AppendContinuityEvent`. */
|
|
85
|
+
readonly appendEvent?: AppendContinuityEvent;
|
|
86
|
+
readonly now?: () => Date;
|
|
87
|
+
};
|
|
88
|
+
export type PlanToolBundle = {
|
|
89
|
+
readonly focusRead: RegisteredTool;
|
|
90
|
+
readonly focusWrite: RegisteredTool;
|
|
91
|
+
readonly planRead: RegisteredTool;
|
|
92
|
+
readonly planUpdate: RegisteredTool;
|
|
93
|
+
readonly planComplete: RegisteredTool;
|
|
94
|
+
readonly goalWrite: RegisteredTool;
|
|
95
|
+
readonly goalUpdate: RegisteredTool;
|
|
96
|
+
readonly goalList: RegisteredTool;
|
|
97
|
+
readonly memoryClear: RegisteredTool;
|
|
98
|
+
/** Every tool above, registration-ready. */
|
|
99
|
+
readonly all: readonly RegisteredTool[];
|
|
100
|
+
/** Exposed for direct inspection (tests, CLI verbs). */
|
|
101
|
+
readonly store: ContinuityStore;
|
|
102
|
+
};
|
|
103
|
+
/** Thrown when a plan-scoped call has neither a planId nor an active plan. */
|
|
104
|
+
export declare class NoActivePlanError extends Error {
|
|
105
|
+
readonly name = "NoActivePlanError";
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Construct the continuity tool set bound to a spec's continuity store. The
|
|
109
|
+
* caller registers the returned tools into the runtime catalog (e.g.
|
|
110
|
+
* `for (const t of bundle.all) defaultCatalog.register(t)`).
|
|
111
|
+
*/
|
|
112
|
+
export declare function createPlanTools(opts: CreatePlanToolsOptions): PlanToolBundle;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.3.0 Goal 1 — `tool-plan` (design §2.4/§2.6, PR 7). RegisteredTools over
|
|
3
|
+
* `@crewhaus/continuity-store`, packaged like `tool-memory`: one factory
|
|
4
|
+
* bound to a spec name, returning tools the caller registers into the
|
|
5
|
+
* runtime catalog.
|
|
6
|
+
*
|
|
7
|
+
* The tool table (product-DX §2.6 + proof ladder §2.4):
|
|
8
|
+
*
|
|
9
|
+
* FocusRead readOnly — current focus, active plan pointer, REQ ledger
|
|
10
|
+
* FocusWrite destructive — the hot loop: audit-and-allow, deliberately
|
|
11
|
+
* NO requireJustification (a judge call per
|
|
12
|
+
* routine focus update would drown the loop)
|
|
13
|
+
* PlanRead readOnly
|
|
14
|
+
* PlanUpdate destructive — create plans/steps, ladder moves UP TO
|
|
15
|
+
* `claimed`, active-plan pointer; audit-and-allow
|
|
16
|
+
* PlanComplete destructive — the `proven` transition: requires
|
|
17
|
+
* evidence [{toolUseId}], machine-checked
|
|
18
|
+
* against session event logs (child sessions
|
|
19
|
+
* included); rejects teach, never bluff
|
|
20
|
+
* GoalWrite destructive
|
|
21
|
+
* GoalUpdate destructive — `proven` requires evidence, like steps
|
|
22
|
+
* GoalList readOnly
|
|
23
|
+
* MemoryClear destructive + requireJustification — clears via trash
|
|
24
|
+
* (never hard-deletes); the intent gate is the
|
|
25
|
+
* brake on model-initiated forgetting
|
|
26
|
+
*
|
|
27
|
+
* Events: mutations emit the additive event-log kinds `plan_update`,
|
|
28
|
+
* `goal_update`, and `action_proof {planId, step, toolUseId, verdict}`
|
|
29
|
+
* through an INJECTED `appendEvent` seam — this package never imports
|
|
30
|
+
* runtime wiring, so emitters/memory-service decide where events land.
|
|
31
|
+
* Rejected proof attempts emit `action_proof` too (verdict `missing` /
|
|
32
|
+
* `error_result`): the audit trail records proof pressure, not just wins.
|
|
33
|
+
*
|
|
34
|
+
* Pillar 3: plan/focus/ledger content is user-authored working state
|
|
35
|
+
* (origin "user" semantics, like tool-memory writes) — recalled wiki/fact
|
|
36
|
+
* content is what gets the new `memory` TrustOrigin, not this. All tools are
|
|
37
|
+
* `scope: "internal"` (process-local files only).
|
|
38
|
+
*/
|
|
39
|
+
import { EvidenceError, createContinuityStore, renderStatus, } from "@crewhaus/continuity-store";
|
|
40
|
+
import { formatAgentIdentity } from "@crewhaus/run-context";
|
|
41
|
+
import { buildTool } from "@crewhaus/tool-builder";
|
|
42
|
+
import { z } from "zod";
|
|
43
|
+
/**
|
|
44
|
+
* v0.3.0 §2.5 seam — the per-run state-store key plan-mutating tools flip
|
|
45
|
+
* (through `ToolExecuteContext.bridge.runState`, when the runtime wires one)
|
|
46
|
+
* so runtime-core re-renders the volatile `<current_plan>` tail before the
|
|
47
|
+
* next model call. Deliberately a duplicated literal: this package never
|
|
48
|
+
* imports runtime wiring, and runtime-core exports the same literal as its
|
|
49
|
+
* own `PLAN_DIRTY_STATE_KEY`; the runtime-core continuity suite pins the
|
|
50
|
+
* end-to-end round trip (a real PlanUpdate execute → the loop's dirty-check
|
|
51
|
+
* → tail re-render), so a drift in either literal fails tests.
|
|
52
|
+
*/
|
|
53
|
+
export const PLAN_DIRTY_STATE_KEY = "plan.dirty";
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// schemas
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
const planIdSchema = z
|
|
58
|
+
.string()
|
|
59
|
+
.regex(/^plan-\d{4}$/)
|
|
60
|
+
.describe("Plan id, e.g. plan-0001.");
|
|
61
|
+
const optionalPlanIdSchema = planIdSchema
|
|
62
|
+
.optional()
|
|
63
|
+
.describe("Plan id, e.g. plan-0001. Omit to use the active plan.");
|
|
64
|
+
const focusReadSchema = z.object({});
|
|
65
|
+
const focusWriteSchema = z.object({
|
|
66
|
+
focus: z
|
|
67
|
+
.string()
|
|
68
|
+
.min(1)
|
|
69
|
+
.describe("The new focus body: what you are working on right now and the next actions. Short — hard-capped at focusMaxChars (default 4096)."),
|
|
70
|
+
});
|
|
71
|
+
const planReadSchema = z.object({
|
|
72
|
+
planId: optionalPlanIdSchema,
|
|
73
|
+
});
|
|
74
|
+
const claimableStatusSchema = z
|
|
75
|
+
.enum(["open", "in_progress", "claimed"])
|
|
76
|
+
.describe("Ladder status. 'claimed' records unverified progress; 'proven' is NOT settable here — use PlanComplete with evidence.");
|
|
77
|
+
const planUpdateSchema = z.discriminatedUnion("action", [
|
|
78
|
+
z.object({
|
|
79
|
+
action: z.literal("create"),
|
|
80
|
+
title: z.string().min(1).describe("Plan title."),
|
|
81
|
+
steps: z.array(z.string().min(1)).optional().describe("Initial numbered steps, in order."),
|
|
82
|
+
}),
|
|
83
|
+
z.object({
|
|
84
|
+
action: z.literal("add_step"),
|
|
85
|
+
planId: optionalPlanIdSchema,
|
|
86
|
+
text: z.string().min(1).describe("The new step."),
|
|
87
|
+
}),
|
|
88
|
+
z.object({
|
|
89
|
+
action: z.literal("set_step_status"),
|
|
90
|
+
planId: optionalPlanIdSchema,
|
|
91
|
+
step: z.number().int().min(1).describe("1-based step number."),
|
|
92
|
+
status: claimableStatusSchema,
|
|
93
|
+
}),
|
|
94
|
+
z.object({
|
|
95
|
+
action: z.literal("set_active"),
|
|
96
|
+
planId: planIdSchema,
|
|
97
|
+
}),
|
|
98
|
+
]);
|
|
99
|
+
const evidenceRefSchema = z.object({
|
|
100
|
+
toolUseId: z.string().min(1).describe("A toolUseId from a tool call you actually ran."),
|
|
101
|
+
sessionId: z
|
|
102
|
+
.string()
|
|
103
|
+
.regex(/^sess_[0-9a-f]{16}$/)
|
|
104
|
+
.optional()
|
|
105
|
+
.describe("Session the call ran in. Omit for the current session."),
|
|
106
|
+
});
|
|
107
|
+
const planCompleteSchema = z.object({
|
|
108
|
+
planId: optionalPlanIdSchema,
|
|
109
|
+
step: z.number().int().min(1).describe("1-based step number to mark proven."),
|
|
110
|
+
evidence: z
|
|
111
|
+
.array(evidenceRefSchema)
|
|
112
|
+
.min(1)
|
|
113
|
+
.describe("Evidence for the proven transition — cite the tool call(s) that did the work."),
|
|
114
|
+
});
|
|
115
|
+
const goalWriteSchema = z.object({
|
|
116
|
+
title: z.string().min(1).describe("The goal."),
|
|
117
|
+
target: z.number().optional().describe("Optional numeric target."),
|
|
118
|
+
current: z.number().optional().describe("Optional current value."),
|
|
119
|
+
unit: z.string().optional().describe("Optional unit for target/current (e.g. '%')."),
|
|
120
|
+
});
|
|
121
|
+
const goalUpdateSchema = z.object({
|
|
122
|
+
goalId: z
|
|
123
|
+
.string()
|
|
124
|
+
.regex(/^goal-\d{4}$/)
|
|
125
|
+
.describe("Goal id, e.g. goal-0001."),
|
|
126
|
+
title: z.string().min(1).optional(),
|
|
127
|
+
status: z
|
|
128
|
+
.enum(["open", "in_progress", "claimed", "proven"])
|
|
129
|
+
.optional()
|
|
130
|
+
.describe("Ladder status. 'proven' requires evidence."),
|
|
131
|
+
target: z.number().optional(),
|
|
132
|
+
current: z.number().optional(),
|
|
133
|
+
unit: z.string().optional(),
|
|
134
|
+
evidence: z
|
|
135
|
+
.array(evidenceRefSchema)
|
|
136
|
+
.optional()
|
|
137
|
+
.describe("Required when status is 'proven' — the tool call(s) that achieved the goal."),
|
|
138
|
+
});
|
|
139
|
+
const goalListSchema = z.object({});
|
|
140
|
+
const memoryClearSchema = z.object({
|
|
141
|
+
scope: z
|
|
142
|
+
.enum(["focus", "plans", "goals", "all"])
|
|
143
|
+
.describe("What to clear. Files move to .crewhaus/trash/<ts>/ — recoverable, never deleted."),
|
|
144
|
+
});
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// rendering helpers
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
function renderPlan(plan) {
|
|
149
|
+
const lines = [`${plan.id} — ${plan.title}`];
|
|
150
|
+
if (plan.steps.length === 0) {
|
|
151
|
+
lines.push(" (no steps yet — add some with PlanUpdate {action: 'add_step'})");
|
|
152
|
+
}
|
|
153
|
+
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}`);
|
|
158
|
+
}
|
|
159
|
+
return lines.join("\n");
|
|
160
|
+
}
|
|
161
|
+
function renderGoal(goal) {
|
|
162
|
+
const progress = goal.target !== undefined
|
|
163
|
+
? ` (${goal.current ?? 0}/${goal.target}${goal.unit !== undefined ? ` ${goal.unit}` : ""})`
|
|
164
|
+
: "";
|
|
165
|
+
return `- ${goal.id} ${renderStatus(goal.status)} ${goal.title}${progress}`;
|
|
166
|
+
}
|
|
167
|
+
function renderRequirement(req) {
|
|
168
|
+
return `- ${req.id} [${req.status}] ${JSON.stringify(req.text)} (user, ${req.source.sessionId}, turn ${req.source.turn})`;
|
|
169
|
+
}
|
|
170
|
+
/** Thrown when a plan-scoped call has neither a planId nor an active plan. */
|
|
171
|
+
export class NoActivePlanError extends Error {
|
|
172
|
+
name = "NoActivePlanError";
|
|
173
|
+
}
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// the factory
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
/**
|
|
178
|
+
* Construct the continuity tool set bound to a spec's continuity store. The
|
|
179
|
+
* caller registers the returned tools into the runtime catalog (e.g.
|
|
180
|
+
* `for (const t of bundle.all) defaultCatalog.register(t)`).
|
|
181
|
+
*/
|
|
182
|
+
export function createPlanTools(opts) {
|
|
183
|
+
const store = opts.store ??
|
|
184
|
+
createContinuityStore({
|
|
185
|
+
specName: opts.specName,
|
|
186
|
+
...(opts.rootDir !== undefined ? { rootDir: opts.rootDir } : {}),
|
|
187
|
+
...(opts.scope !== undefined ? { scope: opts.scope } : {}),
|
|
188
|
+
...(opts.sessionRootDir !== undefined ? { sessionRootDir: opts.sessionRootDir } : {}),
|
|
189
|
+
...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
|
|
190
|
+
...(opts.now !== undefined ? { now: opts.now } : {}),
|
|
191
|
+
});
|
|
192
|
+
async function emit(event) {
|
|
193
|
+
if (opts.appendEvent !== undefined)
|
|
194
|
+
await opts.appendEvent(event);
|
|
195
|
+
}
|
|
196
|
+
function currentSessionId(ctx) {
|
|
197
|
+
return ctx?.runContext?.sessionId ?? opts.sessionId;
|
|
198
|
+
}
|
|
199
|
+
/** v0.3.0 §7.1 — attribution: stamp the acting run context's identity
|
|
200
|
+
* (e.g. `subagent=researcher`) into emitted event payloads so a plan
|
|
201
|
+
* mutation made from inside a sub-agent records WHO acted. Mirrors
|
|
202
|
+
* tool-wiki's `createdBy` stamping: prefers `ctx.runContext`, falls back
|
|
203
|
+
* to the opaque bridge's runContext (the field the runtime threads on
|
|
204
|
+
* every tool execute). Empty for top-level runs. */
|
|
205
|
+
function identityOf(ctx) {
|
|
206
|
+
const rc = ctx?.runContext ?? ctx?.bridge?.runContext;
|
|
207
|
+
return rc?.agentIdentity !== undefined
|
|
208
|
+
? { agentIdentity: formatAgentIdentity(rc.agentIdentity) }
|
|
209
|
+
: {};
|
|
210
|
+
}
|
|
211
|
+
/** §2.5 — flag the runtime's per-run state-store after a successful
|
|
212
|
+
* mutation so the loop re-renders the plan tail before the next model
|
|
213
|
+
* call. No-op when the runtime didn't wire `bridge.runState` (pre-0.3.0
|
|
214
|
+
* bridge builders, direct tool execution in tests/CLI verbs). */
|
|
215
|
+
function markPlanDirty(ctx) {
|
|
216
|
+
const bridge = ctx?.bridge;
|
|
217
|
+
bridge?.runState?.set({ [PLAN_DIRTY_STATE_KEY]: true });
|
|
218
|
+
}
|
|
219
|
+
async function resolvePlanId(planId) {
|
|
220
|
+
if (planId !== undefined)
|
|
221
|
+
return planId;
|
|
222
|
+
const active = await store.getActivePlan();
|
|
223
|
+
if (active === null) {
|
|
224
|
+
throw new NoActivePlanError("no active plan — pass planId, or create a plan first with PlanUpdate {action: 'create'}.");
|
|
225
|
+
}
|
|
226
|
+
return active.id;
|
|
227
|
+
}
|
|
228
|
+
const focusRead = buildTool({
|
|
229
|
+
name: "FocusRead",
|
|
230
|
+
description: "Read the persisted focus: what this harness is working on, the active plan pointer, and the REQ-nnn requirements ledger. Check this FIRST at session start and before asking the user anything they may already have answered.",
|
|
231
|
+
inputSchema: focusReadSchema,
|
|
232
|
+
readOnly: true,
|
|
233
|
+
execute: async () => {
|
|
234
|
+
const focus = await store.readFocus();
|
|
235
|
+
if (focus === null) {
|
|
236
|
+
return "no focus set — write one with FocusWrite.";
|
|
237
|
+
}
|
|
238
|
+
const lines = ["focus:"];
|
|
239
|
+
lines.push(focus.body !== "" ? focus.body : " (empty)");
|
|
240
|
+
lines.push(`active plan: ${focus.activePlanId ?? "none"}`);
|
|
241
|
+
lines.push("requirements:");
|
|
242
|
+
if (focus.requirements.length > 0) {
|
|
243
|
+
if (focus.ledgerTruncated)
|
|
244
|
+
lines.push("[ledger truncated]");
|
|
245
|
+
for (const req of focus.requirements)
|
|
246
|
+
lines.push(renderRequirement(req));
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
lines.push(" (none)");
|
|
250
|
+
}
|
|
251
|
+
return lines.join("\n");
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
const focusWrite = buildTool({
|
|
255
|
+
name: "FocusWrite",
|
|
256
|
+
description: "Replace the persisted focus body (current objective + next actions). This is the hot loop — keep it current and short (capped at focusMaxChars). Requirements are appended via the ledger, not here.",
|
|
257
|
+
inputSchema: focusWriteSchema,
|
|
258
|
+
destructive: true, // audit-and-allow; deliberately NO requireJustification (§7.4)
|
|
259
|
+
execute: async (input, ctx) => {
|
|
260
|
+
await store.writeFocus(input.focus);
|
|
261
|
+
markPlanDirty(ctx);
|
|
262
|
+
return `focus updated (${input.focus.length} chars)`;
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
const planRead = buildTool({
|
|
266
|
+
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.",
|
|
268
|
+
inputSchema: planReadSchema,
|
|
269
|
+
readOnly: true,
|
|
270
|
+
execute: async (input) => {
|
|
271
|
+
if (input.planId !== undefined) {
|
|
272
|
+
const plan = await store.getPlan(input.planId);
|
|
273
|
+
if (plan === null)
|
|
274
|
+
return `no plan "${input.planId}"`;
|
|
275
|
+
return renderPlan(plan);
|
|
276
|
+
}
|
|
277
|
+
const active = await store.getActivePlan();
|
|
278
|
+
if (active !== null)
|
|
279
|
+
return renderPlan(active);
|
|
280
|
+
const plans = await store.listPlans();
|
|
281
|
+
if (plans.length === 0) {
|
|
282
|
+
return "no plans yet — create one with PlanUpdate {action: 'create'}.";
|
|
283
|
+
}
|
|
284
|
+
return [
|
|
285
|
+
"no active plan. available plans:",
|
|
286
|
+
...plans.map((p) => `- ${p.id} — ${p.title}`),
|
|
287
|
+
].join("\n");
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
const planUpdate = buildTool({
|
|
291
|
+
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.",
|
|
293
|
+
inputSchema: planUpdateSchema,
|
|
294
|
+
destructive: true, // audit-and-allow (§7.4): a judge call per routine plan update would drown the loop
|
|
295
|
+
execute: async (input, ctx) => {
|
|
296
|
+
switch (input.action) {
|
|
297
|
+
case "create": {
|
|
298
|
+
const plan = await store.createPlan({
|
|
299
|
+
title: input.title,
|
|
300
|
+
...(input.steps !== undefined ? { steps: input.steps } : {}),
|
|
301
|
+
});
|
|
302
|
+
await emit({
|
|
303
|
+
kind: "plan_update",
|
|
304
|
+
payload: { planId: plan.id, action: "create", title: plan.title, ...identityOf(ctx) },
|
|
305
|
+
});
|
|
306
|
+
markPlanDirty(ctx);
|
|
307
|
+
return `created ${plan.id} — ${plan.title} (${plan.steps.length} step(s))\n${renderPlan(plan)}`;
|
|
308
|
+
}
|
|
309
|
+
case "add_step": {
|
|
310
|
+
const planId = await resolvePlanId(input.planId);
|
|
311
|
+
const plan = await store.addStep(planId, input.text);
|
|
312
|
+
await emit({
|
|
313
|
+
kind: "plan_update",
|
|
314
|
+
payload: { planId, action: "add_step", step: plan.steps.length, ...identityOf(ctx) },
|
|
315
|
+
});
|
|
316
|
+
markPlanDirty(ctx);
|
|
317
|
+
return `added step ${plan.steps.length} to ${planId}`;
|
|
318
|
+
}
|
|
319
|
+
case "set_step_status": {
|
|
320
|
+
const planId = await resolvePlanId(input.planId);
|
|
321
|
+
await store.setStepStatus(planId, input.step, input.status);
|
|
322
|
+
await emit({
|
|
323
|
+
kind: "plan_update",
|
|
324
|
+
payload: {
|
|
325
|
+
planId,
|
|
326
|
+
action: "set_step_status",
|
|
327
|
+
step: input.step,
|
|
328
|
+
status: input.status,
|
|
329
|
+
...identityOf(ctx),
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
markPlanDirty(ctx);
|
|
333
|
+
return `${planId} step ${input.step} → ${input.status}${input.status === "claimed" ? " (unverified — prove it with PlanComplete)" : ""}`;
|
|
334
|
+
}
|
|
335
|
+
case "set_active": {
|
|
336
|
+
await store.setActivePlan(input.planId);
|
|
337
|
+
await emit({
|
|
338
|
+
kind: "plan_update",
|
|
339
|
+
payload: { planId: input.planId, action: "set_active", ...identityOf(ctx) },
|
|
340
|
+
});
|
|
341
|
+
markPlanDirty(ctx);
|
|
342
|
+
return `active plan → ${input.planId}`;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
const planComplete = buildTool({
|
|
348
|
+
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.",
|
|
350
|
+
inputSchema: planCompleteSchema,
|
|
351
|
+
destructive: true,
|
|
352
|
+
execute: async (input, ctx) => {
|
|
353
|
+
const planId = await resolvePlanId(input.planId);
|
|
354
|
+
const sessionId = currentSessionId(ctx);
|
|
355
|
+
const refs = input.evidence.map((ref) => ({
|
|
356
|
+
toolUseId: ref.toolUseId,
|
|
357
|
+
...(ref.sessionId !== undefined
|
|
358
|
+
? { sessionId: ref.sessionId }
|
|
359
|
+
: sessionId !== undefined
|
|
360
|
+
? { sessionId }
|
|
361
|
+
: {}),
|
|
362
|
+
}));
|
|
363
|
+
try {
|
|
364
|
+
const plan = await store.proveStep(planId, input.step, refs);
|
|
365
|
+
const step = plan.steps.find((s) => s.index === input.step);
|
|
366
|
+
for (const proof of step?.proofs ?? []) {
|
|
367
|
+
if (refs.some((r) => r.toolUseId === proof.toolUseId)) {
|
|
368
|
+
await emit({
|
|
369
|
+
kind: "action_proof",
|
|
370
|
+
payload: {
|
|
371
|
+
planId,
|
|
372
|
+
step: input.step,
|
|
373
|
+
toolUseId: proof.toolUseId,
|
|
374
|
+
verdict: "verified",
|
|
375
|
+
...identityOf(ctx),
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
await emit({
|
|
381
|
+
kind: "plan_update",
|
|
382
|
+
payload: {
|
|
383
|
+
planId,
|
|
384
|
+
action: "prove_step",
|
|
385
|
+
step: input.step,
|
|
386
|
+
status: "proven",
|
|
387
|
+
...identityOf(ctx),
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
markPlanDirty(ctx);
|
|
391
|
+
return `${planId} step ${input.step} proven — evidence: ${refs
|
|
392
|
+
.map((r) => r.toolUseId)
|
|
393
|
+
.join(", ")}`;
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
if (err instanceof EvidenceError) {
|
|
397
|
+
// Audit the rejected attempt too — proof pressure is signal.
|
|
398
|
+
await emit({
|
|
399
|
+
kind: "action_proof",
|
|
400
|
+
payload: {
|
|
401
|
+
planId,
|
|
402
|
+
step: input.step,
|
|
403
|
+
toolUseId: err.toolUseId,
|
|
404
|
+
verdict: err.verdict,
|
|
405
|
+
...identityOf(ctx),
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
throw err;
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
const goalWrite = buildTool({
|
|
414
|
+
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.",
|
|
416
|
+
inputSchema: goalWriteSchema,
|
|
417
|
+
destructive: true,
|
|
418
|
+
execute: async (input, ctx) => {
|
|
419
|
+
const goal = await store.writeGoal({
|
|
420
|
+
title: input.title,
|
|
421
|
+
...(input.target !== undefined ? { target: input.target } : {}),
|
|
422
|
+
...(input.current !== undefined ? { current: input.current } : {}),
|
|
423
|
+
...(input.unit !== undefined ? { unit: input.unit } : {}),
|
|
424
|
+
});
|
|
425
|
+
await emit({
|
|
426
|
+
kind: "goal_update",
|
|
427
|
+
payload: { goalId: goal.id, action: "create", title: goal.title, ...identityOf(ctx) },
|
|
428
|
+
});
|
|
429
|
+
markPlanDirty(ctx);
|
|
430
|
+
return `created ${goal.id} — ${goal.title}`;
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
const goalUpdate = buildTool({
|
|
434
|
+
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.",
|
|
436
|
+
inputSchema: goalUpdateSchema,
|
|
437
|
+
destructive: true,
|
|
438
|
+
execute: async (input, ctx) => {
|
|
439
|
+
const sessionId = currentSessionId(ctx);
|
|
440
|
+
const evidence = input.evidence?.map((ref) => ({
|
|
441
|
+
toolUseId: ref.toolUseId,
|
|
442
|
+
...(ref.sessionId !== undefined
|
|
443
|
+
? { sessionId: ref.sessionId }
|
|
444
|
+
: sessionId !== undefined
|
|
445
|
+
? { sessionId }
|
|
446
|
+
: {}),
|
|
447
|
+
}));
|
|
448
|
+
const goal = await store.updateGoal(input.goalId, {
|
|
449
|
+
...(input.title !== undefined ? { title: input.title } : {}),
|
|
450
|
+
...(input.status !== undefined ? { status: input.status } : {}),
|
|
451
|
+
...(input.target !== undefined ? { target: input.target } : {}),
|
|
452
|
+
...(input.current !== undefined ? { current: input.current } : {}),
|
|
453
|
+
...(input.unit !== undefined ? { unit: input.unit } : {}),
|
|
454
|
+
...(evidence !== undefined ? { evidence } : {}),
|
|
455
|
+
});
|
|
456
|
+
await emit({
|
|
457
|
+
kind: "goal_update",
|
|
458
|
+
payload: {
|
|
459
|
+
goalId: goal.id,
|
|
460
|
+
action: "update",
|
|
461
|
+
...(input.status !== undefined ? { status: input.status } : {}),
|
|
462
|
+
...identityOf(ctx),
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
markPlanDirty(ctx);
|
|
466
|
+
return `${goal.id} updated — ${renderGoal(goal).slice(2)}`;
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
const goalList = buildTool({
|
|
470
|
+
name: "GoalList",
|
|
471
|
+
description: "List persisted goals with ladder statuses and progress. 'claimed' goals are rendered as unverified.",
|
|
472
|
+
inputSchema: goalListSchema,
|
|
473
|
+
readOnly: true,
|
|
474
|
+
execute: async () => {
|
|
475
|
+
const goals = await store.listGoals();
|
|
476
|
+
if (goals.length === 0)
|
|
477
|
+
return "no goals yet — create one with GoalWrite.";
|
|
478
|
+
return goals.map(renderGoal).join("\n");
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
const memoryClear = buildTool({
|
|
482
|
+
name: "MemoryClear",
|
|
483
|
+
description: "Clear continuity state (focus | plans | goals | all). Files move to .crewhaus/trash/<ts>/ and can be restored — nothing is hard-deleted. Destructive: requires a justification.",
|
|
484
|
+
inputSchema: memoryClearSchema,
|
|
485
|
+
destructive: true,
|
|
486
|
+
// Pillar 3 intent gate (§7.4): model-initiated forgetting is exactly
|
|
487
|
+
// where the justification judge belongs — unlike the hot-loop
|
|
488
|
+
// Focus/Plan updates, clears are rare and destructive-by-intent.
|
|
489
|
+
requireJustification: true,
|
|
490
|
+
execute: async (input, ctx) => {
|
|
491
|
+
const result = await store.clear(input.scope);
|
|
492
|
+
if (result.moved.length === 0) {
|
|
493
|
+
return `nothing to clear for scope "${input.scope}" (store is empty)`;
|
|
494
|
+
}
|
|
495
|
+
markPlanDirty(ctx);
|
|
496
|
+
return (`cleared: ${input.scope} (${result.moved.length} file(s)/dir(s)) → ${result.trashDir}\n` +
|
|
497
|
+
`undo: crewhaus memory restore ${result.ts}`);
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
const all = [
|
|
501
|
+
focusRead,
|
|
502
|
+
focusWrite,
|
|
503
|
+
planRead,
|
|
504
|
+
planUpdate,
|
|
505
|
+
planComplete,
|
|
506
|
+
goalWrite,
|
|
507
|
+
goalUpdate,
|
|
508
|
+
goalList,
|
|
509
|
+
memoryClear,
|
|
510
|
+
];
|
|
511
|
+
return {
|
|
512
|
+
focusRead,
|
|
513
|
+
focusWrite,
|
|
514
|
+
planRead,
|
|
515
|
+
planUpdate,
|
|
516
|
+
planComplete,
|
|
517
|
+
goalWrite,
|
|
518
|
+
goalUpdate,
|
|
519
|
+
goalList,
|
|
520
|
+
memoryClear,
|
|
521
|
+
all,
|
|
522
|
+
store,
|
|
523
|
+
};
|
|
524
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crewhaus/tool-plan",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "module",
|
|
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
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "bun test src"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@crewhaus/continuity-store": "0.3.0",
|
|
19
|
+
"@crewhaus/event-log": "0.3.0",
|
|
20
|
+
"@crewhaus/run-context": "0.3.0",
|
|
21
|
+
"@crewhaus/tool-builder": "0.3.0",
|
|
22
|
+
"@crewhaus/tool-catalog": "0.3.0",
|
|
23
|
+
"zod": "^3.23.8"
|
|
24
|
+
},
|
|
25
|
+
"license": "Apache-2.0",
|
|
26
|
+
"author": {
|
|
27
|
+
"name": "Max Meier",
|
|
28
|
+
"email": "max@crewhaus.ai",
|
|
29
|
+
"url": "https://crewhaus.ai"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/crewhaus/factory.git",
|
|
34
|
+
"directory": "packages/tool-plan"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/crewhaus/factory/tree/main/packages/tool-plan#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/crewhaus/factory/issues"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
44
|
+
}
|