Skip to content

Releases: laurentftech/pi-outpost

v0.16.4

Choose a tag to compare

@laurentftech laurentftech released this 27 Aug 14:14
3ee68fa

What's Changed

Full Changelog: v0.16.3...v0.16.4

v0.16.3

Choose a tag to compare

@laurentftech laurentftech released this 26 Aug 11:05
9674c2c

What's Changed

Full Changelog: v0.16.2...v0.16.3

v0.16.2

Choose a tag to compare

@laurentftech laurentftech released this 26 Aug 09:07
a7cf0ed

What's Changed

Full Changelog: v0.16.1...v0.16.2

v0.16.1

Choose a tag to compare

@github-actions github-actions released this 25 Aug 22:49
231a42d

What's Changed

  • test(agent): make the mention replay assert what a real child persists by @laurentftech in #106
  • fix(ui): diff lines by their content, not by the terminator that ended them by @laurentftech in #105
  • test(pdf): wait for the observer before pretending the page scrolled by @laurentftech in #107

Full Changelog: v0.16.0...v0.16.1

v0.16.0

Choose a tag to compare

@github-actions github-actions released this 25 Aug 22:00
775e231

An @-mentioned path arrives unambiguous

The composer sends @ui/src/App.tsx — a path relative to the browser root, which is
also the agent's own sandbox root. That should be enough. But there is no structured
wire field for a mention: the model reads it as plain text and resolves it itself, and a
bash call earlier in the turn can leave it assuming a different working directory. The
agent mis-resolved an @-mentioned path roughly half the time.

Every mention that both resolves under the browser root and exists on disk is now
rewritten to its absolute form before the prompt reaches the runtime. A mention that is
not a real path — @someone in prose, a typo — is left exactly as it was typed;
resolving alone was not enough to decide that, because it deliberately tolerates a
missing tail, and a typo would have become a confident absolute path to nothing.

You never see it. The bubble carries the text you typed while the turn runs, and the
same substitution is reversed when a reconnect replays the conversation — the SDK
persists what reached the model, so without that reversal a page reload would have
leaked the absolute path into the bubble that had just shown the relative one.

A refused Work Plan call says which field it refused

v0.15.0 gave every work_plan operation its own typed schema. That fixed what the model
could read. It did not fix what the model is told when it gets something wrong, and a
ten-branch union is exactly where that goes bad: the whole schema is validated, so every
branch that fails contributes its complaint.

Asked for a five-task plan with dependencies, a small model wrote dependsOn on its
creation tasks — the natural way to express a plan that has dependencies — and got back:

Validation failed for tool "work_plan":
  - root: must not have additional properties
  - action: must be equal to constant
  - root: must not have additional properties
  - action: must be equal to constant
  - tasks.2: must not have additional properties
  - plan: must have required properties plan
  - root: must not have additional properties

Six of those seven lines describe actions the model never asked for. The word
dependsOn appears nowhere. That model repaired by guessing, at the cost of three extra
calls; a smaller one gave up on the plan entirely.

The tool now publishes one object schema whose action is an enum, and each
operation's requirements are checked where they can be named:

tasks[2].dependsOn is not accepted
unknown dependency: task_1
action=remove_task requires taskId

And creation accepts dependencies, where a model writes them:

{ "action": "create", "title": "Ship dark mode",
  "tasks": [{ "id": "a", "title": "Add the theme tokens" },
            { "id": "b", "title": "Wire the toggle", "dependsOn": ["a"] }] }

A dependency may name a task declared further down — a plan is written in the order the
work reads, not in dependency order. update_task also accepts its changed fields
beside taskId rather than only inside changes. The tool ships a worked example in
its guidelines, which it never had.

Persisted plans are unchanged, and every operation that worked before works the same.

The bubble tells the truth about what was sent, and what failed

Two things a reader was being told wrongly.

A prompt showed nothing until the server accepted it. The user bubble is broadcast
only once the runtime takes the prompt — deliberately, so a refused prompt leaves no
ghost behind — and acceptance waits for session creation, runtime start-up, and, on a
loaded provider, the request being taken. At the start of a conversation that was
seconds in which the composer had emptied and the transcript had not moved: long enough
to wonder whether the message was lost. The prompt now appears the moment it is sent,
dimmed, and gives way to the real bubble when it arrives. A refusal takes it away with
it.

A provider failure showed its markup. Providers do not fail in JSON — a proxy in
front of one answers with a whole HTML document, and that document was reaching the
bubble intact:

504 <html><body><h1>504 Gateway Time-out</h1> The server didn't respond in time. </body></html>

The words are recovered and the markup dropped, where the text enters the protocol
rather than where it is drawn — so a reopened session reads the same sentence the turn
showed. A message somebody wrote to be read is never reworded.

update runs the npm this platform can execute

On Windows, npm is npm.cmd, a batch file that CreateProcess cannot execute. Every
one of the three places that reach for npm — the registry probe, the global-root probe,
and the installer itself — failed with ENOENT before npm ever started.

It landed on exactly the deployment this code exists to serve: an air-gapped site whose
Nexus address lives only in its .npmrc, the file npm alone reads. The probe's failure
was swallowed, the public registry was used instead, and the operator was told:

[pi] could not check for updates: getaddrinfo ENOTFOUND registry.npmjs.org

Had the check passed, the install would have failed too, reported as the installer exited with 1 — npm blamed for a process that never ran.

Nothing needs to be configured on the affected host now: npm config get registry
answers with the internal address, and npm install -g reads the same .npmrc by
itself. A check that does fall back to the public registry now says why: getaddrinfo ENOTFOUND registry.npmjs.org (asked the public registry: spawn npm ENOENT).


Upgrading: nothing to change. Persisted Work Plans, sessions and configuration load
unchanged. An operator on Windows behind an internal registry should upgrade with
npm install -g pi-outpost this once — the copy they are running is the one that cannot
fetch. Running pi-outpost from npm needs Node ≥ 24; the executables attached below
require Node ≥ 26 to build.

v0.15.2

Choose a tag to compare

@github-actions github-actions released this 24 Aug 22:36
c16dcfb

What's Changed

  • fix(ui): keep the open file preview mounted during a background re-read by @laurentftech in #99
  • chore(deps): bump mermaid, pi-coding-agent, typebox, katex, typescript by @laurentftech in #100

Full Changelog: v0.15.1...v0.15.2

v0.15.1

Choose a tag to compare

@laurentftech laurentftech released this 23 Aug 20:51
v0.15.1
e3659cd

update works on a global install

pi-outpost update, on a copy installed with npm install -g pi-outpost, answered:

[pi] cannot tell how this copy was installed, so nothing was changed
[pi]   entry: /Users/you/.npm-global/bin/pi-outpost

— and changed nothing. On the ordinary global install, which is the one case the command
exists to automate, and on every machine: this was never specific to a custom npm prefix.

npm puts the command at <prefix>/bin/<name>, a symlink into
<prefix>/lib/node_modules, and that link is the path process.argv[1] carries. It has
no node_modules segment in it, so the global root was never even looked up and
detection fell through to "unknown" — which refuses and prints its evidence, exactly as
it is supposed to when it cannot tell.

The entry is now resolved through its symlinks before it is classified, and the refusal
prints the target as well as the path when the two differ.

The rule itself did not change, and the reasons it is strict have not gone away: a
project's own ./node_modules/.bin/pi-outpost still refuses rather than running
npm install -g against a different copy, an npx cache is still ephemeral rather than
global, and an entry that cannot be resolved classifies exactly as it did before.

Verified against a real npm install -g into an isolated prefix, not only in tests: a
0.14.1 copy upgraded itself to 0.15.0, and a copy already at the newest version now says
nothing to install instead of refusing.


Upgrading: this is the release that fixes upgrading, so a copy at 0.15.0 or earlier
cannot pull it in with pi-outpost update — run npm install -g pi-outpost once, and
update works from there. Nothing else changed: @pi-outpost/embed is untouched and
stays at 0.15.0.

v0.15.0

Choose a tag to compare

@laurentftech laurentftech released this 23 Aug 20:28
v0.15.0
4e37c13

A Work Plan the model can actually write

The work_plan tool shipped in v0.14.0 with three of its inputs — plan, task,
changes — declared as bare objects. A JSON Schema object with no properties says
nothing: not which fields are required, not which statuses are accepted, not how a
hierarchy is encoded. The model had to learn the contract from rejections. In a real
session, Mistral Medium 3.5 needed four refused attempts before one landed.

Every operation now publishes its own complete schema. Ten typed branches, one per
action, each declaring exactly the fields that action takes — bounded lengths,
enumerated statuses, and the null that clears an optional value, which was previously
undiscoverable.

And creation no longer asks for storage mechanics:

{ "action": "create", "title": "Port to multi-user",
  "tasks": [{ "title": "Authentication", "subtasks": [{ "title": "Sessions" }] }] }

A title, and tasks that need nothing but a title of their own. The server flattens the
tree, assigns parent relationships, sets version 1, the timestamp, the default todo
statuses and the empty dependency and resource collections, then returns the complete
normalized plan so the next call needs no lookup. Two levels, because the ceiling has to
be visible in the schema before the model composes something invalid; deeper
decomposition belongs in a description or a later refinement.

Nothing that worked before stopped working. replace still takes a full normalized
version-1 document, add_task still takes a fully specified task, and persisted plans
are unchanged.

The model names its own tasks

The first browser-driven run against a real server failed every single first call, in
two independent sessions, for a reason no offline probe had produced: the model puts an
id on each task, and the schema refused it.

The schema is a ten-branch root anyOf sent with strict: false — captured from the
outgoing provider request, so it does arrive intact, and nothing constrains generation to
one branch. id is required by the neighbouring add_task and replace task shapes,
so the model merges them into create.

Then the refusal cannot guide the repair. A failed union reports must not have additional properties once per branch and never names the property. One session deleted
its description fields — which were valid — then its entire second level, and still
failed. Another gave up on hierarchy and persisted six flat tasks.

So create adopts the identifier the model chose, rejects a collection whose identifiers
collide, and generates one for every task that omits it. Identity is unique either way,
the repair round disappears, and the agent can address its own tasks straight away: in
the verification run it marked audit_securite done without an intervening read.

Guidance with one owner

Whether to keep a Work Plan at all is now stated once, in a product-owned system-prompt
fragment, and only when the tool is actually available to the session. The tool
description keeps mechanical calling guidance and nothing else. Embedded and RPC runtimes
compose that prompt through one shared path, so the two cannot drift apart — and every
operator appendSystemPrompt entry is passed through byte for byte, after the
product-owned blocks.

What this does not buy

The compact create call is 64% smaller than the equivalent replace payload, but its
schema branch costs ~2 KB on every request while the saving lands once. Under eager
tool-schema delivery there is no multi-turn break-even. It is here because it produces
valid first calls, not because it saves context.

Under the hood

The continuous-write file-watcher test failed once on Windows and passed on the next run:
it wrote for a fixed 200 ms and assumed the platform would deliver the first event early
enough for two coalescing windows to close inside that stretch. It now writes until the
second announcement arrives, so first-event latency is no longer part of the assertion.

The window it uses also widened, for a worse reason: substituting a resetting debounce —
the exact regression that test exists to catch — left it passing on macOS, because the
platform's own gaps between delivered events were wider than the 25 ms window, so the
resetting timer fired anyway. At 250 ms the substitution fails and the real
implementation passes in half a second.


Upgrading: nothing to change. Existing Work Plans load unchanged, and replace and
add_task accept exactly what they accepted in v0.14.0. Running pi-outpost from npm
needs Node ≥ 24; the executables attached below require Node ≥ 26 to build.

v0.14.0

Choose a tag to compare

@github-actions github-actions released this 23 Aug 17:06
v0.14.0
e363407

The agent keeps a Work Plan

Pi Outpost preserved the conversation and the workspace, and neither said what the agent
was actually trying to do. Interrupt long work and the operational context was gone:
what was decided, what is done, what is blocked and why, all of it recoverable only by
reading back through a transcript.

There is now a Work Plan — one per session, owned by the agent, persisted beside the
session file.

It is a hierarchy of tasks, each with a stable identifier, a human-readable title, a
status (todo, in_progress, done, blocked, needs_review), and optional
description, parent, dependencies, resource references and status reason. The agent
mutates it through a structured interface, one atomic operation at a time. Nothing is
inferred from tool activity: an agent that ran forty commands and completed nothing has
completed nothing, and the plan says so.

This is meant as working state, not a progress bar. It is there to make decomposition
explicit, to survive an interruption, and to be reconciled before the agent claims the
work is finished.

It outlives the things that used to erase it. Reopening a session restores its plan.
Forking a conversation seeds the fork with a copy that then moves on its own — two
branches of the same work do not share one plan. And persistence is deliberately
independent of conversation compaction: when the transcript is summarised, the complete
current plan is still there, in full, directly readable by the agent rather than
reconstructed from a summary of itself.

It is visible. A panel beside the conversation shows the hierarchy, each task's
state, the current focus and aggregate progress, live over the same WebSocket protocol
as everything else. Selecting a task reveals its description, children, dependencies,
reason and resources. Resources are links: selecting one navigates to it with the UI
that already exists.

Work Plans stay distinct from conversation activity and from Structured Exchange. A task
may reference those artifacts; it never duplicates them. This first slice is read-only
for the human — steering the plan by hand is a compatible extension, not a missing half.

Hardening

  • A session path that changes spelling no longer loses the plan. macOS reaches the
    same session file through /var and /private/var; comparing the two as strings said
    they were different sessions. They are compared through their real paths now, with the
    lexical comparison kept as the fallback for a session that has just been deleted.
  • The release workflow distinguishes "nothing to publish" from "nothing to do." A tag
    whose packages are already on npm used to fail the job outright, which also meant the
    GitHub Release it was supposed to finish never got its executables. Now a version that
    matches no package is still an error, while a version already published continues on to
    repair or refresh the release — and the release is created if the tag arrived without
    one.

Upgrading: nothing to change. Sessions without a Work Plan behave exactly as before,
and one appears only when the agent creates it. Running pi-outpost from npm needs
Node ≥ 24; the executables attached below require Node ≥ 26 to build.

v0.13.0

Choose a tag to compare

@laurentftech laurentftech released this 23 Aug 13:33
v0.13.0
613fd55

Diagrams that can leave the conversation

A structured-exchange diagram could be looked at, and that was all. It lived inside a
reply, and anything you wanted to do with it elsewhere — put it in a report, open it
again tomorrow, hand it to someone who was not in the session — meant a screenshot.

Two ways out of the transcript now.

As a file the agent writes. write_structure_figure performs, for the agent, the
act a reader performs by hand: read a document through the confined read path, narrow it
to the part that matters, and write one .svg through the same confinement as every
other agent write — then reference it from Markdown like any other image. A report the
agent is authoring can carry its own figures.

As a document the viewer draws. Open a structured-exchange document from the
workspace and you get the rendering, the narrowing and the export a reader gets in a
conversation. It is recognised by the schema it declares, never by its extension: any
other JSON keeps the display it had.

What makes those two the same picture is that the rendering core moved to shared/ and
now computes the drawing as data — a list of shapes with the attributes they are drawn
with. React maps a shape to an element; serializeFigure maps the same shape to a
string. Neither decides anything, so a figure in a report and the diagram it came from
cannot drift apart. A seam test compares the two shape for shape, in document order, for
a graph, a proposal, a sequence and a narrowing.

The edges are where the care went:

  • A document that declares the contract and fails it is named as such, carrying the
    reference validator's diagnosis. The browser's own check is a verdict without a
    reason, deliberately — and the reader is the one person able to tell the producer what
    is wrong.
  • An unsupported version falls back to text rather than being validated against a
    contract it never claimed.
  • figureForDocument validates before it draws and refuses with the reason. No partial
    figure, and a narrowing that selects nothing is reported rather than written as an
    empty canvas.
  • Nothing that exists only for pointing travels: the transparent hit shapes a browser
    adds are marked data-hit, and the reader's own download strips them too.
  • A narrowed figure carries its own statement of how much of the document it shows, so a
    figure separated from its source is never mistaken for the whole.

Editing beside the rendering

Revising a structured-exchange document by hand meant toggling: type, switch, look,
switch back, having lost your place in the text. Now both are on screen, and the
rendering follows what is typed, not what was last saved — someone who has saved
nothing has still changed the model in front of them.

The viewer's source/rendered boolean became one mode — rendered, split, source — so
"raw and split" can no longer be represented at all. The split is offered for any file
with a rendering: structured-exchange documents, and Markdown, which gives up its own
two-state toggle for the same three-way control.

Text under revision does not parse for most of the keystrokes that produce it, so a
diagram that vanished on each one would be unusable. The last rendering that validated
stays, marked as no longer matching the editor, with the reason beside it — and those
reasons now distinguish text that is not parseable JSON from text that no longer
declares a schema. Different situations for someone editing, and the commoner one used
to say nothing at all.

The editing itself is the editing that already existed: one textarea, one save, the same
mtime guard, the same confirmation. Cancelling in the split discards the draft and keeps
the mode — discarding an edit is not a request to stop looking at the document.

Both places that render Markdown call one renderer, so a figure referenced from a report
resolves the same in the split as at full width.


Upgrading: nothing to change. write_structure_figure is new, the document viewer
and the split appear on their own, and no existing file changes meaning. Running
pi-outpost from npm needs Node ≥ 24; the executables attached below require Node ≥ 26
to build.