@raidiant/notifai 4.0.0 → 5.0.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 +46 -0
- package/README.md +5 -0
- package/dist/commands.d.ts +10 -1
- package/dist/commands.js +174 -58
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.js +14 -5
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +3 -1
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -1
- package/dist/file-lock.d.ts +6 -1
- package/dist/file-lock.js +55 -9
- package/dist/file-lock.js.map +1 -1
- package/dist/harnesses.d.ts +1 -1
- package/dist/harnesses.js.map +1 -1
- package/dist/hooks.d.ts +72 -5
- package/dist/hooks.js +407 -79
- package/dist/hooks.js.map +1 -1
- package/dist/install-hooks.d.ts +91 -14
- package/dist/install-hooks.js +226 -34
- package/dist/install-hooks.js.map +1 -1
- package/dist/interactive.d.ts +1 -1
- package/dist/interactive.js +1 -1
- package/dist/logging.d.ts +8 -0
- package/dist/logging.js +8 -0
- package/dist/logging.js.map +1 -1
- package/dist/main.js +44 -17
- package/dist/main.js.map +1 -1
- package/dist/opencode-plugin.d.ts +8 -0
- package/dist/opencode-plugin.js +24 -162
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/send.d.ts +6 -0
- package/dist/send.js +25 -14
- package/dist/send.js.map +1 -1
- package/dist/ui/help.d.ts +0 -1
- package/dist/ui/help.js +6 -3
- package/dist/ui/help.js.map +1 -1
- package/dist/ui/theme.d.ts +0 -15
- package/dist/ui/theme.js +2 -18
- package/dist/ui/theme.js.map +1 -1
- package/package.json +2 -2
- package/src/commands.test.ts +185 -51
- package/src/commands.ts +198 -56
- package/src/config-schema.test.ts +1 -1
- package/src/config-schema.ts +16 -5
- package/src/config.test.ts +41 -9
- package/src/config.ts +13 -0
- package/src/file-lock.test.ts +41 -0
- package/src/file-lock.ts +59 -9
- package/src/harnesses.test.ts +37 -29
- package/src/harnesses.ts +1 -4
- package/src/hooks.test.ts +358 -21
- package/src/hooks.ts +457 -97
- package/src/install-hooks.test.ts +232 -12
- package/src/install-hooks.ts +241 -37
- package/src/interactive.ts +1 -1
- package/src/logging-process-worker.test.ts +8 -2
- package/src/logging.test.ts +33 -18
- package/src/logging.ts +11 -0
- package/src/main.ts +57 -23
- package/src/opencode-plugin.ts +24 -164
- package/src/question-caps.test.ts +49 -0
- package/src/send.ts +32 -16
- package/src/skill.test.ts +199 -146
- package/src/ui/help.ts +8 -3
- package/src/ui/theme.ts +2 -16
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
|
|
9
9
|
and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
|
|
10
10
|
|
|
11
|
+
## [5.0.0](https://github.com/Raidiant-io/notifai/compare/v4.0.0...v5.0.0) (2026-08-16)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### ⚠ BREAKING CHANGES
|
|
15
|
+
|
|
16
|
+
* `notifai send` now requires --kind. The account preference `agent_acknowledgements_enabled` is renamed to `agent_acknowledgement_text_enabled` and governs only the written reply.
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* **cli:** report an ask registration as data, and stop init promising routing it lacks ([156159b](https://github.com/Raidiant-io/notifai/commit/156159b4d39b249a1be65ac86a9f7452ee9720d2))
|
|
21
|
+
* keep a question answerable for a day, and let the user choose ([ace207e](https://github.com/Raidiant-io/notifai/commit/ace207e04cfdc3a0ccda14dffc79a882a663034c))
|
|
22
|
+
* make kind required and carry attention, and always acknowledge an answer ([07e7c21](https://github.com/Raidiant-io/notifai/commit/07e7c216d52f3041e50ce5c7ec1ce4a3f8491e64))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
* clean up what the help, the README and a test comment were saying ([221ca6b](https://github.com/Raidiant-io/notifai/commit/221ca6b1d04a80cbd8473d54cd50a6024dcf6c50))
|
|
28
|
+
* **cli:** retire drafts the server rejects as invalid ([4023e42](https://github.com/Raidiant-io/notifai/commit/4023e42e3aeb9f8674239ab0516ccf2527b1428c))
|
|
29
|
+
* **cli:** retire drafts the server rejects as invalid ([d676726](https://github.com/Raidiant-io/notifai/commit/d6767265e4718102f7c97c6ee939073e79b038a2))
|
|
30
|
+
* **cli:** stop --reply-window advertising the default it no longer has ([b9eeabc](https://github.com/Raidiant-io/notifai/commit/b9eeabcbafd9a8e84d2aa86f44b04ca4ece237f2))
|
|
31
|
+
* give the detached waiter the wall clock its host already allows ([3d4b111](https://github.com/Raidiant-io/notifai/commit/3d4b111afb4d35bd8c04121deee82b849dd0ade1))
|
|
32
|
+
* **hooks:** never write a Codex hooks.json Notifai does not own ([8fdd00c](https://github.com/Raidiant-io/notifai/commit/8fdd00c6018915fe2164800c5797a9c3cb92da68))
|
|
33
|
+
* **hooks:** never write a Codex hooks.json Notifai does not own ([8948519](https://github.com/Raidiant-io/notifai/commit/894851942db6283ff856025e7f7d2dd86695013d))
|
|
34
|
+
* **hooks:** read the duplicate-representation warning as a sentence ([e0d041f](https://github.com/Raidiant-io/notifai/commit/e0d041f729e75a4c909f41c5dfafe6602f88edeb))
|
|
35
|
+
* **hooks:** read the duplicate-representation warning as a sentence ([936f068](https://github.com/Raidiant-io/notifai/commit/936f068d2587d56fc10a89a7d87c81c37ef50e2d))
|
|
36
|
+
* **hooks:** report a Codex dual representation only when Notifai is in both ([f4426dc](https://github.com/Raidiant-io/notifai/commit/f4426dc6e3876ac0a75f31ce5fb81c4ec2f4719d))
|
|
37
|
+
* **hooks:** report a Codex dual representation only when Notifai is in both ([2bab419](https://github.com/Raidiant-io/notifai/commit/2bab41987bf9fa251f71785b9881e3c8418be1bb))
|
|
38
|
+
* **hooks:** stop rewriting the whole of a user's Codex config.toml ([e037e22](https://github.com/Raidiant-io/notifai/commit/e037e227646178a0cbc08e7a00ab47129b3b435f))
|
|
39
|
+
* **hooks:** stop rewriting the whole of a user's Codex config.toml ([92ff189](https://github.com/Raidiant-io/notifai/commit/92ff1891c3f42092758af257ec3be8b68e51c0f1))
|
|
40
|
+
* let a question outlive the waiter that pushed it ([de242e7](https://github.com/Raidiant-io/notifai/commit/de242e753434dda87f665de6474491320fbc7365))
|
|
41
|
+
* **lock:** survive losing the rendezvous while registering ([2b5d46d](https://github.com/Raidiant-io/notifai/commit/2b5d46d5221b843fc2db5a803da561e3dbb3b7b0))
|
|
42
|
+
* **lock:** survive losing the rendezvous while registering ([efb8bd6](https://github.com/Raidiant-io/notifai/commit/efb8bd6f718f9f5e7e9bda1471cb88db0a77ef4c))
|
|
43
|
+
* reject an unknown --platform locally, the way send already does ([7ba7678](https://github.com/Raidiant-io/notifai/commit/7ba7678214cc3c98919ece2f533ed40f080951f3))
|
|
44
|
+
* report a failed image upload like every other API failure ([be76c48](https://github.com/Raidiant-io/notifai/commit/be76c4808e6c37bb97d64b14b018b148c4887d94))
|
|
45
|
+
* stop a busy session discarding the answer it was too busy to take ([13ce700](https://github.com/Raidiant-io/notifai/commit/13ce7003c393741b82886910cb51b18d0a9f1065))
|
|
46
|
+
* stop a turn-ending question expiring with the waiter that pushed it ([dd2cda6](https://github.com/Raidiant-io/notifai/commit/dd2cda6a339d711bc4c6723b93ccf940207565a7))
|
|
47
|
+
* stop a typed prompt erasing an outstanding acknowledgement ([ed15e18](https://github.com/Raidiant-io/notifai/commit/ed15e18fffd893ef0c5ad27bc17d4f78b0a88b6c))
|
|
48
|
+
* stop several surfaces stating things that are not true ([56402b9](https://github.com/Raidiant-io/notifai/commit/56402b98b763bdce2fb75c79f8502a92968a0abb))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
### Dependencies
|
|
52
|
+
|
|
53
|
+
* The following workspace dependencies were updated
|
|
54
|
+
* dependencies
|
|
55
|
+
* @raidiant/notifai-protocol bumped from 1.0.0 to 2.0.0
|
|
56
|
+
|
|
11
57
|
## [4.0.0](https://github.com/Raidiant-io/notifai/compare/v3.0.1...v4.0.0) (2026-08-15)
|
|
12
58
|
|
|
13
59
|
|
package/README.md
CHANGED
|
@@ -54,6 +54,11 @@ By default a question reaches your devices when the agent turn ends, whether or
|
|
|
54
54
|
not you are at the keyboard (`ask_grace_seconds = 0`). Set a positive grace
|
|
55
55
|
period for a terminal-only answer window first.
|
|
56
56
|
|
|
57
|
+
When you answer, the agent acknowledges it before it does anything else, so you
|
|
58
|
+
find out your reply landed and what it set in motion — not just that you sent
|
|
59
|
+
it. Answering from your phone and wondering whether anything happened is the
|
|
60
|
+
whole problem this solves.
|
|
61
|
+
|
|
57
62
|
For a harness that cannot resume an idle agent turn, `notifai send
|
|
58
63
|
--reply` blocks and waits for the answer instead.
|
|
59
64
|
|
package/dist/commands.d.ts
CHANGED
|
@@ -202,6 +202,8 @@ export declare function hookRunCommand(deps: CommandDeps, event: string, readStd
|
|
|
202
202
|
*/
|
|
203
203
|
export declare function describeHookFailure(err: unknown): string[];
|
|
204
204
|
export interface AskFlags {
|
|
205
|
+
/** Emit the registration and its turn obligation as one JSON object. */
|
|
206
|
+
json?: boolean;
|
|
205
207
|
choice?: string[];
|
|
206
208
|
/** The single question is multi-select: several answers may be chosen. */
|
|
207
209
|
multi?: boolean;
|
|
@@ -238,7 +240,14 @@ export declare function buildQuestions(flags: AskFlags, question: string | undef
|
|
|
238
240
|
* for `ask_grace_seconds` before it reaches any device.
|
|
239
241
|
*/
|
|
240
242
|
export declare function askCommand(deps: CommandDeps, question: string | undefined, flags: AskFlags): number | Promise<number>;
|
|
241
|
-
/**
|
|
243
|
+
/**
|
|
244
|
+
* Record the one Agent Acknowledgement associated with a replied-to request.
|
|
245
|
+
*
|
|
246
|
+
* `--text` is optional here because the acknowledgement is not: an account may
|
|
247
|
+
* turn the agent's written reply off, and the receipt must still be recorded so
|
|
248
|
+
* the user sees that an agent read the answer. The service holds the account's
|
|
249
|
+
* snapshot, so it — not this process — decides whether text was owed.
|
|
250
|
+
*/
|
|
242
251
|
export declare function acknowledgeCommand(deps: CommandDeps, requestId: string, flags: {
|
|
243
252
|
text?: string;
|
|
244
253
|
json?: boolean;
|
package/dist/commands.js
CHANGED
|
@@ -3,19 +3,19 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, u
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { parse as parseToml, stringify as stringifyToml } from 'smol-toml';
|
|
6
|
-
import { AGENT_ACKNOWLEDGEMENT_MAX_LENGTH, CAPABILITIES_V1, QUESTION_TEXT_MAX_LENGTH, REPLY_MAX_QUESTIONS, REPLY_MAX_WINDOW_SECONDS, validateDraft, } from '@raidiant/notifai-protocol';
|
|
6
|
+
import { AGENT_ACKNOWLEDGEMENT_MAX_LENGTH, CAPABILITIES_V1, PLATFORMS, QUESTION_TEXT_MAX_LENGTH, REPLY_MAX_QUESTIONS, REPLY_MAX_WINDOW_SECONDS, validateDraft, } from '@raidiant/notifai-protocol';
|
|
7
7
|
import { sha256Hex } from '@raidiant/notifai-protocol/node';
|
|
8
8
|
import { ApiCallError, NetworkError, createClient, isRetryableReplyPollError, } from './client.js';
|
|
9
|
-
import { BOOLEAN_CONFIG_KEYS, CONFIG_KEYS,
|
|
9
|
+
import { BOOLEAN_CONFIG_KEYS, CONFIG_KEYS, NUMERIC_CONFIG_KEYS, configBounds, configDefaultValue, findProjectConfigPath, personalProjectConfigPath, globalConfigPath, loadConfig, sessionConfigPath, stateDir, } from './config.js';
|
|
10
10
|
import { acceptedValues, configInfo } from './config-schema.js';
|
|
11
11
|
import { packageVersion, skillsSource } from './release.js';
|
|
12
12
|
import { atomicWriteFileSync } from './atomic-file.js';
|
|
13
13
|
import { withTargetFileLock } from './file-lock.js';
|
|
14
14
|
import { renderConfigExplain, renderConfigList, renderConfigPlain, } from './ui/config-view.js';
|
|
15
15
|
import { firstBlocker, } from './readiness.js';
|
|
16
|
-
import {
|
|
17
|
-
import { LOG_EVENTS, activeLogPath, archiveLogPaths, logConfigResolved, logSettingsFrom, logsDiskUsage, nullLogger, readLogRecords, renderRecord, } from './logging.js';
|
|
18
|
-
import { BLOCKING_STOP_TIMEOUT_SECONDS, CLAUDE_ASYNC_STOP_TIMEOUT_SECONDS, HARNESSES, applyPlan, buildCursorHookConfig, buildHookConfig, codexLayerDir, codexLayerPaths, codexRepresentationProblems, codexTrustProblems, codexProjectRoot, detectHarness, detectedHarnesses, findInstallations, handlerEvent, hookDefinitionFiles, inspectCodexLayer, loadCursorSettings, loadSettings, mergeCursorHooks, mergeHooks, removeCursorHooks, removeHooks, settingsFile, withCodexLayerTransaction, } from './install-hooks.js';
|
|
16
|
+
import { waiterCeilingSeconds, clearAcknowledgementObligation, handleSessionEnd, handleStop, handleUserPromptSubmit, parseHookInput, pruneAbandonedSessions, readLiveProjectSessionPointers, readMatchingProjectSessionPointer, readProjectSession, readProjectSessionPointer, readSessionState, registerQuestion, MIN_REPLY_WINDOW_SECONDS, } from './hooks.js';
|
|
17
|
+
import { LOG_EVENTS, activeLogPath, archiveLogPaths, logConfigResolved, logSettingsFrom, logsDiskUsage, nullLogger, readLogRecords, renderRecord, RECORD_LEVELS, } from './logging.js';
|
|
18
|
+
import { BLOCKING_STOP_TIMEOUT_SECONDS, stopHandlerIsDetached, CLAUDE_ASYNC_STOP_TIMEOUT_SECONDS, HARNESSES, applyPlan, buildCursorHookConfig, buildHookConfig, codexLayerDir, codexLayerPaths, codexCoexistenceNotes, codexHomeNote, codexRepresentationProblems, codexTrustProblems, codexProjectRoot, detectHarness, detectedHarnesses, findInstallations, handlerEvent, hookDefinitionFiles, inspectCodexLayer, loadCursorSettings, loadSettings, mergeCursorHooks, mergeHooks, removeCursorHooks, removeHooks, settingsFile, withCodexLayerTransaction, } from './install-hooks.js';
|
|
19
19
|
import { HARNESS_CAPABILITIES, HARNESS_LABELS } from './harnesses.js';
|
|
20
20
|
import { claudeWakeRoute, inspectClaudeInbox, systemClaudeWakeAdapters, } from './claude-wake.js';
|
|
21
21
|
import { codexWakeRoute, inspectCodexResume, } from './codex-wake.js';
|
|
@@ -88,7 +88,9 @@ function reportError(deps, err, context = {}) {
|
|
|
88
88
|
deps.io.err(`the server rejected: ${paths.join(', ')}`);
|
|
89
89
|
// A 422 on a request this CLI built is not a user error: the two sides
|
|
90
90
|
// disagree about the contract, in whichever direction. `doctor` says which.
|
|
91
|
-
|
|
91
|
+
// Codes that name a policy the account chose are the exception — the
|
|
92
|
+
// contract held, the body simply did not satisfy that account.
|
|
93
|
+
if (err.status === 422 && err.code !== 'acknowledgement_text_required') {
|
|
92
94
|
deps.io.err('this build sent a field the server did not accept — the CLI and server ' +
|
|
93
95
|
'disagree about the contract; check with `notifai doctor`');
|
|
94
96
|
}
|
|
@@ -314,6 +316,13 @@ export function canDeviceReceive(device) {
|
|
|
314
316
|
return deviceCanReceive(device);
|
|
315
317
|
}
|
|
316
318
|
export async function capabilitiesCommand(deps, flags) {
|
|
319
|
+
// Locally, and with the same message `send` gives. Spending a round trip to
|
|
320
|
+
// have the server answer "Request validation failed" told the caller neither
|
|
321
|
+
// which flag was wrong nor what it accepts.
|
|
322
|
+
if (flags.platform !== undefined && !PLATFORMS.includes(flags.platform)) {
|
|
323
|
+
deps.io.err(`Unknown platform "${flags.platform}" — use ${PLATFORMS.join(' or ')}.`);
|
|
324
|
+
return EXIT.usage;
|
|
325
|
+
}
|
|
317
326
|
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env });
|
|
318
327
|
const credential = deps.store.load();
|
|
319
328
|
const baseUrl = resolvedBaseUrl(config, credential);
|
|
@@ -371,6 +380,15 @@ export async function sendCommand(deps, flags) {
|
|
|
371
380
|
deps.io.err(`--kind ${flags.kind} cannot be combined with --reply; a reply request is a question.`);
|
|
372
381
|
return EXIT.usage;
|
|
373
382
|
}
|
|
383
|
+
// Kind now decides the sound a notification arrives with, so it is asked for
|
|
384
|
+
// at the boundary rather than defaulted silently: an unlabelled send would
|
|
385
|
+
// reach the user as ordinary news whatever actually happened.
|
|
386
|
+
if (!flags.reply && flags.kind === undefined) {
|
|
387
|
+
deps.io.err('--kind is required: say what this notification is. ' +
|
|
388
|
+
'update (news) · done (finished) · failed (terminal failure) · blocked (cannot proceed). ' +
|
|
389
|
+
'--reply makes it a question without --kind.');
|
|
390
|
+
return EXIT.usage;
|
|
391
|
+
}
|
|
374
392
|
if (!flags.reply && flags.kind === 'question') {
|
|
375
393
|
deps.io.err('--kind question requires --reply so the question can be answered.');
|
|
376
394
|
return EXIT.usage;
|
|
@@ -404,9 +422,9 @@ export async function sendCommand(deps, flags) {
|
|
|
404
422
|
if (flags.reply &&
|
|
405
423
|
flags.replyWindow !== undefined &&
|
|
406
424
|
(!Number.isInteger(flags.replyWindow) ||
|
|
407
|
-
flags.replyWindow <
|
|
425
|
+
flags.replyWindow < MIN_REPLY_WINDOW_SECONDS ||
|
|
408
426
|
flags.replyWindow > REPLY_MAX_WINDOW_SECONDS)) {
|
|
409
|
-
deps.io.err(`--reply-window must be an integer from
|
|
427
|
+
deps.io.err(`--reply-window must be an integer from ${MIN_REPLY_WINDOW_SECONDS} to ${REPLY_MAX_WINDOW_SECONDS} seconds.`);
|
|
410
428
|
return EXIT.usage;
|
|
411
429
|
}
|
|
412
430
|
const mediaInputError = validateMediaInputs(flags.image, flags.imageAlt);
|
|
@@ -435,7 +453,8 @@ export async function sendCommand(deps, flags) {
|
|
|
435
453
|
}
|
|
436
454
|
const uploaded = await uploadImage(deps, authed.client, image);
|
|
437
455
|
if (!uploaded.ok) {
|
|
438
|
-
|
|
456
|
+
if (uploaded.error !== null)
|
|
457
|
+
deps.io.err(uploaded.error);
|
|
439
458
|
return uploaded.exit;
|
|
440
459
|
}
|
|
441
460
|
mediaIds.push(uploaded.mediaId);
|
|
@@ -513,6 +532,7 @@ export async function sendCommand(deps, flags) {
|
|
|
513
532
|
request_id: receipt.request_id,
|
|
514
533
|
replies: [],
|
|
515
534
|
agent_acknowledgement_required: receipt.agent_acknowledgement_required,
|
|
535
|
+
agent_acknowledgement_text_required: receipt.agent_acknowledgement_text_required,
|
|
516
536
|
agent_acknowledgement: null,
|
|
517
537
|
acknowledgement_command: null,
|
|
518
538
|
degraded: false,
|
|
@@ -629,7 +649,15 @@ export async function repliesCommand(deps, requestedId, flags) {
|
|
|
629
649
|
.map((entry) => entry.request_id)
|
|
630
650
|
.filter((id) => id !== undefined);
|
|
631
651
|
if (requestIds.length === 0) {
|
|
632
|
-
|
|
652
|
+
// `--json` is a promise about stdout, and an empty result is still a
|
|
653
|
+
// result: a caller that asked for machine-readable output must not have
|
|
654
|
+
// to parse an English sentence to learn there was nothing pending.
|
|
655
|
+
if (flags.json === true) {
|
|
656
|
+
deps.io.out(JSON.stringify({ session_id: sessionId, pending: [], replies: [] }, null, 2));
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
deps.io.err(`Session ${sessionId} has no pushed question pending.`);
|
|
660
|
+
}
|
|
633
661
|
return EXIT.noReply;
|
|
634
662
|
}
|
|
635
663
|
}
|
|
@@ -748,6 +776,7 @@ export async function waitForReply(client, requestId, options) {
|
|
|
748
776
|
request_id: requestId,
|
|
749
777
|
reply_expires_at: null,
|
|
750
778
|
agent_acknowledgement_required: false,
|
|
779
|
+
agent_acknowledgement_text_required: false,
|
|
751
780
|
agent_acknowledgement: null,
|
|
752
781
|
replies: [],
|
|
753
782
|
},
|
|
@@ -786,9 +815,14 @@ function degradedWaitWarning(requestId) {
|
|
|
786
815
|
function isNonNegativeInteger(value) {
|
|
787
816
|
return Number.isInteger(value) && value >= 0;
|
|
788
817
|
}
|
|
789
|
-
function
|
|
790
|
-
return
|
|
818
|
+
function acknowledgeInvocation(requestId, textRequired) {
|
|
819
|
+
return textRequired
|
|
791
820
|
? `notifai acknowledge ${requestId} --text <text>`
|
|
821
|
+
: `notifai acknowledge ${requestId}`;
|
|
822
|
+
}
|
|
823
|
+
function acknowledgementCommand(requestId, required, textRequired, acknowledgement, hasReply = true) {
|
|
824
|
+
return required && acknowledgement === null && hasReply
|
|
825
|
+
? acknowledgeInvocation(requestId, textRequired)
|
|
792
826
|
: null;
|
|
793
827
|
}
|
|
794
828
|
function replyResultJson(response, degraded) {
|
|
@@ -799,7 +833,8 @@ function replyResultJson(response, degraded) {
|
|
|
799
833
|
replies: response.replies,
|
|
800
834
|
agent_acknowledgement_required: response.agent_acknowledgement_required,
|
|
801
835
|
agent_acknowledgement: response.agent_acknowledgement,
|
|
802
|
-
|
|
836
|
+
agent_acknowledgement_text_required: response.agent_acknowledgement_text_required,
|
|
837
|
+
acknowledgement_command: acknowledgementCommand(response.request_id, response.agent_acknowledgement_required, response.agent_acknowledgement_text_required, response.agent_acknowledgement, response.replies.length > 0),
|
|
803
838
|
degraded,
|
|
804
839
|
};
|
|
805
840
|
}
|
|
@@ -808,13 +843,19 @@ function printAcknowledgementStatus(deps, response) {
|
|
|
808
843
|
deps.io.out('Agent Acknowledgement: not required for this request.');
|
|
809
844
|
return;
|
|
810
845
|
}
|
|
846
|
+
const textRequired = response.agent_acknowledgement_text_required;
|
|
811
847
|
if (response.agent_acknowledgement !== null) {
|
|
812
|
-
|
|
848
|
+
const recorded = `Agent Acknowledgement: recorded at ${response.agent_acknowledgement.created_at}`;
|
|
849
|
+
deps.io.out(response.agent_acknowledgement.text.length > 0
|
|
850
|
+
? `${recorded}: ${response.agent_acknowledgement.text}`
|
|
851
|
+
: `${recorded}.`);
|
|
813
852
|
return;
|
|
814
853
|
}
|
|
815
854
|
if (response.replies.length > 0) {
|
|
816
855
|
deps.io.out('Agent Acknowledgement required.');
|
|
817
|
-
deps.io.out(
|
|
856
|
+
deps.io.out(textRequired
|
|
857
|
+
? `next: Run \`${acknowledgeInvocation(response.request_id, true)}\` with concrete text saying what you will do because of the reply.`
|
|
858
|
+
: `next: Run \`${acknowledgeInvocation(response.request_id, false)}\` so the user sees you read the reply; this account turned acknowledgement text off.`);
|
|
818
859
|
return;
|
|
819
860
|
}
|
|
820
861
|
deps.io.out('Agent Acknowledgement: required after a user reply; no reply is recorded yet.');
|
|
@@ -969,7 +1010,13 @@ async function uploadImage(deps, client, source) {
|
|
|
969
1010
|
return { ok: true, mediaId: grant.media_id };
|
|
970
1011
|
}
|
|
971
1012
|
catch (err) {
|
|
972
|
-
|
|
1013
|
+
// Every other API failure in this file goes through `reportError`, which
|
|
1014
|
+
// maps a revoked credential to the auth code, a server fault to the network
|
|
1015
|
+
// one and everything else to plain failure, and records it in the local
|
|
1016
|
+
// log with whatever next step the server named. This used to answer
|
|
1017
|
+
// `network` to all of them, so an image too large for the account exited
|
|
1018
|
+
// the same way an unreachable server did, and nothing about it was logged.
|
|
1019
|
+
return { ok: false, error: null, exit: reportError(deps, err) };
|
|
973
1020
|
}
|
|
974
1021
|
}
|
|
975
1022
|
// ---------------------------------------------------------------------------
|
|
@@ -999,7 +1046,11 @@ export async function hookRunCommand(deps, event, readStdin, harness) {
|
|
|
999
1046
|
// would grant slow setup a second budget and let the harness kill us before
|
|
1000
1047
|
// an accepted answer is journaled or written to stdout.
|
|
1001
1048
|
const now = deps.now ?? Date.now;
|
|
1002
|
-
|
|
1049
|
+
// The waiter may spend a long wall clock exactly when no turn is held open
|
|
1050
|
+
// for it, which is the same condition the installer used to declare
|
|
1051
|
+
// `async: true`. One predicate decides it for both.
|
|
1052
|
+
const detachedWaiter = stopHandlerIsDetached(harness, deps.hookPlatform ?? process.platform);
|
|
1053
|
+
const processDeadlineAt = now() + waiterCeilingSeconds(detachedWaiter) * 1000;
|
|
1003
1054
|
const logger = log(deps);
|
|
1004
1055
|
logger.bind({ cmd: `hook ${event}` });
|
|
1005
1056
|
let started = false;
|
|
@@ -1179,18 +1230,8 @@ export async function hookRunCommand(deps, event, readStdin, harness) {
|
|
|
1179
1230
|
});
|
|
1180
1231
|
for (const note of outcome.notes)
|
|
1181
1232
|
deps.io.err(`notifai: ${note}`);
|
|
1182
|
-
if (outcome.stdout !== undefined)
|
|
1183
|
-
|
|
1184
|
-
if (harness !== undefined &&
|
|
1185
|
-
event === 'stop' &&
|
|
1186
|
-
HARNESS_CAPABILITIES[harness].stopContinuation === 'followup-message') {
|
|
1187
|
-
const decision = JSON.parse(outcome.stdout);
|
|
1188
|
-
if (decision.decision === 'block' && typeof decision.reason === 'string') {
|
|
1189
|
-
stdout = JSON.stringify({ followup_message: decision.reason });
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
deps.io.out(stdout);
|
|
1193
|
-
}
|
|
1233
|
+
if (outcome.stdout !== undefined)
|
|
1234
|
+
deps.io.out(outcome.stdout);
|
|
1194
1235
|
return EXIT.ok;
|
|
1195
1236
|
}
|
|
1196
1237
|
catch (err) {
|
|
@@ -1322,7 +1363,7 @@ export function buildQuestions(flags, question) {
|
|
|
1322
1363
|
};
|
|
1323
1364
|
}
|
|
1324
1365
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
1325
|
-
return { ok: false, error:
|
|
1366
|
+
return { ok: false, error: `--form needs a "questions" array (1-${REPLY_MAX_QUESTIONS} entries).` };
|
|
1326
1367
|
}
|
|
1327
1368
|
const record = parsed;
|
|
1328
1369
|
const unknownKeys = Object.keys(record).filter((key) => key !== 'questions' && key !== 'body');
|
|
@@ -1333,7 +1374,7 @@ export function buildQuestions(flags, question) {
|
|
|
1333
1374
|
};
|
|
1334
1375
|
}
|
|
1335
1376
|
if (!Array.isArray(record['questions'])) {
|
|
1336
|
-
return { ok: false, error:
|
|
1377
|
+
return { ok: false, error: `--form needs a "questions" array (1-${REPLY_MAX_QUESTIONS} entries).` };
|
|
1337
1378
|
}
|
|
1338
1379
|
const formQuestions = record['questions'];
|
|
1339
1380
|
if (formQuestions.length < 1 || formQuestions.length > REPLY_MAX_QUESTIONS) {
|
|
@@ -1438,7 +1479,7 @@ function buildAskDraft(config, built, flags, invocation, mediaIds) {
|
|
|
1438
1479
|
}
|
|
1439
1480
|
return { ok: true, draft: result.draft };
|
|
1440
1481
|
}
|
|
1441
|
-
function recordRegisteredQuestion(deps, sessionId, built, draft) {
|
|
1482
|
+
function recordRegisteredQuestion(deps, sessionId, built, draft, json = false) {
|
|
1442
1483
|
try {
|
|
1443
1484
|
registerQuestion(sessionId, deps.env, {
|
|
1444
1485
|
question: built.questions[0].text,
|
|
@@ -1462,6 +1503,28 @@ function recordRegisteredQuestion(deps, sessionId, built, draft) {
|
|
|
1462
1503
|
choices: built.questions[0].choices?.length ?? 0,
|
|
1463
1504
|
media: draft.presentation.media?.length ?? 0,
|
|
1464
1505
|
});
|
|
1506
|
+
// The block below is the densest guidance this CLI prints, and until now it
|
|
1507
|
+
// was prose only: an agent could not read back the choice ids it must branch
|
|
1508
|
+
// on without asking the server for them. The JSON form carries the same
|
|
1509
|
+
// obligation as data.
|
|
1510
|
+
if (json) {
|
|
1511
|
+
deps.io.out(JSON.stringify({
|
|
1512
|
+
registered: true,
|
|
1513
|
+
questions: built.questions.map((entry) => ({
|
|
1514
|
+
id: entry.id,
|
|
1515
|
+
text: entry.text,
|
|
1516
|
+
...(entry.choices === undefined ? {} : { choices: entry.choices }),
|
|
1517
|
+
...(entry.multi === true ? { multi: true } : {}),
|
|
1518
|
+
})),
|
|
1519
|
+
next: {
|
|
1520
|
+
end_turn: true,
|
|
1521
|
+
in_this_turn: 'Ask the question in the conversation and say what concrete work each possible answer will make you resume, then end the turn.',
|
|
1522
|
+
route_neutral: 'Never say where the answer must arrive; it returns by whatever route the harness supports.',
|
|
1523
|
+
on_answer: 'Acknowledge it, then resume the committed work without asking the user to confirm again.',
|
|
1524
|
+
},
|
|
1525
|
+
}, null, 2));
|
|
1526
|
+
return EXIT.ok;
|
|
1527
|
+
}
|
|
1465
1528
|
for (const [index, entry] of built.questions.entries()) {
|
|
1466
1529
|
const prefix = built.questions.length > 1 ? `${index + 1}. ` : '';
|
|
1467
1530
|
if (entry.choices !== undefined) {
|
|
@@ -1504,7 +1567,8 @@ async function uploadAskMedia(deps, config, sessionId, built, flags, invocation)
|
|
|
1504
1567
|
}
|
|
1505
1568
|
const uploaded = await uploadImage(deps, authed.client, image);
|
|
1506
1569
|
if (!uploaded.ok) {
|
|
1507
|
-
|
|
1570
|
+
if (uploaded.error !== null)
|
|
1571
|
+
deps.io.err(uploaded.error);
|
|
1508
1572
|
return uploaded.exit;
|
|
1509
1573
|
}
|
|
1510
1574
|
mediaIds.push(uploaded.mediaId);
|
|
@@ -1514,7 +1578,7 @@ async function uploadAskMedia(deps, config, sessionId, built, flags, invocation)
|
|
|
1514
1578
|
deps.io.err(ready.error);
|
|
1515
1579
|
return EXIT.usage;
|
|
1516
1580
|
}
|
|
1517
|
-
return recordRegisteredQuestion(deps, sessionId, built, ready.draft);
|
|
1581
|
+
return recordRegisteredQuestion(deps, sessionId, built, ready.draft, flags.json === true);
|
|
1518
1582
|
}
|
|
1519
1583
|
/**
|
|
1520
1584
|
* Registers a question for turn-end routing. Returns immediately so the agent
|
|
@@ -1615,7 +1679,7 @@ export function askCommand(deps, question, flags) {
|
|
|
1615
1679
|
if ((flags.image?.length ?? 0) > 0) {
|
|
1616
1680
|
return uploadAskMedia(deps, routingConfig, sessionId, built, flags, source.invocation);
|
|
1617
1681
|
}
|
|
1618
|
-
return recordRegisteredQuestion(deps, sessionId, built, preflight.draft);
|
|
1682
|
+
return recordRegisteredQuestion(deps, sessionId, built, preflight.draft, flags.json === true);
|
|
1619
1683
|
}
|
|
1620
1684
|
/**
|
|
1621
1685
|
* Every harness marker present in this environment, in declared order.
|
|
@@ -1814,15 +1878,22 @@ function hookActivationAdvice(installations) {
|
|
|
1814
1878
|
}
|
|
1815
1879
|
return `${advice.join('. ')}.`;
|
|
1816
1880
|
}
|
|
1817
|
-
/**
|
|
1881
|
+
/**
|
|
1882
|
+
* Record the one Agent Acknowledgement associated with a replied-to request.
|
|
1883
|
+
*
|
|
1884
|
+
* `--text` is optional here because the acknowledgement is not: an account may
|
|
1885
|
+
* turn the agent's written reply off, and the receipt must still be recorded so
|
|
1886
|
+
* the user sees that an agent read the answer. The service holds the account's
|
|
1887
|
+
* snapshot, so it — not this process — decides whether text was owed.
|
|
1888
|
+
*/
|
|
1818
1889
|
export async function acknowledgeCommand(deps, requestId, flags) {
|
|
1819
1890
|
const text = flags.text?.trim() ?? '';
|
|
1820
|
-
if (text.length === 0) {
|
|
1821
|
-
deps.io.err('--text
|
|
1891
|
+
if (flags.text !== undefined && text.length === 0) {
|
|
1892
|
+
deps.io.err('--text must contain non-whitespace text. Drop it to acknowledge without text.');
|
|
1822
1893
|
return EXIT.usage;
|
|
1823
1894
|
}
|
|
1824
1895
|
if (text.length > AGENT_ACKNOWLEDGEMENT_MAX_LENGTH) {
|
|
1825
|
-
deps.io.err(`--text must be at most ${AGENT_ACKNOWLEDGEMENT_MAX_LENGTH} characters after trimming.`);
|
|
1896
|
+
deps.io.err(`--text must be at most ${AGENT_ACKNOWLEDGEMENT_MAX_LENGTH} characters after trimming. Shorten it: an acknowledgement is a receipt, not a report.`);
|
|
1826
1897
|
return EXIT.usage;
|
|
1827
1898
|
}
|
|
1828
1899
|
const logger = log(deps);
|
|
@@ -1836,7 +1907,7 @@ export async function acknowledgeCommand(deps, requestId, flags) {
|
|
|
1836
1907
|
if (!authed)
|
|
1837
1908
|
return EXIT.auth;
|
|
1838
1909
|
try {
|
|
1839
|
-
const result = await authed.client.putAgentAcknowledgement(requestId, { text });
|
|
1910
|
+
const result = await authed.client.putAgentAcknowledgement(requestId, text.length > 0 ? { text } : {});
|
|
1840
1911
|
const output = {
|
|
1841
1912
|
request_id: requestId,
|
|
1842
1913
|
outcome: result.status,
|
|
@@ -1876,7 +1947,7 @@ export async function closeCommand(deps, requestId, flags = {}) {
|
|
|
1876
1947
|
if (flags.json) {
|
|
1877
1948
|
deps.io.out(JSON.stringify({
|
|
1878
1949
|
...response,
|
|
1879
|
-
acknowledgement_command: acknowledgementCommand(response.request_id, response.agent_acknowledgement_required, response.agent_acknowledgement, response.replies.length > 0),
|
|
1950
|
+
acknowledgement_command: acknowledgementCommand(response.request_id, response.agent_acknowledgement_required, response.agent_acknowledgement_text_required, response.agent_acknowledgement, response.replies.length > 0),
|
|
1880
1951
|
}, null, 2));
|
|
1881
1952
|
}
|
|
1882
1953
|
else {
|
|
@@ -1934,7 +2005,7 @@ export function hooksInstallCommand(deps, flags) {
|
|
|
1934
2005
|
if (flags.harness === undefined) {
|
|
1935
2006
|
const detected = detectedHarnesses(deps.cwd, deps.env);
|
|
1936
2007
|
if (detected.length === 0) {
|
|
1937
|
-
deps.io.err(`Could not tell which harness
|
|
2008
|
+
deps.io.err(`Could not tell which harness you mean — pass --harness <${HARNESSES.join('|')}>.`);
|
|
1938
2009
|
return EXIT.usage;
|
|
1939
2010
|
}
|
|
1940
2011
|
let ok = true;
|
|
@@ -2043,6 +2114,14 @@ export function hooksInstallCommand(deps, flags) {
|
|
|
2043
2114
|
for (const problem of codexRepresentationProblems(deps.cwd, deps.env, hookPlatform)) {
|
|
2044
2115
|
deps.io.out(problem);
|
|
2045
2116
|
}
|
|
2117
|
+
for (const note of codexCoexistenceNotes(deps.cwd, deps.env, hookPlatform)) {
|
|
2118
|
+
deps.io.out(note);
|
|
2119
|
+
}
|
|
2120
|
+
if (flags.global) {
|
|
2121
|
+
const home = codexHomeNote(deps.env, hookPlatform);
|
|
2122
|
+
if (home !== null)
|
|
2123
|
+
deps.io.out(home);
|
|
2124
|
+
}
|
|
2046
2125
|
}
|
|
2047
2126
|
return EXIT.ok;
|
|
2048
2127
|
}
|
|
@@ -2195,12 +2274,6 @@ function assertOwnedRegularFile(file) {
|
|
|
2195
2274
|
throw new Error(`${file} is owned by uid ${stat.uid}, not the current user.`);
|
|
2196
2275
|
}
|
|
2197
2276
|
}
|
|
2198
|
-
const HARNESS_LABEL = {
|
|
2199
|
-
'claude-code': 'Claude Code',
|
|
2200
|
-
codex: 'Codex',
|
|
2201
|
-
cursor: 'Cursor',
|
|
2202
|
-
opencode: 'OpenCode',
|
|
2203
|
-
};
|
|
2204
2277
|
function resolveHarness(deps, requested) {
|
|
2205
2278
|
if (requested !== undefined) {
|
|
2206
2279
|
if (HARNESSES.includes(requested))
|
|
@@ -2231,7 +2304,7 @@ async function pickHarnessesToInstall(deps, requested) {
|
|
|
2231
2304
|
if (deps.io.interactive === true && deps.io.multiselect) {
|
|
2232
2305
|
const picked = await deps.io.multiselect('Which agent harnesses should Notifai wire here?', HARNESSES.map((name) => ({
|
|
2233
2306
|
value: name,
|
|
2234
|
-
label:
|
|
2307
|
+
label: HARNESS_LABELS[name],
|
|
2235
2308
|
...(detected.includes(name) ? { hint: 'detected on this machine' } : {}),
|
|
2236
2309
|
})), detected);
|
|
2237
2310
|
if (picked === null)
|
|
@@ -2617,8 +2690,8 @@ export function logsCommand(deps, flags) {
|
|
|
2617
2690
|
query.since = since;
|
|
2618
2691
|
}
|
|
2619
2692
|
if (flags.level !== undefined) {
|
|
2620
|
-
if (!
|
|
2621
|
-
deps.io.err(`--level takes one of: ${
|
|
2693
|
+
if (!RECORD_LEVELS.includes(flags.level)) {
|
|
2694
|
+
deps.io.err(`--level takes one of: ${RECORD_LEVELS.join(', ')} — not "${flags.level}".`);
|
|
2622
2695
|
return EXIT.usage;
|
|
2623
2696
|
}
|
|
2624
2697
|
query.level = flags.level;
|
|
@@ -3349,7 +3422,9 @@ async function printInitClose(deps, readiness, flags) {
|
|
|
3349
3422
|
];
|
|
3350
3423
|
await deps.io.note?.(lines.join('\n'), 'Ready');
|
|
3351
3424
|
printOptionalLeftovers(deps, leftovers);
|
|
3352
|
-
deps.io.out(
|
|
3425
|
+
deps.io.out(questions
|
|
3426
|
+
? 'All set. Agents in this project can notify you and ask you questions.'
|
|
3427
|
+
: 'All set. Agents in this project can notify you. Questions stay in the terminal until hooks are installed.');
|
|
3353
3428
|
await deps.io.outro?.('All set ✨');
|
|
3354
3429
|
return;
|
|
3355
3430
|
}
|
|
@@ -3364,7 +3439,9 @@ async function printInitClose(deps, readiness, flags) {
|
|
|
3364
3439
|
}
|
|
3365
3440
|
if (blocker === null) {
|
|
3366
3441
|
printOptionalLeftovers(deps, leftovers);
|
|
3367
|
-
deps.io.out(
|
|
3442
|
+
deps.io.out(questions
|
|
3443
|
+
? 'All set. Agents in this project can notify you and ask you questions.'
|
|
3444
|
+
: 'All set. Agents in this project can notify you. Questions stay in the terminal until hooks are installed.');
|
|
3368
3445
|
return;
|
|
3369
3446
|
}
|
|
3370
3447
|
deps.io.out(`Next: ${blocker.title} — ${blocker.detail}`);
|
|
@@ -3893,14 +3970,39 @@ function hookStates(deps) {
|
|
|
3893
3970
|
settings,
|
|
3894
3971
|
];
|
|
3895
3972
|
}
|
|
3973
|
+
/**
|
|
3974
|
+
* A human title per check.
|
|
3975
|
+
*
|
|
3976
|
+
* Three checks used to collapse onto "Question routing", so a reader could not
|
|
3977
|
+
* tell which of them had failed, and the rest fell through to their internal
|
|
3978
|
+
* name — `hooks (stale)` beside `Delivery proof`. The `id` stays the stable
|
|
3979
|
+
* thing to branch on; this is only what a person reads.
|
|
3980
|
+
*/
|
|
3981
|
+
const CHECK_TITLES = {
|
|
3982
|
+
hooks: 'Question routing',
|
|
3983
|
+
'hooks (detected)': 'Harnesses detected',
|
|
3984
|
+
'hooks (active harness)': 'Routing for this harness',
|
|
3985
|
+
'hooks (active session)': 'Routing for this session',
|
|
3986
|
+
'hooks (stale)': 'Hook definitions current',
|
|
3987
|
+
'hooks (adapter)': 'Hook adapter',
|
|
3988
|
+
'hooks (trust)': 'Codex hook trust',
|
|
3989
|
+
'hooks (stop shape)': 'Turn-end hook shape',
|
|
3990
|
+
'hooks (duplicates)': 'Duplicate hook installs',
|
|
3991
|
+
'hooks (codex representation)': 'Codex hook representation',
|
|
3992
|
+
'hooks (question admission)': 'Question admission',
|
|
3993
|
+
'hooks (fired)': 'Hooks have run here',
|
|
3994
|
+
'hooks (answer continuation)': 'How an answer returns',
|
|
3995
|
+
'hooks (wake route)': 'Direct wake route',
|
|
3996
|
+
};
|
|
3997
|
+
function checkTitle(name) {
|
|
3998
|
+
return CHECK_TITLES[name] ?? name;
|
|
3999
|
+
}
|
|
3896
4000
|
/** Real but not in the way; see the note above. */
|
|
3897
4001
|
const informational = new Set();
|
|
3898
4002
|
return [
|
|
3899
4003
|
...hookChecks(deps).map((check) => ({
|
|
3900
4004
|
id: check.name.replace(/[ ()]+/g, '-').replace(/-$/, ''),
|
|
3901
|
-
title: check.name
|
|
3902
|
-
? 'Question routing'
|
|
3903
|
-
: check.name,
|
|
4005
|
+
title: checkTitle(check.name),
|
|
3904
4006
|
status: check.ok
|
|
3905
4007
|
? 'ready'
|
|
3906
4008
|
: check.informational === true || informational.has(check.name)
|
|
@@ -3945,7 +4047,7 @@ function stopShapeProblems(installation, platform = process.platform) {
|
|
|
3945
4047
|
return [];
|
|
3946
4048
|
const problems = [];
|
|
3947
4049
|
for (const handler of installation.handlers.filter((entry) => handlerEvent(entry.command) === 'stop')) {
|
|
3948
|
-
if (installation.harness
|
|
4050
|
+
if (stopHandlerIsDetached(installation.harness, platform)) {
|
|
3949
4051
|
if (handler.async !== true) {
|
|
3950
4052
|
problems.push(`${installation.file} declares a blocking Stop handler; the Claude Code wake route needs \`async: true\` so the turn ends while the waiter runs`);
|
|
3951
4053
|
}
|
|
@@ -3987,7 +4089,7 @@ function hookChecks(deps) {
|
|
|
3987
4089
|
name: 'hooks (detected)',
|
|
3988
4090
|
ok: false,
|
|
3989
4091
|
informational: true,
|
|
3990
|
-
detail: `${unwired.map((harness) =>
|
|
4092
|
+
detail: `${unwired.map((harness) => HARNESS_LABELS[harness]).join(', ')} detected on this machine but not wired`,
|
|
3991
4093
|
remedy: {
|
|
3992
4094
|
summary: 'install hooks for every detected harness',
|
|
3993
4095
|
command: 'notifai hooks install',
|
|
@@ -4123,6 +4225,20 @@ function hookChecks(deps) {
|
|
|
4123
4225
|
detail: representationProblems.join('; '),
|
|
4124
4226
|
});
|
|
4125
4227
|
}
|
|
4228
|
+
else {
|
|
4229
|
+
const coexistence = codexCoexistenceNotes(deps.cwd, deps.env, deps.hookPlatform);
|
|
4230
|
+
if (coexistence.length > 0) {
|
|
4231
|
+
checks.push({
|
|
4232
|
+
name: 'hooks (codex representation)',
|
|
4233
|
+
ok: true,
|
|
4234
|
+
detail: coexistence.join('; '),
|
|
4235
|
+
});
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
const codexHome = codexHomeNote(deps.env, deps.hookPlatform);
|
|
4239
|
+
if (codexHome !== null) {
|
|
4240
|
+
checks.push({ name: 'hooks (codex home)', ok: true, detail: codexHome });
|
|
4241
|
+
}
|
|
4126
4242
|
if (active !== null && activeInstallations.length > 0) {
|
|
4127
4243
|
const admissionProblems = activeQuestionRouteProblems(deps, active, installations);
|
|
4128
4244
|
checks.push({
|