Skip to content

Commit f9193be

Browse files
fix(cli): respect active harnesses during sync (#630)
1 parent 123e568 commit f9193be

7 files changed

Lines changed: 182 additions & 21 deletions

File tree

docs/CLI.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -701,10 +701,12 @@ Options:
701701
---
702702

703703
Sync hooks, extensions, built-in template files, and skills to your `$SIGNET_WORKSPACE/` directory,
704-
and re-register hooks for any detected harnesses. Run this after an
704+
and re-register hooks for the active harnesses listed in `agent.yaml`. Run this after an
705705
upgrade if built-in skills appear stale or hooks need updating. If OpenClaw is still configured on
706706
the legacy Signet hook path, `signet sync` now migrates it to the plugin
707707
runtime path automatically so full lifecycle capture resumes.
708+
The command may report installed harnesses it detects on disk, but inactive
709+
harnesses are not modified.
708710

709711
```bash
710712
signet sync
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import { loadConfiguredHarnesses, parseHarnessList } from "../harness-config";
6+
7+
describe("parseHarnessList", () => {
8+
test("normalizes array and comma-separated harness config", () => {
9+
expect(parseHarnessList(["pi", " codex ", "", 42])).toEqual(["pi", "codex"]);
10+
expect(parseHarnessList("pi, codex,,opencode")).toEqual(["pi", "codex", "opencode"]);
11+
});
12+
});
13+
14+
describe("loadConfiguredHarnesses", () => {
15+
test("loads active harnesses from agent.yaml", () => {
16+
const root = mkdtempSync(join(tmpdir(), "signet-harness-config-"));
17+
18+
try {
19+
mkdirSync(root, { recursive: true });
20+
writeFileSync(join(root, "agent.yaml"), "harnesses:\n - pi\n - opencode\n");
21+
22+
expect(loadConfiguredHarnesses(root)).toEqual(["pi", "opencode"]);
23+
} finally {
24+
rmSync(root, { recursive: true, force: true });
25+
}
26+
});
27+
28+
test("returns empty list when no config declares harnesses", () => {
29+
const root = mkdtempSync(join(tmpdir(), "signet-harness-config-empty-"));
30+
31+
try {
32+
writeFileSync(join(root, "agent.yaml"), "agent:\n name: test\n");
33+
34+
expect(loadConfiguredHarnesses(root)).toEqual([]);
35+
} finally {
36+
rmSync(root, { recursive: true, force: true });
37+
}
38+
});
39+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { existsSync, readFileSync } from "node:fs";
2+
import { join } from "node:path";
3+
import { parseSimpleYaml } from "./yaml";
4+
5+
const CONFIG_FILES = ["agent.yaml", "AGENT.yaml", "config.yaml"] as const;
6+
7+
export function loadConfiguredHarnesses(agentsDir: string): readonly string[] {
8+
for (const name of CONFIG_FILES) {
9+
const path = join(agentsDir, name);
10+
if (!existsSync(path)) continue;
11+
12+
try {
13+
const parsed = parseSimpleYaml(readFileSync(path, "utf-8"));
14+
if (!isRecord(parsed)) return [];
15+
return parseHarnessList(parsed.harnesses);
16+
} catch {
17+
return [];
18+
}
19+
}
20+
21+
return [];
22+
}
23+
24+
export function parseHarnessList(value: unknown): readonly string[] {
25+
if (Array.isArray(value)) {
26+
return value.flatMap((entry) => (typeof entry === "string" && entry.trim().length > 0 ? [entry.trim()] : []));
27+
}
28+
if (typeof value === "string") {
29+
return value
30+
.split(",")
31+
.map((entry) => entry.trim())
32+
.filter((entry) => entry.length > 0);
33+
}
34+
return [];
35+
}
36+
37+
function isRecord(value: unknown): value is Record<string, unknown> {
38+
return typeof value === "object" && value !== null;
39+
}

platform/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export {
105105
resolveNetworkBinding,
106106
} from "./network";
107107
export type { NetworkMode } from "./network";
108+
export { loadConfiguredHarnesses, parseHarnessList } from "./harness-config";
108109
export { resolveSignetDaemonUrl } from "./daemon-url";
109110
export type { SignetDaemonUrlOptions } from "./daemon-url";
110111
export {

platform/daemon/src/daemon.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
type AgentDefinition,
1818
type PipelineSynthesisConfig,
1919
buildArchitectureDoc,
20+
loadConfiguredHarnesses,
2021
normalizeAgentRosterEntry,
2122
parseSimpleYaml,
2223
stripSignetBlock,
@@ -248,6 +249,7 @@ const SYNC_DEBOUNCE_MS = 2000;
248249
async function syncHarnessConfigs() {
249250
const agentsMdPath = join(AGENTS_DIR, "AGENTS.md");
250251
if (!existsSync(agentsMdPath)) return;
252+
const activeHarnesses = new Set(loadConfiguredHarnesses(AGENTS_DIR));
251253

252254
const rawContent = await Bun.file(agentsMdPath).text();
253255
const content = stripSignetBlock(rawContent);
@@ -309,7 +311,7 @@ ${fileList}
309311
const composed = content + identityExtras;
310312

311313
const opencodeDir = join(homedir(), ".config", "opencode");
312-
if (existsSync(opencodeDir)) {
314+
if (activeHarnesses.has("opencode") && existsSync(opencodeDir)) {
313315
try {
314316
const opencodeAgentsPath = join(opencodeDir, "AGENTS.md");
315317
if (await writeFileIfChangedAsync(opencodeAgentsPath, buildHeader("AGENTS.md") + composed)) {
@@ -673,6 +675,8 @@ function startFileWatcher() {
673675
watcher = watch(
674676
[
675677
join(AGENTS_DIR, "agent.yaml"),
678+
join(AGENTS_DIR, "AGENT.yaml"),
679+
join(AGENTS_DIR, "config.yaml"),
676680
join(AGENTS_DIR, "AGENTS.md"),
677681
join(AGENTS_DIR, "SOUL.md"),
678682
join(AGENTS_DIR, "MEMORY.md"),
@@ -693,7 +697,7 @@ function startFileWatcher() {
693697
scheduleAutoCommit(path);
694698

695699
const base = basename(path);
696-
if (base === "agent.yaml" || base === "AGENT.yaml") {
700+
if (base === "agent.yaml" || base === "AGENT.yaml" || base === "config.yaml") {
697701
try {
698702
reloadAuthState(AGENTS_DIR);
699703
logger.info("config", "Auth config reloaded from disk");
@@ -702,7 +706,16 @@ function startFileWatcher() {
702706
}
703707
}
704708

705-
const SYNC_TRIGGER_FILES = ["AGENTS.md", "SOUL.md", "IDENTITY.md", "USER.md", "MEMORY.md"];
709+
const SYNC_TRIGGER_FILES = [
710+
"agent.yaml",
711+
"AGENT.yaml",
712+
"config.yaml",
713+
"AGENTS.md",
714+
"SOUL.md",
715+
"IDENTITY.md",
716+
"USER.md",
717+
"MEMORY.md",
718+
];
706719
const normalizedForSync = path.replace(/\\/g, "/");
707720
const isAgentSubdir = normalizedForSync.includes(`${AGENTS_DIR.replace(/\\/g, "/")}/agents/`);
708721
if (SYNC_TRIGGER_FILES.some((f) => path.endsWith(f)) || isAgentSubdir) {

surfaces/cli/src/features/sync.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,55 @@ describe("syncTemplates workspace detection", () => {
101101
console.log = origLog;
102102
}
103103
});
104+
105+
it("only re-registers hooks for harnesses configured in agent.yaml", async () => {
106+
const root = mkdtempSync(join(tmpdir(), "sync-active-harnesses-"));
107+
const basePath = join(root, "agents");
108+
const origLog = console.log;
109+
110+
try {
111+
process.env.HOME = root;
112+
mkdirSync(basePath, { recursive: true });
113+
mkdirSync(join(root, ".codex"), { recursive: true });
114+
mkdirSync(join(root, ".config", "opencode"), { recursive: true });
115+
writeFileSync(join(root, ".codex", "config.toml"), "[model]\n");
116+
writeFileSync(join(basePath, "agent.yaml"), "harnesses:\n - pi\n");
117+
118+
const logs: string[] = [];
119+
console.log = (...args: unknown[]) => logs.push(args.join(" "));
120+
const configureHarnessHooks = mock(async () => {});
121+
122+
await syncTemplates({
123+
agentsDir: basePath,
124+
configureHarnessHooks,
125+
getSkillsSourceDir: () => join(root, "skills-src"),
126+
getTemplatesDir: () => join(root, "templates"),
127+
signetLogo: () => "signet",
128+
syncBuiltinSkills: () => ({ installed: [], updated: [], skipped: [] }),
129+
syncNativeEmbeddingModel: async () => ({ status: "current", message: "ready" }),
130+
syncPredictorBinary: async () => ({ status: "current", message: "ready" }),
131+
syncWorkspaceSourceRepo: async () => ({
132+
status: "current",
133+
path: join(basePath, "signetai"),
134+
message: "current",
135+
branch: "main",
136+
defaultBranch: "main",
137+
}),
138+
});
139+
140+
expect(configureHarnessHooks.mock.calls.map((call) => call[0])).toEqual(["pi"]);
141+
const output = logs.join("\n");
142+
expect(output).toContain("Installed harnesses detected:");
143+
expect(output).toContain("codex");
144+
expect(output).toContain("opencode");
145+
expect(output).toContain("Installed but inactive:");
146+
expect(output).not.toContain("hooks re-registered for codex");
147+
expect(output).not.toContain("hooks re-registered for opencode");
148+
} finally {
149+
console.log = origLog;
150+
rmSync(root, { recursive: true, force: true });
151+
}
152+
});
104153
});
105154

106155
describe("syncTemplates openclaw migration", () => {
@@ -113,6 +162,7 @@ describe("syncTemplates openclaw migration", () => {
113162
process.env.HOME = root;
114163
process.env.OPENCLAW_CONFIG_PATH = configPath;
115164
mkdirSync(basePath, { recursive: true });
165+
writeFileSync(join(basePath, "agent.yaml"), "harnesses:\n - openclaw\n");
116166

117167
writeFileSync(
118168
configPath,
@@ -194,6 +244,7 @@ describe("syncTemplates openclaw migration", () => {
194244
process.env.OPENCLAW_CONFIG_PATH = openClawConfigPath;
195245
process.env.CLAWDBOT_CONFIG_PATH = clawdbotConfigPath;
196246
mkdirSync(basePath, { recursive: true });
247+
writeFileSync(join(basePath, "agent.yaml"), "harnesses:\n - openclaw\n");
197248

198249
writeFileSync(
199250
openClawConfigPath,

surfaces/cli/src/features/sync.ts

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { copyFileSync, existsSync } from "node:fs";
22
import { homedir } from "node:os";
33
import { join } from "node:path";
4-
import { GeminiConnector } from "@signet/connector-gemini";
5-
import { HermesAgentConnector } from "@signet/connector-hermes-agent";
6-
import { OhMyPiConnector } from "@signet/connector-oh-my-pi";
74
import { OpenClawConnector } from "@signet/connector-openclaw";
8-
import { PiConnector } from "@signet/connector-pi";
9-
import type { WorkspaceSourceRepoSyncResult } from "@signet/core";
5+
import {
6+
type WorkspaceSourceRepoSyncResult,
7+
getOhMyPiConfigPath,
8+
getPiConfigPath,
9+
loadConfiguredHarnesses,
10+
resolveHermesRepoPath,
11+
} from "@signet/core";
1012
import chalk from "chalk";
1113

1214
interface SkillSync {
@@ -149,7 +151,23 @@ async function syncNative(basePath: string, deps: Deps): Promise<number> {
149151

150152
async function syncHarnessHooks(basePath: string, deps: Deps): Promise<number> {
151153
let synced = 0;
152-
for (const harness of detectHarnesses()) {
154+
const harnesses = [...loadConfiguredHarnesses(basePath)];
155+
const detected = detectInstalledHarnesses();
156+
if (detected.length > 0) {
157+
console.log(chalk.dim(` Installed harnesses detected: ${detected.join(", ")}`));
158+
}
159+
160+
if (harnesses.length === 0) {
161+
console.log(chalk.dim(" No active harnesses configured; skipping hook re-registration"));
162+
return 0;
163+
}
164+
165+
const inactive = detected.filter((harness) => !harnesses.includes(harness));
166+
if (inactive.length > 0) {
167+
console.log(chalk.dim(` Installed but inactive: ${inactive.join(", ")}`));
168+
}
169+
170+
for (const harness of harnesses) {
153171
try {
154172
let runtimePath: "plugin" | "legacy" | undefined;
155173
if (harness === "openclaw") {
@@ -180,34 +198,32 @@ async function syncHarnessHooks(basePath: string, deps: Deps): Promise<number> {
180198
return synced;
181199
}
182200

183-
function detectHarnesses(): string[] {
201+
function detectInstalledHarnesses(): string[] {
184202
const found: string[] = [];
203+
const home = process.env.HOME ?? homedir();
185204

186-
if (existsSync(join(homedir(), ".claude", "settings.json"))) {
205+
if (existsSync(join(home, ".claude", "settings.json"))) {
187206
found.push("claude-code");
188207
}
189-
if (
190-
existsSync(join(homedir(), ".config", "signet", "bin", "codex")) ||
191-
existsSync(join(homedir(), ".codex", "config.toml"))
192-
) {
208+
if (existsSync(join(home, ".config", "signet", "bin", "codex")) || existsSync(join(home, ".codex", "config.toml"))) {
193209
found.push("codex");
194210
}
195-
if (existsSync(join(homedir(), ".config", "opencode"))) {
211+
if (existsSync(join(home, ".config", "opencode"))) {
196212
found.push("opencode");
197213
}
198214
if (new OpenClawConnector().isInstalled()) {
199215
found.push("openclaw");
200216
}
201-
if (new OhMyPiConnector().isInstalled()) {
217+
if (existsSync(getOhMyPiConfigPath())) {
202218
found.push("oh-my-pi");
203219
}
204-
if (new HermesAgentConnector().isInstalled()) {
220+
if (resolveHermesRepoPath() !== null || existsSync(join(home, ".hermes"))) {
205221
found.push("hermes-agent");
206222
}
207-
if (new GeminiConnector().isInstalled()) {
223+
if (existsSync(join(home, ".gemini", "settings.json"))) {
208224
found.push("gemini");
209225
}
210-
if (new PiConnector().isInstalled()) {
226+
if (existsSync(getPiConfigPath())) {
211227
found.push("pi");
212228
}
213229

0 commit comments

Comments
 (0)