@hypercarrier/pi-team-durable 0.0.0-stage → 0.1.0-rc.2

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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +209 -2
  3. package/dist/authority/anchor.d.ts +10 -0
  4. package/dist/authority/anchor.js +19 -0
  5. package/dist/authority/card-types.d.ts +152 -0
  6. package/dist/authority/card-types.js +123 -0
  7. package/dist/authority/contracts.d.ts +19 -0
  8. package/dist/authority/contracts.js +45 -0
  9. package/dist/authority/delivery.d.ts +60 -0
  10. package/dist/authority/delivery.js +158 -0
  11. package/dist/authority/graph-card.d.ts +4 -0
  12. package/dist/authority/graph-card.js +39 -0
  13. package/dist/authority/graph-control.d.ts +299 -0
  14. package/dist/authority/graph-control.js +1009 -0
  15. package/dist/authority/incremental-graph-control.d.ts +156 -0
  16. package/dist/authority/incremental-graph-control.js +1051 -0
  17. package/dist/authority/index.d.ts +15 -0
  18. package/dist/authority/index.js +15 -0
  19. package/dist/authority/interrupt.d.ts +11 -0
  20. package/dist/authority/interrupt.js +26 -0
  21. package/dist/authority/limits.d.ts +7 -0
  22. package/dist/authority/limits.js +7 -0
  23. package/dist/authority/notifier.d.ts +32 -0
  24. package/dist/authority/notifier.js +89 -0
  25. package/dist/authority/observation-store.d.ts +104 -0
  26. package/dist/authority/observation-store.js +226 -0
  27. package/dist/authority/observation.d.ts +13 -0
  28. package/dist/authority/observation.js +143 -0
  29. package/dist/authority/panel.d.ts +121 -0
  30. package/dist/authority/panel.js +44 -0
  31. package/dist/authority/projection.d.ts +5 -0
  32. package/dist/authority/projection.js +15 -0
  33. package/dist/authority/rc7-contract.json +9390 -0
  34. package/dist/authority/runtime.d.ts +106 -0
  35. package/dist/authority/runtime.js +115 -0
  36. package/dist/authority/state.d.ts +165 -0
  37. package/dist/authority/state.js +28 -0
  38. package/dist/authority/store.d.ts +144 -0
  39. package/dist/authority/store.js +675 -0
  40. package/dist/authority/sync.d.ts +13 -0
  41. package/dist/authority/sync.js +78 -0
  42. package/dist/authority/task-version-ref.d.ts +3 -0
  43. package/dist/authority/task-version-ref.js +5 -0
  44. package/dist/authority/tools.d.ts +105 -0
  45. package/dist/authority/tools.js +302 -0
  46. package/dist/testing.d.ts +11 -0
  47. package/dist/testing.js +10 -0
  48. package/package.json +63 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 deephbz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,210 @@
1
- # Temporary Holding Version
1
+ # @hypercarrier/pi-team-durable
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Pi Team Bright semantics on [pi-durable](https://www.npmjs.com/package/@earendil-works/pi-durable):
4
+ one Task authority per Team, Workers as durable subthreads, accepted-then-delivered
5
+ notices.
6
+
7
+ ## Status
8
+
9
+ - **Prerelease.** The npm `next` tag carries it. This package has its own version
10
+ line, independent of Pi Team Bright. Pi Team Bright 0.25.1 is its contract pin.
11
+ - **No standalone runtime.** The package needs a host that supplies the Team ports:
12
+ model roles or a `catalog`, `observationPort`, `abortRun` and `stop`. The runtime
13
+ builds Worker creation and stop settlement from them. Today the host is the AmPi
14
+ node.
15
+ - **Exact peers.** The peer dependencies pin `@earendil-works/pi-durable`,
16
+ `@earendil-works/pi-ai` and `@earendil-works/chord` to 1.4.0. Other versions give
17
+ a peer conflict.
18
+ - **First version without provenance.** The first version was published by hand.
19
+ Later versions publish from CI with npm provenance.
20
+ - **Protocol value.** The persisted notice value `sender: 'durable-team'` is a
21
+ protocol value from the package's former name. It stays unchanged.
22
+ - **Tests here.** CI runs the unit suites and a 100-seed differential oracle run.
23
+ Run 500 or more seeds by hand before a release.
24
+ The SIGKILL crash suite and `bench/e2e.ts` stay in HyperCarrier (private).
25
+ - **Before deploy.** The first open migrates the store to the indexed format. The
26
+ migration is one-way: rollback means restoring a backup. Back up the store, and
27
+ stop every older node process first. 0.1.0-rc.1 stores version-1 documents.
28
+ This version migrates them to version 2, and a downgrade cannot read a migrated
29
+ store.
30
+
31
+ Known issues:
32
+
33
+ 1. An Alert is lost when the recipient's model errors. It shows as unanswered. Pi
34
+ Team Bright retries it; this package does not.
35
+ 2. `team_sync` can replay pages that an earlier call released. Act only on
36
+ `task_read`, and never call `team_sync` from a nested tool call.
37
+ 3. Interrupt: a Worker can interrupt the lead, the run to stop is chosen when the
38
+ Alert is accepted, and the abort has no settle bound.
39
+ 4. An Alert can be admitted after its recipient is closed or fenced. After an owner
40
+ Close, a busy Worker can still run one tool call.
41
+
42
+ ## Install
43
+
44
+ ```sh
45
+ npm install @hypercarrier/pi-team-durable@next
46
+ ```
47
+
48
+ The package ships compiled JavaScript and type declarations under `dist`.
49
+ Node 22.19 or newer. Install the three pinned peers beside it.
50
+
51
+ `./testing` exports named internals for a host's own tests. It is unstable test support:
52
+ its names can change in any release. Production code imports `./authority` only.
53
+
54
+ ## API
55
+
56
+ `./authority` exports the runtime factory, authority documents, observation
57
+ observer, and structural stop ports. A host adapter registers this runtime.
58
+
59
+ - `ptb.team` owns membership and lifecycle. `ptb.head` holds current graph
60
+ coordinates, readiness, occupancy, attention, and bounded recent references.
61
+ Task family members hold current facts, retained Attempts, cards, and operator
62
+ projections. Immutable graph indexes hold topology. Worker indexes hold
63
+ nonterminal Task IDs. Each changed document is stored as a base.
64
+ - A controller-only conversation holds graph revision diffs and controller
65
+ events. Commands restore the incremental engine from current Task records.
66
+ They commit changed facts, indexes, receipts, obligations, and history together.
67
+ - Operation receipts retain exact result strings per epoch. Native tool-call
68
+ receipts use task scope and retire with the tool. Direct callers use permanent
69
+ family members. Obligations retain their identity after settlement; bounded
70
+ hash partitions index pending and unscheduled keys.
71
+ - Nine lead tools and three Worker tools use the rc.7 contract. Exact conversation
72
+ and generation bindings guard authority. A fork does not acquire an existing
73
+ Team's lead authority.
74
+ - `ptb.worker-anchor` owns a Worker conversation and renews a durable sleep.
75
+ Lead Esc does not end it. Stops use stored target IDs through the node's single
76
+ mark-before-resume fence. Failed confirmation retains the intent.
77
+ - `ptb.deliver` rechecks an obligation, submits with a stable request ID, records
78
+ admission, and waits for settlement. Notices use `team.notice` entries whose
79
+ data (`TeamNoticeData`) names the delivery kind and its sender: an Alert's
80
+ author, or `team-lead` for Tasks and stops. An interrupt Alert's data also
81
+ has `interrupt: true`. A reader names a notice from this data, never from its
82
+ text.
83
+ Interrupt Alerts persist the recipient run ID at acceptance. A separate phase
84
+ asks the host `abortRun` port to stop that exact run, then checkpoints before
85
+ normal admission. Queue identities stay native; no input is re-queued.
86
+ A failed interruption appears as unproved in the notice. Retry never selects
87
+ a replacement run. The node supplies a cancellable wait outside its admission lock.
88
+ - `ptb.notify` reserves an automatic page before admission. A failed lead input
89
+ pauses notification. Explicit sync or a new owner input resumes it.
90
+ - `team_sync` reserves successive bounded pages. It never waits for consumption.
91
+ The final request hook records exact page visibility. A later observer requires
92
+ a matching persisted native assistant entry and successful input settlement
93
+ before it advances the cursor. Compaction and failed runs release unseen pages.
94
+ - The head keeps references to 100 recent journal events. Their immutable
95
+ `ptb.event` entries stay in the lead conversation, so sync page boundaries stay
96
+ unchanged.
97
+ - Observation indexes, page metadata, immutable page bodies, and open proofs
98
+ use separate base-checkpointed documents. Requests check active pages from
99
+ the newest message backward. They do not hash transcript history.
100
+ - The observer retires a proof after its run settles and it processes the
101
+ evidence. It retires a page body only when the page is acknowledged or
102
+ replaced and the contiguous cursor covers it. Unsettled proofs and notifier
103
+ submit/settle checkpoints keep referenced bodies. Unknown notifier ownership
104
+ keeps the body. Page metadata and the exact replay receipts remain.
105
+ - `observations(tx)` and `observationStats(tx)` are diagnostic readers. The
106
+ latter reports current-record counts and UTF-8 JSON bytes. Production hooks
107
+ and the observer read current indexes.
108
+ - `/ptb` reads one acknowledged scan anchor for the current epoch. This anchor
109
+ differs from the runtime reservation anchor in `scanByEpoch`. Cursor advance
110
+ folds eligible waiting-page anchors before removing their waiting entries.
111
+ Body retirement retains the anchor; epoch change removes it.
112
+ - `readTeamPanel` reads current Task projections and recent records.
113
+ `readTeamSummary` reads head indexes. `state.open()` is a full-history diagnostic
114
+ adapter; runtime paths do not use it.
115
+
116
+ The first authority open migrates rc.7 documents in one transaction. It checks
117
+ current facts against the reference controller and writes the new records and
118
+ log. The same transaction raises all six shared PTB document kinds to version 2:
119
+ `ptb.team`, `ptb.graph`, `ptb.journal`, `ptb.outbox`, `ptb.receipts`, and
120
+ `ptb.observation`. `ptb.team` keeps its data. The other five retain only
121
+ `{fence:"ptb-authority-v2"}`. Current commands use the new authority records.
122
+ These occupied addresses prevent old code from creating fresh version-1
123
+ documents. pi-durable rejects old typed reads before mutation. Fresh stores
124
+ receive the same fence. Native pi-durable documents keep their own versions.
125
+
126
+ A migrated operation receipt receives its original projection check on its
127
+ first replay. Downgrade is not supported.
128
+
129
+ The fence covers stopped and reopened stores, not an old process that is still
130
+ running. Before the first open with this build, stop every node process that
131
+ runs older code, and back up the whole store directory. To roll back, restore
132
+ that backup and run the older build on it. A rollback discards all Team work
133
+ accepted after the migration.
134
+
135
+ Known hazard until the next contract change: `team_sync` can replay a stale
136
+ page. This occurred with a direct snapshot call as well as nested calls. Leads
137
+ use `task_read` for any Task state they act on. A direct `team_sync` call does
138
+ not establish current Task state.
139
+
140
+ The host injects model roles: a static `modelRoles` list, or a `catalog(tx,
141
+ lead)` resolver that gives each lead its own roles and default. The AmPi
142
+ node's `default` role captures the authoritative lead model at each Worker
143
+ generation. Unknown roles return a typed refusal.
144
+ The package never reads Pi configuration. The observation commit port resolves
145
+ its store for every hook; a registry can serve multiple stores.
146
+
147
+ The executable contract snapshot comes from Pi Team Bright 0.24.0-rc.7 at
148
+ `382950ce62bd38b6cd9f7fb80b91ef5b9140e15a` (unreleased Alert interrupt support).
149
+ `scripts/rc7-contract.mjs` records source hashes. Pi Team Bright 0.25.1 is the
150
+ contract pin: it ships the same source files, and a test compares their SHA-256
151
+ values with the recorded ones. The tests regenerate the contract from the
152
+ installed devDependency `@hypercarrier/pi-team-bright@0.25.1`.
153
+
154
+ Each role's tool labels and descriptions come from Pi Team Bright's own
155
+ registration. `scripts/pi-registration.mjs` captures it per role through an
156
+ inert host. The parity test compares every durable tool with the installed
157
+ `@hypercarrier/pi-team-bright`. Known gap: Pi's Worker `alert_send` requires
158
+ non-empty `text` and `task_id`; the durable schema does not yet.
159
+
160
+ ```sh
161
+ npm ci
162
+ npm test
163
+ ```
164
+
165
+ The hardening oracle freezes `src/authority` at
166
+ certified P, the 0.1.0-rc.1 port, in `test/oracle/authority`.
167
+ Do not update that copy when optimizing the runtime. The seeded differential
168
+ harness compares exact tool results or error messages, all public Task cards,
169
+ and created `ptb.deliver` inputs after every command. Each runtime has its own
170
+ MemoryStorage Harness. Both schedulers stay paused. The stop port returns fake
171
+ settlement. A synthetic persisted lead run permits snapshot and update reads.
172
+ Delivery execution and observation acknowledgement have separate tests.
173
+
174
+ ```sh
175
+ # From the repository root: N seeds over K processes (defaults: 500 and 4).
176
+ npm run test:oracle -- --seeds 500 --shards 4
177
+ # Replay one failing seed. Failures print its full command trace.
178
+ npm run test:oracle -- --seed 42 --seeds 1 --shards 1
179
+ # Run only the deterministic boundary cases.
180
+ npm run test:oracle -- --targeted-only
181
+ ```
182
+
183
+ Each seed runs about 70 commands over at most seven Tasks. It combines a fixed
184
+ lifecycle and transition prefix with 30 random commands, then starts a new Team
185
+ epoch. Use `--steps` to change the random command count. A separate deterministic
186
+ sequence covers prerequisite repair, superseded dependents, removed and
187
+ reintroduced keys, stale lineage, blocked Worker capacity, reorder-only
188
+ restatement, and stop notices after Worker reconnection. The normal Vitest suite
189
+ runs these cases and three smoke seeds. This oracle detects behavior drift from the frozen
190
+ runtime; it does not prove that the frozen behavior is correct.
191
+
192
+ Fast tests use MemoryStorage, a fixed clock, inert timers, and faux responses.
193
+ The SIGKILL crash-recovery suite and `bench/e2e.ts` stay in HyperCarrier (private).
194
+ They use the AmPi node's subprocess carrier. This repository's tests do not
195
+ cover crash recovery.
196
+
197
+ The C1 delivery hold remains unported. pi-durable places steers and follow-ups at
198
+ turn boundaries. A stale admitted notice cannot grant authority: Task version and
199
+ Worker generation checks still apply.
200
+
201
+ ## Development
202
+
203
+ `npm run build` emits `dist`; `npm install` runs it through `prepare`.
204
+ `npm run typecheck`, `npm test` and `npm run verify:package` are the CI checks.
205
+ See [releasing](docs/maintainers/releasing.md) for the release procedure.
206
+
207
+ ## Provenance
208
+
209
+ Extracted from HyperCarrier (private) at the certified port, renamed from
210
+ `durable-team`, without its history.
@@ -0,0 +1,10 @@
1
+ import { type ConversationId, type TaskId, type Tx } from '@earendil-works/pi-durable';
2
+ export declare const ANCHOR_SLEEP_MS: number;
3
+ export declare const WorkerAnchor: import("@earendil-works/pi-durable").Task<null, {
4
+ phase: "hold";
5
+ }, null, object>;
6
+ /** The caller commits this binding with Membership in the same transaction. */
7
+ export declare function createAnchoredConversation(tx: Tx, lead: ConversationId): Promise<{
8
+ conversationId: ConversationId;
9
+ anchorId: TaskId;
10
+ }>;
@@ -0,0 +1,19 @@
1
+ import { defineTask } from '@earendil-works/pi-durable';
2
+ // A live background root owns the Worker conversation. A terminal placeholder
3
+ // cannot be an abort boundary because abortTask returns early for terminal tasks.
4
+ export const ANCHOR_SLEEP_MS = 24 * 60 * 60 * 1000;
5
+ export const WorkerAnchor = defineTask({
6
+ name: 'ptb.worker-anchor', version: 1, initial: () => ({ phase: 'hold' }),
7
+ phases: { hold: async (_task, runtime, context) => {
8
+ await runtime.sleepUntil(runtime.now() + ANCHOR_SLEEP_MS, { phase: 'hold' }, context);
9
+ } },
10
+ abort: async (_task, runtime, context) => {
11
+ await runtime.commit(() => ({ status: 'terminal', outcome: { status: 'aborted' } }), context);
12
+ },
13
+ });
14
+ /** The caller commits this binding with Membership in the same transaction. */
15
+ export async function createAnchoredConversation(tx, lead) {
16
+ const anchorId = await tx.createTask(WorkerAnchor, null, { conversationId: lead, ownership: { kind: 'conversation' }, background: true, abandonOnRestart: false });
17
+ const conversation = await tx.createConversation({ ownership: { kind: 'task', taskId: anchorId } });
18
+ return { conversationId: conversation.id, anchorId };
19
+ }
@@ -0,0 +1,152 @@
1
+ import { Type, type Static } from "typebox";
2
+ export declare const GraphVersionRefSchema: Type.TString;
3
+ export declare const GraphTaskTransitionSchema: Type.TEnum<["claim", "block", "resume", "goal_achieved", "goal_failed", "cancel"]>;
4
+ export declare const GraphFailureEdgeSchema: Type.TObject<{
5
+ target: Type.TString;
6
+ max_traversals: Type.TInteger;
7
+ }>;
8
+ export declare const GraphTaskStateSchema: Type.TUnion<[Type.TObject<{
9
+ kind: Type.TLiteral<"dependency_waiting">;
10
+ prerequisite_task_ids: Type.TArray<Type.TString>;
11
+ }>, Type.TObject<{
12
+ kind: Type.TLiteral<"ready">;
13
+ }>, Type.TObject<{
14
+ kind: Type.TLiteral<"in_progress">;
15
+ attempt_id: Type.TString;
16
+ }>, Type.TObject<{
17
+ kind: Type.TLiteral<"blocked">;
18
+ attempt_id: Type.TString;
19
+ evidence: Type.TString;
20
+ }>, Type.TObject<{
21
+ kind: Type.TLiteral<"goal_achieved">;
22
+ attempt_id: Type.TString;
23
+ }>, Type.TObject<{
24
+ kind: Type.TLiteral<"goal_failed">;
25
+ reason: Type.TLiteral<"criterion_failed">;
26
+ attempt_id: Type.TString;
27
+ }>, Type.TObject<{
28
+ kind: Type.TLiteral<"goal_failed">;
29
+ reason: Type.TLiteral<"failure_edge_exhausted">;
30
+ attempt_id: Type.TString;
31
+ target_task_id: Type.TString;
32
+ traversals: Type.TInteger;
33
+ exhaustion_reason: Type.TEnum<["limit_reached", "target_cancelled"]>;
34
+ }>, Type.TObject<{
35
+ kind: Type.TLiteral<"goal_failed">;
36
+ reason: Type.TEnum<["dependency_failed", "dependency_cancelled"]>;
37
+ prerequisite_task_ids: Type.TArray<Type.TString>;
38
+ }>, Type.TObject<{
39
+ kind: Type.TLiteral<"cancelled">;
40
+ reason: Type.TString;
41
+ }>]>;
42
+ export declare const GraphAttemptSummarySchema: Type.TObject<{
43
+ id: Type.TString;
44
+ ordinal: Type.TInteger;
45
+ input_attempt_ids: Type.TRecord<"^.*$", Type.TString>;
46
+ }>;
47
+ /** Canonical graph-native Task card used by tools, delivery, and Coordination. */
48
+ export declare const GraphTaskCardSchema: Type.TObject<{
49
+ id: Type.TString;
50
+ title: Type.TString;
51
+ goal: Type.TString;
52
+ assignee: Type.TString;
53
+ needs: Type.TArray<Type.TString>;
54
+ on_goal_failed: Type.TOptional<Type.TObject<{
55
+ target: Type.TString;
56
+ max_traversals: Type.TInteger;
57
+ }>>;
58
+ status: Type.TEnum<["dependency_waiting", "ready", "in_progress", "blocked", "goal_failed", "goal_achieved", "cancelled"]>;
59
+ state: Type.TUnion<[Type.TObject<{
60
+ kind: Type.TLiteral<"dependency_waiting">;
61
+ prerequisite_task_ids: Type.TArray<Type.TString>;
62
+ }>, Type.TObject<{
63
+ kind: Type.TLiteral<"ready">;
64
+ }>, Type.TObject<{
65
+ kind: Type.TLiteral<"in_progress">;
66
+ attempt_id: Type.TString;
67
+ }>, Type.TObject<{
68
+ kind: Type.TLiteral<"blocked">;
69
+ attempt_id: Type.TString;
70
+ evidence: Type.TString;
71
+ }>, Type.TObject<{
72
+ kind: Type.TLiteral<"goal_achieved">;
73
+ attempt_id: Type.TString;
74
+ }>, Type.TObject<{
75
+ kind: Type.TLiteral<"goal_failed">;
76
+ reason: Type.TLiteral<"criterion_failed">;
77
+ attempt_id: Type.TString;
78
+ }>, Type.TObject<{
79
+ kind: Type.TLiteral<"goal_failed">;
80
+ reason: Type.TLiteral<"failure_edge_exhausted">;
81
+ attempt_id: Type.TString;
82
+ target_task_id: Type.TString;
83
+ traversals: Type.TInteger;
84
+ exhaustion_reason: Type.TEnum<["limit_reached", "target_cancelled"]>;
85
+ }>, Type.TObject<{
86
+ kind: Type.TLiteral<"goal_failed">;
87
+ reason: Type.TEnum<["dependency_failed", "dependency_cancelled"]>;
88
+ prerequisite_task_ids: Type.TArray<Type.TString>;
89
+ }>, Type.TObject<{
90
+ kind: Type.TLiteral<"cancelled">;
91
+ reason: Type.TString;
92
+ }>]>;
93
+ current_context: Type.TString;
94
+ version: Type.TString;
95
+ activation_key: Type.TOptional<Type.TString>;
96
+ accepted_attempt_id: Type.TOptional<Type.TString>;
97
+ current_attempt: Type.TOptional<Type.TObject<{
98
+ id: Type.TString;
99
+ ordinal: Type.TInteger;
100
+ input_attempt_ids: Type.TRecord<"^.*$", Type.TString>;
101
+ }>>;
102
+ attempts_started: Type.TInteger;
103
+ relations: Type.TOptional<Type.TArray<Type.TObject<{
104
+ relation: Type.TEnum<["blocked_by", "parent", "related"]>;
105
+ target_task_id: Type.TString;
106
+ }>>>;
107
+ dependency_state: Type.TOptional<Type.TUnion<[Type.TObject<{
108
+ kind: Type.TLiteral<"ready">;
109
+ active_blocker_ids: Type.TArray<Type.TString>;
110
+ }>, Type.TObject<{
111
+ kind: Type.TLiteral<"waiting">;
112
+ active_blocker_ids: Type.TArray<Type.TString>;
113
+ }>, Type.TObject<{
114
+ kind: Type.TLiteral<"terminal">;
115
+ active_blocker_ids: Type.TArray<Type.TString>;
116
+ }>]>>;
117
+ projection_warnings: Type.TOptional<Type.TArray<Type.TObject<{
118
+ task_id: Type.TString;
119
+ truncated_fields: Type.TArray<Type.TEnum<["title", "goal", "current_context"]>>;
120
+ incomplete_fields: Type.TArray<Type.TEnum<["title", "goal", "current_context"]>>;
121
+ message: Type.TString;
122
+ }>>>;
123
+ }>;
124
+ /** Breaking replacement for task_create. It applies one complete graph revision. */
125
+ export declare const TaskGraphApplyParametersSchema: Type.TObject<{
126
+ operation_id: Type.TString;
127
+ expected_graph_version: Type.TOptional<Type.TString>;
128
+ tasks: Type.TArray<Type.TObject<{
129
+ key: Type.TString;
130
+ title: Type.TString;
131
+ goal: Type.TString;
132
+ assignee: Type.TString;
133
+ needs: Type.TOptional<Type.TArray<Type.TString>>;
134
+ on_goal_failed: Type.TOptional<Type.TObject<{
135
+ target: Type.TString;
136
+ max_traversals: Type.TInteger;
137
+ }>>;
138
+ }>>;
139
+ }>;
140
+ /** One graph-native Task command. Waiting and ready are derived, never authored. */
141
+ export declare const GraphTaskUpdateParametersSchema: Type.TObject<{
142
+ task_id: Type.TString;
143
+ operation_id: Type.TString;
144
+ expected_version: Type.TString;
145
+ transition: Type.TOptional<Type.TEnum<["claim", "block", "resume", "goal_achieved", "goal_failed", "cancel"]>>;
146
+ current_context: Type.TOptional<Type.TString>;
147
+ evidence: Type.TOptional<Type.TString>;
148
+ }>;
149
+ export type GraphTaskCard = Static<typeof GraphTaskCardSchema>;
150
+ export type GraphTaskStateCard = Static<typeof GraphTaskStateSchema>;
151
+ export type TaskGraphApplyParameters = Static<typeof TaskGraphApplyParametersSchema>;
152
+ export type GraphTaskUpdateParameters = Static<typeof GraphTaskUpdateParametersSchema>;
@@ -0,0 +1,123 @@
1
+ // Ported from Pi Team Bright 0.24.0-rc.4 src/task-authority/graph-control-schemas.ts (MIT). Limits inlined.
2
+ import { Type } from "typebox";
3
+ import { TaskVersionRefSchema } from "./limits.js";
4
+ import { TASK_CARD_GOAL_MAX_LENGTH } from "./limits.js";
5
+ import { TASK_CARD_CONTEXT_MAX_LENGTH, TASK_CARD_TITLE_MAX_LENGTH, TASK_ID_MAX_LENGTH } from "./limits.js";
6
+ const TaskId = Type.String({ minLength: 1, maxLength: TASK_ID_MAX_LENGTH });
7
+ const TaskKey = Type.String({ minLength: 1, maxLength: 64, pattern: "^[A-Za-z0-9_-]+$" });
8
+ const WorkerName = Type.String({ minLength: 1, maxLength: 64 });
9
+ const OperationId = Type.String({ minLength: 1, maxLength: 128 });
10
+ export const GraphVersionRefSchema = Type.String({ pattern: "^g_[0-9a-f]{16}$", minLength: 18, maxLength: 18 });
11
+ export const GraphTaskTransitionSchema = Type.Enum(["claim", "block", "resume", "goal_achieved", "goal_failed", "cancel"]);
12
+ export const GraphFailureEdgeSchema = Type.Object({
13
+ target: TaskKey,
14
+ max_traversals: Type.Integer({ minimum: 1, maximum: 8 }),
15
+ }, { additionalProperties: false });
16
+ export const GraphTaskStateSchema = Type.Union([
17
+ Type.Object({
18
+ kind: Type.Literal("dependency_waiting"),
19
+ prerequisite_task_ids: Type.Array(TaskId, { minItems: 1 }),
20
+ }, { additionalProperties: false }),
21
+ Type.Object({ kind: Type.Literal("ready") }, { additionalProperties: false }),
22
+ Type.Object({ kind: Type.Literal("in_progress"), attempt_id: Type.String({ minLength: 1 }) }, { additionalProperties: false }),
23
+ Type.Object({
24
+ kind: Type.Literal("blocked"),
25
+ attempt_id: Type.String({ minLength: 1 }),
26
+ evidence: Type.String({ minLength: 1 }),
27
+ }, { additionalProperties: false }),
28
+ Type.Object({
29
+ kind: Type.Literal("goal_achieved"),
30
+ attempt_id: Type.String({ minLength: 1 }),
31
+ }, { additionalProperties: false }),
32
+ Type.Object({
33
+ kind: Type.Literal("goal_failed"),
34
+ reason: Type.Literal("criterion_failed"),
35
+ attempt_id: Type.String({ minLength: 1 }),
36
+ }, { additionalProperties: false }),
37
+ Type.Object({
38
+ kind: Type.Literal("goal_failed"),
39
+ reason: Type.Literal("failure_edge_exhausted"),
40
+ attempt_id: Type.String({ minLength: 1 }),
41
+ target_task_id: TaskId,
42
+ traversals: Type.Integer({ minimum: 0 }),
43
+ exhaustion_reason: Type.Enum(["limit_reached", "target_cancelled"]),
44
+ }, { additionalProperties: false }),
45
+ Type.Object({
46
+ kind: Type.Literal("goal_failed"),
47
+ reason: Type.Enum(["dependency_failed", "dependency_cancelled"]),
48
+ prerequisite_task_ids: Type.Array(TaskId, { minItems: 1 }),
49
+ }, { additionalProperties: false }),
50
+ Type.Object({
51
+ kind: Type.Literal("cancelled"),
52
+ reason: Type.String({ minLength: 1 }),
53
+ }, { additionalProperties: false }),
54
+ ]);
55
+ export const GraphAttemptSummarySchema = Type.Object({
56
+ id: Type.String({ minLength: 1 }),
57
+ ordinal: Type.Integer({ minimum: 1 }),
58
+ input_attempt_ids: Type.Record(TaskId, Type.String({ minLength: 1 })),
59
+ }, { additionalProperties: false });
60
+ /** Canonical graph-native Task card used by tools, delivery, and Coordination. */
61
+ export const GraphTaskCardSchema = Type.Object({
62
+ id: TaskId,
63
+ title: Type.String({ minLength: 1, maxLength: TASK_CARD_TITLE_MAX_LENGTH }),
64
+ goal: Type.String({ minLength: 1, maxLength: TASK_CARD_GOAL_MAX_LENGTH }),
65
+ assignee: WorkerName,
66
+ needs: Type.Array(TaskId),
67
+ on_goal_failed: Type.Optional(GraphFailureEdgeSchema),
68
+ status: Type.Enum(["dependency_waiting", "ready", "in_progress", "blocked", "goal_failed", "goal_achieved", "cancelled"]),
69
+ state: GraphTaskStateSchema,
70
+ current_context: Type.String({ minLength: 1, maxLength: TASK_CARD_CONTEXT_MAX_LENGTH }),
71
+ version: TaskVersionRefSchema,
72
+ activation_key: Type.Optional(Type.String({ minLength: 1 })),
73
+ accepted_attempt_id: Type.Optional(Type.String({ minLength: 1 })),
74
+ current_attempt: Type.Optional(GraphAttemptSummarySchema),
75
+ attempts_started: Type.Integer({ minimum: 0 }),
76
+ // Optional compatibility coordinates let generic read-only consumers accept
77
+ // both legacy and graph-native cards without making them graph authority.
78
+ relations: Type.Optional(Type.Array(Type.Object({
79
+ relation: Type.Enum(["blocked_by", "parent", "related"]),
80
+ target_task_id: TaskId,
81
+ }, { additionalProperties: false }))),
82
+ dependency_state: Type.Optional(Type.Union([
83
+ Type.Object({ kind: Type.Literal("ready"), active_blocker_ids: Type.Array(TaskId, { maxItems: 0 }) }, { additionalProperties: false }),
84
+ Type.Object({ kind: Type.Literal("waiting"), active_blocker_ids: Type.Array(TaskId, { minItems: 1 }) }, { additionalProperties: false }),
85
+ Type.Object({ kind: Type.Literal("terminal"), active_blocker_ids: Type.Array(TaskId) }, { additionalProperties: false }),
86
+ ])),
87
+ projection_warnings: Type.Optional(Type.Array(Type.Object({
88
+ task_id: TaskId,
89
+ truncated_fields: Type.Array(Type.Enum(["title", "goal", "current_context"])),
90
+ incomplete_fields: Type.Array(Type.Enum(["title", "goal", "current_context"])),
91
+ message: Type.String({ minLength: 1 }),
92
+ }, { additionalProperties: false }))),
93
+ }, { additionalProperties: false });
94
+ const GraphTaskItemSchema = Type.Object({
95
+ key: TaskKey,
96
+ title: Type.String({ minLength: 1, maxLength: TASK_CARD_TITLE_MAX_LENGTH }),
97
+ goal: Type.String({ minLength: 1, maxLength: TASK_CARD_GOAL_MAX_LENGTH, description: "Desired outcome and external success signal." }),
98
+ assignee: WorkerName,
99
+ needs: Type.Optional(Type.Array(TaskKey, { uniqueItems: true })),
100
+ on_goal_failed: Type.Optional(GraphFailureEdgeSchema),
101
+ }, { additionalProperties: false });
102
+ /** Breaking replacement for task_create. It applies one complete graph revision. */
103
+ export const TaskGraphApplyParametersSchema = Type.Object({
104
+ operation_id: OperationId,
105
+ expected_graph_version: Type.Optional(GraphVersionRefSchema),
106
+ tasks: Type.Array(GraphTaskItemSchema, { minItems: 1 }),
107
+ }, {
108
+ additionalProperties: false,
109
+ description: "Atomically apply the complete assigned Task graph. Omit expected_graph_version only for its first revision.",
110
+ });
111
+ /** One graph-native Task command. Waiting and ready are derived, never authored. */
112
+ export const GraphTaskUpdateParametersSchema = Type.Object({
113
+ task_id: TaskId,
114
+ operation_id: OperationId,
115
+ expected_version: TaskVersionRefSchema,
116
+ transition: Type.Optional(GraphTaskTransitionSchema),
117
+ current_context: Type.Optional(Type.String({ minLength: 1, maxLength: TASK_CARD_CONTEXT_MAX_LENGTH })),
118
+ evidence: Type.Optional(Type.String({ minLength: 1 })),
119
+ }, {
120
+ additionalProperties: false,
121
+ minProperties: 4,
122
+ description: "Apply one explicit Task transition or record current context. dependency_waiting and ready cannot be authored.",
123
+ });
@@ -0,0 +1,19 @@
1
+ import type { TSchema } from 'typebox';
2
+ import type { Result } from './state.ts';
3
+ export declare const TOOL_NAMES: readonly ["team_create", "team_sync", "ensure_worker", "task_graph_apply", "task_read", "task_update", "worker_stop", "team_shutdown", "alert_send"];
4
+ export type ToolName = typeof TOOL_NAMES[number];
5
+ export type Role = 'lead' | 'worker';
6
+ /** The label and description Pi Team Bright registers for one role's tool. */
7
+ export interface ToolPresentation {
8
+ label: string;
9
+ description: string;
10
+ }
11
+ export interface Contract {
12
+ lead: Record<ToolName, TSchema>;
13
+ worker: Partial<Record<ToolName, TSchema>>;
14
+ results: Record<ToolName, TSchema>;
15
+ presentation: Record<Role, Partial<Record<ToolName, ToolPresentation>>>;
16
+ }
17
+ export declare function checkSchema(schema: TSchema, value: unknown): boolean;
18
+ export declare function contracts(goalMaxLength?: number): Contract;
19
+ export declare function checkResult(contract: Contract, name: ToolName, value: Result): Result;
@@ -0,0 +1,45 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { Compile } from 'typebox/compile';
3
+ export const TOOL_NAMES = ['team_create', 'team_sync', 'ensure_worker', 'task_graph_apply', 'task_read', 'task_update', 'worker_stop', 'team_shutdown', 'alert_send'];
4
+ // Generated from the pinned executable rc.7 contract, not the stale agent surface.
5
+ const source = readFileSync(new URL('./rc7-contract.json', import.meta.url), 'utf8');
6
+ const patchedContracts = new Map();
7
+ const validators = new Map();
8
+ export function checkSchema(schema, value) {
9
+ const key = JSON.stringify(schema, (_key, value) => value && typeof value === 'object' && !Array.isArray(value)
10
+ ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : value);
11
+ let validator = validators.get(key);
12
+ if (!validator) {
13
+ validator = Compile(JSON.parse(key));
14
+ validators.set(key, validator);
15
+ }
16
+ return validator.Check(value);
17
+ }
18
+ export function contracts(goalMaxLength = 1600) {
19
+ if (!Number.isSafeInteger(goalMaxLength) || goalMaxLength < 1)
20
+ throw new Error('goalMaxLength must be a positive safe integer');
21
+ const cached = patchedContracts.get(goalMaxLength);
22
+ if (cached !== undefined)
23
+ return JSON.parse(cached);
24
+ const copy = JSON.parse(source);
25
+ function patch(value) {
26
+ if (!value || typeof value !== 'object')
27
+ return;
28
+ const node = value;
29
+ if (node.properties && typeof node.properties === 'object') {
30
+ const goal = node.properties.goal;
31
+ if (goal && typeof goal === 'object')
32
+ goal.maxLength = goalMaxLength;
33
+ }
34
+ for (const child of Object.values(node))
35
+ patch(child);
36
+ }
37
+ patch(copy);
38
+ patchedContracts.set(goalMaxLength, JSON.stringify(copy));
39
+ return copy;
40
+ }
41
+ export function checkResult(contract, name, value) {
42
+ if (!checkSchema(contract.results[name], value))
43
+ throw new Error(`PTB ${name} produced a result outside the rc.7 contract: ${JSON.stringify(value)}`);
44
+ return value;
45
+ }