@raidiant/notifai 8.0.0 → 9.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.
Files changed (145) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +20 -13
  3. package/dist/atomic-file.d.ts +14 -0
  4. package/dist/atomic-file.js +27 -2
  5. package/dist/atomic-file.js.map +1 -1
  6. package/dist/cli-bin.d.ts +4 -0
  7. package/dist/cli-bin.js +62 -0
  8. package/dist/cli-bin.js.map +1 -0
  9. package/dist/cli-release.d.ts +10 -0
  10. package/dist/cli-release.js +75 -0
  11. package/dist/cli-release.js.map +1 -0
  12. package/dist/client.d.ts +1 -0
  13. package/dist/client.js +13 -3
  14. package/dist/client.js.map +1 -1
  15. package/dist/commands-auth.d.ts +6 -0
  16. package/dist/commands-auth.js +28 -3
  17. package/dist/commands-auth.js.map +1 -1
  18. package/dist/commands-config.js +22 -3
  19. package/dist/commands-config.js.map +1 -1
  20. package/dist/commands-core.d.ts +12 -0
  21. package/dist/commands-core.js +27 -3
  22. package/dist/commands-core.js.map +1 -1
  23. package/dist/commands-doctor.d.ts +1 -0
  24. package/dist/commands-doctor.js +71 -15
  25. package/dist/commands-doctor.js.map +1 -1
  26. package/dist/commands-guidance.d.ts +7 -3
  27. package/dist/commands-guidance.js +63 -28
  28. package/dist/commands-guidance.js.map +1 -1
  29. package/dist/commands-hooks.d.ts +9 -4
  30. package/dist/commands-hooks.js +430 -32
  31. package/dist/commands-hooks.js.map +1 -1
  32. package/dist/commands-init.d.ts +10 -18
  33. package/dist/commands-init.js +114 -32
  34. package/dist/commands-init.js.map +1 -1
  35. package/dist/commands-send-support.d.ts +17 -2
  36. package/dist/commands-send-support.js +61 -5
  37. package/dist/commands-send-support.js.map +1 -1
  38. package/dist/commands-send.js +60 -11
  39. package/dist/commands-send.js.map +1 -1
  40. package/dist/commands-setup-proof.d.ts +8 -0
  41. package/dist/commands-setup-proof.js +31 -2
  42. package/dist/commands-setup-proof.js.map +1 -1
  43. package/dist/commands-skill.d.ts +11 -1
  44. package/dist/commands-skill.js +61 -11
  45. package/dist/commands-skill.js.map +1 -1
  46. package/dist/commands.d.ts +1 -1
  47. package/dist/commands.js.map +1 -1
  48. package/dist/config-schema.d.ts +1 -1
  49. package/dist/config-schema.js +25 -0
  50. package/dist/config-schema.js.map +1 -1
  51. package/dist/config.d.ts +24 -3
  52. package/dist/config.js +37 -5
  53. package/dist/config.js.map +1 -1
  54. package/dist/credentials.d.ts +10 -1
  55. package/dist/credentials.js +25 -12
  56. package/dist/credentials.js.map +1 -1
  57. package/dist/guidance-content.d.ts +20 -0
  58. package/dist/guidance-content.js +73 -8
  59. package/dist/guidance-content.js.map +1 -1
  60. package/dist/guidance.d.ts +21 -0
  61. package/dist/guidance.js +73 -18
  62. package/dist/guidance.js.map +1 -1
  63. package/dist/hook-adapter.js +37 -3
  64. package/dist/hook-adapter.js.map +1 -1
  65. package/dist/hooks.d.ts +68 -7
  66. package/dist/hooks.js +231 -85
  67. package/dist/hooks.js.map +1 -1
  68. package/dist/install-hooks.d.ts +10 -18
  69. package/dist/install-hooks.js +110 -23
  70. package/dist/install-hooks.js.map +1 -1
  71. package/dist/interactive.d.ts +1 -1
  72. package/dist/interactive.js +1 -1
  73. package/dist/interactive.js.map +1 -1
  74. package/dist/logging.d.ts +8 -1
  75. package/dist/logging.js +43 -2
  76. package/dist/logging.js.map +1 -1
  77. package/dist/main.js +2 -13
  78. package/dist/main.js.map +1 -1
  79. package/dist/native-skills.d.ts +19 -0
  80. package/dist/native-skills.js +30 -8
  81. package/dist/native-skills.js.map +1 -1
  82. package/dist/opencode-plugin.js +14 -1
  83. package/dist/opencode-plugin.js.map +1 -1
  84. package/dist/program.js +13 -8
  85. package/dist/program.js.map +1 -1
  86. package/dist/send.d.ts +8 -1
  87. package/dist/send.js +17 -1
  88. package/dist/send.js.map +1 -1
  89. package/dist/session-activation.d.ts +15 -0
  90. package/dist/session-activation.js +35 -0
  91. package/dist/session-activation.js.map +1 -0
  92. package/dist/url-policy.d.ts +85 -0
  93. package/dist/url-policy.js +369 -0
  94. package/dist/url-policy.js.map +1 -0
  95. package/package.json +3 -3
  96. package/src/atomic-file.ts +36 -2
  97. package/src/cli-bin.test.ts +38 -0
  98. package/src/cli-bin.ts +67 -0
  99. package/src/cli-release.test.ts +46 -0
  100. package/src/cli-release.ts +78 -0
  101. package/src/client.test.ts +95 -0
  102. package/src/client.ts +16 -3
  103. package/src/commands-auth.ts +32 -3
  104. package/src/commands-config.ts +25 -2
  105. package/src/commands-core.ts +38 -3
  106. package/src/commands-doctor.ts +99 -13
  107. package/src/commands-guidance.ts +75 -29
  108. package/src/commands-hooks.ts +503 -34
  109. package/src/commands-init.ts +139 -38
  110. package/src/commands-send-support.test.ts +156 -0
  111. package/src/commands-send-support.ts +67 -5
  112. package/src/commands-send.ts +73 -10
  113. package/src/commands-setup-proof.ts +36 -2
  114. package/src/commands-skill.ts +78 -15
  115. package/src/commands.test.ts +1083 -101
  116. package/src/commands.ts +1 -0
  117. package/src/config-schema.ts +28 -1
  118. package/src/config.test.ts +1 -1
  119. package/src/config.ts +49 -5
  120. package/src/credentials.test.ts +71 -3
  121. package/src/credentials.ts +29 -15
  122. package/src/guidance-content.ts +74 -8
  123. package/src/guidance.test.ts +42 -11
  124. package/src/guidance.ts +100 -18
  125. package/src/hook-adapter.test.ts +113 -1
  126. package/src/hook-adapter.ts +37 -3
  127. package/src/hooks.test.ts +746 -165
  128. package/src/hooks.ts +299 -124
  129. package/src/install-hooks.test.ts +135 -16
  130. package/src/install-hooks.ts +113 -25
  131. package/src/interactive.ts +1 -1
  132. package/src/logging.test.ts +66 -3
  133. package/src/logging.ts +45 -2
  134. package/src/main.ts +2 -14
  135. package/src/native-skills.test.ts +41 -1
  136. package/src/native-skills.ts +40 -6
  137. package/src/opencode-plugin.ts +14 -1
  138. package/src/program.test.ts +37 -2
  139. package/src/program.ts +40 -12
  140. package/src/send.test.ts +28 -0
  141. package/src/send.ts +28 -2
  142. package/src/session-activation.ts +45 -0
  143. package/src/skill.test.ts +120 -4
  144. package/src/trust-policy.test.ts +701 -0
  145. package/src/url-policy.ts +399 -0
@@ -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,25 @@ import {
56
58
  type ActiveHarnessSession,
57
59
  } from './commands-harness-context.js'
58
60
  import { stopShapeProblems } from './commands-hook-shape.js'
59
- import { HOOK_EVENTS, activeQuestionRouteProblems, hookActivationAdvice } from './commands-hooks.js'
60
- import { observedCompanionReceipt, readSetupProof } from './commands-setup-proof.js'
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
+ setupProofIsStale,
79
+ } from './commands-setup-proof.js'
61
80
  import { skillReadiness } from './commands-skill.js'
62
81
 
63
82
  // ---------------------------------------------------------------------------
@@ -236,10 +255,14 @@ function remoteStatesFrom(previous: Readiness): {
236
255
  return { credential, server, contract, auth, devices, proof }
237
256
  }
238
257
 
239
- function remoteInvalidatedByConfig(previous: Readiness, config: CliConfig): boolean {
240
- if (config.base_url.source === 'default') return false
258
+ function remoteInvalidatedByConfig(
259
+ previous: Readiness,
260
+ config: CliConfig,
261
+ credential: MachineCredential | null,
262
+ ): boolean {
263
+ const origin = resolvedBaseUrl(config, credential)
241
264
  const server = previous.states.find((state) => state.id === 'server')
242
- return server === undefined || !server.detail.includes(config.base_url.value)
265
+ return server === undefined || !server.detail.includes(origin)
243
266
  }
244
267
 
245
268
  /**
@@ -257,9 +280,12 @@ export async function assessReadiness(
257
280
  skillScope?: SkillScope
258
281
  previous?: Readiness
259
282
  refresh?: readonly ReadinessRefresh[]
283
+ json?: boolean
260
284
  } = {},
261
285
  ): Promise<Readiness> {
262
286
  const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env })
287
+ const credential = deps.store.load()
288
+ if (credential) diagnoseIgnoredOriginOverride(deps.io, config, credential)
263
289
  const previous = options.previous
264
290
  const refresh = options.refresh
265
291
  if (previous !== undefined && refresh !== undefined && refresh.length === 0) return previous
@@ -268,12 +294,13 @@ export async function assessReadiness(
268
294
  previous !== undefined &&
269
295
  refresh !== undefined &&
270
296
  !refresh.includes('remote') &&
271
- !remoteInvalidatedByConfig(previous, config)
297
+ !remoteInvalidatedByConfig(previous, config, credential)
272
298
  if (reuseRemote) {
273
299
  const reused = remoteStatesFrom(previous)
274
300
  if (reused !== null) {
275
301
  return {
276
302
  states: [
303
+ cliBinReadiness(deps.env, deps.hookPlatform ?? process.platform),
277
304
  projectReadiness(deps, config),
278
305
  reused.credential,
279
306
  reused.server,
@@ -292,9 +319,9 @@ export async function assessReadiness(
292
319
  let accountClient: ApiClient | null = null
293
320
  let accountDevices: RoutableDevice[] | null = null
294
321
 
322
+ states.push(cliBinReadiness(deps.env, deps.hookPlatform ?? process.platform))
295
323
  states.push(projectReadiness(deps, config))
296
324
 
297
- const credential = deps.store.load()
298
325
  states.push(
299
326
  credential
300
327
  ? {
@@ -457,10 +484,39 @@ export async function assessReadiness(
457
484
  }
458
485
 
459
486
  states.push(await setupProofState(deps, config, accountClient, accountDevices))
487
+ await applyRegistryRecommendation(deps, options.json === true, states)
460
488
 
461
489
  return { states }
462
490
  }
463
491
 
492
+ async function applyRegistryRecommendation(
493
+ deps: CommandDeps,
494
+ json: boolean,
495
+ states: ReadinessState[],
496
+ ): Promise<void> {
497
+ if (
498
+ !shouldConsultCliRegistry({
499
+ ...(deps.io.interactive === undefined ? {} : { interactive: deps.io.interactive }),
500
+ json,
501
+ env: deps.env,
502
+ })
503
+ ) {
504
+ return
505
+ }
506
+ const latest = await latestPublishedCliVersion(deps.fetchImpl)
507
+ const newer = newerPublishedCli(thisCliVersion(), latest)
508
+ if (newer === null) return
509
+ const contract = states.find((state) => state.id === 'contract')
510
+ if (contract === undefined || contract.status === 'gap') return
511
+ contract.status = 'optional-gap'
512
+ contract.detail = 'A newer Notifai is available.'
513
+ contract.remedy = {
514
+ by: 'user-here',
515
+ summary: 'update Notifai',
516
+ command: UPDATE_CLI_COMMAND,
517
+ }
518
+ }
519
+
464
520
  async function setupProofState(
465
521
  deps: CommandDeps,
466
522
  config: CliConfig,
@@ -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 = options.readiness ?? (await assessReadiness(deps))
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
 
@@ -879,13 +950,25 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
879
950
  })
880
951
  .map((h) => `${h.event} -> ${handlerEvent(h.command)} in ${i.file}`),
881
952
  )
953
+ const missing = installations.flatMap((installation) => {
954
+ const required = requiredHookEvents(installation.harness)
955
+ const installed = new Set(
956
+ installation.handlers
957
+ .map((handler) => handlerEvent(handler.command))
958
+ .filter((event): event is string => event !== null),
959
+ )
960
+ const absent = required.filter((event) => !installed.has(event))
961
+ return absent.length === 0
962
+ ? []
963
+ : [`${installation.file} is missing ${absent.join(', ')}`]
964
+ })
882
965
  checks.push({
883
966
  name: 'hooks (stale)',
884
- ok: stale.length === 0,
967
+ ok: stale.length === 0 && missing.length === 0,
885
968
  detail:
886
- stale.length === 0
887
- ? 'every installed handler names an event this build serves'
888
- : `${stale.join('; ')} — rerun \`notifai hooks install\` to drop ${stale.length === 1 ? 'it' : 'them'}`,
969
+ stale.length === 0 && missing.length === 0
970
+ ? 'every installed handler names an event this build serves, and every required lifecycle handler is present'
971
+ : `${[...stale, ...missing].join('; ')} — rerun \`notifai hooks install\` to refresh the complete lifecycle set`,
889
972
  })
890
973
 
891
974
  const adapterProblems = installations.flatMap((installation) =>
@@ -940,7 +1023,10 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
940
1023
  // Project and global definitions for one harness both fire. Stable adapter
941
1024
  // identity deliberately makes their command bytes equal, so comparing
942
1025
  // command targets would now hide this duplicate rather than diagnose it.
943
- // Different harnesses remain independent: only the active one runs.
1026
+ // Cursor is the exception to harness independence because it may also load
1027
+ // Claude's user hook file. The shared adapter detects Cursor's hook-only
1028
+ // environment and makes that compatibility copy a no-op; the native Cursor
1029
+ // definition remains the single routing owner.
944
1030
  const duplicated = [...new Set(installations.map((i) => i.harness))]
945
1031
  .map((harness) => ({
946
1032
  harness,
@@ -2,7 +2,11 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { describeSource } from './config-schema.js'
4
4
  import { EXIT, type CommandDeps } from './commands-core.js'
5
- import { SHIPPED_GUIDANCE, shippedGuidanceTopic } from './guidance-content.js'
5
+ import {
6
+ GUIDANCE_TRUST_PREAMBLE,
7
+ SHIPPED_GUIDANCE,
8
+ shippedGuidanceTopic,
9
+ } from './guidance-content.js'
6
10
  import {
7
11
  GUIDANCE_TOPIC_MAX_BYTES,
8
12
  GUIDANCE_TOPIC_PATTERN,
@@ -10,28 +14,60 @@ import {
10
14
  globalGuidanceDir,
11
15
  personalProjectGuidanceDir,
12
16
  resolveGuidance,
17
+ type GuidanceAuthority,
13
18
  } from './guidance.js'
14
19
 
15
20
  // ---------------------------------------------------------------------------
16
21
  // guidance show / set / unset
17
22
  // ---------------------------------------------------------------------------
18
23
 
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
+
19
31
  /**
20
- * The whole effective guidance, ready to be followed as read. Each topic is
21
- * printed under a provenance comment so a reader can tell the user's standing
22
- * word from the shipped default without the output stopping being Markdown.
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
+ /**
48
+ * The whole effective guidance, ready to be followed as read.
49
+ *
50
+ * The trust preamble comes first and belongs to the CLI: it says who supplied
51
+ * each topic and what no topic may ask for. Each topic then follows its own
52
+ * marker, so a reader can tell the User's standing word from a repository's
53
+ * house rules from the shipped default without the output stopping being
54
+ * Markdown.
23
55
  */
24
56
  export function guidanceShowCommand(deps: CommandDeps, flags: { json?: boolean }): number {
25
57
  const topics = resolveGuidance({ cwd: deps.cwd, env: deps.env })
26
58
  if (flags.json) {
27
59
  deps.io.out(
28
60
  JSON.stringify(
29
- topics.map((topic) => ({
30
- name: topic.name,
31
- source: topic.source,
32
- summary: topic.summary,
33
- content: topic.content,
34
- })),
61
+ {
62
+ trust: GUIDANCE_TRUST_PREAMBLE,
63
+ topics: topics.map((topic) => ({
64
+ name: topic.name,
65
+ source: topic.source,
66
+ authority: topic.authority,
67
+ summary: topic.summary,
68
+ content: topic.content,
69
+ })),
70
+ },
35
71
  null,
36
72
  2,
37
73
  ),
@@ -39,12 +75,13 @@ export function guidanceShowCommand(deps: CommandDeps, flags: { json?: boolean }
39
75
  return EXIT.ok
40
76
  }
41
77
  const blocks = topics.map((topic) => {
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()}`
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()}`
46
83
  })
47
- deps.io.out(blocks.join('\n\n'))
84
+ deps.io.out([GUIDANCE_TRUST_PREAMBLE, ...blocks].join('\n\n'))
48
85
  return EXIT.ok
49
86
  }
50
87
 
@@ -72,7 +109,7 @@ async function guidanceMutationTarget(
72
109
  deps: CommandDeps,
73
110
  topic: string,
74
111
  flags: GuidanceMutationFlags,
75
- ): Promise<string | null> {
112
+ ): Promise<{ path: string; layer: GuidanceLayer } | null> {
76
113
  if (!GUIDANCE_TOPIC_PATTERN.test(topic)) {
77
114
  deps.io.err(
78
115
  `"${topic}" is not a topic name. Use lowercase letters, digits, "-" and "_", e.g. ${SHIPPED_GUIDANCE.map((t) => t.name).join(', ')}.`,
@@ -88,9 +125,9 @@ async function guidanceMutationTarget(
88
125
  deps.io.select
89
126
  ) {
90
127
  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: 'stored on this machine, not in the repo' },
128
+ { value: 'global', label: 'This machine', hint: 'your standing word, applies across projects' },
129
+ { value: 'project', label: 'This project (shared)', hint: '.notifai/guidance/ — committed, read as project policy' },
130
+ { value: 'local', label: 'This project (personal)', hint: 'your standing word here, stored outside the repo' },
94
131
  ])
95
132
  if (selected === null) {
96
133
  deps.io.err('No guidance layer selected.')
@@ -98,7 +135,7 @@ async function guidanceMutationTarget(
98
135
  }
99
136
  layer = selected as GuidanceLayer
100
137
  }
101
- return path.join(guidanceLayerDir(deps, layer), `${topic}.md`)
138
+ return { path: path.join(guidanceLayerDir(deps, layer), `${topic}.md`), layer }
102
139
  }
103
140
 
104
141
  /**
@@ -130,19 +167,28 @@ export async function guidanceSetCommand(
130
167
  shipped === undefined
131
168
  ? `Add guidance topic "${topic}"`
132
169
  : `Replace the shipped "${topic}" guidance for this scope`
133
- const confirmed = await deps.io.confirm(`${replaces} at ${target}?`)
170
+ const confirmed = await deps.io.confirm(`${replaces} at ${target.path}?`)
134
171
  if (!confirmed) {
135
172
  deps.io.err('Not confirmed. Pass --yes to skip the confirmation gate.')
136
173
  return EXIT.usage
137
174
  }
138
175
  }
139
176
 
140
- mkdirSync(path.dirname(target), { recursive: true })
141
- writeFileSync(target, text.endsWith('\n') ? text : `${text}\n`)
142
- deps.io.out(`Wrote ${topic} to ${target}`)
177
+ mkdirSync(path.dirname(target.path), { recursive: true })
178
+ writeFileSync(target.path, text.endsWith('\n') ? text : `${text}\n`)
179
+ deps.io.out(`Wrote ${topic} to ${target.path}`)
143
180
  if (shipped !== undefined) {
144
181
  deps.io.out(`This file now replaces the shipped "${topic}" guidance wherever it applies.`)
145
182
  }
183
+ // A shared file is committed and travels to everyone who clones this
184
+ // repository, where it is read as the project's policy rather than as
185
+ // anyone's personal standing word. Worth saying at the moment of writing:
186
+ // the same sentence means something different in the two locations.
187
+ if (target.layer === 'project') {
188
+ deps.io.out(
189
+ '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.',
190
+ )
191
+ }
146
192
  return EXIT.ok
147
193
  }
148
194
 
@@ -153,18 +199,18 @@ export async function guidanceUnsetCommand(
153
199
  ): Promise<number> {
154
200
  const target = await guidanceMutationTarget(deps, topic, flags)
155
201
  if (target === null) return EXIT.usage
156
- if (!existsSync(target)) {
157
- deps.io.out(`${topic} is not set at ${target}`)
202
+ if (!existsSync(target.path)) {
203
+ deps.io.out(`${topic} is not set at ${target.path}`)
158
204
  return EXIT.ok
159
205
  }
160
206
  if (!flags.yes) {
161
- const confirmed = await deps.io.confirm(`Remove ${target}?`)
207
+ const confirmed = await deps.io.confirm(`Remove ${target.path}?`)
162
208
  if (!confirmed) {
163
209
  deps.io.err('Not confirmed. Pass --yes to skip the confirmation gate.')
164
210
  return EXIT.usage
165
211
  }
166
212
  }
167
- rmSync(target, { force: true })
168
- deps.io.out(`Removed ${target}; the inherited guidance now applies.`)
213
+ rmSync(target.path, { force: true })
214
+ deps.io.out(`Removed ${target.path}; the inherited guidance now applies.`)
169
215
  return EXIT.ok
170
216
  }