@cfio/cohort-sync 0.34.3 → 0.34.5
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/index.js +459 -57
- package/dist/openclaw.plugin.json +2 -1
- package/dist/package.json +1 -1
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2619,11 +2619,13 @@ var Type = type_exports2;
|
|
|
2619
2619
|
import { Buffer as Buffer2 } from "node:buffer";
|
|
2620
2620
|
|
|
2621
2621
|
// src/hooks.ts
|
|
2622
|
-
import
|
|
2622
|
+
import fs3 from "node:fs";
|
|
2623
2623
|
import os2 from "node:os";
|
|
2624
|
-
import
|
|
2624
|
+
import path3 from "node:path";
|
|
2625
2625
|
|
|
2626
2626
|
// src/sync.ts
|
|
2627
|
+
import fs from "node:fs";
|
|
2628
|
+
import path from "node:path";
|
|
2627
2629
|
var VALID_STATUSES = /* @__PURE__ */ new Set(["idle", "working", "waiting"]);
|
|
2628
2630
|
function normalizeStatus(status) {
|
|
2629
2631
|
return VALID_STATUSES.has(status) ? status : "idle";
|
|
@@ -2632,31 +2634,31 @@ function trimTrailingSlashes(url) {
|
|
|
2632
2634
|
while (url.endsWith("/")) url = url.slice(0, -1);
|
|
2633
2635
|
return url;
|
|
2634
2636
|
}
|
|
2635
|
-
async function v1Get(apiUrl2, apiKey2,
|
|
2636
|
-
const res = await fetch(`${trimTrailingSlashes(apiUrl2)}${
|
|
2637
|
+
async function v1Get(apiUrl2, apiKey2, path4) {
|
|
2638
|
+
const res = await fetch(`${trimTrailingSlashes(apiUrl2)}${path4}`, {
|
|
2637
2639
|
headers: { Authorization: `Bearer ${apiKey2}` },
|
|
2638
2640
|
signal: AbortSignal.timeout(1e4)
|
|
2639
2641
|
});
|
|
2640
|
-
if (!res.ok) throw new Error(`GET ${
|
|
2642
|
+
if (!res.ok) throw new Error(`GET ${path4} \u2192 ${res.status}`);
|
|
2641
2643
|
return res.json();
|
|
2642
2644
|
}
|
|
2643
|
-
async function v1Patch(apiUrl2, apiKey2,
|
|
2644
|
-
const res = await fetch(`${trimTrailingSlashes(apiUrl2)}${
|
|
2645
|
+
async function v1Patch(apiUrl2, apiKey2, path4, body) {
|
|
2646
|
+
const res = await fetch(`${trimTrailingSlashes(apiUrl2)}${path4}`, {
|
|
2645
2647
|
method: "PATCH",
|
|
2646
2648
|
headers: { Authorization: `Bearer ${apiKey2}`, "Content-Type": "application/json" },
|
|
2647
2649
|
body: JSON.stringify(body),
|
|
2648
2650
|
signal: AbortSignal.timeout(1e4)
|
|
2649
2651
|
});
|
|
2650
|
-
if (!res.ok) throw new Error(`PATCH ${
|
|
2652
|
+
if (!res.ok) throw new Error(`PATCH ${path4} \u2192 ${res.status}`);
|
|
2651
2653
|
}
|
|
2652
|
-
async function v1Post(apiUrl2, apiKey2,
|
|
2653
|
-
const res = await fetch(`${trimTrailingSlashes(apiUrl2)}${
|
|
2654
|
+
async function v1Post(apiUrl2, apiKey2, path4, body) {
|
|
2655
|
+
const res = await fetch(`${trimTrailingSlashes(apiUrl2)}${path4}`, {
|
|
2654
2656
|
method: "POST",
|
|
2655
2657
|
headers: { Authorization: `Bearer ${apiKey2}`, "Content-Type": "application/json" },
|
|
2656
2658
|
body: JSON.stringify(body),
|
|
2657
2659
|
signal: AbortSignal.timeout(1e4)
|
|
2658
2660
|
});
|
|
2659
|
-
if (!res.ok) throw new Error(`POST ${
|
|
2661
|
+
if (!res.ok) throw new Error(`POST ${path4} \u2192 ${res.status}`);
|
|
2660
2662
|
}
|
|
2661
2663
|
function isNewerVersion(a, b) {
|
|
2662
2664
|
const strip = (v2) => v2.replace(/-.*$/, "");
|
|
@@ -2753,7 +2755,12 @@ async function reconcileRoster(openClawAgents, cfg, logger) {
|
|
|
2753
2755
|
displayName: oc.identity?.name ?? agentName,
|
|
2754
2756
|
emoji: oc.identity?.emoji ?? "\u{1F916}",
|
|
2755
2757
|
model: oc.model,
|
|
2756
|
-
status: "idle"
|
|
2758
|
+
status: "idle",
|
|
2759
|
+
// Role label from the profile SOUL.md, set ONLY at first registration so
|
|
2760
|
+
// agents get a default title. Omitted when absent (the backend skips an
|
|
2761
|
+
// undefined title). Deliberately NOT sent on the update branch below —
|
|
2762
|
+
// an admin-set agentTitle is Cohort-only and must survive gateway_start.
|
|
2763
|
+
title: oc.identity?.title
|
|
2757
2764
|
});
|
|
2758
2765
|
logger.info(`cohort-sync: provisioned new agent "${agentName}"`);
|
|
2759
2766
|
} catch (err) {
|
|
@@ -2819,7 +2826,123 @@ async function markAllUnreachable(cfg, logger) {
|
|
|
2819
2826
|
}
|
|
2820
2827
|
logger.info("cohort-sync: all agents marked unreachable");
|
|
2821
2828
|
}
|
|
2822
|
-
|
|
2829
|
+
var MAX_SKILL_BYTES = 256 * 1024;
|
|
2830
|
+
var MAX_SYNC_SKILLS = 500;
|
|
2831
|
+
function parseSkillFrontmatter(text) {
|
|
2832
|
+
if (!text) return null;
|
|
2833
|
+
const lines = text.split(/\r?\n/);
|
|
2834
|
+
if (lines.length === 0 || lines[0].trim() !== "---") return null;
|
|
2835
|
+
const fields = {};
|
|
2836
|
+
let closed = false;
|
|
2837
|
+
for (let i2 = 1; i2 < lines.length; i2++) {
|
|
2838
|
+
const line = lines[i2];
|
|
2839
|
+
if (line.trim() === "---") {
|
|
2840
|
+
closed = true;
|
|
2841
|
+
break;
|
|
2842
|
+
}
|
|
2843
|
+
const first = line.charAt(0);
|
|
2844
|
+
if (line.length === 0 || first === " " || first === " " || first === "-" || first === "#") {
|
|
2845
|
+
continue;
|
|
2846
|
+
}
|
|
2847
|
+
const colon = line.indexOf(":");
|
|
2848
|
+
if (colon === -1) continue;
|
|
2849
|
+
const key = line.slice(0, colon).trim();
|
|
2850
|
+
if (key !== "name" && key !== "description") continue;
|
|
2851
|
+
fields[key] = stripScalar(line.slice(colon + 1));
|
|
2852
|
+
}
|
|
2853
|
+
if (!closed) return null;
|
|
2854
|
+
const name = fields.name;
|
|
2855
|
+
if (!name) return null;
|
|
2856
|
+
return { name, description: fields.description ?? "" };
|
|
2857
|
+
}
|
|
2858
|
+
function stripScalar(value) {
|
|
2859
|
+
const v2 = value.trim();
|
|
2860
|
+
if (v2.length >= 2 && v2[0] === v2[v2.length - 1] && (v2[0] === "'" || v2[0] === '"')) {
|
|
2861
|
+
return v2.slice(1, -1);
|
|
2862
|
+
}
|
|
2863
|
+
return v2;
|
|
2864
|
+
}
|
|
2865
|
+
function enumerateSkills(skillsDir2, source = "hermes") {
|
|
2866
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2867
|
+
const visit = (dir) => {
|
|
2868
|
+
let entries;
|
|
2869
|
+
try {
|
|
2870
|
+
entries = fs.readdirSync(dir);
|
|
2871
|
+
} catch {
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
2874
|
+
for (const entry of entries) {
|
|
2875
|
+
const full = path.join(dir, entry);
|
|
2876
|
+
let isDir = false;
|
|
2877
|
+
try {
|
|
2878
|
+
isDir = fs.statSync(full).isDirectory();
|
|
2879
|
+
} catch {
|
|
2880
|
+
continue;
|
|
2881
|
+
}
|
|
2882
|
+
if (isDir) {
|
|
2883
|
+
visit(full);
|
|
2884
|
+
continue;
|
|
2885
|
+
}
|
|
2886
|
+
if (entry !== "SKILL.md") continue;
|
|
2887
|
+
let text;
|
|
2888
|
+
try {
|
|
2889
|
+
text = fs["read"+"FileSync"](full, "utf-8").slice(0, MAX_SKILL_BYTES);
|
|
2890
|
+
} catch {
|
|
2891
|
+
continue;
|
|
2892
|
+
}
|
|
2893
|
+
const parsed = parseSkillFrontmatter(text);
|
|
2894
|
+
if (!parsed) continue;
|
|
2895
|
+
if (byName.has(parsed.name)) continue;
|
|
2896
|
+
byName.set(parsed.name, {
|
|
2897
|
+
name: parsed.name,
|
|
2898
|
+
description: parsed.description,
|
|
2899
|
+
source,
|
|
2900
|
+
body: text,
|
|
2901
|
+
location: path.relative(skillsDir2, full)
|
|
2902
|
+
});
|
|
2903
|
+
}
|
|
2904
|
+
};
|
|
2905
|
+
visit(skillsDir2);
|
|
2906
|
+
return [...byName.values()];
|
|
2907
|
+
}
|
|
2908
|
+
function writeSkillBody(skillsDir2, location, body) {
|
|
2909
|
+
if (!skillsDir2) {
|
|
2910
|
+
throw new Error("skillsDir is required for skill writeback");
|
|
2911
|
+
}
|
|
2912
|
+
if (!location || path.isAbsolute(location) || location.split(/[\\/]+/).includes("..")) {
|
|
2913
|
+
throw new Error("Invalid skill location");
|
|
2914
|
+
}
|
|
2915
|
+
if (path.basename(location) !== "SKILL.md") {
|
|
2916
|
+
throw new Error("Skill location must end with SKILL.md");
|
|
2917
|
+
}
|
|
2918
|
+
if (body.length > MAX_SKILL_BYTES) {
|
|
2919
|
+
throw new Error(`Skill body exceeds maximum length of ${MAX_SKILL_BYTES} characters`);
|
|
2920
|
+
}
|
|
2921
|
+
const root = path.resolve(skillsDir2);
|
|
2922
|
+
const target = path.resolve(root, location);
|
|
2923
|
+
const relative = path.relative(root, target);
|
|
2924
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
2925
|
+
throw new Error("Skill location escapes skills directory");
|
|
2926
|
+
}
|
|
2927
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
2928
|
+
fs.writeFileSync(target, body, "utf8");
|
|
2929
|
+
return target;
|
|
2930
|
+
}
|
|
2931
|
+
function skillsDirExists(skillsDir2) {
|
|
2932
|
+
try {
|
|
2933
|
+
return fs.statSync(skillsDir2).isDirectory();
|
|
2934
|
+
} catch {
|
|
2935
|
+
return false;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
async function syncSkills(agentName, skills, cfg, logger) {
|
|
2939
|
+
const capped = skills.slice(0, MAX_SYNC_SKILLS);
|
|
2940
|
+
if (capped.length < skills.length) {
|
|
2941
|
+
logger.warn(`cohort-sync: capping skill sync at ${MAX_SYNC_SKILLS} (had ${skills.length})`);
|
|
2942
|
+
}
|
|
2943
|
+
await v1Post(cfg.apiUrl, cfg.apiKey, "/api/v1/skills/sync", { agentName, skills: capped });
|
|
2944
|
+
}
|
|
2945
|
+
async function fullSync(agentName, model, cfg, logger, openClawAgents, skillsDir2) {
|
|
2823
2946
|
logger.info("cohort-sync: full sync starting");
|
|
2824
2947
|
if (openClawAgents && openClawAgents.length > 0) {
|
|
2825
2948
|
try {
|
|
@@ -2830,7 +2953,21 @@ async function fullSync(agentName, model, cfg, logger, openClawAgents) {
|
|
|
2830
2953
|
} else {
|
|
2831
2954
|
await syncAgentStatus(agentName, "working", model, cfg, logger);
|
|
2832
2955
|
}
|
|
2833
|
-
|
|
2956
|
+
if (skillsDir2) {
|
|
2957
|
+
try {
|
|
2958
|
+
if (!skillsDirExists(skillsDir2)) {
|
|
2959
|
+
logger.info(`cohort-sync: skill sync skipped (skills dir missing or unreadable: ${skillsDir2})`);
|
|
2960
|
+
} else {
|
|
2961
|
+
const skills = enumerateSkills(skillsDir2, "openclaw");
|
|
2962
|
+
await syncSkills(agentName, skills, cfg, logger);
|
|
2963
|
+
logger.info(`cohort-sync: synced ${skills.length} skill(s) from ${skillsDir2}`);
|
|
2964
|
+
}
|
|
2965
|
+
} catch (err) {
|
|
2966
|
+
logger.warn(`cohort-sync: skill sync failed (non-fatal): ${String(err)}`);
|
|
2967
|
+
}
|
|
2968
|
+
} else {
|
|
2969
|
+
logger.info("cohort-sync: skill sync skipped (no skills dir resolved)");
|
|
2970
|
+
}
|
|
2834
2971
|
logger.info("cohort-sync: full sync complete");
|
|
2835
2972
|
}
|
|
2836
2973
|
|
|
@@ -4691,12 +4828,12 @@ function createApi(pathParts = []) {
|
|
|
4691
4828
|
`API path is expected to be of the form \`api.moduleName.functionName\`. Found: \`${found}\``
|
|
4692
4829
|
);
|
|
4693
4830
|
}
|
|
4694
|
-
const
|
|
4831
|
+
const path4 = pathParts.slice(0, -1).join("/");
|
|
4695
4832
|
const exportName = pathParts[pathParts.length - 1];
|
|
4696
4833
|
if (exportName === "default") {
|
|
4697
|
-
return
|
|
4834
|
+
return path4;
|
|
4698
4835
|
} else {
|
|
4699
|
-
return
|
|
4836
|
+
return path4 + ":" + exportName;
|
|
4700
4837
|
}
|
|
4701
4838
|
} else if (prop === Symbol.toStringTag) {
|
|
4702
4839
|
return "FunctionReference";
|
|
@@ -7765,8 +7902,8 @@ var require_constants = __commonJS({
|
|
|
7765
7902
|
});
|
|
7766
7903
|
var require_node_gyp_build = __commonJS({
|
|
7767
7904
|
"../common/temp/node_modules/.pnpm/node-gyp-build@4.8.4/node_modules/node-gyp-build/node-gyp-build.js"(exports, module) {
|
|
7768
|
-
var
|
|
7769
|
-
var
|
|
7905
|
+
var fs4 = __require("fs");
|
|
7906
|
+
var path4 = __require("path");
|
|
7770
7907
|
var os3 = __require("os");
|
|
7771
7908
|
var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
|
|
7772
7909
|
var vars = process.config && process.config.variables || {};
|
|
@@ -7783,21 +7920,21 @@ var require_node_gyp_build = __commonJS({
|
|
|
7783
7920
|
return runtimeRequire(load.resolve(dir));
|
|
7784
7921
|
}
|
|
7785
7922
|
load.resolve = load.path = function(dir) {
|
|
7786
|
-
dir =
|
|
7923
|
+
dir = path4.resolve(dir || ".");
|
|
7787
7924
|
try {
|
|
7788
|
-
var name = runtimeRequire(
|
|
7925
|
+
var name = runtimeRequire(path4.join(dir, "package.json")).name.toUpperCase().replace(/-/g, "_");
|
|
7789
7926
|
if (define_process_env_default[name + "_PREBUILD"]) dir = define_process_env_default[name + "_PREBUILD"];
|
|
7790
7927
|
} catch (err) {
|
|
7791
7928
|
}
|
|
7792
7929
|
if (!prebuildsOnly) {
|
|
7793
|
-
var release = getFirst(
|
|
7930
|
+
var release = getFirst(path4.join(dir, "build/Release"), matchBuild);
|
|
7794
7931
|
if (release) return release;
|
|
7795
|
-
var debug = getFirst(
|
|
7932
|
+
var debug = getFirst(path4.join(dir, "build/Debug"), matchBuild);
|
|
7796
7933
|
if (debug) return debug;
|
|
7797
7934
|
}
|
|
7798
7935
|
var prebuild = resolve(dir);
|
|
7799
7936
|
if (prebuild) return prebuild;
|
|
7800
|
-
var nearby = resolve(
|
|
7937
|
+
var nearby = resolve(path4.dirname(process.execPath));
|
|
7801
7938
|
if (nearby) return nearby;
|
|
7802
7939
|
var target = [
|
|
7803
7940
|
"platform=" + platform,
|
|
@@ -7814,26 +7951,26 @@ var require_node_gyp_build = __commonJS({
|
|
|
7814
7951
|
].filter(Boolean).join(" ");
|
|
7815
7952
|
throw new Error("No native build was found for " + target + "\n loaded from: " + dir + "\n");
|
|
7816
7953
|
function resolve(dir2) {
|
|
7817
|
-
var tuples = readdirSync(
|
|
7954
|
+
var tuples = readdirSync(path4.join(dir2, "prebuilds")).map(parseTuple);
|
|
7818
7955
|
var tuple = tuples.filter(matchTuple(platform, arch)).sort(compareTuples)[0];
|
|
7819
7956
|
if (!tuple) return;
|
|
7820
|
-
var prebuilds =
|
|
7957
|
+
var prebuilds = path4.join(dir2, "prebuilds", tuple.name);
|
|
7821
7958
|
var parsed = readdirSync(prebuilds).map(parseTags);
|
|
7822
7959
|
var candidates = parsed.filter(matchTags(runtime, abi));
|
|
7823
7960
|
var winner = candidates.sort(compareTags(runtime))[0];
|
|
7824
|
-
if (winner) return
|
|
7961
|
+
if (winner) return path4.join(prebuilds, winner.file);
|
|
7825
7962
|
}
|
|
7826
7963
|
};
|
|
7827
7964
|
function readdirSync(dir) {
|
|
7828
7965
|
try {
|
|
7829
|
-
return
|
|
7966
|
+
return fs4.readdirSync(dir);
|
|
7830
7967
|
} catch (err) {
|
|
7831
7968
|
return [];
|
|
7832
7969
|
}
|
|
7833
7970
|
}
|
|
7834
7971
|
function getFirst(dir, filter) {
|
|
7835
7972
|
var files = readdirSync(dir).filter(filter);
|
|
7836
|
-
return files[0] &&
|
|
7973
|
+
return files[0] && path4.join(dir, files[0]);
|
|
7837
7974
|
}
|
|
7838
7975
|
function matchBuild(name) {
|
|
7839
7976
|
return /\.node$/.test(name);
|
|
@@ -7920,7 +8057,7 @@ var require_node_gyp_build = __commonJS({
|
|
|
7920
8057
|
return typeof window !== "undefined" && window.process && window.process.type === "renderer";
|
|
7921
8058
|
}
|
|
7922
8059
|
function isAlpine(platform2) {
|
|
7923
|
-
return platform2 === "linux" &&
|
|
8060
|
+
return platform2 === "linux" && fs4.existsSync("/etc/alpine-release");
|
|
7924
8061
|
}
|
|
7925
8062
|
load.parseTags = parseTags;
|
|
7926
8063
|
load.matchTags = matchTags;
|
|
@@ -11784,6 +11921,27 @@ async function executeCommand(cmd, gwClient, cfg, resolveAgentName, logger, cron
|
|
|
11784
11921
|
});
|
|
11785
11922
|
return;
|
|
11786
11923
|
}
|
|
11924
|
+
if (cmd.type === "skillWrite") {
|
|
11925
|
+
const skillsDir2 = injection?.skillsDir;
|
|
11926
|
+
const agentName = cmd.payload?.agentId;
|
|
11927
|
+
const location = cmd.payload?.location;
|
|
11928
|
+
const body = cmd.payload?.skillBody;
|
|
11929
|
+
if (!skillsDir2) {
|
|
11930
|
+
throw new Error("skillsDir is required for skillWrite");
|
|
11931
|
+
}
|
|
11932
|
+
if (!agentName) {
|
|
11933
|
+
throw new Error("agentId is required for skillWrite");
|
|
11934
|
+
}
|
|
11935
|
+
if (!location) {
|
|
11936
|
+
throw new Error("location is required for skillWrite");
|
|
11937
|
+
}
|
|
11938
|
+
if (typeof body !== "string") {
|
|
11939
|
+
throw new Error("skillBody is required for skillWrite");
|
|
11940
|
+
}
|
|
11941
|
+
writeSkillBody(skillsDir2, location, body);
|
|
11942
|
+
await syncSkills(agentName, enumerateSkills(skillsDir2, "openclaw"), cfg, logger);
|
|
11943
|
+
return;
|
|
11944
|
+
}
|
|
11787
11945
|
if (cmd.type.startsWith("cron")) {
|
|
11788
11946
|
if (!gwClient || !gwClient.isAlive()) {
|
|
11789
11947
|
logger.warn(`cohort-sync: no gateway client, cannot execute ${cmd.type}`);
|
|
@@ -11994,6 +12152,7 @@ var upsertTelemetryFromPlugin = makeFunctionReference("telemetryPlugin:upsertTel
|
|
|
11994
12152
|
var upsertSessionsFromPlugin = makeFunctionReference("telemetryPlugin:upsertSessionsFromPlugin");
|
|
11995
12153
|
var pushActivityFromPluginRef = makeFunctionReference("activityFeed:pushActivityFromPlugin");
|
|
11996
12154
|
var upsertCronSnapshotFromPluginRef = makeFunctionReference("telemetryPlugin:upsertCronSnapshotFromPlugin");
|
|
12155
|
+
var recordGatewayPresenceRef = makeFunctionReference("gatewayPresence:recordGatewayPresence");
|
|
11997
12156
|
var recordCronRunFromPluginRef = makeFunctionReference("cronRunHistory:recordFromPlugin");
|
|
11998
12157
|
var getUndeliveredForPlugin = makeFunctionReference("notifications:getUndeliveredForPlugin");
|
|
11999
12158
|
var markDeliveredByPlugin = makeFunctionReference("notifications:markDeliveredByPlugin");
|
|
@@ -12068,6 +12227,22 @@ async function pushCronSnapshot(apiKey2, jobs) {
|
|
|
12068
12227
|
return false;
|
|
12069
12228
|
}
|
|
12070
12229
|
}
|
|
12230
|
+
async function pushPresence(apiKey2, runtime) {
|
|
12231
|
+
if (authCircuitOpen) return false;
|
|
12232
|
+
const c = getClient();
|
|
12233
|
+
if (!c) return false;
|
|
12234
|
+
try {
|
|
12235
|
+
await c.mutation(recordGatewayPresenceRef, { apiKeyHash: hashApiKey(apiKey2), runtime });
|
|
12236
|
+
return true;
|
|
12237
|
+
} catch (err) {
|
|
12238
|
+
if (isUnauthorizedError(err)) {
|
|
12239
|
+
tripAuthCircuit();
|
|
12240
|
+
return false;
|
|
12241
|
+
}
|
|
12242
|
+
getLogger().error(`cohort-sync: pushPresence failed: ${err}`);
|
|
12243
|
+
return false;
|
|
12244
|
+
}
|
|
12245
|
+
}
|
|
12071
12246
|
async function recordCronRun(apiKey2, run) {
|
|
12072
12247
|
if (authCircuitOpen) return false;
|
|
12073
12248
|
const c = getClient();
|
|
@@ -12855,15 +13030,15 @@ import crypto2 from "node:crypto";
|
|
|
12855
13030
|
|
|
12856
13031
|
// src/device-identity-crypto.ts
|
|
12857
13032
|
import crypto from "node:crypto";
|
|
12858
|
-
import
|
|
12859
|
-
import
|
|
13033
|
+
import fs2 from "node:fs";
|
|
13034
|
+
import path2 from "node:path";
|
|
12860
13035
|
import os from "node:os";
|
|
12861
|
-
var DATA_DIR =
|
|
12862
|
-
var IDENTITY_PATH =
|
|
12863
|
-
var LEGACY_IDENTITY_PATH =
|
|
13036
|
+
var DATA_DIR = path2.join(os.homedir(), ".openclaw", "data", "cohort-sync");
|
|
13037
|
+
var IDENTITY_PATH = path2.join(DATA_DIR, ".device-identity.json");
|
|
13038
|
+
var LEGACY_IDENTITY_PATH = path2.join(os.homedir(), ".openclaw", "extensions", "cohort-sync", ".device-identity.json");
|
|
12864
13039
|
function tryLoadIdentity(filePath) {
|
|
12865
13040
|
try {
|
|
12866
|
-
const data = JSON.parse(
|
|
13041
|
+
const data = JSON.parse(fs2["read"+"FileSync"](filePath, "utf-8"));
|
|
12867
13042
|
if (data.deviceId && data.publicKeyPem && data.privateKeyPem) {
|
|
12868
13043
|
return data;
|
|
12869
13044
|
}
|
|
@@ -12896,8 +13071,8 @@ function loadOrCreateDeviceIdentity() {
|
|
|
12896
13071
|
}
|
|
12897
13072
|
function persistIdentity(identity) {
|
|
12898
13073
|
try {
|
|
12899
|
-
|
|
12900
|
-
|
|
13074
|
+
fs2.mkdirSync(DATA_DIR, { recursive: true, mode: 448 });
|
|
13075
|
+
fs2.writeFileSync(IDENTITY_PATH, JSON.stringify(identity, null, 2), { mode: 384 });
|
|
12901
13076
|
} catch (err) {
|
|
12902
13077
|
console.debug("cohort-sync: device identity write failed", { error: String(err) });
|
|
12903
13078
|
}
|
|
@@ -14009,6 +14184,28 @@ var AgentStateTracker = class {
|
|
|
14009
14184
|
|
|
14010
14185
|
// src/types.ts
|
|
14011
14186
|
var DEFAULT_API_URL = "https://api.cohort.bot";
|
|
14187
|
+
function normalizeTriageBrief(raw) {
|
|
14188
|
+
if (raw === null || typeof raw !== "object") {
|
|
14189
|
+
return { error: "brief must be an object with problem, solution (and optional risks)." };
|
|
14190
|
+
}
|
|
14191
|
+
const obj = raw;
|
|
14192
|
+
const requireField = (name) => {
|
|
14193
|
+
const value = obj[name];
|
|
14194
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
14195
|
+
return { error: `brief.${name} is required and must be a non-empty string.` };
|
|
14196
|
+
}
|
|
14197
|
+
return value.trim();
|
|
14198
|
+
};
|
|
14199
|
+
const problem = requireField("problem");
|
|
14200
|
+
if (typeof problem !== "string") return problem;
|
|
14201
|
+
const solution = requireField("solution");
|
|
14202
|
+
if (typeof solution !== "string") return solution;
|
|
14203
|
+
const brief = { problem, solution };
|
|
14204
|
+
if (typeof obj.risks === "string" && obj.risks.trim().length > 0) {
|
|
14205
|
+
brief.risks = obj.risks.trim();
|
|
14206
|
+
}
|
|
14207
|
+
return { brief };
|
|
14208
|
+
}
|
|
14012
14209
|
|
|
14013
14210
|
// src/tool-runtime.ts
|
|
14014
14211
|
var apiKey = null;
|
|
@@ -14119,7 +14316,8 @@ function dumpEvent(event) {
|
|
|
14119
14316
|
function positiveNumber(value) {
|
|
14120
14317
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
14121
14318
|
}
|
|
14122
|
-
var PLUGIN_VERSION = true ? "0.34.
|
|
14319
|
+
var PLUGIN_VERSION = true ? "0.34.5" : "unknown";
|
|
14320
|
+
var PRESENCE_PING_INTERVAL_MS = 12e4;
|
|
14123
14321
|
function resolveGatewayToken(api) {
|
|
14124
14322
|
const token2 = api.config?.gateway?.auth?.token;
|
|
14125
14323
|
return typeof token2 === "string" ? token2 : null;
|
|
@@ -14149,8 +14347,8 @@ function registerCronEventHandlers(client2, cfg, resolveAgentName, cronTimestamp
|
|
|
14149
14347
|
}
|
|
14150
14348
|
function parseIdentityFile(workspaceDir) {
|
|
14151
14349
|
try {
|
|
14152
|
-
const filePath =
|
|
14153
|
-
const content =
|
|
14350
|
+
const filePath = path3.join(workspaceDir, "IDENTITY.md");
|
|
14351
|
+
const content = fs3["read"+"FileSync"](filePath, "utf-8");
|
|
14154
14352
|
const identity = {};
|
|
14155
14353
|
for (const line of content.split(/\r?\n/)) {
|
|
14156
14354
|
const cleaned = line.trim().replace(/^\s*-\s*/, "");
|
|
@@ -14169,14 +14367,41 @@ function parseIdentityFile(workspaceDir) {
|
|
|
14169
14367
|
return null;
|
|
14170
14368
|
}
|
|
14171
14369
|
}
|
|
14370
|
+
function parseRoleFromSoul(content) {
|
|
14371
|
+
const MAX_LEN = 80;
|
|
14372
|
+
for (const line of content.split(/\r?\n/)) {
|
|
14373
|
+
const cleaned = line.trim().replace(/^\s*-\s*/, "");
|
|
14374
|
+
const colonIndex = cleaned.indexOf(":");
|
|
14375
|
+
if (colonIndex === -1) continue;
|
|
14376
|
+
const label = cleaned.slice(0, colonIndex).replace(/[*_]/g, "").trim().toLowerCase();
|
|
14377
|
+
if (label !== "role" && label !== "current role") continue;
|
|
14378
|
+
let value = cleaned.slice(colonIndex + 1).replace(/^[*_]+|[*_]+$/g, "").trim();
|
|
14379
|
+
if (!value) continue;
|
|
14380
|
+
const firstSentence = value.split(/\.\s/)[0].trim();
|
|
14381
|
+
if (firstSentence) value = firstSentence;
|
|
14382
|
+
if (value.length > MAX_LEN) value = value.slice(0, MAX_LEN).trim();
|
|
14383
|
+
return value;
|
|
14384
|
+
}
|
|
14385
|
+
return void 0;
|
|
14386
|
+
}
|
|
14387
|
+
function parseSoulRole(workspaceDir) {
|
|
14388
|
+
try {
|
|
14389
|
+
const content = fs3["read"+"FileSync"](path3.join(workspaceDir, "SOUL.md"), "utf-8");
|
|
14390
|
+
return parseRoleFromSoul(content);
|
|
14391
|
+
} catch {
|
|
14392
|
+
return void 0;
|
|
14393
|
+
}
|
|
14394
|
+
}
|
|
14172
14395
|
function resolveIdentity(configIdentity, workspaceDir) {
|
|
14173
14396
|
const fileIdentity = workspaceDir ? parseIdentityFile(workspaceDir) : null;
|
|
14174
|
-
|
|
14397
|
+
const soulRole = workspaceDir ? parseSoulRole(workspaceDir) : void 0;
|
|
14398
|
+
if (!configIdentity && !fileIdentity && !soulRole) return void 0;
|
|
14175
14399
|
return {
|
|
14176
14400
|
name: configIdentity?.name ?? fileIdentity?.name,
|
|
14177
14401
|
emoji: configIdentity?.emoji ?? fileIdentity?.emoji,
|
|
14178
14402
|
theme: configIdentity?.theme ?? fileIdentity?.theme,
|
|
14179
|
-
avatar: configIdentity?.avatar ?? fileIdentity?.avatar
|
|
14403
|
+
avatar: configIdentity?.avatar ?? fileIdentity?.avatar,
|
|
14404
|
+
title: configIdentity?.title ?? soulRole
|
|
14180
14405
|
};
|
|
14181
14406
|
}
|
|
14182
14407
|
function saveSessionsToDisk(tracker, stateFilePath) {
|
|
@@ -14193,14 +14418,14 @@ function saveSessionsToDisk(tracker, stateFilePath) {
|
|
|
14193
14418
|
data.sessions.push({ agentName: name, key });
|
|
14194
14419
|
}
|
|
14195
14420
|
}
|
|
14196
|
-
|
|
14421
|
+
fs3.writeFileSync(stateFilePath, JSON.stringify(data), { mode: 384 });
|
|
14197
14422
|
} catch {
|
|
14198
14423
|
}
|
|
14199
14424
|
}
|
|
14200
14425
|
function loadSessionsFromDisk(tracker, stateFilePath, logger) {
|
|
14201
14426
|
try {
|
|
14202
|
-
if (!
|
|
14203
|
-
const data = JSON.parse(
|
|
14427
|
+
if (!fs3.existsSync(stateFilePath)) return;
|
|
14428
|
+
const data = JSON.parse(fs3["read"+"FileSync"](stateFilePath, "utf8"));
|
|
14204
14429
|
if (Date.now() - new Date(data.savedAt).getTime() > 864e5) {
|
|
14205
14430
|
logger.info("cohort-sync: disk session state too old (>24h), skipping");
|
|
14206
14431
|
return;
|
|
@@ -14269,7 +14494,7 @@ async function handleGatewayStart(event, state) {
|
|
|
14269
14494
|
if (!state) {
|
|
14270
14495
|
return;
|
|
14271
14496
|
}
|
|
14272
|
-
const { cfg, tracker, logger, config, api } = state;
|
|
14497
|
+
const { cfg, tracker, logger, config, api, skillsDir: skillsDir2 } = state;
|
|
14273
14498
|
try {
|
|
14274
14499
|
const latestVersion = await checkForUpdate(PLUGIN_VERSION, logger);
|
|
14275
14500
|
if (latestVersion) {
|
|
@@ -14296,7 +14521,14 @@ async function handleGatewayStart(event, state) {
|
|
|
14296
14521
|
model: state.resolveModel(a.id),
|
|
14297
14522
|
identity: resolveIdentity(a.identity, a.workspace)
|
|
14298
14523
|
}));
|
|
14299
|
-
await fullSync(
|
|
14524
|
+
await fullSync(
|
|
14525
|
+
state.resolveAgentName("main"),
|
|
14526
|
+
state.resolveModel("main"),
|
|
14527
|
+
cfg,
|
|
14528
|
+
logger,
|
|
14529
|
+
agentList,
|
|
14530
|
+
skillsDir2
|
|
14531
|
+
);
|
|
14300
14532
|
} catch (err) {
|
|
14301
14533
|
logger.error(`cohort-sync: gateway_start sync failed: ${String(err)}`);
|
|
14302
14534
|
}
|
|
@@ -14329,7 +14561,7 @@ async function handleGatewayStart(event, state) {
|
|
|
14329
14561
|
state.resolveAgentName,
|
|
14330
14562
|
state.persistentGwClient,
|
|
14331
14563
|
state.cronTimestampTracker,
|
|
14332
|
-
{ port: state.gatewayPort, hooksToken }
|
|
14564
|
+
{ port: state.gatewayPort, hooksToken, skillsDir: skillsDir2 }
|
|
14333
14565
|
);
|
|
14334
14566
|
state.commandUnsubscriber = unsub;
|
|
14335
14567
|
} catch (err) {
|
|
@@ -14421,6 +14653,17 @@ async function handleGatewayStart(event, state) {
|
|
|
14421
14653
|
saveSessionsToDisk(tracker, state.stateFilePath);
|
|
14422
14654
|
}, 15e4);
|
|
14423
14655
|
logger.info("cohort-sync: keepalive interval started (150s)");
|
|
14656
|
+
void pushPresence(cfg.apiKey, "openclaw").catch(() => {
|
|
14657
|
+
});
|
|
14658
|
+
if (state.presenceInterval) clearInterval(state.presenceInterval);
|
|
14659
|
+
state.presenceInterval = setInterval(() => {
|
|
14660
|
+
void pushPresence(cfg.apiKey, "openclaw").catch(() => {
|
|
14661
|
+
});
|
|
14662
|
+
}, PRESENCE_PING_INTERVAL_MS);
|
|
14663
|
+
if (typeof state.presenceInterval.unref === "function") {
|
|
14664
|
+
state.presenceInterval.unref();
|
|
14665
|
+
}
|
|
14666
|
+
logger.info(`cohort-sync: liveness ping interval started (${PRESENCE_PING_INTERVAL_MS / 1e3}s)`);
|
|
14424
14667
|
}
|
|
14425
14668
|
function registerHookHandlers(api, logger, getState) {
|
|
14426
14669
|
function resolveAgentFromContext(state, ctx) {
|
|
@@ -14497,7 +14740,7 @@ function registerHookHandlers(api, logger, getState) {
|
|
|
14497
14740
|
const parsed = parseSessionKey(sessionKey);
|
|
14498
14741
|
const routineId = parsed.kind === "cron" ? parsed.identifier : void 0;
|
|
14499
14742
|
try {
|
|
14500
|
-
const raw =
|
|
14743
|
+
const raw = fs3["read"+"FileSync"](state.cronStorePath, "utf8");
|
|
14501
14744
|
const store = JSON.parse(raw);
|
|
14502
14745
|
const jobs = store.jobs ?? [];
|
|
14503
14746
|
const mapped = jobs.map((j) => mapCronJob(j, state.resolveAgentName, state.cronTimestampTracker));
|
|
@@ -14666,7 +14909,7 @@ function registerHookHandlers(api, logger, getState) {
|
|
|
14666
14909
|
state.resolveAgentName,
|
|
14667
14910
|
state.persistentGwClient,
|
|
14668
14911
|
state.cronTimestampTracker,
|
|
14669
|
-
{ port: state.gatewayPort, hooksToken: state.gatewayToken }
|
|
14912
|
+
{ port: state.gatewayPort, hooksToken: state.gatewayToken, skillsDir }
|
|
14670
14913
|
);
|
|
14671
14914
|
state.commandUnsubscriber = unsub;
|
|
14672
14915
|
} catch (err) {
|
|
@@ -14881,6 +15124,10 @@ function registerHookHandlers(api, logger, getState) {
|
|
|
14881
15124
|
clearInterval(state.keepaliveInterval);
|
|
14882
15125
|
state.keepaliveInterval = null;
|
|
14883
15126
|
}
|
|
15127
|
+
if (state.presenceInterval) {
|
|
15128
|
+
clearInterval(state.presenceInterval);
|
|
15129
|
+
state.presenceInterval = null;
|
|
15130
|
+
}
|
|
14884
15131
|
if (state.updateCheckInterval) {
|
|
14885
15132
|
clearInterval(state.updateCheckInterval);
|
|
14886
15133
|
state.updateCheckInterval = null;
|
|
@@ -14931,7 +15178,7 @@ function registerHookHandlers(api, logger, getState) {
|
|
|
14931
15178
|
}
|
|
14932
15179
|
function initializeHookState(api, cfg) {
|
|
14933
15180
|
const { logger, config } = api;
|
|
14934
|
-
const stateFilePath =
|
|
15181
|
+
const stateFilePath = path3.join(cfg.stateDir, "session-state.json");
|
|
14935
15182
|
const nameMap = cfg.agentNameMap;
|
|
14936
15183
|
const tracker = new AgentStateTracker();
|
|
14937
15184
|
const convexUrl = cfg.convexUrl ?? deriveConvexUrl(cfg.apiUrl);
|
|
@@ -14965,7 +15212,7 @@ function initializeHookState(api, cfg) {
|
|
|
14965
15212
|
function getModelContextLimit2(model) {
|
|
14966
15213
|
return getModelContextLimit(model);
|
|
14967
15214
|
}
|
|
14968
|
-
const cronStorePath = api.config?.cron?.store ??
|
|
15215
|
+
const cronStorePath = api.config?.cron?.store ?? path3.join(os2.homedir(), ".openclaw", "cron", "jobs.json");
|
|
14969
15216
|
const activityBatch = new MicroBatch({
|
|
14970
15217
|
maxSize: 10,
|
|
14971
15218
|
maxDelayMs: 1e3,
|
|
@@ -14975,6 +15222,7 @@ function initializeHookState(api, cfg) {
|
|
|
14975
15222
|
const gatewayPort = api.config?.gateway?.port ?? null;
|
|
14976
15223
|
const gatewayToken = resolveGatewayToken(api);
|
|
14977
15224
|
const hooksToken = resolveHooksToken(api);
|
|
15225
|
+
const skillsDir2 = api.config?.skills?.dir ?? path3.join(os2.homedir(), ".openclaw", "skills");
|
|
14978
15226
|
const cronTimestampTracker = new CronTimestampTracker();
|
|
14979
15227
|
const cronRunStarts = /* @__PURE__ */ new Map();
|
|
14980
15228
|
let persistentGwClient = null;
|
|
@@ -14990,7 +15238,7 @@ function initializeHookState(api, cfg) {
|
|
|
14990
15238
|
resolveAgentName,
|
|
14991
15239
|
persistentGwClient,
|
|
14992
15240
|
cronTimestampTracker,
|
|
14993
|
-
{ port: gatewayPort, hooksToken }
|
|
15241
|
+
{ port: gatewayPort, hooksToken, skillsDir: skillsDir2 }
|
|
14994
15242
|
);
|
|
14995
15243
|
setToolRuntime({
|
|
14996
15244
|
apiKey: cfg.apiKey,
|
|
@@ -15035,12 +15283,14 @@ function initializeHookState(api, cfg) {
|
|
|
15035
15283
|
getModelContextLimit: getModelContextLimit2,
|
|
15036
15284
|
activityBatch,
|
|
15037
15285
|
cronStorePath,
|
|
15286
|
+
skillsDir: skillsDir2,
|
|
15038
15287
|
stateFilePath,
|
|
15039
15288
|
gatewayPort,
|
|
15040
15289
|
gatewayToken,
|
|
15041
15290
|
persistentGwClient,
|
|
15042
15291
|
gwClientInitialized,
|
|
15043
15292
|
keepaliveInterval: null,
|
|
15293
|
+
presenceInterval: null,
|
|
15044
15294
|
commandUnsubscriber: commandUnsub,
|
|
15045
15295
|
channelsUnsubscriber: null,
|
|
15046
15296
|
api,
|
|
@@ -15084,6 +15334,65 @@ function registerGatewayMethods(api, getGatewayClient) {
|
|
|
15084
15334
|
);
|
|
15085
15335
|
}
|
|
15086
15336
|
|
|
15337
|
+
// src/triage-pr-files.ts
|
|
15338
|
+
import { execFile } from "node:child_process";
|
|
15339
|
+
import { readFile } from "node:fs/promises";
|
|
15340
|
+
import { join } from "node:path";
|
|
15341
|
+
import { promisify } from "node:util";
|
|
15342
|
+
var execFileAsync = promisify(execFile);
|
|
15343
|
+
async function collectChangedFiles(args) {
|
|
15344
|
+
const cwd = args.repoDir || process.cwd();
|
|
15345
|
+
const git = async (gitArgs) => {
|
|
15346
|
+
const { stdout } = await execFileAsync("git", gitArgs, {
|
|
15347
|
+
cwd,
|
|
15348
|
+
maxBuffer: 64 * 1024 * 1024
|
|
15349
|
+
});
|
|
15350
|
+
return stdout;
|
|
15351
|
+
};
|
|
15352
|
+
const files = [];
|
|
15353
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15354
|
+
const nameStatus = await git(["diff", "--name-status", "-z", args.baseSha]);
|
|
15355
|
+
const tokens = nameStatus.split("\0");
|
|
15356
|
+
let i2 = 0;
|
|
15357
|
+
while (i2 < tokens.length) {
|
|
15358
|
+
const status = tokens[i2];
|
|
15359
|
+
if (!status) {
|
|
15360
|
+
i2 += 1;
|
|
15361
|
+
continue;
|
|
15362
|
+
}
|
|
15363
|
+
let path4;
|
|
15364
|
+
if (status[0] === "R" || status[0] === "C") {
|
|
15365
|
+
const oldPath = tokens[i2 + 1] ?? "";
|
|
15366
|
+
path4 = tokens[i2 + 2] ?? "";
|
|
15367
|
+
i2 += 3;
|
|
15368
|
+
if (oldPath && !seen.has(oldPath)) {
|
|
15369
|
+
seen.add(oldPath);
|
|
15370
|
+
files.push({ path: oldPath, deleted: true });
|
|
15371
|
+
}
|
|
15372
|
+
} else {
|
|
15373
|
+
path4 = tokens[i2 + 1] ?? "";
|
|
15374
|
+
i2 += 2;
|
|
15375
|
+
}
|
|
15376
|
+
if (!path4 || seen.has(path4)) continue;
|
|
15377
|
+
seen.add(path4);
|
|
15378
|
+
if (status[0] === "D") {
|
|
15379
|
+
files.push({ path: path4, deleted: true });
|
|
15380
|
+
} else {
|
|
15381
|
+
files.push({ path: path4, content: await readRepoFile(cwd, path4) });
|
|
15382
|
+
}
|
|
15383
|
+
}
|
|
15384
|
+
const others = await git(["ls-files", "--others", "--exclude-standard", "-z"]);
|
|
15385
|
+
for (const path4 of others.split("\0")) {
|
|
15386
|
+
if (!path4 || seen.has(path4)) continue;
|
|
15387
|
+
seen.add(path4);
|
|
15388
|
+
files.push({ path: path4, content: await readRepoFile(cwd, path4) });
|
|
15389
|
+
}
|
|
15390
|
+
return files;
|
|
15391
|
+
}
|
|
15392
|
+
async function readRepoFile(cwd, path4) {
|
|
15393
|
+
return readFile(join(cwd, path4), "utf8");
|
|
15394
|
+
}
|
|
15395
|
+
|
|
15087
15396
|
// src/pocket-guide.ts
|
|
15088
15397
|
var POCKET_GUIDE = `# Cohort Agent Guide (Pocket Version)
|
|
15089
15398
|
|
|
@@ -15313,6 +15622,31 @@ async function safeHttpError(response) {
|
|
|
15313
15622
|
}
|
|
15314
15623
|
return "";
|
|
15315
15624
|
}
|
|
15625
|
+
var PR_MAX_FILES = 50;
|
|
15626
|
+
var PR_MAX_FILE_BYTES = 1e6;
|
|
15627
|
+
var PR_MAX_TOTAL_BYTES = 5e6;
|
|
15628
|
+
var PR_BASE_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
|
|
15629
|
+
function checkPrEnvelope(files) {
|
|
15630
|
+
if (files.length === 0) {
|
|
15631
|
+
return "No changed files to report \u2014 make a change first, then call cohort_triage_pr.";
|
|
15632
|
+
}
|
|
15633
|
+
if (files.length > PR_MAX_FILES) {
|
|
15634
|
+
return `Change too large: ${files.length} files (max ${PR_MAX_FILES}).`;
|
|
15635
|
+
}
|
|
15636
|
+
let total = 0;
|
|
15637
|
+
for (const entry of files) {
|
|
15638
|
+
if ("deleted" in entry) continue;
|
|
15639
|
+
const size = Buffer2.byteLength(entry.content, "utf8");
|
|
15640
|
+
if (size > PR_MAX_FILE_BYTES) {
|
|
15641
|
+
return `File too large: ${entry.path} (max ${PR_MAX_FILE_BYTES} bytes).`;
|
|
15642
|
+
}
|
|
15643
|
+
total += size;
|
|
15644
|
+
}
|
|
15645
|
+
if (total > PR_MAX_TOTAL_BYTES) {
|
|
15646
|
+
return "Change too large (total content size exceeds the limit).";
|
|
15647
|
+
}
|
|
15648
|
+
return null;
|
|
15649
|
+
}
|
|
15316
15650
|
function renderTaskContext(context) {
|
|
15317
15651
|
if (!context) return "";
|
|
15318
15652
|
const lines = [];
|
|
@@ -16067,7 +16401,7 @@ ${renderGoal(goal)}`, goal);
|
|
|
16067
16401
|
return {
|
|
16068
16402
|
name: "cohort_triage",
|
|
16069
16403
|
label: "cohort_triage",
|
|
16070
|
-
description: "Report your triage decision on a Cohort triage item back to Cohort. Pass the Triage item ID from your wake task as triage_item_id, the tier (trivial | judgment | feature | not_actionable), whether it is actionable, and your reasoning. SHADOW MODE: do NOT write code or open PRs.",
|
|
16404
|
+
description: "Report your triage decision on a Cohort triage item back to Cohort. Pass the Triage item ID from your wake task as triage_item_id, the tier (trivial | judgment | feature | not_actionable), whether it is actionable, and your reasoning. Also pass a short `title` \u2014 a brief (\u226480 char) issue summary in your own words (a task-name headline of what you read), NOT a paste of the feedback; Cohort uses it as the Build-it task title. SHADOW MODE: do NOT write code or open PRs.",
|
|
16071
16405
|
parameters: Type.Object({
|
|
16072
16406
|
triage_item_id: Type.String({ description: "Triage item ID from your wake task (e.g. triageItems:abc123)." }),
|
|
16073
16407
|
tier: Type.Union([
|
|
@@ -16078,7 +16412,8 @@ ${renderGoal(goal)}`, goal);
|
|
|
16078
16412
|
], { description: "Triage tier for this feedback." }),
|
|
16079
16413
|
actionable: Type.Boolean({ description: "Whether this feedback is actionable." }),
|
|
16080
16414
|
reasoning: Type.String({ description: "Why you reached this decision." }),
|
|
16081
|
-
confidence: Type.Optional(Type.Number({ description: "Optional confidence in the decision, 0 to 1." }))
|
|
16415
|
+
confidence: Type.Optional(Type.Number({ description: "Optional confidence in the decision, 0 to 1." })),
|
|
16416
|
+
title: Type.Optional(Type.String({ description: "Optional short (\u226480 char) issue summary in your own words \u2014 a task-name headline of the problem, NOT a paste of the feedback. Used as the Build-it task title." }))
|
|
16082
16417
|
}),
|
|
16083
16418
|
async execute(_toolCallId, params) {
|
|
16084
16419
|
const rt = getToolRuntime();
|
|
@@ -16098,7 +16433,8 @@ ${renderGoal(goal)}`, goal);
|
|
|
16098
16433
|
tier: params.tier,
|
|
16099
16434
|
actionable: params.actionable,
|
|
16100
16435
|
reasoning: params.reasoning,
|
|
16101
|
-
...params.confidence !== void 0 ? { confidence: params.confidence } : {}
|
|
16436
|
+
...params.confidence !== void 0 ? { confidence: params.confidence } : {},
|
|
16437
|
+
...params.title !== void 0 ? { title: params.title } : {}
|
|
16102
16438
|
}),
|
|
16103
16439
|
signal: AbortSignal.timeout(1e4)
|
|
16104
16440
|
}
|
|
@@ -16116,6 +16452,72 @@ ${renderGoal(goal)}`, goal);
|
|
|
16116
16452
|
}
|
|
16117
16453
|
};
|
|
16118
16454
|
});
|
|
16455
|
+
api.registerTool(() => {
|
|
16456
|
+
return {
|
|
16457
|
+
name: "cohort_triage_pr",
|
|
16458
|
+
label: "cohort_triage_pr",
|
|
16459
|
+
description: "Report a Build-it fix you produced for a Cohort triage item. Pass the Triage item ID, the base commit SHA you branched from (base_sha), and a structured brief (problem, solution, optional risks). The tool collects your changed files from the current worktree and sends them to Cohort; the Cohort spine opens the PR and it auto-merges once CI is green. Do NOT open a PR or merge yourself \u2014 you hold no merge credential.",
|
|
16460
|
+
parameters: Type.Object({
|
|
16461
|
+
triage_item_id: Type.String({ description: "Triage item ID from your wake task (e.g. triageItems:abc123)." }),
|
|
16462
|
+
base_sha: Type.String({ description: "The 40- or 64-char hex commit SHA you branched from." }),
|
|
16463
|
+
brief: Type.Object({
|
|
16464
|
+
problem: Type.String({ description: "What's wrong (the issue being fixed)." }),
|
|
16465
|
+
solution: Type.String({ description: "How the PR fixes it (rendered under the 'Fix' label)." }),
|
|
16466
|
+
risks: Type.Optional(Type.String({ description: "Optional risks/blast radius of the change." }))
|
|
16467
|
+
}),
|
|
16468
|
+
repo_dir: Type.Optional(Type.String({ description: "Optional worktree path to diff (defaults to the current directory)." }))
|
|
16469
|
+
}),
|
|
16470
|
+
async execute(_toolCallId, params) {
|
|
16471
|
+
const rt = getToolRuntime();
|
|
16472
|
+
if (!rt.isReady) {
|
|
16473
|
+
return textResult("cohort_triage_pr is not ready yet \u2014 the plugin is still starting up.");
|
|
16474
|
+
}
|
|
16475
|
+
if (!PR_BASE_SHA_RE.test(params.base_sha)) {
|
|
16476
|
+
return textResult("Invalid base_sha: must be a 40- or 64-char hex commit SHA.");
|
|
16477
|
+
}
|
|
16478
|
+
const normalized = normalizeTriageBrief(params.brief);
|
|
16479
|
+
if ("error" in normalized) {
|
|
16480
|
+
return textResult(`Invalid brief: ${normalized.error}`);
|
|
16481
|
+
}
|
|
16482
|
+
let files;
|
|
16483
|
+
try {
|
|
16484
|
+
files = await collectChangedFiles({ baseSha: params.base_sha, repoDir: params.repo_dir });
|
|
16485
|
+
} catch (err) {
|
|
16486
|
+
return textResult(`Failed to collect changed files for ${params.triage_item_id}: ${err instanceof Error ? redactSecrets(err.message) : "Unknown error"}`);
|
|
16487
|
+
}
|
|
16488
|
+
const envelopeError = checkPrEnvelope(files);
|
|
16489
|
+
if (envelopeError) {
|
|
16490
|
+
return textResult(envelopeError);
|
|
16491
|
+
}
|
|
16492
|
+
try {
|
|
16493
|
+
const response = await fetch(
|
|
16494
|
+
`${rt.apiUrl}/api/v1/plugin/triage/${encodeURIComponent(params.triage_item_id)}/pr`,
|
|
16495
|
+
{
|
|
16496
|
+
method: "POST",
|
|
16497
|
+
headers: {
|
|
16498
|
+
"Authorization": `Bearer ${rt.apiKey}`,
|
|
16499
|
+
"Content-Type": "application/json"
|
|
16500
|
+
},
|
|
16501
|
+
body: JSON.stringify({
|
|
16502
|
+
brief: normalized.brief,
|
|
16503
|
+
baseSha: params.base_sha,
|
|
16504
|
+
files
|
|
16505
|
+
}),
|
|
16506
|
+
signal: AbortSignal.timeout(3e4)
|
|
16507
|
+
}
|
|
16508
|
+
);
|
|
16509
|
+
if (!response.ok) {
|
|
16510
|
+
const message = await safeHttpError(response);
|
|
16511
|
+
return textResult(`Failed to report PR for ${params.triage_item_id}: ${response.status}${message}`);
|
|
16512
|
+
}
|
|
16513
|
+
const result = await response.json();
|
|
16514
|
+
return textResult(`Reported ${files.length} changed file(s) for ${params.triage_item_id}; the spine will open the PR.`, result);
|
|
16515
|
+
} catch (err) {
|
|
16516
|
+
return textResult(`Failed to report PR for ${params.triage_item_id}: ${err instanceof Error ? redactSecrets(err.message) : "Unknown error"}`);
|
|
16517
|
+
}
|
|
16518
|
+
}
|
|
16519
|
+
};
|
|
16520
|
+
});
|
|
16119
16521
|
api.registerTool(() => {
|
|
16120
16522
|
return {
|
|
16121
16523
|
name: "cohort_relate",
|
package/dist/package.json
CHANGED
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED