@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/command-session.d.ts +14 -0
  3. package/dist/command-session.js +30 -0
  4. package/dist/command-session.js.map +1 -0
  5. package/dist/commands-doctor.js +17 -15
  6. package/dist/commands-doctor.js.map +1 -1
  7. package/dist/commands-guidance.js +3 -29
  8. package/dist/commands-guidance.js.map +1 -1
  9. package/dist/commands-harness-context.js +21 -1
  10. package/dist/commands-harness-context.js.map +1 -1
  11. package/dist/commands-hooks.d.ts +11 -1
  12. package/dist/commands-hooks.js +97 -141
  13. package/dist/commands-hooks.js.map +1 -1
  14. package/dist/commands-init.d.ts +2 -0
  15. package/dist/commands-init.js +41 -21
  16. package/dist/commands-init.js.map +1 -1
  17. package/dist/commands-send.d.ts +1 -0
  18. package/dist/commands-send.js +62 -14
  19. package/dist/commands-send.js.map +1 -1
  20. package/dist/commands-setup-proof.d.ts +4 -3
  21. package/dist/commands-setup-proof.js +29 -14
  22. package/dist/commands-setup-proof.js.map +1 -1
  23. package/dist/commands.d.ts +1 -1
  24. package/dist/commands.js +1 -1
  25. package/dist/commands.js.map +1 -1
  26. package/dist/guidance-render.d.ts +20 -0
  27. package/dist/guidance-render.js +42 -0
  28. package/dist/guidance-render.js.map +1 -0
  29. package/dist/hooks.d.ts +17 -2
  30. package/dist/hooks.js +59 -7
  31. package/dist/hooks.js.map +1 -1
  32. package/dist/opencode-plugin.js +16 -5
  33. package/dist/opencode-plugin.js.map +1 -1
  34. package/dist/program.js +21 -12
  35. package/dist/program.js.map +1 -1
  36. package/dist/readiness.d.ts +6 -0
  37. package/dist/readiness.js +37 -0
  38. package/dist/readiness.js.map +1 -1
  39. package/dist/send-attempts.d.ts +26 -0
  40. package/dist/send-attempts.js +139 -0
  41. package/dist/send-attempts.js.map +1 -0
  42. package/dist/session-activation.d.ts +4 -8
  43. package/dist/session-activation.js +25 -14
  44. package/dist/session-activation.js.map +1 -1
  45. package/package.json +1 -1
  46. package/src/command-session.ts +38 -0
  47. package/src/commands-doctor.ts +18 -28
  48. package/src/commands-guidance.ts +3 -38
  49. package/src/commands-harness-context.ts +24 -1
  50. package/src/commands-hooks.ts +217 -170
  51. package/src/commands-init.ts +46 -24
  52. package/src/commands-send.ts +70 -14
  53. package/src/commands-setup-proof.test.ts +72 -0
  54. package/src/commands-setup-proof.ts +33 -14
  55. package/src/commands.test.ts +209 -67
  56. package/src/commands.ts +1 -0
  57. package/src/guidance-render.ts +56 -0
  58. package/src/guidance.test.ts +28 -0
  59. package/src/hooks.test.ts +38 -7
  60. package/src/hooks.ts +69 -5
  61. package/src/install-hooks.test.ts +3 -3
  62. package/src/opencode-plugin.ts +16 -5
  63. package/src/program.ts +22 -12
  64. package/src/readiness.ts +44 -0
  65. package/src/send-attempts.test.ts +143 -0
  66. package/src/send-attempts.ts +188 -0
  67. package/src/session-activation.ts +34 -17
  68. package/src/skill.test.ts +12 -14
@@ -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,
@@ -75,7 +75,9 @@ import {
75
75
  SETUP_PROOF_STALE_MS,
76
76
  observedCompanionReceipt,
77
77
  readSetupProof,
78
+ setupProofProject,
78
79
  setupProofIsStale,
80
+ setupProofApplies,
79
81
  } from './commands-setup-proof.js'
80
82
  import { skillReadiness } from './commands-skill.js'
81
83
 
@@ -542,12 +544,10 @@ async function setupProofState(
542
544
  }
543
545
  }
544
546
 
545
- const proof = readSetupProof(deps)
546
- const target =
547
- proof === null
548
- ? null
549
- : companions.find((device) => device.device_id === proof.device_id)
550
- 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) {
551
551
  return {
552
552
  id: 'proof',
553
553
  title: 'Delivery proof',
@@ -629,7 +629,7 @@ export async function doctorCommand(
629
629
  if (flags.json || deps.io.interactive !== true) {
630
630
  deps.io.out(
631
631
  JSON.stringify(
632
- { ok, exit_code: ok ? EXIT.ok : EXIT.failed, states: readiness.states },
632
+ { ...readinessJson(readiness), ok, exit_code: ok ? EXIT.ok : EXIT.failed },
633
633
  null,
634
634
  2,
635
635
  ),
@@ -905,17 +905,11 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
905
905
  }),
906
906
  })
907
907
  if (activeInstallations.length > 0) {
908
- const pointer =
909
- active.sessionId === undefined
910
- ? null
911
- : readMatchingProjectSessionPointer(
912
- deps.cwd,
913
- deps.env,
914
- (deps.now ?? Date.now)(),
915
- active.sessionId,
916
- active.harness,
917
- )
918
- 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) {
919
913
  // The normal condition of hooks installed moments ago: the pointer
920
914
  // appears when the harness next fires a hook, and no command can
921
915
  // force that. Informational, so `init` walks on to the states it can
@@ -924,7 +918,7 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
924
918
  name: 'hooks (active session)',
925
919
  ok: false,
926
920
  informational: true,
927
- detail: `active ${active.label} session has not published a live pointer — send one ${active.label} prompt, then check again`,
921
+ detail: `active ${active.label} session has not published exact lifecycle state — send one ${active.label} prompt, then check again`,
928
922
  remedy: {
929
923
  summary: `send one ${active.label} prompt — its hook publishes the routing pointer`,
930
924
  command: 'notifai doctor',
@@ -934,7 +928,7 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
934
928
  checks.push({
935
929
  name: 'hooks (active session)',
936
930
  ok: true,
937
- detail: `the concurrent project index contains the active ${active.label} session`,
931
+ detail: `the exact active ${active.label} session has published lifecycle state`,
938
932
  })
939
933
  }
940
934
  }
@@ -1102,13 +1096,9 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
1102
1096
  ? (readLiveProjectSessionPointers(deps.cwd, deps.env, (deps.now ?? Date.now)()).find(
1103
1097
  (pointer) => pointer.harness === active.harness,
1104
1098
  ) ?? null)
1105
- : readMatchingProjectSessionPointer(
1106
- deps.cwd,
1107
- deps.env,
1108
- (deps.now ?? Date.now)(),
1109
- active.sessionId,
1110
- active.harness,
1111
- )
1099
+ : readSessionState(active.sessionId, deps.env).harness === active.harness
1100
+ ? { sessionId: active.sessionId, harness: active.harness }
1101
+ : null
1112
1102
  const firedState = firedPointer === null ? null : readSessionState(firedPointer.sessionId, deps.env)
1113
1103
  const promptFired = firedState?.last_prompt_at !== undefined
1114
1104
  const stopFired = firedState?.last_stop_at !== undefined
@@ -1,12 +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 {
6
- GUIDANCE_TRUST_PREAMBLE,
7
- SHIPPED_GUIDANCE,
8
- shippedGuidanceTopic,
9
- } from './guidance-content.js'
4
+ import { GUIDANCE_TRUST_PREAMBLE, SHIPPED_GUIDANCE, shippedGuidanceTopic } from './guidance-content.js'
10
5
  import {
11
6
  GUIDANCE_TOPIC_MAX_BYTES,
12
7
  GUIDANCE_TOPIC_PATTERN,
@@ -14,36 +9,13 @@ import {
14
9
  globalGuidanceDir,
15
10
  personalProjectGuidanceDir,
16
11
  resolveGuidance,
17
- type GuidanceAuthority,
18
12
  } from './guidance.js'
13
+ import { renderGuidance } from './guidance-render.js'
19
14
 
20
15
  // ---------------------------------------------------------------------------
21
16
  // guidance show / set / unset
22
17
  // ---------------------------------------------------------------------------
23
18
 
24
- /** How each authority is named in the marker above a topic. */
25
- const AUTHORITY_LABEL: Record<GuidanceAuthority, string> = {
26
- user: 'you',
27
- repository: 'this repository',
28
- shipped: 'shipped default',
29
- }
30
-
31
- /**
32
- * The token that makes a marker line ours.
33
- *
34
- * Without it, a repository topic could open with a handwritten
35
- * `<!-- when-to-notify · this machine · /Users/... -->` and everything after
36
- * it would read as the User's own standing word. The marker therefore carries
37
- * a fixed token, and any occurrence of that token inside file content is
38
- * defanged below — so the only lines that can claim to be provenance are the
39
- * ones this command wrote.
40
- */
41
- const MARKER_TOKEN = 'notifai:guidance'
42
-
43
- function markerSafe(content: string): string {
44
- return content.replaceAll(MARKER_TOKEN, `${MARKER_TOKEN.replace(':', '-')} [not a provenance marker]`)
45
- }
46
-
47
19
  /**
48
20
  * The whole effective guidance, ready to be followed as read.
49
21
  *
@@ -74,14 +46,7 @@ export function guidanceShowCommand(deps: CommandDeps, flags: { json?: boolean }
74
46
  )
75
47
  return EXIT.ok
76
48
  }
77
- const blocks = topics.map((topic) => {
78
- const { path: filePath } = describeSource(topic.source)
79
- const marker =
80
- `<!-- ${MARKER_TOKEN} topic=${topic.name} from=${AUTHORITY_LABEL[topic.authority]}` +
81
- `${filePath === null ? '' : ` file=${encodeURIComponent(filePath)}`} -->`
82
- return `${marker}\n${markerSafe(topic.content).trimEnd()}`
83
- })
84
- deps.io.out([GUIDANCE_TRUST_PREAMBLE, ...blocks].join('\n\n'))
49
+ deps.io.out(renderGuidance(topics))
85
50
  return EXIT.ok
86
51
  }
87
52
 
@@ -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) =>