@cleocode/caamp 2026.4.7 → 2026.4.10
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-XVZT7K6F.js} +874 -66
- package/dist/chunk-XVZT7K6F.js.map +1 -0
- package/dist/cli.js +279 -29
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1265 -317
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/package.json +6 -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.
|
|
@@ -662,19 +760,25 @@ var PiHarness = class {
|
|
|
662
760
|
return scope.kind === "global" ? join5(getPiAgentDir2(), "AGENTS.md") : join5(scope.projectDir, "AGENTS.md");
|
|
663
761
|
}
|
|
664
762
|
// ── Skills ──────────────────────────────────────────────────────────
|
|
665
|
-
/**
|
|
763
|
+
/**
|
|
764
|
+
* Install a skill directory into the resolved Pi skills location.
|
|
765
|
+
*/
|
|
666
766
|
async installSkill(sourcePath, skillName, scope) {
|
|
667
767
|
const targetDir = join5(this.skillsDir(scope), skillName);
|
|
668
768
|
await rm3(targetDir, { recursive: true, force: true });
|
|
669
769
|
await mkdir3(dirname2(targetDir), { recursive: true });
|
|
670
770
|
await cp3(sourcePath, targetDir, { recursive: true });
|
|
671
771
|
}
|
|
672
|
-
/**
|
|
772
|
+
/**
|
|
773
|
+
* Remove a skill directory from the resolved Pi skills location.
|
|
774
|
+
*/
|
|
673
775
|
async removeSkill(skillName, scope) {
|
|
674
776
|
const targetDir = join5(this.skillsDir(scope), skillName);
|
|
675
777
|
await rm3(targetDir, { recursive: true, force: true });
|
|
676
778
|
}
|
|
677
|
-
/**
|
|
779
|
+
/**
|
|
780
|
+
* List the installed skill directories at the given scope.
|
|
781
|
+
*/
|
|
678
782
|
async listSkills(scope) {
|
|
679
783
|
const dir = this.skillsDir(scope);
|
|
680
784
|
if (!existsSync4(dir)) return [];
|
|
@@ -682,7 +786,10 @@ var PiHarness = class {
|
|
|
682
786
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
683
787
|
}
|
|
684
788
|
// ── Instructions ────────────────────────────────────────────────────
|
|
685
|
-
/**
|
|
789
|
+
/**
|
|
790
|
+
* Inject or replace a CAAMP-managed instruction block inside the Pi
|
|
791
|
+
* `AGENTS.md` file for the resolved scope.
|
|
792
|
+
*/
|
|
686
793
|
async injectInstructions(content, scope) {
|
|
687
794
|
const filePath = this.agentsMdPath(scope);
|
|
688
795
|
await mkdir3(dirname2(filePath), { recursive: true });
|
|
@@ -706,7 +813,10 @@ ${MARKER_END}`;
|
|
|
706
813
|
}
|
|
707
814
|
await writeFile(filePath, updated, "utf8");
|
|
708
815
|
}
|
|
709
|
-
/**
|
|
816
|
+
/**
|
|
817
|
+
* Remove the CAAMP-managed instruction block from the Pi `AGENTS.md`
|
|
818
|
+
* file at the resolved scope.
|
|
819
|
+
*/
|
|
710
820
|
async removeInstructions(scope) {
|
|
711
821
|
const filePath = this.agentsMdPath(scope);
|
|
712
822
|
if (!existsSync4(filePath)) return;
|
|
@@ -716,21 +826,79 @@ ${MARKER_END}`;
|
|
|
716
826
|
await writeFile(filePath, stripped.length === 0 ? "" : `${stripped}
|
|
717
827
|
`, "utf8");
|
|
718
828
|
}
|
|
719
|
-
// ── Subagent spawn
|
|
829
|
+
// ── Subagent spawn (ADR-035 §D6) ────────────────────────────────────
|
|
720
830
|
/**
|
|
721
|
-
*
|
|
831
|
+
* Spawn a subagent through Pi's configured `spawnCommand` and return a
|
|
832
|
+
* live handle bound to the canonical streaming, attribution, and
|
|
833
|
+
* cleanup contract.
|
|
722
834
|
*
|
|
723
835
|
* @remarks
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
836
|
+
* Per ADR-035 §D6 this is the **only** sanctioned subagent spawn path
|
|
837
|
+
* in CLEO. All historical direct `child_process.spawn` callers in
|
|
838
|
+
* subagent contexts (including the `cant-bridge.ts` Pi extension and
|
|
839
|
+
* the legacy CLEO orchestrator paths) MUST migrate to this method so
|
|
840
|
+
* the contract below holds uniformly. A custom biome rule banning
|
|
841
|
+
* raw `spawn()` from subagent code is planned for v3 cleanup but is
|
|
842
|
+
* intentionally NOT enforced in v2 to keep the migration incremental.
|
|
729
843
|
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
844
|
+
* **Streaming semantics** — Pi's `--mode json` produces line-delimited
|
|
845
|
+
* JSON on stdout. The harness:
|
|
846
|
+
*
|
|
847
|
+
* - Line-buffers stdout, parses each line as JSON, and forwards a
|
|
848
|
+
* `{ kind: 'message', subagentId, lineNumber, payload }`
|
|
849
|
+
* {@link SubagentStreamEvent} via {@link SubagentSpawnOptions.onStream}.
|
|
850
|
+
* Non-parseable lines increment a warning counter (recorded in the
|
|
851
|
+
* child session as `{ type: 'raw' }`) but never crash the loop.
|
|
852
|
+
* - Line-buffers stderr separately, forwards each line as
|
|
853
|
+
* `{ kind: 'stderr', subagentId, payload: { line } }`, and stores
|
|
854
|
+
* it in a 100-line ring buffer accessible via
|
|
855
|
+
* {@link SubagentHandle.recentStderr}. Stderr is **never** injected
|
|
856
|
+
* into the parent LLM context per ADR-035 §D6.
|
|
857
|
+
* - Emits a final `{ kind: 'exit', subagentId, payload: SubagentExitResult }`
|
|
858
|
+
* when the child terminates.
|
|
859
|
+
*
|
|
860
|
+
* **Session attribution** — Every spawn produces a child session JSONL
|
|
861
|
+
* file at
|
|
862
|
+
* `~/.pi/agent/sessions/subagents/subagent-{parentSessionId}-{taskId}.jsonl`.
|
|
863
|
+
* The header line records the subagentId, taskId, and parent linkage.
|
|
864
|
+
* When {@link SubagentTask.parentSessionPath} is supplied, a
|
|
865
|
+
* {@link SubagentLinkEntry} is appended to the parent session file as
|
|
866
|
+
* a JSONL line so listing the parent surfaces its children.
|
|
867
|
+
*
|
|
868
|
+
* **Exit propagation** — {@link SubagentHandle.exitPromise} resolves
|
|
869
|
+
* with `{ code, signal, childSessionPath, durationMs }` exactly once
|
|
870
|
+
* when the child exits. The promise NEVER rejects: failure is
|
|
871
|
+
* encoded by a non-zero `code`, a non-null `signal`, or partial
|
|
872
|
+
* output preserved in the child session file.
|
|
873
|
+
*
|
|
874
|
+
* **Cleanup** — {@link SubagentHandle.terminate} sends SIGTERM, waits
|
|
875
|
+
* the configured grace window, then sends SIGKILL if the child is
|
|
876
|
+
* still alive. The grace window is sourced from
|
|
877
|
+
* {@link SubagentSpawnOptions.terminateGraceMs} when supplied,
|
|
878
|
+
* otherwise from `settings.json:pi.subagent.terminateGraceMs`,
|
|
879
|
+
* otherwise from {@link DEFAULT_TERMINATE_GRACE_MS}. A
|
|
880
|
+
* `subagent_exit` entry with reason `terminated` is appended to the
|
|
881
|
+
* child session file when cleanup runs.
|
|
882
|
+
*
|
|
883
|
+
* **Concurrency** — Use the static helpers
|
|
884
|
+
* {@link PiHarness.raceSubagents} and
|
|
885
|
+
* {@link PiHarness.settleAllSubagents} to compose `parallel: race`
|
|
886
|
+
* and `parallel: settle` constructs from CANT workflows over multiple
|
|
887
|
+
* handles.
|
|
888
|
+
*
|
|
889
|
+
* **Orphan handling** — On the first spawn the harness registers a
|
|
890
|
+
* process-wide `'exit'` handler that terminates every still-active
|
|
891
|
+
* subagent so a parent crash never strands children.
|
|
892
|
+
*
|
|
893
|
+
* Throws immediately when the provider entry is missing a
|
|
894
|
+
* `spawnCommand` so callers see configuration errors early rather
|
|
895
|
+
* than at child-exit time.
|
|
896
|
+
*
|
|
897
|
+
* @param task - Subagent task specification.
|
|
898
|
+
* @param opts - Per-call streaming and cleanup overrides.
|
|
899
|
+
* @returns A live subagent handle.
|
|
732
900
|
*/
|
|
733
|
-
async spawnSubagent(task) {
|
|
901
|
+
async spawnSubagent(task, opts = {}) {
|
|
734
902
|
const cmd = this.provider.capabilities.spawn.spawnCommand;
|
|
735
903
|
if (cmd === null || cmd.length === 0) {
|
|
736
904
|
throw new Error(
|
|
@@ -741,46 +909,316 @@ ${MARKER_END}`;
|
|
|
741
909
|
if (typeof program !== "string" || program.length === 0) {
|
|
742
910
|
throw new Error("PiHarness.spawnSubagent: invalid spawnCommand (missing program)");
|
|
743
911
|
}
|
|
912
|
+
const taskId = task.taskId ?? generateShortId();
|
|
913
|
+
const parentSessionId = task.parentSessionId ?? "orphan";
|
|
914
|
+
const subagentId = `sub-${taskId}-${generateShortId().slice(0, 6)}`;
|
|
915
|
+
const childSessionPath = join5(
|
|
916
|
+
getPiAgentDir2(),
|
|
917
|
+
"sessions",
|
|
918
|
+
"subagents",
|
|
919
|
+
`subagent-${parentSessionId}-${taskId}.jsonl`
|
|
920
|
+
);
|
|
921
|
+
await mkdir3(dirname2(childSessionPath), { recursive: true });
|
|
922
|
+
let grace = opts.terminateGraceMs;
|
|
923
|
+
if (grace === void 0) {
|
|
924
|
+
try {
|
|
925
|
+
const settings = await this.readSettings({ kind: "global" });
|
|
926
|
+
grace = readTerminateGraceFromSettings(settings, DEFAULT_TERMINATE_GRACE_MS);
|
|
927
|
+
} catch {
|
|
928
|
+
grace = DEFAULT_TERMINATE_GRACE_MS;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (!Number.isFinite(grace) || grace < 0) {
|
|
932
|
+
grace = DEFAULT_TERMINATE_GRACE_MS;
|
|
933
|
+
}
|
|
934
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
935
|
+
const startedAtIso = startedAt.toISOString();
|
|
936
|
+
const sessionHeader = {
|
|
937
|
+
type: "session",
|
|
938
|
+
version: 3,
|
|
939
|
+
id: subagentId,
|
|
940
|
+
timestamp: startedAtIso,
|
|
941
|
+
cwd: opts.cwd ?? task.cwd ?? process.cwd(),
|
|
942
|
+
parentSession: task.parentSessionId ?? null,
|
|
943
|
+
taskId,
|
|
944
|
+
childSessionPath
|
|
945
|
+
};
|
|
946
|
+
await writeFile(childSessionPath, `${JSON.stringify(sessionHeader)}
|
|
947
|
+
`, "utf8");
|
|
744
948
|
const baseArgs = cmd.slice(1);
|
|
745
949
|
const args = [...baseArgs, task.prompt];
|
|
746
950
|
const child = spawn(program, args, {
|
|
747
|
-
cwd: task.cwd,
|
|
748
|
-
env: { ...process.env, ...task.env },
|
|
951
|
+
cwd: opts.cwd ?? task.cwd,
|
|
952
|
+
env: { ...process.env, ...task.env, ...opts.env },
|
|
749
953
|
stdio: ["ignore", "pipe", "pipe"]
|
|
750
954
|
});
|
|
751
|
-
let
|
|
752
|
-
let
|
|
955
|
+
let stdoutAccum = "";
|
|
956
|
+
let stderrAccum = "";
|
|
957
|
+
let stdoutBuffer = "";
|
|
958
|
+
let stderrBuffer = "";
|
|
959
|
+
let stdoutLineNumber = 0;
|
|
960
|
+
let nonJsonLineCount = 0;
|
|
961
|
+
const stderrRing = [];
|
|
962
|
+
const safeOnStream = (event) => {
|
|
963
|
+
if (opts.onStream === void 0) return;
|
|
964
|
+
try {
|
|
965
|
+
opts.onStream(event);
|
|
966
|
+
} catch (err) {
|
|
967
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
968
|
+
stderrRing.push(`[onStream] ${message}`);
|
|
969
|
+
if (stderrRing.length > STDERR_RING_BUFFER_SIZE) stderrRing.shift();
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
const writeChildSession = (entry) => {
|
|
973
|
+
void appendFile(childSessionPath, `${JSON.stringify(entry)}
|
|
974
|
+
`, "utf8").catch(() => {
|
|
975
|
+
const synthetic = `[childSession] failed to append entry`;
|
|
976
|
+
stderrRing.push(synthetic);
|
|
977
|
+
if (stderrRing.length > STDERR_RING_BUFFER_SIZE) stderrRing.shift();
|
|
978
|
+
});
|
|
979
|
+
};
|
|
980
|
+
const flushStdoutBuffer = (final) => {
|
|
981
|
+
let nlIdx = stdoutBuffer.indexOf("\n");
|
|
982
|
+
while (nlIdx !== -1) {
|
|
983
|
+
const line = stdoutBuffer.slice(0, nlIdx);
|
|
984
|
+
stdoutBuffer = stdoutBuffer.slice(nlIdx + 1);
|
|
985
|
+
this.handleStdoutLine(line, {
|
|
986
|
+
subagentId,
|
|
987
|
+
increment: () => ++stdoutLineNumber,
|
|
988
|
+
incrementNonJson: () => ++nonJsonLineCount,
|
|
989
|
+
writeChildSession,
|
|
990
|
+
safeOnStream
|
|
991
|
+
});
|
|
992
|
+
nlIdx = stdoutBuffer.indexOf("\n");
|
|
993
|
+
}
|
|
994
|
+
if (final && stdoutBuffer.length > 0) {
|
|
995
|
+
const remainder = stdoutBuffer;
|
|
996
|
+
stdoutBuffer = "";
|
|
997
|
+
this.handleStdoutLine(remainder, {
|
|
998
|
+
subagentId,
|
|
999
|
+
increment: () => ++stdoutLineNumber,
|
|
1000
|
+
incrementNonJson: () => ++nonJsonLineCount,
|
|
1001
|
+
writeChildSession,
|
|
1002
|
+
safeOnStream
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
const flushStderrBuffer = (final) => {
|
|
1007
|
+
let nlIdx = stderrBuffer.indexOf("\n");
|
|
1008
|
+
while (nlIdx !== -1) {
|
|
1009
|
+
const line = stderrBuffer.slice(0, nlIdx);
|
|
1010
|
+
stderrBuffer = stderrBuffer.slice(nlIdx + 1);
|
|
1011
|
+
stderrRing.push(line);
|
|
1012
|
+
if (stderrRing.length > STDERR_RING_BUFFER_SIZE) stderrRing.shift();
|
|
1013
|
+
writeChildSession({ type: "subagent_stderr", line });
|
|
1014
|
+
safeOnStream({ kind: "stderr", subagentId, payload: { line } });
|
|
1015
|
+
nlIdx = stderrBuffer.indexOf("\n");
|
|
1016
|
+
}
|
|
1017
|
+
if (final && stderrBuffer.length > 0) {
|
|
1018
|
+
const line = stderrBuffer;
|
|
1019
|
+
stderrBuffer = "";
|
|
1020
|
+
stderrRing.push(line);
|
|
1021
|
+
if (stderrRing.length > STDERR_RING_BUFFER_SIZE) stderrRing.shift();
|
|
1022
|
+
writeChildSession({ type: "subagent_stderr", line });
|
|
1023
|
+
safeOnStream({ kind: "stderr", subagentId, payload: { line } });
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
753
1026
|
child.stdout?.on("data", (chunk) => {
|
|
754
|
-
|
|
1027
|
+
const text = chunk.toString("utf8");
|
|
1028
|
+
stdoutAccum += text;
|
|
1029
|
+
stdoutBuffer += text;
|
|
1030
|
+
flushStdoutBuffer(false);
|
|
755
1031
|
});
|
|
756
1032
|
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
|
-
});
|
|
1033
|
+
const text = chunk.toString("utf8");
|
|
1034
|
+
stderrAccum += text;
|
|
1035
|
+
stderrBuffer += text;
|
|
1036
|
+
flushStderrBuffer(false);
|
|
768
1037
|
});
|
|
1038
|
+
let terminating = false;
|
|
1039
|
+
let terminationReason = "natural";
|
|
1040
|
+
let terminatePromise = null;
|
|
1041
|
+
const terminateImpl = () => {
|
|
1042
|
+
if (terminatePromise !== null) return terminatePromise;
|
|
1043
|
+
terminating = true;
|
|
1044
|
+
terminationReason = "terminated";
|
|
1045
|
+
terminatePromise = terminateSubagent(child, grace ?? DEFAULT_TERMINATE_GRACE_MS);
|
|
1046
|
+
return terminatePromise;
|
|
1047
|
+
};
|
|
1048
|
+
const terminateSync = () => {
|
|
1049
|
+
if (terminating) return;
|
|
1050
|
+
terminating = true;
|
|
1051
|
+
terminationReason = "terminated";
|
|
1052
|
+
try {
|
|
1053
|
+
child.kill("SIGTERM");
|
|
1054
|
+
} catch {
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
const activeRecord = {
|
|
1058
|
+
child,
|
|
1059
|
+
subagentId,
|
|
1060
|
+
terminate: terminateSync
|
|
1061
|
+
};
|
|
1062
|
+
activeSubagents.add(activeRecord);
|
|
1063
|
+
ensureOrphanSweeperRegistered();
|
|
769
1064
|
if (task.signal !== void 0) {
|
|
770
|
-
|
|
771
|
-
|
|
1065
|
+
const onAbort = () => {
|
|
1066
|
+
void terminateImpl();
|
|
1067
|
+
};
|
|
1068
|
+
if (task.signal.aborted) {
|
|
1069
|
+
onAbort();
|
|
1070
|
+
} else {
|
|
1071
|
+
task.signal.addEventListener("abort", onAbort, { once: true });
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
const exitPromise = new Promise((resolve) => {
|
|
1075
|
+
child.on("close", (exitCode, signal) => {
|
|
1076
|
+
flushStdoutBuffer(true);
|
|
1077
|
+
flushStderrBuffer(true);
|
|
1078
|
+
const durationMs = Date.now() - startedAt.getTime();
|
|
1079
|
+
writeChildSession({
|
|
1080
|
+
type: "subagent_exit",
|
|
1081
|
+
code: exitCode,
|
|
1082
|
+
signal,
|
|
1083
|
+
reason: terminationReason,
|
|
1084
|
+
durationMs,
|
|
1085
|
+
nonJsonLineCount
|
|
1086
|
+
});
|
|
1087
|
+
activeSubagents.delete(activeRecord);
|
|
1088
|
+
const result2 = {
|
|
1089
|
+
code: exitCode,
|
|
1090
|
+
signal,
|
|
1091
|
+
childSessionPath,
|
|
1092
|
+
durationMs
|
|
1093
|
+
};
|
|
1094
|
+
safeOnStream({ kind: "exit", subagentId, payload: result2 });
|
|
1095
|
+
resolve(result2);
|
|
772
1096
|
});
|
|
1097
|
+
child.on("error", () => {
|
|
1098
|
+
});
|
|
1099
|
+
});
|
|
1100
|
+
const result = exitPromise.then(({ code }) => {
|
|
1101
|
+
let parsed;
|
|
1102
|
+
try {
|
|
1103
|
+
parsed = JSON.parse(stdoutAccum);
|
|
1104
|
+
} catch {
|
|
1105
|
+
}
|
|
1106
|
+
return { exitCode: code, stdout: stdoutAccum, stderr: stderrAccum, parsed };
|
|
1107
|
+
});
|
|
1108
|
+
const linkEntry = {
|
|
1109
|
+
type: "subagent_link",
|
|
1110
|
+
subagentId,
|
|
1111
|
+
taskId,
|
|
1112
|
+
childSessionPath,
|
|
1113
|
+
startedAt: startedAtIso
|
|
1114
|
+
};
|
|
1115
|
+
if (task.parentSessionPath !== void 0 && task.parentSessionPath.length > 0) {
|
|
1116
|
+
try {
|
|
1117
|
+
await writeSubagentLink(task.parentSessionPath, linkEntry);
|
|
1118
|
+
safeOnStream({ kind: "link", subagentId, payload: linkEntry });
|
|
1119
|
+
} catch {
|
|
1120
|
+
stderrRing.push(`[link] failed to write subagent_link to parent`);
|
|
1121
|
+
if (stderrRing.length > STDERR_RING_BUFFER_SIZE) stderrRing.shift();
|
|
1122
|
+
}
|
|
773
1123
|
}
|
|
774
1124
|
return {
|
|
1125
|
+
subagentId,
|
|
1126
|
+
taskId,
|
|
1127
|
+
childSessionPath,
|
|
775
1128
|
pid: child.pid ?? null,
|
|
1129
|
+
startedAt,
|
|
1130
|
+
exitPromise,
|
|
776
1131
|
result,
|
|
1132
|
+
terminate: terminateImpl,
|
|
777
1133
|
abort: () => {
|
|
778
|
-
|
|
779
|
-
}
|
|
1134
|
+
void terminateImpl();
|
|
1135
|
+
},
|
|
1136
|
+
recentStderr: () => stderrRing.slice()
|
|
780
1137
|
};
|
|
781
1138
|
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Race a set of subagent handles, returning the first one that exits.
|
|
1141
|
+
*
|
|
1142
|
+
* @remarks
|
|
1143
|
+
* Maps CANT's `parallel: race` construct (per ADR-035 §D6) onto the
|
|
1144
|
+
* canonical {@link spawnSubagent} contract. The losing handles are
|
|
1145
|
+
* gracefully terminated via {@link SubagentHandle.terminate} once the
|
|
1146
|
+
* first settles so no straggler children outlive the race.
|
|
1147
|
+
*
|
|
1148
|
+
* @param handles - Subagent handles to race.
|
|
1149
|
+
* @returns The {@link SubagentExitResult} of the first child to exit.
|
|
1150
|
+
* @throws When `handles` is empty (caller bug — a race over zero
|
|
1151
|
+
* children has no winner).
|
|
1152
|
+
*/
|
|
1153
|
+
static async raceSubagents(handles) {
|
|
1154
|
+
if (handles.length === 0) {
|
|
1155
|
+
throw new Error("PiHarness.raceSubagents: cannot race an empty handle list");
|
|
1156
|
+
}
|
|
1157
|
+
const tagged = handles.map(
|
|
1158
|
+
(handle, index) => handle.exitPromise.then((value) => ({ index, value }))
|
|
1159
|
+
);
|
|
1160
|
+
const winner = await Promise.race(tagged);
|
|
1161
|
+
const losers = [];
|
|
1162
|
+
for (let i = 0; i < handles.length; i += 1) {
|
|
1163
|
+
if (i === winner.index) continue;
|
|
1164
|
+
const loser = handles[i];
|
|
1165
|
+
if (loser === void 0) continue;
|
|
1166
|
+
losers.push(loser.terminate().catch(() => void 0));
|
|
1167
|
+
}
|
|
1168
|
+
await Promise.all(losers);
|
|
1169
|
+
return winner.value;
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Settle a set of subagent handles, returning a parallel array of
|
|
1173
|
+
* results.
|
|
1174
|
+
*
|
|
1175
|
+
* @remarks
|
|
1176
|
+
* Maps CANT's `parallel: settle` construct (per ADR-035 §D6) onto the
|
|
1177
|
+
* canonical {@link spawnSubagent} contract. Because
|
|
1178
|
+
* {@link SubagentHandle.exitPromise} never rejects, every entry in
|
|
1179
|
+
* the returned array is `{ status: 'fulfilled', value: ... }` under
|
|
1180
|
+
* normal operation; the `PromiseSettledResult` shape is preserved
|
|
1181
|
+
* for forward compatibility with future failure modes.
|
|
1182
|
+
*
|
|
1183
|
+
* @param handles - Subagent handles to settle.
|
|
1184
|
+
* @returns Parallel array of settled exit results, one per input.
|
|
1185
|
+
*/
|
|
1186
|
+
static async settleAllSubagents(handles) {
|
|
1187
|
+
return Promise.allSettled(handles.map((h) => h.exitPromise));
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Per-line stdout dispatcher used by the streaming buffer flusher.
|
|
1191
|
+
*
|
|
1192
|
+
* @remarks
|
|
1193
|
+
* Extracted as a private method so the line-handling logic stays
|
|
1194
|
+
* close to {@link spawnSubagent} but does not bloat the parent
|
|
1195
|
+
* function. Skips empty lines (a leading newline produces a zero-
|
|
1196
|
+
* length entry that has no semantic meaning).
|
|
1197
|
+
*/
|
|
1198
|
+
handleStdoutLine(rawLine, ctx) {
|
|
1199
|
+
const trimmed = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
1200
|
+
if (trimmed.length === 0) return;
|
|
1201
|
+
const lineNumber = ctx.increment();
|
|
1202
|
+
let parsed;
|
|
1203
|
+
try {
|
|
1204
|
+
parsed = JSON.parse(trimmed);
|
|
1205
|
+
} catch {
|
|
1206
|
+
ctx.incrementNonJson();
|
|
1207
|
+
ctx.writeChildSession({ type: "raw", lineNumber, line: trimmed });
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
ctx.writeChildSession({ type: "custom_message", lineNumber, payload: parsed });
|
|
1211
|
+
ctx.safeOnStream({
|
|
1212
|
+
kind: "message",
|
|
1213
|
+
subagentId: ctx.subagentId,
|
|
1214
|
+
lineNumber,
|
|
1215
|
+
payload: parsed
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
782
1218
|
// ── Settings ────────────────────────────────────────────────────────
|
|
783
|
-
/**
|
|
1219
|
+
/**
|
|
1220
|
+
* Read the Pi `settings.json` file for the resolved scope.
|
|
1221
|
+
*/
|
|
784
1222
|
async readSettings(scope) {
|
|
785
1223
|
const filePath = this.settingsPath(scope);
|
|
786
1224
|
if (!existsSync4(filePath)) return {};
|
|
@@ -791,7 +1229,10 @@ ${MARKER_END}`;
|
|
|
791
1229
|
return {};
|
|
792
1230
|
}
|
|
793
1231
|
}
|
|
794
|
-
/**
|
|
1232
|
+
/**
|
|
1233
|
+
* Merge a partial patch into the Pi `settings.json` file for the
|
|
1234
|
+
* resolved scope using an atomic write.
|
|
1235
|
+
*/
|
|
795
1236
|
async writeSettings(patch, scope) {
|
|
796
1237
|
const filePath = this.settingsPath(scope);
|
|
797
1238
|
const current = await this.readSettings(scope);
|
|
@@ -799,7 +1240,10 @@ ${MARKER_END}`;
|
|
|
799
1240
|
const merged = deepMerge(currentObj, patch);
|
|
800
1241
|
await atomicWriteJson(filePath, merged);
|
|
801
1242
|
}
|
|
802
|
-
/**
|
|
1243
|
+
/**
|
|
1244
|
+
* Persist the supplied model-name patterns into `settings.enabledModels`
|
|
1245
|
+
* at the resolved scope.
|
|
1246
|
+
*/
|
|
803
1247
|
async configureModels(modelPatterns, scope) {
|
|
804
1248
|
await this.writeSettings({ enabledModels: modelPatterns }, scope);
|
|
805
1249
|
}
|
|
@@ -824,7 +1268,10 @@ ${MARKER_END}`;
|
|
|
824
1268
|
return join5(getPiAgentDir2(), "sessions");
|
|
825
1269
|
}
|
|
826
1270
|
// ── Extensions (Wave-1, T263) ───────────────────────────────────────
|
|
827
|
-
/**
|
|
1271
|
+
/**
|
|
1272
|
+
* Install a Pi extension `.ts` source file into the resolved tier's
|
|
1273
|
+
* extensions directory, validating that it has a default export.
|
|
1274
|
+
*/
|
|
828
1275
|
async installExtension(sourcePath, name, tier, projectDir, opts) {
|
|
829
1276
|
if (!existsSync4(sourcePath)) {
|
|
830
1277
|
throw new Error(`installExtension: source file does not exist: ${sourcePath}`);
|
|
@@ -856,7 +1303,9 @@ ${MARKER_END}`;
|
|
|
856
1303
|
await writeFile(targetPath, contents, "utf8");
|
|
857
1304
|
return { targetPath, tier };
|
|
858
1305
|
}
|
|
859
|
-
/**
|
|
1306
|
+
/**
|
|
1307
|
+
* Remove a Pi extension `.ts` source file from the resolved tier.
|
|
1308
|
+
*/
|
|
860
1309
|
async removeExtension(name, tier, projectDir) {
|
|
861
1310
|
const dir = resolveTierDir({ tier, kind: "extensions", projectDir });
|
|
862
1311
|
const targetPath = join5(dir, `${name}.ts`);
|
|
@@ -864,7 +1313,10 @@ ${MARKER_END}`;
|
|
|
864
1313
|
await rm3(targetPath, { force: true });
|
|
865
1314
|
return true;
|
|
866
1315
|
}
|
|
867
|
-
/**
|
|
1316
|
+
/**
|
|
1317
|
+
* List Pi extension files across every tier in precedence order,
|
|
1318
|
+
* flagging shadowed entries from lower tiers.
|
|
1319
|
+
*/
|
|
868
1320
|
async listExtensions(projectDir) {
|
|
869
1321
|
const tiers = resolveAllTiers("extensions", projectDir);
|
|
870
1322
|
const out = [];
|
|
@@ -895,7 +1347,10 @@ ${MARKER_END}`;
|
|
|
895
1347
|
return out;
|
|
896
1348
|
}
|
|
897
1349
|
// ── Sessions (Wave-1, T264) ─────────────────────────────────────────
|
|
898
|
-
/**
|
|
1350
|
+
/**
|
|
1351
|
+
* List Pi session JSONL files (including subagent children when
|
|
1352
|
+
* requested), summarising only the header line per file.
|
|
1353
|
+
*/
|
|
899
1354
|
async listSessions(opts) {
|
|
900
1355
|
const rootDir = this.sessionsDir();
|
|
901
1356
|
if (!existsSync4(rootDir)) return [];
|
|
@@ -935,7 +1390,9 @@ ${MARKER_END}`;
|
|
|
935
1390
|
summaries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
936
1391
|
return summaries;
|
|
937
1392
|
}
|
|
938
|
-
/**
|
|
1393
|
+
/**
|
|
1394
|
+
* Show the full entries of a single Pi session by id.
|
|
1395
|
+
*/
|
|
939
1396
|
async showSession(id) {
|
|
940
1397
|
const summaries = await this.listSessions({ includeSubagents: true });
|
|
941
1398
|
const match = summaries.find((s) => s.id === id);
|
|
@@ -951,7 +1408,10 @@ ${MARKER_END}`;
|
|
|
951
1408
|
return { summary: match, entries };
|
|
952
1409
|
}
|
|
953
1410
|
// ── Models (Wave-1, T265) ───────────────────────────────────────────
|
|
954
|
-
/**
|
|
1411
|
+
/**
|
|
1412
|
+
* Read the Pi `models.json` file for the resolved scope, tolerating
|
|
1413
|
+
* missing or malformed files by returning an empty provider map.
|
|
1414
|
+
*/
|
|
955
1415
|
async readModelsConfig(scope) {
|
|
956
1416
|
const filePath = this.modelsConfigPath(scope);
|
|
957
1417
|
if (!existsSync4(filePath)) return { providers: {} };
|
|
@@ -977,12 +1437,18 @@ ${MARKER_END}`;
|
|
|
977
1437
|
return { providers: {} };
|
|
978
1438
|
}
|
|
979
1439
|
}
|
|
980
|
-
/**
|
|
1440
|
+
/**
|
|
1441
|
+
* Write the Pi `models.json` file for the resolved scope via an atomic
|
|
1442
|
+
* tmp-then-rename sequence.
|
|
1443
|
+
*/
|
|
981
1444
|
async writeModelsConfig(config, scope) {
|
|
982
1445
|
const filePath = this.modelsConfigPath(scope);
|
|
983
1446
|
await atomicWriteJson(filePath, config);
|
|
984
1447
|
}
|
|
985
|
-
/**
|
|
1448
|
+
/**
|
|
1449
|
+
* Compose a flat `ModelListEntry` list from `models.json` plus the
|
|
1450
|
+
* `enabledModels` and default-model hints in `settings.json`.
|
|
1451
|
+
*/
|
|
986
1452
|
async listModels(scope) {
|
|
987
1453
|
const models = await this.readModelsConfig(scope);
|
|
988
1454
|
const settings = await this.readSettings(scope);
|
|
@@ -1044,7 +1510,10 @@ ${MARKER_END}`;
|
|
|
1044
1510
|
return out;
|
|
1045
1511
|
}
|
|
1046
1512
|
// ── Prompts (Wave-1, T266) ──────────────────────────────────────────
|
|
1047
|
-
/**
|
|
1513
|
+
/**
|
|
1514
|
+
* Install a Pi prompt directory (containing `prompt.md`) into the
|
|
1515
|
+
* resolved tier's prompts directory.
|
|
1516
|
+
*/
|
|
1048
1517
|
async installPrompt(sourceDir, name, tier, projectDir, opts) {
|
|
1049
1518
|
if (!existsSync4(sourceDir)) {
|
|
1050
1519
|
throw new Error(`installPrompt: source directory does not exist: ${sourceDir}`);
|
|
@@ -1070,7 +1539,10 @@ ${MARKER_END}`;
|
|
|
1070
1539
|
await cp3(sourceDir, targetPath, { recursive: true });
|
|
1071
1540
|
return { targetPath, tier };
|
|
1072
1541
|
}
|
|
1073
|
-
/**
|
|
1542
|
+
/**
|
|
1543
|
+
* List Pi prompt directories across every tier in precedence order,
|
|
1544
|
+
* flagging shadowed entries from lower tiers.
|
|
1545
|
+
*/
|
|
1074
1546
|
async listPrompts(projectDir) {
|
|
1075
1547
|
const tiers = resolveAllTiers("prompts", projectDir);
|
|
1076
1548
|
const out = [];
|
|
@@ -1098,7 +1570,9 @@ ${MARKER_END}`;
|
|
|
1098
1570
|
}
|
|
1099
1571
|
return out;
|
|
1100
1572
|
}
|
|
1101
|
-
/**
|
|
1573
|
+
/**
|
|
1574
|
+
* Remove a Pi prompt directory from the resolved tier.
|
|
1575
|
+
*/
|
|
1102
1576
|
async removePrompt(name, tier, projectDir) {
|
|
1103
1577
|
const dir = resolveTierDir({ tier, kind: "prompts", projectDir });
|
|
1104
1578
|
const targetPath = join5(dir, name);
|
|
@@ -1107,7 +1581,11 @@ ${MARKER_END}`;
|
|
|
1107
1581
|
return true;
|
|
1108
1582
|
}
|
|
1109
1583
|
// ── Themes (Wave-1, T267) ───────────────────────────────────────────
|
|
1110
|
-
/**
|
|
1584
|
+
/**
|
|
1585
|
+
* Install a Pi theme file (`.ts`/`.tsx`/`.mts`/`.json`) into the
|
|
1586
|
+
* resolved tier's themes directory, blocking same-stem conflicts
|
|
1587
|
+
* unless `--force` is supplied.
|
|
1588
|
+
*/
|
|
1111
1589
|
async installTheme(sourceFile, name, tier, projectDir, opts) {
|
|
1112
1590
|
if (!existsSync4(sourceFile)) {
|
|
1113
1591
|
throw new Error(`installTheme: source file does not exist: ${sourceFile}`);
|
|
@@ -1146,7 +1624,10 @@ ${MARKER_END}`;
|
|
|
1146
1624
|
await writeFile(targetPath, contents);
|
|
1147
1625
|
return { targetPath, tier };
|
|
1148
1626
|
}
|
|
1149
|
-
/**
|
|
1627
|
+
/**
|
|
1628
|
+
* List Pi theme files across every tier in precedence order, flagging
|
|
1629
|
+
* shadowed entries from lower tiers.
|
|
1630
|
+
*/
|
|
1150
1631
|
async listThemes(projectDir) {
|
|
1151
1632
|
const tiers = resolveAllTiers("themes", projectDir);
|
|
1152
1633
|
const out = [];
|
|
@@ -1178,7 +1659,10 @@ ${MARKER_END}`;
|
|
|
1178
1659
|
}
|
|
1179
1660
|
return out;
|
|
1180
1661
|
}
|
|
1181
|
-
/**
|
|
1662
|
+
/**
|
|
1663
|
+
* Remove a Pi theme from the resolved tier, matching any of the
|
|
1664
|
+
* supported theme extensions for the given name stem.
|
|
1665
|
+
*/
|
|
1182
1666
|
async removeTheme(name, tier, projectDir) {
|
|
1183
1667
|
const dir = resolveTierDir({ tier, kind: "themes", projectDir });
|
|
1184
1668
|
let removed = false;
|
|
@@ -1191,7 +1675,187 @@ ${MARKER_END}`;
|
|
|
1191
1675
|
}
|
|
1192
1676
|
return removed;
|
|
1193
1677
|
}
|
|
1678
|
+
// ── CANT profiles (Wave-1, T276) ────────────────────────────────────
|
|
1679
|
+
/**
|
|
1680
|
+
* Install a `.cant` profile into the resolved tier after passing it
|
|
1681
|
+
* through the cant-core validator.
|
|
1682
|
+
*
|
|
1683
|
+
* Validates the source via {@link PiHarness.validateCantProfile}
|
|
1684
|
+
* before copying so we never persist a `.cant` file the runtime bridge
|
|
1685
|
+
* cannot load. The target layout is `<tier-root>/cant/<name>.cant`,
|
|
1686
|
+
* resolved through `resolveTierDir` so the project/user/global
|
|
1687
|
+
* hierarchy stays consistent with the other Wave-1 verbs.
|
|
1688
|
+
*/
|
|
1689
|
+
async installCantProfile(sourcePath, name, tier, projectDir, opts) {
|
|
1690
|
+
if (!existsSync4(sourcePath)) {
|
|
1691
|
+
throw new Error(`installCantProfile: source file does not exist: ${sourcePath}`);
|
|
1692
|
+
}
|
|
1693
|
+
const stats = await stat(sourcePath);
|
|
1694
|
+
if (!stats.isFile()) {
|
|
1695
|
+
throw new Error(`installCantProfile: source path is not a regular file: ${sourcePath}`);
|
|
1696
|
+
}
|
|
1697
|
+
const ext = extname(sourcePath);
|
|
1698
|
+
if (ext !== ".cant") {
|
|
1699
|
+
throw new Error(
|
|
1700
|
+
`installCantProfile: expected a CANT source file (.cant), got: ${ext || "(no extension)"}`
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
const validation = await this.validateCantProfile(sourcePath);
|
|
1704
|
+
if (!validation.valid) {
|
|
1705
|
+
const firstError = validation.errors.find((e) => e.severity === "error") ?? validation.errors[0];
|
|
1706
|
+
const detail = firstError !== void 0 ? ` (${firstError.ruleId} at ${firstError.line}:${firstError.col}: ${firstError.message})` : "";
|
|
1707
|
+
throw new Error(`installCantProfile: source file failed cant-core validation${detail}`);
|
|
1708
|
+
}
|
|
1709
|
+
const dir = resolveTierDir({ tier, kind: "cant", projectDir });
|
|
1710
|
+
const targetPath = join5(dir, `${name}.cant`);
|
|
1711
|
+
if (existsSync4(targetPath) && opts?.force !== true) {
|
|
1712
|
+
throw new Error(
|
|
1713
|
+
`installCantProfile: target already exists at ${targetPath} (pass --force to overwrite)`
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
const contents = await readFile(sourcePath);
|
|
1717
|
+
await mkdir3(dir, { recursive: true });
|
|
1718
|
+
await writeFile(targetPath, contents);
|
|
1719
|
+
return { targetPath, tier, counts: validation.counts };
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Remove a `.cant` profile from the resolved tier.
|
|
1723
|
+
*/
|
|
1724
|
+
async removeCantProfile(name, tier, projectDir) {
|
|
1725
|
+
const dir = resolveTierDir({ tier, kind: "cant", projectDir });
|
|
1726
|
+
const targetPath = join5(dir, `${name}.cant`);
|
|
1727
|
+
if (!existsSync4(targetPath)) return false;
|
|
1728
|
+
await rm3(targetPath, { force: true });
|
|
1729
|
+
return true;
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* List installed `.cant` profiles across every tier in precedence
|
|
1733
|
+
* order, parsing each file to extract section counts.
|
|
1734
|
+
*
|
|
1735
|
+
* Walks every tier in `TIER_PRECEDENCE` order, parsing each
|
|
1736
|
+
* discovered `.cant` file via cant-core to extract a
|
|
1737
|
+
* {@link CantProfileCounts} bag. Higher-precedence tiers shadow
|
|
1738
|
+
* lower-precedence entries with the same name; shadowed entries
|
|
1739
|
+
* still appear in the result but carry the
|
|
1740
|
+
* `shadowedByHigherTier` flag so callers can render the precedence
|
|
1741
|
+
* story without losing visibility of the duplicate.
|
|
1742
|
+
*/
|
|
1743
|
+
async listCantProfiles(projectDir) {
|
|
1744
|
+
const tiers = resolveAllTiers("cant", projectDir);
|
|
1745
|
+
const out = [];
|
|
1746
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
1747
|
+
for (const { tier, dir } of tiers) {
|
|
1748
|
+
if (!existsSync4(dir)) continue;
|
|
1749
|
+
let entries;
|
|
1750
|
+
try {
|
|
1751
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1752
|
+
} catch {
|
|
1753
|
+
continue;
|
|
1754
|
+
}
|
|
1755
|
+
for (const entry of entries) {
|
|
1756
|
+
if (!entry.isFile()) continue;
|
|
1757
|
+
const fileName = entry.name;
|
|
1758
|
+
if (!fileName.endsWith(".cant")) continue;
|
|
1759
|
+
const name = fileName.slice(0, -".cant".length);
|
|
1760
|
+
const sourcePath = join5(dir, fileName);
|
|
1761
|
+
const counts = await extractCantCounts(sourcePath);
|
|
1762
|
+
const shadowed = seenNames.has(name);
|
|
1763
|
+
const profile = {
|
|
1764
|
+
name,
|
|
1765
|
+
tier,
|
|
1766
|
+
sourcePath,
|
|
1767
|
+
counts
|
|
1768
|
+
};
|
|
1769
|
+
if (shadowed) {
|
|
1770
|
+
profile.shadowedByHigherTier = true;
|
|
1771
|
+
}
|
|
1772
|
+
out.push(profile);
|
|
1773
|
+
seenNames.add(name);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
return out;
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Validate a `.cant` source file against cant-core's parser and
|
|
1780
|
+
* 42-rule linter, returning section counts and per-diagnostic detail.
|
|
1781
|
+
*
|
|
1782
|
+
* Pure validator. Reads the file, runs `parseDocument` to derive
|
|
1783
|
+
* counts (when parsing succeeds) and `validateDocument` to collect
|
|
1784
|
+
* the 42-rule diagnostic feed. The two calls are kept independent so
|
|
1785
|
+
* we can still report counts for files that pass parsing but fail a
|
|
1786
|
+
* lint rule.
|
|
1787
|
+
*/
|
|
1788
|
+
async validateCantProfile(sourcePath) {
|
|
1789
|
+
if (!existsSync4(sourcePath)) {
|
|
1790
|
+
throw new Error(`validateCantProfile: source file does not exist: ${sourcePath}`);
|
|
1791
|
+
}
|
|
1792
|
+
const stats = await stat(sourcePath);
|
|
1793
|
+
if (!stats.isFile()) {
|
|
1794
|
+
throw new Error(`validateCantProfile: source path is not a regular file: ${sourcePath}`);
|
|
1795
|
+
}
|
|
1796
|
+
const counts = await extractCantCounts(sourcePath);
|
|
1797
|
+
const validation = await validateDocument(sourcePath);
|
|
1798
|
+
const errors = validation.diagnostics.map((d) => ({
|
|
1799
|
+
ruleId: d.ruleId,
|
|
1800
|
+
message: d.message,
|
|
1801
|
+
line: d.line,
|
|
1802
|
+
col: d.col,
|
|
1803
|
+
severity: normaliseSeverity(d.severity)
|
|
1804
|
+
}));
|
|
1805
|
+
return {
|
|
1806
|
+
valid: validation.valid,
|
|
1807
|
+
errors,
|
|
1808
|
+
counts
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1194
1811
|
};
|
|
1812
|
+
async function terminateSubagent(child, graceMs) {
|
|
1813
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
try {
|
|
1817
|
+
child.kill("SIGTERM");
|
|
1818
|
+
} catch {
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
const pollInterval = Math.min(25, Math.max(1, graceMs));
|
|
1822
|
+
const deadline = Date.now() + graceMs;
|
|
1823
|
+
await new Promise((resolve) => {
|
|
1824
|
+
const timer = setInterval(() => {
|
|
1825
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
1826
|
+
clearInterval(timer);
|
|
1827
|
+
resolve();
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
if (Date.now() >= deadline) {
|
|
1831
|
+
clearInterval(timer);
|
|
1832
|
+
try {
|
|
1833
|
+
child.kill("SIGKILL");
|
|
1834
|
+
} catch {
|
|
1835
|
+
}
|
|
1836
|
+
resolve();
|
|
1837
|
+
}
|
|
1838
|
+
}, pollInterval);
|
|
1839
|
+
child.once("close", () => {
|
|
1840
|
+
clearInterval(timer);
|
|
1841
|
+
resolve();
|
|
1842
|
+
});
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
async function writeSubagentLink(parentSessionPath, entry) {
|
|
1846
|
+
await mkdir3(dirname2(parentSessionPath), { recursive: true });
|
|
1847
|
+
const wrapped = {
|
|
1848
|
+
type: "custom",
|
|
1849
|
+
subtype: entry.type,
|
|
1850
|
+
subagentId: entry.subagentId,
|
|
1851
|
+
taskId: entry.taskId,
|
|
1852
|
+
childSessionPath: entry.childSessionPath,
|
|
1853
|
+
startedAt: entry.startedAt
|
|
1854
|
+
};
|
|
1855
|
+
const line = `${JSON.stringify(wrapped)}
|
|
1856
|
+
`;
|
|
1857
|
+
await appendFile(parentSessionPath, line, "utf8");
|
|
1858
|
+
}
|
|
1195
1859
|
async function readSessionHeader(filePath) {
|
|
1196
1860
|
let handle = null;
|
|
1197
1861
|
try {
|
|
@@ -1243,6 +1907,104 @@ async function readSessionHeader(filePath) {
|
|
|
1243
1907
|
}
|
|
1244
1908
|
}
|
|
1245
1909
|
}
|
|
1910
|
+
var EMPTY_CANT_COUNTS = {
|
|
1911
|
+
agentCount: 0,
|
|
1912
|
+
workflowCount: 0,
|
|
1913
|
+
pipelineCount: 0,
|
|
1914
|
+
hookCount: 0,
|
|
1915
|
+
skillCount: 0
|
|
1916
|
+
};
|
|
1917
|
+
function isRecord(v) {
|
|
1918
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1919
|
+
}
|
|
1920
|
+
function unwrapSpanned(value) {
|
|
1921
|
+
if (typeof value === "string") return value;
|
|
1922
|
+
if (isRecord(value) && typeof value["value"] === "string") {
|
|
1923
|
+
return value["value"];
|
|
1924
|
+
}
|
|
1925
|
+
return null;
|
|
1926
|
+
}
|
|
1927
|
+
function collectSkillNames(value, out) {
|
|
1928
|
+
if (!isRecord(value)) return;
|
|
1929
|
+
const arr = value["Array"];
|
|
1930
|
+
if (!Array.isArray(arr)) return;
|
|
1931
|
+
for (const item of arr) {
|
|
1932
|
+
if (!isRecord(item)) continue;
|
|
1933
|
+
const stringWrapper = item["String"];
|
|
1934
|
+
if (isRecord(stringWrapper) && typeof stringWrapper["raw"] === "string") {
|
|
1935
|
+
out.add(stringWrapper["raw"]);
|
|
1936
|
+
continue;
|
|
1937
|
+
}
|
|
1938
|
+
const identWrapper = item["Identifier"];
|
|
1939
|
+
if (typeof identWrapper === "string") {
|
|
1940
|
+
out.add(identWrapper);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
async function extractCantCounts(sourcePath) {
|
|
1945
|
+
let parsed;
|
|
1946
|
+
try {
|
|
1947
|
+
parsed = await parseDocument(sourcePath);
|
|
1948
|
+
} catch {
|
|
1949
|
+
return { ...EMPTY_CANT_COUNTS };
|
|
1950
|
+
}
|
|
1951
|
+
if (!parsed.success || !isRecord(parsed.document)) {
|
|
1952
|
+
return { ...EMPTY_CANT_COUNTS };
|
|
1953
|
+
}
|
|
1954
|
+
const sections = parsed.document["sections"];
|
|
1955
|
+
if (!Array.isArray(sections)) {
|
|
1956
|
+
return { ...EMPTY_CANT_COUNTS };
|
|
1957
|
+
}
|
|
1958
|
+
let agentCount = 0;
|
|
1959
|
+
let workflowCount = 0;
|
|
1960
|
+
let pipelineCount = 0;
|
|
1961
|
+
let hookCount = 0;
|
|
1962
|
+
const skillNames = /* @__PURE__ */ new Set();
|
|
1963
|
+
for (const section of sections) {
|
|
1964
|
+
if (!isRecord(section)) continue;
|
|
1965
|
+
if (isRecord(section["Agent"])) {
|
|
1966
|
+
agentCount += 1;
|
|
1967
|
+
const agent = section["Agent"];
|
|
1968
|
+
const hooks = agent["hooks"];
|
|
1969
|
+
if (Array.isArray(hooks)) {
|
|
1970
|
+
hookCount += hooks.length;
|
|
1971
|
+
}
|
|
1972
|
+
const properties = agent["properties"];
|
|
1973
|
+
if (Array.isArray(properties)) {
|
|
1974
|
+
for (const prop of properties) {
|
|
1975
|
+
if (!isRecord(prop)) continue;
|
|
1976
|
+
const key = unwrapSpanned(prop["key"]);
|
|
1977
|
+
if (key === "skills") {
|
|
1978
|
+
collectSkillNames(prop["value"], skillNames);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
continue;
|
|
1983
|
+
}
|
|
1984
|
+
if (isRecord(section["Workflow"])) {
|
|
1985
|
+
workflowCount += 1;
|
|
1986
|
+
continue;
|
|
1987
|
+
}
|
|
1988
|
+
if (isRecord(section["Pipeline"])) {
|
|
1989
|
+
pipelineCount += 1;
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1992
|
+
if (isRecord(section["Hook"])) {
|
|
1993
|
+
hookCount += 1;
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
return {
|
|
1997
|
+
agentCount,
|
|
1998
|
+
workflowCount,
|
|
1999
|
+
pipelineCount,
|
|
2000
|
+
hookCount,
|
|
2001
|
+
skillCount: skillNames.size
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
function normaliseSeverity(raw) {
|
|
2005
|
+
if (raw === "warning" || raw === "info" || raw === "hint") return raw;
|
|
2006
|
+
return "error";
|
|
2007
|
+
}
|
|
1246
2008
|
|
|
1247
2009
|
// src/core/harness/index.ts
|
|
1248
2010
|
function getHarnessFor(provider) {
|
|
@@ -1262,28 +2024,67 @@ function getAllHarnesses() {
|
|
|
1262
2024
|
}
|
|
1263
2025
|
return result;
|
|
1264
2026
|
}
|
|
1265
|
-
function resolveDefaultTargetProviders() {
|
|
2027
|
+
function resolveDefaultTargetProviders(options = {}) {
|
|
2028
|
+
const mode = getExclusivityMode();
|
|
1266
2029
|
let primary = null;
|
|
1267
2030
|
try {
|
|
1268
2031
|
primary = getPrimaryHarness();
|
|
1269
2032
|
} catch {
|
|
1270
2033
|
primary = null;
|
|
1271
2034
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
2035
|
+
let installed;
|
|
2036
|
+
try {
|
|
2037
|
+
installed = getInstalledProviders();
|
|
2038
|
+
} catch {
|
|
2039
|
+
installed = [];
|
|
2040
|
+
}
|
|
2041
|
+
const primaryId = primary?.provider.id ?? null;
|
|
2042
|
+
const primaryInstalled = primaryId !== null && installed.some((provider) => provider.id === primaryId);
|
|
2043
|
+
const explicit = options.explicit;
|
|
2044
|
+
const explicitContainsPrimary = explicit !== void 0 && primaryId !== null ? explicit.some((provider) => provider.id === primaryId) : false;
|
|
2045
|
+
const legacyFallback = () => {
|
|
2046
|
+
if (primary !== null && primaryInstalled) {
|
|
1277
2047
|
return [primary.provider];
|
|
1278
2048
|
}
|
|
2049
|
+
const highTier = installed.filter(
|
|
2050
|
+
(provider) => provider.priority === "primary" || provider.priority === "high"
|
|
2051
|
+
);
|
|
2052
|
+
if (highTier.length > 0) {
|
|
2053
|
+
return highTier;
|
|
2054
|
+
}
|
|
2055
|
+
return installed;
|
|
2056
|
+
};
|
|
2057
|
+
if (mode === "force-pi") {
|
|
2058
|
+
if (primary === null || !primaryInstalled) {
|
|
2059
|
+
throw new PiRequiredError();
|
|
2060
|
+
}
|
|
2061
|
+
return [primary.provider];
|
|
1279
2062
|
}
|
|
1280
|
-
|
|
1281
|
-
(
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
return
|
|
2063
|
+
if (mode === "legacy") {
|
|
2064
|
+
if (explicit !== void 0) {
|
|
2065
|
+
return explicit;
|
|
2066
|
+
}
|
|
2067
|
+
return legacyFallback();
|
|
2068
|
+
}
|
|
2069
|
+
if (explicit !== void 0 && explicit.length > 0 && !explicitContainsPrimary && primaryInstalled && !hasExplicitNonPiAutoWarned()) {
|
|
2070
|
+
console.warn(
|
|
2071
|
+
"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'."
|
|
2072
|
+
);
|
|
2073
|
+
markExplicitNonPiAutoWarned();
|
|
2074
|
+
}
|
|
2075
|
+
if (explicit !== void 0) {
|
|
2076
|
+
return explicit;
|
|
2077
|
+
}
|
|
2078
|
+
if (primary !== null && primaryInstalled) {
|
|
2079
|
+
return [primary.provider];
|
|
2080
|
+
}
|
|
2081
|
+
if (!hasPiAbsentAutoWarned()) {
|
|
2082
|
+
console.warn(
|
|
2083
|
+
"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."
|
|
2084
|
+
);
|
|
2085
|
+
markPiAbsentAutoWarned();
|
|
1285
2086
|
}
|
|
1286
|
-
return
|
|
2087
|
+
return legacyFallback();
|
|
1287
2088
|
}
|
|
1288
2089
|
async function dispatchInstallSkillAcrossProviders(sourcePath, skillName, providers, isGlobal, projectDir) {
|
|
1289
2090
|
const harnessTargets = [];
|
|
@@ -3817,6 +4618,13 @@ export {
|
|
|
3817
4618
|
selectProvidersByMinimumPriority,
|
|
3818
4619
|
installBatchWithRollback,
|
|
3819
4620
|
updateInstructionsSingleOperation,
|
|
4621
|
+
DEFAULT_EXCLUSIVITY_MODE,
|
|
4622
|
+
EXCLUSIVITY_MODE_ENV_VAR,
|
|
4623
|
+
PiRequiredError,
|
|
4624
|
+
isExclusivityMode,
|
|
4625
|
+
getExclusivityMode,
|
|
4626
|
+
setExclusivityMode,
|
|
4627
|
+
resetExclusivityModeOverride,
|
|
3820
4628
|
PiHarness,
|
|
3821
4629
|
getHarnessFor,
|
|
3822
4630
|
getPrimaryHarness,
|
|
@@ -3878,4 +4686,4 @@ export {
|
|
|
3878
4686
|
discoverSkillsMulti,
|
|
3879
4687
|
validateSkill
|
|
3880
4688
|
};
|
|
3881
|
-
//# sourceMappingURL=chunk-
|
|
4689
|
+
//# sourceMappingURL=chunk-XVZT7K6F.js.map
|