allye-pi 1.6.2 → 1.7.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/README.md CHANGED
@@ -269,7 +269,7 @@ Every phase, on every platform:
269
269
  - Responds in **your language** (detected from your messages, falling back to your profile only before you've said anything)
270
270
  - Calls `initialize` to load your profile and team context
271
271
  - **Discovers team skills** before starting — coding standards, templates, checklists
272
- - If no standards found → **suggests creating them** with your chosen scope (personal/team/org/marketplace)
272
+ - If no standards found → **suggests creating them** with your chosen scope (personal/team/organization). Marketplace is a legacy read-only browsing/source surface, never a write destination.
273
273
  - Searches memories for past decisions and session state
274
274
  - Saves session state before ending
275
275
 
@@ -81,8 +81,9 @@ adaptive toolkit in the current session. Use `/allye-capabilities` to inspect
81
81
  Allye/MCP, filesystem, subagent, and Herdr capabilities.
82
82
 
83
83
  When `HERDR_ENV=1`, the optional `allye_herdr` tool provides bounded `detect`,
84
- `spawn`, `dispatch`, `wait`, and Allye-backed `collect` operations. Without it,
85
- Pi continues locally. Herdr and subagents are capabilities, not prerequisites.
84
+ `workspace`, `tab`, `spawn`, `dispatch`, `status`, `mark_intervened`, `wait`,
85
+ `collect`, and ownership-guarded `cleanup` operations. Without it, Pi continues
86
+ locally. Herdr and subagents are capabilities, not prerequisites.
86
87
  Tasks are recommended for meaningful, delegated, multi-step, or review-heavy
87
88
  work, but an explicitly approved no-task path is supported.
88
89
 
@@ -138,6 +139,8 @@ Manual `herdr agent wait` commands started outside this Pi session (for example
138
139
  with `nohup` or Bash) do not wake Pi and do not generate this notification.
139
140
 
140
141
  A real Herdr dispatch still requires `HERDR_ENV=1`, a compatible Herdr session,
141
- and an existing isolated worktree. Pi records workspace/tab/pane/agent ownership
142
- for safe teardown; it never adopts foreign panes, stops Herdr, closes panes,
143
- merges, pushes, or publishes without explicit authorization.
142
+ and an existing isolated worktree. Pi records execution IDs plus
143
+ workspace/tab/pane/agent ownership for safe teardown; it never adopts foreign
144
+ panes, stops Herdr, closes panes, merges, pushes, or publishes without explicit
145
+ authorization. Cleanup is only allowed after a settled and collected execution;
146
+ blocked, unknown, timed-out, interrupted, or manually assumed resources remain open.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "allye-pi",
3
- "version": "1.6.2",
3
+ "version": "1.7.1",
4
4
  "description": "Pi package adapter for the canonical Allye workflow",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -3,6 +3,7 @@ import { promisify } from "node:util";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { randomUUID } from "node:crypto";
6
7
  import { Type } from "typebox";
7
8
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
9
 
@@ -26,9 +27,9 @@ type StartupContext = {
26
27
  allyeUnavailable: boolean;
27
28
  teams: Array<{ id: string; name: string; prefix?: string }>;
28
29
  };
29
- type WaitRegistrar = (name: string, timeoutMs: number) => Record<string, unknown>;
30
+ type WaitRegistrar = (name: string, timeoutMs: number, execution?: DelegationExecution) => Record<string, unknown>;
30
31
  type WaitCanceller = (name: string) => void;
31
- export type WaitOutcome = "completed" | "timeout" | "error" | "aborted";
32
+ export type WaitOutcome = "completed" | "blocked" | "unknown" | "timeout" | "error" | "aborted";
32
33
  export type WaitExecutionResult = {
33
34
  code: number;
34
35
  killed: boolean;
@@ -36,6 +37,8 @@ export type WaitExecutionResult = {
36
37
  stderr: string;
37
38
  };
38
39
  export type WaitEvent = {
40
+ kind?: "settled" | "intervened";
41
+ executionId?: string;
39
42
  name: string;
40
43
  timeoutMs: number;
41
44
  outcome: WaitOutcome;
@@ -66,10 +69,37 @@ export type OwnedPane = {
66
69
  tabId: string;
67
70
  paneId: string;
68
71
  agentName?: string;
72
+ resourceId?: string;
73
+ };
74
+ export type OwnedResource = {
75
+ resourceId: string;
76
+ level: "pane" | "tab" | "workspace";
77
+ workspaceId: string;
78
+ tabId?: string;
79
+ paneId?: string;
80
+ createdAt: string;
81
+ };
82
+ export type DelegationExecution = {
83
+ executionId: string;
84
+ callerPaneId?: string;
85
+ workspaceId: string;
86
+ tabId: string;
87
+ paneId: string;
88
+ resourceLevel: "pane" | "tab" | "workspace";
89
+ resourceId?: string;
90
+ worktree: string;
91
+ agentName?: string;
92
+ status: "created" | "working" | "settled" | "blocked" | "unknown" | "intervened" | "cleaned";
93
+ runtimeState?: "working" | "idle" | "done" | "blocked" | "unknown";
94
+ manualIntervention: boolean;
95
+ createdAt: string;
96
+ settledAt?: string;
69
97
  };
70
98
  export type DelegationState = {
71
99
  panes: OwnedPane[];
100
+ resources: Map<string, OwnedResource>;
72
101
  waits: Set<string>;
102
+ executions: Map<string, DelegationExecution>;
73
103
  };
74
104
  const WAIT_EVENT_TYPE = "allye-herdr-wait";
75
105
  const RUNTIME_TOOL_NAME = "allye_herdr";
@@ -187,6 +217,11 @@ export function classifyWaitResult(result?: WaitExecutionResult, timeoutMs = 0,
187
217
  const errorText = error instanceof Error ? error.message : String(error ?? "");
188
218
  if (/abort/i.test(errorText)) return "aborted";
189
219
  if (/timeout|timed out/i.test(errorText) || Boolean(result?.killed && timeoutMs > 0)) return "timeout";
220
+ if (/blocked/i.test(errorText)) return "blocked";
221
+ if (/unknown/i.test(errorText)) return "unknown";
222
+ const runtimeState = result ? extractAgentLifecycleState(parseJson(result.stdout)) ?? extractAgentLifecycleState(parseJson(result.stderr)) : null;
223
+ if (runtimeState === "blocked") return "blocked";
224
+ if (runtimeState === "unknown") return "unknown";
190
225
  if (!result || result.code !== 0) return "error";
191
226
  return "completed";
192
227
  }
@@ -200,7 +235,8 @@ export function shouldEmitWaitEvent(shuttingDown: boolean, settledWaits: Set<str
200
235
  export function formatWaitEvent(event: WaitEvent): string {
201
236
  const output = event.stdout?.trim() || "(no stdout)";
202
237
  const error = event.error || event.stderr?.trim() || "(none)";
203
- return `Herdr wait settled for agent ${event.name}. Outcome: ${event.outcome}. `
238
+ const prefix = event.kind === "intervened" ? "Herdr agent intervention detected" : "Herdr wait settled";
239
+ return `${prefix} for agent ${event.name}. Outcome: ${event.outcome}. `
204
240
  + `This is delegation evidence only, not a completion verdict. `
205
241
  + `Use allye_herdr collect when a managed work item exists: read work_children for the story and search Allye memories for Review and Implementation evidence before declaring completion.\n\n`
206
242
  + `timeout_ms=${event.timeoutMs} code=${event.code ?? "n/a"} killed=${event.killed ?? false} timestamp=${event.timestamp}\n`
@@ -208,7 +244,7 @@ export function formatWaitEvent(event: WaitEvent): string {
208
244
  }
209
245
 
210
246
  export function waitEventNotificationLevel(event: WaitEvent): "info" | "warning" | "error" {
211
- if (event.outcome === "error") return "error";
247
+ if (event.outcome === "error" || event.outcome === "blocked" || event.outcome === "unknown") return "error";
212
248
  if (event.outcome === "timeout") return "warning";
213
249
  return "info";
214
250
  }
@@ -412,9 +448,30 @@ export function activeToolsForCapabilities(activeTools: string[], capabilities:
412
448
  return [...next];
413
449
  }
414
450
 
451
+ export type AgentLifecycleState = "working" | "idle" | "done" | "blocked" | "unknown";
452
+
453
+ export function extractAgentLifecycleState(value: unknown): AgentLifecycleState | null {
454
+ if (!value || typeof value !== "object") return null;
455
+ if (Array.isArray(value)) {
456
+ for (const item of value) {
457
+ const state = extractAgentLifecycleState(item);
458
+ if (state) return state;
459
+ }
460
+ return null;
461
+ }
462
+ const object = value as Record<string, unknown>;
463
+ const raw = object.agent_status;
464
+ if (raw === "working" || raw === "idle" || raw === "done" || raw === "blocked" || raw === "unknown") return raw;
465
+ for (const child of Object.values(object)) {
466
+ const state = extractAgentLifecycleState(child);
467
+ if (state) return state;
468
+ }
469
+ return null;
470
+ }
471
+
415
472
  export function dispatchStateIsAcceptable(stateText: string): boolean {
416
- if (/blocked|unknown/i.test(stateText)) return false;
417
- return /working|idle|done/i.test(stateText);
473
+ const state = extractAgentLifecycleState(parseJson(stateText));
474
+ return state === "working" || state === "idle" || state === "done";
418
475
  }
419
476
 
420
477
  function requireHerdr(capabilities: AllyeCapabilities): void {
@@ -428,6 +485,23 @@ function requireAbsolutePath(value: unknown, name: string): string {
428
485
  return value;
429
486
  }
430
487
 
488
+ async function cleanupExecution(execution: DelegationExecution, ownership: DelegationState): Promise<unknown> {
489
+ if (execution.status === "working" || execution.status === "blocked" || execution.status === "unknown" || execution.manualIntervention) {
490
+ throw new Error(`execution ${execution.executionId} is not safe to clean up while status=${execution.status}`);
491
+ }
492
+ const resourceId = execution.resourceId;
493
+ if (!resourceId) return { cleaned: false, reason: "execution has no owned resource" };
494
+ const resource = ownership.resources.get(resourceId);
495
+ if (!resource) return { cleaned: false, reason: "owned resource is already absent" };
496
+ if (resource.level === "pane" && resource.paneId) await runHerdr(["pane", "close", resource.paneId]);
497
+ else if (resource.level === "tab" && resource.tabId) await runHerdr(["tab", "close", resource.tabId]);
498
+ else if (resource.level === "workspace") await runHerdr(["workspace", "close", resource.workspaceId]);
499
+ execution.status = "cleaned";
500
+ ownership.resources.delete(resourceId);
501
+ ownership.panes = ownership.panes.filter((pane) => pane.resourceId !== resourceId);
502
+ return { cleaned: true, resource };
503
+ }
504
+
431
505
  async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<string, unknown>, registerWait: WaitRegistrar, cancelWait: WaitCanceller, ownership: DelegationState): Promise<unknown> {
432
506
  const operation = params.operation;
433
507
  if (operation === "detect") {
@@ -442,6 +516,45 @@ async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<
442
516
 
443
517
  requireHerdr(capabilities);
444
518
 
519
+ if (operation === "workspace") {
520
+ const cwd = requireAbsolutePath(params.cwd, "cwd");
521
+ if (typeof params.worktree === "string" && params.worktree !== cwd) throw new Error("workspace cwd must match the requested worktree; use worktree management before creating a workspace");
522
+ const label = typeof params.label === "string" && params.label.trim() ? params.label.trim() : `allye-${randomUUID().slice(0, 8)}`;
523
+ const focus = params.focus === true ? "--focus" : "--no-focus";
524
+ const resource = herdrResult(await runHerdr(["workspace", "create", "--cwd", cwd, "--label", label, focus]));
525
+ const record = resource as Record<string, unknown>;
526
+ const workspace = record.workspace as Record<string, unknown> | undefined;
527
+ const tab = record.tab as Record<string, unknown> | undefined;
528
+ const rootPane = record.root_pane as Record<string, unknown> | undefined;
529
+ const workspaceId = typeof workspace?.workspace_id === "string" ? workspace.workspace_id : "";
530
+ const tabId = typeof tab?.tab_id === "string" ? tab.tab_id : "";
531
+ const paneId = typeof rootPane?.pane_id === "string" ? rootPane.pane_id : "";
532
+ if (!workspaceId || !tabId || !paneId) throw new Error("Herdr workspace response did not include workspace, tab, and root pane identifiers");
533
+ ownership.resources.set(workspaceId, { resourceId: workspaceId, level: "workspace", workspaceId, tabId, paneId, createdAt: new Date().toISOString() });
534
+ ownership.panes.push({ workspaceId, tabId, paneId, resourceId: workspaceId });
535
+ return { created: true, level: "workspace", resource };
536
+ }
537
+
538
+ if (operation === "tab") {
539
+ const workspace = typeof params.workspaceId === "string" ? params.workspaceId : "";
540
+ const cwd = requireAbsolutePath(params.cwd, "cwd");
541
+ const label = typeof params.label === "string" && params.label.trim() ? params.label.trim() : `task-${randomUUID().slice(0, 8)}`;
542
+ const focus = params.focus === true ? "--focus" : "--no-focus";
543
+ const args = ["tab", "create", "--workspace", workspace, "--cwd", cwd, "--label", label, focus];
544
+ if (!workspace) throw new Error("tab creation requires workspaceId");
545
+ const resource = herdrResult(await runHerdr(args));
546
+ const record = resource as Record<string, unknown>;
547
+ const tab = record.tab as Record<string, unknown> | undefined;
548
+ const rootPane = record.root_pane as Record<string, unknown> | undefined;
549
+ const tabId = typeof tab?.tab_id === "string" ? tab.tab_id : "";
550
+ const paneId = typeof rootPane?.pane_id === "string" ? rootPane.pane_id : "";
551
+ if (!tabId || !paneId) throw new Error("Herdr tab response did not include tab and root pane identifiers");
552
+ const resourceId = `${workspace}:${tabId}`;
553
+ ownership.resources.set(resourceId, { resourceId, level: "tab", workspaceId: workspace, tabId, paneId, createdAt: new Date().toISOString() });
554
+ ownership.panes.push({ workspaceId: workspace, tabId, paneId, resourceId });
555
+ return { created: true, level: "tab", resource };
556
+ }
557
+
445
558
  if (operation === "spawn") {
446
559
  const cwd = requireAbsolutePath(params.cwd, "cwd");
447
560
  const worktree = requireAbsolutePath(params.worktree, "worktree");
@@ -462,9 +575,11 @@ async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<
462
575
  && candidate.workspaceId === String(layoutMetadata.workspace_id ?? candidate.workspaceId)
463
576
  && candidate.tabId === String(layoutMetadata.tab_id ?? candidate.tabId));
464
577
  if (!record) throw new Error("Herdr returned a pane without workspace/tab ownership metadata");
465
- ownership.panes.push(record);
578
+ const resourceId = `${record.workspaceId}:${record.tabId}:${record.paneId}`;
579
+ ownership.resources.set(resourceId, { resourceId, level: "pane", workspaceId: record.workspaceId, tabId: record.tabId, paneId: record.paneId, createdAt: new Date().toISOString() });
580
+ ownership.panes.push({ ...record, resourceId });
466
581
  await waitForInteractiveShell(newPane);
467
- return { pane: newPane, worktree, cwd, direction, ready: true, ownership: record };
582
+ return { pane: newPane, worktree, cwd, direction, ready: true, ownership: { ...record, resourceId } };
468
583
  }
469
584
 
470
585
  if (operation === "dispatch") {
@@ -496,7 +611,23 @@ async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<
496
611
  // A fast agent can already be idle/done by the time it is inspected. The
497
612
  // server-owned wait is registered immediately so that completion is not
498
613
  // lost and a fast, successful turn is not falsely rejected.
499
- const wait = registerWait(name, 3_600_000);
614
+ const execution: DelegationExecution = {
615
+ executionId: randomUUID(),
616
+ callerPaneId: process.env.HERDR_PANE_ID,
617
+ workspaceId: ownedPane.workspaceId,
618
+ tabId: ownedPane.tabId,
619
+ paneId: ownedPane.paneId,
620
+ resourceLevel: "pane",
621
+ resourceId: ownedPane.resourceId,
622
+ worktree,
623
+ agentName: name,
624
+ status: "working",
625
+ runtimeState: "working",
626
+ manualIntervention: false,
627
+ createdAt: new Date().toISOString(),
628
+ };
629
+ ownership.executions.set(execution.executionId, execution);
630
+ const wait = registerWait(name, 3_600_000, execution);
500
631
  let agentState: unknown;
501
632
  try {
502
633
  agentState = herdrResult(await runHerdr(["agent", "get", name]));
@@ -514,10 +645,33 @@ async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<
514
645
  started: herdrResult(start),
515
646
  state: agentState,
516
647
  wait,
648
+ execution,
517
649
  ownership: ownedPane,
518
650
  };
519
651
  }
520
652
 
653
+ if (operation === "status") {
654
+ const executionId = params.executionId;
655
+ if (typeof executionId !== "string") throw new Error("status requires executionId");
656
+ const execution = ownership.executions.get(executionId);
657
+ if (!execution) throw new Error(`execution ${executionId} is not owned by this Pi session`);
658
+ if (execution.agentName) {
659
+ const current = extractAgentLifecycleState(herdrResult(await runHerdr(["agent", "get", execution.agentName])));
660
+ if (current) execution.runtimeState = current;
661
+ }
662
+ return execution;
663
+ }
664
+
665
+ if (operation === "mark_intervened") {
666
+ const executionId = params.executionId;
667
+ if (typeof executionId !== "string") throw new Error("mark_intervened requires executionId");
668
+ const execution = ownership.executions.get(executionId);
669
+ if (!execution) throw new Error(`execution ${executionId} is not owned by this Pi session`);
670
+ execution.manualIntervention = true;
671
+ execution.status = "intervened";
672
+ return execution;
673
+ }
674
+
521
675
  if (operation === "wait") {
522
676
  const name = params.name;
523
677
  if (typeof name !== "string" || !AGENT_NAME.test(name)) throw new Error("valid agent name is required");
@@ -526,6 +680,14 @@ async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<
526
680
  return registerWait(name, timeout);
527
681
  }
528
682
 
683
+ if (operation === "cleanup") {
684
+ const executionId = params.executionId;
685
+ if (typeof executionId !== "string") throw new Error("cleanup requires executionId");
686
+ const execution = ownership.executions.get(executionId);
687
+ if (!execution) throw new Error(`execution ${executionId} is not owned by this Pi session`);
688
+ return cleanupExecution(execution, ownership);
689
+ }
690
+
529
691
  if (operation === "collect") {
530
692
  const storyId = params.storyId;
531
693
  if (typeof storyId !== "string" || storyId.length === 0) throw new Error("collect requires the story UUID as storyId");
@@ -534,7 +696,7 @@ async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<
534
696
  const review = await callAllye("allye_intelligence", { action: "memory_search", query: reviewQuery, limit: 10, return_content: true });
535
697
  const implementationQuery = typeof params.taskKey === "string" ? `Implementation ${params.taskKey}` : `Implementation ${storyId}`;
536
698
  const implementation = await callAllye("allye_intelligence", { action: "memory_search", query: implementationQuery, limit: 10, return_content: true });
537
- return { storyChildren: story, review, implementation };
699
+ return { storyChildren: story, review, implementation, cleanupAvailable: typeof params.executionId === "string" };
538
700
  }
539
701
 
540
702
  throw new Error(`Unknown runtime operation: ${String(operation)}`);
@@ -546,15 +708,19 @@ function registerRuntimeTool(pi: ExtensionAPI, getCapabilities: () => AllyeCapab
546
708
  label: "Allye Runtime",
547
709
  description: "Use optional Herdr capabilities for delegation, waiting, and collecting results when available.",
548
710
  parameters: Type.Object({
549
- operation: Type.String({ description: "detect | spawn | dispatch | wait | collect" }),
711
+ operation: Type.String({ description: "detect | workspace | tab | spawn | dispatch | status | mark_intervened | wait | collect | cleanup" }),
550
712
  cwd: Type.Optional(Type.String({ description: "Absolute plugin-enabled repository root for spawn" })),
551
713
  worktree: Type.Optional(Type.String({ description: "Absolute isolated worktree path; required for spawn/dispatch" })),
714
+ workspaceId: Type.Optional(Type.String({ description: "Workspace id for tab creation" })),
715
+ label: Type.Optional(Type.String({ description: "Human-readable workspace/tab label" })),
716
+ focus: Type.Optional(Type.Boolean({ description: "Focus newly created workspace/tab" })),
552
717
  direction: Type.Optional(Type.String({ description: "right or down" })),
553
718
  pane: Type.Optional(Type.String({ description: "Pane id returned by Herdr" })),
554
719
  name: Type.Optional(Type.String({ description: "Stable Herdr agent name" })),
555
720
  kind: Type.Optional(Type.String({ description: "Herdr agent kind, normally pi" })),
556
721
  briefing: Type.Optional(Type.String({ description: "Complete story briefing, including the absolute worktree path" })),
557
722
  timeoutMs: Type.Optional(Type.Number({ description: "Bounded wait timeout in milliseconds" })),
723
+ executionId: Type.Optional(Type.String({ description: "Caller execution id for status/intervention/cleanup" })),
558
724
  storyId: Type.Optional(Type.String({ description: "Allye story UUID for collect" })),
559
725
  storyKey: Type.Optional(Type.String({ description: "Story key for review memory search" })),
560
726
  taskKey: Type.Optional(Type.String({ description: "Task key for implementation memory search" })),
@@ -580,9 +746,9 @@ export default function allyePiAdapter(pi: ExtensionAPI): void {
580
746
  let activeContext: ExtensionContext | undefined;
581
747
  const waits = new Map<string, AbortController>();
582
748
  const settledWaits = new Set<string>();
583
- const ownership: DelegationState = { panes: [], waits: new Set() };
749
+ const ownership: DelegationState = { panes: [], resources: new Map(), waits: new Set(), executions: new Map() };
584
750
 
585
- const registerWait: WaitRegistrar = (name, timeoutMs) => {
751
+ const registerWait: WaitRegistrar = (name, timeoutMs, execution) => {
586
752
  if (settledWaits.has(name)) return { registered: false, name, timeoutMs, duplicate: true, settled: true };
587
753
  const existing = waits.get(name);
588
754
  if (existing) return { registered: true, name, timeoutMs, duplicate: true };
@@ -593,6 +759,8 @@ export default function allyePiAdapter(pi: ExtensionAPI): void {
593
759
  signal: controller.signal,
594
760
  }).then((result) => {
595
761
  const event: WaitEvent = {
762
+ kind: execution?.manualIntervention ? "intervened" : "settled",
763
+ executionId: execution?.executionId,
596
764
  name,
597
765
  timeoutMs,
598
766
  outcome: classifyWaitResult(result, timeoutMs),
@@ -602,6 +770,11 @@ export default function allyePiAdapter(pi: ExtensionAPI): void {
602
770
  stderr: result.stderr,
603
771
  timestamp: new Date().toISOString(),
604
772
  };
773
+ if (execution) {
774
+ execution.status = execution.manualIntervention ? "intervened" : event.outcome === "blocked" ? "blocked" : event.outcome === "completed" ? "settled" : "unknown";
775
+ execution.runtimeState = event.outcome === "completed" ? "done" : event.outcome === "blocked" ? "blocked" : "unknown";
776
+ execution.settledAt = event.timestamp;
777
+ }
605
778
  deliverWaitEvent(event, shuttingDown, settledWaits, {
606
779
  appendEntry: (customType, value) => pi.appendEntry(customType, value),
607
780
  notify: activeContext?.hasUI ? (message, level) => activeContext?.ui.notify(message, level) : undefined,
@@ -609,12 +782,19 @@ export default function allyePiAdapter(pi: ExtensionAPI): void {
609
782
  });
610
783
  }).catch((error) => {
611
784
  const event: WaitEvent = {
785
+ kind: execution?.manualIntervention ? "intervened" : "settled",
786
+ executionId: execution?.executionId,
612
787
  name,
613
788
  timeoutMs,
614
789
  outcome: classifyWaitResult(undefined, timeoutMs, error),
615
790
  error: error instanceof Error ? error.message : String(error),
616
791
  timestamp: new Date().toISOString(),
617
792
  };
793
+ if (execution) {
794
+ execution.status = execution.manualIntervention ? "intervened" : event.outcome === "blocked" ? "blocked" : event.outcome === "completed" ? "settled" : "unknown";
795
+ execution.runtimeState = event.outcome === "completed" ? "done" : event.outcome === "blocked" ? "blocked" : "unknown";
796
+ execution.settledAt = event.timestamp;
797
+ }
618
798
  deliverWaitEvent(event, shuttingDown, settledWaits, {
619
799
  appendEntry: (customType, value) => pi.appendEntry(customType, value),
620
800
  notify: activeContext?.hasUI ? (message, level) => activeContext?.ui.notify(message, level) : undefined,
@@ -664,7 +844,9 @@ export default function allyePiAdapter(pi: ExtensionAPI): void {
664
844
  for (const controller of waits.values()) controller.abort();
665
845
  waits.clear();
666
846
  ownership.waits.clear();
667
- ownership.panes.length = 0;
847
+ // Owned Herdr resources intentionally remain open across a Pi shutdown.
848
+ // Cleanup requires an explicit, verified cleanup operation and never runs
849
+ // implicitly while the caller cannot inspect its final state.
668
850
  });
669
851
 
670
852
  pi.on("before_agent_start", async (event) => {
@@ -13,7 +13,13 @@ any runtime that satisfies the contract rather than one specific tool.
13
13
 
14
14
  Load this only when a compatible runtime is actually detected and the user benefits from delegation. Without one, continue locally or use another available subagent mechanism; never let the absence or degradation of a runtime block delivery.
15
15
 
16
- Concrete commands for the runtimes we implement live in `references/`. This file is the
16
+ Concrete commands for the runtimes we implement live in `references/`.
17
+
18
+ For Herdr, the Pi adapter exposes `workspace` and `tab` creation in addition to
19
+ `spawn`, and returns an execution ID from `dispatch`. The caller must retain this
20
+ ID, use `status`/`wait`, collect the result, and only then request ownership-guarded
21
+ `cleanup`. `mark_intervened` records human takeover and permanently blocks automatic
22
+ cleanup for that execution. This file is the
17
23
  contract; the reference is the implementation.
18
24
 
19
25
  <!-- adapted from ogulcancelik/herdr SKILL.md (Apache-2.0) — the primitive vocabulary and the safety rules -->
@@ -22,6 +22,18 @@ fails in a way that suggests the server went away.
22
22
  Caller context is injected into every managed pane: `$HERDR_PANE_ID`, `$HERDR_TAB_ID`,
23
23
  `$HERDR_WORKSPACE_ID`.
24
24
 
25
+ ## workspace and tab ownership
26
+
27
+ The Pi adapter can create a workspace or a tab without focusing it:
28
+
29
+ ```text
30
+ allye_herdr(operation="workspace", cwd="/absolute/repo", label="feature-runtime")
31
+ allye_herdr(operation="tab", workspaceId="w...", cwd="/absolute/repo", label="story-1")
32
+ ```
33
+
34
+ The returned IDs are registered as owned resources. Never infer IDs from layout
35
+ order and never use these operations to adopt an existing user resource.
36
+
25
37
  ## spawn
26
38
 
27
39
  ```bash
@@ -132,13 +144,31 @@ herdr agent send-keys {name} esc
132
144
  Then dispatch normally, including the submit confirmation — a freshly dismissed menu leaves
133
145
  the agent idle, which is exactly the state where a prompt lands unsubmitted.
134
146
 
135
- ## teardown
147
+ ## execution status, intervention, and teardown
148
+
149
+ Each dispatch returns an `executionId`. Retain it for the whole lifecycle:
150
+
151
+ ```text
152
+ allye_herdr(operation="status", executionId="...")
153
+ allye_herdr(operation="wait", name="story-agent", timeoutMs=3600000)
154
+ allye_herdr(operation="mark_intervened", executionId="...")
155
+ allye_herdr(operation="cleanup", executionId="...")
156
+ ```
157
+
158
+ Use `mark_intervened` when the user takes over or modifies the managed pane. A
159
+ manual intervention, `blocked`, `unknown`, timeout, or missing collection must
160
+ keep the resource open. Cleanup is valid only after wait settlement, Allye/result
161
+ collection, and verification.
162
+
163
+ The adapter rejects cleanup for resources it did not create and for executions
164
+ that are still working, blocked, unknown, or manually intervened.
136
165
 
137
166
  ```bash
138
167
  herdr pane close {pane_id}
139
168
  ```
140
169
 
141
- Only panes the plugin created, and only after §7.1's merge gates have passed.
170
+ Only panes/tabs/workspaces the plugin created, and only after §7.1's merge gates
171
+ have passed.
142
172
 
143
173
  ## Safety rules — never widened
144
174
 
@@ -193,15 +193,20 @@ Manage and export workflow skills.
193
193
  | `skill_update` | Update a skill | `skill_id`* or `id`*, `skill_name`, `skill_content`, `skill_description`, `skill_category`, `skill_is_active` |
194
194
  | `skill_delete` | Delete a skill | `skill_id`* or `id`* |
195
195
  | `skill_marketplace` | Browse marketplace skills | `query`, `limit`, `offset` |
196
- | `skill_fork` | Fork a marketplace skill | `skill_id`* or `id`*, `skill_scope`* (only `personal`/`team`/`organization` — no `marketplace`) |
196
+ | `skill_fork` | Fork a readable source skill into an independent internal draft | `skill_id`* or `id`*, `skill_scope`*, `skill_slug` (explicit namespace key; required to retry a collision), `skill_name`, `skill_category` (scope only `personal`/`team`/`organization` — no `marketplace`) |
197
197
  | `skill_export` | Export a skill in agent format | `skill_id`* or `id`*, `skill_export_format`* |
198
198
  | `skill_import_github` | Import skill from GitHub | `repo_url`* |
199
199
  | `skill_install` | Install a skill | `skill_id`* or `id`* |
200
200
  | `skill_export_merged` | Export multiple skills merged | `skill_ids`*, `skill_export_format`* |
201
201
 
202
- **`skill_create` requires all three of `skill_content`, `skill_scope`, and `skill_category`** — easy to miss since only `skill_name` reads as obviously required.
202
+ **`skill_create` requires all three of `skill_content`, `skill_scope`, and `skill_category`** — easy to miss since only `skill_name` reads as obviously required. Write scopes are only `personal`, `team`, and `organization`; `marketplace` is legacy read-only browsing/source material and must never be sent as a create, import, update, fork target, publication, or rating destination.
203
+
204
+ For shared writes, the API is authoritative: the authenticated actor must be the container owner/admin, have tenant-scoped `skills:manage`, or hold an active maintainer grant. Membership gives read access, not write access. Team writes require the selected active team (`team_id`/request context); personal and organization writes do not. Use `skill_resolve` for canonical `team > organization > personal` resolution and honor its `candidates`, `selected`, `owner`, `rule`, `reason`, and `conflict` fields.
205
+
206
+ Forks are independent drafts with `forked_from_id`, a new author, no copied grants, and preserved source provenance when available. If a fork returns `409 Conflict`, choose a different explicit `skill_slug` and retry; never silently suffix or overwrite a namespace. Marketplace can be browsed and used as readable source material for an internal fork, but it is never an internal write scope.
207
+
203
208
  **Export formats:** `cursor`, `claude`, `copilot`, `windsurf`, `opencode`, `codex`, `gemini`
204
- **Skill scopes (create):** `personal`, `team`, `organization`, `marketplace`
209
+ **Skill scopes (create/import/fork):** `personal`, `team`, `organization` (never `marketplace`)
205
210
  **Skill categories:** `frontend`, `backend`, `mobile`, `fullstack`, `devops`, `infrastructure`, `architecture`, `code_review`, `testing`, `security`, `documentation`, `performance`, `other`
206
211
 
207
212
  ---