@gajae-code/coding-agent 0.7.7 → 0.7.9
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 +38 -1
- package/README.md +14 -0
- package/dist/types/cli/local-provider-smoke.d.ts +59 -0
- package/dist/types/cli/notify-cli.d.ts +1 -1
- package/dist/types/cli/update-cli.d.ts +24 -1
- package/dist/types/commands/local-provider.d.ts +34 -0
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/model-profile-activation.d.ts +19 -2
- package/dist/types/config/model-profiles.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +9 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +45 -0
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +8 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +14 -5
- package/dist/types/gjc-runtime/team-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +12 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +3 -1
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
- package/dist/types/modes/components/hook-input.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +11 -0
- package/dist/types/modes/components/settings-selector.d.ts +4 -1
- package/dist/types/modes/components/status-line/types.d.ts +5 -7
- package/dist/types/modes/components/status-line.d.ts +7 -3
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -1
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +3 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +21 -1
- package/dist/types/modes/tmux-scroll.d.ts +18 -0
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
- package/dist/types/notifications/daemon-paths.d.ts +9 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +21 -4
- package/dist/types/notifications/telegram-daemon.d.ts +1 -10
- package/dist/types/tools/ask.d.ts +24 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
- package/package.json +7 -7
- package/src/cli/local-provider-smoke.ts +632 -0
- package/src/cli/notify-cli.ts +5 -3
- package/src/cli/update-cli.ts +155 -13
- package/src/cli.ts +19 -0
- package/src/commands/launch.ts +46 -1
- package/src/commands/local-provider.ts +62 -0
- package/src/commands/notify.ts +1 -1
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-profile-activation.ts +92 -11
- package/src/config/model-profiles.ts +15 -2
- package/src/config/model-registry.ts +136 -6
- package/src/config/models-config-schema.ts +9 -0
- package/src/config/settings-schema.ts +48 -0
- package/src/config/settings.ts +11 -0
- package/src/coordinator-mcp/server.ts +280 -36
- package/src/deep-interview/render-middleware.ts +1 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +14 -6
- package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -7
- package/src/gjc-runtime/deep-interview-runtime.ts +1 -1
- package/src/gjc-runtime/launch-tmux.ts +321 -38
- package/src/gjc-runtime/launch-worktree.ts +16 -2
- package/src/gjc-runtime/team-runtime.ts +87 -7
- package/src/gjc-runtime/tmux-common.ts +16 -0
- package/src/gjc-runtime/tmux-sessions.ts +13 -9
- package/src/gjc-runtime/ultragoal-guard.ts +11 -5
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/modes/components/agent-dashboard.ts +2 -1
- package/src/modes/components/custom-model-preset-wizard.ts +31 -192
- package/src/modes/components/hook-editor.ts +7 -4
- package/src/modes/components/hook-input.ts +4 -0
- package/src/modes/components/model-selector.ts +260 -12
- package/src/modes/components/session-observer-overlay.ts +3 -3
- package/src/modes/components/settings-selector.ts +506 -5
- package/src/modes/components/status-line/segments.ts +16 -19
- package/src/modes/components/status-line/types.ts +6 -2
- package/src/modes/components/status-line.ts +70 -35
- package/src/modes/controllers/event-controller.ts +98 -4
- package/src/modes/controllers/extension-ui-controller.ts +5 -0
- package/src/modes/controllers/input-controller.ts +15 -0
- package/src/modes/controllers/selector-controller.ts +142 -11
- package/src/modes/interactive-mode.ts +7 -2
- package/src/modes/prompt-action-autocomplete.ts +8 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +11 -4
- package/src/modes/tmux-scroll.ts +92 -0
- package/src/modes/types.ts +6 -1
- package/src/modes/utils/terminal-bell.ts +48 -0
- package/src/notifications/daemon-paths.ts +22 -0
- package/src/notifications/telegram-daemon-cli.ts +125 -13
- package/src/notifications/telegram-daemon.ts +6 -21
- package/src/prompts/system/system-prompt.md +5 -0
- package/src/sdk.ts +10 -1
- package/src/session/agent-session.ts +84 -6
- package/src/slash-commands/builtin-registry.ts +106 -13
- package/src/tools/ask.ts +11 -1
- package/src/tools/eval.ts +2 -2
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/ultragoal-ask-guard.ts +39 -5
|
@@ -119,7 +119,7 @@ interface TurnRecord {
|
|
|
119
119
|
target: string | null;
|
|
120
120
|
tmux_keys_sent?: boolean;
|
|
121
121
|
prompt_acknowledged?: boolean;
|
|
122
|
-
state?: "queued" | "tmux_keys_sent" | "acknowledged" | "unavailable";
|
|
122
|
+
state?: "queued" | "tmux_keys_sent" | "acknowledged" | "unavailable" | "unacknowledged";
|
|
123
123
|
attempts: Array<{
|
|
124
124
|
delivered: boolean;
|
|
125
125
|
created_at: string;
|
|
@@ -214,6 +214,9 @@ interface CoordinatorEventInput {
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
const MISSING_FINAL_RESPONSE_ADVISORY = "completion_missing_final_response";
|
|
217
|
+
const PROMPT_ACK_TIMEOUT_REASON = "runtime_prompt_ack_timeout";
|
|
218
|
+
const DEFAULT_RUNTIME_PROMPT_ACK_TIMEOUT_MS = 10_000;
|
|
219
|
+
const MAX_RUNTIME_PROMPT_ACK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
217
220
|
const ACTIVE_TURN_STATUSES = new Set<TurnStatus>(["delivering", "active", "waiting_for_answer", "completing"]);
|
|
218
221
|
const TERMINAL_TURN_STATUSES = new Set<TurnStatus>(["completed", "failed", "cancelled", "superseded"]);
|
|
219
222
|
const TURN_ID_PATTERN = /^turn-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -317,8 +320,14 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
317
320
|
properties: {
|
|
318
321
|
session_id: sessionId,
|
|
319
322
|
turn_id: { type: "string" },
|
|
320
|
-
timeout_ms: {
|
|
321
|
-
|
|
323
|
+
timeout_ms: {
|
|
324
|
+
type: "number",
|
|
325
|
+
description: "Bounded await timeout in milliseconds, capped at 30 minutes.",
|
|
326
|
+
},
|
|
327
|
+
poll_interval_ms: {
|
|
328
|
+
type: "number",
|
|
329
|
+
description: "Bounded polling interval in milliseconds, capped at 10 seconds.",
|
|
330
|
+
},
|
|
322
331
|
lines: { type: "number" },
|
|
323
332
|
},
|
|
324
333
|
required: ["turn_id"],
|
|
@@ -406,7 +415,10 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
406
415
|
after_seq: { type: "number" },
|
|
407
416
|
session_id: sessionId,
|
|
408
417
|
event_types: { type: "array", items: { type: "string" } },
|
|
409
|
-
timeout_ms: {
|
|
418
|
+
timeout_ms: {
|
|
419
|
+
type: "number",
|
|
420
|
+
description: "Bounded event long-poll timeout in milliseconds, capped at 30 seconds.",
|
|
421
|
+
},
|
|
410
422
|
limit: { type: "number" },
|
|
411
423
|
},
|
|
412
424
|
},
|
|
@@ -447,7 +459,8 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
447
459
|
await_completion: { type: "boolean", description: "If true, poll the turn until terminal or timeout." },
|
|
448
460
|
timeout_ms: {
|
|
449
461
|
type: "number",
|
|
450
|
-
description:
|
|
462
|
+
description:
|
|
463
|
+
"Bounded await timeout in milliseconds, capped at 30 minutes like gjc_coordinator_await_turn.",
|
|
451
464
|
},
|
|
452
465
|
poll_interval_ms: { type: "number", description: "Bounded await polling interval." },
|
|
453
466
|
lines: { type: "number", description: "Bounded advisory tail lines returned with await/read payloads." },
|
|
@@ -1039,6 +1052,135 @@ async function markTurnTerminalFromSessionState(
|
|
|
1039
1052
|
return resolved;
|
|
1040
1053
|
}
|
|
1041
1054
|
|
|
1055
|
+
function runtimeStateAcknowledgesTurn(turn: TurnRecord, sessionState: CoordinatorSessionState | null): boolean {
|
|
1056
|
+
return (
|
|
1057
|
+
sessionState?.source === "agent_session_event" &&
|
|
1058
|
+
sessionState.current_turn_id === turn.turn_id &&
|
|
1059
|
+
(sessionState.state === "running" ||
|
|
1060
|
+
sessionState.state === "needs_user_input" ||
|
|
1061
|
+
sessionState.state === "completed" ||
|
|
1062
|
+
sessionState.state === "errored")
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
async function markTurnAcknowledgedFromRuntimeState(
|
|
1067
|
+
namespaceDir: string,
|
|
1068
|
+
turn: TurnRecord,
|
|
1069
|
+
sessionState: CoordinatorSessionState,
|
|
1070
|
+
): Promise<TurnRecord> {
|
|
1071
|
+
if (turn.delivery.prompt_acknowledged === true && turn.delivery.state === "acknowledged") return turn;
|
|
1072
|
+
const timestamp = new Date().toISOString();
|
|
1073
|
+
const acknowledged: TurnRecord = {
|
|
1074
|
+
...turn,
|
|
1075
|
+
delivery: {
|
|
1076
|
+
...turn.delivery,
|
|
1077
|
+
delivered: true,
|
|
1078
|
+
prompt_acknowledged: true,
|
|
1079
|
+
state: "acknowledged",
|
|
1080
|
+
attempts: [
|
|
1081
|
+
...turn.delivery.attempts,
|
|
1082
|
+
{
|
|
1083
|
+
delivered: true,
|
|
1084
|
+
created_at: sessionState.updated_at,
|
|
1085
|
+
reason: "runtime_prompt_acknowledged",
|
|
1086
|
+
channel: "runtime_ack",
|
|
1087
|
+
tmux_keys_sent: turn.delivery.tmux_keys_sent,
|
|
1088
|
+
},
|
|
1089
|
+
],
|
|
1090
|
+
},
|
|
1091
|
+
updated_at: timestamp,
|
|
1092
|
+
};
|
|
1093
|
+
await writeTurnRecord(namespaceDir, acknowledged);
|
|
1094
|
+
await writeActiveTurn(namespaceDir, acknowledged);
|
|
1095
|
+
return acknowledged;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function turnAwaitingRuntimeAckExpired(turn: TurnRecord, nowMs: number, ackTimeoutMs: number): boolean {
|
|
1099
|
+
if (!ACTIVE_TURN_STATUSES.has(turn.status)) return false;
|
|
1100
|
+
if (turn.delivery.tmux_keys_sent !== true) return false;
|
|
1101
|
+
if (turn.delivery.prompt_acknowledged === true) return false;
|
|
1102
|
+
if (turn.delivery.state !== "tmux_keys_sent") return false;
|
|
1103
|
+
const deliveredAt =
|
|
1104
|
+
turn.delivery.attempts.findLast(attempt => attempt.channel === "tmux_keys")?.created_at ?? turn.updated_at;
|
|
1105
|
+
const deliveredMs = Date.parse(deliveredAt);
|
|
1106
|
+
return Number.isFinite(deliveredMs) && nowMs - deliveredMs >= ackTimeoutMs;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
async function markTurnFailedForUnacknowledgedDelivery(
|
|
1110
|
+
namespaceDir: string,
|
|
1111
|
+
turn: TurnRecord,
|
|
1112
|
+
ackTimeoutMs: number,
|
|
1113
|
+
): Promise<TurnRecord> {
|
|
1114
|
+
const timestamp = new Date().toISOString();
|
|
1115
|
+
const message = `Tmux key delivery succeeded, but the GJC runtime did not acknowledge the prompt or emit turn_start within ${ackTimeoutMs}ms. The turn never started; stop waiting and inspect/retry the coordinator session.`;
|
|
1116
|
+
const failed: TurnRecord = {
|
|
1117
|
+
...turn,
|
|
1118
|
+
status: "failed",
|
|
1119
|
+
delivery: {
|
|
1120
|
+
...turn.delivery,
|
|
1121
|
+
delivered: false,
|
|
1122
|
+
queued: false,
|
|
1123
|
+
prompt_acknowledged: false,
|
|
1124
|
+
state: "unacknowledged",
|
|
1125
|
+
attempts: [
|
|
1126
|
+
...turn.delivery.attempts,
|
|
1127
|
+
{
|
|
1128
|
+
delivered: false,
|
|
1129
|
+
created_at: timestamp,
|
|
1130
|
+
reason: PROMPT_ACK_TIMEOUT_REASON,
|
|
1131
|
+
channel: "runtime_ack",
|
|
1132
|
+
tmux_keys_sent: true,
|
|
1133
|
+
},
|
|
1134
|
+
],
|
|
1135
|
+
},
|
|
1136
|
+
final_response: {
|
|
1137
|
+
text: message,
|
|
1138
|
+
format: "markdown",
|
|
1139
|
+
source: "coordinator_delivery_ack_timeout",
|
|
1140
|
+
artifact_path: null,
|
|
1141
|
+
truncated: false,
|
|
1142
|
+
},
|
|
1143
|
+
error: { code: PROMPT_ACK_TIMEOUT_REASON, message, recoverable: true },
|
|
1144
|
+
evidence: [
|
|
1145
|
+
...turn.evidence,
|
|
1146
|
+
{
|
|
1147
|
+
type: PROMPT_ACK_TIMEOUT_REASON,
|
|
1148
|
+
message,
|
|
1149
|
+
tmux_keys_sent: true,
|
|
1150
|
+
prompt_acknowledged: false,
|
|
1151
|
+
created_at: timestamp,
|
|
1152
|
+
},
|
|
1153
|
+
],
|
|
1154
|
+
liveness: { checked_at: timestamp, live: turn.liveness.live, reason: PROMPT_ACK_TIMEOUT_REASON },
|
|
1155
|
+
updated_at: timestamp,
|
|
1156
|
+
completed_at: timestamp,
|
|
1157
|
+
};
|
|
1158
|
+
await writeTurnRecord(namespaceDir, failed);
|
|
1159
|
+
await clearActiveTurn(namespaceDir, failed);
|
|
1160
|
+
await writeSessionState(namespaceDir, failed.session_id, "stale", {
|
|
1161
|
+
lastTurnId: failed.turn_id,
|
|
1162
|
+
live: failed.liveness.live,
|
|
1163
|
+
reason: PROMPT_ACK_TIMEOUT_REASON,
|
|
1164
|
+
});
|
|
1165
|
+
return failed;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
async function reconcileRuntimeAcknowledgement(
|
|
1169
|
+
namespaceDir: string,
|
|
1170
|
+
turn: TurnRecord,
|
|
1171
|
+
sessionState: CoordinatorSessionState | null,
|
|
1172
|
+
ackTimeoutMs: number,
|
|
1173
|
+
options: { failOnTimeout: boolean } = { failOnTimeout: true },
|
|
1174
|
+
): Promise<TurnRecord> {
|
|
1175
|
+
if (sessionState && runtimeStateAcknowledgesTurn(turn, sessionState)) {
|
|
1176
|
+
return await markTurnAcknowledgedFromRuntimeState(namespaceDir, turn, sessionState);
|
|
1177
|
+
}
|
|
1178
|
+
if (options.failOnTimeout && turnAwaitingRuntimeAckExpired(turn, Date.now(), ackTimeoutMs)) {
|
|
1179
|
+
return await markTurnFailedForUnacknowledgedDelivery(namespaceDir, turn, ackTimeoutMs);
|
|
1180
|
+
}
|
|
1181
|
+
return turn;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1042
1184
|
function shellQuote(value: string): string {
|
|
1043
1185
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
1044
1186
|
}
|
|
@@ -1086,16 +1228,33 @@ function asTerminalTurnStatus(status: unknown): TurnStatus | null {
|
|
|
1086
1228
|
return null;
|
|
1087
1229
|
}
|
|
1088
1230
|
|
|
1089
|
-
|
|
1231
|
+
export const COORDINATOR_AWAIT_TURN_TIMEOUT_MAX_MS = 30 * 60 * 1000;
|
|
1232
|
+
export const COORDINATOR_RUNTIME_PROMPT_ACK_TIMEOUT_MAX_MS = MAX_RUNTIME_PROMPT_ACK_TIMEOUT_MS;
|
|
1233
|
+
export const COORDINATOR_EVENT_WATCH_TIMEOUT_MAX_MS = 30_000;
|
|
1234
|
+
export const COORDINATOR_POLL_INTERVAL_MAX_MS = 10_000;
|
|
1235
|
+
|
|
1236
|
+
function parsePositiveIntegerMs(value: unknown, fallback: number): number {
|
|
1090
1237
|
const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
|
|
1091
|
-
|
|
1092
|
-
return Math.min(parsed, 30_000);
|
|
1238
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
1093
1239
|
}
|
|
1094
1240
|
|
|
1095
|
-
function
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1241
|
+
export function boundedAwaitTurnTimeoutMs(value: unknown): number {
|
|
1242
|
+
return Math.min(parsePositiveIntegerMs(value, 1000), COORDINATOR_AWAIT_TURN_TIMEOUT_MAX_MS);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
export function boundedRuntimePromptAckTimeoutMs(value: unknown): number {
|
|
1246
|
+
return Math.min(
|
|
1247
|
+
parsePositiveIntegerMs(value, DEFAULT_RUNTIME_PROMPT_ACK_TIMEOUT_MS),
|
|
1248
|
+
COORDINATOR_RUNTIME_PROMPT_ACK_TIMEOUT_MAX_MS,
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
export function boundedEventWatchTimeoutMs(value: unknown): number {
|
|
1253
|
+
return Math.min(parsePositiveIntegerMs(value, 1000), COORDINATOR_EVENT_WATCH_TIMEOUT_MAX_MS);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
export function boundedPollIntervalMs(value: unknown): number {
|
|
1257
|
+
return Math.min(Math.max(parsePositiveIntegerMs(value, 100), 10), COORDINATOR_POLL_INTERVAL_MAX_MS);
|
|
1099
1258
|
}
|
|
1100
1259
|
async function runCommand(command: string[]): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
1101
1260
|
const proc = Bun.spawn(command, { stdout: "pipe", stderr: "pipe" });
|
|
@@ -1114,8 +1273,10 @@ async function sendTmuxPromptKeys(
|
|
|
1114
1273
|
prompt: string,
|
|
1115
1274
|
runner: CommandRunner = runCommand,
|
|
1116
1275
|
): Promise<boolean> {
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1276
|
+
const typed = await runner(["tmux", "send-keys", "-t", target, "-l", prompt]);
|
|
1277
|
+
if (typed.exitCode !== 0) return false;
|
|
1278
|
+
const submitted = await runner(["tmux", "send-keys", "-t", target, "-l", "\x1b[13;5u"]);
|
|
1279
|
+
return submitted.exitCode === 0;
|
|
1119
1280
|
}
|
|
1120
1281
|
|
|
1121
1282
|
function boundedLineCount(value: unknown): number {
|
|
@@ -1387,7 +1548,9 @@ export async function readCoordinatorArtifact(
|
|
|
1387
1548
|
}
|
|
1388
1549
|
|
|
1389
1550
|
export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions = {}) {
|
|
1390
|
-
const
|
|
1551
|
+
const env = options.env ?? process.env;
|
|
1552
|
+
const config = buildCoordinatorMcpConfig(env);
|
|
1553
|
+
const promptAckTimeoutMs = boundedRuntimePromptAckTimeoutMs(env.GJC_COORDINATOR_MCP_PROMPT_ACK_TIMEOUT_MS);
|
|
1391
1554
|
const services = options.services ?? {};
|
|
1392
1555
|
const namespaceDir = coordinatorNamespacePath(config);
|
|
1393
1556
|
const commandRunner = services.commandRunner ?? runCommand;
|
|
@@ -1485,14 +1648,17 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1485
1648
|
await writeTurnRecord(namespaceDir, activeTurn);
|
|
1486
1649
|
await writeActiveTurn(namespaceDir, activeTurn);
|
|
1487
1650
|
const sessionState = await readSessionState(namespaceDir, activeTurn.session_id);
|
|
1488
|
-
const
|
|
1489
|
-
sessionState
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1651
|
+
const runtimeStateAlreadyAcknowledged =
|
|
1652
|
+
sessionState !== null && runtimeStateAcknowledgesTurn(activeTurn, sessionState);
|
|
1653
|
+
const resolvedTurn =
|
|
1654
|
+
runtimeStateAlreadyAcknowledged && sessionState
|
|
1655
|
+
? await markTurnAcknowledgedFromRuntimeState(namespaceDir, activeTurn, sessionState)
|
|
1656
|
+
: activeTurn;
|
|
1657
|
+
if (!runtimeStateAlreadyAcknowledged && !tmuxKeysSent) {
|
|
1658
|
+
await writeSessionState(namespaceDir, activeTurn.session_id, "stale", {
|
|
1493
1659
|
currentTurnId: activeTurn.turn_id,
|
|
1494
1660
|
live,
|
|
1495
|
-
reason:
|
|
1661
|
+
reason: "tmux_delivery_unavailable",
|
|
1496
1662
|
});
|
|
1497
1663
|
}
|
|
1498
1664
|
await appendCoordinatorEvent(namespaceDir, {
|
|
@@ -1505,7 +1671,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1505
1671
|
payloadRef: path.relative(namespaceDir, turnFile(namespaceDir, activeTurn.turn_id)),
|
|
1506
1672
|
metadata: { target: typeof target === "string" ? target : null, live },
|
|
1507
1673
|
});
|
|
1508
|
-
return
|
|
1674
|
+
return resolvedTurn;
|
|
1509
1675
|
}
|
|
1510
1676
|
|
|
1511
1677
|
async function promoteNextQueuedTurn(sessionId: string): Promise<TurnRecord | null> {
|
|
@@ -1533,33 +1699,67 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1533
1699
|
let resolvedTurn = turn;
|
|
1534
1700
|
let advisoryStatus: Record<string, unknown> = { live: false };
|
|
1535
1701
|
let sessionState = await readSessionState(namespaceDir, turn.session_id);
|
|
1702
|
+
resolvedTurn = await reconcileRuntimeAcknowledgement(
|
|
1703
|
+
namespaceDir,
|
|
1704
|
+
resolvedTurn,
|
|
1705
|
+
sessionState,
|
|
1706
|
+
promptAckTimeoutMs,
|
|
1707
|
+
{ failOnTimeout: false },
|
|
1708
|
+
);
|
|
1709
|
+
if (resolvedTurn !== turn) sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1536
1710
|
if (
|
|
1537
1711
|
sessionState &&
|
|
1538
|
-
ACTIVE_TURN_STATUSES.has(
|
|
1539
|
-
sessionState.current_turn_id ===
|
|
1712
|
+
ACTIVE_TURN_STATUSES.has(resolvedTurn.status) &&
|
|
1713
|
+
(sessionState.current_turn_id === resolvedTurn.turn_id ||
|
|
1714
|
+
(sessionState.state === "errored" &&
|
|
1715
|
+
sessionState.source === "agent_session_event" &&
|
|
1716
|
+
sessionState.current_turn_id == null)) &&
|
|
1540
1717
|
(sessionState.state === "completed" || sessionState.state === "errored")
|
|
1541
1718
|
) {
|
|
1542
|
-
resolvedTurn = await markTurnTerminalFromSessionState(namespaceDir,
|
|
1719
|
+
resolvedTurn = await markTurnTerminalFromSessionState(namespaceDir, resolvedTurn, sessionState);
|
|
1543
1720
|
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1544
1721
|
} else if (
|
|
1545
1722
|
sessionState &&
|
|
1546
|
-
ACTIVE_TURN_STATUSES.has(
|
|
1547
|
-
sessionState.current_turn_id ===
|
|
1723
|
+
ACTIVE_TURN_STATUSES.has(resolvedTurn.status) &&
|
|
1724
|
+
sessionState.current_turn_id === resolvedTurn.turn_id &&
|
|
1548
1725
|
sessionState.state === "stale" &&
|
|
1549
1726
|
sessionState.reason === "tmux_delivery_unavailable" &&
|
|
1550
|
-
|
|
1727
|
+
resolvedTurn.delivery.state === "unavailable" &&
|
|
1551
1728
|
session &&
|
|
1552
1729
|
hasTmuxIdentity(session)
|
|
1553
1730
|
) {
|
|
1554
|
-
resolvedTurn = await markTurnFailedForUnavailableSession(
|
|
1731
|
+
resolvedTurn = await markTurnFailedForUnavailableSession(
|
|
1732
|
+
namespaceDir,
|
|
1733
|
+
resolvedTurn,
|
|
1734
|
+
"tmux_delivery_unavailable",
|
|
1735
|
+
);
|
|
1555
1736
|
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1556
|
-
} else if (!session && ACTIVE_TURN_STATUSES.has(
|
|
1557
|
-
resolvedTurn = await markTurnFailedForUnavailableSession(namespaceDir,
|
|
1737
|
+
} else if (!session && ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
|
|
1738
|
+
resolvedTurn = await markTurnFailedForUnavailableSession(namespaceDir, resolvedTurn, "session_record_missing");
|
|
1558
1739
|
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1559
1740
|
} else if (session) {
|
|
1560
1741
|
advisoryStatus = await inspectTmuxSession(session, boundedLineCount(lines), commandRunner);
|
|
1561
|
-
if (
|
|
1562
|
-
resolvedTurn
|
|
1742
|
+
if (
|
|
1743
|
+
ACTIVE_TURN_STATUSES.has(resolvedTurn.status) &&
|
|
1744
|
+
hasTmuxIdentity(session) &&
|
|
1745
|
+
advisoryStatus.live === false
|
|
1746
|
+
) {
|
|
1747
|
+
resolvedTurn = await markTurnFailedForUnavailableSession(
|
|
1748
|
+
namespaceDir,
|
|
1749
|
+
resolvedTurn,
|
|
1750
|
+
"tmux_session_missing",
|
|
1751
|
+
);
|
|
1752
|
+
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
if (ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
|
|
1756
|
+
resolvedTurn = await reconcileRuntimeAcknowledgement(
|
|
1757
|
+
namespaceDir,
|
|
1758
|
+
resolvedTurn,
|
|
1759
|
+
sessionState,
|
|
1760
|
+
promptAckTimeoutMs,
|
|
1761
|
+
);
|
|
1762
|
+
if (!ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
|
|
1563
1763
|
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1564
1764
|
}
|
|
1565
1765
|
}
|
|
@@ -1579,6 +1779,46 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1579
1779
|
};
|
|
1580
1780
|
}
|
|
1581
1781
|
|
|
1782
|
+
async function reconcileActiveTurnAcknowledgements(): Promise<void> {
|
|
1783
|
+
const turns = (await listJsonFiles(turnsDir(namespaceDir)))
|
|
1784
|
+
.map(turn => asRecord(turn) as TurnRecord | null)
|
|
1785
|
+
.filter((turn): turn is TurnRecord => turn !== null && ACTIVE_TURN_STATUSES.has(turn.status));
|
|
1786
|
+
for (const turn of turns) {
|
|
1787
|
+
let sessionState = await readSessionState(namespaceDir, turn.session_id);
|
|
1788
|
+
const resolvedTurn = await reconcileRuntimeAcknowledgement(
|
|
1789
|
+
namespaceDir,
|
|
1790
|
+
turn,
|
|
1791
|
+
sessionState,
|
|
1792
|
+
promptAckTimeoutMs,
|
|
1793
|
+
{ failOnTimeout: false },
|
|
1794
|
+
);
|
|
1795
|
+
if (!ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) continue;
|
|
1796
|
+
if (resolvedTurn !== turn) sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1797
|
+
const session = asRecord(await readJsonFile(sessionFile(resolvedTurn.session_id)));
|
|
1798
|
+
if (
|
|
1799
|
+
sessionState &&
|
|
1800
|
+
sessionState.current_turn_id === resolvedTurn.turn_id &&
|
|
1801
|
+
sessionState.state === "stale" &&
|
|
1802
|
+
sessionState.reason === "tmux_delivery_unavailable" &&
|
|
1803
|
+
resolvedTurn.delivery.state === "unavailable" &&
|
|
1804
|
+
session &&
|
|
1805
|
+
hasTmuxIdentity(session)
|
|
1806
|
+
) {
|
|
1807
|
+
await markTurnFailedForUnavailableSession(namespaceDir, resolvedTurn, "tmux_delivery_unavailable");
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
if (!session) {
|
|
1811
|
+
await markTurnFailedForUnavailableSession(namespaceDir, resolvedTurn, "session_record_missing");
|
|
1812
|
+
continue;
|
|
1813
|
+
}
|
|
1814
|
+
if (hasTmuxIdentity(session) && (await hasTmuxSession(session, commandRunner)) === false) {
|
|
1815
|
+
await markTurnFailedForUnavailableSession(namespaceDir, resolvedTurn, "tmux_session_missing");
|
|
1816
|
+
continue;
|
|
1817
|
+
}
|
|
1818
|
+
await reconcileRuntimeAcknowledgement(namespaceDir, resolvedTurn, sessionState, promptAckTimeoutMs);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1582
1822
|
async function callTool(name: string, args: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
|
|
1583
1823
|
try {
|
|
1584
1824
|
if (name === "gjc_coordinator_list_sessions") return { ok: true, sessions: await listSessions() };
|
|
@@ -1619,6 +1859,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1619
1859
|
};
|
|
1620
1860
|
}
|
|
1621
1861
|
if (name === "gjc_coordinator_read_status") {
|
|
1862
|
+
await reconcileActiveTurnAcknowledgements();
|
|
1622
1863
|
const sessionId = args.session_id;
|
|
1623
1864
|
if (sessionId) {
|
|
1624
1865
|
const session = asRecord(await readJsonFile(sessionFile(sessionId)));
|
|
@@ -1651,6 +1892,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1651
1892
|
if (name === "gjc_coordinator_read_artifact")
|
|
1652
1893
|
return await readCoordinatorArtifact(config, { path: args.path });
|
|
1653
1894
|
if (name === "gjc_coordinator_read_coordination_status") {
|
|
1895
|
+
await reconcileActiveTurnAcknowledgements();
|
|
1654
1896
|
const sessions = jsonRecords(await listSessions());
|
|
1655
1897
|
const sessionStates = jsonRecords(await listJsonFiles(path.join(namespaceDir, "session-states")));
|
|
1656
1898
|
const turns = jsonRecords(await listJsonFiles(turnsDir(namespaceDir)));
|
|
@@ -1684,13 +1926,15 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1684
1926
|
};
|
|
1685
1927
|
}
|
|
1686
1928
|
if (name === "gjc_coordinator_watch_events") {
|
|
1929
|
+
await reconcileActiveTurnAcknowledgements();
|
|
1687
1930
|
const limit = boundedEventLimit(args.limit);
|
|
1688
|
-
const timeoutMs =
|
|
1931
|
+
const timeoutMs = boundedEventWatchTimeoutMs(args.timeout_ms);
|
|
1689
1932
|
let events = await readCoordinatorEvents(namespaceDir);
|
|
1690
1933
|
let matched = filterCoordinatorEvents(events, args, limit);
|
|
1691
1934
|
let timedOut = false;
|
|
1692
1935
|
if (matched.length === 0 && timeoutMs > 0) {
|
|
1693
1936
|
await waitForCoordinatorEvents(namespaceDir, timeoutMs);
|
|
1937
|
+
await reconcileActiveTurnAcknowledgements();
|
|
1694
1938
|
events = await readCoordinatorEvents(namespaceDir);
|
|
1695
1939
|
matched = filterCoordinatorEvents(events, args, limit);
|
|
1696
1940
|
timedOut = matched.length === 0;
|
|
@@ -1819,7 +2063,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1819
2063
|
};
|
|
1820
2064
|
if (promptAliasIgnored) base.prompt_alias_ignored = true;
|
|
1821
2065
|
if (args.await_completion === true && !shouldQueue) {
|
|
1822
|
-
const timeoutMs =
|
|
2066
|
+
const timeoutMs = boundedAwaitTurnTimeoutMs(args.timeout_ms);
|
|
1823
2067
|
const pollIntervalMs = boundedPollIntervalMs(args.poll_interval_ms);
|
|
1824
2068
|
const deadline = Date.now() + timeoutMs;
|
|
1825
2069
|
let payload = await readTurnPayload(turn.turn_id, sessionId, args.lines);
|
|
@@ -1979,7 +2223,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1979
2223
|
return await readTurnPayload(args.turn_id, args.session_id, args.lines);
|
|
1980
2224
|
}
|
|
1981
2225
|
if (name === "gjc_coordinator_await_turn") {
|
|
1982
|
-
const timeoutMs =
|
|
2226
|
+
const timeoutMs = boundedAwaitTurnTimeoutMs(args.timeout_ms);
|
|
1983
2227
|
const pollIntervalMs = boundedPollIntervalMs(args.poll_interval_ms);
|
|
1984
2228
|
const deadline = Date.now() + timeoutMs;
|
|
1985
2229
|
let payload = await readTurnPayload(args.turn_id, args.session_id, args.lines);
|
|
@@ -49,6 +49,7 @@ interface ThresholdModel {
|
|
|
49
49
|
type DeepInterviewModel = RoundQuestionModel | TopologyQuestionModel | ProgressModel | ThresholdModel;
|
|
50
50
|
|
|
51
51
|
function normalizeText(text: string): string {
|
|
52
|
+
if (typeof text !== "string") return "";
|
|
52
53
|
return text.trim().replaceAll("\r\n", "\n");
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -51,6 +51,10 @@ Inspired by the [Ouroboros project](https://github.com/Q00/ouroboros) which demo
|
|
|
51
51
|
- Keep prompt payloads budgeted: summarize or trim oversized initial context/history before composing question, scoring, spec, or handoff prompts
|
|
52
52
|
- If the user's initial context is oversized, create a concise prompt-safe summary first and wait for that summary before ambiguity scoring, question generation, or downstream execution handoff
|
|
53
53
|
- Do not proceed to execution until ambiguity ≤ the resolved threshold for this run and the user explicitly approves a scoped execution path
|
|
54
|
+
- Treat user wording such as `implementation`, "implementation plan", Korean `구현`, or "구현 계획" as describing the eventual target, not permission to implement now.
|
|
55
|
+
- While still in deep-interview, do not implement, edit/write code, launch implementation workers, or start task/skill/ultragoal implementation; continue interviewing for scope, risks, acceptance criteria, and unknowns.
|
|
56
|
+
- When the user wants interview output for eventual implementation, say: "I can interview for an implementation plan, but I won't implement during deep-interview." Then continue clarifying scope, risks, acceptance criteria, and unknowns.
|
|
57
|
+
- Implementation requires an explicit phase transition/approval after the interview: deep-interview must first produce its spec/handoff, the workflow phase must explicitly transition out of deep-interview, and execution approval must be captured by a downstream execution path.
|
|
54
58
|
- Allow early exit with a clear warning if ambiguity is still high
|
|
55
59
|
- Persist interview state for resume across session interruptions
|
|
56
60
|
- A multi-persona lateral-review panel convenes at ambiguity-milestone transitions (and before synthesizing any agent-supplied answer) to expose blind spots from independent perspectives
|
|
@@ -74,7 +78,7 @@ Inspired by the [Ouroboros project](https://github.com/Q00/ouroboros) which demo
|
|
|
74
78
|
|
|
75
79
|
## Native Plugin Invocation Guard (Issue #3030)
|
|
76
80
|
|
|
77
|
-
If this raw bundled skill is loaded by GJC's native skill loader through `/skill:deep-interview`, do not treat that path as permission to skip rendered GJC setup. The user-facing invocation is `/skill:deep-interview`; do not recommend or advertise CLI bridge commands as the deep-interview entrypoint. Regardless of invocation path, Phase 0 below remains blocking and must resolve `gjc.deepInterview.ambiguityThreshold` from settings before any announcement, state write, question, or ambiguity score.
|
|
81
|
+
If this raw bundled skill is loaded by GJC's native skill loader through `/skill:deep-interview`, do not treat that path as permission to skip rendered GJC setup. The user-facing invocation is `/skill:deep-interview`; do not recommend or advertise CLI bridge commands as the deep-interview entrypoint. Regardless of invocation path, Phase 0 below remains blocking and must resolve `gjc.deepInterview.ambiguityThreshold` from pre-resolved native state or settings before any announcement, state write, question, or ambiguity score.
|
|
78
82
|
|
|
79
83
|
## Corrupt current-session state recovery
|
|
80
84
|
|
|
@@ -84,20 +88,24 @@ When deep-interview detects its own current-session state is corrupt, tampered,
|
|
|
84
88
|
|
|
85
89
|
Complete this phase before Phase 1, before brownfield exploration, before GJC state persistence, before Round 0, and before any ambiguity scoring. Do not continue if the resolved threshold and source are unknown.
|
|
86
90
|
|
|
87
|
-
1. **
|
|
91
|
+
1. **Prefer pre-resolved native state**:
|
|
92
|
+
- First inspect active deep-interview state with `gjc state deep-interview read --json`.
|
|
93
|
+
- If state contains a finite numeric `threshold` and a non-empty `threshold_source`, use those values, set `<resolvedThreshold>`, `<resolvedThresholdPercent>`, and `<resolvedThresholdSource>`, and skip optional settings-file reads. This is the normal `/skill:deep-interview` path because the native hook already resolved settings quietly before loading the skill.
|
|
94
|
+
2. **Only if native state lacks a resolved threshold, read threshold settings in precedence order**:
|
|
88
95
|
- User settings: `[$GJC_CONFIG_DIR|~/.gjc]/settings.json`
|
|
89
96
|
- Project settings: `./.gjc/settings.json` (overrides user settings)
|
|
90
|
-
|
|
91
|
-
-
|
|
97
|
+
- Read `gjc.deepInterview.ambiguityThreshold` only from files that are known to exist; optional settings-file absence is expected and must not be surfaced as failed `Read` calls.
|
|
98
|
+
- Do not probe arbitrary ancestor candidates such as `../../.gjc/settings.json`; use the current project `.gjc/settings.json` and user settings only.
|
|
99
|
+
3. **Resolve threshold and source**:
|
|
92
100
|
- Use the project value when valid; otherwise use the user value when valid; otherwise use the default `0.05`.
|
|
93
101
|
- Set these run variables exactly: `<resolvedThreshold>`, `<resolvedThresholdPercent>`, and `<resolvedThresholdSource>` (for example `./.gjc/settings.json`, `[$GJC_CONFIG_DIR|~/.gjc]/settings.json`, or `default`).
|
|
94
|
-
|
|
102
|
+
4. **Emit the required first line to the user before any other interview announcement**:
|
|
95
103
|
|
|
96
104
|
```
|
|
97
105
|
Deep Interview threshold: <resolvedThresholdPercent> (source: <resolvedThresholdSource>)
|
|
98
106
|
```
|
|
99
107
|
|
|
100
|
-
|
|
108
|
+
5. **Carry threshold source forward mechanically**:
|
|
101
109
|
- Substitute `<resolvedThreshold>`, `<resolvedThresholdPercent>`, and `<resolvedThresholdSource>` throughout the remaining instructions before continuing.
|
|
102
110
|
- Include `threshold_source` in the first `gjc state write` payload and preserve it on later state updates; do not edit `.gjc/_session-{sessionid}/state` files directly unless an explicit force override is active.
|
|
103
111
|
- Include both threshold and source in the final spec metadata.
|
|
@@ -19,7 +19,7 @@ Ralplan is the consensus planning workflow. It triggers iterative planning with
|
|
|
19
19
|
|
|
20
20
|
## Flags
|
|
21
21
|
|
|
22
|
-
- `--interactive`: Enables user prompts
|
|
22
|
+
- `--interactive`: Enables extra mid-loop user prompts (draft review in step 2 and one-at-a-time reconciliation in step 6c). Regardless of this flag, the workflow always finishes the post-interview gate with an `ask`-tool prompt offering Refine further / Approve ultragoal / Approve team / Stop here, and never auto-executes — execution always requires explicit approval through that prompt.
|
|
23
23
|
- `--deliberate`: Forces deliberate mode for high-risk work. Adds pre-mortem (3 scenarios) and expanded test planning (unit/integration/e2e/observability). Without this flag, deliberate mode can still auto-enable when the request explicitly signals high risk (auth/security, migrations, destructive changes, production incidents, compliance/PII, public API breakage).
|
|
24
24
|
- `--architect openai-code`: Use OpenAI code for the Architect pass when OpenAI code CLI is available. Otherwise, briefly note the fallback and keep the default GJC Architect review.
|
|
25
25
|
- `--critic openai-code`: Use OpenAI code for the Critic pass when OpenAI code CLI is available. Otherwise, briefly note the fallback and keep the default GJC Critic review.
|
|
@@ -83,13 +83,20 @@ The consensus workflow:
|
|
|
83
83
|
6. **Post-ralplan interview** (intent reconciliation gate): After Critic returns `APPROVE` and before the plan is finalized, reconcile the consensus plan against the user's actual intent. The goal is to make sure ralplan did not silently bake in assumptions that conflict with what the user wants.
|
|
84
84
|
a. **Collect open items** from the run: every assumption the Planner/Architect/Critic resolved by assumption rather than by stated fact, every ambiguity flagged during review, and every decision the loop made without explicit user input. Source these from the persisted `planner`/`architect`/`critic`/`revision` stage artifacts, not from memory.
|
|
85
85
|
b. **Cross-check prior context for conflicts**: glob `.gjc/_session-{sessionid}/specs/deep-interview-*.md` and other prior specs/plans/context relevant by topic. For each, list points where the consensus plan contradicts, weakens, or expands beyond a previously crystallized decision, constraint, or non-goal. Cite the conflicting artifact and line/section.
|
|
86
|
-
c. **Reconcile with the user**:
|
|
87
|
-
-
|
|
88
|
-
-
|
|
86
|
+
c. **Reconcile with the user via the `ask` tool (always, regardless of `--interactive`)**: Never stop idle with plain-text prose after the consensus loop. Every reconciliation question MUST go through the `ask` tool with contextual options plus free-text.
|
|
87
|
+
- If open items exist, confirm the open assumptions and conflicts **one at a time** with the `ask` tool, weakest/highest-impact first, polishing intent. If any confirmation reveals that the plan diverges from user intent, route the consolidated correction back into the re-review loop (step 5b Planner revision) and re-run Architect + Critic before returning here. Cap at the same 5-iteration ceiling.
|
|
88
|
+
- If the plan is crystal clear (no open assumptions or prior-context conflicts), skip straight to the step 8 final-options `ask` instead of inventing filler questions.
|
|
89
|
+
- For every confirmed open item, embed the resolved outcome into the final plan under an **## Intent Reconciliation** section so the `pending approval` artifact records each decision; record any item the user explicitly defers as an open confirmation under that same section.
|
|
89
90
|
d. Persist the reconciliation with `gjc ralplan --write --stage post-interview --stage_n <N> --artifact "..." --json`, then return the receipt/path plus a compact status (reconciled-clean / reconciled-with-revision / open-confirmations-pending) instead of pasting the full body.
|
|
90
|
-
7. On reconciliation completion, mark the plan `pending approval` unless explicit execution approval has already been captured, persist the ADR/final plan via `gjc ralplan --write --stage final --stage_n <N> --artifact "..."`, and do not directly edit `.gjc/_session-{sessionid}/plans`.
|
|
91
|
-
8.
|
|
92
|
-
|
|
91
|
+
7. On reconciliation completion, mark the plan `pending approval` unless explicit execution approval has already been captured, persist the ADR/final plan via `gjc ralplan --write --stage final --stage_n <N> --artifact "..."`, and do not directly edit `.gjc/_session-{sessionid}/plans`. Final plan must include ADR (Decision, Drivers, Alternatives considered, Why chosen, Consequences, Follow-ups) and, when present, the **## Intent Reconciliation** section.
|
|
92
|
+
8. **Always** present the finalized plan via the `ask` tool (regardless of `--interactive`) with `workflowGate: { stage: "ralplan", kind: "approval" }` on the final question so RPC/headless clients receive a `ralplan`/`approval` workflow gate, not a deep-interview question gate. Use these options:
|
|
93
|
+
- **Refine further** — re-run the consensus loop / request changes, then return here
|
|
94
|
+
- **Approve execution via ultragoal (Recommended)** — goal-tracked autonomous execution
|
|
95
|
+
- **Approve execution via team** — only when tmux-based interactive worker parallelization is required
|
|
96
|
+
- **Stop here** — keep the plan as `pending approval` and make no further changes
|
|
97
|
+
|
|
98
|
+
Always include a free-text option. Do not stop with plain text and no `ask`; the post-interview gate's terminal action is this `ask`.
|
|
99
|
+
9. On approval: invoke `/skill:ultragoal` for execution by default; invoke `/skill:team` only when the user explicitly needs tmux-based interactive worker parallelization. On **Refine further**, return to the step 5 re-review loop. On **Stop here**, leave the `pending approval` artifact and stop. Never implement directly.
|
|
93
100
|
|
|
94
101
|
Before invoking `/skill:team` or `/skill:ultragoal`, mark ralplan ready for handoff so the skill tool's chain guard permits the transition:
|
|
95
102
|
|
|
@@ -100,7 +100,7 @@ async function resolveSpecContent(rawSpec: string, cwd: string): Promise<string>
|
|
|
100
100
|
if (stat.isFile()) return await fs.readFile(candidate, "utf-8");
|
|
101
101
|
} catch (error) {
|
|
102
102
|
const err = error as NodeJS.ErrnoException;
|
|
103
|
-
if (err.code !== "ENOENT" && err.code !== "ENOTDIR") {
|
|
103
|
+
if (err.code !== "ENOENT" && err.code !== "ENOTDIR" && err.code !== "ENAMETOOLONG") {
|
|
104
104
|
throw new DeepInterviewCommandError(2, `failed to read --spec ${candidate}: ${err.message}`);
|
|
105
105
|
}
|
|
106
106
|
}
|