Skip to content

Commit cbbc864

Browse files
authored
fix(coding-agents): seed the actual harness, not the "opencode" default (#3247) (#3266)
The background seed engine (deepen.js) resolves {harness} from cfg.harness, which falls back to a hardcoded "opencode". buildSessionStartContext fired the seed via startSeed(cwd, { limit }) without the harness — so every non-opencode session's codebase survey and git history were misfiled into an `opencode::<project>` bank that no session ever reads, while the session hooks correctly wrote to `<harness>::<project>`. - session-start.ts: forward the asking harness to startSeed, mirroring the survey spawn right beside it. - config.ts: resolve cfg.harness to the harness that called loadConfig when the config file sets none, instead of the silent "opencode" default — this also fixes the same latent misfiling for kilo and opencode-fork harnesses. Also (#3248): add a supersession clause to OBSERVATIONS_MISSION so a revised convention updates its existing observation instead of accumulating a contradictory sibling, matching the language already used in the conversation and reflect missions. Verified end-to-end with the built artifacts: the real claude-sessionstart-hook now spawns `deepen.js ... --harness claude-code`, which resolves `bank=claude-code::<project>`.
1 parent 5b2f5d8 commit cbbc864

5 files changed

Lines changed: 50 additions & 9 deletions

File tree

hindsight-integrations/coding-agents/src/core/config.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ describe("loadConfig layering", () => {
5454
expect(loadConfig({ path: globalCfg }).bankId).toBe("shared"); // no harness: base only
5555
});
5656

57+
it("resolves harness to the ASKING harness when the file sets none — not the opencode default (#3247)", () => {
58+
writeJson(globalCfg, { bankId: "shared" }); // no explicit `harness:` field
59+
expect(loadConfig({ path: globalCfg, harness: "claude-code" }).harness).toBe("claude-code");
60+
expect(loadConfig({ path: globalCfg, harness: "kilo" }).harness).toBe("kilo");
61+
});
62+
63+
it("an explicit harness field in the config file still wins over the asking harness", () => {
64+
writeJson(globalCfg, { harness: "opencode" });
65+
expect(loadConfig({ path: globalCfg, harness: "claude-code" }).harness).toBe("opencode");
66+
});
67+
5768
it("legacy string signature still works as the global path", () => {
5869
writeJson(globalCfg, { bankId: "legacy" });
5970
expect(loadConfig(globalCfg).bankId).toBe("legacy");

hindsight-integrations/coding-agents/src/core/config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,12 @@ export function loadConfig(opts: LoadOptions | string = {}): Config {
314314
const o: LoadOptions = typeof opts === "string" ? { path: opts } : opts; // legacy: loadConfig(path)
315315
// Env first so the FILE wins on any field it sets.
316316
const withEnv = applyLayer({}, readEnvConfig(), o.harness);
317-
return resolveConfig(applyLayer(withEnv, readRaw(o.path ?? CONFIG_PATH), o.harness));
317+
const raw = applyLayer(withEnv, readRaw(o.path ?? CONFIG_PATH), o.harness);
318+
// The harness that ASKED is the correct fallback for an unset `harness` field — not a hardcoded
319+
// "opencode", whose silent default misfiled every other harness's background seed into an
320+
// `opencode::<project>` bank (#3247). An explicit `harness:` in the config file still wins.
321+
if (!raw.harness && o.harness) raw.harness = o.harness;
322+
return resolveConfig(raw);
318323
}
319324

320325
/** Bank-resolution fields are meaningless inside a `banks.<id>` section (they can't change the id

hindsight-integrations/coding-agents/src/core/missions.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ export const DOCUMENT_MISSION =
6363
export const OBSERVATIONS_MISSION =
6464
"Consolidate durable knowledge about THIS codebase — recurring patterns, conventions, module " +
6565
"responsibilities, and how components relate — from the ingested commits and conversations. " +
66-
"Favor stable structural understanding over one-off details.";
66+
"Favor stable structural understanding over one-off details. When a new fact contradicts or " +
67+
"supersedes an existing observation, UPDATE that observation to reflect the current state rather " +
68+
"than creating a sibling alongside it; note that the rule was revised and when, so the superseded " +
69+
"version is visible as history rather than as a competing claim.";
6770

6871
export const RETAIN_STRATEGIES = {
6972
git: { retain_mission: GIT_MISSION, retain_extraction_mode: "verbose" },

hindsight-integrations/coding-agents/src/core/session-start.test.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe("buildSessionStartContext", () => {
2020
startSeed,
2121
startSurvey,
2222
});
23-
expect(startSeed).toHaveBeenCalledWith("/repo/dir", { limit: 300 });
23+
expect(startSeed).toHaveBeenCalledWith("/repo/dir", { limit: 300, harness: "claude-code" });
2424
expect(startSurvey).toHaveBeenCalledWith("/repo/dir", {
2525
harness: "claude-code",
2626
model: "haiku",
@@ -40,6 +40,25 @@ describe("buildSessionStartContext", () => {
4040
expect(out.deferInitialReflect).toBe(true);
4141
});
4242

43+
it("threads the ASKING harness to the background seed (not the config loader's default) — #3247", async () => {
44+
// Regression: the seed used to fire without a harness, so deepen.js fell back to the config
45+
// loader's "opencode" default and misfiled a non-opencode session's survey + git history into
46+
// an `opencode::<project>` bank. The seed must receive the harness that asked.
47+
const client = { listDocumentIds: async () => new Set<string>(), listPages: listPagesOk };
48+
const startSeed = vi.fn();
49+
await buildSessionStartContext({
50+
cwd: "/repo/dir",
51+
bankId: "bank-1",
52+
cfg: resolveConfig(),
53+
client,
54+
harness: "codex",
55+
hasGit: () => true,
56+
startSeed,
57+
startSurvey: vi.fn(),
58+
});
59+
expect(startSeed).toHaveBeenCalledWith("/repo/dir", { limit: 300, harness: "codex" });
60+
});
61+
4362
it("cold git repo + codebaseSurvey:false -> starts the seed but NOT the survey", async () => {
4463
const client = { listDocumentIds: async () => new Set<string>(), listPages: listPagesOk };
4564
const startSeed = vi.fn();
@@ -53,7 +72,7 @@ describe("buildSessionStartContext", () => {
5372
startSeed,
5473
startSurvey,
5574
});
56-
expect(startSeed).toHaveBeenCalledWith("/repo/dir", { limit: 300 });
75+
expect(startSeed).toHaveBeenCalledWith("/repo/dir", { limit: 300, harness: "claude-code" });
5776
expect(startSurvey).not.toHaveBeenCalled();
5877
expect(out.systemMessage).toContain("is learning");
5978
});
@@ -123,7 +142,7 @@ describe("buildSessionStartContext", () => {
123142
});
124143
// The live bank is consulted, and an empty bank seeds — no client-side flag can contradict it.
125144
expect(called).toBe(true);
126-
expect(startSeed).toHaveBeenCalledWith("/repo/dir", { limit: 300 });
145+
expect(startSeed).toHaveBeenCalledWith("/repo/dir", { limit: 300, harness: "claude-code" });
127146
expect(out.systemMessage).toContain("is learning");
128147
});
129148

@@ -141,7 +160,7 @@ describe("buildSessionStartContext", () => {
141160
startSurvey,
142161
});
143162
// The engine is idempotent, so every warm session start re-fires it to pick up missing work.
144-
expect(startSeed).toHaveBeenCalledWith("/repo/dir", { limit: 300 });
163+
expect(startSeed).toHaveBeenCalledWith("/repo/dir", { limit: 300, harness: "claude-code" });
145164
// The cold-only extras stay off: no survey, no user-facing learning note.
146165
expect(startSurvey).not.toHaveBeenCalled();
147166
expect(out.additionalContext).toContain("- Component map (p1)");
@@ -188,7 +207,7 @@ describe("buildSessionStartContext", () => {
188207
startSeed,
189208
});
190209
// Seeding is unaffected by a listPages failure.
191-
expect(startSeed).toHaveBeenCalledWith("/repo/dir", { limit: 300 });
210+
expect(startSeed).toHaveBeenCalledWith("/repo/dir", { limit: 300, harness: "claude-code" });
192211
// Empty-state roster preamble still renders (no page names, no throw).
193212
expect(out.additionalContext).toContain("<hindsight_knowledge>");
194213
expect(out.additionalContext).toContain("No knowledge pages yet");

hindsight-integrations/coding-agents/src/core/session-start.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export async function buildSessionStartContext(args: {
138138
harness?: string;
139139
stateDir?: string;
140140
hasGit?: (dir: string) => boolean;
141-
startSeed?: (repoDir: string, opts?: { limit?: number }) => void;
141+
startSeed?: (repoDir: string, opts?: { limit?: number; harness?: string }) => void;
142142
startSurvey?: (
143143
repoDir: string,
144144
opts?: { harness?: SurveyHarness; model?: string; budgetUsd?: number }
@@ -208,7 +208,10 @@ export async function buildSessionStartContext(args: {
208208
// idempotent (per-bank lock, dedup by document id) and each run does only the missing
209209
// work: cold seed, newly appeared conversations, the next per-commit diff batch. The
210210
// one-time extras stay cold-gated below.
211-
startSeed(cwd, { limit: cfg.seedLimit });
211+
// Pass the ASKING harness through: without it deepen.js falls back to the config
212+
// loader's harness default and misfiles this session's survey + git history into the
213+
// wrong bank (e.g. `opencode::<project>` for a claude-code session). See #3247.
214+
startSeed(cwd, { limit: cfg.seedLimit, harness });
212215
// Cold iff the bank has zero source:git docs (an undefined result — server error — is
213216
// NOT treated as cold; we never surveyed/noted on an unconfirmed-empty bank).
214217
if (docIds.size === 0) {

0 commit comments

Comments
 (0)