taskplane 0.22.17 → 0.23.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.
@@ -1895,13 +1895,14 @@ export type EngineEventCallback = (event: EngineEvent) => void;
1895
1895
  * - `task-failure`: A task failed after deterministic recovery was exhausted
1896
1896
  * - `merge-failure`: Wave merge failed and batch paused
1897
1897
  * - `batch-complete`: Batch finished (all waves done)
1898
+ * - `agent-message`: Runtime mailbox reply/escalation from a running agent
1898
1899
  *
1899
1900
  * Note: `stall` detection is deferred to a future phase (requires
1900
1901
  * last-activity tracking not yet built).
1901
1902
  *
1902
1903
  * @since TP-076
1903
1904
  */
1904
- export type SupervisorAlertCategory = "task-failure" | "merge-failure" | "batch-complete";
1905
+ export type SupervisorAlertCategory = "task-failure" | "merge-failure" | "batch-complete" | "agent-message";
1905
1906
 
1906
1907
  /**
1907
1908
  * Structured context payload for supervisor alerts.
@@ -1922,6 +1923,10 @@ export interface SupervisorAlertContext {
1922
1923
  waveIndex?: number;
1923
1924
  /** Exit reason string (for task-failure alerts) */
1924
1925
  exitReason?: string;
1926
+ /** Agent ID (for agent-message alerts) */
1927
+ agentId?: string;
1928
+ /** Mailbox message ID (for agent-message alerts) */
1929
+ messageId?: string;
1925
1930
  /** Whether partial progress was preserved (for task-failure alerts) */
1926
1931
  partialProgress?: boolean;
1927
1932
  /** Batch progress summary */
@@ -3451,3 +3456,514 @@ export interface WriteMailboxMessageOpts {
3451
3456
  replyTo?: string | null;
3452
3457
  }
3453
3458
 
3459
+ // ── Runtime V2 Contracts (TP-102) ────────────────────────────────────
3460
+ //
3461
+ // These types define the foundational contracts for the no-TMUX Runtime V2
3462
+ // architecture. They are additive — existing runtime paths continue to work
3463
+ // while Runtime V2 is incrementally adopted.
3464
+ //
3465
+ // Design principles:
3466
+ // 1. Agent identity is a stable runtime ID, not a TMUX session name.
3467
+ // 2. Packet-path authority is explicit, never inferred from cwd.
3468
+ // 3. Process ownership uses a registry, not terminal session discovery.
3469
+ // 4. Normalized events flow directly from child to parent.
3470
+ //
3471
+ // See: docs/specifications/framework/taskplane-runtime-v2/
3472
+ // ─────────────────────────────────────────────────────────────────────
3473
+
3474
+ /**
3475
+ * Canonical agent roles in the Runtime V2 process model.
3476
+ *
3477
+ * Every spawned agent process has exactly one role. The role determines
3478
+ * the process's responsibilities, tools, and lifecycle semantics.
3479
+ *
3480
+ * @since TP-102
3481
+ */
3482
+ export type RuntimeAgentRole = "worker" | "reviewer" | "merger" | "lane-runner";
3483
+
3484
+ /**
3485
+ * Agent lifecycle states in the process registry.
3486
+ *
3487
+ * State machine:
3488
+ * spawning → running → wrapping_up → exited
3489
+ * → crashed
3490
+ * → timed_out
3491
+ * → killed
3492
+ *
3493
+ * @since TP-102
3494
+ */
3495
+ export type RuntimeAgentStatus =
3496
+ | "spawning"
3497
+ | "running"
3498
+ | "wrapping_up"
3499
+ | "exited"
3500
+ | "crashed"
3501
+ | "timed_out"
3502
+ | "killed";
3503
+
3504
+ /** Set of terminal agent statuses (process is no longer alive). @since TP-102 */
3505
+ export const TERMINAL_AGENT_STATUSES: ReadonlySet<RuntimeAgentStatus> = new Set([
3506
+ "exited", "crashed", "timed_out", "killed",
3507
+ ]);
3508
+
3509
+ /**
3510
+ * Stable agent identity for Runtime V2.
3511
+ *
3512
+ * This replaces TMUX session names as the canonical identifier for a
3513
+ * spawned agent process. The string format is deliberately compatible
3514
+ * with existing naming conventions (e.g., "orch-henrylach-lane-1-worker")
3515
+ * to minimize churn in supervisor tools, dashboard, and mailbox addressing.
3516
+ *
3517
+ * The key semantic change: this is a **runtime process ID**, not a terminal
3518
+ * session name. Code must not assume `tmuxHasSession(agentId)` is valid.
3519
+ *
3520
+ * @since TP-102
3521
+ */
3522
+ export type RuntimeAgentId = string;
3523
+
3524
+ /**
3525
+ * Explicit packet-path authority for a task execution.
3526
+ *
3527
+ * In workspace mode, the packet home (where PROMPT.md / STATUS.md / .DONE
3528
+ * live) may differ from the execution cwd (the active segment repo worktree).
3529
+ * Runtime V2 requires these paths to be resolved explicitly and passed
3530
+ * through the execution chain — never inferred from cwd.
3531
+ *
3532
+ * In repo mode (single repo), all paths point into the same filesystem tree.
3533
+ * The contract is the same; the values just happen to be co-located.
3534
+ *
3535
+ * @since TP-102
3536
+ */
3537
+ export interface PacketPaths {
3538
+ /** Absolute path to the task's PROMPT.md */
3539
+ promptPath: string;
3540
+ /** Absolute path to the task's STATUS.md */
3541
+ statusPath: string;
3542
+ /** Absolute path to the task's .DONE marker */
3543
+ donePath: string;
3544
+ /** Absolute path to the task's .reviews/ directory */
3545
+ reviewsDir: string;
3546
+ /** Absolute path to the task folder containing packet files */
3547
+ taskFolder: string;
3548
+ }
3549
+
3550
+ /**
3551
+ * Resolve a PacketPaths object from a task folder path.
3552
+ *
3553
+ * This is a pure helper — it does not check whether the files exist.
3554
+ * Consumers should use this to build authoritative paths from an
3555
+ * already-resolved task folder location.
3556
+ *
3557
+ * @param taskFolder - Absolute path to the task folder
3558
+ * @returns Complete PacketPaths with all derived paths
3559
+ *
3560
+ * @since TP-102
3561
+ */
3562
+ export function resolvePacketPaths(taskFolder: string): PacketPaths {
3563
+ return {
3564
+ promptPath: `${taskFolder}/PROMPT.md`,
3565
+ statusPath: `${taskFolder}/STATUS.md`,
3566
+ donePath: `${taskFolder}/.DONE`,
3567
+ reviewsDir: `${taskFolder}/.reviews`,
3568
+ taskFolder,
3569
+ };
3570
+ }
3571
+
3572
+ /**
3573
+ * A single execution unit in Runtime V2.
3574
+ *
3575
+ * Represents one unit of work to be executed in one lane: either a whole
3576
+ * task (repo mode / single-segment workspace mode) or one segment of a
3577
+ * multi-repo task.
3578
+ *
3579
+ * This is the contract between the engine (which decides what to run) and
3580
+ * the lane-runner (which runs it). It carries everything the lane-runner
3581
+ * needs without requiring it to re-derive paths from cwd or session state.
3582
+ *
3583
+ * @since TP-102
3584
+ */
3585
+ export interface ExecutionUnit {
3586
+ /** Unique identifier: taskId for whole-task units, `taskId::repoId` for segments */
3587
+ id: string;
3588
+ /** Parent task identifier */
3589
+ taskId: string;
3590
+ /** Segment identifier (null for whole-task execution) */
3591
+ segmentId: string | null;
3592
+ /** Repo ID where execution happens (cwd of the worker) */
3593
+ executionRepoId: string;
3594
+ /** Repo ID that owns the packet files (may differ in workspace mode) */
3595
+ packetHomeRepoId: string;
3596
+ /** Absolute path to the execution worktree */
3597
+ worktreePath: string;
3598
+ /** Authoritative packet file paths */
3599
+ packet: PacketPaths;
3600
+ /** Full parsed task metadata */
3601
+ task: ParsedTask;
3602
+ }
3603
+
3604
+ /**
3605
+ * Per-agent process manifest for the runtime registry.
3606
+ *
3607
+ * Written by the agent's parent process (lane-runner or engine) before
3608
+ * the agent is considered visible. Updated on status transitions and
3609
+ * cleaned up on batch completion.
3610
+ *
3611
+ * Replaces TMUX session discovery as the source of truth for agent
3612
+ * liveness, identity, and attribution.
3613
+ *
3614
+ * File location: `.pi/runtime/{batchId}/agents/{agentId}/manifest.json`
3615
+ *
3616
+ * @since TP-102
3617
+ */
3618
+ export interface RuntimeAgentManifest {
3619
+ /** Batch this agent belongs to */
3620
+ batchId: string;
3621
+ /** Stable agent identity (e.g., "orch-henrylach-lane-1-worker") */
3622
+ agentId: RuntimeAgentId;
3623
+ /** Agent role */
3624
+ role: RuntimeAgentRole;
3625
+ /** Lane number (null for merge agents) */
3626
+ laneNumber: number | null;
3627
+ /** Current task ID being executed (null before first assignment) */
3628
+ taskId: string | null;
3629
+ /** Repo ID the agent is operating in */
3630
+ repoId: string;
3631
+ /** OS process ID of the agent host process */
3632
+ pid: number;
3633
+ /** OS process ID of the parent (lane-runner or engine) */
3634
+ parentPid: number;
3635
+ /** Epoch ms when the agent was spawned */
3636
+ startedAt: number;
3637
+ /** Current lifecycle status */
3638
+ status: RuntimeAgentStatus;
3639
+ /** Absolute path to the agent's working directory */
3640
+ cwd: string;
3641
+ /** Authoritative packet paths (null for merge agents or pre-assignment) */
3642
+ packet: PacketPaths | null;
3643
+ }
3644
+
3645
+ /**
3646
+ * Batch-level runtime registry snapshot.
3647
+ *
3648
+ * Contains all active and recently-exited agents for one batch.
3649
+ * The authoritative source of truth for which agents exist, replacing
3650
+ * TMUX session discovery.
3651
+ *
3652
+ * File location: `.pi/runtime/{batchId}/registry.json`
3653
+ *
3654
+ * @since TP-102
3655
+ */
3656
+ export interface RuntimeRegistry {
3657
+ /** Batch ID this registry belongs to */
3658
+ batchId: string;
3659
+ /** Epoch ms when the registry was last updated */
3660
+ updatedAt: number;
3661
+ /** All known agents (keyed by agentId for fast lookup in JSON form) */
3662
+ agents: Record<RuntimeAgentId, RuntimeAgentManifest>;
3663
+ }
3664
+
3665
+ /**
3666
+ * Lane execution snapshot emitted by the lane-runner.
3667
+ *
3668
+ * Replaces the current `lane-state-*.json` sidecar with a first-class
3669
+ * contract. Written by the lane-runner directly (not by tailing sidecar
3670
+ * files from a sibling process).
3671
+ *
3672
+ * File location: `.pi/runtime/{batchId}/lanes/lane-{N}.json`
3673
+ *
3674
+ * @since TP-102
3675
+ */
3676
+ export interface RuntimeLaneSnapshot {
3677
+ /** Batch this lane belongs to */
3678
+ batchId: string;
3679
+ /** Lane number (1-indexed) */
3680
+ laneNumber: number;
3681
+ /** Lane identifier (e.g., "lane-1") */
3682
+ laneId: string;
3683
+ /** Repo ID this lane targets */
3684
+ repoId: string;
3685
+ /** Current task ID being executed */
3686
+ taskId: string | null;
3687
+ /** Current segment ID (null for whole-task execution) */
3688
+ segmentId: string | null;
3689
+ /** Lane execution status */
3690
+ status: "idle" | "running" | "complete" | "failed";
3691
+ /** Worker agent snapshot (null when no worker is active) */
3692
+ worker: RuntimeAgentTelemetrySnapshot | null;
3693
+ /** Reviewer agent snapshot (null when no reviewer is active) */
3694
+ reviewer: RuntimeAgentTelemetrySnapshot | null;
3695
+ /** Task progress derived from STATUS.md */
3696
+ progress: RuntimeTaskProgress | null;
3697
+ /** Epoch ms when this snapshot was last updated */
3698
+ updatedAt: number;
3699
+ }
3700
+
3701
+ /**
3702
+ * Telemetry snapshot for a single agent within a lane.
3703
+ *
3704
+ * @since TP-102
3705
+ */
3706
+ export interface RuntimeAgentTelemetrySnapshot {
3707
+ /** Agent ID */
3708
+ agentId: RuntimeAgentId;
3709
+ /** Agent lifecycle status */
3710
+ status: RuntimeAgentStatus;
3711
+ /** Elapsed time in milliseconds */
3712
+ elapsedMs: number;
3713
+ /** Number of tool calls made */
3714
+ toolCalls: number;
3715
+ /** Context window utilization percentage (0-100) */
3716
+ contextPct: number;
3717
+ /** Cumulative cost in USD */
3718
+ costUsd: number;
3719
+ /** Last tool call description */
3720
+ lastTool: string;
3721
+ /** Input tokens consumed */
3722
+ inputTokens: number;
3723
+ /** Output tokens generated */
3724
+ outputTokens: number;
3725
+ /** Cache read tokens */
3726
+ cacheReadTokens: number;
3727
+ /** Cache write tokens */
3728
+ cacheWriteTokens: number;
3729
+ }
3730
+
3731
+ /**
3732
+ * Task progress derived from STATUS.md parsing.
3733
+ *
3734
+ * @since TP-102
3735
+ */
3736
+ export interface RuntimeTaskProgress {
3737
+ /** Human-readable current step label */
3738
+ currentStep: string;
3739
+ /** Number of checked checkboxes across all steps */
3740
+ checked: number;
3741
+ /** Total number of checkboxes across all steps */
3742
+ total: number;
3743
+ /** Current worker iteration number */
3744
+ iteration: number;
3745
+ /** Number of reviews performed */
3746
+ reviews: number;
3747
+ }
3748
+
3749
+ /**
3750
+ * Normalized event emitted by an agent host.
3751
+ *
3752
+ * The canonical telemetry/conversation event shape for Runtime V2.
3753
+ * Agent hosts write these to per-agent event logs and stream them
3754
+ * to their parent process via IPC.
3755
+ *
3756
+ * File location: `.pi/runtime/{batchId}/agents/{agentId}/events.jsonl`
3757
+ *
3758
+ * @since TP-102
3759
+ */
3760
+ export interface RuntimeAgentEvent {
3761
+ /** Batch ID */
3762
+ batchId: string;
3763
+ /** Agent that produced this event */
3764
+ agentId: RuntimeAgentId;
3765
+ /** Agent role */
3766
+ role: RuntimeAgentRole;
3767
+ /** Lane number (null for merge agents) */
3768
+ laneNumber: number | null;
3769
+ /** Task ID being executed when the event was produced */
3770
+ taskId: string | null;
3771
+ /** Repo ID */
3772
+ repoId: string;
3773
+ /** Epoch ms timestamp */
3774
+ ts: number;
3775
+ /** Event type */
3776
+ type: RuntimeAgentEventType;
3777
+ /** Event-specific payload */
3778
+ payload: Record<string, unknown>;
3779
+ }
3780
+
3781
+ /**
3782
+ * Normalized event types for the Runtime V2 agent event stream.
3783
+ *
3784
+ * @since TP-102
3785
+ */
3786
+ export type RuntimeAgentEventType =
3787
+ // Lifecycle
3788
+ | "agent_started"
3789
+ | "agent_exited"
3790
+ | "agent_killed"
3791
+ | "agent_crashed"
3792
+ | "agent_timeout"
3793
+ // Conversation
3794
+ | "prompt_sent"
3795
+ | "assistant_message"
3796
+ | "tool_call"
3797
+ | "tool_result"
3798
+ // Telemetry
3799
+ | "usage_delta"
3800
+ | "context_usage"
3801
+ | "retry_started"
3802
+ | "retry_finished"
3803
+ | "compaction_started"
3804
+ | "compaction_finished"
3805
+ // Steering
3806
+ | "message_delivered"
3807
+ | "reply_sent"
3808
+ | "escalation_sent"
3809
+ // Review / bridge
3810
+ | "review_requested"
3811
+ | "review_completed"
3812
+ | "review_failed";
3813
+
3814
+ // ── Runtime V2 Path Helpers (TP-102) ─────────────────────────────────
3815
+
3816
+ /**
3817
+ * Resolve the root directory for Runtime V2 artifacts for a given batch.
3818
+ *
3819
+ * @param stateRoot - Root directory containing .pi/ (workspace root or repo root)
3820
+ * @param batchId - Batch identifier
3821
+ * @returns Absolute path: `{stateRoot}/.pi/runtime/{batchId}/`
3822
+ *
3823
+ * @since TP-102
3824
+ */
3825
+ export function runtimeRoot(stateRoot: string, batchId: string): string {
3826
+ return `${stateRoot}/.pi/runtime/${batchId}`;
3827
+ }
3828
+
3829
+ /**
3830
+ * Resolve the path for a specific agent's runtime directory.
3831
+ *
3832
+ * @param stateRoot - Root directory containing .pi/
3833
+ * @param batchId - Batch identifier
3834
+ * @param agentId - Runtime agent identifier
3835
+ * @returns Absolute path: `{stateRoot}/.pi/runtime/{batchId}/agents/{agentId}/`
3836
+ *
3837
+ * @since TP-102
3838
+ */
3839
+ export function runtimeAgentDir(stateRoot: string, batchId: string, agentId: RuntimeAgentId): string {
3840
+ return `${stateRoot}/.pi/runtime/${batchId}/agents/${agentId}`;
3841
+ }
3842
+
3843
+ /**
3844
+ * Resolve the path for a specific agent's manifest file.
3845
+ *
3846
+ * @since TP-102
3847
+ */
3848
+ export function runtimeManifestPath(stateRoot: string, batchId: string, agentId: RuntimeAgentId): string {
3849
+ return `${runtimeAgentDir(stateRoot, batchId, agentId)}/manifest.json`;
3850
+ }
3851
+
3852
+ /**
3853
+ * Resolve the path for a specific agent's event log.
3854
+ *
3855
+ * @since TP-102
3856
+ */
3857
+ export function runtimeAgentEventsPath(stateRoot: string, batchId: string, agentId: RuntimeAgentId): string {
3858
+ return `${runtimeAgentDir(stateRoot, batchId, agentId)}/events.jsonl`;
3859
+ }
3860
+
3861
+ /**
3862
+ * Resolve the path for a lane snapshot file.
3863
+ *
3864
+ * @since TP-102
3865
+ */
3866
+ export function runtimeLaneSnapshotPath(stateRoot: string, batchId: string, laneNumber: number): string {
3867
+ return `${stateRoot}/.pi/runtime/${batchId}/lanes/lane-${laneNumber}.json`;
3868
+ }
3869
+
3870
+ /**
3871
+ * Resolve the path for the batch runtime registry.
3872
+ *
3873
+ * @since TP-102
3874
+ */
3875
+ export function runtimeRegistryPath(stateRoot: string, batchId: string): string {
3876
+ return `${stateRoot}/.pi/runtime/${batchId}/registry.json`;
3877
+ }
3878
+
3879
+ /**
3880
+ * Build a canonical RuntimeAgentId from components.
3881
+ *
3882
+ * Produces IDs compatible with the existing naming convention
3883
+ * (e.g., "orch-henrylach-lane-1-worker") while semantically
3884
+ * decoupling them from TMUX session names.
3885
+ *
3886
+ * @param prefix - Operator/batch prefix (e.g., "orch-henrylach")
3887
+ * @param laneNumber - Lane number (null for merge agents)
3888
+ * @param role - Agent role
3889
+ * @param mergeIndex - Merge wave index (only for merge agents)
3890
+ * @returns Canonical agent ID string
3891
+ *
3892
+ * @since TP-102
3893
+ */
3894
+ export function buildRuntimeAgentId(
3895
+ prefix: string,
3896
+ laneNumber: number | null,
3897
+ role: RuntimeAgentRole,
3898
+ mergeIndex?: number,
3899
+ ): RuntimeAgentId {
3900
+ if (role === "merger" && mergeIndex != null) {
3901
+ return `${prefix}-merge-${mergeIndex}`;
3902
+ }
3903
+ if (role === "lane-runner" && laneNumber != null) {
3904
+ return `${prefix}-lane-${laneNumber}`;
3905
+ }
3906
+ if (laneNumber != null) {
3907
+ return `${prefix}-lane-${laneNumber}-${role}`;
3908
+ }
3909
+ return `${prefix}-${role}`;
3910
+ }
3911
+
3912
+ /**
3913
+ * Validate that a RuntimeAgentManifest has required fields and sane values.
3914
+ *
3915
+ * Returns an array of validation error strings (empty = valid).
3916
+ *
3917
+ * @since TP-102
3918
+ */
3919
+ export function validateAgentManifest(manifest: unknown): string[] {
3920
+ const errors: string[] = [];
3921
+ if (!manifest || typeof manifest !== "object") {
3922
+ return ["manifest must be a non-null object"];
3923
+ }
3924
+ const m = manifest as Record<string, unknown>;
3925
+
3926
+ if (typeof m.batchId !== "string" || !m.batchId) errors.push("batchId must be a non-empty string");
3927
+ if (typeof m.agentId !== "string" || !m.agentId) errors.push("agentId must be a non-empty string");
3928
+ if (typeof m.role !== "string") errors.push("role must be a string");
3929
+ else {
3930
+ const validRoles: ReadonlySet<string> = new Set(["worker", "reviewer", "merger", "lane-runner"]);
3931
+ if (!validRoles.has(m.role as string)) errors.push(`role must be one of: ${[...validRoles].join(", ")}`);
3932
+ }
3933
+ if (typeof m.pid !== "number" || !Number.isFinite(m.pid) || m.pid <= 0) errors.push("pid must be a positive finite number");
3934
+ if (typeof m.parentPid !== "number" || !Number.isFinite(m.parentPid) || m.parentPid <= 0) errors.push("parentPid must be a positive finite number");
3935
+ if (typeof m.startedAt !== "number" || !Number.isFinite(m.startedAt)) errors.push("startedAt must be a finite number");
3936
+ if (typeof m.status !== "string") errors.push("status must be a string");
3937
+ else {
3938
+ const validStatuses: ReadonlySet<string> = new Set(["spawning", "running", "wrapping_up", "exited", "crashed", "timed_out", "killed"]);
3939
+ if (!validStatuses.has(m.status as string)) errors.push(`status must be one of: ${[...validStatuses].join(", ")}`);
3940
+ }
3941
+ if (typeof m.cwd !== "string" || !m.cwd) errors.push("cwd must be a non-empty string");
3942
+ if (typeof m.repoId !== "string") errors.push("repoId must be a string");
3943
+
3944
+ return errors;
3945
+ }
3946
+
3947
+ /**
3948
+ * Validate that a PacketPaths object has all required fields.
3949
+ *
3950
+ * Returns an array of validation error strings (empty = valid).
3951
+ *
3952
+ * @since TP-102
3953
+ */
3954
+ export function validatePacketPaths(packet: unknown): string[] {
3955
+ const errors: string[] = [];
3956
+ if (!packet || typeof packet !== "object") {
3957
+ return ["packet must be a non-null object"];
3958
+ }
3959
+ const p = packet as Record<string, unknown>;
3960
+
3961
+ for (const field of ["promptPath", "statusPath", "donePath", "reviewsDir", "taskFolder"] as const) {
3962
+ if (typeof p[field] !== "string" || !(p[field] as string)) {
3963
+ errors.push(`${field} must be a non-empty string`);
3964
+ }
3965
+ }
3966
+
3967
+ return errors;
3968
+ }
3969
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.22.17",
3
+ "version": "0.23.0",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",