talking-stick 0.4.8 → 0.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/README.md +8 -6
- package/dist/cli/event-stream.js +7 -2
- package/dist/cli/grok-session-hook.js +69 -0
- package/dist/cli/guardian.js +22 -1
- package/dist/cli/install-commands.js +33 -8
- package/dist/cli/output.js +1 -1
- package/dist/cli/registry.js +10 -0
- package/dist/cli/room-commands.js +4 -2
- package/dist/cli/turn-commands.js +4 -2
- package/dist/commands.js +16 -4
- package/dist/config.js +1 -0
- package/dist/grok-session-store.js +140 -0
- package/dist/identity.js +77 -8
- package/dist/index.js +2 -1
- package/dist/install.js +104 -1
- package/dist/instructions.js +9 -1
- package/dist/path-resolution.js +31 -0
- package/dist/service.js +229 -57
- package/dist/skill-install.js +3 -1
- package/docs/releases/0.4.10.md +17 -0
- package/docs/releases/0.4.9.md +17 -0
- package/docs/talking-stick-plan.md +1 -1
- package/package.json +1 -1
- package/skills/talking-stick/SKILL.md +2 -2
package/dist/identity.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { findGrokSessionRecord, resolveGrokSessionLogPath } from "./grok-session-store.js";
|
|
5
|
+
import { resolveContextPath } from "./path-resolution.js";
|
|
4
6
|
import { createSystemProcessInspector } from "./process-utils.js";
|
|
5
7
|
const HARNESS_CLI_EXPORT_ENV = "TT_HARNESS_EXPORT";
|
|
6
8
|
const HARNESS_CLI_AGENT_ID_ENV = "TT_HARNESS_AGENT_ID";
|
|
@@ -41,7 +43,11 @@ export function deriveMcpHarnessIdentity(options = {}) {
|
|
|
41
43
|
const signal = detectHarnessSignal(env);
|
|
42
44
|
if (signal) {
|
|
43
45
|
const processRef = resolveSignalProcessRef(signal, parentPid, parentInspection, inspector);
|
|
44
|
-
const sessionId = resolveHarnessSessionId(signal, env, processRef.pid, processRef.inspection, username, hostId, inspector
|
|
46
|
+
const sessionId = resolveHarnessSessionId(signal, env, processRef.pid, processRef.inspection, username, hostId, inspector, {
|
|
47
|
+
contextPath: options.contextPath,
|
|
48
|
+
grokSessionLogPath: options.grokSessionLogPath,
|
|
49
|
+
now: options.now
|
|
50
|
+
});
|
|
45
51
|
const harnessProcess = resolveHarnessProcessRef(signal, processRef, inspector);
|
|
46
52
|
const agentId = options.agentId ?? harnessAgentId(signal.harness, sessionId, hostId, username);
|
|
47
53
|
return {
|
|
@@ -100,7 +106,9 @@ export function deriveHarnessCliIdentity(options = {}) {
|
|
|
100
106
|
}
|
|
101
107
|
let signal = detectHarnessSignal(env);
|
|
102
108
|
if (!signal && !isHarnessCliExportEnabled(env)) {
|
|
103
|
-
|
|
109
|
+
signal = detectGrokViaAncestry(parentPid, parentInspection, inspector);
|
|
110
|
+
if (!signal)
|
|
111
|
+
return null;
|
|
104
112
|
}
|
|
105
113
|
if (!signal) {
|
|
106
114
|
signal = detectHarnessViaAncestry(parentPid, inspector);
|
|
@@ -109,7 +117,11 @@ export function deriveHarnessCliIdentity(options = {}) {
|
|
|
109
117
|
return null;
|
|
110
118
|
const processRef = resolveSignalProcessRef(signal, parentPid, parentInspection, inspector);
|
|
111
119
|
const username = options.username ?? safeUsername();
|
|
112
|
-
const sessionId = resolveHarnessSessionId(signal, env, processRef.pid, processRef.inspection, username, hostId, inspector
|
|
120
|
+
const sessionId = resolveHarnessSessionId(signal, env, processRef.pid, processRef.inspection, username, hostId, inspector, {
|
|
121
|
+
contextPath: options.contextPath,
|
|
122
|
+
grokSessionLogPath: options.grokSessionLogPath,
|
|
123
|
+
now: options.now
|
|
124
|
+
});
|
|
113
125
|
const agentId = options.agentId ?? harnessAgentId(signal.harness, sessionId, hostId, username);
|
|
114
126
|
const harnessProcess = resolveHarnessProcessRef(signal, processRef, inspector);
|
|
115
127
|
return {
|
|
@@ -136,10 +148,16 @@ function harnessAgentId(harness, sessionId, hostId, username) {
|
|
|
136
148
|
sanitizeIdentityComponent(username)
|
|
137
149
|
])}`;
|
|
138
150
|
}
|
|
139
|
-
function resolveHarnessSessionId(signal, env, parentPid, parentInspection, username, hostId, inspector) {
|
|
151
|
+
function resolveHarnessSessionId(signal, env, parentPid, parentInspection, username, hostId, inspector, options = {}) {
|
|
140
152
|
if (signal.sessionId)
|
|
141
153
|
return `harness:${signal.sessionId}`;
|
|
142
154
|
const harnessRoot = findHarnessRootInAncestry(signal.harness, parentPid, parentInspection, inspector);
|
|
155
|
+
if (signal.harness === "grok") {
|
|
156
|
+
const grokSessionId = resolveGrokHookSessionId(env, harnessRoot, options);
|
|
157
|
+
if (grokSessionId) {
|
|
158
|
+
return `harness:${grokSessionId}`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
143
161
|
if (harnessRoot) {
|
|
144
162
|
return `pid:${harnessRoot.pid}@${harnessRoot.startTime}`;
|
|
145
163
|
}
|
|
@@ -165,7 +183,7 @@ function resolveHarnessProcessRef(signal, processRef, inspector) {
|
|
|
165
183
|
// process whose command matches the named harness. Anchoring session id to
|
|
166
184
|
// that root keeps `tt` invocations stable whether they're spawned directly
|
|
167
185
|
// by the harness (MCP subprocess) or through intermediate shells (CLI shell-out).
|
|
168
|
-
function findHarnessRootInAncestry(harness, startPid, startInspection, inspector, maxDepth = 10) {
|
|
186
|
+
export function findHarnessRootInAncestry(harness, startPid, startInspection, inspector, maxDepth = 10) {
|
|
169
187
|
let result = null;
|
|
170
188
|
let currentPid = startPid;
|
|
171
189
|
let currentInspection = startInspection;
|
|
@@ -220,6 +238,7 @@ const HARNESS_COMMAND_MAPPING = {
|
|
|
220
238
|
"claude-code": "claude",
|
|
221
239
|
codex: "codex",
|
|
222
240
|
gemini: "gemini",
|
|
241
|
+
grok: "grok",
|
|
223
242
|
opencode: "opencode"
|
|
224
243
|
};
|
|
225
244
|
function detectHarnessViaAncestry(pid, inspector, maxDepth = 10) {
|
|
@@ -231,10 +250,13 @@ function detectHarnessViaAncestry(pid, inspector, maxDepth = 10) {
|
|
|
231
250
|
if (!inspection)
|
|
232
251
|
break;
|
|
233
252
|
const label = deriveCommandLabel(inspection.command);
|
|
234
|
-
|
|
253
|
+
const harness = HARNESS_COMMAND_MAPPING[label];
|
|
254
|
+
if (harness) {
|
|
235
255
|
return {
|
|
236
|
-
harness
|
|
237
|
-
sessionId:
|
|
256
|
+
harness,
|
|
257
|
+
sessionId: harness === "grok"
|
|
258
|
+
? null
|
|
259
|
+
: `pid:${inspection.pid}@${inspection.startTime}`,
|
|
238
260
|
pidHint: null
|
|
239
261
|
};
|
|
240
262
|
}
|
|
@@ -267,8 +289,51 @@ function detectHarnessSignal(env) {
|
|
|
267
289
|
pidHint: null
|
|
268
290
|
};
|
|
269
291
|
}
|
|
292
|
+
const cmuxHarness = resolveCmuxLaunchHarness(env);
|
|
293
|
+
if (cmuxHarness) {
|
|
294
|
+
return {
|
|
295
|
+
harness: cmuxHarness,
|
|
296
|
+
sessionId: null,
|
|
297
|
+
pidHint: null
|
|
298
|
+
};
|
|
299
|
+
}
|
|
270
300
|
return null;
|
|
271
301
|
}
|
|
302
|
+
function detectGrokViaAncestry(parentPid, parentInspection, inspector) {
|
|
303
|
+
const grokRoot = findHarnessRootInAncestry("grok", parentPid, parentInspection, inspector, 20);
|
|
304
|
+
return grokRoot
|
|
305
|
+
? { harness: "grok", sessionId: null, pidHint: null }
|
|
306
|
+
: null;
|
|
307
|
+
}
|
|
308
|
+
function resolveGrokHookSessionId(env, harnessRoot, options) {
|
|
309
|
+
const workspaceRoot = resolveGrokWorkspaceRoot(env, options.contextPath);
|
|
310
|
+
const record = findGrokSessionRecord({
|
|
311
|
+
logPath: options.grokSessionLogPath ??
|
|
312
|
+
resolveGrokSessionLogPath({ env }),
|
|
313
|
+
workspaceRoot,
|
|
314
|
+
grokPid: harnessRoot?.pid ?? null,
|
|
315
|
+
grokProcessStartedAt: harnessRoot?.startTime ?? null,
|
|
316
|
+
now: options.now
|
|
317
|
+
});
|
|
318
|
+
return record?.grok_session_id ?? null;
|
|
319
|
+
}
|
|
320
|
+
function resolveGrokWorkspaceRoot(env, contextPath) {
|
|
321
|
+
const explicit = nonEmpty(env.GROK_WORKSPACE_ROOT) ??
|
|
322
|
+
nonEmpty(env.CLAUDE_PROJECT_DIR);
|
|
323
|
+
if (explicit)
|
|
324
|
+
return path.resolve(explicit);
|
|
325
|
+
const candidate = contextPath ?? nonEmpty(env.PWD) ?? process.cwd();
|
|
326
|
+
try {
|
|
327
|
+
return resolveContextPath(candidate).workspace_root;
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return path.resolve(candidate);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function resolveCmuxLaunchHarness(env) {
|
|
334
|
+
const launchKind = normalizeEnvValue(env.CMUX_AGENT_LAUNCH_KIND);
|
|
335
|
+
return launchKind ? HARNESS_COMMAND_MAPPING[launchKind] ?? null : null;
|
|
336
|
+
}
|
|
272
337
|
function resolveSignalProcessRef(signal, fallbackPid, fallbackInspection, inspector) {
|
|
273
338
|
if (signal.pidHint && signal.pidHint !== fallbackPid) {
|
|
274
339
|
const hintedInspection = inspector.inspect(signal.pidHint);
|
|
@@ -294,6 +359,10 @@ function parsePositiveInteger(value) {
|
|
|
294
359
|
function nonEmpty(value) {
|
|
295
360
|
return value && value.trim().length > 0 ? value : null;
|
|
296
361
|
}
|
|
362
|
+
function normalizeEnvValue(value) {
|
|
363
|
+
const nonBlank = nonEmpty(value);
|
|
364
|
+
return nonBlank ? nonBlank.toLowerCase() : null;
|
|
365
|
+
}
|
|
297
366
|
function deriveCommandLabel(command) {
|
|
298
367
|
if (!command) {
|
|
299
368
|
return "harness";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,8 @@ export { ProtocolError, isProtocolError } from "./errors.js";
|
|
|
5
5
|
export { deriveHarnessCliIdentity, deriveHumanCliIdentity, deriveMcpHarnessIdentity } from "./identity.js";
|
|
6
6
|
export { ancestorPaths, canonicalizeContextPath, resolveContextPath, resolveWorkspaceRoot } from "./path-resolution.js";
|
|
7
7
|
export { DEFAULT_MAX_INSTRUCTION_FILE_BYTES, DEFAULT_INSTRUCTIONS_MARKDOWN, editInstructions, extractHarnessInstructions, normalizeInstructionHarness, parseInstructionScope, resetInstructions, resolveInstructionHarness, resolveInstructionPaths, showInstructions } from "./instructions.js";
|
|
8
|
-
export { SUPPORTED_HARNESSES, MissingHarnessError, detectHarness, parseHarnessList, planUninstall, resolveHarnessConfigDir, resolveOpencodeConfigDir, resolveOpencodeConfigPath, runAction, skipAction } from "./install.js";
|
|
8
|
+
export { SUPPORTED_HARNESSES, buildGrokSessionHookConfig, DEFAULT_GROK_SESSION_HOOK_COMMAND, GROK_SESSION_HOOK_EVENTS, GROK_SESSION_HOOK_FILE, MissingHarnessError, detectHarness, parseHarnessList, planGrokSessionHookInstall, planGrokSessionHookUninstall, planUninstall, resolveGrokSessionHookPath, resolveHarnessConfigDir, resolveOpencodeConfigDir, resolveOpencodeConfigPath, runAction, skipAction } from "./install.js";
|
|
9
|
+
export { DEFAULT_GROK_SESSION_RECORD_MAX_AGE_MS, appendGrokSessionRecord, findGrokSessionRecord, isGrokSessionEndEvent, readGrokSessionRecords, resolveGrokSessionLogPath } from "./grok-session-store.js";
|
|
9
10
|
export { DEFAULT_SKILL_NAME, planSkillInstall, planSkillUninstall, resolveBundledSkillPath, resolveSkillTargetPath, syncInstalledSkills } from "./skill-install.js";
|
|
10
11
|
export { readPackageVersion, readUpdateMigrationState, resolveUpdateMigrationStatePath, runFirstRunMcpMigration, runStaleMcpCleanup, writeUpdateMigrationState } from "./update-migration.js";
|
|
11
12
|
export { createSystemProcessInspector, terminateKnownProcess } from "./process-utils.js";
|
package/dist/install.js
CHANGED
|
@@ -2,9 +2,17 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
export const SUPPORTED_HARNESSES = ["claude-code", "codex", "gemini", "opencode"];
|
|
5
|
+
export const SUPPORTED_HARNESSES = ["claude-code", "codex", "gemini", "grok", "opencode"];
|
|
6
6
|
export const DEFAULT_SERVER_NAME = "talking-stick";
|
|
7
7
|
export const DEFAULT_SERVER_COMMAND = ["tt", "mcp"];
|
|
8
|
+
export const GROK_SESSION_HOOK_FILE = "talking-stick-session.json";
|
|
9
|
+
export const DEFAULT_GROK_SESSION_HOOK_COMMAND = ": talking-stick-grok-session-hook; if command -v tt >/dev/null 2>&1; then tt grok-session-hook >/dev/null 2>/dev/null || true; fi";
|
|
10
|
+
export const GROK_SESSION_HOOK_EVENTS = [
|
|
11
|
+
"SessionStart",
|
|
12
|
+
"UserPromptSubmit",
|
|
13
|
+
"PreToolUse",
|
|
14
|
+
"SessionEnd"
|
|
15
|
+
];
|
|
8
16
|
export class MissingHarnessError extends Error {
|
|
9
17
|
constructor(message) {
|
|
10
18
|
super(message);
|
|
@@ -96,11 +104,21 @@ export function resolveHarnessConfigDir(harness, options = {}) {
|
|
|
96
104
|
const resolved = resolveOptions(options);
|
|
97
105
|
return resolveHarnessConfigDirFromResolved(harness, resolved);
|
|
98
106
|
}
|
|
107
|
+
export function resolveGrokSessionHookPath(options = {}) {
|
|
108
|
+
const resolved = resolveOptions(options);
|
|
109
|
+
return path.join(resolveGrokConfigDirFromResolved(resolved), "hooks", GROK_SESSION_HOOK_FILE);
|
|
110
|
+
}
|
|
99
111
|
function resolveOpencodeConfigDirFromResolved(resolved) {
|
|
100
112
|
const xdg = resolved.env.XDG_CONFIG_HOME?.trim();
|
|
101
113
|
const base = xdg && xdg.length > 0 ? xdg : path.join(resolved.homeDir, ".config");
|
|
102
114
|
return path.join(base, "opencode");
|
|
103
115
|
}
|
|
116
|
+
function resolveGrokConfigDirFromResolved(resolved) {
|
|
117
|
+
const grokHome = resolved.env.GROK_HOME?.trim();
|
|
118
|
+
return grokHome && grokHome.length > 0
|
|
119
|
+
? grokHome
|
|
120
|
+
: path.join(resolved.homeDir, ".grok");
|
|
121
|
+
}
|
|
104
122
|
function resolveHarnessConfigDirFromResolved(harness, resolved) {
|
|
105
123
|
switch (harness) {
|
|
106
124
|
case "claude-code":
|
|
@@ -109,6 +127,8 @@ function resolveHarnessConfigDirFromResolved(harness, resolved) {
|
|
|
109
127
|
return path.join(resolved.homeDir, ".codex");
|
|
110
128
|
case "gemini":
|
|
111
129
|
return path.join(resolved.homeDir, ".gemini");
|
|
130
|
+
case "grok":
|
|
131
|
+
return resolveGrokConfigDirFromResolved(resolved);
|
|
112
132
|
case "opencode":
|
|
113
133
|
return resolveOpencodeConfigDirFromResolved(resolved);
|
|
114
134
|
default:
|
|
@@ -157,6 +177,8 @@ export function planUninstall(harness, options = {}) {
|
|
|
157
177
|
operation: "uninstall",
|
|
158
178
|
serverName: resolved.serverName
|
|
159
179
|
};
|
|
180
|
+
case "grok":
|
|
181
|
+
return skipAction(harness, "legacy Talking Stick cleanup is not applicable for grok");
|
|
160
182
|
case "opencode": {
|
|
161
183
|
const filePath = resolveOpencodeConfigPath(options);
|
|
162
184
|
const configDir = path.dirname(filePath);
|
|
@@ -189,6 +211,76 @@ export function skipAction(harness, message) {
|
|
|
189
211
|
message
|
|
190
212
|
};
|
|
191
213
|
}
|
|
214
|
+
export function planGrokSessionHookInstall(options = {}) {
|
|
215
|
+
const resolved = resolveOptions(options);
|
|
216
|
+
const grokConfigDir = resolveGrokConfigDirFromResolved(resolved);
|
|
217
|
+
const filePath = resolveGrokSessionHookPath(options);
|
|
218
|
+
if (resolved.skipMissing && !resolved.hooks.pathExists(grokConfigDir)) {
|
|
219
|
+
return skipAction("grok", `grok config directory not found: ${grokConfigDir}`);
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
kind: "file-patch",
|
|
223
|
+
harness: "grok",
|
|
224
|
+
filePath,
|
|
225
|
+
description: `write Grok session hook ${filePath}`,
|
|
226
|
+
inspect: () => inspectGrokSessionHook(filePath, resolved),
|
|
227
|
+
apply: () => writeGrokSessionHook(filePath, resolved)
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
export function planGrokSessionHookUninstall(options = {}) {
|
|
231
|
+
const resolved = resolveOptions(options);
|
|
232
|
+
const grokConfigDir = resolveGrokConfigDirFromResolved(resolved);
|
|
233
|
+
const filePath = resolveGrokSessionHookPath(options);
|
|
234
|
+
if (resolved.skipMissing && !resolved.hooks.pathExists(grokConfigDir)) {
|
|
235
|
+
return skipAction("grok", `grok config directory not found: ${grokConfigDir}`);
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
kind: "file-patch",
|
|
239
|
+
harness: "grok",
|
|
240
|
+
filePath,
|
|
241
|
+
description: `remove Grok session hook ${filePath}`,
|
|
242
|
+
inspect: () => resolved.hooks.readFile(filePath) === null ? "absent" : "present",
|
|
243
|
+
apply: () => removeGrokSessionHook(filePath, resolved)
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
export function buildGrokSessionHookConfig() {
|
|
247
|
+
const hook = {
|
|
248
|
+
type: "command",
|
|
249
|
+
command: DEFAULT_GROK_SESSION_HOOK_COMMAND,
|
|
250
|
+
timeout: 5
|
|
251
|
+
};
|
|
252
|
+
const hooks = Object.fromEntries(GROK_SESSION_HOOK_EVENTS.map((event) => [
|
|
253
|
+
event,
|
|
254
|
+
[
|
|
255
|
+
{
|
|
256
|
+
hooks: [hook]
|
|
257
|
+
}
|
|
258
|
+
]
|
|
259
|
+
]));
|
|
260
|
+
return JSON.stringify({ hooks }, null, 2) + "\n";
|
|
261
|
+
}
|
|
262
|
+
function inspectGrokSessionHook(filePath, resolved) {
|
|
263
|
+
const existing = resolved.hooks.readFile(filePath);
|
|
264
|
+
if (existing === null)
|
|
265
|
+
return "absent";
|
|
266
|
+
return existing === buildGrokSessionHookConfig() ? "present" : "different";
|
|
267
|
+
}
|
|
268
|
+
function writeGrokSessionHook(filePath, resolved) {
|
|
269
|
+
resolved.hooks.ensureDir(path.dirname(filePath));
|
|
270
|
+
resolved.hooks.writeFile(filePath, buildGrokSessionHookConfig());
|
|
271
|
+
}
|
|
272
|
+
function removeGrokSessionHook(filePath, resolved) {
|
|
273
|
+
void resolved;
|
|
274
|
+
try {
|
|
275
|
+
fs.rmSync(filePath, { force: true });
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (error.code === "ENOENT") {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
192
284
|
function patchOpencodeConfig(filePath, resolved, mode) {
|
|
193
285
|
const existing = resolved.hooks.readFile(filePath);
|
|
194
286
|
if (resolved.skipMissing) {
|
|
@@ -305,6 +397,15 @@ export function detectHarness(harness, options = {}) {
|
|
|
305
397
|
return { harness, detected: true, evidence: configDir };
|
|
306
398
|
return { harness, detected: false, evidence: "gemini not on PATH and no config directory" };
|
|
307
399
|
}
|
|
400
|
+
case "grok": {
|
|
401
|
+
const bin = resolved.hooks.which("grok");
|
|
402
|
+
if (bin)
|
|
403
|
+
return { harness, detected: true, evidence: bin };
|
|
404
|
+
const configDir = resolveHarnessConfigDirFromResolved(harness, resolved);
|
|
405
|
+
if (resolved.hooks.pathExists(configDir))
|
|
406
|
+
return { harness, detected: true, evidence: configDir };
|
|
407
|
+
return { harness, detected: false, evidence: "grok not on PATH and no config directory" };
|
|
408
|
+
}
|
|
308
409
|
case "opencode": {
|
|
309
410
|
const bin = resolved.hooks.which("opencode");
|
|
310
411
|
if (bin)
|
|
@@ -534,6 +635,8 @@ function mcpConfigLocation(action) {
|
|
|
534
635
|
return "Codex global config";
|
|
535
636
|
case "gemini":
|
|
536
637
|
return "Gemini user config";
|
|
638
|
+
case "grok":
|
|
639
|
+
return "Grok config";
|
|
537
640
|
case "opencode":
|
|
538
641
|
return "OpenCode config";
|
|
539
642
|
default:
|
package/dist/instructions.js
CHANGED
|
@@ -30,6 +30,10 @@ Lean into adversarial review, convergence, precise implementation, edge-case swe
|
|
|
30
30
|
|
|
31
31
|
Use broad context review and exploration conservatively until the project has stronger Gemini-specific dogfood. Keep handoffs concrete and do not assume responsibility that the operator assigned to another harness.
|
|
32
32
|
|
|
33
|
+
## Grok
|
|
34
|
+
|
|
35
|
+
Use Grok Build as a first-class local coding harness. Keep coordination safety ahead of speed, rely on the native Grok skill and session hook when installed, and keep handoffs concrete when another harness is better positioned to implement or review.
|
|
36
|
+
|
|
33
37
|
## OpenCode
|
|
34
38
|
|
|
35
39
|
Use terminal-native local exploration and implementation conservatively until the project has stronger OpenCode-specific dogfood. Keep coordination safety ahead of speed.
|
|
@@ -41,6 +45,8 @@ const HARNESS_ALIASES = {
|
|
|
41
45
|
"claude-code": "claude",
|
|
42
46
|
codex: "codex",
|
|
43
47
|
gemini: "gemini",
|
|
48
|
+
grok: "grok",
|
|
49
|
+
"grok-build": "grok",
|
|
44
50
|
opencode: "opencode"
|
|
45
51
|
};
|
|
46
52
|
export function resolveInstructionPaths(options = {}) {
|
|
@@ -108,7 +114,7 @@ export function resolveInstructionHarness(explicitHarness, identity) {
|
|
|
108
114
|
export function normalizeInstructionHarness(value) {
|
|
109
115
|
const normalized = HARNESS_ALIASES[normalizeKey(value)];
|
|
110
116
|
if (!normalized) {
|
|
111
|
-
throw new Error(`--harness must be one of claude, codex, gemini, opencode, all (got ${value}).`);
|
|
117
|
+
throw new Error(`--harness must be one of claude, codex, gemini, grok, opencode, all (got ${value}).`);
|
|
112
118
|
}
|
|
113
119
|
return normalized;
|
|
114
120
|
}
|
|
@@ -208,6 +214,8 @@ function parseHarnessHeader(line) {
|
|
|
208
214
|
return "codex";
|
|
209
215
|
if (key.startsWith("gemini"))
|
|
210
216
|
return "gemini";
|
|
217
|
+
if (key.startsWith("grok"))
|
|
218
|
+
return "grok";
|
|
211
219
|
if (key.startsWith("opencode"))
|
|
212
220
|
return "opencode";
|
|
213
221
|
return null;
|
package/dist/path-resolution.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
const workspaceMarkers = [
|
|
5
6
|
"CLAUDE.md",
|
|
@@ -78,8 +79,12 @@ function resolveGitRoot(canonicalContextPath) {
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
function findNearestWorkspaceMarker(startPath) {
|
|
82
|
+
const homeMarkerBoundary = resolveHomeMarkerBoundary(startPath);
|
|
81
83
|
let current = startPath;
|
|
82
84
|
while (true) {
|
|
85
|
+
if (homeMarkerBoundary && samePath(current, homeMarkerBoundary)) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
83
88
|
for (const marker of workspaceMarkers) {
|
|
84
89
|
if (fs.existsSync(path.join(current, marker))) {
|
|
85
90
|
return current;
|
|
@@ -92,6 +97,32 @@ function findNearestWorkspaceMarker(startPath) {
|
|
|
92
97
|
current = parent;
|
|
93
98
|
}
|
|
94
99
|
}
|
|
100
|
+
function resolveHomeMarkerBoundary(startPath) {
|
|
101
|
+
const homeDir = os.homedir();
|
|
102
|
+
if (!homeDir) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const resolvedHomeDir = path.resolve(homeDir);
|
|
106
|
+
const candidateHomes = [
|
|
107
|
+
canonicalizeDirectoryPath(resolvedHomeDir),
|
|
108
|
+
path.normalize(resolvedHomeDir)
|
|
109
|
+
];
|
|
110
|
+
for (const candidateHome of candidateHomes) {
|
|
111
|
+
if (!samePath(startPath, candidateHome) &&
|
|
112
|
+
isWithinOrSame(startPath, candidateHome)) {
|
|
113
|
+
return candidateHome;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function canonicalizeDirectoryPath(directoryPath) {
|
|
119
|
+
try {
|
|
120
|
+
return fs.realpathSync.native(directoryPath);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return path.normalize(directoryPath);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
95
126
|
function samePath(left, right) {
|
|
96
127
|
return path.normalize(left) === path.normalize(right);
|
|
97
128
|
}
|