@raidiant/notifai 9.0.0 → 9.1.0
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 +7 -0
- package/dist/command-session.d.ts +14 -0
- package/dist/command-session.js +30 -0
- package/dist/command-session.js.map +1 -0
- package/dist/commands-doctor.js +17 -15
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-guidance.js +3 -29
- package/dist/commands-guidance.js.map +1 -1
- package/dist/commands-harness-context.js +21 -1
- package/dist/commands-harness-context.js.map +1 -1
- package/dist/commands-hooks.d.ts +11 -1
- package/dist/commands-hooks.js +97 -141
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +2 -0
- package/dist/commands-init.js +41 -21
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send.d.ts +1 -0
- package/dist/commands-send.js +62 -14
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-setup-proof.d.ts +4 -3
- package/dist/commands-setup-proof.js +29 -14
- package/dist/commands-setup-proof.js.map +1 -1
- package/dist/commands.d.ts +1 -1
- package/dist/commands.js +1 -1
- package/dist/commands.js.map +1 -1
- package/dist/guidance-render.d.ts +20 -0
- package/dist/guidance-render.js +42 -0
- package/dist/guidance-render.js.map +1 -0
- package/dist/hooks.d.ts +17 -2
- package/dist/hooks.js +59 -7
- package/dist/hooks.js.map +1 -1
- package/dist/opencode-plugin.js +16 -5
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/program.js +21 -12
- package/dist/program.js.map +1 -1
- package/dist/readiness.d.ts +6 -0
- package/dist/readiness.js +37 -0
- package/dist/readiness.js.map +1 -1
- package/dist/send-attempts.d.ts +26 -0
- package/dist/send-attempts.js +139 -0
- package/dist/send-attempts.js.map +1 -0
- package/dist/session-activation.d.ts +4 -8
- package/dist/session-activation.js +25 -14
- package/dist/session-activation.js.map +1 -1
- package/package.json +1 -1
- package/src/command-session.ts +38 -0
- package/src/commands-doctor.ts +18 -28
- package/src/commands-guidance.ts +3 -38
- package/src/commands-harness-context.ts +24 -1
- package/src/commands-hooks.ts +217 -170
- package/src/commands-init.ts +46 -24
- package/src/commands-send.ts +70 -14
- package/src/commands-setup-proof.test.ts +72 -0
- package/src/commands-setup-proof.ts +33 -14
- package/src/commands.test.ts +209 -67
- package/src/commands.ts +1 -0
- package/src/guidance-render.ts +56 -0
- package/src/guidance.test.ts +28 -0
- package/src/hooks.test.ts +38 -7
- package/src/hooks.ts +69 -5
- package/src/install-hooks.test.ts +3 -3
- package/src/opencode-plugin.ts +16 -5
- package/src/program.ts +22 -12
- package/src/readiness.ts +44 -0
- package/src/send-attempts.test.ts +143 -0
- package/src/send-attempts.ts +188 -0
- package/src/session-activation.ts +34 -17
- package/src/skill.test.ts +12 -14
package/src/commands-hooks.ts
CHANGED
|
@@ -41,8 +41,6 @@ import {
|
|
|
41
41
|
parkForRetirement,
|
|
42
42
|
parseHookInput,
|
|
43
43
|
pruneAbandonedSessions,
|
|
44
|
-
readMatchingProjectSessionPointer,
|
|
45
|
-
readProjectSession,
|
|
46
44
|
readSessionState,
|
|
47
45
|
resetCursorStopActivation,
|
|
48
46
|
registerQuestion,
|
|
@@ -123,6 +121,7 @@ import {
|
|
|
123
121
|
uploadImage,
|
|
124
122
|
waitForReply,
|
|
125
123
|
} from './commands-send-support.js'
|
|
124
|
+
import { resolveCommandSession } from './command-session.js'
|
|
126
125
|
|
|
127
126
|
// ---------------------------------------------------------------------------
|
|
128
127
|
// hook / ask / close — harness integration
|
|
@@ -282,6 +281,8 @@ export async function hookRunCommand(
|
|
|
282
281
|
const stdout = sessionActivationOutput(
|
|
283
282
|
harness,
|
|
284
283
|
event === 'session-start' ? 'SessionStart' : 'SubagentStart',
|
|
284
|
+
cwd,
|
|
285
|
+
deps.env,
|
|
285
286
|
)
|
|
286
287
|
if (stdout !== undefined) deps.io.out(stdout)
|
|
287
288
|
if (
|
|
@@ -294,7 +295,7 @@ export async function hookRunCommand(
|
|
|
294
295
|
// Write stdout first. If the process dies between these operations a
|
|
295
296
|
// later prompt may duplicate context, but it cannot suppress context
|
|
296
297
|
// that the host never received.
|
|
297
|
-
markSessionActivation(envelope.session_id, deps.env, now(), activationOwnerPid)
|
|
298
|
+
markSessionActivation(envelope.session_id, deps.env, now(), activationOwnerPid, harness, cwd)
|
|
298
299
|
} catch (err) {
|
|
299
300
|
logger.error('hook.end', {
|
|
300
301
|
hook: event,
|
|
@@ -325,8 +326,8 @@ export async function hookRunCommand(
|
|
|
325
326
|
envelope.session_id !== undefined
|
|
326
327
|
) {
|
|
327
328
|
try {
|
|
328
|
-
if (claimPromptActivation(envelope.session_id, deps.env, now(), activationOwnerPid)) {
|
|
329
|
-
promptActivationStdout = sessionActivationOutput(harness, 'UserPromptSubmit')
|
|
329
|
+
if (claimPromptActivation(envelope.session_id, deps.env, now(), activationOwnerPid, harness, cwd)) {
|
|
330
|
+
promptActivationStdout = sessionActivationOutput(harness, 'UserPromptSubmit', cwd, deps.env)
|
|
330
331
|
if (promptActivationStdout !== undefined) deps.io.out(promptActivationStdout)
|
|
331
332
|
}
|
|
332
333
|
} catch (err) {
|
|
@@ -417,7 +418,7 @@ export async function hookRunCommand(
|
|
|
417
418
|
outcome: claimed ? 'followup-added' : 'already-activated',
|
|
418
419
|
decided: claimed,
|
|
419
420
|
})
|
|
420
|
-
if (claimed) deps.io.out(cursorStopActivationOutput())
|
|
421
|
+
if (claimed) deps.io.out(cursorStopActivationOutput(cwd, deps.env))
|
|
421
422
|
return EXIT.ok
|
|
422
423
|
}
|
|
423
424
|
|
|
@@ -666,10 +667,52 @@ export interface AskFlags {
|
|
|
666
667
|
image?: string[]
|
|
667
668
|
imageAlt?: string[]
|
|
668
669
|
project?: string
|
|
669
|
-
sessionId?: string
|
|
670
670
|
sessionLabel?: string
|
|
671
671
|
}
|
|
672
672
|
|
|
673
|
+
export interface AskFailure {
|
|
674
|
+
ok: false
|
|
675
|
+
registered: false
|
|
676
|
+
code: string
|
|
677
|
+
check_id: string
|
|
678
|
+
exit_code: number
|
|
679
|
+
remedy: string
|
|
680
|
+
message: string
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** One stable failure contract for every machine-readable ask refusal. */
|
|
684
|
+
export function reportAskFailure(
|
|
685
|
+
deps: CommandDeps,
|
|
686
|
+
flags: Pick<AskFlags, 'json'>,
|
|
687
|
+
failure: Omit<AskFailure, 'ok' | 'registered'>,
|
|
688
|
+
): number {
|
|
689
|
+
if (flags.json === true) {
|
|
690
|
+
deps.io.out(JSON.stringify({ ok: false, registered: false, ...failure }, null, 2))
|
|
691
|
+
} else {
|
|
692
|
+
deps.io.err(failure.message)
|
|
693
|
+
deps.io.err(`next: ${failure.remedy}`)
|
|
694
|
+
}
|
|
695
|
+
return failure.exit_code
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function askFailure(
|
|
699
|
+
deps: CommandDeps,
|
|
700
|
+
flags: AskFlags,
|
|
701
|
+
code: string,
|
|
702
|
+
checkId: string,
|
|
703
|
+
message: string,
|
|
704
|
+
remedy: string,
|
|
705
|
+
exitCode: number = EXIT.usage,
|
|
706
|
+
): number {
|
|
707
|
+
return reportAskFailure(deps, flags, {
|
|
708
|
+
code,
|
|
709
|
+
check_id: checkId,
|
|
710
|
+
exit_code: exitCode,
|
|
711
|
+
remedy,
|
|
712
|
+
message,
|
|
713
|
+
})
|
|
714
|
+
}
|
|
715
|
+
|
|
673
716
|
/** The `--form` document: what an agent writes to ask several things at once. */
|
|
674
717
|
interface AskFormQuestion {
|
|
675
718
|
text: string
|
|
@@ -863,8 +906,15 @@ function recordRegisteredQuestion(
|
|
|
863
906
|
)
|
|
864
907
|
} catch (err) {
|
|
865
908
|
log(deps).error('ask.registered', { ok: false, session: sessionId, message: String(err) })
|
|
866
|
-
|
|
867
|
-
|
|
909
|
+
return askFailure(
|
|
910
|
+
deps,
|
|
911
|
+
{ json },
|
|
912
|
+
'registration_failed',
|
|
913
|
+
'question_registration',
|
|
914
|
+
`Could not register the question: ${err instanceof Error ? err.message : String(err)}`,
|
|
915
|
+
'retire an older pending question if the limit was reached, then retry the same ask',
|
|
916
|
+
EXIT.failed,
|
|
917
|
+
)
|
|
868
918
|
}
|
|
869
919
|
log(deps).info('ask.registered', {
|
|
870
920
|
ok: true,
|
|
@@ -962,7 +1012,17 @@ async function uploadAskMedia(
|
|
|
962
1012
|
invocation: DraftInvocation,
|
|
963
1013
|
): Promise<number> {
|
|
964
1014
|
const authed = authedClient(deps, config)
|
|
965
|
-
if (!authed)
|
|
1015
|
+
if (!authed) {
|
|
1016
|
+
return askFailure(
|
|
1017
|
+
deps,
|
|
1018
|
+
flags,
|
|
1019
|
+
'auth_required',
|
|
1020
|
+
'credential',
|
|
1021
|
+
'Question routing is not paired on this machine.',
|
|
1022
|
+
'run `notifai init --json`',
|
|
1023
|
+
EXIT.auth,
|
|
1024
|
+
)
|
|
1025
|
+
}
|
|
966
1026
|
const mediaIds: string[] = []
|
|
967
1027
|
for (const image of flags.image ?? []) {
|
|
968
1028
|
if (image.startsWith('med_')) {
|
|
@@ -972,14 +1032,21 @@ async function uploadAskMedia(
|
|
|
972
1032
|
const uploaded = await uploadImage(deps, authed.client, image, config.media_origins.value)
|
|
973
1033
|
if (!uploaded.ok) {
|
|
974
1034
|
if (uploaded.error !== null) deps.io.err(uploaded.error)
|
|
975
|
-
return
|
|
1035
|
+
return askFailure(
|
|
1036
|
+
deps,
|
|
1037
|
+
flags,
|
|
1038
|
+
'media_upload_failed',
|
|
1039
|
+
'media',
|
|
1040
|
+
uploaded.error ?? 'The image upload failed.',
|
|
1041
|
+
'fix the reported image or network problem, then retry the same ask',
|
|
1042
|
+
uploaded.exit,
|
|
1043
|
+
)
|
|
976
1044
|
}
|
|
977
1045
|
mediaIds.push(uploaded.mediaId)
|
|
978
1046
|
}
|
|
979
1047
|
const ready = buildAskDraft(config, built, flags, invocation, mediaIds)
|
|
980
1048
|
if (!ready.ok) {
|
|
981
|
-
deps
|
|
982
|
-
return EXIT.usage
|
|
1049
|
+
return askFailure(deps, flags, 'invalid_draft', 'draft', ready.error, 'fix the reported field and retry')
|
|
983
1050
|
}
|
|
984
1051
|
return recordRegisteredQuestion(deps, sessionId, built, ready.draft, flags.json === true)
|
|
985
1052
|
}
|
|
@@ -999,31 +1066,26 @@ export function askCommand(
|
|
|
999
1066
|
// happens to exist on this machine.
|
|
1000
1067
|
const escapedBody = rejectAccidentalEscapedNewlines(flags.body, flags.literalBackslashN)
|
|
1001
1068
|
if (escapedBody !== null) {
|
|
1002
|
-
deps
|
|
1003
|
-
return EXIT.usage
|
|
1069
|
+
return askFailure(deps, flags, 'invalid_input', 'body', escapedBody, 'fix the body and retry')
|
|
1004
1070
|
}
|
|
1005
1071
|
const built = buildQuestions(flags, question)
|
|
1006
1072
|
if (!built.ok) {
|
|
1007
|
-
deps
|
|
1008
|
-
return EXIT.usage
|
|
1073
|
+
return askFailure(deps, flags, 'invalid_input', 'questions', built.error, 'fix the question input and retry')
|
|
1009
1074
|
}
|
|
1010
1075
|
const mediaInputError = validateMediaInputs(flags.image, flags.imageAlt)
|
|
1011
1076
|
if (mediaInputError !== null) {
|
|
1012
|
-
deps
|
|
1013
|
-
return EXIT.usage
|
|
1014
|
-
}
|
|
1015
|
-
const routingConfig = loadConfig({ cwd: deps.cwd, env: deps.env })
|
|
1016
|
-
if (!routingConfig.ask_notifications.value) {
|
|
1017
|
-
deps.io.err(
|
|
1018
|
-
'Question routing is disabled by ask_notifications=false; enable it or use a blocking `notifai send --reply` question.',
|
|
1019
|
-
)
|
|
1020
|
-
return EXIT.usage
|
|
1077
|
+
return askFailure(deps, flags, 'invalid_input', 'media', mediaInputError, 'fix the media flags and retry')
|
|
1021
1078
|
}
|
|
1022
1079
|
if (deps.store.load() === null) {
|
|
1023
|
-
|
|
1024
|
-
|
|
1080
|
+
return askFailure(
|
|
1081
|
+
deps,
|
|
1082
|
+
flags,
|
|
1083
|
+
'auth_required',
|
|
1084
|
+
'credential',
|
|
1085
|
+
'Question routing is not paired on this machine.',
|
|
1086
|
+
'run `notifai init --json`',
|
|
1087
|
+
EXIT.auth,
|
|
1025
1088
|
)
|
|
1026
|
-
return EXIT.usage
|
|
1027
1089
|
}
|
|
1028
1090
|
// An agent calling this gets no hook payload. Harness-native environment
|
|
1029
1091
|
// markers identify the active owner, while UserPromptSubmit adds the hook's
|
|
@@ -1032,78 +1094,127 @@ export function askCommand(
|
|
|
1032
1094
|
const { active, contested } = resolveActiveHarness(deps.env, deps.cwd, now)
|
|
1033
1095
|
let sessionId: string | undefined
|
|
1034
1096
|
if (active !== null) {
|
|
1035
|
-
|
|
1097
|
+
if (contested.length > 1) {
|
|
1098
|
+
return askFailure(
|
|
1099
|
+
deps,
|
|
1100
|
+
flags,
|
|
1101
|
+
'session_identity_ambiguous',
|
|
1102
|
+
'exact_session',
|
|
1103
|
+
`Several harness sessions could own this shell (${contested.map((candidate) => candidate.label).join(', ')}).`,
|
|
1104
|
+
'run the ask from a shell with one exact active harness session',
|
|
1105
|
+
)
|
|
1106
|
+
}
|
|
1107
|
+
const exactState = active.sessionId === undefined
|
|
1108
|
+
? null
|
|
1109
|
+
: readSessionState(active.sessionId, deps.env)
|
|
1110
|
+
const installationCwd = exactState?.activation_cwd ?? deps.cwd
|
|
1111
|
+
const installationDeps = installationCwd === deps.cwd ? deps : { ...deps, cwd: installationCwd }
|
|
1112
|
+
const installations = findInstallations(installationCwd, deps.env, deps.hookAdapterHome, deps.hookPlatform)
|
|
1036
1113
|
const activeInstalled = installations.some(
|
|
1037
1114
|
(installation) => installation.harness === active.harness,
|
|
1038
1115
|
)
|
|
1039
1116
|
if (!activeInstalled) {
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
return EXIT.usage
|
|
1117
|
+
return askFailure(
|
|
1118
|
+
deps,
|
|
1119
|
+
flags,
|
|
1120
|
+
'hooks_not_installed',
|
|
1121
|
+
'hook_installation',
|
|
1122
|
+
`Notifai ${active.label} hooks are not installed for this project.`,
|
|
1123
|
+
`run \`notifai init --json\` from this ${active.label} session`,
|
|
1124
|
+
)
|
|
1049
1125
|
}
|
|
1050
1126
|
if (active.sessionId === undefined) {
|
|
1051
|
-
|
|
1052
|
-
deps
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
now,
|
|
1060
|
-
active.sessionId,
|
|
1061
|
-
active.harness,
|
|
1062
|
-
)
|
|
1063
|
-
if (projectPointer === null) {
|
|
1064
|
-
for (const line of diagnoseActiveHarnessSession(
|
|
1065
|
-
active,
|
|
1066
|
-
'not-fired',
|
|
1067
|
-
installations,
|
|
1068
|
-
contested,
|
|
1069
|
-
)) {
|
|
1070
|
-
deps.io.err(line)
|
|
1071
|
-
}
|
|
1072
|
-
return EXIT.usage
|
|
1127
|
+
return askFailure(
|
|
1128
|
+
deps,
|
|
1129
|
+
flags,
|
|
1130
|
+
'session_identity_missing',
|
|
1131
|
+
'exact_session',
|
|
1132
|
+
`The active ${active.label} shell does not expose an exact session id.`,
|
|
1133
|
+
'use a blocking `notifai send --reply` question',
|
|
1134
|
+
)
|
|
1073
1135
|
}
|
|
1074
|
-
const routeProblems = activeQuestionRouteProblems(
|
|
1136
|
+
const routeProblems = activeQuestionRouteProblems(installationDeps, active, installations)
|
|
1075
1137
|
if (routeProblems.length > 0) {
|
|
1076
|
-
|
|
1077
|
-
|
|
1138
|
+
return askFailure(
|
|
1139
|
+
deps,
|
|
1140
|
+
flags,
|
|
1141
|
+
'question_routing_unavailable',
|
|
1142
|
+
'hook_contract',
|
|
1143
|
+
`Question routing is not ready: ${routeProblems.join('; ')}`,
|
|
1144
|
+
'run `notifai init --json` and follow its question-routing remedy',
|
|
1145
|
+
)
|
|
1078
1146
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1147
|
+
const state = exactState ?? readSessionState(active.sessionId, deps.env)
|
|
1148
|
+
if (state.harness !== active.harness || state.last_prompt_at === undefined) {
|
|
1149
|
+
return askFailure(
|
|
1150
|
+
deps,
|
|
1151
|
+
flags,
|
|
1152
|
+
'session_not_activated',
|
|
1153
|
+
'user_prompt_submit',
|
|
1154
|
+
`This exact ${active.label} session has not fired UserPromptSubmit.`,
|
|
1155
|
+
`send one prompt in this ${active.label} session, then retry \`notifai ask --json\``,
|
|
1156
|
+
)
|
|
1157
|
+
}
|
|
1158
|
+
if (state.last_stop_at === undefined) {
|
|
1159
|
+
return askFailure(
|
|
1160
|
+
deps,
|
|
1161
|
+
flags,
|
|
1162
|
+
'stop_not_observed',
|
|
1163
|
+
'stop_hook',
|
|
1164
|
+
`This exact ${active.label} session has fired UserPromptSubmit, but its Stop hook has not been observed.`,
|
|
1165
|
+
`end one harmless turn, send a new prompt, then retry \`notifai ask --json\``,
|
|
1082
1166
|
)
|
|
1083
|
-
return EXIT.usage
|
|
1084
1167
|
}
|
|
1085
|
-
sessionId =
|
|
1168
|
+
sessionId = active.sessionId
|
|
1086
1169
|
} else {
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1170
|
+
return askFailure(
|
|
1171
|
+
deps,
|
|
1172
|
+
flags,
|
|
1173
|
+
'session_identity_missing',
|
|
1174
|
+
'exact_session',
|
|
1175
|
+
'Could not prove which live harness session owns this command.',
|
|
1176
|
+
'run it from a supported harness with exact session identity, or use a blocking `notifai send --reply` question',
|
|
1090
1177
|
)
|
|
1091
|
-
return EXIT.usage
|
|
1092
1178
|
}
|
|
1093
1179
|
if (!sessionId) {
|
|
1094
|
-
|
|
1095
|
-
|
|
1180
|
+
return askFailure(deps, flags, 'session_identity_missing', 'exact_session', 'No exact active session is available.', 'use a blocking `notifai send --reply` question')
|
|
1181
|
+
}
|
|
1182
|
+
let routingConfig: CliConfig
|
|
1183
|
+
try {
|
|
1184
|
+
routingConfig = loadConfig({ cwd: deps.cwd, env: deps.env, sessionId })
|
|
1185
|
+
} catch (err) {
|
|
1186
|
+
return askFailure(
|
|
1187
|
+
deps,
|
|
1188
|
+
flags,
|
|
1189
|
+
'config_invalid',
|
|
1190
|
+
'configuration',
|
|
1191
|
+
`Question routing configuration is invalid: ${String(err)}`,
|
|
1192
|
+
'fix the reported Notifai configuration and retry',
|
|
1193
|
+
)
|
|
1194
|
+
}
|
|
1195
|
+
if (!routingConfig.ask_notifications.value) {
|
|
1196
|
+
return askFailure(
|
|
1197
|
+
deps,
|
|
1198
|
+
flags,
|
|
1199
|
+
'question_routing_disabled',
|
|
1200
|
+
'ask_notifications',
|
|
1201
|
+
'Question routing is disabled by ask_notifications=false.',
|
|
1202
|
+
'enable question routing or use a blocking `notifai send --reply` question',
|
|
1203
|
+
)
|
|
1096
1204
|
}
|
|
1097
1205
|
const source = resolveDraftInvocation(deps, flags, active)
|
|
1098
1206
|
if (!source.ok) {
|
|
1099
|
-
deps
|
|
1100
|
-
return EXIT.usage
|
|
1207
|
+
return askFailure(deps, flags, 'invalid_source', 'source_context', source.error, 'remove the conflicting source override and retry')
|
|
1101
1208
|
}
|
|
1102
1209
|
if (source.invocation.source?.session_id !== sessionId) {
|
|
1103
|
-
|
|
1104
|
-
|
|
1210
|
+
return askFailure(
|
|
1211
|
+
deps,
|
|
1212
|
+
flags,
|
|
1213
|
+
'session_identity_mismatch',
|
|
1214
|
+
'exact_session',
|
|
1215
|
+
`The inferred session does not match the exact active ${active.label} session.`,
|
|
1216
|
+
'remove NOTIFAI_SESSION_ID and retry from the exact active session',
|
|
1105
1217
|
)
|
|
1106
|
-
return EXIT.usage
|
|
1107
1218
|
}
|
|
1108
1219
|
|
|
1109
1220
|
// Placeholders let every body, source, project, media, and payload limit fail
|
|
@@ -1111,8 +1222,7 @@ export function askCommand(
|
|
|
1111
1222
|
const placeholders = (flags.image ?? []).map((_, index) => `med_pending_${index + 1}`)
|
|
1112
1223
|
const preflight = buildAskDraft(routingConfig, built, flags, source.invocation, placeholders)
|
|
1113
1224
|
if (!preflight.ok) {
|
|
1114
|
-
deps
|
|
1115
|
-
return EXIT.usage
|
|
1225
|
+
return askFailure(deps, flags, 'invalid_draft', 'draft', preflight.error, 'fix the reported field and retry')
|
|
1116
1226
|
}
|
|
1117
1227
|
if ((flags.image?.length ?? 0) > 0) {
|
|
1118
1228
|
return uploadAskMedia(
|
|
@@ -1178,79 +1288,6 @@ export function activeQuestionRouteProblems(
|
|
|
1178
1288
|
return [...new Set(problems)]
|
|
1179
1289
|
}
|
|
1180
1290
|
|
|
1181
|
-
type ActiveHarnessProblem = 'not-installed' | 'not-fired' | 'different-session'
|
|
1182
|
-
|
|
1183
|
-
function diagnoseActiveHarnessSession(
|
|
1184
|
-
active: ActiveHarnessSession,
|
|
1185
|
-
problem: ActiveHarnessProblem,
|
|
1186
|
-
installations: Installation[],
|
|
1187
|
-
contested: ActiveHarnessSession[] = [],
|
|
1188
|
-
): string[] {
|
|
1189
|
-
// Naming one harness confidently is wrong when the environment carries the
|
|
1190
|
-
// markers of several and none of them has fired here: whichever one the
|
|
1191
|
-
// agent is told to prompt may not be the one it is running in.
|
|
1192
|
-
const ambiguity =
|
|
1193
|
-
contested.length > 1
|
|
1194
|
-
? [
|
|
1195
|
-
`Several harness markers are present here (${contested.map((candidate) => candidate.label).join(', ')}) and none names a session that has fired in this directory, so ${active.label} is a guess from the environment. Send the prompt in the harness you are actually running.`,
|
|
1196
|
-
]
|
|
1197
|
-
: []
|
|
1198
|
-
if (problem === 'not-installed') {
|
|
1199
|
-
const others = installations.map((installation) => installation.harness)
|
|
1200
|
-
return [
|
|
1201
|
-
`Could not register the question for the active ${active.label} session: Notifai ${active.label} hooks are not installed for this project.`,
|
|
1202
|
-
...ambiguity,
|
|
1203
|
-
...(others.length === 0
|
|
1204
|
-
? []
|
|
1205
|
-
: [
|
|
1206
|
-
`Hooks installed for ${[...new Set(others)].join(', ')} do not route an active ${active.label} session.`,
|
|
1207
|
-
]),
|
|
1208
|
-
`Run \`notifai hooks install --harness ${active.harness}\`, then send one ${active.label} prompt and run \`notifai doctor\`.`,
|
|
1209
|
-
`Retry \`notifai ask\` only after doctor reports that the ${active.label} hooks fired.`,
|
|
1210
|
-
]
|
|
1211
|
-
}
|
|
1212
|
-
if (problem === 'different-session') {
|
|
1213
|
-
return [
|
|
1214
|
-
`Could not register the question for the active ${active.label} session: the project pointer belongs to another ${active.label} session or harness.`,
|
|
1215
|
-
...ambiguity,
|
|
1216
|
-
`Refusing to guess or cross-wire the question. Send one prompt in this ${active.label} session, then run \`notifai doctor\`.`,
|
|
1217
|
-
`Retry \`notifai ask\` only after doctor reports that this active ${active.label} session fired the hooks.`,
|
|
1218
|
-
]
|
|
1219
|
-
}
|
|
1220
|
-
return [
|
|
1221
|
-
`Could not register the question for the active ${active.label} session: Notifai hooks are installed, but this session has not published its pointer.`,
|
|
1222
|
-
...ambiguity,
|
|
1223
|
-
`Send one ${active.label} prompt, then run \`notifai doctor\`.`,
|
|
1224
|
-
`Retry \`notifai ask\` only after doctor reports that the active ${active.label} session fired the hooks.`,
|
|
1225
|
-
]
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
/**
|
|
1229
|
-
* Why `ask` cannot see a session, in terms of what to do about it.
|
|
1230
|
-
*
|
|
1231
|
-
* Only a UserPromptSubmit hook firing produces the pointer this reads, and the
|
|
1232
|
-
* old message answered every cause with "run `notifai hooks install` and send
|
|
1233
|
-
* one prompt". The useful next action depends on the harness: some reload
|
|
1234
|
-
* project hook files, OpenCode loads its plugin at startup, and Codex should be
|
|
1235
|
-
* checked after a prompt before assuming that a new session is required.
|
|
1236
|
-
*/
|
|
1237
|
-
function diagnoseMissingSession(deps: CommandDeps): string[] {
|
|
1238
|
-
const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome, deps.hookPlatform)
|
|
1239
|
-
if (installations.length === 0) {
|
|
1240
|
-
return [
|
|
1241
|
-
'Could not tell which harness session this is: no Notifai hooks are installed for this project.',
|
|
1242
|
-
'Run `notifai hooks install`, then follow the activation instruction it prints.',
|
|
1243
|
-
]
|
|
1244
|
-
}
|
|
1245
|
-
const where = installations.map((i) => `${i.harness} in ${i.file}`).join(', ')
|
|
1246
|
-
return [
|
|
1247
|
-
`Could not tell which harness session this is. Notifai hooks are installed (${where}),`,
|
|
1248
|
-
'but no usable session pointer from the last 24 hours exists here.',
|
|
1249
|
-
hookActivationAdvice(installations),
|
|
1250
|
-
'Do not bypass this with a guessed session id; run from the exact active harness session or use a blocking `notifai send --reply` question.',
|
|
1251
|
-
]
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
1291
|
/** The least disruptive verified way to make each installed adapter run once. */
|
|
1255
1292
|
export function hookActivationAdvice(installations: Installation[]): string {
|
|
1256
1293
|
const harnesses = new Set(installations.map((installation) => installation.harness))
|
|
@@ -1324,7 +1361,12 @@ export async function acknowledgeCommand(
|
|
|
1324
1361
|
}
|
|
1325
1362
|
|
|
1326
1363
|
const logger = log(deps)
|
|
1327
|
-
const
|
|
1364
|
+
const lifecycleSession = resolveCommandSession(deps, requestId)
|
|
1365
|
+
const config = loadLoggedConfig(deps, {
|
|
1366
|
+
cwd: deps.cwd,
|
|
1367
|
+
env: deps.env,
|
|
1368
|
+
...(lifecycleSession === null ? {} : { sessionId: lifecycleSession.sessionId }),
|
|
1369
|
+
})
|
|
1328
1370
|
logger.info('acknowledgement.attempted', {
|
|
1329
1371
|
request_id: requestId,
|
|
1330
1372
|
characters: text.length,
|
|
@@ -1349,9 +1391,8 @@ export async function acknowledgeCommand(
|
|
|
1349
1391
|
created_at: result.agent_acknowledgement.created_at,
|
|
1350
1392
|
agent_acknowledgement_required: true,
|
|
1351
1393
|
})
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
clearAcknowledgementObligation(sessionId, deps.env, requestId)
|
|
1394
|
+
if (lifecycleSession !== null) {
|
|
1395
|
+
clearAcknowledgementObligation(lifecycleSession.sessionId, deps.env, requestId)
|
|
1355
1396
|
}
|
|
1356
1397
|
if (flags.json) deps.io.out(JSON.stringify(output, null, 2))
|
|
1357
1398
|
else {
|
|
@@ -1387,7 +1428,12 @@ export async function closeCommand(
|
|
|
1387
1428
|
const local = await closeLocalQuestion(deps, requestId!, flags.json === true)
|
|
1388
1429
|
if (local !== null) return local
|
|
1389
1430
|
|
|
1390
|
-
const
|
|
1431
|
+
const lifecycleSession = resolveCommandSession(deps, requestId!)
|
|
1432
|
+
const config = loadLoggedConfig(deps, {
|
|
1433
|
+
cwd: deps.cwd,
|
|
1434
|
+
env: deps.env,
|
|
1435
|
+
...(lifecycleSession === null ? {} : { sessionId: lifecycleSession.sessionId }),
|
|
1436
|
+
})
|
|
1391
1437
|
const authed = authedClient(deps, config)
|
|
1392
1438
|
if (!authed) return EXIT.auth
|
|
1393
1439
|
try {
|
|
@@ -1426,12 +1472,12 @@ export async function closeCommand(
|
|
|
1426
1472
|
* them is the whole retirement. Live ones still need their reply window closed.
|
|
1427
1473
|
*/
|
|
1428
1474
|
async function closePendingQuestions(deps: CommandDeps, json: boolean): Promise<number> {
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1431
|
-
if (sessionId === null) {
|
|
1475
|
+
const lifecycleSession = resolveCommandSession(deps)
|
|
1476
|
+
if (lifecycleSession === null) {
|
|
1432
1477
|
deps.io.err('No active session pointer is available in this directory.')
|
|
1433
1478
|
return EXIT.noReply
|
|
1434
1479
|
}
|
|
1480
|
+
const sessionId = lifecycleSession.sessionId
|
|
1435
1481
|
const pending = readSessionState(sessionId, deps.env).pending ?? []
|
|
1436
1482
|
if (pending.length === 0) {
|
|
1437
1483
|
if (json) {
|
|
@@ -1452,7 +1498,7 @@ async function closePendingQuestions(deps: CommandDeps, json: boolean): Promise<
|
|
|
1452
1498
|
|
|
1453
1499
|
const closed: string[] = []
|
|
1454
1500
|
if (live.length > 0) {
|
|
1455
|
-
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env })
|
|
1501
|
+
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env, sessionId })
|
|
1456
1502
|
const authed = authedClient(deps, config)
|
|
1457
1503
|
if (!authed) return EXIT.auth
|
|
1458
1504
|
for (const entry of live) {
|
|
@@ -1502,9 +1548,9 @@ async function closeLocalQuestion(
|
|
|
1502
1548
|
id: string,
|
|
1503
1549
|
json: boolean,
|
|
1504
1550
|
): Promise<number | null> {
|
|
1505
|
-
const
|
|
1506
|
-
|
|
1507
|
-
|
|
1551
|
+
const lifecycleSession = resolveCommandSession(deps, id)
|
|
1552
|
+
if (lifecycleSession === null) return null
|
|
1553
|
+
const sessionId = lifecycleSession.sessionId
|
|
1508
1554
|
const entry = (readSessionState(sessionId, deps.env).pending ?? []).find(
|
|
1509
1555
|
(candidate) => candidate.question_id === id || candidate.request_id === id,
|
|
1510
1556
|
)
|
|
@@ -1531,7 +1577,7 @@ async function closeLocalQuestion(
|
|
|
1531
1577
|
return EXIT.ok
|
|
1532
1578
|
}
|
|
1533
1579
|
|
|
1534
|
-
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env })
|
|
1580
|
+
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env, sessionId })
|
|
1535
1581
|
const authed = authedClient(deps, config)
|
|
1536
1582
|
if (!authed) return EXIT.auth
|
|
1537
1583
|
try {
|
|
@@ -1567,8 +1613,9 @@ async function closeLocalQuestion(
|
|
|
1567
1613
|
}
|
|
1568
1614
|
|
|
1569
1615
|
function forgetClosedQuestion(deps: CommandDeps, requestId: string): void {
|
|
1570
|
-
const
|
|
1571
|
-
if (
|
|
1616
|
+
const lifecycleSession = resolveCommandSession(deps, requestId)
|
|
1617
|
+
if (lifecycleSession === null) return
|
|
1618
|
+
const sessionId = lifecycleSession.sessionId
|
|
1572
1619
|
const entry = (readSessionState(sessionId, deps.env).pending ?? []).find(
|
|
1573
1620
|
(candidate) => candidate.request_id === requestId,
|
|
1574
1621
|
)
|