pi-herdr-agents 3.0.1 → 3.0.3
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/AGENTS.md +2 -0
- package/CHANGELOG.md +40 -1
- package/CONTEXT.md +14 -3
- package/README.md +29 -9
- package/docs/README.md +2 -1
- package/docs/adr/0011-explicit-worktree-cleanup.md +1 -1
- package/docs/adr/0015-report-opened-primary-workspace.md +121 -0
- package/docs/worktree-subagents.md +7 -3
- package/maestro/adapters/pi/activity-file.ts +11 -1
- package/maestro/adapters/pi/launch.ts +29 -15
- package/maestro/adapters/pi/process-identity.ts +38 -0
- package/maestro/core/activity.ts +1 -1
- package/maestro/core/opened-primary-workspace.ts +108 -0
- package/maestro/core/supervision.ts +38 -13
- package/maestro/core/surface-provider.ts +22 -0
- package/maestro/core/wake.ts +37 -0
- package/maestro/core/worktree-cleanup.ts +52 -1
- package/maestro/core/worktree.ts +2 -0
- package/maestro/runtime/worktree-operations.ts +16 -2
- package/maestro/surfaces/fake/fake-surface-provider.ts +8 -0
- package/maestro/surfaces/herdr/herdr-surface-provider.ts +12 -1
- package/maestro/surfaces/herdr/herdr.ts +345 -49
- package/maestro/surfaces/herdr/terminal.ts +0 -10
- package/package.json +1 -1
- package/pi-extension/subagents/index.ts +35 -16
- package/skills/pi-herdr-agents/SKILL.md +6 -1
|
@@ -3,10 +3,19 @@ import { promisify } from "node:util";
|
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { resolve, relative, isAbsolute, sep } from "node:path";
|
|
5
5
|
import {
|
|
6
|
+
isBoolean,
|
|
6
7
|
isFiniteNumber,
|
|
8
|
+
isNonEmptyString,
|
|
7
9
|
isPlainObject,
|
|
8
10
|
isString,
|
|
9
11
|
} from "../../core/config/type-guards.ts";
|
|
12
|
+
import {
|
|
13
|
+
isCompletePrimaryWorkspaceClaim,
|
|
14
|
+
rememberOpenedPrimaryWorkspace,
|
|
15
|
+
type OpenedPrimaryWorkspaceReport,
|
|
16
|
+
type PrimaryWorkspaceClaim,
|
|
17
|
+
type ReleasedPrimaryWorkspaceClaim,
|
|
18
|
+
} from "../../core/opened-primary-workspace.ts";
|
|
10
19
|
import { WorktreeProvisioningError } from "../../core/surface-provider.ts";
|
|
11
20
|
|
|
12
21
|
const execFileAsync = promisify(execFile);
|
|
@@ -83,6 +92,8 @@ export interface HerdrWorktreeSurface {
|
|
|
83
92
|
branch: string;
|
|
84
93
|
workspaceId: string;
|
|
85
94
|
paneId: string;
|
|
95
|
+
/** Primary-workspace snapshot failures; the launch still succeeds. */
|
|
96
|
+
diagnostics?: string[];
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
function extractHerdrWorktree(output: string): HerdrWorktreeSurface {
|
|
@@ -536,15 +547,181 @@ function retainWorktreeTab(
|
|
|
536
547
|
return worktree;
|
|
537
548
|
}
|
|
538
549
|
|
|
550
|
+
/** Each launch snapshot: `worktree list` before and after create, and the opened workspace's `pane list`. */
|
|
551
|
+
export const OPENED_PRIMARY_SNAPSHOT_TIMEOUT_MS = 3_000;
|
|
552
|
+
export const OPENED_PRIMARY_SNAPSHOT_CALLS = 3;
|
|
553
|
+
/** Synchronous launch blocking if every snapshot runs to its timeout. */
|
|
554
|
+
export const OPENED_PRIMARY_SNAPSHOT_WORST_CASE_MS =
|
|
555
|
+
OPENED_PRIMARY_SNAPSHOT_CALLS * OPENED_PRIMARY_SNAPSHOT_TIMEOUT_MS;
|
|
556
|
+
|
|
557
|
+
/** Each asynchronous removal read: `worktree list`, `workspace get`, `pane list`. */
|
|
558
|
+
export const OPENED_PRIMARY_REPORT_TIMEOUT_MS = 3_000;
|
|
559
|
+
export const OPENED_PRIMARY_REPORT_CALLS = 3;
|
|
560
|
+
/** Wall time added to a removal if every read runs to its timeout; the event loop is not blocked. */
|
|
561
|
+
export const OPENED_PRIMARY_REPORT_WORST_CASE_MS =
|
|
562
|
+
OPENED_PRIMARY_REPORT_CALLS * OPENED_PRIMARY_REPORT_TIMEOUT_MS;
|
|
563
|
+
|
|
564
|
+
interface HerdrWorktreeSource {
|
|
565
|
+
repoKey?: string;
|
|
566
|
+
checkoutPath?: string;
|
|
567
|
+
primaryWorkspaceId?: string;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
type SnapshotPhase = "before create" | "after create" | "opened workspace";
|
|
571
|
+
|
|
572
|
+
function snapshotFailure(
|
|
573
|
+
phase: SnapshotPhase,
|
|
574
|
+
error: any,
|
|
575
|
+
diagnostics: string[],
|
|
576
|
+
): void {
|
|
577
|
+
diagnostics.push(
|
|
578
|
+
`Primary workspace snapshot (${phase}) failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function parseHerdrWorktreeSource(output: string): HerdrWorktreeSource {
|
|
583
|
+
const parsed = parseHerdrJson(output);
|
|
584
|
+
if (parsed?.result?.type !== "worktree_list") {
|
|
585
|
+
throw new Error("Unexpected herdr worktree list output");
|
|
586
|
+
}
|
|
587
|
+
const source = parsed.result.source;
|
|
588
|
+
const info: HerdrWorktreeSource = {};
|
|
589
|
+
if (!isPlainObject(source)) return info;
|
|
590
|
+
if (isNonEmptyString(source.repo_key)) info.repoKey = source.repo_key;
|
|
591
|
+
if (isNonEmptyString(source.source_checkout_path))
|
|
592
|
+
info.checkoutPath = source.source_checkout_path;
|
|
593
|
+
if (isNonEmptyString(source.source_workspace_id))
|
|
594
|
+
info.primaryWorkspaceId = source.source_workspace_id;
|
|
595
|
+
return info;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function readHerdrWorktreeSource(
|
|
599
|
+
cwd: string,
|
|
600
|
+
timeout: number,
|
|
601
|
+
phase: SnapshotPhase,
|
|
602
|
+
diagnostics: string[],
|
|
603
|
+
): HerdrWorktreeSource | undefined {
|
|
604
|
+
try {
|
|
605
|
+
return parseHerdrWorktreeSource(
|
|
606
|
+
herdrExec(["worktree", "list", "--cwd", cwd], timeout),
|
|
607
|
+
);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
snapshotFailure(phase, error, diagnostics);
|
|
610
|
+
return undefined;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
interface ClaimedPane {
|
|
615
|
+
paneId: string;
|
|
616
|
+
terminalId?: string;
|
|
617
|
+
cwd?: string;
|
|
618
|
+
foregroundCwd?: string;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function parseClaimedPanes(output: string, workspaceId: string): ClaimedPane[] {
|
|
622
|
+
const parsed = parseHerdrJson(output);
|
|
623
|
+
if (
|
|
624
|
+
parsed?.result?.type !== "pane_list" ||
|
|
625
|
+
!Array.isArray(parsed.result.panes)
|
|
626
|
+
) {
|
|
627
|
+
throw new Error("Unexpected herdr pane list output");
|
|
628
|
+
}
|
|
629
|
+
const panes: ClaimedPane[] = [];
|
|
630
|
+
for (const pane of parsed.result.panes) {
|
|
631
|
+
if (!isPlainObject(pane) || pane.workspace_id !== workspaceId) continue;
|
|
632
|
+
if (!isNonEmptyString(pane.pane_id)) continue;
|
|
633
|
+
const record: ClaimedPane = { paneId: pane.pane_id };
|
|
634
|
+
if (isNonEmptyString(pane.terminal_id))
|
|
635
|
+
record.terminalId = pane.terminal_id;
|
|
636
|
+
if (isNonEmptyString(pane.cwd)) record.cwd = pane.cwd;
|
|
637
|
+
if (isNonEmptyString(pane.foreground_cwd))
|
|
638
|
+
record.foregroundCwd = pane.foreground_cwd;
|
|
639
|
+
panes.push(record);
|
|
640
|
+
}
|
|
641
|
+
return panes;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function readPrimaryTerminalId(
|
|
645
|
+
workspaceId: string,
|
|
646
|
+
timeout: number,
|
|
647
|
+
diagnostics: string[],
|
|
648
|
+
): string | undefined {
|
|
649
|
+
try {
|
|
650
|
+
const panes = parseClaimedPanes(
|
|
651
|
+
herdrExec(["pane", "list", "--workspace", workspaceId], timeout),
|
|
652
|
+
workspaceId,
|
|
653
|
+
);
|
|
654
|
+
return panes.length === 1 ? panes[0].terminalId : undefined;
|
|
655
|
+
} catch (error) {
|
|
656
|
+
snapshotFailure("opened workspace", error, diagnostics);
|
|
657
|
+
return undefined;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function openedPrimaryClaim(
|
|
662
|
+
source: HerdrWorktreeSource | undefined,
|
|
663
|
+
timeout: number,
|
|
664
|
+
diagnostics: string[],
|
|
665
|
+
): PrimaryWorkspaceClaim | undefined {
|
|
666
|
+
if (!source?.primaryWorkspaceId || !source.repoKey || !source.checkoutPath)
|
|
667
|
+
return undefined;
|
|
668
|
+
const terminalId = readPrimaryTerminalId(
|
|
669
|
+
source.primaryWorkspaceId,
|
|
670
|
+
timeout,
|
|
671
|
+
diagnostics,
|
|
672
|
+
);
|
|
673
|
+
if (!terminalId) return undefined;
|
|
674
|
+
const claim: PrimaryWorkspaceClaim = {
|
|
675
|
+
workspaceId: source.primaryWorkspaceId,
|
|
676
|
+
repoKey: source.repoKey,
|
|
677
|
+
terminalId,
|
|
678
|
+
checkoutPath: source.checkoutPath,
|
|
679
|
+
};
|
|
680
|
+
return isCompletePrimaryWorkspaceClaim(claim) ? claim : undefined;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function withDiagnostics(
|
|
684
|
+
worktree: HerdrWorktreeSurface,
|
|
685
|
+
diagnostics: string[],
|
|
686
|
+
): HerdrWorktreeSurface {
|
|
687
|
+
return diagnostics.length ? { ...worktree, diagnostics } : worktree;
|
|
688
|
+
}
|
|
689
|
+
|
|
539
690
|
export function createHerdrWorktree(
|
|
540
691
|
name: string,
|
|
541
692
|
cwd: string,
|
|
542
693
|
branch: string,
|
|
543
694
|
base: string,
|
|
695
|
+
snapshotTimeoutMs = OPENED_PRIMARY_SNAPSHOT_TIMEOUT_MS,
|
|
544
696
|
): HerdrWorktreeSurface {
|
|
697
|
+
// A failed before-snapshot claims nothing and skips the later snapshots.
|
|
698
|
+
// Herdr does not report which workspace create opened, so a workspace that
|
|
699
|
+
// appears during a long checkout can be misattributed. Removal only
|
|
700
|
+
// suggests closing it; it never closes the workspace.
|
|
701
|
+
const diagnostics: string[] = [];
|
|
702
|
+
const before = readHerdrWorktreeSource(
|
|
703
|
+
cwd,
|
|
704
|
+
snapshotTimeoutMs,
|
|
705
|
+
"before create",
|
|
706
|
+
diagnostics,
|
|
707
|
+
);
|
|
545
708
|
const output = herdrExec(buildWorktreeCreateArgs(name, cwd, branch, base));
|
|
709
|
+
const after =
|
|
710
|
+
before && !before.primaryWorkspaceId
|
|
711
|
+
? readHerdrWorktreeSource(
|
|
712
|
+
cwd,
|
|
713
|
+
snapshotTimeoutMs,
|
|
714
|
+
"after create",
|
|
715
|
+
diagnostics,
|
|
716
|
+
)
|
|
717
|
+
: undefined;
|
|
718
|
+
const claim = openedPrimaryClaim(after, snapshotTimeoutMs, diagnostics);
|
|
719
|
+
if (claim) rememberOpenedPrimaryWorkspace(claim);
|
|
546
720
|
try {
|
|
547
|
-
return
|
|
721
|
+
return withDiagnostics(
|
|
722
|
+
retainWorktreeTab(extractHerdrWorktree(output), output),
|
|
723
|
+
diagnostics,
|
|
724
|
+
);
|
|
548
725
|
} catch (parseError) {
|
|
549
726
|
let recovered: HerdrWorktreeSurface | HerdrWorktreeInfo | undefined;
|
|
550
727
|
try {
|
|
@@ -553,7 +730,7 @@ export function createHerdrWorktree(
|
|
|
553
730
|
throw parseError;
|
|
554
731
|
}
|
|
555
732
|
if (recovered?.workspaceId && "paneId" in recovered)
|
|
556
|
-
return retainWorktreeTab(recovered, output);
|
|
733
|
+
return withDiagnostics(retainWorktreeTab(recovered, output), diagnostics);
|
|
557
734
|
if (recovered) {
|
|
558
735
|
throw new HerdrWorktreeCreateError(
|
|
559
736
|
`Herdr created branch ${branch}, but its workspace response was incomplete`,
|
|
@@ -564,6 +741,172 @@ export function createHerdrWorktree(
|
|
|
564
741
|
}
|
|
565
742
|
}
|
|
566
743
|
|
|
744
|
+
interface DecodedPrimaryWorkspace {
|
|
745
|
+
label?: string;
|
|
746
|
+
focused?: boolean;
|
|
747
|
+
tabCount?: number;
|
|
748
|
+
linked?: boolean;
|
|
749
|
+
repoKey?: string;
|
|
750
|
+
repoName?: string;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function decodePrimaryWorkspace(
|
|
754
|
+
value: any,
|
|
755
|
+
): DecodedPrimaryWorkspace | undefined {
|
|
756
|
+
if (!isPlainObject(value)) return undefined;
|
|
757
|
+
const decoded: DecodedPrimaryWorkspace = {};
|
|
758
|
+
if (isNonEmptyString(value.label)) decoded.label = value.label;
|
|
759
|
+
if (isBoolean(value.focused)) decoded.focused = value.focused;
|
|
760
|
+
if (isFiniteNumber(value.tab_count)) decoded.tabCount = value.tab_count;
|
|
761
|
+
if (!isPlainObject(value.worktree)) return decoded;
|
|
762
|
+
if (isBoolean(value.worktree.is_linked_worktree))
|
|
763
|
+
decoded.linked = value.worktree.is_linked_worktree;
|
|
764
|
+
if (isNonEmptyString(value.worktree.repo_key))
|
|
765
|
+
decoded.repoKey = value.worktree.repo_key;
|
|
766
|
+
if (isNonEmptyString(value.worktree.repo_name))
|
|
767
|
+
decoded.repoName = value.worktree.repo_name;
|
|
768
|
+
return decoded;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function workspaceLooksUntouched(
|
|
772
|
+
workspace: DecodedPrimaryWorkspace | undefined,
|
|
773
|
+
repoKey: string,
|
|
774
|
+
): boolean {
|
|
775
|
+
return (
|
|
776
|
+
workspace?.label !== undefined &&
|
|
777
|
+
workspace.repoName !== undefined &&
|
|
778
|
+
workspace.label === workspace.repoName &&
|
|
779
|
+
workspace.focused === false &&
|
|
780
|
+
workspace.tabCount === 1 &&
|
|
781
|
+
workspace.linked === false &&
|
|
782
|
+
workspace.repoKey === repoKey
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Any open workspace in the listing other than the primary's own principal
|
|
788
|
+
* row blocks the note, including rows whose linked flag is missing.
|
|
789
|
+
*/
|
|
790
|
+
function otherWorkspaceIsOpen(listing: string, primaryId: string): boolean {
|
|
791
|
+
const worktrees = parseHerdrJson(listing)?.result?.worktrees;
|
|
792
|
+
if (!Array.isArray(worktrees)) return true;
|
|
793
|
+
return worktrees.some((row) => {
|
|
794
|
+
if (!isPlainObject(row)) return true;
|
|
795
|
+
const open = row.open_workspace_id;
|
|
796
|
+
if (open === undefined || open === null) return false;
|
|
797
|
+
return !(open === primaryId && row.is_linked_worktree === false);
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function sameCheckoutPath(left: string, right: string): boolean {
|
|
802
|
+
if (left === right) return true;
|
|
803
|
+
try {
|
|
804
|
+
return realpathSync(left) === realpathSync(right);
|
|
805
|
+
} catch {
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function releaseClaim(
|
|
811
|
+
claim: PrimaryWorkspaceClaim,
|
|
812
|
+
): ReleasedPrimaryWorkspaceClaim {
|
|
813
|
+
return {
|
|
814
|
+
workspaceId: claim.workspaceId,
|
|
815
|
+
repoKey: claim.repoKey,
|
|
816
|
+
terminalId: claim.terminalId,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
interface PrimaryInspection {
|
|
821
|
+
release: ReleasedPrimaryWorkspaceClaim[];
|
|
822
|
+
matched?: PrimaryWorkspaceClaim;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
async function inspectOpenedPrimary(
|
|
826
|
+
workspaceId: string,
|
|
827
|
+
repoKey: string,
|
|
828
|
+
named: readonly PrimaryWorkspaceClaim[],
|
|
829
|
+
): Promise<PrimaryInspection> {
|
|
830
|
+
const decoded = decodePrimaryWorkspace(
|
|
831
|
+
parseHerdrJson(
|
|
832
|
+
await herdrExecAsync(
|
|
833
|
+
["workspace", "get", workspaceId],
|
|
834
|
+
OPENED_PRIMARY_REPORT_TIMEOUT_MS,
|
|
835
|
+
),
|
|
836
|
+
)?.result?.workspace,
|
|
837
|
+
);
|
|
838
|
+
if (!workspaceLooksUntouched(decoded, repoKey)) return { release: [] };
|
|
839
|
+
const panes = parseClaimedPanes(
|
|
840
|
+
await herdrExecAsync(
|
|
841
|
+
["pane", "list", "--workspace", workspaceId],
|
|
842
|
+
OPENED_PRIMARY_REPORT_TIMEOUT_MS,
|
|
843
|
+
),
|
|
844
|
+
workspaceId,
|
|
845
|
+
);
|
|
846
|
+
if (panes.length !== 1) return { release: [] };
|
|
847
|
+
const pane = panes[0];
|
|
848
|
+
const matched = pane.terminalId
|
|
849
|
+
? named.find((claim) => claim.terminalId === pane.terminalId)
|
|
850
|
+
: undefined;
|
|
851
|
+
if (!matched) return { release: named.map(releaseClaim) };
|
|
852
|
+
const stale = named.filter((claim) => claim !== matched).map(releaseClaim);
|
|
853
|
+
if (
|
|
854
|
+
!pane.cwd ||
|
|
855
|
+
!pane.foregroundCwd ||
|
|
856
|
+
!sameCheckoutPath(pane.cwd, matched.checkoutPath) ||
|
|
857
|
+
!sameCheckoutPath(pane.foregroundCwd, matched.checkoutPath)
|
|
858
|
+
)
|
|
859
|
+
return { release: stale };
|
|
860
|
+
return { release: stale, matched };
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
export function openedPrimaryWorkspaceNote(workspaceId: string): string {
|
|
864
|
+
return `${workspaceId} appears to have been opened by worktree creation; if you haven't used it, close it with herdr workspace close ${workspaceId}`;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Suggest closing a primary workspace this process recorded at worktree
|
|
869
|
+
* creation when it still looks untouched. Never closes the workspace. At most
|
|
870
|
+
* three asynchronous reads, each bounded by OPENED_PRIMARY_REPORT_TIMEOUT_MS,
|
|
871
|
+
* all taken after the checkout is removed and immediately before the note.
|
|
872
|
+
* Returns undefined when no claim applies to the source Herdr reports.
|
|
873
|
+
*/
|
|
874
|
+
export async function reportOpenedPrimaryWorkspace(
|
|
875
|
+
cwd: string,
|
|
876
|
+
claims: readonly PrimaryWorkspaceClaim[],
|
|
877
|
+
): Promise<OpenedPrimaryWorkspaceReport | undefined> {
|
|
878
|
+
const complete = claims.filter(isCompletePrimaryWorkspaceClaim);
|
|
879
|
+
if (!complete.length) return undefined;
|
|
880
|
+
const listing = await herdrExecAsync(
|
|
881
|
+
["worktree", "list", "--cwd", cwd],
|
|
882
|
+
OPENED_PRIMARY_REPORT_TIMEOUT_MS,
|
|
883
|
+
);
|
|
884
|
+
const source = parseHerdrWorktreeSource(listing);
|
|
885
|
+
if (!source.repoKey) return undefined;
|
|
886
|
+
const repoKey = source.repoKey;
|
|
887
|
+
const own = complete.filter((claim) => claim.repoKey === repoKey);
|
|
888
|
+
if (!own.length) return undefined;
|
|
889
|
+
const id = source.primaryWorkspaceId;
|
|
890
|
+
const elsewhere = own
|
|
891
|
+
.filter((claim) => claim.workspaceId !== id)
|
|
892
|
+
.map(releaseClaim);
|
|
893
|
+
const named = own.filter((claim) => claim.workspaceId === id);
|
|
894
|
+
if (!id || !named.length || otherWorkspaceIsOpen(listing, id))
|
|
895
|
+
return { repoKey, releasedClaims: elsewhere };
|
|
896
|
+
const inspection = await inspectOpenedPrimary(id, repoKey, named);
|
|
897
|
+
if (!inspection.matched)
|
|
898
|
+
return { repoKey, releasedClaims: [...elsewhere, ...inspection.release] };
|
|
899
|
+
return {
|
|
900
|
+
note: openedPrimaryWorkspaceNote(id),
|
|
901
|
+
repoKey,
|
|
902
|
+
releasedClaims: [
|
|
903
|
+
...elsewhere,
|
|
904
|
+
...inspection.release,
|
|
905
|
+
releaseClaim(inspection.matched),
|
|
906
|
+
],
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
567
910
|
export function createHerdrSurfaceSplit(
|
|
568
911
|
name: string,
|
|
569
912
|
direction: "right" | "down",
|
|
@@ -847,52 +1190,6 @@ function isHerdrShellReady(info: HerdrPaneProcessInfo): boolean {
|
|
|
847
1190
|
);
|
|
848
1191
|
}
|
|
849
1192
|
|
|
850
|
-
function isExpectedPiProcess(
|
|
851
|
-
process: HerdrForegroundProcess,
|
|
852
|
-
sessionFile: string,
|
|
853
|
-
cwd: string,
|
|
854
|
-
): boolean {
|
|
855
|
-
const sessionIndex = process.argv?.indexOf("--session") ?? -1;
|
|
856
|
-
return (
|
|
857
|
-
(process.name === "pi" || process.argv0?.split("/").pop() === "pi") &&
|
|
858
|
-
sessionIndex >= 0 &&
|
|
859
|
-
process.argv?.[sessionIndex + 1] === sessionFile &&
|
|
860
|
-
process.cwd === cwd
|
|
861
|
-
);
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
export async function waitForHerdrPiReady(
|
|
865
|
-
surface: string,
|
|
866
|
-
sessionFile: string,
|
|
867
|
-
cwd: string,
|
|
868
|
-
options: { timeoutMs?: number; intervalMs?: number } = {},
|
|
869
|
-
): Promise<void> {
|
|
870
|
-
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
871
|
-
const intervalMs = options.intervalMs ?? 50;
|
|
872
|
-
const deadline = Date.now() + timeoutMs;
|
|
873
|
-
let lastError = "expected Pi process not observed";
|
|
874
|
-
|
|
875
|
-
while (Date.now() <= deadline) {
|
|
876
|
-
try {
|
|
877
|
-
const info = await getHerdrPaneProcessInfoAsync(surface);
|
|
878
|
-
if (
|
|
879
|
-
info.foregroundProcesses.some((process) =>
|
|
880
|
-
isExpectedPiProcess(process, sessionFile, cwd),
|
|
881
|
-
)
|
|
882
|
-
) {
|
|
883
|
-
return;
|
|
884
|
-
}
|
|
885
|
-
} catch (error) {
|
|
886
|
-
lastError = error instanceof Error ? error.message : String(error);
|
|
887
|
-
}
|
|
888
|
-
if (Date.now() >= deadline) break;
|
|
889
|
-
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
890
|
-
}
|
|
891
|
-
throw new Error(
|
|
892
|
-
`Timed out waiting for Pi session ${sessionFile} in Herdr pane ${surface}: ${lastError}`,
|
|
893
|
-
);
|
|
894
|
-
}
|
|
895
|
-
|
|
896
1193
|
export async function waitForHerdrShellReady(
|
|
897
1194
|
surface: string,
|
|
898
1195
|
options: {
|
|
@@ -1051,6 +1348,5 @@ export const __herdrTest__ = {
|
|
|
1051
1348
|
parseHerdrPaneSnapshot,
|
|
1052
1349
|
parsePaneProcessInfo,
|
|
1053
1350
|
isHerdrShellReady,
|
|
1054
|
-
isExpectedPiProcess,
|
|
1055
1351
|
withMockHerdrExec,
|
|
1056
1352
|
};
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
createHerdrWorktree,
|
|
11
11
|
focusHerdrWorkspace,
|
|
12
12
|
getHerdrPaneProcessInfo,
|
|
13
|
-
waitForHerdrPiReady,
|
|
14
13
|
waitForHerdrShellReady,
|
|
15
14
|
isHerdrAvailable,
|
|
16
15
|
isProcessAlive,
|
|
@@ -191,15 +190,6 @@ export async function waitForShellReady(
|
|
|
191
190
|
return waitForHerdrShellReady(paneId, options);
|
|
192
191
|
}
|
|
193
192
|
|
|
194
|
-
export async function waitForPiReady(
|
|
195
|
-
paneId: PaneId,
|
|
196
|
-
sessionFile: string,
|
|
197
|
-
cwd: string,
|
|
198
|
-
): Promise<void> {
|
|
199
|
-
assertTerminalAvailable();
|
|
200
|
-
return waitForHerdrPiReady(paneId, sessionFile, cwd);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
193
|
export async function waitForPaneAbsence(
|
|
204
194
|
paneId: PaneId,
|
|
205
195
|
options?: { timeoutMs?: number; intervalMs?: number },
|
package/package.json
CHANGED
|
@@ -684,6 +684,16 @@ function formatWorktreeHandoff(worktree: WorktreeHandoff): string {
|
|
|
684
684
|
return lines.join("\n");
|
|
685
685
|
}
|
|
686
686
|
|
|
687
|
+
function launchDiagnosticsText(
|
|
688
|
+
diagnostics: readonly string[] | undefined,
|
|
689
|
+
prefix: string,
|
|
690
|
+
suffix: string,
|
|
691
|
+
): string {
|
|
692
|
+
return diagnostics?.length
|
|
693
|
+
? `${prefix}Launch diagnostics: ${diagnostics.join("; ")}.${suffix}`
|
|
694
|
+
: "";
|
|
695
|
+
}
|
|
696
|
+
|
|
687
697
|
function resolveResultPresentation(
|
|
688
698
|
result: Pick<
|
|
689
699
|
SubagentResult,
|
|
@@ -1950,10 +1960,6 @@ function startWidgetRefresh() {
|
|
|
1950
1960
|
writeGlobalSlot(WIDGET_INTERVAL_KEY, widgetInterval);
|
|
1951
1961
|
}
|
|
1952
1962
|
|
|
1953
|
-
/**
|
|
1954
|
-
* Normalize each actual attempt against its invocation context. Role files may
|
|
1955
|
-
* change between attempts; the validated runtime candidates do not.
|
|
1956
|
-
*/
|
|
1957
1963
|
function normalizePiAttempt(
|
|
1958
1964
|
params: typeof SubagentParams.static,
|
|
1959
1965
|
ctx: {
|
|
@@ -1975,6 +1981,7 @@ function normalizePiAttempt(
|
|
|
1975
1981
|
options: {
|
|
1976
1982
|
id: string;
|
|
1977
1983
|
controlTaskId: string;
|
|
1984
|
+
origin: Pick<PiLaunchSnapshot["parent"], "cwd" | "invocationCwd">;
|
|
1978
1985
|
},
|
|
1979
1986
|
): PiAttemptSnapshot {
|
|
1980
1987
|
const agentDefs = params.agent
|
|
@@ -1999,15 +2006,19 @@ function normalizePiAttempt(
|
|
|
1999
2006
|
const taskId = randomUUID();
|
|
2000
2007
|
const parentSessionFile = ctx.sessionManager.getSessionFile();
|
|
2001
2008
|
if (!parentSessionFile) throw new Error("No session file");
|
|
2009
|
+
const snapshot = launchSnapshot(ctx, parentThinking);
|
|
2002
2010
|
|
|
2003
2011
|
return {
|
|
2004
|
-
snapshot:
|
|
2012
|
+
snapshot: {
|
|
2013
|
+
...snapshot,
|
|
2014
|
+
parent: { ...snapshot.parent, ...options.origin },
|
|
2015
|
+
},
|
|
2005
2016
|
task: {
|
|
2006
2017
|
id: options.controlTaskId,
|
|
2007
2018
|
name: params.name,
|
|
2008
2019
|
prompt: params.task,
|
|
2009
2020
|
role: params.agent ?? "",
|
|
2010
|
-
cwd:
|
|
2021
|
+
cwd: options.origin.cwd,
|
|
2011
2022
|
worktree: params.worktree ?? undefined,
|
|
2012
2023
|
session: { mode: resolveEffectiveSessionMode(params, agentDefs) },
|
|
2013
2024
|
behavior: {
|
|
@@ -2792,20 +2803,24 @@ export default function subagentsExtension(
|
|
|
2792
2803
|
);
|
|
2793
2804
|
runtime.latestCtx = ctx;
|
|
2794
2805
|
const id = randomUUID();
|
|
2795
|
-
const
|
|
2806
|
+
const attempt = {
|
|
2796
2807
|
id,
|
|
2797
2808
|
controlTaskId: id,
|
|
2798
|
-
|
|
2809
|
+
origin: { cwd: ctx.cwd, invocationCwd: process.cwd() },
|
|
2810
|
+
};
|
|
2811
|
+
const first = normalizePiAttempt(params, ctx, parentThinking, attempt);
|
|
2799
2812
|
const [plan, ...fallbacks] = runtimePlans;
|
|
2800
2813
|
if (!plan) throw new Error("No resolved runtime plans");
|
|
2801
2814
|
const input: PiLaunchInput = {
|
|
2802
2815
|
...first,
|
|
2803
2816
|
plans: [plan, ...fallbacks],
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2817
|
+
// Fallbacks outlive this tool call; session replacement invalidates ctx.
|
|
2818
|
+
prepareAttempt: () => {
|
|
2819
|
+
const live = runtime.latestCtx;
|
|
2820
|
+
if (!live)
|
|
2821
|
+
throw new Error("No live parent context for the fallback launch");
|
|
2822
|
+
return normalizePiAttempt(params, live, parentThinking, attempt);
|
|
2823
|
+
},
|
|
2809
2824
|
};
|
|
2810
2825
|
const session = runtime.session!;
|
|
2811
2826
|
const handle = await session.spawnPi(input);
|
|
@@ -2828,6 +2843,7 @@ export default function subagentsExtension(
|
|
|
2828
2843
|
(running.worktree
|
|
2829
2844
|
? ` in worktree ${running.worktree.path} on branch ${running.worktree.branch}. `
|
|
2830
2845
|
: ". ") +
|
|
2846
|
+
launchDiagnosticsText(running.worktree?.diagnostics, "", " ") +
|
|
2831
2847
|
`Do NOT generate or assume any results — you have no idea what the sub-agent will do or produce. ` +
|
|
2832
2848
|
`The results will be delivered to you automatically as a steer message when the sub-agent finishes. ` +
|
|
2833
2849
|
`Until then, move on to other work or tell the user you're waiting.`,
|
|
@@ -3457,10 +3473,13 @@ export default function subagentsExtension(
|
|
|
3457
3473
|
throw new Error("Worktree handoff did not return worktree metadata");
|
|
3458
3474
|
}
|
|
3459
3475
|
ctx.ui.notify(
|
|
3460
|
-
result.focusError
|
|
3476
|
+
(result.focusError
|
|
3461
3477
|
? `Worktree launched, but workspace focus failed: ${result.focusError}\nWorktree: ${worktree.path}`
|
|
3462
|
-
: `Worktree launched in ${worktree.path} (workspace ${worktree.workspaceId})
|
|
3463
|
-
|
|
3478
|
+
: `Worktree launched in ${worktree.path} (workspace ${worktree.workspaceId}).`) +
|
|
3479
|
+
launchDiagnosticsText(worktree.diagnostics, "\n", ""),
|
|
3480
|
+
result.focusError || worktree.diagnostics?.length
|
|
3481
|
+
? "warning"
|
|
3482
|
+
: "info",
|
|
3464
3483
|
);
|
|
3465
3484
|
} catch (error) {
|
|
3466
3485
|
ctx.ui.notify(
|
|
@@ -210,7 +210,12 @@ independent writer.
|
|
|
210
210
|
blockers. Startup does not scan it.
|
|
211
211
|
- `worktree_remove({ target, preserve? })` / `/worktree remove <target>
|
|
212
212
|
[--preserve]` is explicit, parent-only, and needs separate authorization;
|
|
213
|
-
stopping or interrupting a child never authorizes cleanup.
|
|
213
|
+
stopping or interrupting a child never authorizes cleanup. Removal never
|
|
214
|
+
closes the source repository's primary workspace. If this process's create
|
|
215
|
+
appears to have opened that workspace and it still looks untouched, the
|
|
216
|
+
result suggests `herdr workspace close <id>` if you haven't used it. Another
|
|
217
|
+
session or a restarted process says nothing about it. Primary-workspace
|
|
218
|
+
snapshot failures at launch appear as launch diagnostics. Dirty work is
|
|
214
219
|
blocked unless committed first or `preserve: true` makes a WIP commit (SHA
|
|
215
220
|
reported). Eligibility is rechecked at removal and fails closed (unknown
|
|
216
221
|
state, live holder, untracked files, conflicts, detached HEAD, locks,
|