@fungi.computer/corral 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jamstack Consulting LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,108 @@
1
- # Temporary Holding Version
1
+ # Corral (`@fungi.computer/corral`)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Corral reports a Shiitake client's current activity in the active Herdr pane. It
4
+ is an optional client extension: the client supplies activity, and Herdr owns
5
+ the pane and its lifecycle.
6
+
7
+ ## Install or set up
8
+
9
+ ```sh
10
+ npm install @fungi.computer/corral
11
+ ```
12
+
13
+ The package ships ESM only and depends on `effect` and
14
+ [Shiitake](../shiitake/README.md) at runtime. Its only entrypoint,
15
+ `@fungi.computer/corral`, exports the Shiitake client extension as its default
16
+ export and `attachCorral` for hosts that supply their own activity stream.
17
+
18
+ The default extension reads `HERDR_ENV`, `HERDR_SOCKET_PATH`, and
19
+ `HERDR_PANE_ID` from its process environment. It attaches only when `HERDR_ENV`
20
+ is `1` and both the socket path and pane ID are set. Outside that environment,
21
+ attachment has no effect.
22
+
23
+ ## Quick start
24
+
25
+ This illustrative example attaches a stream supplied by the client. Closing the
26
+ Effect scope stops reporting and releases the pane's report:
27
+
28
+ ```ts
29
+ import * as Effect from "effect/Effect";
30
+ import * as Exit from "effect/Exit";
31
+ import * as Scope from "effect/Scope";
32
+ import * as Stream from "effect/Stream";
33
+ import { attachCorral } from "@fungi.computer/corral";
34
+
35
+ const scope = await Effect.runPromise(Scope.make());
36
+ await Effect.runPromise(
37
+ attachCorral(Stream.fromIterable([{ state: "idle" }, { state: "working" }]), {
38
+ environment: {
39
+ HERDR_ENV: "1",
40
+ HERDR_SOCKET_PATH: "/path/to/herdr.sock",
41
+ HERDR_PANE_ID: "pane-id",
42
+ },
43
+ onError: (error) => console.error("Herdr report failed", error),
44
+ }).pipe(Scope.provide(scope)),
45
+ );
46
+ await Effect.runPromise(Scope.close(scope, Exit.void));
47
+ ```
48
+
49
+ The snippet is illustrative: a real client supplies its live activity stream and
50
+ the Herdr values from its managed environment. The integration tests run report
51
+ and release requests against a local socket server in
52
+ [`test/activity.test.ts`](test/activity.test.ts).
53
+
54
+ ## Core concepts
55
+
56
+ The activity stream contains `idle`, `working`, `blocked`, or `unknown`. Corral
57
+ reports changes from that stream. It does not open another Session watch or read
58
+ a Session database. The client remains responsible for producing activity. The
59
+ default entry borrows an AsyncIterable with an independent sliding latest-value
60
+ subscription per iterator and a detach signal. `attachCorral` accepts an Effect
61
+ stream, including a stream with typed `Error` failures. Source and socket errors
62
+ reach `onError`; closing the scope still releases the pane report. Fungi borrows
63
+ its existing Session observation and command interaction store. An open agent
64
+ question, a pending command question or a browser hand-off reports `blocked`.
65
+ Answering it restores the current activity. An agent question may add its short
66
+ header to the blocked message; question bodies, answer choices, answers and
67
+ credentials are never included in reports.
68
+
69
+ Herdr derives `done` when an unseen background turn returns to `idle`. It owns
70
+ whether the user has seen that completion. Corral never guesses completion from
71
+ an unavailable observation.
72
+
73
+ `attachCorral` needs an Effect scope. When that scope closes, Corral cancels its
74
+ reporter, closes any in-flight socket, and releases the current pane report. It
75
+ sends `pane.report_agent` and `pane.release_agent` over Herdr's API socket with
76
+ the `fungi:agent:v1` source and `fungi` agent label. A monotone timestamp
77
+ sequence orders reports across attachments and process restarts.
78
+
79
+ ## API reference
80
+
81
+ See the generated [Corral API reference](docs/api/README.md). The default export
82
+ is an attach-only `shiitake.client/v1` entry. `attachCorral` is the lower-level
83
+ integration for hosts that supply an activity stream directly.
84
+
85
+ ## Examples and checks
86
+
87
+ The activity tests cover environment gating, activity changes, cleanup on scope
88
+ close, and report failure handling. Run them with:
89
+
90
+ ```sh
91
+ pnpm --filter @fungi.computer/corral test
92
+ ```
93
+
94
+ ## Limits and errors
95
+
96
+ Corral is trusted local extension code, not a sandbox. It does not start or stop
97
+ the Herdr service and does not decide what counts as successful work. Report and
98
+ release requests are optional IPC: each is limited to two seconds. Failures are
99
+ passed to `onError`. A reporting failure does not veto client activity or
100
+ terminal restoration. The caller owns any user-facing recovery.
101
+
102
+ The wire contract follows Herdr's
103
+ [agent integration guide](https://herdr.dev/docs/add-herdr-support/) and its
104
+ [Pi extension](https://github.com/herdrdev/herdr/blob/v0.9.3/src/integration/assets/pi/herdr-agent-state.ts).
105
+
106
+ ## Contribute and license
107
+
108
+ Corral is MIT licensed. See [`LICENSE`](LICENSE).
@@ -0,0 +1,29 @@
1
+ import { ExtensionId, type ClientActivityV0 } from "@fungi.computer/shiitake/extensions";
2
+ import * as Stream from "effect/Stream";
3
+ import * as Effect from "effect/Effect";
4
+ import type * as Scope from "effect/Scope";
5
+ /** Pane environment supplied by the local Herdr host. */
6
+ export type CorralEnvironment = Readonly<{
7
+ /** Herdr sets this to "1" for managed processes. */
8
+ HERDR_ENV?: string | undefined;
9
+ /** Opaque pane identity inherited by the client. */
10
+ HERDR_PANE_ID?: string | undefined;
11
+ /** Local API socket (or named pipe on Windows). */
12
+ HERDR_SOCKET_PATH?: string | undefined;
13
+ }>;
14
+ /** Activity supplied by the client; blocked means a human response is pending. */
15
+ export type CorralActivity = ClientActivityV0;
16
+ /** Attach to borrowed activity; never opens a Session watch or starts Herdr. */
17
+ export declare function attachCorral(activity: Stream.Stream<CorralActivity, Error>, options: Readonly<{
18
+ environment: CorralEnvironment;
19
+ onError: (cause: Error) => void;
20
+ }>): Effect.Effect<void, Error, Scope.Scope>;
21
+ /** Ordinary Fungi client extension; disabled outside a Herdr pane. */
22
+ declare const extension: Readonly<{
23
+ protocol: "shiitake.client/v1";
24
+ id: ExtensionId;
25
+ promptPresentation?: "footer";
26
+ attach?: (context: import("@fungi.computer/shiitake/client-entry").ClientExtensionContextV1) => Effect.Effect<void, Error, Scope.Scope>;
27
+ contribute?: (slot: import("@fungi.computer/shiitake/extensions").ExtensionSlotV0 | undefined) => import("@fungi.computer/shiitake/client-entry").ClientContributionV1;
28
+ }>;
29
+ export default extension;
package/dist/index.js ADDED
@@ -0,0 +1,106 @@
1
+ import { defineClientEntry } from "@fungi.computer/shiitake/client-entry";
2
+ import process from "node:process";
3
+ import { once } from "node:events";
4
+ import { createConnection } from "node:net";
5
+ import { createInterface } from "node:readline";
6
+ import { ExtensionId, } from "@fungi.computer/shiitake/extensions";
7
+ import * as Stream from "effect/Stream";
8
+ import * as Effect from "effect/Effect";
9
+ import * as Schema from "effect/Schema";
10
+ const source = "fungi:agent:v1";
11
+ let reportSequence = Date.now() * 1000;
12
+ const decodeResponse = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Union([
13
+ Schema.Struct({ id: Schema.String, result: Schema.Unknown }),
14
+ Schema.Struct({
15
+ id: Schema.String,
16
+ error: Schema.Struct({ code: Schema.String, message: Schema.String }),
17
+ }),
18
+ ])));
19
+ function nextSequence() {
20
+ reportSequence = Math.max(reportSequence + 1, Date.now() * 1000);
21
+ return reportSequence;
22
+ }
23
+ function request(endpoint, pane, activity) {
24
+ const state = activity?.state;
25
+ const seq = nextSequence();
26
+ const id = `${source}:${String(seq)}`;
27
+ const params = {
28
+ pane_id: pane,
29
+ source,
30
+ agent: "fungi",
31
+ seq,
32
+ };
33
+ if (state !== undefined)
34
+ params.state = state;
35
+ if (state === "blocked")
36
+ params.message =
37
+ activity?.header === undefined
38
+ ? "Waiting for your response"
39
+ : `Waiting for your response: ${activity.header}`;
40
+ const body = {
41
+ id,
42
+ method: state === undefined ? "pane.release_agent" : "pane.report_agent",
43
+ params,
44
+ };
45
+ return Effect.tryPromise({
46
+ try: async (signal) => {
47
+ const socket = createConnection({ path: endpoint, signal });
48
+ const lines = createInterface({ input: socket, crlfDelay: Infinity });
49
+ try {
50
+ const replies = lines[Symbol.asyncIterator]();
51
+ const [, reply] = await Promise.all([
52
+ once(socket, "connect", { signal }).then(() => {
53
+ socket.write(`${JSON.stringify(body)}\n`);
54
+ }),
55
+ replies.next(),
56
+ ]);
57
+ if (reply.done)
58
+ throw new Error("Herdr closed without a response");
59
+ const response = decodeResponse(reply.value);
60
+ if (response.id !== id)
61
+ throw new Error("Herdr response ID mismatch");
62
+ if ("error" in response)
63
+ throw new Error(`Herdr report refused: ${response.error.code}`);
64
+ }
65
+ finally {
66
+ lines.close();
67
+ socket.destroy();
68
+ }
69
+ },
70
+ catch: (cause) => new Error("Herdr report failed", { cause }),
71
+ }).pipe(Effect.timeout("2 seconds"));
72
+ }
73
+ /** Attach to borrowed activity; never opens a Session watch or starts Herdr. */
74
+ export function attachCorral(activity, options) {
75
+ const { HERDR_ENV, HERDR_SOCKET_PATH, HERDR_PANE_ID } = options.environment;
76
+ if (HERDR_ENV !== "1" || !HERDR_SOCKET_PATH || !HERDR_PANE_ID)
77
+ return Effect.void;
78
+ const endpoint = process.platform === "win32"
79
+ ? `\\\\.\\pipe\\${HERDR_SOCKET_PATH}`
80
+ : HERDR_SOCKET_PATH;
81
+ const send = (activity) => request(endpoint, HERDR_PANE_ID, activity).pipe(Effect.catch((cause) => Effect.sync(() => {
82
+ options.onError(cause);
83
+ })));
84
+ return Effect.gen(function* () {
85
+ yield* Effect.acquireRelease(Effect.void, () => send(undefined));
86
+ yield* activity.pipe(Stream.changesWith((a, b) => a.state === b.state && a.header === b.header), Stream.runForEach(send), Effect.catch((cause) => Effect.sync(() => {
87
+ options.onError(cause);
88
+ })), Effect.forkScoped);
89
+ });
90
+ }
91
+ /** Ordinary Fungi client extension; disabled outside a Herdr pane. */
92
+ const extension = defineClientEntry({
93
+ id: ExtensionId.make("corral"),
94
+ commands: {},
95
+ projection: null,
96
+ }, {
97
+ attach: ({ activity, onError }) => attachCorral(Stream.fromAsyncIterable(activity, (cause) => new Error("Corral activity delivery failed", { cause })), {
98
+ environment: {
99
+ HERDR_ENV: process.env.HERDR_ENV,
100
+ HERDR_SOCKET_PATH: process.env.HERDR_SOCKET_PATH,
101
+ HERDR_PANE_ID: process.env.HERDR_PANE_ID,
102
+ },
103
+ onError,
104
+ }),
105
+ });
106
+ export default extension;
@@ -0,0 +1,68 @@
1
+ <!-- Generated by Botanical API docs (TypeDoc + typedoc-plugin-markdown). Do not edit. -->
2
+
3
+ **@fungi.computer/corral**
4
+
5
+ ---
6
+
7
+ # @fungi.computer/corral
8
+
9
+ ## Type Aliases
10
+
11
+ ### CorralEnvironment
12
+
13
+ ```ts
14
+ type CorralEnvironment = Readonly<{
15
+ HERDR_ENV?: string;
16
+ HERDR_PANE_ID?: string;
17
+ HERDR_SOCKET_PATH?: string;
18
+ }>;
19
+ ```
20
+
21
+ Pane environment supplied by the local Herdr host.
22
+
23
+ ---
24
+
25
+ ### CorralActivity
26
+
27
+ ```ts
28
+ type CorralActivity = ClientActivityV0;
29
+ ```
30
+
31
+ Activity supplied by the client; blocked means a human response is pending.
32
+
33
+ ## Variables
34
+
35
+ ### default
36
+
37
+ ```ts
38
+ const default: Readonly<{
39
+ protocol: "shiitake.client/v1";
40
+ id: string & Brand<"ExtensionId">;
41
+ promptPresentation?: "footer";
42
+ attach?: (context) => Effect<void, Error, Scope>;
43
+ contribute?: (slot) => object;
44
+ }>;
45
+ ```
46
+
47
+ Ordinary Fungi client extension; disabled outside a Herdr pane.
48
+
49
+ ## Functions
50
+
51
+ ### attachCorral()
52
+
53
+ ```ts
54
+ function attachCorral(activity, options): Effect<void, Error, Scope>;
55
+ ```
56
+
57
+ Attach to borrowed activity; never opens a Session watch or starts Herdr.
58
+
59
+ #### Parameters
60
+
61
+ | Parameter | Type |
62
+ | ---------- | ------------------------------------------------------------------------------------------------------------- |
63
+ | `activity` | `Stream`\<`Readonly`\<\{ `state`: `ClientActivityStateV0`; `header?`: `string`; \}\>, `Error`\> |
64
+ | `options` | `Readonly`\<\{ `environment`: [`CorralEnvironment`](#corralenvironment); `onError`: (`cause`) => `void`; \}\> |
65
+
66
+ #### Returns
67
+
68
+ `Effect`\<`void`, `Error`, `Scope`\>
package/package.json CHANGED
@@ -1,6 +1,62 @@
1
1
  {
2
2
  "name": "@fungi.computer/corral",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Optional Herdr activity reporting for Shiitake clients",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "docs/api",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "dependencies": {
20
+ "effect": "4.0.2",
21
+ "@fungi.computer/shiitake": "0.3.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "24.13.3",
25
+ "typescript": "5.9.3",
26
+ "vitest": "4.1.11"
27
+ },
28
+ "license": "MIT",
29
+ "author": "Levi <levi@technomancy.dev>",
30
+ "main": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "botanical": {
33
+ "role": "package",
34
+ "operations": {
35
+ "lint": "required",
36
+ "typecheck": "required",
37
+ "build": "required",
38
+ "test": "required",
39
+ "docs": "required",
40
+ "pack": "required",
41
+ "smoke": "required",
42
+ "stranger": "required"
43
+ }
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/fungi-computer/corral.git"
48
+ },
49
+ "scripts": {
50
+ "typecheck": "tsc -p tsconfig.json --noEmit",
51
+ "build": "tsc -p tsconfig.build.json",
52
+ "lint": "eslint src test --max-warnings=0",
53
+ "test": "vitest run",
54
+ "docs": "tsx ../../tooling/repository/src/docs/cli.ts check --root ../..",
55
+ "docs:write": "tsx ../../tooling/repository/src/docs/cli.ts write --root ../..",
56
+ "pack": "mkdir -p ../../.botanical/corral-pack && pnpm pack --pack-destination ../../.botanical/corral-pack",
57
+ "smoke": "node --input-type=module -e \"import('./dist/index.js').then(({ attachCorral }) => { if (typeof attachCorral !== 'function') process.exit(1); })\"",
58
+ "stranger": "tsx ../../tooling/repository/src/artifacts/cli.ts --repo-root ../.. .",
59
+ "format:check": "node ../../tooling/repository/scripts/format-package-check.mjs",
60
+ "lint:anti-slop": "node ../../tooling/repository/scripts/lint-package-anti-slop.mjs"
61
+ }
6
62
  }