@cleocode/caamp 2026.4.7 → 2026.4.9
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/dist/{chunk-HEAGCHKU.js → chunk-JC77OAHA.js} +790 -44
- package/dist/chunk-JC77OAHA.js.map +1 -0
- package/dist/cli.js +279 -29
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1103 -290
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-HEAGCHKU.js.map +0 -1
|
@@ -511,12 +511,78 @@ async function updateInstructionsSingleOperation(providers, content, scope = "pr
|
|
|
511
511
|
return summary;
|
|
512
512
|
}
|
|
513
513
|
|
|
514
|
+
// src/core/config/caamp-config.ts
|
|
515
|
+
var DEFAULT_EXCLUSIVITY_MODE = "auto";
|
|
516
|
+
var EXCLUSIVITY_MODE_ENV_VAR = "CAAMP_EXCLUSIVITY_MODE";
|
|
517
|
+
var programmaticOverride = null;
|
|
518
|
+
var exclusivityWarningState = {
|
|
519
|
+
piAbsentAutoWarned: false,
|
|
520
|
+
explicitNonPiAutoWarned: false
|
|
521
|
+
};
|
|
522
|
+
function hasPiAbsentAutoWarned() {
|
|
523
|
+
return exclusivityWarningState.piAbsentAutoWarned;
|
|
524
|
+
}
|
|
525
|
+
function hasExplicitNonPiAutoWarned() {
|
|
526
|
+
return exclusivityWarningState.explicitNonPiAutoWarned;
|
|
527
|
+
}
|
|
528
|
+
function markPiAbsentAutoWarned() {
|
|
529
|
+
exclusivityWarningState.piAbsentAutoWarned = true;
|
|
530
|
+
}
|
|
531
|
+
function markExplicitNonPiAutoWarned() {
|
|
532
|
+
exclusivityWarningState.explicitNonPiAutoWarned = true;
|
|
533
|
+
}
|
|
534
|
+
var PiRequiredError = class extends Error {
|
|
535
|
+
/** LAFS-stable error code identifying this failure mode. */
|
|
536
|
+
code = "E_NOT_FOUND_RESOURCE";
|
|
537
|
+
/**
|
|
538
|
+
* Construct a new {@link PiRequiredError}.
|
|
539
|
+
*
|
|
540
|
+
* @param message - Human-readable failure description; defaults to a
|
|
541
|
+
* stable string suitable for direct CLI display.
|
|
542
|
+
*/
|
|
543
|
+
constructor(message = 'caamp.exclusivityMode is set to "force-pi" but Pi is not installed. Install Pi (https://github.com/mariozechner/pi-coding-agent) or change the mode with CAAMP_EXCLUSIVITY_MODE=auto.') {
|
|
544
|
+
super(message);
|
|
545
|
+
this.name = "PiRequiredError";
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
function isExclusivityMode(value) {
|
|
549
|
+
return value === "auto" || value === "force-pi" || value === "legacy";
|
|
550
|
+
}
|
|
551
|
+
function getExclusivityMode() {
|
|
552
|
+
if (programmaticOverride !== null) {
|
|
553
|
+
return programmaticOverride;
|
|
554
|
+
}
|
|
555
|
+
const envValue = process.env[EXCLUSIVITY_MODE_ENV_VAR];
|
|
556
|
+
if (envValue !== void 0 && isExclusivityMode(envValue)) {
|
|
557
|
+
return envValue;
|
|
558
|
+
}
|
|
559
|
+
return DEFAULT_EXCLUSIVITY_MODE;
|
|
560
|
+
}
|
|
561
|
+
function setExclusivityMode(mode) {
|
|
562
|
+
programmaticOverride = mode;
|
|
563
|
+
}
|
|
564
|
+
function resetExclusivityModeOverride() {
|
|
565
|
+
programmaticOverride = null;
|
|
566
|
+
}
|
|
567
|
+
|
|
514
568
|
// src/core/harness/pi.ts
|
|
515
569
|
import { spawn } from "child_process";
|
|
516
570
|
import { existsSync as existsSync4 } from "fs";
|
|
517
|
-
import {
|
|
571
|
+
import {
|
|
572
|
+
appendFile,
|
|
573
|
+
cp as cp3,
|
|
574
|
+
mkdir as mkdir3,
|
|
575
|
+
open,
|
|
576
|
+
readdir,
|
|
577
|
+
readFile,
|
|
578
|
+
rename,
|
|
579
|
+
rm as rm3,
|
|
580
|
+
stat,
|
|
581
|
+
writeFile
|
|
582
|
+
} from "fs/promises";
|
|
518
583
|
import { homedir as homedir2 } from "os";
|
|
519
584
|
import { basename as basename2, dirname as dirname2, extname, join as join5 } from "path";
|
|
585
|
+
import { parseDocument, validateDocument } from "@cleocode/cant";
|
|
520
586
|
|
|
521
587
|
// src/core/harness/scope.ts
|
|
522
588
|
import { homedir } from "os";
|
|
@@ -626,6 +692,38 @@ async function atomicWriteJson(filePath, data) {
|
|
|
626
692
|
`, "utf8");
|
|
627
693
|
await rename(tmp, filePath);
|
|
628
694
|
}
|
|
695
|
+
var DEFAULT_TERMINATE_GRACE_MS = 5e3;
|
|
696
|
+
var STDERR_RING_BUFFER_SIZE = 100;
|
|
697
|
+
var activeSubagents = /* @__PURE__ */ new Set();
|
|
698
|
+
var orphanSweeperRegistered = false;
|
|
699
|
+
function ensureOrphanSweeperRegistered() {
|
|
700
|
+
if (orphanSweeperRegistered) return;
|
|
701
|
+
orphanSweeperRegistered = true;
|
|
702
|
+
const sweeper = () => {
|
|
703
|
+
for (const entry of activeSubagents) {
|
|
704
|
+
try {
|
|
705
|
+
entry.terminate();
|
|
706
|
+
} catch {
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
process.on("exit", sweeper);
|
|
711
|
+
}
|
|
712
|
+
function generateShortId() {
|
|
713
|
+
const ts = Date.now().toString(36);
|
|
714
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
715
|
+
return `${ts}${rand}`;
|
|
716
|
+
}
|
|
717
|
+
function readTerminateGraceFromSettings(settings, fallback) {
|
|
718
|
+
if (!isPlainObject(settings)) return fallback;
|
|
719
|
+
const piBlock = settings["pi"];
|
|
720
|
+
if (!isPlainObject(piBlock)) return fallback;
|
|
721
|
+
const subBlock = piBlock["subagent"];
|
|
722
|
+
if (!isPlainObject(subBlock)) return fallback;
|
|
723
|
+
const value = subBlock["terminateGraceMs"];
|
|
724
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return fallback;
|
|
725
|
+
return value;
|
|
726
|
+
}
|
|
629
727
|
var PiHarness = class {
|
|
630
728
|
/**
|
|
631
729
|
* Construct a harness bound to a resolved Pi provider.
|
|
@@ -716,21 +814,79 @@ ${MARKER_END}`;
|
|
|
716
814
|
await writeFile(filePath, stripped.length === 0 ? "" : `${stripped}
|
|
717
815
|
`, "utf8");
|
|
718
816
|
}
|
|
719
|
-
// ── Subagent spawn
|
|
817
|
+
// ── Subagent spawn (ADR-035 §D6) ────────────────────────────────────
|
|
720
818
|
/**
|
|
721
|
-
*
|
|
819
|
+
* Spawn a subagent through Pi's configured `spawnCommand` and return a
|
|
820
|
+
* live handle bound to the canonical streaming, attribution, and
|
|
821
|
+
* cleanup contract.
|
|
722
822
|
*
|
|
723
823
|
* @remarks
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
824
|
+
* Per ADR-035 §D6 this is the **only** sanctioned subagent spawn path
|
|
825
|
+
* in CLEO. All historical direct `child_process.spawn` callers in
|
|
826
|
+
* subagent contexts (including the `cant-bridge.ts` Pi extension and
|
|
827
|
+
* the legacy CLEO orchestrator paths) MUST migrate to this method so
|
|
828
|
+
* the contract below holds uniformly. A custom biome rule banning
|
|
829
|
+
* raw `spawn()` from subagent code is planned for v3 cleanup but is
|
|
830
|
+
* intentionally NOT enforced in v2 to keep the migration incremental.
|
|
831
|
+
*
|
|
832
|
+
* **Streaming semantics** — Pi's `--mode json` produces line-delimited
|
|
833
|
+
* JSON on stdout. The harness:
|
|
834
|
+
*
|
|
835
|
+
* - Line-buffers stdout, parses each line as JSON, and forwards a
|
|
836
|
+
* `{ kind: 'message', subagentId, lineNumber, payload }`
|
|
837
|
+
* {@link SubagentStreamEvent} via {@link SubagentSpawnOptions.onStream}.
|
|
838
|
+
* Non-parseable lines increment a warning counter (recorded in the
|
|
839
|
+
* child session as `{ type: 'raw' }`) but never crash the loop.
|
|
840
|
+
* - Line-buffers stderr separately, forwards each line as
|
|
841
|
+
* `{ kind: 'stderr', subagentId, payload: { line } }`, and stores
|
|
842
|
+
* it in a 100-line ring buffer accessible via
|
|
843
|
+
* {@link SubagentHandle.recentStderr}. Stderr is **never** injected
|
|
844
|
+
* into the parent LLM context per ADR-035 §D6.
|
|
845
|
+
* - Emits a final `{ kind: 'exit', subagentId, payload: SubagentExitResult }`
|
|
846
|
+
* when the child terminates.
|
|
847
|
+
*
|
|
848
|
+
* **Session attribution** — Every spawn produces a child session JSONL
|
|
849
|
+
* file at
|
|
850
|
+
* `~/.pi/agent/sessions/subagents/subagent-{parentSessionId}-{taskId}.jsonl`.
|
|
851
|
+
* The header line records the subagentId, taskId, and parent linkage.
|
|
852
|
+
* When {@link SubagentTask.parentSessionPath} is supplied, a
|
|
853
|
+
* {@link SubagentLinkEntry} is appended to the parent session file as
|
|
854
|
+
* a JSONL line so listing the parent surfaces its children.
|
|
855
|
+
*
|
|
856
|
+
* **Exit propagation** — {@link SubagentHandle.exitPromise} resolves
|
|
857
|
+
* with `{ code, signal, childSessionPath, durationMs }` exactly once
|
|
858
|
+
* when the child exits. The promise NEVER rejects: failure is
|
|
859
|
+
* encoded by a non-zero `code`, a non-null `signal`, or partial
|
|
860
|
+
* output preserved in the child session file.
|
|
861
|
+
*
|
|
862
|
+
* **Cleanup** — {@link SubagentHandle.terminate} sends SIGTERM, waits
|
|
863
|
+
* the configured grace window, then sends SIGKILL if the child is
|
|
864
|
+
* still alive. The grace window is sourced from
|
|
865
|
+
* {@link SubagentSpawnOptions.terminateGraceMs} when supplied,
|
|
866
|
+
* otherwise from `settings.json:pi.subagent.terminateGraceMs`,
|
|
867
|
+
* otherwise from {@link DEFAULT_TERMINATE_GRACE_MS}. A
|
|
868
|
+
* `subagent_exit` entry with reason `terminated` is appended to the
|
|
869
|
+
* child session file when cleanup runs.
|
|
729
870
|
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
871
|
+
* **Concurrency** — Use the static helpers
|
|
872
|
+
* {@link PiHarness.raceSubagents} and
|
|
873
|
+
* {@link PiHarness.settleAllSubagents} to compose `parallel: race`
|
|
874
|
+
* and `parallel: settle` constructs from CANT workflows over multiple
|
|
875
|
+
* handles.
|
|
876
|
+
*
|
|
877
|
+
* **Orphan handling** — On the first spawn the harness registers a
|
|
878
|
+
* process-wide `'exit'` handler that terminates every still-active
|
|
879
|
+
* subagent so a parent crash never strands children.
|
|
880
|
+
*
|
|
881
|
+
* Throws immediately when the provider entry is missing a
|
|
882
|
+
* `spawnCommand` so callers see configuration errors early rather
|
|
883
|
+
* than at child-exit time.
|
|
884
|
+
*
|
|
885
|
+
* @param task - Subagent task specification.
|
|
886
|
+
* @param opts - Per-call streaming and cleanup overrides.
|
|
887
|
+
* @returns A live subagent handle.
|
|
732
888
|
*/
|
|
733
|
-
async spawnSubagent(task) {
|
|
889
|
+
async spawnSubagent(task, opts = {}) {
|
|
734
890
|
const cmd = this.provider.capabilities.spawn.spawnCommand;
|
|
735
891
|
if (cmd === null || cmd.length === 0) {
|
|
736
892
|
throw new Error(
|
|
@@ -741,44 +897,312 @@ ${MARKER_END}`;
|
|
|
741
897
|
if (typeof program !== "string" || program.length === 0) {
|
|
742
898
|
throw new Error("PiHarness.spawnSubagent: invalid spawnCommand (missing program)");
|
|
743
899
|
}
|
|
900
|
+
const taskId = task.taskId ?? generateShortId();
|
|
901
|
+
const parentSessionId = task.parentSessionId ?? "orphan";
|
|
902
|
+
const subagentId = `sub-${taskId}-${generateShortId().slice(0, 6)}`;
|
|
903
|
+
const childSessionPath = join5(
|
|
904
|
+
getPiAgentDir2(),
|
|
905
|
+
"sessions",
|
|
906
|
+
"subagents",
|
|
907
|
+
`subagent-${parentSessionId}-${taskId}.jsonl`
|
|
908
|
+
);
|
|
909
|
+
await mkdir3(dirname2(childSessionPath), { recursive: true });
|
|
910
|
+
let grace = opts.terminateGraceMs;
|
|
911
|
+
if (grace === void 0) {
|
|
912
|
+
try {
|
|
913
|
+
const settings = await this.readSettings({ kind: "global" });
|
|
914
|
+
grace = readTerminateGraceFromSettings(settings, DEFAULT_TERMINATE_GRACE_MS);
|
|
915
|
+
} catch {
|
|
916
|
+
grace = DEFAULT_TERMINATE_GRACE_MS;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (!Number.isFinite(grace) || grace < 0) {
|
|
920
|
+
grace = DEFAULT_TERMINATE_GRACE_MS;
|
|
921
|
+
}
|
|
922
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
923
|
+
const startedAtIso = startedAt.toISOString();
|
|
924
|
+
const sessionHeader = {
|
|
925
|
+
type: "session",
|
|
926
|
+
version: 3,
|
|
927
|
+
id: subagentId,
|
|
928
|
+
timestamp: startedAtIso,
|
|
929
|
+
cwd: opts.cwd ?? task.cwd ?? process.cwd(),
|
|
930
|
+
parentSession: task.parentSessionId ?? null,
|
|
931
|
+
taskId,
|
|
932
|
+
childSessionPath
|
|
933
|
+
};
|
|
934
|
+
await writeFile(childSessionPath, `${JSON.stringify(sessionHeader)}
|
|
935
|
+
`, "utf8");
|
|
744
936
|
const baseArgs = cmd.slice(1);
|
|
745
937
|
const args = [...baseArgs, task.prompt];
|
|
746
938
|
const child = spawn(program, args, {
|
|
747
|
-
cwd: task.cwd,
|
|
748
|
-
env: { ...process.env, ...task.env },
|
|
939
|
+
cwd: opts.cwd ?? task.cwd,
|
|
940
|
+
env: { ...process.env, ...task.env, ...opts.env },
|
|
749
941
|
stdio: ["ignore", "pipe", "pipe"]
|
|
750
942
|
});
|
|
751
|
-
let
|
|
752
|
-
let
|
|
943
|
+
let stdoutAccum = "";
|
|
944
|
+
let stderrAccum = "";
|
|
945
|
+
let stdoutBuffer = "";
|
|
946
|
+
let stderrBuffer = "";
|
|
947
|
+
let stdoutLineNumber = 0;
|
|
948
|
+
let nonJsonLineCount = 0;
|
|
949
|
+
const stderrRing = [];
|
|
950
|
+
const safeOnStream = (event) => {
|
|
951
|
+
if (opts.onStream === void 0) return;
|
|
952
|
+
try {
|
|
953
|
+
opts.onStream(event);
|
|
954
|
+
} catch (err) {
|
|
955
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
956
|
+
stderrRing.push(`[onStream] ${message}`);
|
|
957
|
+
if (stderrRing.length > STDERR_RING_BUFFER_SIZE) stderrRing.shift();
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
const writeChildSession = (entry) => {
|
|
961
|
+
void appendFile(childSessionPath, `${JSON.stringify(entry)}
|
|
962
|
+
`, "utf8").catch(() => {
|
|
963
|
+
const synthetic = `[childSession] failed to append entry`;
|
|
964
|
+
stderrRing.push(synthetic);
|
|
965
|
+
if (stderrRing.length > STDERR_RING_BUFFER_SIZE) stderrRing.shift();
|
|
966
|
+
});
|
|
967
|
+
};
|
|
968
|
+
const flushStdoutBuffer = (final) => {
|
|
969
|
+
let nlIdx = stdoutBuffer.indexOf("\n");
|
|
970
|
+
while (nlIdx !== -1) {
|
|
971
|
+
const line = stdoutBuffer.slice(0, nlIdx);
|
|
972
|
+
stdoutBuffer = stdoutBuffer.slice(nlIdx + 1);
|
|
973
|
+
this.handleStdoutLine(line, {
|
|
974
|
+
subagentId,
|
|
975
|
+
increment: () => ++stdoutLineNumber,
|
|
976
|
+
incrementNonJson: () => ++nonJsonLineCount,
|
|
977
|
+
writeChildSession,
|
|
978
|
+
safeOnStream
|
|
979
|
+
});
|
|
980
|
+
nlIdx = stdoutBuffer.indexOf("\n");
|
|
981
|
+
}
|
|
982
|
+
if (final && stdoutBuffer.length > 0) {
|
|
983
|
+
const remainder = stdoutBuffer;
|
|
984
|
+
stdoutBuffer = "";
|
|
985
|
+
this.handleStdoutLine(remainder, {
|
|
986
|
+
subagentId,
|
|
987
|
+
increment: () => ++stdoutLineNumber,
|
|
988
|
+
incrementNonJson: () => ++nonJsonLineCount,
|
|
989
|
+
writeChildSession,
|
|
990
|
+
safeOnStream
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
const flushStderrBuffer = (final) => {
|
|
995
|
+
let nlIdx = stderrBuffer.indexOf("\n");
|
|
996
|
+
while (nlIdx !== -1) {
|
|
997
|
+
const line = stderrBuffer.slice(0, nlIdx);
|
|
998
|
+
stderrBuffer = stderrBuffer.slice(nlIdx + 1);
|
|
999
|
+
stderrRing.push(line);
|
|
1000
|
+
if (stderrRing.length > STDERR_RING_BUFFER_SIZE) stderrRing.shift();
|
|
1001
|
+
writeChildSession({ type: "subagent_stderr", line });
|
|
1002
|
+
safeOnStream({ kind: "stderr", subagentId, payload: { line } });
|
|
1003
|
+
nlIdx = stderrBuffer.indexOf("\n");
|
|
1004
|
+
}
|
|
1005
|
+
if (final && stderrBuffer.length > 0) {
|
|
1006
|
+
const line = stderrBuffer;
|
|
1007
|
+
stderrBuffer = "";
|
|
1008
|
+
stderrRing.push(line);
|
|
1009
|
+
if (stderrRing.length > STDERR_RING_BUFFER_SIZE) stderrRing.shift();
|
|
1010
|
+
writeChildSession({ type: "subagent_stderr", line });
|
|
1011
|
+
safeOnStream({ kind: "stderr", subagentId, payload: { line } });
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
753
1014
|
child.stdout?.on("data", (chunk) => {
|
|
754
|
-
|
|
1015
|
+
const text = chunk.toString("utf8");
|
|
1016
|
+
stdoutAccum += text;
|
|
1017
|
+
stdoutBuffer += text;
|
|
1018
|
+
flushStdoutBuffer(false);
|
|
755
1019
|
});
|
|
756
1020
|
child.stderr?.on("data", (chunk) => {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
let parsed;
|
|
762
|
-
try {
|
|
763
|
-
parsed = JSON.parse(stdout);
|
|
764
|
-
} catch {
|
|
765
|
-
}
|
|
766
|
-
resolve({ exitCode, stdout, stderr, parsed });
|
|
767
|
-
});
|
|
1021
|
+
const text = chunk.toString("utf8");
|
|
1022
|
+
stderrAccum += text;
|
|
1023
|
+
stderrBuffer += text;
|
|
1024
|
+
flushStderrBuffer(false);
|
|
768
1025
|
});
|
|
1026
|
+
let terminating = false;
|
|
1027
|
+
let terminationReason = "natural";
|
|
1028
|
+
let terminatePromise = null;
|
|
1029
|
+
const terminateImpl = () => {
|
|
1030
|
+
if (terminatePromise !== null) return terminatePromise;
|
|
1031
|
+
terminating = true;
|
|
1032
|
+
terminationReason = "terminated";
|
|
1033
|
+
terminatePromise = terminateSubagent(child, grace ?? DEFAULT_TERMINATE_GRACE_MS);
|
|
1034
|
+
return terminatePromise;
|
|
1035
|
+
};
|
|
1036
|
+
const terminateSync = () => {
|
|
1037
|
+
if (terminating) return;
|
|
1038
|
+
terminating = true;
|
|
1039
|
+
terminationReason = "terminated";
|
|
1040
|
+
try {
|
|
1041
|
+
child.kill("SIGTERM");
|
|
1042
|
+
} catch {
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
const activeRecord = {
|
|
1046
|
+
child,
|
|
1047
|
+
subagentId,
|
|
1048
|
+
terminate: terminateSync
|
|
1049
|
+
};
|
|
1050
|
+
activeSubagents.add(activeRecord);
|
|
1051
|
+
ensureOrphanSweeperRegistered();
|
|
769
1052
|
if (task.signal !== void 0) {
|
|
770
|
-
|
|
771
|
-
|
|
1053
|
+
const onAbort = () => {
|
|
1054
|
+
void terminateImpl();
|
|
1055
|
+
};
|
|
1056
|
+
if (task.signal.aborted) {
|
|
1057
|
+
onAbort();
|
|
1058
|
+
} else {
|
|
1059
|
+
task.signal.addEventListener("abort", onAbort, { once: true });
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
const exitPromise = new Promise((resolve) => {
|
|
1063
|
+
child.on("close", (exitCode, signal) => {
|
|
1064
|
+
flushStdoutBuffer(true);
|
|
1065
|
+
flushStderrBuffer(true);
|
|
1066
|
+
const durationMs = Date.now() - startedAt.getTime();
|
|
1067
|
+
writeChildSession({
|
|
1068
|
+
type: "subagent_exit",
|
|
1069
|
+
code: exitCode,
|
|
1070
|
+
signal,
|
|
1071
|
+
reason: terminationReason,
|
|
1072
|
+
durationMs,
|
|
1073
|
+
nonJsonLineCount
|
|
1074
|
+
});
|
|
1075
|
+
activeSubagents.delete(activeRecord);
|
|
1076
|
+
const result2 = {
|
|
1077
|
+
code: exitCode,
|
|
1078
|
+
signal,
|
|
1079
|
+
childSessionPath,
|
|
1080
|
+
durationMs
|
|
1081
|
+
};
|
|
1082
|
+
safeOnStream({ kind: "exit", subagentId, payload: result2 });
|
|
1083
|
+
resolve(result2);
|
|
1084
|
+
});
|
|
1085
|
+
child.on("error", () => {
|
|
772
1086
|
});
|
|
1087
|
+
});
|
|
1088
|
+
const result = exitPromise.then(({ code }) => {
|
|
1089
|
+
let parsed;
|
|
1090
|
+
try {
|
|
1091
|
+
parsed = JSON.parse(stdoutAccum);
|
|
1092
|
+
} catch {
|
|
1093
|
+
}
|
|
1094
|
+
return { exitCode: code, stdout: stdoutAccum, stderr: stderrAccum, parsed };
|
|
1095
|
+
});
|
|
1096
|
+
const linkEntry = {
|
|
1097
|
+
type: "subagent_link",
|
|
1098
|
+
subagentId,
|
|
1099
|
+
taskId,
|
|
1100
|
+
childSessionPath,
|
|
1101
|
+
startedAt: startedAtIso
|
|
1102
|
+
};
|
|
1103
|
+
if (task.parentSessionPath !== void 0 && task.parentSessionPath.length > 0) {
|
|
1104
|
+
try {
|
|
1105
|
+
await writeSubagentLink(task.parentSessionPath, linkEntry);
|
|
1106
|
+
safeOnStream({ kind: "link", subagentId, payload: linkEntry });
|
|
1107
|
+
} catch {
|
|
1108
|
+
stderrRing.push(`[link] failed to write subagent_link to parent`);
|
|
1109
|
+
if (stderrRing.length > STDERR_RING_BUFFER_SIZE) stderrRing.shift();
|
|
1110
|
+
}
|
|
773
1111
|
}
|
|
774
1112
|
return {
|
|
1113
|
+
subagentId,
|
|
1114
|
+
taskId,
|
|
1115
|
+
childSessionPath,
|
|
775
1116
|
pid: child.pid ?? null,
|
|
1117
|
+
startedAt,
|
|
1118
|
+
exitPromise,
|
|
776
1119
|
result,
|
|
1120
|
+
terminate: terminateImpl,
|
|
777
1121
|
abort: () => {
|
|
778
|
-
|
|
779
|
-
}
|
|
1122
|
+
void terminateImpl();
|
|
1123
|
+
},
|
|
1124
|
+
recentStderr: () => stderrRing.slice()
|
|
780
1125
|
};
|
|
781
1126
|
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Race a set of subagent handles, returning the first one that exits.
|
|
1129
|
+
*
|
|
1130
|
+
* @remarks
|
|
1131
|
+
* Maps CANT's `parallel: race` construct (per ADR-035 §D6) onto the
|
|
1132
|
+
* canonical {@link spawnSubagent} contract. The losing handles are
|
|
1133
|
+
* gracefully terminated via {@link SubagentHandle.terminate} once the
|
|
1134
|
+
* first settles so no straggler children outlive the race.
|
|
1135
|
+
*
|
|
1136
|
+
* @param handles - Subagent handles to race.
|
|
1137
|
+
* @returns The {@link SubagentExitResult} of the first child to exit.
|
|
1138
|
+
* @throws When `handles` is empty (caller bug — a race over zero
|
|
1139
|
+
* children has no winner).
|
|
1140
|
+
*/
|
|
1141
|
+
static async raceSubagents(handles) {
|
|
1142
|
+
if (handles.length === 0) {
|
|
1143
|
+
throw new Error("PiHarness.raceSubagents: cannot race an empty handle list");
|
|
1144
|
+
}
|
|
1145
|
+
const tagged = handles.map(
|
|
1146
|
+
(handle, index) => handle.exitPromise.then((value) => ({ index, value }))
|
|
1147
|
+
);
|
|
1148
|
+
const winner = await Promise.race(tagged);
|
|
1149
|
+
const losers = [];
|
|
1150
|
+
for (let i = 0; i < handles.length; i += 1) {
|
|
1151
|
+
if (i === winner.index) continue;
|
|
1152
|
+
const loser = handles[i];
|
|
1153
|
+
if (loser === void 0) continue;
|
|
1154
|
+
losers.push(loser.terminate().catch(() => void 0));
|
|
1155
|
+
}
|
|
1156
|
+
await Promise.all(losers);
|
|
1157
|
+
return winner.value;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Settle a set of subagent handles, returning a parallel array of
|
|
1161
|
+
* results.
|
|
1162
|
+
*
|
|
1163
|
+
* @remarks
|
|
1164
|
+
* Maps CANT's `parallel: settle` construct (per ADR-035 §D6) onto the
|
|
1165
|
+
* canonical {@link spawnSubagent} contract. Because
|
|
1166
|
+
* {@link SubagentHandle.exitPromise} never rejects, every entry in
|
|
1167
|
+
* the returned array is `{ status: 'fulfilled', value: ... }` under
|
|
1168
|
+
* normal operation; the `PromiseSettledResult` shape is preserved
|
|
1169
|
+
* for forward compatibility with future failure modes.
|
|
1170
|
+
*
|
|
1171
|
+
* @param handles - Subagent handles to settle.
|
|
1172
|
+
* @returns Parallel array of settled exit results, one per input.
|
|
1173
|
+
*/
|
|
1174
|
+
static async settleAllSubagents(handles) {
|
|
1175
|
+
return Promise.allSettled(handles.map((h) => h.exitPromise));
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Per-line stdout dispatcher used by the streaming buffer flusher.
|
|
1179
|
+
*
|
|
1180
|
+
* @remarks
|
|
1181
|
+
* Extracted as a private method so the line-handling logic stays
|
|
1182
|
+
* close to {@link spawnSubagent} but does not bloat the parent
|
|
1183
|
+
* function. Skips empty lines (a leading newline produces a zero-
|
|
1184
|
+
* length entry that has no semantic meaning).
|
|
1185
|
+
*/
|
|
1186
|
+
handleStdoutLine(rawLine, ctx) {
|
|
1187
|
+
const trimmed = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
1188
|
+
if (trimmed.length === 0) return;
|
|
1189
|
+
const lineNumber = ctx.increment();
|
|
1190
|
+
let parsed;
|
|
1191
|
+
try {
|
|
1192
|
+
parsed = JSON.parse(trimmed);
|
|
1193
|
+
} catch {
|
|
1194
|
+
ctx.incrementNonJson();
|
|
1195
|
+
ctx.writeChildSession({ type: "raw", lineNumber, line: trimmed });
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
ctx.writeChildSession({ type: "custom_message", lineNumber, payload: parsed });
|
|
1199
|
+
ctx.safeOnStream({
|
|
1200
|
+
kind: "message",
|
|
1201
|
+
subagentId: ctx.subagentId,
|
|
1202
|
+
lineNumber,
|
|
1203
|
+
payload: parsed
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
782
1206
|
// ── Settings ────────────────────────────────────────────────────────
|
|
783
1207
|
/** {@inheritDoc Harness.readSettings} */
|
|
784
1208
|
async readSettings(scope) {
|
|
@@ -1191,7 +1615,185 @@ ${MARKER_END}`;
|
|
|
1191
1615
|
}
|
|
1192
1616
|
return removed;
|
|
1193
1617
|
}
|
|
1618
|
+
// ── CANT profiles (Wave-1, T276) ────────────────────────────────────
|
|
1619
|
+
/**
|
|
1620
|
+
* {@inheritDoc Harness.installCantProfile}
|
|
1621
|
+
*
|
|
1622
|
+
* @remarks
|
|
1623
|
+
* Validates the source via {@link validateCantProfile} before copying so
|
|
1624
|
+
* we never persist a `.cant` file the runtime bridge cannot load. The
|
|
1625
|
+
* target layout is `<tier-root>/cant/<name>.cant`, resolved through
|
|
1626
|
+
* {@link resolveTierDir} so the project/user/global hierarchy stays
|
|
1627
|
+
* consistent with the other Wave-1 verbs.
|
|
1628
|
+
*/
|
|
1629
|
+
async installCantProfile(sourcePath, name, tier, projectDir, opts) {
|
|
1630
|
+
if (!existsSync4(sourcePath)) {
|
|
1631
|
+
throw new Error(`installCantProfile: source file does not exist: ${sourcePath}`);
|
|
1632
|
+
}
|
|
1633
|
+
const stats = await stat(sourcePath);
|
|
1634
|
+
if (!stats.isFile()) {
|
|
1635
|
+
throw new Error(`installCantProfile: source path is not a regular file: ${sourcePath}`);
|
|
1636
|
+
}
|
|
1637
|
+
const ext = extname(sourcePath);
|
|
1638
|
+
if (ext !== ".cant") {
|
|
1639
|
+
throw new Error(
|
|
1640
|
+
`installCantProfile: expected a CANT source file (.cant), got: ${ext || "(no extension)"}`
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
const validation = await this.validateCantProfile(sourcePath);
|
|
1644
|
+
if (!validation.valid) {
|
|
1645
|
+
const firstError = validation.errors.find((e) => e.severity === "error") ?? validation.errors[0];
|
|
1646
|
+
const detail = firstError !== void 0 ? ` (${firstError.ruleId} at ${firstError.line}:${firstError.col}: ${firstError.message})` : "";
|
|
1647
|
+
throw new Error(`installCantProfile: source file failed cant-core validation${detail}`);
|
|
1648
|
+
}
|
|
1649
|
+
const dir = resolveTierDir({ tier, kind: "cant", projectDir });
|
|
1650
|
+
const targetPath = join5(dir, `${name}.cant`);
|
|
1651
|
+
if (existsSync4(targetPath) && opts?.force !== true) {
|
|
1652
|
+
throw new Error(
|
|
1653
|
+
`installCantProfile: target already exists at ${targetPath} (pass --force to overwrite)`
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
const contents = await readFile(sourcePath);
|
|
1657
|
+
await mkdir3(dir, { recursive: true });
|
|
1658
|
+
await writeFile(targetPath, contents);
|
|
1659
|
+
return { targetPath, tier, counts: validation.counts };
|
|
1660
|
+
}
|
|
1661
|
+
/** {@inheritDoc Harness.removeCantProfile} */
|
|
1662
|
+
async removeCantProfile(name, tier, projectDir) {
|
|
1663
|
+
const dir = resolveTierDir({ tier, kind: "cant", projectDir });
|
|
1664
|
+
const targetPath = join5(dir, `${name}.cant`);
|
|
1665
|
+
if (!existsSync4(targetPath)) return false;
|
|
1666
|
+
await rm3(targetPath, { force: true });
|
|
1667
|
+
return true;
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* {@inheritDoc Harness.listCantProfiles}
|
|
1671
|
+
*
|
|
1672
|
+
* @remarks
|
|
1673
|
+
* Walks every tier in {@link TIER_PRECEDENCE} order, parsing each
|
|
1674
|
+
* discovered `.cant` file via cant-core to extract a
|
|
1675
|
+
* {@link CantProfileCounts} bag. Higher-precedence tiers shadow
|
|
1676
|
+
* lower-precedence entries with the same name; shadowed entries
|
|
1677
|
+
* still appear in the result but carry the
|
|
1678
|
+
* `shadowedByHigherTier` flag so callers can render the precedence
|
|
1679
|
+
* story without losing visibility of the duplicate.
|
|
1680
|
+
*/
|
|
1681
|
+
async listCantProfiles(projectDir) {
|
|
1682
|
+
const tiers = resolveAllTiers("cant", projectDir);
|
|
1683
|
+
const out = [];
|
|
1684
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
1685
|
+
for (const { tier, dir } of tiers) {
|
|
1686
|
+
if (!existsSync4(dir)) continue;
|
|
1687
|
+
let entries;
|
|
1688
|
+
try {
|
|
1689
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1690
|
+
} catch {
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
for (const entry of entries) {
|
|
1694
|
+
if (!entry.isFile()) continue;
|
|
1695
|
+
const fileName = entry.name;
|
|
1696
|
+
if (!fileName.endsWith(".cant")) continue;
|
|
1697
|
+
const name = fileName.slice(0, -".cant".length);
|
|
1698
|
+
const sourcePath = join5(dir, fileName);
|
|
1699
|
+
const counts = await extractCantCounts(sourcePath);
|
|
1700
|
+
const shadowed = seenNames.has(name);
|
|
1701
|
+
const profile = {
|
|
1702
|
+
name,
|
|
1703
|
+
tier,
|
|
1704
|
+
sourcePath,
|
|
1705
|
+
counts
|
|
1706
|
+
};
|
|
1707
|
+
if (shadowed) {
|
|
1708
|
+
profile.shadowedByHigherTier = true;
|
|
1709
|
+
}
|
|
1710
|
+
out.push(profile);
|
|
1711
|
+
seenNames.add(name);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
return out;
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* {@inheritDoc Harness.validateCantProfile}
|
|
1718
|
+
*
|
|
1719
|
+
* @remarks
|
|
1720
|
+
* Pure validator. Reads the file, runs `parseDocument` to derive
|
|
1721
|
+
* counts (when parsing succeeds) and `validateDocument` to collect
|
|
1722
|
+
* the 42-rule diagnostic feed. The two calls are kept independent so
|
|
1723
|
+
* we can still report counts for files that pass parsing but fail a
|
|
1724
|
+
* lint rule.
|
|
1725
|
+
*/
|
|
1726
|
+
async validateCantProfile(sourcePath) {
|
|
1727
|
+
if (!existsSync4(sourcePath)) {
|
|
1728
|
+
throw new Error(`validateCantProfile: source file does not exist: ${sourcePath}`);
|
|
1729
|
+
}
|
|
1730
|
+
const stats = await stat(sourcePath);
|
|
1731
|
+
if (!stats.isFile()) {
|
|
1732
|
+
throw new Error(`validateCantProfile: source path is not a regular file: ${sourcePath}`);
|
|
1733
|
+
}
|
|
1734
|
+
const counts = await extractCantCounts(sourcePath);
|
|
1735
|
+
const validation = await validateDocument(sourcePath);
|
|
1736
|
+
const errors = validation.diagnostics.map((d) => ({
|
|
1737
|
+
ruleId: d.ruleId,
|
|
1738
|
+
message: d.message,
|
|
1739
|
+
line: d.line,
|
|
1740
|
+
col: d.col,
|
|
1741
|
+
severity: normaliseSeverity(d.severity)
|
|
1742
|
+
}));
|
|
1743
|
+
return {
|
|
1744
|
+
valid: validation.valid,
|
|
1745
|
+
errors,
|
|
1746
|
+
counts
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1194
1749
|
};
|
|
1750
|
+
async function terminateSubagent(child, graceMs) {
|
|
1751
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
try {
|
|
1755
|
+
child.kill("SIGTERM");
|
|
1756
|
+
} catch {
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
const pollInterval = Math.min(25, Math.max(1, graceMs));
|
|
1760
|
+
const deadline = Date.now() + graceMs;
|
|
1761
|
+
await new Promise((resolve) => {
|
|
1762
|
+
const timer = setInterval(() => {
|
|
1763
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
1764
|
+
clearInterval(timer);
|
|
1765
|
+
resolve();
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
if (Date.now() >= deadline) {
|
|
1769
|
+
clearInterval(timer);
|
|
1770
|
+
try {
|
|
1771
|
+
child.kill("SIGKILL");
|
|
1772
|
+
} catch {
|
|
1773
|
+
}
|
|
1774
|
+
resolve();
|
|
1775
|
+
}
|
|
1776
|
+
}, pollInterval);
|
|
1777
|
+
child.once("close", () => {
|
|
1778
|
+
clearInterval(timer);
|
|
1779
|
+
resolve();
|
|
1780
|
+
});
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
async function writeSubagentLink(parentSessionPath, entry) {
|
|
1784
|
+
await mkdir3(dirname2(parentSessionPath), { recursive: true });
|
|
1785
|
+
const wrapped = {
|
|
1786
|
+
type: "custom",
|
|
1787
|
+
subtype: entry.type,
|
|
1788
|
+
subagentId: entry.subagentId,
|
|
1789
|
+
taskId: entry.taskId,
|
|
1790
|
+
childSessionPath: entry.childSessionPath,
|
|
1791
|
+
startedAt: entry.startedAt
|
|
1792
|
+
};
|
|
1793
|
+
const line = `${JSON.stringify(wrapped)}
|
|
1794
|
+
`;
|
|
1795
|
+
await appendFile(parentSessionPath, line, "utf8");
|
|
1796
|
+
}
|
|
1195
1797
|
async function readSessionHeader(filePath) {
|
|
1196
1798
|
let handle = null;
|
|
1197
1799
|
try {
|
|
@@ -1243,6 +1845,104 @@ async function readSessionHeader(filePath) {
|
|
|
1243
1845
|
}
|
|
1244
1846
|
}
|
|
1245
1847
|
}
|
|
1848
|
+
var EMPTY_CANT_COUNTS = {
|
|
1849
|
+
agentCount: 0,
|
|
1850
|
+
workflowCount: 0,
|
|
1851
|
+
pipelineCount: 0,
|
|
1852
|
+
hookCount: 0,
|
|
1853
|
+
skillCount: 0
|
|
1854
|
+
};
|
|
1855
|
+
function isRecord(v) {
|
|
1856
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1857
|
+
}
|
|
1858
|
+
function unwrapSpanned(value) {
|
|
1859
|
+
if (typeof value === "string") return value;
|
|
1860
|
+
if (isRecord(value) && typeof value["value"] === "string") {
|
|
1861
|
+
return value["value"];
|
|
1862
|
+
}
|
|
1863
|
+
return null;
|
|
1864
|
+
}
|
|
1865
|
+
function collectSkillNames(value, out) {
|
|
1866
|
+
if (!isRecord(value)) return;
|
|
1867
|
+
const arr = value["Array"];
|
|
1868
|
+
if (!Array.isArray(arr)) return;
|
|
1869
|
+
for (const item of arr) {
|
|
1870
|
+
if (!isRecord(item)) continue;
|
|
1871
|
+
const stringWrapper = item["String"];
|
|
1872
|
+
if (isRecord(stringWrapper) && typeof stringWrapper["raw"] === "string") {
|
|
1873
|
+
out.add(stringWrapper["raw"]);
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1876
|
+
const identWrapper = item["Identifier"];
|
|
1877
|
+
if (typeof identWrapper === "string") {
|
|
1878
|
+
out.add(identWrapper);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
async function extractCantCounts(sourcePath) {
|
|
1883
|
+
let parsed;
|
|
1884
|
+
try {
|
|
1885
|
+
parsed = await parseDocument(sourcePath);
|
|
1886
|
+
} catch {
|
|
1887
|
+
return { ...EMPTY_CANT_COUNTS };
|
|
1888
|
+
}
|
|
1889
|
+
if (!parsed.success || !isRecord(parsed.document)) {
|
|
1890
|
+
return { ...EMPTY_CANT_COUNTS };
|
|
1891
|
+
}
|
|
1892
|
+
const sections = parsed.document["sections"];
|
|
1893
|
+
if (!Array.isArray(sections)) {
|
|
1894
|
+
return { ...EMPTY_CANT_COUNTS };
|
|
1895
|
+
}
|
|
1896
|
+
let agentCount = 0;
|
|
1897
|
+
let workflowCount = 0;
|
|
1898
|
+
let pipelineCount = 0;
|
|
1899
|
+
let hookCount = 0;
|
|
1900
|
+
const skillNames = /* @__PURE__ */ new Set();
|
|
1901
|
+
for (const section of sections) {
|
|
1902
|
+
if (!isRecord(section)) continue;
|
|
1903
|
+
if (isRecord(section["Agent"])) {
|
|
1904
|
+
agentCount += 1;
|
|
1905
|
+
const agent = section["Agent"];
|
|
1906
|
+
const hooks = agent["hooks"];
|
|
1907
|
+
if (Array.isArray(hooks)) {
|
|
1908
|
+
hookCount += hooks.length;
|
|
1909
|
+
}
|
|
1910
|
+
const properties = agent["properties"];
|
|
1911
|
+
if (Array.isArray(properties)) {
|
|
1912
|
+
for (const prop of properties) {
|
|
1913
|
+
if (!isRecord(prop)) continue;
|
|
1914
|
+
const key = unwrapSpanned(prop["key"]);
|
|
1915
|
+
if (key === "skills") {
|
|
1916
|
+
collectSkillNames(prop["value"], skillNames);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
continue;
|
|
1921
|
+
}
|
|
1922
|
+
if (isRecord(section["Workflow"])) {
|
|
1923
|
+
workflowCount += 1;
|
|
1924
|
+
continue;
|
|
1925
|
+
}
|
|
1926
|
+
if (isRecord(section["Pipeline"])) {
|
|
1927
|
+
pipelineCount += 1;
|
|
1928
|
+
continue;
|
|
1929
|
+
}
|
|
1930
|
+
if (isRecord(section["Hook"])) {
|
|
1931
|
+
hookCount += 1;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
return {
|
|
1935
|
+
agentCount,
|
|
1936
|
+
workflowCount,
|
|
1937
|
+
pipelineCount,
|
|
1938
|
+
hookCount,
|
|
1939
|
+
skillCount: skillNames.size
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
function normaliseSeverity(raw) {
|
|
1943
|
+
if (raw === "warning" || raw === "info" || raw === "hint") return raw;
|
|
1944
|
+
return "error";
|
|
1945
|
+
}
|
|
1246
1946
|
|
|
1247
1947
|
// src/core/harness/index.ts
|
|
1248
1948
|
function getHarnessFor(provider) {
|
|
@@ -1262,28 +1962,67 @@ function getAllHarnesses() {
|
|
|
1262
1962
|
}
|
|
1263
1963
|
return result;
|
|
1264
1964
|
}
|
|
1265
|
-
function resolveDefaultTargetProviders() {
|
|
1965
|
+
function resolveDefaultTargetProviders(options = {}) {
|
|
1966
|
+
const mode = getExclusivityMode();
|
|
1266
1967
|
let primary = null;
|
|
1267
1968
|
try {
|
|
1268
1969
|
primary = getPrimaryHarness();
|
|
1269
1970
|
} catch {
|
|
1270
1971
|
primary = null;
|
|
1271
1972
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1973
|
+
let installed;
|
|
1974
|
+
try {
|
|
1975
|
+
installed = getInstalledProviders();
|
|
1976
|
+
} catch {
|
|
1977
|
+
installed = [];
|
|
1978
|
+
}
|
|
1979
|
+
const primaryId = primary?.provider.id ?? null;
|
|
1980
|
+
const primaryInstalled = primaryId !== null && installed.some((provider) => provider.id === primaryId);
|
|
1981
|
+
const explicit = options.explicit;
|
|
1982
|
+
const explicitContainsPrimary = explicit !== void 0 && primaryId !== null ? explicit.some((provider) => provider.id === primaryId) : false;
|
|
1983
|
+
const legacyFallback = () => {
|
|
1984
|
+
if (primary !== null && primaryInstalled) {
|
|
1277
1985
|
return [primary.provider];
|
|
1278
1986
|
}
|
|
1987
|
+
const highTier = installed.filter(
|
|
1988
|
+
(provider) => provider.priority === "primary" || provider.priority === "high"
|
|
1989
|
+
);
|
|
1990
|
+
if (highTier.length > 0) {
|
|
1991
|
+
return highTier;
|
|
1992
|
+
}
|
|
1993
|
+
return installed;
|
|
1994
|
+
};
|
|
1995
|
+
if (mode === "force-pi") {
|
|
1996
|
+
if (primary === null || !primaryInstalled) {
|
|
1997
|
+
throw new PiRequiredError();
|
|
1998
|
+
}
|
|
1999
|
+
return [primary.provider];
|
|
1279
2000
|
}
|
|
1280
|
-
|
|
1281
|
-
(
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
return
|
|
2001
|
+
if (mode === "legacy") {
|
|
2002
|
+
if (explicit !== void 0) {
|
|
2003
|
+
return explicit;
|
|
2004
|
+
}
|
|
2005
|
+
return legacyFallback();
|
|
2006
|
+
}
|
|
2007
|
+
if (explicit !== void 0 && explicit.length > 0 && !explicitContainsPrimary && primaryInstalled && !hasExplicitNonPiAutoWarned()) {
|
|
2008
|
+
console.warn(
|
|
2009
|
+
"Warning: Targeting a non-Pi provider explicitly is deprecated when Pi is installed. Future versions will route all runtime commands through Pi. To suppress this warning, set caamp.exclusivityMode to 'legacy'."
|
|
2010
|
+
);
|
|
2011
|
+
markExplicitNonPiAutoWarned();
|
|
2012
|
+
}
|
|
2013
|
+
if (explicit !== void 0) {
|
|
2014
|
+
return explicit;
|
|
2015
|
+
}
|
|
2016
|
+
if (primary !== null && primaryInstalled) {
|
|
2017
|
+
return [primary.provider];
|
|
2018
|
+
}
|
|
2019
|
+
if (!hasPiAbsentAutoWarned()) {
|
|
2020
|
+
console.warn(
|
|
2021
|
+
"Warning: Pi is not installed. CAAMP is falling back to direct provider dispatch. Install Pi (https://github.com/mariozechner/pi-coding-agent) to enable orchestration, or set caamp.exclusivityMode to 'legacy' to suppress this warning."
|
|
2022
|
+
);
|
|
2023
|
+
markPiAbsentAutoWarned();
|
|
1285
2024
|
}
|
|
1286
|
-
return
|
|
2025
|
+
return legacyFallback();
|
|
1287
2026
|
}
|
|
1288
2027
|
async function dispatchInstallSkillAcrossProviders(sourcePath, skillName, providers, isGlobal, projectDir) {
|
|
1289
2028
|
const harnessTargets = [];
|
|
@@ -3817,6 +4556,13 @@ export {
|
|
|
3817
4556
|
selectProvidersByMinimumPriority,
|
|
3818
4557
|
installBatchWithRollback,
|
|
3819
4558
|
updateInstructionsSingleOperation,
|
|
4559
|
+
DEFAULT_EXCLUSIVITY_MODE,
|
|
4560
|
+
EXCLUSIVITY_MODE_ENV_VAR,
|
|
4561
|
+
PiRequiredError,
|
|
4562
|
+
isExclusivityMode,
|
|
4563
|
+
getExclusivityMode,
|
|
4564
|
+
setExclusivityMode,
|
|
4565
|
+
resetExclusivityModeOverride,
|
|
3820
4566
|
PiHarness,
|
|
3821
4567
|
getHarnessFor,
|
|
3822
4568
|
getPrimaryHarness,
|
|
@@ -3878,4 +4624,4 @@ export {
|
|
|
3878
4624
|
discoverSkillsMulti,
|
|
3879
4625
|
validateSkill
|
|
3880
4626
|
};
|
|
3881
|
-
//# sourceMappingURL=chunk-
|
|
4627
|
+
//# sourceMappingURL=chunk-JC77OAHA.js.map
|