@raidiant/notifai 8.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 +48 -0
- package/README.md +20 -13
- package/dist/atomic-file.d.ts +14 -0
- package/dist/atomic-file.js +27 -2
- package/dist/atomic-file.js.map +1 -1
- package/dist/cli-bin.d.ts +4 -0
- package/dist/cli-bin.js +62 -0
- package/dist/cli-bin.js.map +1 -0
- package/dist/cli-release.d.ts +10 -0
- package/dist/cli-release.js +75 -0
- package/dist/cli-release.js.map +1 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +13 -3
- package/dist/client.js.map +1 -1
- 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-auth.d.ts +6 -0
- package/dist/commands-auth.js +28 -3
- package/dist/commands-auth.js.map +1 -1
- package/dist/commands-config.js +22 -3
- package/dist/commands-config.js.map +1 -1
- package/dist/commands-core.d.ts +12 -0
- package/dist/commands-core.js +27 -3
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-doctor.d.ts +1 -0
- package/dist/commands-doctor.js +87 -29
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-guidance.d.ts +7 -3
- package/dist/commands-guidance.js +40 -31
- 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 +20 -5
- package/dist/commands-hooks.js +509 -155
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +12 -18
- package/dist/commands-init.js +149 -47
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.d.ts +17 -2
- package/dist/commands-send-support.js +61 -5
- package/dist/commands-send-support.js.map +1 -1
- package/dist/commands-send.d.ts +1 -0
- package/dist/commands-send.js +114 -17
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-setup-proof.d.ts +12 -3
- package/dist/commands-setup-proof.js +60 -16
- package/dist/commands-setup-proof.js.map +1 -1
- package/dist/commands-skill.d.ts +11 -1
- package/dist/commands-skill.js +61 -11
- package/dist/commands-skill.js.map +1 -1
- package/dist/commands.d.ts +2 -2
- package/dist/commands.js +1 -1
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +25 -0
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +24 -3
- package/dist/config.js +37 -5
- package/dist/config.js.map +1 -1
- package/dist/credentials.d.ts +10 -1
- package/dist/credentials.js +25 -12
- package/dist/credentials.js.map +1 -1
- package/dist/guidance-content.d.ts +20 -0
- package/dist/guidance-content.js +73 -8
- package/dist/guidance-content.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/guidance.d.ts +21 -0
- package/dist/guidance.js +73 -18
- package/dist/guidance.js.map +1 -1
- package/dist/hook-adapter.js +37 -3
- package/dist/hook-adapter.js.map +1 -1
- package/dist/hooks.d.ts +83 -7
- package/dist/hooks.js +288 -90
- package/dist/hooks.js.map +1 -1
- package/dist/install-hooks.d.ts +10 -18
- package/dist/install-hooks.js +110 -23
- package/dist/install-hooks.js.map +1 -1
- package/dist/interactive.d.ts +1 -1
- package/dist/interactive.js +1 -1
- package/dist/interactive.js.map +1 -1
- package/dist/logging.d.ts +8 -1
- package/dist/logging.js +43 -2
- package/dist/logging.js.map +1 -1
- package/dist/main.js +2 -13
- package/dist/main.js.map +1 -1
- package/dist/native-skills.d.ts +19 -0
- package/dist/native-skills.js +30 -8
- package/dist/native-skills.js.map +1 -1
- package/dist/opencode-plugin.js +25 -1
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/program.js +34 -20
- 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/send.d.ts +8 -1
- package/dist/send.js +17 -1
- package/dist/send.js.map +1 -1
- package/dist/session-activation.d.ts +11 -0
- package/dist/session-activation.js +46 -0
- package/dist/session-activation.js.map +1 -0
- package/dist/url-policy.d.ts +85 -0
- package/dist/url-policy.js +369 -0
- package/dist/url-policy.js.map +1 -0
- package/package.json +3 -3
- package/src/atomic-file.ts +36 -2
- package/src/cli-bin.test.ts +38 -0
- package/src/cli-bin.ts +67 -0
- package/src/cli-release.test.ts +46 -0
- package/src/cli-release.ts +78 -0
- package/src/client.test.ts +95 -0
- package/src/client.ts +16 -3
- package/src/command-session.ts +38 -0
- package/src/commands-auth.ts +32 -3
- package/src/commands-config.ts +25 -2
- package/src/commands-core.ts +38 -3
- package/src/commands-doctor.ts +117 -41
- package/src/commands-guidance.ts +43 -32
- package/src/commands-harness-context.ts +24 -1
- package/src/commands-hooks.ts +704 -188
- package/src/commands-init.ts +180 -57
- package/src/commands-send-support.test.ts +156 -0
- package/src/commands-send-support.ts +67 -5
- package/src/commands-send.ts +135 -16
- package/src/commands-setup-proof.test.ts +72 -0
- package/src/commands-setup-proof.ts +69 -16
- package/src/commands-skill.ts +78 -15
- package/src/commands.test.ts +1281 -157
- package/src/commands.ts +2 -0
- package/src/config-schema.ts +28 -1
- package/src/config.test.ts +1 -1
- package/src/config.ts +49 -5
- package/src/credentials.test.ts +71 -3
- package/src/credentials.ts +29 -15
- package/src/guidance-content.ts +74 -8
- package/src/guidance-render.ts +56 -0
- package/src/guidance.test.ts +70 -11
- package/src/guidance.ts +100 -18
- package/src/hook-adapter.test.ts +113 -1
- package/src/hook-adapter.ts +37 -3
- package/src/hooks.test.ts +778 -166
- package/src/hooks.ts +368 -129
- package/src/install-hooks.test.ts +135 -16
- package/src/install-hooks.ts +113 -25
- package/src/interactive.ts +1 -1
- package/src/logging.test.ts +66 -3
- package/src/logging.ts +45 -2
- package/src/main.ts +2 -14
- package/src/native-skills.test.ts +41 -1
- package/src/native-skills.ts +40 -6
- package/src/opencode-plugin.ts +25 -1
- package/src/program.test.ts +37 -2
- package/src/program.ts +62 -24
- package/src/readiness.ts +44 -0
- package/src/send-attempts.test.ts +143 -0
- package/src/send-attempts.ts +188 -0
- package/src/send.test.ts +28 -0
- package/src/send.ts +28 -2
- package/src/session-activation.ts +62 -0
- package/src/skill.test.ts +125 -11
- package/src/trust-policy.test.ts +701 -0
- package/src/url-policy.ts +399 -0
package/src/commands-doctor.ts
CHANGED
|
@@ -9,7 +9,6 @@ import { HARNESS_CAPABILITIES, HARNESS_LABELS } from './harnesses.js'
|
|
|
9
9
|
import { inspectHookAdapter } from './hook-adapter.js'
|
|
10
10
|
import {
|
|
11
11
|
readLiveProjectSessionPointers,
|
|
12
|
-
readMatchingProjectSessionPointer,
|
|
13
12
|
readProjectSessionPointer,
|
|
14
13
|
readSessionState,
|
|
15
14
|
} from './hooks.js'
|
|
@@ -33,6 +32,7 @@ import { inferInvocationContext } from './invocation-context.js'
|
|
|
33
32
|
import type { SkillScope } from './native-skills.js'
|
|
34
33
|
import {
|
|
35
34
|
firstBlocker,
|
|
35
|
+
readinessJson,
|
|
36
36
|
type Readiness,
|
|
37
37
|
type ReadinessRefresh,
|
|
38
38
|
type ReadinessState,
|
|
@@ -40,9 +40,11 @@ import {
|
|
|
40
40
|
} from './readiness.js'
|
|
41
41
|
import { packageVersion } from './release.js'
|
|
42
42
|
import type { Tone } from './ui/theme.js'
|
|
43
|
+
import type { MachineCredential } from './credentials.js'
|
|
43
44
|
import {
|
|
44
45
|
EXIT,
|
|
45
46
|
UPDATE_CLI_COMMAND,
|
|
47
|
+
diagnoseIgnoredOriginOverride,
|
|
46
48
|
loadLoggedConfig,
|
|
47
49
|
makeClient,
|
|
48
50
|
resolvedBaseUrl,
|
|
@@ -56,8 +58,27 @@ import {
|
|
|
56
58
|
type ActiveHarnessSession,
|
|
57
59
|
} from './commands-harness-context.js'
|
|
58
60
|
import { stopShapeProblems } from './commands-hook-shape.js'
|
|
59
|
-
import {
|
|
60
|
-
|
|
61
|
+
import {
|
|
62
|
+
HOOK_EVENTS,
|
|
63
|
+
activeQuestionRouteProblems,
|
|
64
|
+
hookActivationAdvice,
|
|
65
|
+
requiredHookEvents,
|
|
66
|
+
} from './commands-hooks.js'
|
|
67
|
+
import { cliBinReadiness } from './cli-bin.js'
|
|
68
|
+
import {
|
|
69
|
+
latestPublishedCliVersion,
|
|
70
|
+
newerPublishedCli,
|
|
71
|
+
shouldConsultCliRegistry,
|
|
72
|
+
thisCliVersion,
|
|
73
|
+
} from './cli-release.js'
|
|
74
|
+
import {
|
|
75
|
+
SETUP_PROOF_STALE_MS,
|
|
76
|
+
observedCompanionReceipt,
|
|
77
|
+
readSetupProof,
|
|
78
|
+
setupProofProject,
|
|
79
|
+
setupProofIsStale,
|
|
80
|
+
setupProofApplies,
|
|
81
|
+
} from './commands-setup-proof.js'
|
|
61
82
|
import { skillReadiness } from './commands-skill.js'
|
|
62
83
|
|
|
63
84
|
// ---------------------------------------------------------------------------
|
|
@@ -236,10 +257,14 @@ function remoteStatesFrom(previous: Readiness): {
|
|
|
236
257
|
return { credential, server, contract, auth, devices, proof }
|
|
237
258
|
}
|
|
238
259
|
|
|
239
|
-
function remoteInvalidatedByConfig(
|
|
240
|
-
|
|
260
|
+
function remoteInvalidatedByConfig(
|
|
261
|
+
previous: Readiness,
|
|
262
|
+
config: CliConfig,
|
|
263
|
+
credential: MachineCredential | null,
|
|
264
|
+
): boolean {
|
|
265
|
+
const origin = resolvedBaseUrl(config, credential)
|
|
241
266
|
const server = previous.states.find((state) => state.id === 'server')
|
|
242
|
-
return server === undefined || !server.detail.includes(
|
|
267
|
+
return server === undefined || !server.detail.includes(origin)
|
|
243
268
|
}
|
|
244
269
|
|
|
245
270
|
/**
|
|
@@ -257,9 +282,12 @@ export async function assessReadiness(
|
|
|
257
282
|
skillScope?: SkillScope
|
|
258
283
|
previous?: Readiness
|
|
259
284
|
refresh?: readonly ReadinessRefresh[]
|
|
285
|
+
json?: boolean
|
|
260
286
|
} = {},
|
|
261
287
|
): Promise<Readiness> {
|
|
262
288
|
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env })
|
|
289
|
+
const credential = deps.store.load()
|
|
290
|
+
if (credential) diagnoseIgnoredOriginOverride(deps.io, config, credential)
|
|
263
291
|
const previous = options.previous
|
|
264
292
|
const refresh = options.refresh
|
|
265
293
|
if (previous !== undefined && refresh !== undefined && refresh.length === 0) return previous
|
|
@@ -268,12 +296,13 @@ export async function assessReadiness(
|
|
|
268
296
|
previous !== undefined &&
|
|
269
297
|
refresh !== undefined &&
|
|
270
298
|
!refresh.includes('remote') &&
|
|
271
|
-
!remoteInvalidatedByConfig(previous, config)
|
|
299
|
+
!remoteInvalidatedByConfig(previous, config, credential)
|
|
272
300
|
if (reuseRemote) {
|
|
273
301
|
const reused = remoteStatesFrom(previous)
|
|
274
302
|
if (reused !== null) {
|
|
275
303
|
return {
|
|
276
304
|
states: [
|
|
305
|
+
cliBinReadiness(deps.env, deps.hookPlatform ?? process.platform),
|
|
277
306
|
projectReadiness(deps, config),
|
|
278
307
|
reused.credential,
|
|
279
308
|
reused.server,
|
|
@@ -292,9 +321,9 @@ export async function assessReadiness(
|
|
|
292
321
|
let accountClient: ApiClient | null = null
|
|
293
322
|
let accountDevices: RoutableDevice[] | null = null
|
|
294
323
|
|
|
324
|
+
states.push(cliBinReadiness(deps.env, deps.hookPlatform ?? process.platform))
|
|
295
325
|
states.push(projectReadiness(deps, config))
|
|
296
326
|
|
|
297
|
-
const credential = deps.store.load()
|
|
298
327
|
states.push(
|
|
299
328
|
credential
|
|
300
329
|
? {
|
|
@@ -457,10 +486,39 @@ export async function assessReadiness(
|
|
|
457
486
|
}
|
|
458
487
|
|
|
459
488
|
states.push(await setupProofState(deps, config, accountClient, accountDevices))
|
|
489
|
+
await applyRegistryRecommendation(deps, options.json === true, states)
|
|
460
490
|
|
|
461
491
|
return { states }
|
|
462
492
|
}
|
|
463
493
|
|
|
494
|
+
async function applyRegistryRecommendation(
|
|
495
|
+
deps: CommandDeps,
|
|
496
|
+
json: boolean,
|
|
497
|
+
states: ReadinessState[],
|
|
498
|
+
): Promise<void> {
|
|
499
|
+
if (
|
|
500
|
+
!shouldConsultCliRegistry({
|
|
501
|
+
...(deps.io.interactive === undefined ? {} : { interactive: deps.io.interactive }),
|
|
502
|
+
json,
|
|
503
|
+
env: deps.env,
|
|
504
|
+
})
|
|
505
|
+
) {
|
|
506
|
+
return
|
|
507
|
+
}
|
|
508
|
+
const latest = await latestPublishedCliVersion(deps.fetchImpl)
|
|
509
|
+
const newer = newerPublishedCli(thisCliVersion(), latest)
|
|
510
|
+
if (newer === null) return
|
|
511
|
+
const contract = states.find((state) => state.id === 'contract')
|
|
512
|
+
if (contract === undefined || contract.status === 'gap') return
|
|
513
|
+
contract.status = 'optional-gap'
|
|
514
|
+
contract.detail = 'A newer Notifai is available.'
|
|
515
|
+
contract.remedy = {
|
|
516
|
+
by: 'user-here',
|
|
517
|
+
summary: 'update Notifai',
|
|
518
|
+
command: UPDATE_CLI_COMMAND,
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
464
522
|
async function setupProofState(
|
|
465
523
|
deps: CommandDeps,
|
|
466
524
|
config: CliConfig,
|
|
@@ -486,12 +544,10 @@ async function setupProofState(
|
|
|
486
544
|
}
|
|
487
545
|
}
|
|
488
546
|
|
|
489
|
-
const
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
: companions.find((device) => device.device_id === proof.device_id)
|
|
494
|
-
if (proof === null || proof.project !== config.project.value || target === undefined) {
|
|
547
|
+
const project = setupProofProject(deps, config.project.value)
|
|
548
|
+
const proof = readSetupProof(deps, project)
|
|
549
|
+
const applies = setupProofApplies(proof, project, companions.map((device) => device.device_id))
|
|
550
|
+
if (!applies) {
|
|
495
551
|
return {
|
|
496
552
|
id: 'proof',
|
|
497
553
|
title: 'Delivery proof',
|
|
@@ -518,6 +574,20 @@ async function setupProofState(
|
|
|
518
574
|
detail: `Companion Receipt (the app's delivery confirmation) observed from ${observed.delivery.device_name} at ${observed.observedAt} (${proof.request_id})`,
|
|
519
575
|
}
|
|
520
576
|
}
|
|
577
|
+
const now = (deps.now ?? Date.now)()
|
|
578
|
+
if (setupProofIsStale(proof, now)) {
|
|
579
|
+
return {
|
|
580
|
+
id: 'proof',
|
|
581
|
+
title: 'Delivery proof',
|
|
582
|
+
status: 'gap',
|
|
583
|
+
detail: `${proof.request_id} is older than ${SETUP_PROOF_STALE_MS / 36e5}h with no Companion Receipt; init will send a replacement`,
|
|
584
|
+
remedy: {
|
|
585
|
+
by: 'cli',
|
|
586
|
+
summary: 'replace the stale verification notification',
|
|
587
|
+
command: 'notifai init',
|
|
588
|
+
},
|
|
589
|
+
}
|
|
590
|
+
}
|
|
521
591
|
return {
|
|
522
592
|
id: 'proof',
|
|
523
593
|
title: 'Delivery proof',
|
|
@@ -549,7 +619,8 @@ export async function doctorCommand(
|
|
|
549
619
|
flags: { json?: boolean },
|
|
550
620
|
options: { readiness?: Readiness } = {},
|
|
551
621
|
): Promise<number> {
|
|
552
|
-
const readiness =
|
|
622
|
+
const readiness =
|
|
623
|
+
options.readiness ?? (await assessReadiness(deps, flags.json === true ? { json: true } : {}))
|
|
553
624
|
const blocker = firstBlocker(readiness)
|
|
554
625
|
const ok = blocker === null
|
|
555
626
|
|
|
@@ -558,7 +629,7 @@ export async function doctorCommand(
|
|
|
558
629
|
if (flags.json || deps.io.interactive !== true) {
|
|
559
630
|
deps.io.out(
|
|
560
631
|
JSON.stringify(
|
|
561
|
-
{ ok, exit_code: ok ? EXIT.ok : EXIT.failed
|
|
632
|
+
{ ...readinessJson(readiness), ok, exit_code: ok ? EXIT.ok : EXIT.failed },
|
|
562
633
|
null,
|
|
563
634
|
2,
|
|
564
635
|
),
|
|
@@ -834,17 +905,11 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
|
|
|
834
905
|
}),
|
|
835
906
|
})
|
|
836
907
|
if (activeInstallations.length > 0) {
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
deps.env,
|
|
843
|
-
(deps.now ?? Date.now)(),
|
|
844
|
-
active.sessionId,
|
|
845
|
-
active.harness,
|
|
846
|
-
)
|
|
847
|
-
if (pointer === null) {
|
|
908
|
+
const exactState = active.sessionId === undefined
|
|
909
|
+
? null
|
|
910
|
+
: readSessionState(active.sessionId, deps.env)
|
|
911
|
+
const activated = exactState?.harness === active.harness && exactState.last_prompt_at !== undefined
|
|
912
|
+
if (!activated) {
|
|
848
913
|
// The normal condition of hooks installed moments ago: the pointer
|
|
849
914
|
// appears when the harness next fires a hook, and no command can
|
|
850
915
|
// force that. Informational, so `init` walks on to the states it can
|
|
@@ -853,7 +918,7 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
|
|
|
853
918
|
name: 'hooks (active session)',
|
|
854
919
|
ok: false,
|
|
855
920
|
informational: true,
|
|
856
|
-
detail: `active ${active.label} session has not published
|
|
921
|
+
detail: `active ${active.label} session has not published exact lifecycle state — send one ${active.label} prompt, then check again`,
|
|
857
922
|
remedy: {
|
|
858
923
|
summary: `send one ${active.label} prompt — its hook publishes the routing pointer`,
|
|
859
924
|
command: 'notifai doctor',
|
|
@@ -863,7 +928,7 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
|
|
|
863
928
|
checks.push({
|
|
864
929
|
name: 'hooks (active session)',
|
|
865
930
|
ok: true,
|
|
866
|
-
detail: `the
|
|
931
|
+
detail: `the exact active ${active.label} session has published lifecycle state`,
|
|
867
932
|
})
|
|
868
933
|
}
|
|
869
934
|
}
|
|
@@ -879,13 +944,25 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
|
|
|
879
944
|
})
|
|
880
945
|
.map((h) => `${h.event} -> ${handlerEvent(h.command)} in ${i.file}`),
|
|
881
946
|
)
|
|
947
|
+
const missing = installations.flatMap((installation) => {
|
|
948
|
+
const required = requiredHookEvents(installation.harness)
|
|
949
|
+
const installed = new Set(
|
|
950
|
+
installation.handlers
|
|
951
|
+
.map((handler) => handlerEvent(handler.command))
|
|
952
|
+
.filter((event): event is string => event !== null),
|
|
953
|
+
)
|
|
954
|
+
const absent = required.filter((event) => !installed.has(event))
|
|
955
|
+
return absent.length === 0
|
|
956
|
+
? []
|
|
957
|
+
: [`${installation.file} is missing ${absent.join(', ')}`]
|
|
958
|
+
})
|
|
882
959
|
checks.push({
|
|
883
960
|
name: 'hooks (stale)',
|
|
884
|
-
ok: stale.length === 0,
|
|
961
|
+
ok: stale.length === 0 && missing.length === 0,
|
|
885
962
|
detail:
|
|
886
|
-
stale.length === 0
|
|
887
|
-
? 'every installed handler names an event this build serves'
|
|
888
|
-
: `${stale.join('; ')} — rerun \`notifai hooks install\` to
|
|
963
|
+
stale.length === 0 && missing.length === 0
|
|
964
|
+
? 'every installed handler names an event this build serves, and every required lifecycle handler is present'
|
|
965
|
+
: `${[...stale, ...missing].join('; ')} — rerun \`notifai hooks install\` to refresh the complete lifecycle set`,
|
|
889
966
|
})
|
|
890
967
|
|
|
891
968
|
const adapterProblems = installations.flatMap((installation) =>
|
|
@@ -940,7 +1017,10 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
|
|
|
940
1017
|
// Project and global definitions for one harness both fire. Stable adapter
|
|
941
1018
|
// identity deliberately makes their command bytes equal, so comparing
|
|
942
1019
|
// command targets would now hide this duplicate rather than diagnose it.
|
|
943
|
-
//
|
|
1020
|
+
// Cursor is the exception to harness independence because it may also load
|
|
1021
|
+
// Claude's user hook file. The shared adapter detects Cursor's hook-only
|
|
1022
|
+
// environment and makes that compatibility copy a no-op; the native Cursor
|
|
1023
|
+
// definition remains the single routing owner.
|
|
944
1024
|
const duplicated = [...new Set(installations.map((i) => i.harness))]
|
|
945
1025
|
.map((harness) => ({
|
|
946
1026
|
harness,
|
|
@@ -1016,13 +1096,9 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
|
|
|
1016
1096
|
? (readLiveProjectSessionPointers(deps.cwd, deps.env, (deps.now ?? Date.now)()).find(
|
|
1017
1097
|
(pointer) => pointer.harness === active.harness,
|
|
1018
1098
|
) ?? null)
|
|
1019
|
-
:
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
(deps.now ?? Date.now)(),
|
|
1023
|
-
active.sessionId,
|
|
1024
|
-
active.harness,
|
|
1025
|
-
)
|
|
1099
|
+
: readSessionState(active.sessionId, deps.env).harness === active.harness
|
|
1100
|
+
? { sessionId: active.sessionId, harness: active.harness }
|
|
1101
|
+
: null
|
|
1026
1102
|
const firedState = firedPointer === null ? null : readSessionState(firedPointer.sessionId, deps.env)
|
|
1027
1103
|
const promptFired = firedState?.last_prompt_at !== undefined
|
|
1028
1104
|
const stopFired = firedState?.last_stop_at !== undefined
|
package/src/commands-guidance.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import { describeSource } from './config-schema.js'
|
|
4
3
|
import { EXIT, type CommandDeps } from './commands-core.js'
|
|
5
|
-
import { SHIPPED_GUIDANCE, shippedGuidanceTopic } from './guidance-content.js'
|
|
4
|
+
import { GUIDANCE_TRUST_PREAMBLE, SHIPPED_GUIDANCE, shippedGuidanceTopic } from './guidance-content.js'
|
|
6
5
|
import {
|
|
7
6
|
GUIDANCE_TOPIC_MAX_BYTES,
|
|
8
7
|
GUIDANCE_TOPIC_PATTERN,
|
|
@@ -11,40 +10,43 @@ import {
|
|
|
11
10
|
personalProjectGuidanceDir,
|
|
12
11
|
resolveGuidance,
|
|
13
12
|
} from './guidance.js'
|
|
13
|
+
import { renderGuidance } from './guidance-render.js'
|
|
14
14
|
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
// guidance show / set / unset
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* The whole effective guidance, ready to be followed as read.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* The whole effective guidance, ready to be followed as read.
|
|
21
|
+
*
|
|
22
|
+
* The trust preamble comes first and belongs to the CLI: it says who supplied
|
|
23
|
+
* each topic and what no topic may ask for. Each topic then follows its own
|
|
24
|
+
* marker, so a reader can tell the User's standing word from a repository's
|
|
25
|
+
* house rules from the shipped default without the output stopping being
|
|
26
|
+
* Markdown.
|
|
23
27
|
*/
|
|
24
28
|
export function guidanceShowCommand(deps: CommandDeps, flags: { json?: boolean }): number {
|
|
25
29
|
const topics = resolveGuidance({ cwd: deps.cwd, env: deps.env })
|
|
26
30
|
if (flags.json) {
|
|
27
31
|
deps.io.out(
|
|
28
32
|
JSON.stringify(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
{
|
|
34
|
+
trust: GUIDANCE_TRUST_PREAMBLE,
|
|
35
|
+
topics: topics.map((topic) => ({
|
|
36
|
+
name: topic.name,
|
|
37
|
+
source: topic.source,
|
|
38
|
+
authority: topic.authority,
|
|
39
|
+
summary: topic.summary,
|
|
40
|
+
content: topic.content,
|
|
41
|
+
})),
|
|
42
|
+
},
|
|
35
43
|
null,
|
|
36
44
|
2,
|
|
37
45
|
),
|
|
38
46
|
)
|
|
39
47
|
return EXIT.ok
|
|
40
48
|
}
|
|
41
|
-
|
|
42
|
-
const { label, path: filePath } = describeSource(topic.source)
|
|
43
|
-
const origin = topic.source === 'default' ? 'shipped default' : label
|
|
44
|
-
const provenance = filePath === null ? origin : `${origin} · ${filePath}`
|
|
45
|
-
return `<!-- ${topic.name} · ${provenance} -->\n${topic.content.trimEnd()}`
|
|
46
|
-
})
|
|
47
|
-
deps.io.out(blocks.join('\n\n'))
|
|
49
|
+
deps.io.out(renderGuidance(topics))
|
|
48
50
|
return EXIT.ok
|
|
49
51
|
}
|
|
50
52
|
|
|
@@ -72,7 +74,7 @@ async function guidanceMutationTarget(
|
|
|
72
74
|
deps: CommandDeps,
|
|
73
75
|
topic: string,
|
|
74
76
|
flags: GuidanceMutationFlags,
|
|
75
|
-
): Promise<string | null> {
|
|
77
|
+
): Promise<{ path: string; layer: GuidanceLayer } | null> {
|
|
76
78
|
if (!GUIDANCE_TOPIC_PATTERN.test(topic)) {
|
|
77
79
|
deps.io.err(
|
|
78
80
|
`"${topic}" is not a topic name. Use lowercase letters, digits, "-" and "_", e.g. ${SHIPPED_GUIDANCE.map((t) => t.name).join(', ')}.`,
|
|
@@ -88,9 +90,9 @@ async function guidanceMutationTarget(
|
|
|
88
90
|
deps.io.select
|
|
89
91
|
) {
|
|
90
92
|
const selected = await deps.io.select('Where should this guidance live?', [
|
|
91
|
-
{ value: 'global', label: 'This machine', hint: 'applies across projects' },
|
|
92
|
-
{ value: 'project', label: 'This project (shared)', hint: '.notifai/guidance/' },
|
|
93
|
-
{ value: 'local', label: 'This project (personal)', hint: '
|
|
93
|
+
{ value: 'global', label: 'This machine', hint: 'your standing word, applies across projects' },
|
|
94
|
+
{ value: 'project', label: 'This project (shared)', hint: '.notifai/guidance/ — committed, read as project policy' },
|
|
95
|
+
{ value: 'local', label: 'This project (personal)', hint: 'your standing word here, stored outside the repo' },
|
|
94
96
|
])
|
|
95
97
|
if (selected === null) {
|
|
96
98
|
deps.io.err('No guidance layer selected.')
|
|
@@ -98,7 +100,7 @@ async function guidanceMutationTarget(
|
|
|
98
100
|
}
|
|
99
101
|
layer = selected as GuidanceLayer
|
|
100
102
|
}
|
|
101
|
-
return path.join(guidanceLayerDir(deps, layer), `${topic}.md`)
|
|
103
|
+
return { path: path.join(guidanceLayerDir(deps, layer), `${topic}.md`), layer }
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
/**
|
|
@@ -130,19 +132,28 @@ export async function guidanceSetCommand(
|
|
|
130
132
|
shipped === undefined
|
|
131
133
|
? `Add guidance topic "${topic}"`
|
|
132
134
|
: `Replace the shipped "${topic}" guidance for this scope`
|
|
133
|
-
const confirmed = await deps.io.confirm(`${replaces} at ${target}?`)
|
|
135
|
+
const confirmed = await deps.io.confirm(`${replaces} at ${target.path}?`)
|
|
134
136
|
if (!confirmed) {
|
|
135
137
|
deps.io.err('Not confirmed. Pass --yes to skip the confirmation gate.')
|
|
136
138
|
return EXIT.usage
|
|
137
139
|
}
|
|
138
140
|
}
|
|
139
141
|
|
|
140
|
-
mkdirSync(path.dirname(target), { recursive: true })
|
|
141
|
-
writeFileSync(target, text.endsWith('\n') ? text : `${text}\n`)
|
|
142
|
-
deps.io.out(`Wrote ${topic} to ${target}`)
|
|
142
|
+
mkdirSync(path.dirname(target.path), { recursive: true })
|
|
143
|
+
writeFileSync(target.path, text.endsWith('\n') ? text : `${text}\n`)
|
|
144
|
+
deps.io.out(`Wrote ${topic} to ${target.path}`)
|
|
143
145
|
if (shipped !== undefined) {
|
|
144
146
|
deps.io.out(`This file now replaces the shipped "${topic}" guidance wherever it applies.`)
|
|
145
147
|
}
|
|
148
|
+
// A shared file is committed and travels to everyone who clones this
|
|
149
|
+
// repository, where it is read as the project's policy rather than as
|
|
150
|
+
// anyone's personal standing word. Worth saying at the moment of writing:
|
|
151
|
+
// the same sentence means something different in the two locations.
|
|
152
|
+
if (target.layer === 'project') {
|
|
153
|
+
deps.io.out(
|
|
154
|
+
'This is shared project policy: it is committed, everyone working here reads it, and it is never read as the User\'s own standing word. Use --local for a personal preference.',
|
|
155
|
+
)
|
|
156
|
+
}
|
|
146
157
|
return EXIT.ok
|
|
147
158
|
}
|
|
148
159
|
|
|
@@ -153,18 +164,18 @@ export async function guidanceUnsetCommand(
|
|
|
153
164
|
): Promise<number> {
|
|
154
165
|
const target = await guidanceMutationTarget(deps, topic, flags)
|
|
155
166
|
if (target === null) return EXIT.usage
|
|
156
|
-
if (!existsSync(target)) {
|
|
157
|
-
deps.io.out(`${topic} is not set at ${target}`)
|
|
167
|
+
if (!existsSync(target.path)) {
|
|
168
|
+
deps.io.out(`${topic} is not set at ${target.path}`)
|
|
158
169
|
return EXIT.ok
|
|
159
170
|
}
|
|
160
171
|
if (!flags.yes) {
|
|
161
|
-
const confirmed = await deps.io.confirm(`Remove ${target}?`)
|
|
172
|
+
const confirmed = await deps.io.confirm(`Remove ${target.path}?`)
|
|
162
173
|
if (!confirmed) {
|
|
163
174
|
deps.io.err('Not confirmed. Pass --yes to skip the confirmation gate.')
|
|
164
175
|
return EXIT.usage
|
|
165
176
|
}
|
|
166
177
|
}
|
|
167
|
-
rmSync(target, { force: true })
|
|
168
|
-
deps.io.out(`Removed ${target}; the inherited guidance now applies.`)
|
|
178
|
+
rmSync(target.path, { force: true })
|
|
179
|
+
deps.io.out(`Removed ${target.path}; the inherited guidance now applies.`)
|
|
169
180
|
return EXIT.ok
|
|
170
181
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readLiveProjectSessionPointers } from './hooks.js'
|
|
1
|
+
import { readLiveProjectSessionPointers, readSessionState } from './hooks.js'
|
|
2
2
|
import { type Harness } from './install-hooks.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -96,6 +96,29 @@ export function resolveActiveHarness(
|
|
|
96
96
|
const first = candidates[0]
|
|
97
97
|
if (first === undefined) return { active: null, contested: [] }
|
|
98
98
|
if (candidates.length === 1) return { active: first, contested: [] }
|
|
99
|
+
|
|
100
|
+
// Exact lifecycle state is machine-global and survives a command moving to
|
|
101
|
+
// another linked checkout. Prefer it before the checkout-local compatibility
|
|
102
|
+
// pointer. A unique newest hook event proves which nested harness owns this
|
|
103
|
+
// shell; ties and missing evidence remain contested and fail closed.
|
|
104
|
+
const evidenced = candidates.flatMap((candidate) => {
|
|
105
|
+
if (candidate.sessionId === undefined) return []
|
|
106
|
+
const state = readSessionState(candidate.sessionId, env)
|
|
107
|
+
if (state.harness !== candidate.harness) return []
|
|
108
|
+
const activity = Math.max(
|
|
109
|
+
state.last_prompt_at ?? 0,
|
|
110
|
+
state.last_stop_at ?? 0,
|
|
111
|
+
state.activation_context_emitted_at ?? 0,
|
|
112
|
+
)
|
|
113
|
+
return activity > 0 ? [{ candidate, activity }] : []
|
|
114
|
+
})
|
|
115
|
+
if (evidenced.length > 0) {
|
|
116
|
+
evidenced.sort((left, right) => right.activity - left.activity)
|
|
117
|
+
if (evidenced.length === 1 || evidenced[0]!.activity > evidenced[1]!.activity) {
|
|
118
|
+
return { active: evidenced[0]!.candidate, contested: [] }
|
|
119
|
+
}
|
|
120
|
+
return { active: first, contested: candidates }
|
|
121
|
+
}
|
|
99
122
|
for (const pointer of readLiveProjectSessionPointers(cwd, env, now)) {
|
|
100
123
|
const owner = candidates.find(
|
|
101
124
|
(candidate) =>
|