cli-jaw 2.2.25 → 2.3.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 (159) hide show
  1. package/README.md +5 -2
  2. package/dist/bin/cli-jaw.js +6 -1
  3. package/dist/bin/cli-jaw.js.map +1 -1
  4. package/dist/bin/commands/doctor.js +39 -18
  5. package/dist/bin/commands/doctor.js.map +1 -1
  6. package/dist/bin/commands/init.js +82 -23
  7. package/dist/bin/commands/init.js.map +1 -1
  8. package/dist/bin/commands/messaging.js +262 -0
  9. package/dist/bin/commands/messaging.js.map +1 -0
  10. package/dist/server.js +30 -6
  11. package/dist/server.js.map +1 -1
  12. package/dist/src/cli/commands.js +6 -2
  13. package/dist/src/cli/commands.js.map +1 -1
  14. package/dist/src/cli/handlers/remote-session-commands.js +94 -0
  15. package/dist/src/cli/handlers/remote-session-commands.js.map +1 -0
  16. package/dist/src/command-contract/catalog.js +1 -1
  17. package/dist/src/command-contract/catalog.js.map +1 -1
  18. package/dist/src/core/chat-sessions.js +4 -0
  19. package/dist/src/core/chat-sessions.js.map +1 -1
  20. package/dist/src/core/config.js +63 -22
  21. package/dist/src/core/config.js.map +1 -1
  22. package/dist/src/core/db.js +6 -0
  23. package/dist/src/core/db.js.map +1 -1
  24. package/dist/src/core/dispatch-approval-ingress.js +45 -3
  25. package/dist/src/core/dispatch-approval-ingress.js.map +1 -1
  26. package/dist/src/core/dispatch-approval.js +46 -0
  27. package/dist/src/core/dispatch-approval.js.map +1 -1
  28. package/dist/src/core/logger.js +67 -0
  29. package/dist/src/core/logger.js.map +1 -1
  30. package/dist/src/core/session-generation.js +74 -0
  31. package/dist/src/core/session-generation.js.map +1 -0
  32. package/dist/src/discord/bot.js +91 -5
  33. package/dist/src/discord/bot.js.map +1 -1
  34. package/dist/src/discord/commands.js +34 -1
  35. package/dist/src/discord/commands.js.map +1 -1
  36. package/dist/src/discord/forwarder.js +4 -1
  37. package/dist/src/discord/forwarder.js.map +1 -1
  38. package/dist/src/discord/send-only-client.js +14 -7
  39. package/dist/src/discord/send-only-client.js.map +1 -1
  40. package/dist/src/messaging/access-policy.js +26 -0
  41. package/dist/src/messaging/access-policy.js.map +1 -0
  42. package/dist/src/messaging/approval-presentation.js +89 -0
  43. package/dist/src/messaging/approval-presentation.js.map +1 -0
  44. package/dist/src/messaging/channel-adapter.js +47 -0
  45. package/dist/src/messaging/channel-adapter.js.map +1 -0
  46. package/dist/src/messaging/channel-capabilities.js +47 -15
  47. package/dist/src/messaging/channel-capabilities.js.map +1 -1
  48. package/dist/src/messaging/channel-health.js +23 -3
  49. package/dist/src/messaging/channel-health.js.map +1 -1
  50. package/dist/src/messaging/delivery-outcome.js +12 -0
  51. package/dist/src/messaging/delivery-outcome.js.map +1 -1
  52. package/dist/src/messaging/durable-ingress.js +458 -0
  53. package/dist/src/messaging/durable-ingress.js.map +1 -0
  54. package/dist/src/messaging/effect-once.js +204 -0
  55. package/dist/src/messaging/effect-once.js.map +1 -0
  56. package/dist/src/messaging/inbound-envelope.js +168 -0
  57. package/dist/src/messaging/inbound-envelope.js.map +1 -0
  58. package/dist/src/messaging/ingress-audit.js +41 -0
  59. package/dist/src/messaging/ingress-audit.js.map +1 -0
  60. package/dist/src/messaging/ingress-generation.js +22 -0
  61. package/dist/src/messaging/ingress-generation.js.map +1 -0
  62. package/dist/src/messaging/metrics.js +69 -0
  63. package/dist/src/messaging/metrics.js.map +1 -0
  64. package/dist/src/messaging/outbound-outbox.js +184 -0
  65. package/dist/src/messaging/outbound-outbox.js.map +1 -0
  66. package/dist/src/messaging/remote-command-context.js +30 -0
  67. package/dist/src/messaging/remote-command-context.js.map +1 -0
  68. package/dist/src/messaging/retry.js +11 -2
  69. package/dist/src/messaging/retry.js.map +1 -1
  70. package/dist/src/messaging/runtime.js +129 -30
  71. package/dist/src/messaging/runtime.js.map +1 -1
  72. package/dist/src/messaging/send.js +31 -6
  73. package/dist/src/messaging/send.js.map +1 -1
  74. package/dist/src/messaging/trace-context.js +56 -0
  75. package/dist/src/messaging/trace-context.js.map +1 -0
  76. package/dist/src/messaging/types.js +39 -1
  77. package/dist/src/messaging/types.js.map +1 -1
  78. package/dist/src/routes/orchestrate.js +10 -3
  79. package/dist/src/routes/orchestrate.js.map +1 -1
  80. package/dist/src/routes/settings.js +26 -1
  81. package/dist/src/routes/settings.js.map +1 -1
  82. package/dist/src/slack/bot.js +182 -70
  83. package/dist/src/slack/bot.js.map +1 -1
  84. package/dist/src/slack/commands.js +16 -1
  85. package/dist/src/slack/commands.js.map +1 -1
  86. package/dist/src/slack/format.js +4 -1
  87. package/dist/src/slack/format.js.map +1 -1
  88. package/dist/src/slack/manifest.js +5 -0
  89. package/dist/src/slack/manifest.js.map +1 -1
  90. package/dist/src/slack/send-handler.js +26 -4
  91. package/dist/src/slack/send-handler.js.map +1 -1
  92. package/dist/src/slack/send-only-client.js +26 -2
  93. package/dist/src/slack/send-only-client.js.map +1 -1
  94. package/dist/src/slack/socket.js +36 -13
  95. package/dist/src/slack/socket.js.map +1 -1
  96. package/dist/src/telegram/bot.js +122 -14
  97. package/dist/src/telegram/bot.js.map +1 -1
  98. package/package.json +4 -1
  99. package/public/dist/assets/ChannelEnablementControl-Da_t_jq6.js +1 -0
  100. package/public/dist/assets/ChannelsDiscord-kNjCCJcA.js +1 -0
  101. package/public/dist/assets/ChannelsSlack-BOuoCfeN.js +1 -0
  102. package/public/dist/assets/ChannelsTelegram-DBIFDz4R.js +1 -0
  103. package/public/dist/assets/{app-DUbf-v7B.js → app-TWwXw3iD.js} +4 -4
  104. package/public/dist/assets/bgtask-badge-BhzSqQHG.js +1 -0
  105. package/public/dist/assets/{bgtask-badge-jIH69Dp3.js → bgtask-badge-DaAUnoKD.js} +1 -1
  106. package/public/dist/assets/{employees-BNy8a6XX.js → employees-BFKkaVAe.js} +1 -1
  107. package/public/dist/assets/iframe-renderer-BxEzdKIA.js +1 -0
  108. package/public/dist/assets/{iframe-renderer-C6Zwg9w4.js → iframe-renderer-CvnKi77F.js} +2 -2
  109. package/public/dist/assets/{manager-DzCR6Pam.js → manager-BTpvAFwX.js} +1 -1
  110. package/public/dist/assets/memory-DHOtCiw5.js +1 -0
  111. package/public/dist/assets/{memory-C4O44HMr.js → memory-db_s-38T.js} +1 -1
  112. package/public/dist/assets/{message-history-DCxD21J1.js → message-history-BX9shNKA.js} +1 -1
  113. package/public/dist/assets/message-history-DaSxkyy0.js +1 -0
  114. package/public/dist/assets/{render-A--hkukv.js → render-DZnFK8Kv.js} +1 -1
  115. package/public/dist/assets/{settings-3ZdoKHIf.js → settings-BuFMKcQ-.js} +1 -1
  116. package/public/dist/assets/settings-DotRDITm.js +1 -0
  117. package/public/dist/assets/{settings-core-CQe1uP6x.js → settings-core-3fj0DVoa.js} +17 -17
  118. package/public/dist/assets/settings-core-DohSnMad.js +1 -0
  119. package/public/dist/assets/{sidebar-DxFp_449.js → sidebar-D5TP7TCO.js} +3 -3
  120. package/public/dist/assets/{skills-BU4q8Q8n.js → skills-CQzHFYJS.js} +1 -1
  121. package/public/dist/assets/skills-DZHEvl1x.js +1 -0
  122. package/public/dist/assets/{slash-commands-BiXOgZ5B.js → slash-commands-BWEWWf-c.js} +1 -1
  123. package/public/dist/assets/slash-commands-CuJdloNC.js +1 -0
  124. package/public/dist/assets/{trace-drawer-2BTQCor6.js → trace-drawer-CBR-PZB4.js} +1 -1
  125. package/public/dist/assets/ui-CWl27eMl.js +1 -0
  126. package/public/dist/assets/{ui-Csy42uNW.js → ui-Ca1Q-VFQ.js} +1 -1
  127. package/public/dist/index.html +1 -1
  128. package/public/dist/manager/index.html +1 -1
  129. package/public/js/features/settings-channel.ts +35 -5
  130. package/public/js/features/settings-types.ts +7 -0
  131. package/public/js/features/transport-status-row.ts +23 -4
  132. package/public/manager/src/settings/pages/ChannelsDiscord.tsx +39 -11
  133. package/public/manager/src/settings/pages/ChannelsSlack.tsx +24 -7
  134. package/public/manager/src/settings/pages/ChannelsTelegram.tsx +38 -10
  135. package/public/manager/src/settings/pages/components/ChannelEnablementControl.tsx +98 -0
  136. package/public/manager/src/settings/pages/components/TransportStatusChips.tsx +23 -2
  137. package/scripts/docs/extract-commands.mts +3 -0
  138. package/scripts/generate-channel-capability-table.mts +161 -0
  139. package/scripts/promote-to-main.sh +175 -0
  140. package/scripts/release-gates.mjs +57 -3
  141. package/scripts/release-preview.sh +19 -0
  142. package/scripts/require-release-evidence.mjs +35 -1
  143. package/scripts/validate-messaging-certification.mts +83 -0
  144. package/scripts/write-messaging-certification.mts +65 -0
  145. package/public/dist/assets/ChannelsDiscord-D4fD-9hP.js +0 -1
  146. package/public/dist/assets/ChannelsSlack-_KBwRonM.js +0 -1
  147. package/public/dist/assets/ChannelsTelegram-tihzSWVE.js +0 -1
  148. package/public/dist/assets/TransportStatusChips-B0NR0ljO.js +0 -1
  149. package/public/dist/assets/bgtask-badge-B8KhuBa7.js +0 -1
  150. package/public/dist/assets/iframe-renderer-DBmV8gCQ.js +0 -1
  151. package/public/dist/assets/memory-Dt_Ji6aP.js +0 -1
  152. package/public/dist/assets/message-history-DyISauiv.js +0 -1
  153. package/public/dist/assets/settings-C6Pku2GC.js +0 -1
  154. package/public/dist/assets/settings-core-DqwLJkgP.js +0 -1
  155. package/public/dist/assets/skills-x2gH-1Ry.js +0 -1
  156. package/public/dist/assets/slash-commands-s8eR7Kd1.js +0 -1
  157. package/public/dist/assets/ui-CB0L33o0.js +0 -1
  158. package/scripts/release.sh +0 -224
  159. /package/public/dist/assets/{HealthBadge-CUaJ1PQH.js → HealthBadge-CiNMS-1k.js} +0 -0
@@ -1,6 +1,8 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import type { SettingsClient } from '../../types';
3
3
 
4
+ export type MessengerChannel = 'telegram' | 'discord' | 'slack';
5
+
4
6
  export type TransportStatus = {
5
7
  configured: boolean;
6
8
  activeInbound: boolean;
@@ -9,7 +11,9 @@ export type TransportStatus = {
9
11
  };
10
12
 
11
13
  export type ChannelHealth = {
12
- activeInbound: 'telegram' | 'discord' | 'slack';
14
+ /** @deprecated Use activeInboundChannels for the actual running set. */
15
+ activeInbound: MessengerChannel;
16
+ activeInboundChannels: MessengerChannel[];
13
17
  telegram: TransportStatus;
14
18
  discord: TransportStatus;
15
19
  slack: TransportStatus;
@@ -23,13 +27,29 @@ function isTransportStatus(value: unknown): value is TransportStatus {
23
27
  && typeof row['sendCapable'] === 'boolean';
24
28
  }
25
29
 
30
+ function isMessengerChannel(value: unknown): value is MessengerChannel {
31
+ return value === 'telegram' || value === 'discord' || value === 'slack';
32
+ }
33
+
26
34
  export function parseChannelHealth(payload: unknown): ChannelHealth | null {
27
35
  if (!payload || typeof payload !== 'object') return null;
28
36
  const channels = (payload as { channels?: unknown }).channels;
29
37
  if (!channels || typeof channels !== 'object') return null;
30
38
  const row = channels as Record<string, unknown>;
39
+ const rawActiveChannels = row['activeInboundChannels'];
40
+ let activeInboundChannels: MessengerChannel[] | null;
41
+ if (rawActiveChannels !== undefined) {
42
+ activeInboundChannels = Array.isArray(rawActiveChannels)
43
+ && rawActiveChannels.every(isMessengerChannel)
44
+ ? [...new Set(rawActiveChannels)]
45
+ : null;
46
+ } else {
47
+ const legacyActive = row['activeInbound'];
48
+ activeInboundChannels = isMessengerChannel(legacyActive) ? [legacyActive] : null;
49
+ }
50
+ if (!activeInboundChannels) return null;
31
51
  const active = row['activeInbound'];
32
- if (active !== 'telegram' && active !== 'discord' && active !== 'slack') return null;
52
+ if (!isMessengerChannel(active)) return null;
33
53
  if (!isTransportStatus(row['telegram']) || !isTransportStatus(row['discord'])) return null;
34
54
  // Slack is tolerated as absent: a newer Manager can be pointed at an older
35
55
  // running cli-jaw, and rejecting the whole payload would hide Telegram and
@@ -39,6 +59,7 @@ export function parseChannelHealth(payload: unknown): ChannelHealth | null {
39
59
  : { configured: false, activeInbound: false, sendCapable: false, reason: 'unavailable' };
40
60
  return {
41
61
  activeInbound: active,
62
+ activeInboundChannels,
42
63
  telegram: row['telegram'],
43
64
  discord: row['discord'],
44
65
  slack,
@@ -25,6 +25,9 @@ const CMDLINE_HIDDEN = new Set([
25
25
  'browser',
26
26
  'prompt',
27
27
  'version',
28
+ 'stop',
29
+ 'approve',
30
+ 'deny',
28
31
  ]);
29
32
 
30
33
  type SlashSurface = typeof SURFACES[number];
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Generates the messaging ChannelAdapter capability matrix inside
3
+ * `structure/CAPABILITY_TRUTH_TABLE.md`.
4
+ *
5
+ * node_modules/.bin/tsx scripts/generate-channel-capability-table.mts
6
+ * node_modules/.bin/tsx scripts/generate-channel-capability-table.mts --check
7
+ *
8
+ * Why a generator instead of a hand-kept table: the declaration in
9
+ * `src/messaging/channel-capabilities.ts` is the only thing the runtime obeys, so
10
+ * a hand-written doc can only ever be a second, quieter claim. `--check` is wired
11
+ * into `gate:truth-table-fresh`, which makes doc drift a release failure rather
12
+ * than a review-time nicety.
13
+ */
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import process from 'node:process';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ import { allChannelCapabilities, CHANNEL_CAPABILITY_KEYS } from '../src/messaging/channel-capabilities.ts';
20
+ import type { ChannelCapabilities } from '../src/messaging/channel-capabilities.ts';
21
+ import type { MessengerChannel } from '../src/messaging/types.ts';
22
+
23
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
24
+
25
+ const TABLE_REL = 'structure/CAPABILITY_TRUTH_TABLE.md';
26
+ const SOURCE_REL = 'src/messaging/channel-capabilities.ts';
27
+ const ADAPTER_REL = 'src/messaging/channel-adapter.ts';
28
+ const CONFORMANCE_REL = 'tests/unit/channel-contract-conformance.test.ts';
29
+ const GENERATOR_REL = 'scripts/generate-channel-capability-table.mts';
30
+
31
+ const START = '<!-- BEGIN GENERATED: messaging-channel-capabilities -->';
32
+ const END = '<!-- END GENERATED: messaging-channel-capabilities -->';
33
+
34
+ /** Prose for each capability key. Keyed by the closed set, so adding a capability
35
+ * without describing it is a compile error rather than an empty documentation cell. */
36
+ const CAPABILITY_NOTES: Record<keyof ChannelCapabilities, string> = {
37
+ sendText: '텍스트 전송',
38
+ editText: '전송된 메시지 수정',
39
+ deleteMessage: '전송된 메시지 삭제',
40
+ reaction: '리액션 부착',
41
+ typing: '입력 중 표시',
42
+ fileUpload: '파일 업로드',
43
+ voice: '음성 파일 전달 (녹음 UI 아님)',
44
+ threads: '스레드 타겟팅',
45
+ interactiveActions: '버튼 등 인터랙티브 액션',
46
+ durableIngress: '프로세스 재시작 후에도 유지되는 inbound 중복 제거',
47
+ replayableTransport: '미확인 프레임을 트랜스포트가 재전송',
48
+ maxMessageChars: '단일 메시지 문자 상한 (chunker 상수)',
49
+ };
50
+
51
+ function cell(value: boolean | number): string {
52
+ if (typeof value === 'number') return `\`${value.toLocaleString('en-US')}\``;
53
+ return value ? '✅' : '❌';
54
+ }
55
+
56
+ function buildBlock(): string {
57
+ const capabilities = allChannelCapabilities();
58
+ const channels = Object.keys(capabilities) as MessengerChannel[];
59
+
60
+ const lines: string[] = [];
61
+ lines.push('<!-- 이 블록은 생성됩니다. 손으로 고치지 마세요. -->');
62
+ lines.push(`<!-- 생성기: ${GENERATOR_REL} · 소스: ${SOURCE_REL} -->`);
63
+ lines.push('');
64
+ lines.push(`> **생성된 블록입니다 — 직접 수정하지 마세요.** \`${SOURCE_REL}\`의`);
65
+ lines.push(`> 선언을 읽어 \`npm run docs:channel-capabilities\`가 다시 씁니다.`);
66
+ lines.push(`> 손으로 고친 내용은 \`gate:truth-table-fresh\`에서 drift로 실패합니다.`);
67
+ lines.push('');
68
+ lines.push(`| Capability | ${channels.map((c) => c).join(' | ')} | 의미 |`);
69
+ lines.push(`| --- | ${channels.map(() => '---').join(' | ')} | --- |`);
70
+ for (const key of CHANNEL_CAPABILITY_KEYS) {
71
+ const values = channels.map((channel) => cell(capabilities[channel][key]));
72
+ lines.push(`| \`${key}\` | ${values.join(' | ')} | ${CAPABILITY_NOTES[key]} |`);
73
+ }
74
+ lines.push('');
75
+ lines.push('출처와 검증 지점:');
76
+ lines.push('');
77
+ lines.push(`- 선언 (SoT): [\`${SOURCE_REL}\`](../${SOURCE_REL})`);
78
+ lines.push(`- 어댑터 계약: [\`${ADAPTER_REL}\`](../${ADAPTER_REL})`);
79
+ lines.push(`- conformance test: \`${CONFORMANCE_REL}\` — 이 스위트 통과가 \`true\` 선언의 유일한 근거`);
80
+ lines.push(`- 생성기: [\`${GENERATOR_REL}\`](../${GENERATOR_REL}) (\`--check\`는 \`gate:truth-table-fresh\`가 실행)`);
81
+ lines.push('');
82
+ lines.push('`true`는 이 트리에서 오늘 호출 가능한 동작만을 뜻합니다. 벤더 SDK가 제공한다는');
83
+ lines.push('사실은 근거가 아닙니다 — conformance test가 통과할 때만 선언을 올립니다.');
84
+ return lines.join('\n');
85
+ }
86
+
87
+ type Located = {
88
+ text: string;
89
+ startIdx: number;
90
+ endIdx: number;
91
+ committed: string;
92
+ };
93
+
94
+ function locate(text: string): Located {
95
+ const startIdx = text.indexOf(START);
96
+ const endIdx = text.indexOf(END);
97
+ if (startIdx < 0 || endIdx < 0 || endIdx < startIdx) {
98
+ throw new Error(
99
+ `${TABLE_REL} is missing the generated block markers.\n` +
100
+ ` expected: ${START}\n ${END}`,
101
+ );
102
+ }
103
+ return {
104
+ text,
105
+ startIdx,
106
+ endIdx,
107
+ committed: text.slice(startIdx + START.length, endIdx),
108
+ };
109
+ }
110
+
111
+ /** First differing line, so a failing gate points at the row instead of dumping both blocks. */
112
+ function firstDifference(committed: string, expected: string): string {
113
+ const a = committed.split('\n');
114
+ const b = expected.split('\n');
115
+ for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
116
+ if (a[i] === b[i]) continue;
117
+ return [
118
+ ` first difference at generated-block line ${i + 1}:`,
119
+ ` committed: ${a[i] === undefined ? '(line missing)' : JSON.stringify(a[i])}`,
120
+ ` expected: ${b[i] === undefined ? '(line missing)' : JSON.stringify(b[i])}`,
121
+ ].join('\n');
122
+ }
123
+ return ' blocks differ in trailing bytes only';
124
+ }
125
+
126
+ function main(): number {
127
+ const check = process.argv.includes('--check');
128
+ const abs = path.join(repoRoot, TABLE_REL);
129
+ const original = fs.readFileSync(abs, 'utf8');
130
+ const located = locate(original);
131
+ const expected = `\n${buildBlock()}\n`;
132
+
133
+ if (check) {
134
+ if (located.committed === expected) {
135
+ console.log(`[ok] ${TABLE_REL} messaging capability matrix matches ${SOURCE_REL}`);
136
+ return 0;
137
+ }
138
+ console.error(
139
+ `[drift] ${TABLE_REL} messaging capability matrix does not match ${SOURCE_REL}.\n` +
140
+ `${firstDifference(located.committed, expected)}\n` +
141
+ ` fix: npm run docs:channel-capabilities`,
142
+ );
143
+ return 1;
144
+ }
145
+
146
+ if (located.committed === expected) {
147
+ console.log(`[unchanged] ${TABLE_REL}`);
148
+ return 0;
149
+ }
150
+ const next = original.slice(0, located.startIdx + START.length) + expected + original.slice(located.endIdx);
151
+ fs.writeFileSync(abs, next);
152
+ console.log(`[written] ${TABLE_REL} messaging capability matrix regenerated from ${SOURCE_REL}`);
153
+ return 0;
154
+ }
155
+
156
+ try {
157
+ process.exit(main());
158
+ } catch (err) {
159
+ console.error(`[error] ${(err as Error).message}`);
160
+ process.exit(1);
161
+ }
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ cd "$(dirname "$0")/.."
5
+
6
+ command -v gh >/dev/null 2>&1 || { echo "ERROR: gh is required" >&2; exit 1; }
7
+ gh auth status >/dev/null
8
+ git fetch origin main preview --tags --prune
9
+
10
+ LIVE_PREVIEW_SHA="$(git rev-parse refs/remotes/origin/preview^{commit})"
11
+ PREVIEW_SHA="$(git rev-parse "${1:-$LIVE_PREVIEW_SHA}^{commit}")"
12
+ if [ "$PREVIEW_SHA" != "$LIVE_PREVIEW_SHA" ]; then
13
+ echo "ERROR: requested SHA is not the live origin/preview head" >&2
14
+ exit 1
15
+ fi
16
+
17
+ PREVIEW_VERSION="$(git show "$PREVIEW_SHA:package.json" \
18
+ | node -e 'const fs=require("node:fs"); process.stdout.write(JSON.parse(fs.readFileSync(0,"utf8")).version)')"
19
+ if [[ ! "$PREVIEW_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-preview\.[0-9]+$ ]]; then
20
+ echo "ERROR: preview version must match X.Y.Z-preview.TIMESTAMP; got $PREVIEW_VERSION" >&2
21
+ exit 1
22
+ fi
23
+ STABLE_VERSION="${BASH_REMATCH[1]}"
24
+
25
+ TESTS_URL="$(gh run list \
26
+ --workflow test.yml \
27
+ --branch preview \
28
+ --commit "$PREVIEW_SHA" \
29
+ --event push \
30
+ --status success \
31
+ --limit 1 --json url --jq '.[0].url // ""')"
32
+ if [ -z "$TESTS_URL" ]; then
33
+ echo "ERROR: no successful Tests run for $PREVIEW_SHA on preview" >&2
34
+ exit 1
35
+ fi
36
+
37
+ MAIN_SHA="$(git rev-parse refs/remotes/origin/main^{commit})"
38
+ if ! git merge-base --is-ancestor "$MAIN_SHA" "$PREVIEW_SHA"; then
39
+ echo "ERROR: origin/main is not an ancestor of the certified preview SHA" >&2
40
+ exit 1
41
+ fi
42
+
43
+ WORKTREE="$(mktemp -d "${TMPDIR:-/tmp}/cli-jaw-promote.XXXXXX")"
44
+ PROMOTION_BRANCH="codex/promote-${STABLE_VERSION}-${PREVIEW_SHA:0:12}"
45
+ cleanup() {
46
+ git worktree remove --force "$WORKTREE" >/dev/null 2>&1 || true
47
+ rmdir "$WORKTREE" >/dev/null 2>&1 || true
48
+ }
49
+ trap cleanup EXIT
50
+ git worktree add -b "$PROMOTION_BRANCH" "$WORKTREE" "$PREVIEW_SHA"
51
+
52
+ (
53
+ cd "$WORKTREE"
54
+ npm version "$STABLE_VERSION" --no-git-tag-version --allow-same-version
55
+ node scripts/sync-electron-version.cjs
56
+ npm run gate:all
57
+ node scripts/require-release-evidence.mjs
58
+ git add package.json package-lock.json electron/package.json electron/package-lock.json
59
+ git commit -m "chore: promote v$STABLE_VERSION"
60
+ git push --set-upstream origin "$PROMOTION_BRANCH"
61
+ )
62
+
63
+ PROMOTION_COMMIT="$(git -C "$WORKTREE" rev-parse HEAD)"
64
+ PR_URL="$(gh pr create \
65
+ --base main \
66
+ --head "$PROMOTION_BRANCH" \
67
+ --title "chore: promote v$STABLE_VERSION" \
68
+ --body "Promotes certified preview $PREVIEW_VERSION at $PREVIEW_SHA. Tests: $TESTS_URL")"
69
+
70
+ gh pr checks "$PR_URL" --required --watch --fail-fast
71
+ gh pr merge "$PR_URL" --squash --match-head-commit "$PROMOTION_COMMIT"
72
+
73
+ MERGED_AT="$(gh pr view "$PR_URL" --json mergedAt --jq '.mergedAt // ""')"
74
+ if [ -z "$MERGED_AT" ]; then
75
+ echo "ERROR: promotion PR did not merge" >&2
76
+ exit 1
77
+ fi
78
+ PR_MERGE_SHA="$(gh pr view "$PR_URL" --json mergeCommit --jq '.mergeCommit.oid // ""')"
79
+ if [ -z "$PR_MERGE_SHA" ]; then
80
+ echo "ERROR: promotion PR has no merge commit SHA" >&2
81
+ exit 1
82
+ fi
83
+
84
+ git fetch origin main
85
+ MERGED_MAIN_SHA="$(git rev-parse refs/remotes/origin/main^{commit})"
86
+ LIVE_MAIN_SHA="$(git ls-remote origin refs/heads/main | cut -f1)"
87
+ if [ -z "$LIVE_MAIN_SHA" ] || [ "$MERGED_MAIN_SHA" != "$LIVE_MAIN_SHA" ] \
88
+ || [ "$MERGED_MAIN_SHA" != "$PR_MERGE_SHA" ]; then
89
+ echo "ERROR: merged PR SHA, origin/main, and live main head do not match" >&2
90
+ exit 1
91
+ fi
92
+ MERGED_VERSION="$(git show "$MERGED_MAIN_SHA:package.json" \
93
+ | node -e 'const fs=require("node:fs"); process.stdout.write(JSON.parse(fs.readFileSync(0,"utf8")).version)')"
94
+ if [ "$MERGED_VERSION" != "$STABLE_VERSION" ]; then
95
+ echo "ERROR: merged main version is $MERGED_VERSION, expected $STABLE_VERSION" >&2
96
+ exit 1
97
+ fi
98
+
99
+ deadline=$((SECONDS + 1200))
100
+ MAIN_TESTS_URL=""
101
+ while [ "$SECONDS" -lt "$deadline" ]; do
102
+ MAIN_TESTS_URL="$(gh run list \
103
+ --workflow test.yml \
104
+ --branch main \
105
+ --commit "$MERGED_MAIN_SHA" \
106
+ --event push \
107
+ --status success \
108
+ --limit 1 --json url --jq '.[0].url // ""')"
109
+ [ -n "$MAIN_TESTS_URL" ] && break
110
+ failed="$(gh run list \
111
+ --workflow test.yml \
112
+ --branch main \
113
+ --commit "$MERGED_MAIN_SHA" \
114
+ --event push \
115
+ --status completed \
116
+ --limit 1 --json conclusion --jq '.[0].conclusion // ""')"
117
+ case "$failed" in
118
+ failure|cancelled|timed_out|startup_failure|action_required)
119
+ echo "ERROR: main Tests completed with $failed" >&2
120
+ exit 1
121
+ ;;
122
+ esac
123
+ sleep 10
124
+ done
125
+ if [ -z "$MAIN_TESTS_URL" ]; then
126
+ echo "ERROR: timed out waiting for successful main Tests" >&2
127
+ exit 1
128
+ fi
129
+
130
+ deadline=$((SECONDS + 1200))
131
+ MAIN_PLATFORM_URL=""
132
+ while [ "$SECONDS" -lt "$deadline" ]; do
133
+ MAIN_PLATFORM_URL="$(gh run list \
134
+ --workflow postinstall-platform.yml \
135
+ --branch main \
136
+ --commit "$MERGED_MAIN_SHA" \
137
+ --event push \
138
+ --status success \
139
+ --limit 1 --json url --jq '.[0].url // ""')"
140
+ [ -n "$MAIN_PLATFORM_URL" ] && break
141
+ failed="$(gh run list \
142
+ --workflow postinstall-platform.yml \
143
+ --branch main \
144
+ --commit "$MERGED_MAIN_SHA" \
145
+ --event push \
146
+ --status completed \
147
+ --limit 1 --json conclusion --jq '.[0].conclusion // ""')"
148
+ case "$failed" in
149
+ failure|cancelled|timed_out|startup_failure|action_required)
150
+ echo "ERROR: main Postinstall Platform Checks completed with $failed" >&2
151
+ exit 1
152
+ ;;
153
+ esac
154
+ sleep 10
155
+ done
156
+ if [ -z "$MAIN_PLATFORM_URL" ]; then
157
+ echo "ERROR: timed out waiting for successful main Postinstall Platform Checks" >&2
158
+ exit 1
159
+ fi
160
+
161
+ LIVE_MAIN_SHA="$(git ls-remote origin refs/heads/main | cut -f1)"
162
+ if [ "$LIVE_MAIN_SHA" != "$MERGED_MAIN_SHA" ]; then
163
+ echo "ERROR: origin/main moved while waiting for release CI" >&2
164
+ exit 1
165
+ fi
166
+
167
+ gh workflow run publish.yml \
168
+ --ref main \
169
+ -f version="$STABLE_VERSION" \
170
+ -f tag=latest \
171
+ -f expected-sha="$MERGED_MAIN_SHA" \
172
+ -f dry-run=false \
173
+ -f create-github-release=true
174
+
175
+ echo "stable publish dispatched: cli-jaw@$STABLE_VERSION from $MERGED_MAIN_SHA"
@@ -88,14 +88,24 @@ const GATES = {
88
88
  },
89
89
  },
90
90
  'truth-table-fresh': {
91
- description: 'CAPABILITY_TRUTH_TABLE.md edited within 7 days OR matches code refs',
91
+ description: 'CAPABILITY_TRUTH_TABLE.md edited within 7 days OR matches code refs, AND its generated messaging channel matrix matches src/messaging/channel-capabilities.ts',
92
92
  check() {
93
93
  const rel = 'structure/CAPABILITY_TRUTH_TABLE.md';
94
94
  const abs = path.join(repoRoot, rel);
95
95
  if (!fs.existsSync(abs)) return { ok: false, detail: `${rel} missing` };
96
+ // The generated messaging block first: a stale matrix is a wrong claim
97
+ // regardless of the file's mtime, so freshness must not excuse it.
98
+ // node_modules/.bin/tsx directly — npx resolution is not a gate dependency.
99
+ const generated = run(path.join('node_modules', '.bin', 'tsx'), [
100
+ 'scripts/generate-channel-capability-table.mts', '--check',
101
+ ], { timeout: 60_000 });
102
+ if (generated.status !== 0) {
103
+ const detail = [generated.stderr, generated.stdout].filter(Boolean).join('\n').trim();
104
+ return { ok: false, detail: `messaging capability matrix drift:\n${detail || `tsx exited with ${generated.status}`}` };
105
+ }
96
106
  const stat = fs.statSync(abs);
97
107
  const ageDays = (Date.now() - stat.mtimeMs) / (1000 * 60 * 60 * 24);
98
- if (ageDays <= 7) return { ok: true, detail: `truth table ${ageDays.toFixed(2)}d old` };
108
+ if (ageDays <= 7) return { ok: true, detail: `truth table ${ageDays.toFixed(2)}d old; messaging matrix in sync` };
99
109
  const text = readFile(rel);
100
110
  const required = ['action-intent', 'target-resolver', 'answer-artifact', 'source-audit'];
101
111
  for (const term of required) {
@@ -103,7 +113,7 @@ const GATES = {
103
113
  return { ok: false, detail: `truth table stale (${ageDays.toFixed(1)}d) and missing ${term}` };
104
114
  }
105
115
  }
106
- return { ok: true, detail: `truth table ${ageDays.toFixed(1)}d old but matches required terms` };
116
+ return { ok: true, detail: `truth table ${ageDays.toFixed(1)}d old but matches required terms; messaging matrix in sync` };
107
117
  },
108
118
  },
109
119
  'mcp-scope-frozen': {
@@ -474,6 +484,50 @@ const GATES = {
474
484
  return { ok: true, detail: 'install-integrity + scriptless contract suites green' };
475
485
  },
476
486
  },
487
+ 'messaging-conformance': {
488
+ description: 'Telegram/Slack/Discord durable ingress and command contract suites + functional certification artifact',
489
+ check() {
490
+ const publicDocs = ['README.md', 'README.ko.md', 'README.ja.md', 'README.zh-CN.md', 'AGENTS.md'];
491
+ for (const rel of publicDocs) {
492
+ const abs = path.join(repoRoot, rel);
493
+ if (!fs.existsSync(abs)) continue;
494
+ const text = fs.readFileSync(abs, 'utf8');
495
+ if (/exactly[- ]once/i.test(text)) {
496
+ return { ok: false, detail: `${rel} claims exactly-once` };
497
+ }
498
+ if (/release-certified/i.test(text)) {
499
+ return { ok: false, detail: `${rel} claims release-certified` };
500
+ }
501
+ }
502
+ const suites = [
503
+ 'tests/unit/channel-contract-conformance.test.ts',
504
+ 'tests/unit/messaging-command-parity.test.ts',
505
+ 'tests/unit/messaging-durable-ingress.test.ts',
506
+ 'tests/unit/telegram-ingress-journal.test.ts',
507
+ 'tests/unit/discord-ingress-journal.test.ts',
508
+ 'tests/unit/ingress-generation.test.ts',
509
+ 'tests/unit/messaging-trace-context.test.ts',
510
+ 'tests/unit/messaging-metrics.test.ts',
511
+ ];
512
+ const missing = suites.filter((rel) => !fs.existsSync(path.join(repoRoot, rel)));
513
+ if (missing.length > 0) {
514
+ return { ok: false, detail: `channel fixture missing: ${missing.join(', ')}` };
515
+ }
516
+ const tests = run('npx', ['tsx', '--experimental-test-module-mocks', 'tests/run.mts', ...suites], { timeout: 180_000 });
517
+ if (tests.status !== 0) {
518
+ return { ok: false, detail: [tests.stdout, tests.stderr].filter(Boolean).join('\n').trim().slice(-800) };
519
+ }
520
+ const written = run(path.join('node_modules', '.bin', 'tsx'), ['scripts/write-messaging-certification.mts'], { timeout: 30_000 });
521
+ if (written.status !== 0) {
522
+ return { ok: false, detail: [written.stdout, written.stderr].filter(Boolean).join('\n').trim().slice(-800) };
523
+ }
524
+ const validated = run(path.join('node_modules', '.bin', 'tsx'), ['scripts/validate-messaging-certification.mts'], { timeout: 30_000 });
525
+ if (validated.status !== 0) {
526
+ return { ok: false, detail: [validated.stdout, validated.stderr].filter(Boolean).join('\n').trim().slice(-800) };
527
+ }
528
+ return { ok: true, detail: 'three-channel messaging suites green; functional-certified artifact valid' };
529
+ },
530
+ },
477
531
  'gate-docs': {
478
532
  description: 'structure/INDEX.md documents exactly the gates that exist, and each is npm-addressable',
479
533
  check() {
@@ -15,6 +15,11 @@ set -euo pipefail
15
15
 
16
16
  cd "$(dirname "$0")/.."
17
17
 
18
+ if ! command -v gh &>/dev/null; then
19
+ echo "❌ gh CLI is required but not found in PATH."
20
+ exit 1
21
+ fi
22
+
18
23
  # ─── Flag parsing ──────────────────────────────────────
19
24
  BUMP_KIND="patch"
20
25
  REQUIRE_EVIDENCE=false
@@ -203,6 +208,20 @@ else
203
208
  git push origin HEAD:preview
204
209
  fi
205
210
 
211
+ # ─── Dispatch publish workflow ─────────────────────────
212
+ RELEASE_SHA="$(git rev-parse HEAD)"
213
+ LIVE_SHA="$(git ls-remote origin refs/heads/preview | cut -f1)"
214
+
215
+ if [ -z "$LIVE_SHA" ] || [ "$LIVE_SHA" != "$RELEASE_SHA" ]; then
216
+ echo "❌ origin/preview moved after push (expected $RELEASE_SHA, got ${LIVE_SHA:-'(empty)'}). Aborting."
217
+ exit 1
218
+ fi
219
+
220
+ gh workflow run publish.yml --ref preview \
221
+ -f version="$VERSION" -f tag=preview -f expected-sha="$RELEASE_SHA" -f dry-run=false
222
+
223
+ echo "🚀 publish.yml workflow dispatched for preview v$VERSION (sha $RELEASE_SHA)"
224
+
206
225
  # ─── GitHub Prerelease stub ────────────────────────────
207
226
  echo "📋 Creating/updating GitHub prerelease stub..."
208
227
  RELEASE_BODY="## Preview Release v$VERSION
@@ -27,7 +27,7 @@ const installerSensitivePaths = [
27
27
  'scripts/audit-fresh-install-evidence.mjs',
28
28
  'scripts/verify-release-evidence.mjs',
29
29
  'scripts/require-release-evidence.mjs',
30
- 'scripts/release.sh',
30
+ 'scripts/promote-to-main.sh',
31
31
  'scripts/release-preview.sh',
32
32
  'tests/unit/cli-detect.test.ts',
33
33
  'tests/unit/install-sh-exec.test.ts',
@@ -46,6 +46,9 @@ function usage() {
46
46
 
47
47
  Options:
48
48
  --base-ref REF Compare installer-sensitive files against REF instead of the latest v* tag.
49
+ --changed-files-stdin
50
+ Read changed file paths from stdin and check whether any are installer-sensitive.
51
+ Cannot be combined with --base-ref.
49
52
  -h, --help Show this help.
50
53
 
51
54
  If installer-sensitive files changed, this script requires:
@@ -57,6 +60,7 @@ Then it runs scripts/verify-release-evidence.mjs before publish/release can cont
57
60
  }
58
61
 
59
62
  let baseRef = '';
63
+ let changedFilesStdin = false;
60
64
  const args = process.argv.slice(2);
61
65
  for (let i = 0; i < args.length; i += 1) {
62
66
  const arg = args[i];
@@ -64,6 +68,10 @@ for (let i = 0; i < args.length; i += 1) {
64
68
  usage();
65
69
  process.exit(0);
66
70
  }
71
+ if (arg === '--changed-files-stdin') {
72
+ changedFilesStdin = true;
73
+ continue;
74
+ }
67
75
  if (arg === '--base-ref') {
68
76
  baseRef = args[++i] || '';
69
77
  if (!baseRef) throw new Error('missing value for --base-ref');
@@ -72,6 +80,18 @@ for (let i = 0; i < args.length; i += 1) {
72
80
  throw new Error(`unknown option: ${arg}`);
73
81
  }
74
82
 
83
+ if (baseRef && changedFilesStdin) {
84
+ throw new Error('--changed-files-stdin cannot be combined with --base-ref');
85
+ }
86
+
87
+ function readChangedFilesFromStdin() {
88
+ const input = fs.readFileSync(0, 'utf8');
89
+ return input
90
+ .split(/\r?\n/)
91
+ .map((line) => line.replace(/\r$/, '').trim())
92
+ .filter((line) => line.length > 0);
93
+ }
94
+
75
95
  function git(args, options = {}) {
76
96
  return spawnSync('git', args, {
77
97
  cwd: repoRoot,
@@ -175,6 +195,20 @@ function shortHash(files) {
175
195
  return crypto.createHash('sha256').update(files.join('\n')).digest('hex').slice(0, 12);
176
196
  }
177
197
 
198
+ if (changedFilesStdin) {
199
+ const inputFiles = readChangedFilesFromStdin();
200
+ const matched = inputFiles.filter((file) => installerSensitivePaths.includes(file));
201
+ if (matched.length === 0) {
202
+ console.log('[release-evidence-required] SKIP no installer-sensitive paths in stdin');
203
+ process.exit(0);
204
+ }
205
+ console.error(`[release-evidence-required] CHANGED installer-sensitive files (${matched.length}, ${shortHash(matched)})`);
206
+ for (const file of matched) {
207
+ console.error(`- ${file}`);
208
+ }
209
+ process.exit(1);
210
+ }
211
+
178
212
  if (!isGitRepo()) {
179
213
  console.error('[release-evidence-required] FAIL');
180
214
  console.error('- Cannot determine installer-sensitive changes outside a git checkout.');