@gajae-code/coding-agent 0.15.3 → 0.15.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/CHANGELOG.md +26 -1
- package/dist/types/cli/update-cli.d.ts +25 -5
- package/dist/types/config/autorouting-tier-map.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +52 -0
- package/dist/types/extensibility/extensions/types.d.ts +5 -0
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/gjc-runtime/workflow-placeholder.d.ts +6 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/cli/session-cli.d.ts +42 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/path-utils.d.ts +5 -6
- package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
- package/package.json +7 -7
- package/src/cli/update-cli.ts +102 -1
- package/src/commands/sdk.ts +5 -0
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +10 -4
- package/src/config/settings-schema.ts +53 -0
- package/src/config/settings.ts +1 -1
- package/src/coordinator-mcp/policy.ts +1 -1
- package/src/deep-interview/render-middleware.ts +6 -2
- package/src/extensibility/extensions/types.ts +5 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/launch-worktree.ts +44 -7
- package/src/gjc-runtime/ralplan-runtime.ts +20 -2
- package/src/gjc-runtime/tmux-sessions.ts +145 -48
- package/src/gjc-runtime/ultragoal-runtime.ts +5 -0
- package/src/gjc-runtime/workflow-placeholder.ts +33 -0
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/modes/components/hook-selector.ts +2 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/prompt-action-autocomplete.ts +26 -47
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/kind-aware-reconciliation.ts +10 -2
- package/src/sdk/bus/prompt-reconciliation.ts +5 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/cli/session-cli.ts +149 -4
- package/src/sdk/host/host.ts +7 -0
- package/src/sdk/host/session-runtime.test.ts +821 -22
- package/src/sdk/host/session-runtime.ts +669 -67
- package/src/sdk/router/session-router.ts +69 -15
- package/src/session/agent-session.ts +302 -25
- package/src/session/internal/managed-session-storage.ts +64 -26
- package/src/session/session-manager.ts +26 -8
- package/src/tools/ask-contract.ts +54 -3
- package/src/tools/ask.ts +46 -6
- package/src/tools/bash.ts +14 -0
- package/src/tools/path-utils.ts +77 -29
- package/src/tools/tmux-self-injection-guard.ts +359 -0
|
@@ -153,6 +153,8 @@ export interface ForceCloseOwnerDependencies {
|
|
|
153
153
|
waitForOwnerExitVerdict?(): Promise<OwnerVerdict>;
|
|
154
154
|
}
|
|
155
155
|
const GJC_TMUX_PSMUX_INCARNATION_OPTION = "@gjc-psmux-incarnation";
|
|
156
|
+
const effectiveSessionEnvironments = new WeakMap<GjcTmuxSessionStatus, NodeJS.ProcessEnv>();
|
|
157
|
+
const fallbackSessionEnvironments = new WeakSet<GjcTmuxSessionStatus>();
|
|
156
158
|
|
|
157
159
|
const FORCE_CLOSE_VERDICT_TIMEOUT_MS = 15_000;
|
|
158
160
|
const FORCE_CLOSE_VERDICT_POLL_MS = 50;
|
|
@@ -305,20 +307,79 @@ function parseSessionLine(line: string): GjcTmuxSessionStatus | null {
|
|
|
305
307
|
};
|
|
306
308
|
}
|
|
307
309
|
|
|
308
|
-
|
|
310
|
+
/** tmux failure shapes that mean "this socket has no server", not "tmux is broken". */
|
|
311
|
+
function isMissingServerFailure(message: string): boolean {
|
|
312
|
+
return (
|
|
313
|
+
message.includes("no server running") ||
|
|
314
|
+
(message.includes("failed to connect to server") && /(?:no such file or directory|not found)/iu.test(message))
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function isMissingSocketFailure(message: string): boolean {
|
|
319
|
+
return (
|
|
320
|
+
message.toLowerCase().includes("error connecting to ") &&
|
|
321
|
+
/(?:no such file or directory|not found)/iu.test(message)
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* `$TMUX` is inherited from whatever launched gjc and names the socket tmux
|
|
327
|
+
* talks to. Terminal hosts that emulate tmux for their own agents can export a
|
|
328
|
+
* `$TMUX` pointing at a socket they never created, and tmux then fails with
|
|
329
|
+
* `error connecting to <socket>` — the same message shape as a legitimate
|
|
330
|
+
* "no server" miss. Returning the socket path lets callers tell the two apart
|
|
331
|
+
* instead of reporting zero sessions while sessions are live on the default
|
|
332
|
+
* socket.
|
|
333
|
+
*/
|
|
334
|
+
function inheritedTmuxSocketPath(env: NodeJS.ProcessEnv): string | null {
|
|
335
|
+
const socket = env.TMUX?.split(",")[0]?.trim();
|
|
336
|
+
return socket ? socket : null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function namesInheritedTmuxSocket(message: string, socket: string): boolean {
|
|
340
|
+
const normalizedMessage = process.platform === "win32" ? message.toLowerCase() : message;
|
|
341
|
+
const normalizedSocket = process.platform === "win32" ? socket.toLowerCase() : socket;
|
|
342
|
+
return (
|
|
343
|
+
(normalizedMessage.includes(`error connecting to ${normalizedSocket} `) ||
|
|
344
|
+
normalizedMessage.includes(`failed to connect to server: ${normalizedSocket} `)) &&
|
|
345
|
+
/(?:no such file or directory|not found)/iu.test(message)
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function envWithoutInheritedTmux(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
350
|
+
const withoutTmux: NodeJS.ProcessEnv = { ...env };
|
|
351
|
+
delete withoutTmux.TMUX;
|
|
352
|
+
return withoutTmux;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
interface ListedTmuxSessions {
|
|
356
|
+
lines: string[];
|
|
357
|
+
env: NodeJS.ProcessEnv;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function runListSessions(format: string, env: NodeJS.ProcessEnv = process.env): ListedTmuxSessions {
|
|
361
|
+
let effectiveEnv = env;
|
|
309
362
|
let output = "";
|
|
310
363
|
try {
|
|
311
364
|
output = runTmux(["list-sessions", "-F", format], env);
|
|
312
365
|
} catch (error) {
|
|
313
366
|
const message = error instanceof Error ? error.message : String(error);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
367
|
+
const inheritedSocket = inheritedTmuxSocketPath(env);
|
|
368
|
+
// A miss on the default socket is a real empty list; only the inherited
|
|
369
|
+
// socket is suspect, so retry without it before concluding there is
|
|
370
|
+
// nothing to list.
|
|
371
|
+
if (inheritedSocket && namesInheritedTmuxSocket(message, inheritedSocket)) {
|
|
372
|
+
try {
|
|
373
|
+
effectiveEnv = envWithoutInheritedTmux(env);
|
|
374
|
+
output = runTmux(["list-sessions", "-F", format], effectiveEnv);
|
|
375
|
+
} catch (retryError) {
|
|
376
|
+
const retryMessage = retryError instanceof Error ? retryError.message : String(retryError);
|
|
377
|
+
if (isMissingServerFailure(retryMessage) || isMissingSocketFailure(retryMessage))
|
|
378
|
+
return { lines: [], env: effectiveEnv };
|
|
379
|
+
throw retryError;
|
|
380
|
+
}
|
|
381
|
+
} else if (isMissingServerFailure(message) || isMissingSocketFailure(message)) return { lines: [], env };
|
|
382
|
+
else throw error;
|
|
322
383
|
}
|
|
323
384
|
const lines = output
|
|
324
385
|
.split("\n")
|
|
@@ -333,20 +394,25 @@ function runListSessions(format: string, env: NodeJS.ProcessEnv = process.env):
|
|
|
333
394
|
if (lines.length > 0 && !lines[0].includes("\t")) {
|
|
334
395
|
const binary = resolveGjcTmuxBinary({ env });
|
|
335
396
|
if (binary.isPsmux) {
|
|
336
|
-
return
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
397
|
+
return {
|
|
398
|
+
lines: lines.map(line => {
|
|
399
|
+
const match = line.match(/^([^:]+):\s*(\d+)\s+windows?\s+\(created\s+([^)]+)\)/);
|
|
400
|
+
if (!match) return line;
|
|
401
|
+
const [, name, windows, created] = match;
|
|
402
|
+
const createdEpoch = String(Math.floor(new Date(`${created} UTC`).getTime() / 1000) || 0);
|
|
403
|
+
|
|
404
|
+
return [name, windows, "0", createdEpoch, "", "", "0", "", "", "", "", "", "", "", "", "", ""].join(
|
|
405
|
+
"\t",
|
|
406
|
+
);
|
|
407
|
+
}),
|
|
408
|
+
env: effectiveEnv,
|
|
409
|
+
};
|
|
344
410
|
}
|
|
345
411
|
}
|
|
346
|
-
return lines;
|
|
412
|
+
return { lines, env: effectiveEnv };
|
|
347
413
|
}
|
|
348
414
|
|
|
349
|
-
function listSessionLines(env: NodeJS.ProcessEnv = process.env):
|
|
415
|
+
function listSessionLines(env: NodeJS.ProcessEnv = process.env): ListedTmuxSessions {
|
|
350
416
|
return runListSessions(
|
|
351
417
|
`#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}\t#{${GJC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{pane_pid}\t#{${GJC_TMUX_BRANCH_OPTION}}\t#{${GJC_TMUX_BRANCH_SLUG_OPTION}}\t#{${GJC_TMUX_PROJECT_OPTION}}\t#{${GJC_TMUX_SESSION_ID_OPTION}}\t#{${GJC_TMUX_SESSION_STATE_FILE_OPTION}}\t#{${GJC_TMUX_OWNER_GENERATION_OPTION}}\t#{${GJC_TMUX_VERSION_OPTION}}\t#{${GJC_TMUX_PSMUX_INCARNATION_OPTION}}\t#{session_id}`,
|
|
352
418
|
|
|
@@ -355,8 +421,13 @@ function listSessionLines(env: NodeJS.ProcessEnv = process.env): string[] {
|
|
|
355
421
|
}
|
|
356
422
|
|
|
357
423
|
function listRawTmuxSessionNames(env: NodeJS.ProcessEnv = process.env): string[] {
|
|
358
|
-
return runListSessions("#{session_name}", env).map(line => line.split("\t")[0] ?? line);
|
|
424
|
+
return runListSessions("#{session_name}", env).lines.map(line => line.split("\t")[0] ?? line);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function assertMutationAuthority(session: GjcTmuxSessionStatus): void {
|
|
428
|
+
if (fallbackSessionEnvironments.has(session)) throw new Error("gjc_tmux_fallback_authority_unconfirmed");
|
|
359
429
|
}
|
|
430
|
+
|
|
360
431
|
function canonicalProviderStateDirs(cwd: string): string[] {
|
|
361
432
|
const gjcDir = path.join(cwd, GJC_DIR);
|
|
362
433
|
let entries: fsSync.Dirent[];
|
|
@@ -424,13 +495,24 @@ function psmuxAuthorityEnvironments(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv[]
|
|
|
424
495
|
|
|
425
496
|
export function listGjcTmuxSessions(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionStatus[] {
|
|
426
497
|
const discovered = psmuxAuthorityEnvironments(env).flatMap(authorityEnv => {
|
|
427
|
-
const
|
|
428
|
-
|
|
498
|
+
const listed = listSessionLines(authorityEnv);
|
|
499
|
+
const authority = psmuxAuthorityFromEnv(listed.env) ?? undefined;
|
|
500
|
+
return listed.lines
|
|
429
501
|
.map(parseSessionLine)
|
|
430
502
|
.filter((session): session is GjcTmuxSessionStatus => session != null)
|
|
431
|
-
.map(session =>
|
|
503
|
+
.map(session => {
|
|
504
|
+
const hydrated = hydrateSessionFromExactOptions(session, listed.env);
|
|
505
|
+
effectiveSessionEnvironments.set(hydrated, listed.env);
|
|
506
|
+
if (listed.env !== authorityEnv) fallbackSessionEnvironments.add(hydrated);
|
|
507
|
+
return hydrated;
|
|
508
|
+
})
|
|
432
509
|
.filter((session): session is GjcTmuxSessionStatus => session?.profile === GJC_TMUX_PROFILE_VALUE)
|
|
433
|
-
.map(session =>
|
|
510
|
+
.map(session => {
|
|
511
|
+
const result = authority ? { ...session, providerAuthority: authority } : session;
|
|
512
|
+
effectiveSessionEnvironments.set(result, listed.env);
|
|
513
|
+
if (listed.env !== authorityEnv) fallbackSessionEnvironments.add(result);
|
|
514
|
+
return result;
|
|
515
|
+
});
|
|
434
516
|
});
|
|
435
517
|
const names = new Set<string>();
|
|
436
518
|
for (const session of discovered) {
|
|
@@ -443,12 +525,18 @@ export function listGjcTmuxSessions(env: NodeJS.ProcessEnv = process.env): GjcTm
|
|
|
443
525
|
/** @internal */
|
|
444
526
|
export function listTmuxSessionsForGc(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionsForGc {
|
|
445
527
|
const authorityEnvironments = psmuxAuthorityEnvironments(env);
|
|
446
|
-
const sessions = authorityEnvironments.flatMap(authorityEnv =>
|
|
447
|
-
listSessionLines(authorityEnv)
|
|
528
|
+
const sessions = authorityEnvironments.flatMap(authorityEnv => {
|
|
529
|
+
const listed = listSessionLines(authorityEnv);
|
|
530
|
+
return listed.lines
|
|
448
531
|
.map(parseSessionLine)
|
|
449
532
|
.filter((session): session is GjcTmuxSessionStatus => session != null)
|
|
450
|
-
.map(session =>
|
|
451
|
-
|
|
533
|
+
.map(session => {
|
|
534
|
+
const hydrated = hydrateSessionFromExactOptions(session, listed.env);
|
|
535
|
+
effectiveSessionEnvironments.set(hydrated, listed.env);
|
|
536
|
+
if (listed.env !== authorityEnv) fallbackSessionEnvironments.add(hydrated);
|
|
537
|
+
return hydrated;
|
|
538
|
+
});
|
|
539
|
+
});
|
|
452
540
|
const tagged = sessions
|
|
453
541
|
.filter(session => session.profile === GJC_TMUX_PROFILE_VALUE)
|
|
454
542
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -1148,17 +1236,18 @@ export function readTmuxSessionTagsForGc(
|
|
|
1148
1236
|
env: NodeJS.ProcessEnv = process.env,
|
|
1149
1237
|
): GjcTmuxSessionTagsForGc {
|
|
1150
1238
|
const session = listGjcTmuxSessions(env).find(candidate => candidate.name === sessionName);
|
|
1239
|
+
const sessionEnv = session ? (effectiveSessionEnvironments.get(session) ?? env) : env;
|
|
1151
1240
|
return {
|
|
1152
|
-
profile: readExactOptionForGc(sessionName, GJC_TMUX_PROFILE_OPTION,
|
|
1153
|
-
project: readExactOptionForGc(sessionName, GJC_TMUX_PROJECT_OPTION,
|
|
1154
|
-
psmuxIncarnation: readExactOptionForGc(sessionName, GJC_TMUX_PSMUX_INCARNATION_OPTION,
|
|
1155
|
-
branch: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_OPTION,
|
|
1156
|
-
branchSlug: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_SLUG_OPTION,
|
|
1157
|
-
sessionId: readExactOptionForGc(sessionName, GJC_TMUX_SESSION_ID_OPTION,
|
|
1158
|
-
sessionStateFile: readExactOptionForGc(sessionName, GJC_TMUX_SESSION_STATE_FILE_OPTION,
|
|
1159
|
-
version: readExactOptionForGc(sessionName, GJC_TMUX_VERSION_OPTION,
|
|
1160
|
-
ownerGeneration: readExactOptionForGc(sessionName, GJC_TMUX_OWNER_GENERATION_OPTION,
|
|
1161
|
-
nativeSessionId: session?.nativeSessionId ?? readNativeTmuxSessionId(sessionName,
|
|
1241
|
+
profile: readExactOptionForGc(sessionName, GJC_TMUX_PROFILE_OPTION, sessionEnv),
|
|
1242
|
+
project: readExactOptionForGc(sessionName, GJC_TMUX_PROJECT_OPTION, sessionEnv),
|
|
1243
|
+
psmuxIncarnation: readExactOptionForGc(sessionName, GJC_TMUX_PSMUX_INCARNATION_OPTION, sessionEnv),
|
|
1244
|
+
branch: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_OPTION, sessionEnv),
|
|
1245
|
+
branchSlug: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_SLUG_OPTION, sessionEnv),
|
|
1246
|
+
sessionId: readExactOptionForGc(sessionName, GJC_TMUX_SESSION_ID_OPTION, sessionEnv),
|
|
1247
|
+
sessionStateFile: readExactOptionForGc(sessionName, GJC_TMUX_SESSION_STATE_FILE_OPTION, sessionEnv),
|
|
1248
|
+
version: readExactOptionForGc(sessionName, GJC_TMUX_VERSION_OPTION, sessionEnv),
|
|
1249
|
+
ownerGeneration: readExactOptionForGc(sessionName, GJC_TMUX_OWNER_GENERATION_OPTION, sessionEnv),
|
|
1250
|
+
nativeSessionId: session?.nativeSessionId ?? readNativeTmuxSessionId(sessionName, sessionEnv),
|
|
1162
1251
|
createdAt: session?.createdAt,
|
|
1163
1252
|
attached: session?.attached,
|
|
1164
1253
|
panePids: session?.panePids,
|
|
@@ -1171,6 +1260,9 @@ export function removeGjcTmuxSession(
|
|
|
1171
1260
|
expectedIdentity?: ExpectedGjcTmuxSessionIdentity,
|
|
1172
1261
|
): GjcTmuxSessionStatus {
|
|
1173
1262
|
const session = statusGjcTmuxSession(sessionName, env);
|
|
1263
|
+
assertMutationAuthority(session);
|
|
1264
|
+
const sessionEnv =
|
|
1265
|
+
effectiveSessionEnvironments.get(session) ?? environmentForProviderAuthority(env, session.providerAuthority);
|
|
1174
1266
|
if (session.attached || session.panePids.length > 0) {
|
|
1175
1267
|
throw new Error(`gjc_tmux_session_live:${sessionName}`);
|
|
1176
1268
|
}
|
|
@@ -1185,32 +1277,33 @@ export function removeGjcTmuxSession(
|
|
|
1185
1277
|
session.createdAt !== expectedIdentity.createdAt)
|
|
1186
1278
|
)
|
|
1187
1279
|
throw new Error(`gjc_tmux_owner_changed:${sessionName}`);
|
|
1188
|
-
if (readProfileForExactTarget(session.name,
|
|
1280
|
+
if (readProfileForExactTarget(session.name, sessionEnv) !== GJC_TMUX_PROFILE_VALUE) {
|
|
1189
1281
|
throw new Error(`gjc_tmux_session_not_managed:${sessionName}`);
|
|
1190
1282
|
}
|
|
1191
|
-
const nativeSessionId = readNativeTmuxSessionId(session.name,
|
|
1283
|
+
const nativeSessionId = readNativeTmuxSessionId(session.name, sessionEnv);
|
|
1192
1284
|
if (!nativeSessionId) throw new Error(`gjc_tmux_owner_unverifiable:${sessionName}`);
|
|
1193
1285
|
if (expectedIdentity && nativeSessionId !== expectedIdentity.nativeSessionId)
|
|
1194
1286
|
throw new Error(`gjc_tmux_owner_changed:${sessionName}`);
|
|
1195
1287
|
if (
|
|
1196
1288
|
expectedIdentity &&
|
|
1197
|
-
readExactOptionForGc(session.name, GJC_TMUX_OWNER_GENERATION_OPTION,
|
|
1289
|
+
readExactOptionForGc(session.name, GJC_TMUX_OWNER_GENERATION_OPTION, sessionEnv) !==
|
|
1290
|
+
expectedIdentity.ownerGeneration
|
|
1198
1291
|
)
|
|
1199
1292
|
throw new Error(`gjc_tmux_owner_changed:${sessionName}`);
|
|
1200
|
-
const firstServer = requireSafeTmuxServerForMutation(resolveGjcTmuxCommand(
|
|
1293
|
+
const firstServer = requireSafeTmuxServerForMutation(resolveGjcTmuxCommand(sessionEnv), sessionEnv);
|
|
1201
1294
|
if (
|
|
1202
|
-
readNativeTmuxSessionId(nativeSessionId,
|
|
1203
|
-
readProfileForExactTarget(nativeSessionId,
|
|
1295
|
+
readNativeTmuxSessionId(nativeSessionId, sessionEnv) !== nativeSessionId ||
|
|
1296
|
+
readProfileForExactTarget(nativeSessionId, sessionEnv) !== GJC_TMUX_PROFILE_VALUE
|
|
1204
1297
|
)
|
|
1205
1298
|
throw new Error(`gjc_tmux_owner_changed:${sessionName}`);
|
|
1206
|
-
const finalServer = requireSafeTmuxServerForMutation(resolveGjcTmuxCommand(
|
|
1299
|
+
const finalServer = requireSafeTmuxServerForMutation(resolveGjcTmuxCommand(sessionEnv), sessionEnv);
|
|
1207
1300
|
if (finalServer.pid !== firstServer.pid || finalServer.startTime !== firstServer.startTime)
|
|
1208
1301
|
throw new Error(`gjc_tmux_owner_changed:${sessionName}`);
|
|
1209
1302
|
runGuardedTmuxSessionCommand(
|
|
1210
1303
|
nativeSessionId,
|
|
1211
1304
|
session.name,
|
|
1212
1305
|
finalServer,
|
|
1213
|
-
|
|
1306
|
+
sessionEnv,
|
|
1214
1307
|
`kill-session -t '${nativeSessionId}'`,
|
|
1215
1308
|
expectedIdentity?.ownerGeneration,
|
|
1216
1309
|
);
|
|
@@ -1426,7 +1519,9 @@ export async function forceCloseGjcTmuxSession(
|
|
|
1426
1519
|
deps: Partial<ForceCloseOwnerDependencies> = {},
|
|
1427
1520
|
): Promise<GjcTmuxSessionStatus> {
|
|
1428
1521
|
const session = statusGjcTmuxSession(sessionName, env);
|
|
1429
|
-
|
|
1522
|
+
assertMutationAuthority(session);
|
|
1523
|
+
const sessionEnv =
|
|
1524
|
+
effectiveSessionEnvironments.get(session) ?? environmentForProviderAuthority(env, session.providerAuthority);
|
|
1430
1525
|
if (readProfileForExactTarget(session.name, sessionEnv) !== GJC_TMUX_PROFILE_VALUE)
|
|
1431
1526
|
throw new Error(`gjc_tmux_session_not_managed:${sessionName}`);
|
|
1432
1527
|
const exactPanePids = (deps.listPanePids ?? readExactSessionPanePids)(session.name, sessionEnv);
|
|
@@ -1560,7 +1655,9 @@ export async function forceCloseGjcTmuxSession(
|
|
|
1560
1655
|
|
|
1561
1656
|
export function attachGjcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv = process.env): never {
|
|
1562
1657
|
const session = statusGjcTmuxSession(sessionName, env);
|
|
1563
|
-
|
|
1658
|
+
assertMutationAuthority(session);
|
|
1659
|
+
const sessionEnv =
|
|
1660
|
+
effectiveSessionEnvironments.get(session) ?? environmentForProviderAuthority(env, session.providerAuthority);
|
|
1564
1661
|
const authority = session.providerAuthority ?? psmuxAuthorityFromEnv(sessionEnv);
|
|
1565
1662
|
const tmuxCommand = authority?.command ?? resolveGjcTmuxCommand(sessionEnv);
|
|
1566
1663
|
if (authority) assertGjcTmuxMutationAuthoritySync(authority);
|
|
@@ -86,6 +86,7 @@ import {
|
|
|
86
86
|
type UltragoalValidationLaneSelection,
|
|
87
87
|
validationLaneSelectionFor,
|
|
88
88
|
} from "./ultragoal-validation-policy";
|
|
89
|
+
import { isWorkflowPlaceholderText, WORKFLOW_PLACEHOLDER_CORRECTION } from "./workflow-placeholder";
|
|
89
90
|
import { resolveWorkflowSetting } from "./workflow-settings";
|
|
90
91
|
|
|
91
92
|
export {
|
|
@@ -1166,6 +1167,9 @@ function parseGoalsFromBrief(brief: string): ParsedGoal[] {
|
|
|
1166
1167
|
if (!title && !body) {
|
|
1167
1168
|
throw new Error(`ultragoal @goal block ${index + 1} has no title or objective`);
|
|
1168
1169
|
}
|
|
1170
|
+
if (isWorkflowPlaceholderText(body || title)) {
|
|
1171
|
+
throw new Error(`ultragoal @goal block ${index + 1} requires ${WORKFLOW_PLACEHOLDER_CORRECTION}`);
|
|
1172
|
+
}
|
|
1169
1173
|
return { title: clampTitle(title), objective: body || title };
|
|
1170
1174
|
});
|
|
1171
1175
|
}
|
|
@@ -1180,6 +1184,7 @@ export async function createUltragoalPlan(input: {
|
|
|
1180
1184
|
}): Promise<UltragoalPlan> {
|
|
1181
1185
|
const brief = input.brief.trim();
|
|
1182
1186
|
if (!brief) throw new Error("ultragoal brief is required");
|
|
1187
|
+
if (isWorkflowPlaceholderText(brief)) throw new Error(`ultragoal brief requires ${WORKFLOW_PLACEHOLDER_CORRECTION}`);
|
|
1183
1188
|
const now = new Date().toISOString();
|
|
1184
1189
|
// Parse the untrimmed brief so the raw-line delimiter contract holds: a
|
|
1185
1190
|
// leading-indented `@goal` on the first line must stay objective text rather
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Shared placeholder semantics for model-authored workflow content. */
|
|
2
|
+
|
|
3
|
+
const WORKFLOW_PLACEHOLDER_TOKENS = new Set([
|
|
4
|
+
"empty",
|
|
5
|
+
"n/a",
|
|
6
|
+
"n-a",
|
|
7
|
+
"na",
|
|
8
|
+
"none",
|
|
9
|
+
"placeholder",
|
|
10
|
+
"stub",
|
|
11
|
+
"tbd",
|
|
12
|
+
"todo",
|
|
13
|
+
"unused",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export const WORKFLOW_PLACEHOLDER_CORRECTION =
|
|
17
|
+
"provide a specific, non-placeholder question or objective instead of empty, whitespace, unused, TODO, TBD, N/A, N-A, NA, none, placeholder, or stub";
|
|
18
|
+
|
|
19
|
+
/** Exact, case-insensitive placeholder detection; meaningful sentences remain valid. */
|
|
20
|
+
export function isWorkflowPlaceholderText(value: unknown): boolean {
|
|
21
|
+
if (typeof value !== "string") return false;
|
|
22
|
+
const normalized = value.trim().toLowerCase();
|
|
23
|
+
return normalized.length === 0 || WORKFLOW_PLACEHOLDER_TOKENS.has(normalized);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Detect a legacy formatted deep-interview ask whose visible body is a placeholder. */
|
|
27
|
+
export function isLegacyDeepInterviewPlaceholder(value: unknown): boolean {
|
|
28
|
+
if (typeof value !== "string") return false;
|
|
29
|
+
const lines = value.trim().split(/\r?\n/);
|
|
30
|
+
const headerIndex = lines.findIndex(line => /^Round\s+\d+\s+\|.*\bAmbiguity\b/i.test(line.trim()));
|
|
31
|
+
if (headerIndex < 0) return false;
|
|
32
|
+
return isWorkflowPlaceholderText(lines.slice(headerIndex + 1).join("\n"));
|
|
33
|
+
}
|