@oisin-ee/oisinpi 0.10.2 → 0.11.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @oisin-ee/oisinpi
2
2
 
3
+ ## 0.11.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`f1837f2`](https://github.com/oisin-ee/oisinpi/commit/f1837f28bc25fb00feebf7c47509db9bbd3b897c)]:
8
+ - @oisin-ee/pi-guardrails@0.11.1
9
+ - @oisin-ee/pi-prewalk@0.11.1
10
+ - @oisin-ee/pi-tiers@0.11.1
11
+ - @oisin-ee/pi-todo@0.11.1
12
+
13
+ ## 0.11.0
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [[`49b00f1`](https://github.com/oisin-ee/oisinpi/commit/49b00f117d4d64a51cdb2b502ac505a99ffe83dc), [`0e37d82`](https://github.com/oisin-ee/oisinpi/commit/0e37d8259d4a7cb4cffbeccfce2a36caa0274b6e)]:
18
+ - @oisin-ee/pi-guardrails@0.11.0
19
+ - @oisin-ee/pi-todo@0.11.0
20
+ - @oisin-ee/pi-prewalk@0.11.0
21
+ - @oisin-ee/pi-tiers@0.11.0
22
+
3
23
  ## 0.10.2
4
24
 
5
25
  ### Patch Changes
@@ -13,6 +13,14 @@ Agent guardrails for [pi](https://pi.dev), Claude Code, and Codex. One set of po
13
13
  In pi, it also fills a missing shell timeout (120 s) and blocks one above 10 minutes, naming each tool's unit.
14
14
  Shell text is parsed with `shfmt`, which must be on `PATH`. A command that cannot be parsed is blocked.
15
15
 
16
+ ## Planning models
17
+
18
+ The guardrails do not restrict a model by its identity. The planning selections `tier/plan-claude` and
19
+ `tier/plan-gpt` keep the full policy set: a planning model may run Git commands, write and edit files,
20
+ and use the workflow tools.
21
+
22
+ Planning restrictions belong to Plannotator. It owns plan mode, and it blocks writes while it plans.
23
+
16
24
  ## pi
17
25
 
18
26
  ```bash
@@ -95,7 +95,7 @@ pipelineStages = (stmt, source) => {
95
95
  }
96
96
  };
97
97
  visit(tree);
98
- return { calls, guards };
98
+ return { tree, calls, guards };
99
99
  }, parsedOf = (command) => {
100
100
  if (cache.command !== command || cache.parsed === undefined) {
101
101
  cache.parsed = parse(command);
@@ -117,4 +117,67 @@ shellCalls = (command) => parsedOf(command).calls.map((words) => [...words]),
117
117
  * @returns {(string[][] | null)[]} One guard per call.
118
118
  */
119
119
  shellCallGuards = (command) => parsedOf(command).guards.map((stages) => stages?.map((words) => [...words]) ?? null);
120
- export { shellCallGuards, shellCalls };
120
+ /**
121
+ * Whether a literal contains a glob character that the shell would expand. An
122
+ * escaped character is literal, so it does not expand.
123
+ * @param {string} value The raw literal text.
124
+ * @returns {boolean} True for an active glob character.
125
+ */
126
+ function hasActiveGlob(value) {
127
+ for (let index = 0; index < value.length; index += 1) {
128
+ const character = value[index] ?? "";
129
+ if (!"*?[]{}".includes(character)) {
130
+ continue;
131
+ }
132
+ let backslashes = 0;
133
+ for (let before = index - 1; before >= 0 && value[before] === "\\"; before -= 1) {
134
+ backslashes += 1;
135
+ }
136
+ if (backslashes % 2 === 0) {
137
+ return true;
138
+ }
139
+ }
140
+ return false;
141
+ }
142
+ /**
143
+ * Returns literal calls only when the complete AST has read-only shell syntax.
144
+ * @param {string} command Shell text.
145
+ * @returns {string[][] | null} Literal argv, or null for unsupported syntax.
146
+ */
147
+ function literalShellCalls(command) {
148
+ const parsed = parsedOf(command), allowed = new Set([
149
+ "File",
150
+ "Stmt",
151
+ "CallExpr",
152
+ "Word",
153
+ "Lit",
154
+ "SglQuoted",
155
+ "DblQuoted",
156
+ "BinaryCmd",
157
+ ]), safe = (node, quoted = false) => {
158
+ if (Array.isArray(node)) {
159
+ return node.every((item) => safe(item, quoted));
160
+ }
161
+ if (!isNode(node)) {
162
+ return true;
163
+ }
164
+ if (node.Type !== undefined && !allowed.has(node.Type)) {
165
+ return false;
166
+ }
167
+ if (node.Dollar === true ||
168
+ node.Background === true ||
169
+ (node.Redirs ?? []).length > 0 ||
170
+ (node.Assigns ?? []).length > 0 ||
171
+ (node.Type === "CallExpr" && (node.Args ?? []).length === 0) ||
172
+ (node.Type === "BinaryCmd" && !["&&", "||", "|"].includes(node.Op ?? "")) ||
173
+ (node.Type === "Lit" && !quoted && hasActiveGlob(node.Value ?? "")) ||
174
+ (node.Type === "Lit" && !quoted && (node.Value ?? "").startsWith("~"))) {
175
+ return false;
176
+ }
177
+ return Object.values(node).every((value) => safe(value, quoted || node.Type === "DblQuoted"));
178
+ };
179
+ return safe(parsed.tree) && parsed.calls.length > 0
180
+ ? parsed.calls.map((words) => [...words])
181
+ : null;
182
+ }
183
+ export { literalShellCalls, shellCallGuards, shellCalls };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oisin-ee/pi-guardrails",
3
- "version": "0.10.2",
3
+ "version": "0.11.1",
4
4
  "description": "Agent guardrails for pi, Claude Code, and Codex: Git safety, no waiting, no typing into other agents, GitOps-only clusters.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -49,15 +49,29 @@ const MAX_NESTING = 8,
49
49
  ),
50
50
  editViolation = (call: ToolCall, cwd: string): string | null =>
51
51
  classifyEdits(editInputsFromPayload({ tool_input: call.input, tool_name: call.toolName }), cwd),
52
- nestedCalls = (input: Readonly<Record<string, unknown>>): ToolCall[] => {
52
+ /**
53
+ * The nested calls of a codemode input.
54
+ * @param {Readonly<Record<string, unknown>>} input The tool input.
55
+ * @returns {readonly ToolCall[]} The calls of the envelope.
56
+ */
57
+ nestedCalls = (input: Readonly<Record<string, unknown>>): readonly ToolCall[] => {
53
58
  const uses = input["tool_uses"];
54
59
  if (!Array.isArray(uses)) {
55
60
  return [];
56
61
  }
57
- return uses.filter(isRecord).map((use) => ({
58
- input: isRecord(use["parameters"]) ? use["parameters"] : {},
59
- toolName: typeof use["recipient_name"] === "string" ? use["recipient_name"] : "",
60
- }));
62
+ const calls: ToolCall[] = [];
63
+ for (const use of uses) {
64
+ if (!isRecord(use)) {
65
+ continue;
66
+ }
67
+ const toolName = use["recipient_name"];
68
+ const parameters = use["parameters"];
69
+ calls.push({
70
+ input: isRecord(parameters) ? parameters : {},
71
+ toolName: typeof toolName === "string" ? toolName : "",
72
+ });
73
+ }
74
+ return calls;
61
75
  },
62
76
  toolViolation = (event: ToolCall, cwd: string, depth = 0): string | null => {
63
77
  if (depth > MAX_NESTING) {
@@ -72,8 +86,8 @@ const MAX_NESTING = 8,
72
86
  if (own !== null) {
73
87
  return own;
74
88
  }
75
- for (const nested of nestedCalls(call.input)) {
76
- const message = toolViolation(nested, cwd, depth + 1);
89
+ for (const child of nestedCalls(call.input)) {
90
+ const message = toolViolation(child, cwd, depth + 1);
77
91
  if (message !== null) {
78
92
  return message;
79
93
  }
@@ -32,6 +32,7 @@ interface ShNode {
32
32
  }
33
33
 
34
34
  interface Parsed {
35
+ readonly tree: unknown;
35
36
  readonly calls: readonly Words[];
36
37
  readonly guards: readonly Guard[];
37
38
  }
@@ -145,7 +146,7 @@ const MAX_BUFFER = 16 * 1024 * 1024,
145
146
  }
146
147
  };
147
148
  visit(tree);
148
- return { calls, guards };
149
+ return { tree, calls, guards };
149
150
  },
150
151
  parsedOf = (command: string): Parsed => {
151
152
  if (cache.command !== command || cache.parsed === undefined) {
@@ -170,4 +171,75 @@ const MAX_BUFFER = 16 * 1024 * 1024,
170
171
  shellCallGuards = (command: string): (string[][] | null)[] =>
171
172
  parsedOf(command).guards.map((stages) => stages?.map((words) => [...words]) ?? null);
172
173
 
173
- export { shellCallGuards, shellCalls };
174
+ /**
175
+ * Whether a literal contains a glob character that the shell would expand. An
176
+ * escaped character is literal, so it does not expand.
177
+ * @param {string} value The raw literal text.
178
+ * @returns {boolean} True for an active glob character.
179
+ */
180
+ function hasActiveGlob(value: string): boolean {
181
+ for (let index = 0; index < value.length; index += 1) {
182
+ const character = value[index] ?? "";
183
+ if (!"*?[]{}".includes(character)) {
184
+ continue;
185
+ }
186
+ let backslashes = 0;
187
+ for (let before = index - 1; before >= 0 && value[before] === "\\"; before -= 1) {
188
+ backslashes += 1;
189
+ }
190
+ if (backslashes % 2 === 0) {
191
+ return true;
192
+ }
193
+ }
194
+ return false;
195
+ }
196
+
197
+ /**
198
+ * Returns literal calls only when the complete AST has read-only shell syntax.
199
+ * @param {string} command Shell text.
200
+ * @returns {string[][] | null} Literal argv, or null for unsupported syntax.
201
+ */
202
+ function literalShellCalls(command: string): string[][] | null {
203
+ const parsed = parsedOf(command),
204
+ allowed = new Set([
205
+ "File",
206
+ "Stmt",
207
+ "CallExpr",
208
+ "Word",
209
+ "Lit",
210
+ "SglQuoted",
211
+ "DblQuoted",
212
+ "BinaryCmd",
213
+ ]),
214
+ safe = (node: unknown, quoted = false): boolean => {
215
+ if (Array.isArray(node)) {
216
+ return node.every((item: unknown) => safe(item, quoted));
217
+ }
218
+ if (!isNode(node)) {
219
+ return true;
220
+ }
221
+ if (node.Type !== undefined && !allowed.has(node.Type)) {
222
+ return false;
223
+ }
224
+ if (
225
+ node.Dollar === true ||
226
+ node.Background === true ||
227
+ (node.Redirs ?? []).length > 0 ||
228
+ (node.Assigns ?? []).length > 0 ||
229
+ (node.Type === "CallExpr" && (node.Args ?? []).length === 0) ||
230
+ (node.Type === "BinaryCmd" && !["&&", "||", "|"].includes(node.Op ?? "")) ||
231
+ (node.Type === "Lit" && !quoted && hasActiveGlob(node.Value ?? "")) ||
232
+ (node.Type === "Lit" && !quoted && (node.Value ?? "").startsWith("~"))
233
+ ) {
234
+ return false;
235
+ }
236
+ return Object.values(node).every((value: unknown) =>
237
+ safe(value, quoted || node.Type === "DblQuoted"),
238
+ );
239
+ };
240
+ return safe(parsed.tree) && parsed.calls.length > 0
241
+ ? parsed.calls.map((words) => [...words])
242
+ : null;
243
+ }
244
+
245
+ export { literalShellCalls, shellCallGuards, shellCalls };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oisin-ee/pi-prewalk",
3
- "version": "0.10.2",
3
+ "version": "0.11.1",
4
4
  "description": "A one-shot planner-to-executor handoff for pi, with branch-backed Rust rules.",
5
5
  "keywords": [
6
6
  "pi-extension",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oisin-ee/pi-tiers",
3
- "version": "0.10.2",
3
+ "version": "0.11.1",
4
4
  "description": "Sticky virtual model tiers with quota-aware recovery for pi.",
5
5
  "keywords": [
6
6
  "pi-extension",
@@ -64,6 +64,14 @@ Untargeted `done` and `drop` affect open work only. A parent without `task` is n
64
64
  Appending children to a closed parent reopens it as pending. An active parent stays active.
65
65
  Outcome and subtask progress remain separate. Abandoned work never counts as completed.
66
66
 
67
+ ## Activation gate
68
+
69
+ Shell, code, and mutation tools need one active local unit. Structured reads, workflow control, and `todo` calls stay available without one.
70
+ The gate checks the selected model at each call. The planner selections `tier/plan-claude` and `tier/plan-gpt` skip the activation check, so a planning session can read and inspect without a unit.
71
+ Every other selection needs an active unit, including a tier with another name and a physical model that only looks like a planner.
72
+ The exemption is a workflow prerequisite, not a permission. It does not grant write access, and other policies such as guardrails still apply.
73
+ An active unit stays required for the sessions that implement code.
74
+
67
75
  ## State
68
76
 
69
77
  Each task change is saved as a `todo-state` entry on the session branch. A resumed session, a `/tree` move,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oisin-ee/pi-todo",
3
- "version": "0.10.2",
3
+ "version": "0.11.1",
4
4
  "description": "A todo tool for pi with a Rust core: one active task, state on the session branch.",
5
5
  "keywords": [
6
6
  "pi-extension",
@@ -2,8 +2,17 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { TaskProvider } from "./provider.ts";
3
3
  import { ownsTodo } from "./lifecycle.ts";
4
4
 
5
- /** Exact structured reads and workflow controls, never command-text inference. */
6
- const controls = new Set([
5
+ /** Planning selections that skip the activation check, exact provider and model IDs. */
6
+ const planningSelections = new Set(["plan-claude", "plan-gpt"]),
7
+ /**
8
+ * Whether the selected model is a planning selection.
9
+ * @param {Readonly<object>} model Selected model, when the session has one.
10
+ * @returns {boolean} Whether the activation check does not apply.
11
+ */
12
+ isPlanning = (model: { readonly provider?: string; readonly id?: string } | undefined): boolean =>
13
+ model?.provider === "tier" && planningSelections.has(model.id ?? ""),
14
+ /** Exact structured reads and workflow controls, never command-text inference. */
15
+ controls = new Set([
7
16
  "ask",
8
17
  "codemode",
9
18
  "interview",
@@ -57,7 +66,7 @@ const controls = new Set([
57
66
  return { block: false };
58
67
  }
59
68
  provider.context(ctx);
60
- if (controls.has(event.toolName)) {
69
+ if (controls.has(event.toolName) || isPlanning(ctx.model)) {
61
70
  return { block: false };
62
71
  }
63
72
  const active = provider.capability().active();
@@ -9,6 +9,8 @@ const workUnitGuidance =
9
9
  "Use returned IDs after creation. Update drafts before start; only one complete self leaf executes locally. " +
10
10
  "The lead closes any open leaf with done and a short note, or with drop. " +
11
11
  "Keep content a short headline (aim under 60 characters, imperative, no trailing detail); " +
12
- "put context, approach, or acceptance detail in the optional description (one or two sentences).";
12
+ "put context, approach, or acceptance detail in the optional description (one or two sentences). " +
13
+ "A planning session selected as tier/plan-claude or tier/plan-gpt needs no active unit. " +
14
+ "Every other session needs one active local unit before shell, code, or mutation tools.";
13
15
 
14
16
  export { workUnitGuidance };
@@ -27,6 +27,12 @@ interface Startup {
27
27
  readonly cwd: string;
28
28
  readonly seedPath: string;
29
29
  }
30
+ /** One selectable model in the runtime catalog. */
31
+ interface Selection {
32
+ readonly id: string;
33
+ readonly kind?: "physical" | "virtual";
34
+ readonly provider: string;
35
+ }
30
36
  interface RuntimeFixture {
31
37
  readonly session: Readonly<Pick<Session, "dispose" | "abort" | "messages">> & {
32
38
  readonly extensionRunner: Readonly<Pick<Session["extensionRunner"], "emitToolResult" | "emit">>;
@@ -35,27 +41,32 @@ interface RuntimeFixture {
35
41
  readonly provider: () => TaskProviderV1;
36
42
  readonly call: (name: string, args: Readonly<Record<string, string>>) => Promise<void>;
37
43
  readonly create: () => Promise<void>;
44
+ readonly select: (provider: string, id: string) => Promise<void>;
38
45
  readonly todo: (params: TodoParams) => Promise<unknown>;
39
46
  }
47
+ /** Plain description of one runtime fixture. */
48
+ interface FixtureRequest {
49
+ readonly collision: boolean;
50
+ readonly later: ExtensionFactory | undefined;
51
+ readonly selections: readonly Selection[];
52
+ readonly startup: Startup | undefined;
53
+ }
40
54
 
41
55
  /**
42
56
  * 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.
57
+ * @param {Readonly<FixtureRequest>} request Fixture description.
46
58
  * @returns {object} Local resource owners.
47
59
  */
48
60
  const resources = (
49
- later: ExtensionFactory | undefined,
50
- collision: boolean,
51
- startup: Startup | undefined,
61
+ request: Readonly<FixtureRequest>,
52
62
  ): {
53
63
  events: ReturnType<typeof createEventBus>;
54
64
  manager: SessionManager;
55
65
  settings: SettingsManager;
56
66
  loader: DefaultResourceLoader;
57
67
  } => {
58
- const events = createEventBus(),
68
+ const { collision, later, startup } = request,
69
+ events = createEventBus(),
59
70
  manager = SessionManager.inMemory(),
60
71
  settings = SettingsManager.inMemory({
61
72
  compaction: { enabled: false },
@@ -130,47 +141,55 @@ const resources = (
130
141
  );
131
142
  },
132
143
  /**
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.
144
+ * Creates the model catalog with the requested physical and virtual entries.
145
+ * @param {Readonly<FixtureRequest>} request Fixture description.
146
+ * @returns {object} Scripted provider and model catalog owner.
137
147
  */
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({
148
+ catalog = async (
149
+ request: Readonly<FixtureRequest>,
150
+ ): Promise<{
151
+ faux: ReturnType<typeof fauxProvider>;
152
+ modelRuntime: ModelRuntime;
153
+ }> => {
154
+ const modelRuntime = await ModelRuntime.create({
164
155
  credentials: new InMemoryCredentialStore(),
165
156
  modelsPath: null,
166
157
  modelsStore: new InMemoryModelsStore(),
167
158
  refreshOnCreate: false,
168
159
  }),
169
- faux = fauxProvider({ tokensPerSecond: 0 });
160
+ faux = fauxProvider({
161
+ models: [
162
+ ...request.selections
163
+ .filter((selection) => selection.kind === "physical")
164
+ .map((selection) => ({ id: selection.id })),
165
+ { id: "faux-1" },
166
+ ],
167
+ tokensPerSecond: 0,
168
+ });
170
169
  modelRuntime.registerNativeProvider(faux.provider);
170
+ for (const selection of request.selections.filter((entry) => entry.kind !== "physical")) {
171
+ modelRuntime.registerVirtualModel({
172
+ id: selection.id,
173
+ name: selection.id,
174
+ provider: selection.provider,
175
+ route: () => ({ model: faux.getModel(), thinkingLevel: "off" }),
176
+ thinkingLevels: ["off"],
177
+ });
178
+ }
179
+ await modelRuntime.refresh({ allowNetwork: false });
180
+ return { faux, modelRuntime };
181
+ },
182
+ /**
183
+ * Builds the real session and its callers over prepared resources.
184
+ * @param {Readonly<FixtureRequest>} request Fixture description.
185
+ * @returns {Promise<RuntimeFixture>} Isolated runtime and callers.
186
+ */
187
+ buildFixture = async (request: Readonly<FixtureRequest>): Promise<RuntimeFixture> => {
188
+ const setup = resources(request),
189
+ { faux, modelRuntime } = await catalog(request);
171
190
  await setup.loader.reload();
172
191
  const { session } = await createAgentSession({
173
- cwd: startup?.cwd ?? "/tmp",
192
+ cwd: request.startup?.cwd ?? "/tmp",
174
193
  model: faux.getModel(),
175
194
  modelRuntime,
176
195
  resourceLoader: setup.loader,
@@ -181,7 +200,13 @@ const resources = (
181
200
  await session.bindExtensions({});
182
201
  session.setActiveToolsByName([...session.getActiveToolNames(), "codemode"]);
183
202
  return {
184
- call: caller(session, faux),
203
+ call: async (name, args): Promise<void> => {
204
+ faux.setResponses([
205
+ fauxAssistantMessage(fauxToolCall(name, args)),
206
+ fauxAssistantMessage("Done."),
207
+ ]);
208
+ await session.prompt("Exercise one tool.");
209
+ },
185
210
  create: async (): Promise<void> => {
186
211
  await todoCall(session, {
187
212
  items: [{ content: "Build", contract: contract(), key: "build" }],
@@ -191,10 +216,36 @@ const resources = (
191
216
  },
192
217
  manager: setup.manager,
193
218
  provider: () => acquire(setup.events),
219
+ select: async (provider, id): Promise<void> => {
220
+ const model = modelRuntime.getModel(provider, id);
221
+ if (model === undefined) {
222
+ throw new Error(`Unknown selection: ${provider}/${id}`);
223
+ }
224
+ await session.setModel(model);
225
+ },
194
226
  session,
195
227
  todo: async (params): Promise<unknown> => todoCall(session, params),
196
228
  };
197
229
  },
230
+ /**
231
+ * Loads the real runner and codemode with a scripted model.
232
+ * @param {ExtensionFactory} later Optional additional guard or test tool.
233
+ * @param {boolean} collision Whether the other todo owner loads first.
234
+ * @param {Startup | undefined} startup Optional worker startup configuration.
235
+ * @returns {Promise<RuntimeFixture>} Isolated runtime and callers.
236
+ */
237
+ runtimeFixture = async (
238
+ later?: ExtensionFactory,
239
+ collision = false,
240
+ startup?: Startup,
241
+ ): Promise<RuntimeFixture> => buildFixture({ collision, later, selections: [], startup }),
242
+ /**
243
+ * Loads the real runtime with selectable virtual and physical models.
244
+ * @param {readonly Selection[]} selections Selectable catalog entries.
245
+ * @returns {Promise<RuntimeFixture>} Runtime with the given selections.
246
+ */
247
+ selectableFixture = async (selections: readonly Selection[]): Promise<RuntimeFixture> =>
248
+ buildFixture({ collision: false, later: undefined, selections, startup: undefined }),
198
249
  /**
199
250
  * Produces a deterministic tool result.
200
251
  * @returns {object} Successful content.
@@ -205,5 +256,5 @@ const resources = (
205
256
  }),
206
257
  emptyParameters = Type.Object({});
207
258
 
208
- export { emptyParameters, emptyResult, runtimeFixture };
209
- export type { RuntimeFixture };
259
+ export { emptyParameters, emptyResult, runtimeFixture, selectableFixture };
260
+ export type { RuntimeFixture, Selection };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oisin-ee/oisinpi",
3
- "version": "0.10.2",
3
+ "version": "0.11.1",
4
4
  "description": "Oisin's pi packages in one install: prewalk, todo, tiers, and guardrails.",
5
5
  "keywords": [
6
6
  "guardrails",
@@ -17,10 +17,10 @@
17
17
  },
18
18
  "type": "module",
19
19
  "dependencies": {
20
- "@oisin-ee/pi-guardrails": "0.10.2",
21
- "@oisin-ee/pi-prewalk": "0.10.2",
22
- "@oisin-ee/pi-tiers": "0.10.2",
23
- "@oisin-ee/pi-todo": "0.10.2"
20
+ "@oisin-ee/pi-guardrails": "0.11.1",
21
+ "@oisin-ee/pi-prewalk": "0.11.1",
22
+ "@oisin-ee/pi-tiers": "0.11.1",
23
+ "@oisin-ee/pi-todo": "0.11.1"
24
24
  },
25
25
  "bundleDependencies": [
26
26
  "@oisin-ee/pi-guardrails",