@danypops/pi-papyrus 0.55.5 → 0.55.7
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
CHANGED
|
@@ -17,7 +17,7 @@ Agent-facing domain tools own lifecycle invariants and sit above this store API.
|
|
|
17
17
|
- **docs** (`docs_create`, `docs_list`, `docs_show`, `docs_activate`, `docs_archive`, `docs_reopen`, `docs_link`, `docs_assign_project`, `docs_update`) — activate/archive/reopen and document-safe graph links; Note mutations remain behind the Notes facade
|
|
18
18
|
- **rules** (`rules_create`, `rules_list`, `rules_show`, `rules_preview`, `rules_enable`, `rules_disable`, `rules_gate`, `rules_assign_project`, `rules_update`) — enable/disable and attach governance gates to tasks
|
|
19
19
|
- **playbooks** (`playbooks_create`, `playbooks_list`, `playbooks_show`, `playbooks_invoke`, `playbooks_preview`, `playbooks_enable`, `playbooks_disable`, `playbooks_assign_project`, `playbooks_update`, `playbooks_contain`, `playbooks_uncontain`, `playbooks_depend`, `playbooks_undepend`) — a trigger and an ordered list of steps. Each step is a plain prose string (a task), or a structured object: `{kind:'doc',title,body?,subtype?,labels?}` creates a Doc, `{kind:'rule',title,body?,condition?,action?,severity?,labels?}` creates a Rule, `{kind:'call',title,playbookId,arguments?}` nests another Playbook's own run as a pipeline step gated in the same sequence, `{kind:'task',title?,body}` is an explicit task step. `playbooks_invoke` recycles the shared blueprint materialization engine: it compiles the steps and any `contain`/`depend` composition into real artifacts (a Task per plain/task step, a Doc/Rule per doc/rule step, a nested run per call step), wires task-like steps with `dependsOn` so completing one auto-focuses the next, and focuses the first real task. No text dump — one step surfaces at a time, as it becomes the focused task, same as any other Task. `playbooks_contain`/`playbooks_uncontain` nest a child Playbook inside a parent (its steps run after the parent's own, as part of it); `playbooks_depend`/`playbooks_undepend` chain a prerequisite Playbook before another (it must fully complete first) -- both are whole-Playbook composition, distinct from a `call` step's finer-grained, single-step nesting. `playbooks_preview` renders the whole tree as text with no side effects, for reading before invoking. A Playbook can declare named arguments (`{name, description?, required?, type?('string'|'number'|'boolean', default 'string'), enum?, default?}`, required defaults true; referenced in step text/call arguments as `{{name}}`); invoking with a required one unsupplied creates nothing and reports exactly which are still missing, directing the agent to ask via `discuss_open`/`discuss_reply` with `live:true` rather than guess
|
|
20
|
-
- **discuss** (`discuss_open`, `discuss_reply`, `discuss_defer`, `discuss_resume`, `discuss_settle`, `discuss_block`, `discuss_unblock`, `discuss_show`, `discuss_rounds`, `discuss_list`) — a Discussion persists across multiple rounds and can genuinely block a Task's completion until settled or deferred (`discuss_block`/`discuss_unblock`). `discuss_reply` is refused once deferred or settled -- `discuss_resume` first. `discuss_open`/`discuss_reply` can pose a structured choice via `options` (2-10 entries, each a bare string or `{title, description}`) + `options_mode` (`single`/`multi`); a later `discuss_reply` answers it via `selected`, validated against it. `live: true` on either gets the human's answer synchronously in the same tool call -- the operation itself always durably records the round first (exactly like every other Vehicle operation), then an optional local prompt (the pending choice's picker if one was posed, otherwise a freeform question) runs via a per-operation `interactiveFollowUps` resolver (see `@danypops/vehicle-client-pi`), degrading silently to the plain async round when there's no interactive UI
|
|
20
|
+
- **discuss** (`discuss_open`, `discuss_reply`, `discuss_defer`, `discuss_resume`, `discuss_settle`, `discuss_block`, `discuss_unblock`, `discuss_show`, `discuss_rounds`, `discuss_list`) — a Discussion persists across multiple rounds and can genuinely block a Task's completion until settled or deferred (`discuss_block`/`discuss_unblock`). `discuss_reply` is refused once deferred or settled -- `discuss_resume` first. `discuss_open`/`discuss_reply` can pose a structured choice via `options` (2-10 entries, each a bare string or `{title, description}`) + `options_mode` (`single`/`multi`); a later `discuss_reply` answers it via `selected`, validated against it. Adding `correct_options` + `explanation` turns that same choice into a graded quiz/knowledge assessment -- see "Quiz assessments" below. `live: true` on either gets the human's answer synchronously in the same tool call -- the operation itself always durably records the round first (exactly like every other Vehicle operation), then an optional local prompt (the pending choice's picker if one was posed, otherwise a freeform question) runs via a per-operation `interactiveFollowUps` resolver (see `@danypops/vehicle-client-pi`), degrading silently to the plain async round when there's no interactive UI
|
|
21
21
|
|
|
22
22
|
Every tool operation is registered in the daemon's `/api/v1/ops` registry; parity is verified in tests. The task consumer uses the `tasks.graph` operation, which returns task nodes with explicit parent, child, and dependency IDs rather than leaking SQLite rows or asking the UI to reconstruct relationships.
|
|
23
23
|
|
|
@@ -61,7 +61,7 @@ Blocking is real: `tasks.complete` is refused while any `active` Discussion has
|
|
|
61
61
|
|
|
62
62
|
`open`/`reply` can also pose a structured choice instead of (or alongside) free text: `options` (2-10 entries) plus `options_mode` -- `single` is mutually exclusive (exactly one pick), `multi` allows several. The Discussion remembers the pending choice (`extra.discussion.pendingOptions`/`pendingOptionsMode`) until a `reply` answers it with `selected`, validated against exactly what was offered and the mode's cardinality; a reply can also pose the *next* round's choice in the same call.
|
|
63
63
|
|
|
64
|
-
Run `/discuss` for the interactive panel: browse every Discussion (the real `active`/`deferred`/`settled` state shown per row, alongside any choice awaiting an answer), open a scrollable transcript showing what was posed and picked in each round, and reply/defer/resume/settle or block/unblock a task without leaving the TUI. Replying to a pending choice shows a real picker -- the native single-select list for `single`, or a checkbox multi-select for `multi`, since no built-in multi-select exists in the Pi extension UI. Both modes append a numbered "type your own answer" row -- a genuinely open answer is exactly as valid as any posed option. The multi-select picker supports a number key as a direct quick-select (jump straight to that row instead of scrolling), and steadily highlights checked rows while dimming the rest so the eye reads "what's chosen" independent of cursor position; its cursor row blinks to mark focus. It also auto-cancels after 30s of zero input -- the very first keystroke of any kind stops that countdown permanently for that prompt. Opening a *new* Discussion is left to the agent (same as Docs/Rules/Playbooks) -- `/discuss` browses and drives existing ones.
|
|
64
|
+
Run `/discuss` for the interactive panel: browse every Discussion (the real `active`/`deferred`/`settled` state shown per row, alongside any choice awaiting an answer), open a scrollable transcript showing what was posed and picked in each round, and reply/defer/resume/settle or block/unblock a task without leaving the TUI. Replying to a pending choice shows a real picker -- the native single-select list for `single`, or a checkbox multi-select for `multi`, since no built-in multi-select exists in the Pi extension UI. Both modes append a numbered "type your own answer" row -- a genuinely open answer is exactly as valid as any posed option. The multi-select picker supports a number key as a direct quick-select (jump straight to that row instead of scrolling), and steadily highlights checked rows while dimming the rest so the eye reads "what's chosen" independent of cursor position; its cursor row blinks to mark focus. It also auto-cancels after 30s of zero input -- the very first keystroke of any kind stops that countdown permanently for that prompt. A quiz's options display lettered (A, B, C, ...) in this same picker. Opening a *new* Discussion is left to the agent (same as Docs/Rules/Playbooks) -- `/discuss` browses and drives existing ones.
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
67
|
papyrus discuss open --title "Naming" --actor alice --content "Should we rename this?" --blocks-json '["task-id"]' --json
|
|
@@ -74,6 +74,34 @@ papyrus discuss settle <discussion-id> --settlement "Agreed: renaming to X" --js
|
|
|
74
74
|
papyrus discuss show <discussion-id> --json
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
### Quiz assessments
|
|
78
|
+
|
|
79
|
+
A posed choice becomes a graded knowledge assessment by adding `correct_options` (one or more entries drawn verbatim from `options` -- exact text, never an index or a display letter) + `explanation` (**required**, always shown after grading -- especially when the answer is wrong) to `discuss_open`/`discuss_reply`. Options are dynamic (any count from 2 up to the enforced 10-option ceiling, not hard-coded to four) and display lettered A, B, C, ... in every surface (the live picker, the `/discuss` transcript, the tool-call card) -- single letters always suffice since the 10-option ceiling sits well under the 26-letter limit.
|
|
80
|
+
|
|
81
|
+
- A `"single"` quiz (the participant can only pick one) must have exactly one correct option; a `"multi"` quiz may have several, graded correct iff the reply's `selected` set exactly matches the correct set -- no partial credit.
|
|
82
|
+
- The correct answer is never exposed before submission: `discuss_open`'s own response, and every `discuss_show`/`discuss_rounds` read of an unanswered quiz, carries only a `quiz: true` marker on that round -- never the answer. Server-side, it's held in dedicated hidden storage that Discuss's own general-purpose read queries structurally cannot select, not merely omitted at the API layer.
|
|
83
|
+
- Once answered, the round that carries `selected` also carries `quizResult: { correct, correctOptions, explanation }` -- the durable, permanent record of what was asked, what was picked, and whether it was right, so a later turn can adapt to demonstrated knowledge.
|
|
84
|
+
- Malformed quizzes are rejected: too few options (below the existing 2-option floor), a duplicate/unknown `correct_options` entry, more than one correct option under `"single"` mode, or `correct_options`/`explanation` given without the other.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# A four-option quiz with one correct answer.
|
|
88
|
+
papyrus discuss open --title "Geography check" --actor agent \
|
|
89
|
+
--content "What is the capital of France?" \
|
|
90
|
+
--options-json '["Paris","London","Berlin","Madrid"]' --options-mode single \
|
|
91
|
+
--correct-options-json '["Paris"]' \
|
|
92
|
+
--explanation "Paris has been the capital of France since 987 AD." --json
|
|
93
|
+
|
|
94
|
+
# A "select all that apply" quiz with more than one correct option.
|
|
95
|
+
papyrus discuss open --title "Primes check" --actor agent \
|
|
96
|
+
--content "Which of these are prime numbers?" \
|
|
97
|
+
--options-json '["2","3","4","9"]' --options-mode multi \
|
|
98
|
+
--correct-options-json '["2","3"]' \
|
|
99
|
+
--explanation "2 and 3 are prime; 4 and 9 are not." --json
|
|
100
|
+
|
|
101
|
+
# The participant answers like any other posed choice -- grading and the explanation come back in the same call.
|
|
102
|
+
papyrus discuss reply <discussion-id> --actor human --content "Paris" --selected-json '["Paris"]' --json
|
|
103
|
+
```
|
|
104
|
+
|
|
77
105
|
## Tasks
|
|
78
106
|
|
|
79
107
|
Run `/tasks` for the interactive task panel:
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* human-authored inbox.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { type Artifact, type DiscussionAndRounds, readDiscussionExtra } from "@danypops/papyrus";
|
|
18
|
+
import { type Artifact, type DiscussionAndRounds, quizOptionLabel, readDiscussionExtra } from "@danypops/papyrus";
|
|
19
19
|
import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { showArtifactBrowser } from "../artifact/artifact-browser.ts";
|
|
21
21
|
import { DISCUSSION_STATE_PRESENTATION, DOC_STATUS_PRESENTATION } from "../artifact/artifact-status-presentation.ts";
|
|
@@ -111,27 +111,37 @@ export async function showDiscussions(ctx: ExtensionCommandContext): Promise<voi
|
|
|
111
111
|
const transcript = await callService<Record<string, unknown>, DiscussionAndRounds>("discuss.show", { id: discussion.id });
|
|
112
112
|
const question = transcript.rounds.at(-1)?.content?.trim() || `Reply to "${discussion.title}":`;
|
|
113
113
|
const subtitle = discussion.title;
|
|
114
|
+
// A quiz's options display lettered (A, B, C, ...) in the picker -- the correct answer itself
|
|
115
|
+
// never reaches this client before submission (see domain/discussion.ts's own comment on why).
|
|
114
116
|
const answer =
|
|
115
117
|
pending?.pendingOptions && pending.pendingOptions.length > 0 && pending.pendingOptionsMode
|
|
116
118
|
? await askQuestion(commandCtx, {
|
|
117
119
|
question,
|
|
118
120
|
subtitle,
|
|
119
121
|
options: pending.pendingOptions.map((title, index) => ({
|
|
120
|
-
title,
|
|
122
|
+
title: pending.pendingIsQuiz ? `${quizOptionLabel(index)}. ${title}` : title,
|
|
121
123
|
description: pending.pendingOptionDescriptions?.[index] || undefined,
|
|
122
124
|
})),
|
|
123
125
|
allowMultiple: pending.pendingOptionsMode === "multi",
|
|
124
126
|
})
|
|
125
127
|
: await askQuestion(commandCtx, { question, subtitle });
|
|
126
128
|
if (!answer) return; // canceled
|
|
127
|
-
await callService("discuss.reply", {
|
|
129
|
+
const replied = await callService<Record<string, unknown>, DiscussionAndRounds>("discuss.reply", {
|
|
128
130
|
id: discussion.id,
|
|
129
131
|
actor: ACTOR,
|
|
130
132
|
content: answer.content,
|
|
131
133
|
...(answer.selected ? { selected: answer.selected } : {}),
|
|
132
134
|
source: SOURCE,
|
|
133
135
|
});
|
|
134
|
-
|
|
136
|
+
const quizResult = replied.rounds[0]?.quizResult;
|
|
137
|
+
const message = quizResult
|
|
138
|
+
? quizResult.correct
|
|
139
|
+
? `✅ Correct! ${quizResult.explanation}`
|
|
140
|
+
: `❌ Incorrect -- correct answer(s): ${quizResult.correctOptions.join(", ")}. ${quizResult.explanation}`
|
|
141
|
+
: answer.selected
|
|
142
|
+
? `Selected: ${answer.selected.join(", ")}`
|
|
143
|
+
: "Reply added.";
|
|
144
|
+
commandCtx.ui.notify(message, "info");
|
|
135
145
|
return;
|
|
136
146
|
}
|
|
137
147
|
if (choice === "Defer") {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
ARTIFACT_DETAIL_RESERVED_ROWS,
|
|
16
16
|
type Artifact,
|
|
17
17
|
type DiscussionRound,
|
|
18
|
+
quizOptionLabel,
|
|
18
19
|
readDiscussionExtra,
|
|
19
20
|
} from "@danypops/papyrus";
|
|
20
21
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -126,17 +127,48 @@ class DiscussionTranscriptViewport {
|
|
|
126
127
|
const roundHeader =
|
|
127
128
|
theme.fg("accent", `[round ${round.roundNumber}] `) + theme.bold(round.actor) + theme.fg("dim", ` · ${round.occurredAt}`);
|
|
128
129
|
const body = renderMarkdownBody(round.content, width - 2, this.activeTheme).map((line) => ({ text: ` ${line}` }));
|
|
130
|
+
// Quiz options get lettered (A, B, C, ...) and a distinct "Quiz" label; a plain, non-quiz posed
|
|
131
|
+
// choice keeps its existing comma-joined "Posed" presentation unchanged (backward compatible).
|
|
129
132
|
const posed =
|
|
130
133
|
round.options && round.options.length > 0
|
|
131
134
|
? [
|
|
132
135
|
{
|
|
133
|
-
text: ` ${theme.fg(
|
|
136
|
+
text: ` ${theme.fg(
|
|
137
|
+
"muted",
|
|
138
|
+
round.quiz
|
|
139
|
+
? `Quiz (${round.optionsMode === "multi" ? "pick all that apply" : "pick one"}): ${round.options
|
|
140
|
+
.map((option, optionIndex) => `${quizOptionLabel(optionIndex)}. ${option}`)
|
|
141
|
+
.join(" ")}`
|
|
142
|
+
: `Posed (${round.optionsMode === "multi" ? "pick several" : "pick one"}): ${round.options.join(", ")}`,
|
|
143
|
+
)}`,
|
|
134
144
|
},
|
|
135
145
|
]
|
|
136
146
|
: [];
|
|
137
147
|
const picked =
|
|
138
148
|
round.selected && round.selected.length > 0 ? [{ text: ` ${theme.fg("success", `Selected: ${round.selected.join(", ")}`)}` }] : [];
|
|
139
|
-
|
|
149
|
+
// The graded verdict -- always includes the explanation, especially when wrong. Only ever
|
|
150
|
+
// present on the round that actually answered a pending quiz (see domain/discussion.ts).
|
|
151
|
+
const quizVerdict = round.quizResult
|
|
152
|
+
? [
|
|
153
|
+
{
|
|
154
|
+
text: ` ${theme.fg(
|
|
155
|
+
round.quizResult.correct ? "success" : "error",
|
|
156
|
+
round.quizResult.correct
|
|
157
|
+
? "✅ Correct!"
|
|
158
|
+
: `❌ Incorrect -- correct answer(s): ${round.quizResult.correctOptions.join(", ")}.`,
|
|
159
|
+
)}`,
|
|
160
|
+
},
|
|
161
|
+
{ text: ` ${theme.fg("muted", round.quizResult.explanation)}` },
|
|
162
|
+
]
|
|
163
|
+
: [];
|
|
164
|
+
return [
|
|
165
|
+
{ text: roundHeader },
|
|
166
|
+
...body,
|
|
167
|
+
...posed,
|
|
168
|
+
...picked,
|
|
169
|
+
...quizVerdict,
|
|
170
|
+
...(index < this.rounds.length - 1 ? [{ text: "" }] : []),
|
|
171
|
+
];
|
|
140
172
|
});
|
|
141
173
|
this.lines = [...header, ...(transcript.length > 0 ? transcript : [{ text: theme.fg("muted", "No rounds recorded.") }])];
|
|
142
174
|
this.offsetY = Math.min(this.offsetY, Math.max(0, this.lines.length - this.visibleLines));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Artifact, type DiscussionQuizResult, quizOptionLabel } from "@danypops/papyrus";
|
|
2
2
|
import { expandHint } from "@danypops/vehicle-client-pi/expand-hint";
|
|
3
3
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { type Component, truncateToWidth } from "@earendil-works/pi-tui";
|
|
@@ -7,11 +7,16 @@ import { detailViewTheme, measure, statusColor, statusGlyph } from "../../tool-r
|
|
|
7
7
|
import { isArtifact, isArtifactArray, type RenderableDiscussionParent } from "./shared.ts";
|
|
8
8
|
|
|
9
9
|
/** A Discussion round -- discuss.open/reply/show/rounds' own transcript entry. Detected the
|
|
10
|
-
* same name-independent, shape-based way as the others in this directory.
|
|
10
|
+
* same name-independent, shape-based way as the others in this directory. quiz/quizResult are
|
|
11
|
+
* the two quiz-safe fields (see domain/discussion.ts) -- optional, so a plain non-quiz round
|
|
12
|
+
* renders exactly as before. */
|
|
11
13
|
export interface DiscussionRoundOutput {
|
|
12
14
|
roundNumber: number;
|
|
13
15
|
actor: string;
|
|
14
16
|
content: string;
|
|
17
|
+
options?: string[];
|
|
18
|
+
quiz?: boolean;
|
|
19
|
+
quizResult?: DiscussionQuizResult;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
export function isDiscussionRound(value: unknown): value is DiscussionRoundOutput {
|
|
@@ -55,10 +60,27 @@ export function isDiscussionListOutput(value: unknown): value is DiscussionListO
|
|
|
55
60
|
return isArtifactArray(row.discussions);
|
|
56
61
|
}
|
|
57
62
|
|
|
63
|
+
/** Appended to a quiz round's own body text -- lettered options when posed, the graded verdict +
|
|
64
|
+
* explanation (always shown, especially when wrong) once answered. A plain, non-quiz round's
|
|
65
|
+
* body is untouched (backward compatible). */
|
|
66
|
+
function quizBodySuffix(round: DiscussionRoundOutput): string {
|
|
67
|
+
const posed =
|
|
68
|
+
round.quiz && round.options && round.options.length > 0
|
|
69
|
+
? `\n${round.options.map((option, index) => `${quizOptionLabel(index)}. ${option}`).join(" ")}`
|
|
70
|
+
: "";
|
|
71
|
+
const verdict = round.quizResult
|
|
72
|
+
? `\n${round.quizResult.correct ? "✅ Correct!" : `❌ Incorrect -- correct answer(s): ${round.quizResult.correctOptions.join(", ")}.`} ${round.quizResult.explanation}`
|
|
73
|
+
: "";
|
|
74
|
+
return `${posed}${verdict}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
58
77
|
export function roundsSection(rounds: readonly DiscussionRoundOutput[]): DetailSection {
|
|
59
78
|
return {
|
|
60
79
|
heading: `Rounds (${rounds.length}):`,
|
|
61
|
-
items: rounds.map((round) => ({
|
|
80
|
+
items: rounds.map((round) => ({
|
|
81
|
+
byline: `${round.actor} · round ${round.roundNumber}`,
|
|
82
|
+
body: `${round.content}${quizBodySuffix(round)}`,
|
|
83
|
+
})),
|
|
62
84
|
};
|
|
63
85
|
}
|
|
64
86
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-papyrus",
|
|
3
|
-
"version": "0.55.
|
|
3
|
+
"version": "0.55.7",
|
|
4
4
|
"description": "Pi host extension for Papyrus: native tools, TUI panels, and context injection over the daemon-backed graph store",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": ["pi-package"],
|
|
@@ -12,23 +12,23 @@
|
|
|
12
12
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"@danypops/vehicle-client-pi": "^0.42.
|
|
15
|
+
"@danypops/vehicle-client-pi": "^0.42.1",
|
|
16
16
|
"@earendil-works/pi-coding-agent": "*",
|
|
17
17
|
"@earendil-works/pi-tui": "*",
|
|
18
18
|
"typebox": "*"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@danypops/jittor": "^0.
|
|
22
|
-
"@danypops/papyrus": "^0.
|
|
23
|
-
"@danypops/vehicle-client": "^0.10.
|
|
24
|
-
"@danypops/vehicle-core": "^0.
|
|
25
|
-
"@danypops/vehicle-server": "^0.
|
|
21
|
+
"@danypops/jittor": "^0.19.1",
|
|
22
|
+
"@danypops/papyrus": "^0.59.0",
|
|
23
|
+
"@danypops/vehicle-client": "^0.10.1",
|
|
24
|
+
"@danypops/vehicle-core": "^0.17.1",
|
|
25
|
+
"@danypops/vehicle-server": "^0.24.5",
|
|
26
26
|
"beautiful-mermaid": "1.1.3",
|
|
27
27
|
"malevich-tui-components": "^0.24.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@danypops/pi-tui-harness": "^0.0.2",
|
|
31
|
-
"@danypops/vehicle-client-pi": "^0.42.
|
|
31
|
+
"@danypops/vehicle-client-pi": "^0.42.1",
|
|
32
32
|
"@danypops/vehicle-conformance": "^0.3.0",
|
|
33
33
|
"@earendil-works/pi-coding-agent": "^0.80.10",
|
|
34
34
|
"bun-types": "latest",
|