@oisin-ee/pi-todo 0.7.2 → 0.9.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/package.json +1 -1
- package/src/contract-fixture.ts +103 -0
- package/src/display.ts +8 -2
- package/src/gate.ts +79 -0
- package/src/generated/todo_core.d.ts +342 -5
- package/src/generated/todo_core.js +222 -5
- package/src/generated/todo_core_bg.wasm +0 -0
- package/src/generated/todo_core_bg.wasm.d.ts +8 -1
- package/src/guidance.ts +5 -1
- package/src/handoff.ts +84 -0
- package/src/import-fixture.ts +63 -0
- package/src/index.ts +17 -43
- package/src/lease-fixture.ts +95 -0
- package/src/provider-fixture.ts +50 -0
- package/src/provider.ts +289 -0
- package/src/registration-fixture.ts +126 -0
- package/src/repository.ts +31 -0
- package/src/runtime-fixture.ts +209 -0
- package/src/schema.ts +86 -0
- package/src/seed-fixture.ts +35 -0
- package/src/seed-transport.ts +52 -0
- package/src/source.ts +33 -3
- package/src/state.ts +78 -1
- package/src/tool.ts +15 -14
- package/src/wasm.ts +26 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DefaultResourceLoader,
|
|
3
|
+
ModelRuntime,
|
|
4
|
+
SessionManager,
|
|
5
|
+
SettingsManager,
|
|
6
|
+
createAgentSession,
|
|
7
|
+
createCodemodeExtension,
|
|
8
|
+
createEventBus,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import {
|
|
11
|
+
InMemoryCredentialStore,
|
|
12
|
+
InMemoryModelsStore,
|
|
13
|
+
fauxAssistantMessage,
|
|
14
|
+
fauxProvider,
|
|
15
|
+
fauxToolCall,
|
|
16
|
+
} from "@earendil-works/pi-ai";
|
|
17
|
+
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import type { TaskProviderV1 } from "./provider.ts";
|
|
19
|
+
import type { TodoParams } from "./tool.ts";
|
|
20
|
+
import { Type } from "typebox";
|
|
21
|
+
import { contract } from "./contract-fixture.ts";
|
|
22
|
+
import piTodo from "./index.ts";
|
|
23
|
+
import { registerSeedSource } from "./seed-fixture.ts";
|
|
24
|
+
|
|
25
|
+
type Session = Awaited<ReturnType<typeof createAgentSession>>["session"];
|
|
26
|
+
interface Startup {
|
|
27
|
+
readonly cwd: string;
|
|
28
|
+
readonly seedPath: string;
|
|
29
|
+
}
|
|
30
|
+
interface RuntimeFixture {
|
|
31
|
+
readonly session: Readonly<Pick<Session, "dispose" | "abort" | "messages">> & {
|
|
32
|
+
readonly extensionRunner: Readonly<Pick<Session["extensionRunner"], "emitToolResult" | "emit">>;
|
|
33
|
+
};
|
|
34
|
+
readonly manager: SessionManager;
|
|
35
|
+
readonly provider: () => TaskProviderV1;
|
|
36
|
+
readonly call: (name: string, args: Readonly<Record<string, string>>) => Promise<void>;
|
|
37
|
+
readonly create: () => Promise<void>;
|
|
38
|
+
readonly todo: (params: TodoParams) => Promise<unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Creates resources without user configuration or external services.
|
|
43
|
+
* @param {ExtensionFactory} later Optional additional guard or test tool.
|
|
44
|
+
* @param {boolean} collision Whether the other todo owner loads first.
|
|
45
|
+
* @param {Startup | undefined} startup Optional worker startup configuration.
|
|
46
|
+
* @returns {object} Local resource owners.
|
|
47
|
+
*/
|
|
48
|
+
const resources = (
|
|
49
|
+
later: ExtensionFactory | undefined,
|
|
50
|
+
collision: boolean,
|
|
51
|
+
startup: Startup | undefined,
|
|
52
|
+
): {
|
|
53
|
+
events: ReturnType<typeof createEventBus>;
|
|
54
|
+
manager: SessionManager;
|
|
55
|
+
settings: SettingsManager;
|
|
56
|
+
loader: DefaultResourceLoader;
|
|
57
|
+
} => {
|
|
58
|
+
const events = createEventBus(),
|
|
59
|
+
manager = SessionManager.inMemory(),
|
|
60
|
+
settings = SettingsManager.inMemory({
|
|
61
|
+
compaction: { enabled: false },
|
|
62
|
+
retry: { enabled: false },
|
|
63
|
+
}),
|
|
64
|
+
loader = new DefaultResourceLoader({
|
|
65
|
+
additionalExtensionPaths: collision ? [] : [new URL("index.ts", import.meta.url).pathname],
|
|
66
|
+
agentDir: "/nonexistent/pi-todo-test",
|
|
67
|
+
cwd: startup?.cwd ?? "/tmp",
|
|
68
|
+
eventBus: events,
|
|
69
|
+
extensionFactories: [
|
|
70
|
+
createCodemodeExtension(),
|
|
71
|
+
...(later === undefined ? [] : [later]),
|
|
72
|
+
...(collision ? [piTodo] : []),
|
|
73
|
+
],
|
|
74
|
+
noContextFiles: true,
|
|
75
|
+
noExtensions: true,
|
|
76
|
+
noPromptTemplates: true,
|
|
77
|
+
noSkills: true,
|
|
78
|
+
noThemes: true,
|
|
79
|
+
settingsManager: settings,
|
|
80
|
+
});
|
|
81
|
+
if (startup !== undefined) {
|
|
82
|
+
registerSeedSource(events, { path: startup.seedPath, version: 1 });
|
|
83
|
+
}
|
|
84
|
+
return { events, loader, manager, settings };
|
|
85
|
+
},
|
|
86
|
+
/**
|
|
87
|
+
* Requires one synchronous response from the real event bus.
|
|
88
|
+
* @param {Readonly<object>} events Runtime event bus.
|
|
89
|
+
* @returns {TaskProviderV1} Acquired extension capability.
|
|
90
|
+
*/
|
|
91
|
+
acquire = (events: Readonly<ReturnType<typeof createEventBus>>): TaskProviderV1 => {
|
|
92
|
+
const replies: TaskProviderV1[] = [],
|
|
93
|
+
version = 1;
|
|
94
|
+
events.emit("oisin.tasks.acquire.v1", {
|
|
95
|
+
reply: (value: TaskProviderV1): void => {
|
|
96
|
+
replies.push(value);
|
|
97
|
+
},
|
|
98
|
+
version,
|
|
99
|
+
});
|
|
100
|
+
const [provider] = replies;
|
|
101
|
+
if (provider === undefined || replies.length !== version) {
|
|
102
|
+
throw new Error("Expected exactly one synchronous provider");
|
|
103
|
+
}
|
|
104
|
+
return provider;
|
|
105
|
+
},
|
|
106
|
+
/**
|
|
107
|
+
* Executes the registered todo entry point, including its persistence checks.
|
|
108
|
+
* @param {Session} session Real SDK session.
|
|
109
|
+
* @param {TodoParams} params Requested mutation.
|
|
110
|
+
* @returns {Promise<unknown>} Actual tool result.
|
|
111
|
+
*/
|
|
112
|
+
todoCall = async (
|
|
113
|
+
session: {
|
|
114
|
+
readonly extensionRunner: Readonly<
|
|
115
|
+
Pick<Session["extensionRunner"], "getToolDefinition" | "createToolContext">
|
|
116
|
+
>;
|
|
117
|
+
},
|
|
118
|
+
params: TodoParams,
|
|
119
|
+
): Promise<unknown> => {
|
|
120
|
+
const tool = session.extensionRunner.getToolDefinition("todo");
|
|
121
|
+
if (tool === undefined) {
|
|
122
|
+
throw new Error("Missing todo registration");
|
|
123
|
+
}
|
|
124
|
+
return tool.execute(
|
|
125
|
+
"review",
|
|
126
|
+
params,
|
|
127
|
+
undefined,
|
|
128
|
+
undefined,
|
|
129
|
+
session.extensionRunner.createToolContext("review", new AbortController().signal),
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
/**
|
|
133
|
+
* Executes a scripted tool request through the actual model runtime.
|
|
134
|
+
* @param {Session} session Session execution owner.
|
|
135
|
+
* @param {Readonly<ReturnType<typeof fauxProvider>>} faux Scripted provider.
|
|
136
|
+
* @returns {RuntimeFixture["call"]} Request function.
|
|
137
|
+
*/
|
|
138
|
+
caller =
|
|
139
|
+
(
|
|
140
|
+
session: Readonly<Pick<Session, "prompt">>,
|
|
141
|
+
faux: Readonly<Pick<ReturnType<typeof fauxProvider>, "setResponses">>,
|
|
142
|
+
): RuntimeFixture["call"] =>
|
|
143
|
+
async (name, args): Promise<void> => {
|
|
144
|
+
faux.setResponses([
|
|
145
|
+
fauxAssistantMessage(fauxToolCall(name, args)),
|
|
146
|
+
fauxAssistantMessage("Done."),
|
|
147
|
+
]);
|
|
148
|
+
await session.prompt("Exercise one tool.");
|
|
149
|
+
},
|
|
150
|
+
/**
|
|
151
|
+
* Loads the real runner and codemode with a scripted model.
|
|
152
|
+
* @param {ExtensionFactory} later Additional extension.
|
|
153
|
+
* @param {boolean} collision Loads a competing owner first.
|
|
154
|
+
* @param {Startup | undefined} startup Optional worker startup configuration.
|
|
155
|
+
* @returns {Promise<RuntimeFixture>} Isolated runtime and callers.
|
|
156
|
+
*/
|
|
157
|
+
runtimeFixture = async (
|
|
158
|
+
later?: ExtensionFactory,
|
|
159
|
+
collision = false,
|
|
160
|
+
startup?: Startup,
|
|
161
|
+
): Promise<RuntimeFixture> => {
|
|
162
|
+
const setup = resources(later, collision, startup),
|
|
163
|
+
modelRuntime = await ModelRuntime.create({
|
|
164
|
+
credentials: new InMemoryCredentialStore(),
|
|
165
|
+
modelsPath: null,
|
|
166
|
+
modelsStore: new InMemoryModelsStore(),
|
|
167
|
+
refreshOnCreate: false,
|
|
168
|
+
}),
|
|
169
|
+
faux = fauxProvider({ tokensPerSecond: 0 });
|
|
170
|
+
modelRuntime.registerNativeProvider(faux.provider);
|
|
171
|
+
await setup.loader.reload();
|
|
172
|
+
const { session } = await createAgentSession({
|
|
173
|
+
cwd: startup?.cwd ?? "/tmp",
|
|
174
|
+
model: faux.getModel(),
|
|
175
|
+
modelRuntime,
|
|
176
|
+
resourceLoader: setup.loader,
|
|
177
|
+
sessionManager: setup.manager,
|
|
178
|
+
settingsManager: setup.settings,
|
|
179
|
+
thinkingLevel: "off",
|
|
180
|
+
});
|
|
181
|
+
await session.bindExtensions({});
|
|
182
|
+
session.setActiveToolsByName([...session.getActiveToolNames(), "codemode"]);
|
|
183
|
+
return {
|
|
184
|
+
call: caller(session, faux),
|
|
185
|
+
create: async (): Promise<void> => {
|
|
186
|
+
await todoCall(session, {
|
|
187
|
+
items: [{ content: "Build", contract: contract(), key: "build" }],
|
|
188
|
+
op: "init",
|
|
189
|
+
start: "build",
|
|
190
|
+
});
|
|
191
|
+
},
|
|
192
|
+
manager: setup.manager,
|
|
193
|
+
provider: () => acquire(setup.events),
|
|
194
|
+
session,
|
|
195
|
+
todo: async (params): Promise<unknown> => todoCall(session, params),
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
/**
|
|
199
|
+
* Produces a deterministic tool result.
|
|
200
|
+
* @returns {object} Successful content.
|
|
201
|
+
*/
|
|
202
|
+
emptyResult = (): { content: [{ type: "text"; text: string }]; details: undefined } => ({
|
|
203
|
+
content: [{ text: "ok", type: "text" }],
|
|
204
|
+
details: undefined,
|
|
205
|
+
}),
|
|
206
|
+
emptyParameters = Type.Object({});
|
|
207
|
+
|
|
208
|
+
export { emptyParameters, emptyResult, runtimeFixture };
|
|
209
|
+
export type { RuntimeFixture };
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
const strings = Type.Immutable(Type.Array(Type.String())),
|
|
5
|
+
scope = Type.Immutable(
|
|
6
|
+
Type.Object(
|
|
7
|
+
{
|
|
8
|
+
generates: strings,
|
|
9
|
+
owns: strings,
|
|
10
|
+
reads: strings,
|
|
11
|
+
repositoryId: Type.String(),
|
|
12
|
+
},
|
|
13
|
+
{ additionalProperties: false },
|
|
14
|
+
),
|
|
15
|
+
),
|
|
16
|
+
self = Type.Object({ kind: Type.Literal("self") }, { additionalProperties: false }),
|
|
17
|
+
worker = Type.Object(
|
|
18
|
+
{ kind: Type.Literal("worker"), profile: Type.String() },
|
|
19
|
+
{ additionalProperties: false },
|
|
20
|
+
),
|
|
21
|
+
assignment = Type.Union([self, worker]),
|
|
22
|
+
command = Type.Object(
|
|
23
|
+
{ argv: strings, cwd: Type.String(), expected: Type.String(), kind: Type.Literal("command") },
|
|
24
|
+
{ additionalProperties: false },
|
|
25
|
+
),
|
|
26
|
+
review = Type.Object(
|
|
27
|
+
{ artifact: Type.String(), criterion: Type.String(), kind: Type.Literal("review") },
|
|
28
|
+
{ additionalProperties: false },
|
|
29
|
+
),
|
|
30
|
+
acceptance = Type.Union([command, review]),
|
|
31
|
+
fields = Type.Object(
|
|
32
|
+
{
|
|
33
|
+
acceptance: Type.Optional(acceptance),
|
|
34
|
+
assignment: Type.Optional(assignment),
|
|
35
|
+
deliverable: Type.Optional(Type.String()),
|
|
36
|
+
dependsOn: Type.Optional(strings),
|
|
37
|
+
scope: Type.Optional(scope),
|
|
38
|
+
},
|
|
39
|
+
{ additionalProperties: false },
|
|
40
|
+
),
|
|
41
|
+
contract = Type.Immutable(fields),
|
|
42
|
+
draftFields = Type.Object(
|
|
43
|
+
{
|
|
44
|
+
content: Type.String(),
|
|
45
|
+
contract: Type.Optional(contract),
|
|
46
|
+
key: Type.Optional(Type.String()),
|
|
47
|
+
},
|
|
48
|
+
{ additionalProperties: false },
|
|
49
|
+
),
|
|
50
|
+
draft = Type.Immutable(draftFields),
|
|
51
|
+
leaf = Type.Union([Type.String(), draft]),
|
|
52
|
+
subtasks = Type.Immutable(Type.Array(leaf)),
|
|
53
|
+
outcome = Type.Immutable(
|
|
54
|
+
Type.Object({ content: Type.String(), subtasks }, { additionalProperties: false }),
|
|
55
|
+
),
|
|
56
|
+
item = Type.Union([leaf, outcome]),
|
|
57
|
+
items = Type.Immutable(Type.Array(item)),
|
|
58
|
+
evidence = Type.String({
|
|
59
|
+
description: "Concrete acceptance result or integration-review evidence. Required for done.",
|
|
60
|
+
}),
|
|
61
|
+
parent = Type.String({
|
|
62
|
+
description: "Parent ID. Exact content is accepted only for unmigrated legacy records.",
|
|
63
|
+
}),
|
|
64
|
+
start = Type.String({
|
|
65
|
+
description: "Atomically start this ID or creation key with init/append/update.",
|
|
66
|
+
}),
|
|
67
|
+
task = Type.String({
|
|
68
|
+
description:
|
|
69
|
+
"Unit ID, or exact legacy content before migration. Children can use their ID without parent.",
|
|
70
|
+
}),
|
|
71
|
+
parameters = Type.Immutable(
|
|
72
|
+
Type.Object(
|
|
73
|
+
{
|
|
74
|
+
contract: Type.Optional(contract),
|
|
75
|
+
evidence: Type.Optional(evidence),
|
|
76
|
+
items: Type.Optional(items),
|
|
77
|
+
op: StringEnum(["init", "start", "done", "drop", "append", "view", "update"] as const),
|
|
78
|
+
parent: Type.Optional(parent),
|
|
79
|
+
start: Type.Optional(start),
|
|
80
|
+
task: Type.Optional(task),
|
|
81
|
+
},
|
|
82
|
+
{ additionalProperties: false },
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
export { parameters };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
const version = 1;
|
|
4
|
+
|
|
5
|
+
interface Query {
|
|
6
|
+
readonly version: number;
|
|
7
|
+
readonly reply: (value: unknown) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Checks the transport request before a fixture responds.
|
|
12
|
+
* @param {unknown} request Event payload.
|
|
13
|
+
* @returns {boolean} Whether the response callback exists.
|
|
14
|
+
*/
|
|
15
|
+
const query = (request: unknown): request is Query =>
|
|
16
|
+
typeof request === "object" &&
|
|
17
|
+
request !== null &&
|
|
18
|
+
"version" in request &&
|
|
19
|
+
request.version === version &&
|
|
20
|
+
"reply" in request &&
|
|
21
|
+
typeof request.reply === "function",
|
|
22
|
+
/**
|
|
23
|
+
* Supplies an executor response without loading execution machinery.
|
|
24
|
+
* @param {Readonly<ExtensionAPI["events"]>} events Synchronous runtime bus.
|
|
25
|
+
* @param {unknown} response Test response.
|
|
26
|
+
*/
|
|
27
|
+
registerSeedSource = (events: Readonly<ExtensionAPI["events"]>, response: unknown): void => {
|
|
28
|
+
events.on("oisin.tasks.seed-source.v1", (request: unknown): void => {
|
|
29
|
+
if (query(request)) {
|
|
30
|
+
request.reply(response);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export { registerSeedSource };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
const version = 1,
|
|
4
|
+
empty = 0,
|
|
5
|
+
/**
|
|
6
|
+
* Validates one executor response without interpreting task state.
|
|
7
|
+
* @param {unknown} source Untrusted transport response.
|
|
8
|
+
* @returns {string | undefined} Source path or ordinary launch.
|
|
9
|
+
*/
|
|
10
|
+
sourcePath = (source: unknown): string | undefined => {
|
|
11
|
+
if (
|
|
12
|
+
typeof source !== "object" ||
|
|
13
|
+
source === null ||
|
|
14
|
+
!("version" in source) ||
|
|
15
|
+
source.version !== version ||
|
|
16
|
+
!("path" in source) ||
|
|
17
|
+
Object.keys(source).some((key) => key !== "version" && key !== "path")
|
|
18
|
+
) {
|
|
19
|
+
throw new Error("IMPORT_FAILED: invalid seed source");
|
|
20
|
+
}
|
|
21
|
+
if (source.path === null) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
if (typeof source.path !== "string" || source.path.trim().length === empty) {
|
|
25
|
+
throw new Error("IMPORT_FAILED: invalid seed path");
|
|
26
|
+
}
|
|
27
|
+
return source.path;
|
|
28
|
+
},
|
|
29
|
+
/**
|
|
30
|
+
* Reads executor-owned launch transport without registering an executor flag.
|
|
31
|
+
* @param {Readonly<ExtensionAPI["events"]>} events Synchronous extension event bus.
|
|
32
|
+
* @returns {string | undefined} Validated source path, or no managed launch.
|
|
33
|
+
*/
|
|
34
|
+
seedPath = (events: Readonly<ExtensionAPI["events"]>): string | undefined => {
|
|
35
|
+
const replies: unknown[] = [];
|
|
36
|
+
events.emit("oisin.tasks.seed-source.v1", {
|
|
37
|
+
reply: (value: unknown): void => {
|
|
38
|
+
replies.push(value);
|
|
39
|
+
},
|
|
40
|
+
version,
|
|
41
|
+
});
|
|
42
|
+
if (replies.length === empty) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (replies.length !== version) {
|
|
46
|
+
throw new Error("IMPORT_FAILED: ambiguous seed source");
|
|
47
|
+
}
|
|
48
|
+
const [source] = replies;
|
|
49
|
+
return sourcePath(source);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export { seedPath };
|
package/src/source.ts
CHANGED
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
* path. The two differ when the path has a symlink, such as macOS `/tmp` or a
|
|
5
5
|
* linked agent directory, so both are compared after resolving symlinks.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
closeSync,
|
|
9
|
+
constants,
|
|
10
|
+
existsSync,
|
|
11
|
+
fstatSync,
|
|
12
|
+
openSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
realpathSync,
|
|
15
|
+
} from "node:fs";
|
|
8
16
|
|
|
9
17
|
const OWN = realpathSync(new URL("index.ts", import.meta.url)),
|
|
10
18
|
/**
|
|
@@ -13,6 +21,28 @@ const OWN = realpathSync(new URL("index.ts", import.meta.url)),
|
|
|
13
21
|
* @param {string} path A tool's `sourceInfo.path`.
|
|
14
22
|
* @returns {boolean} Whether the file is this package's `index.ts`.
|
|
15
23
|
*/
|
|
16
|
-
isOwnSource = (path: string): boolean => existsSync(path) && realpathSync(path) === OWN
|
|
24
|
+
isOwnSource = (path: string): boolean => existsSync(path) && realpathSync(path) === OWN,
|
|
25
|
+
/**
|
|
26
|
+
* Reads only a bounded regular transport file owned by the executor.
|
|
27
|
+
* @param {string} path Seed artifact path.
|
|
28
|
+
* @returns {string} Untrusted JSON for Rust validation.
|
|
29
|
+
*/
|
|
30
|
+
seedSource = (path: string): string => {
|
|
31
|
+
const descriptor = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK),
|
|
32
|
+
maximum = 1_048_576;
|
|
33
|
+
try {
|
|
34
|
+
const metadata = fstatSync(descriptor);
|
|
35
|
+
if (!metadata.isFile() || metadata.size > maximum) {
|
|
36
|
+
throw new Error("IMPORT_FAILED: seed must be a regular file of at most 1 MiB");
|
|
37
|
+
}
|
|
38
|
+
const bytes = readFileSync(descriptor);
|
|
39
|
+
if (bytes.byteLength > maximum) {
|
|
40
|
+
throw new Error("IMPORT_FAILED: seed exceeds 1 MiB");
|
|
41
|
+
}
|
|
42
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
43
|
+
} finally {
|
|
44
|
+
closeSync(descriptor);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
17
47
|
|
|
18
|
-
export { isOwnSource };
|
|
48
|
+
export { isOwnSource, seedSource };
|
package/src/state.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { CustomEntry, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ExecutionRef, TodoList, UnitRef } from "./wasm.ts";
|
|
3
|
+
import { parse } from "./wasm.ts";
|
|
2
4
|
|
|
3
5
|
/** The snapshot of a branch with no list yet. */
|
|
4
6
|
const EMPTY: unknown = { tasks: [] },
|
|
@@ -23,6 +25,81 @@ const EMPTY: unknown = { tasks: [] },
|
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
27
|
return EMPTY;
|
|
28
|
+
},
|
|
29
|
+
/**
|
|
30
|
+
* Resolves the first branch entry for a contract revision.
|
|
31
|
+
* @param {Readonly<UnitRef>} reference Revision identity.
|
|
32
|
+
* @param {string} entryId Containing Pi entry.
|
|
33
|
+
* @param {Readonly<Map<string, string>>} origins Prior branch origins.
|
|
34
|
+
* @returns {string} The revision's originating entry.
|
|
35
|
+
*/
|
|
36
|
+
originFor = (
|
|
37
|
+
reference: Readonly<UnitRef>,
|
|
38
|
+
entryId: string,
|
|
39
|
+
origins: Readonly<Map<string, string>>,
|
|
40
|
+
): string => {
|
|
41
|
+
const key = JSON.stringify([reference.sessionId, reference.unitId, reference.revision]),
|
|
42
|
+
origin = origins.get(key) ?? entryId;
|
|
43
|
+
origins.set(key, origin);
|
|
44
|
+
return origin;
|
|
26
45
|
};
|
|
27
46
|
|
|
28
|
-
|
|
47
|
+
/** Derives entry provenance without rewriting the imported lead reference. */
|
|
48
|
+
class Provenance {
|
|
49
|
+
readonly #origins = new Map<string, string>();
|
|
50
|
+
readonly #seals = new Map<string, string>();
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolves contract and seal provenance for one task snapshot.
|
|
54
|
+
* @param {Readonly<object>} unit Task provenance fields.
|
|
55
|
+
* @param {string} entryId Snapshot identity.
|
|
56
|
+
*/
|
|
57
|
+
public resolve(
|
|
58
|
+
unit: {
|
|
59
|
+
readonly reference: Readonly<UnitRef>;
|
|
60
|
+
readonly imported:
|
|
61
|
+
| {
|
|
62
|
+
readonly seed: { readonly execution: Readonly<ExecutionRef> };
|
|
63
|
+
readonly seal: { readonly entryId: string } | undefined;
|
|
64
|
+
}
|
|
65
|
+
| undefined;
|
|
66
|
+
},
|
|
67
|
+
entryId: string,
|
|
68
|
+
): void {
|
|
69
|
+
if (unit.imported === undefined) {
|
|
70
|
+
Object.assign(unit.reference, {
|
|
71
|
+
contractEntryId: originFor(unit.reference, entryId, this.#origins),
|
|
72
|
+
});
|
|
73
|
+
} else if (unit.imported.seal !== undefined) {
|
|
74
|
+
const key = JSON.stringify(unit.imported.seed.execution),
|
|
75
|
+
sealEntry = this.#seals.get(key) ?? entryId;
|
|
76
|
+
this.#seals.set(key, sealEntry);
|
|
77
|
+
Object.assign(unit.imported.seal, { entryId: sealEntry });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Reads revision origins from the entries that first introduced them.
|
|
84
|
+
* Status-only snapshots cannot move a revision's origin.
|
|
85
|
+
* @param {readonly SessionEntry[]} branch Current branch in chronological order.
|
|
86
|
+
* @returns {TodoList} Validated snapshot with derived provenance.
|
|
87
|
+
*/
|
|
88
|
+
const branchSnapshot = (branch: readonly SessionEntry[]): TodoList => {
|
|
89
|
+
const provenance = new Provenance();
|
|
90
|
+
let current: TodoList = { tasks: [] };
|
|
91
|
+
for (const entry of branch.filter((candidate) => isStateEntry(candidate))) {
|
|
92
|
+
current = parse(entry.data);
|
|
93
|
+
for (const task of current.tasks.flatMap((root) => [root, ...(root.subtasks ?? [])])) {
|
|
94
|
+
if (task.unit !== undefined) {
|
|
95
|
+
provenance.resolve(
|
|
96
|
+
{ imported: task.unit.imported, reference: task.unit.reference },
|
|
97
|
+
entry.id,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return current;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export { STATE_ENTRY, branchSnapshot, latestSnapshot };
|
package/src/tool.ts
CHANGED
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
import { STATE_ENTRY,
|
|
2
|
-
import { apply,
|
|
1
|
+
import { STATE_ENTRY, branchSnapshot } from "./state.ts";
|
|
2
|
+
import { apply, view } from "./wasm.ts";
|
|
3
3
|
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { Static } from "typebox";
|
|
4
5
|
import type { TodoList } from "./wasm.ts";
|
|
6
|
+
import type { parameters } from "./schema.ts";
|
|
5
7
|
import { workUnitGuidance } from "./guidance.ts";
|
|
6
8
|
|
|
7
9
|
/** One `todo` call, as the schema in `index.ts` admits it. */
|
|
8
|
-
|
|
9
|
-
readonly op: "init" | "start" | "done" | "drop" | "append" | "view";
|
|
10
|
-
readonly items?: readonly (
|
|
11
|
-
| string
|
|
12
|
-
| { readonly content: string; readonly subtasks: readonly string[] }
|
|
13
|
-
)[];
|
|
14
|
-
readonly parent?: string;
|
|
15
|
-
readonly task?: string;
|
|
16
|
-
}
|
|
10
|
+
type TodoParams = Static<typeof parameters>;
|
|
17
11
|
|
|
18
12
|
/** What a call reads from and writes to the session. */
|
|
19
13
|
interface TodoSession {
|
|
14
|
+
readonly sessionId: string;
|
|
20
15
|
readonly branch: () => readonly SessionEntry[];
|
|
21
16
|
readonly append: (customType: string, list: TodoList) => void;
|
|
22
17
|
}
|
|
@@ -47,12 +42,18 @@ const result = (list: TodoList, creation: boolean): TodoResult => ({
|
|
|
47
42
|
* @returns {TodoResult} The result for the model and the renderer.
|
|
48
43
|
*/
|
|
49
44
|
runTodo = (params: TodoParams, session: TodoSession): TodoResult => {
|
|
50
|
-
const current =
|
|
51
|
-
{ list, persist } = apply(current, params
|
|
45
|
+
const current = branchSnapshot(session.branch()),
|
|
46
|
+
{ list, persist } = apply(current, params, {
|
|
47
|
+
mutationId: crypto.randomUUID(),
|
|
48
|
+
sessionId: session.sessionId,
|
|
49
|
+
});
|
|
52
50
|
if (persist) {
|
|
53
51
|
session.append(STATE_ENTRY, list);
|
|
54
52
|
}
|
|
55
|
-
return result(
|
|
53
|
+
return result(
|
|
54
|
+
persist ? branchSnapshot(session.branch()) : list,
|
|
55
|
+
params.op === "init" || params.op === "append",
|
|
56
|
+
);
|
|
56
57
|
};
|
|
57
58
|
|
|
58
59
|
export { runTodo };
|
package/src/wasm.ts
CHANGED
|
@@ -14,7 +14,14 @@ initSync({
|
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
export {
|
|
17
|
+
active,
|
|
17
18
|
apply,
|
|
19
|
+
claim,
|
|
20
|
+
inspect,
|
|
21
|
+
import_unit as importUnit,
|
|
22
|
+
imported,
|
|
23
|
+
protect,
|
|
24
|
+
settle,
|
|
18
25
|
completion_reminder as completionReminder,
|
|
19
26
|
init,
|
|
20
27
|
parse,
|
|
@@ -22,6 +29,25 @@ export {
|
|
|
22
29
|
widget,
|
|
23
30
|
} from "./generated/todo_core.js";
|
|
24
31
|
export type {
|
|
32
|
+
Acceptance,
|
|
33
|
+
ActiveUnit,
|
|
34
|
+
Assignment,
|
|
35
|
+
ClaimInput,
|
|
36
|
+
ClaimResult,
|
|
37
|
+
ImportReceipt,
|
|
38
|
+
Seed,
|
|
39
|
+
EvidenceRef,
|
|
40
|
+
ExecutionRef,
|
|
41
|
+
Settlement,
|
|
42
|
+
SettlementResult,
|
|
43
|
+
TaskBinding,
|
|
44
|
+
TaskExport,
|
|
45
|
+
ContractDraft,
|
|
46
|
+
Scope,
|
|
47
|
+
MutationContext,
|
|
48
|
+
Unit,
|
|
49
|
+
UnitContract,
|
|
50
|
+
UnitRef,
|
|
25
51
|
Operation,
|
|
26
52
|
ReminderFact,
|
|
27
53
|
Request,
|