kijito-tools 0.2.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 (104) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +6 -0
  3. package/README.md +220 -0
  4. package/bin/cli.js +34 -0
  5. package/install.sh +161 -0
  6. package/package.json +50 -0
  7. package/providers/_shared/wake-core.mjs +358 -0
  8. package/providers/claude/CLAUDE.md.snippet +31 -0
  9. package/providers/claude/install.sh +186 -0
  10. package/providers/claude/scripts/arm-session.sh +66 -0
  11. package/providers/claude/scripts/claude-armed.sh +101 -0
  12. package/providers/claude/scripts/heartbeat-watchdog.sh +209 -0
  13. package/providers/claude/scripts/inbox-selftest.sh +277 -0
  14. package/providers/claude/scripts/kijito-persona-lib.sh +94 -0
  15. package/providers/claude/scripts/kijito-qa-pass.sh +12 -0
  16. package/providers/claude/scripts/lifecycle-lib.sh +167 -0
  17. package/providers/claude/scripts/myctx.sh +46 -0
  18. package/providers/claude/scripts/self-clear.sh +134 -0
  19. package/providers/claude/scripts/session-autosend.sh +85 -0
  20. package/providers/claude/scripts/session-catchup-hint.sh +330 -0
  21. package/providers/claude/scripts/statusline-context.sh +67 -0
  22. package/providers/claude/skills/kijito-qa-memory/SKILL.md +110 -0
  23. package/providers/claude/skills/kijito-recall/SKILL.md +53 -0
  24. package/providers/claude/skills/kijito-start/SKILL.md +122 -0
  25. package/providers/claude/wiring/README.md +93 -0
  26. package/providers/claude/wiring/kijito-heartbeat@.service +21 -0
  27. package/providers/codex/README.md +75 -0
  28. package/providers/codex/codex-kijito-parity-plan.md +265 -0
  29. package/providers/codex/docs-codex-setup.md +64 -0
  30. package/providers/codex/install.mjs +184 -0
  31. package/providers/codex/n0-capability-probe-protocol.md +472 -0
  32. package/providers/codex/n0-harness/README.md +57 -0
  33. package/providers/codex/n0-harness/cli.mjs +58 -0
  34. package/providers/codex/n0-harness/evidence-manifest.mjs +77 -0
  35. package/providers/codex/n0-harness/fixture.mjs +279 -0
  36. package/providers/codex/n0-harness/lib.mjs +141 -0
  37. package/providers/codex/n0-harness/manifest.mjs +36 -0
  38. package/providers/codex/n0-harness/oracle.mjs +464 -0
  39. package/providers/codex/n0-harness/parser.mjs +200 -0
  40. package/providers/codex/n0-harness/prompt.mjs +43 -0
  41. package/providers/codex/n0-harness/snapshot.mjs +120 -0
  42. package/providers/codex/n0-harness/specimen.mjs +37 -0
  43. package/providers/codex/notify/kijito-notify-count.mjs +117 -0
  44. package/providers/codex/notify/kijito-notify-count.test.mjs +76 -0
  45. package/providers/codex/plans/gate4-battery-log.md +184 -0
  46. package/providers/codex/plans/gate4-battery-protocol.md +43 -0
  47. package/providers/codex/plans/hive-user-first-plan-QA.md +34 -0
  48. package/providers/codex/plans/hive-user-first-plan.md +247 -0
  49. package/providers/codex/release-manifest.json +53 -0
  50. package/providers/codex/same-chat-continuation-plan-gate.md +235 -0
  51. package/providers/codex/same-chat-continuation-plan.md +458 -0
  52. package/providers/codex/skills/kijito-qa-memory/SKILL.md +193 -0
  53. package/providers/codex/skills/kijito-qa-memory/agents/openai.yaml +4 -0
  54. package/providers/codex/skills/kijito-recall/SKILL.md +53 -0
  55. package/providers/codex/skills/kijito-start/SKILL.md +173 -0
  56. package/providers/codex/skills/kijito-start/agents/openai.yaml +4 -0
  57. package/providers/codex/test/n0-cli-refusal.test.mjs +31 -0
  58. package/providers/codex/test/n0-counterexample-matrix.test.mjs +118 -0
  59. package/providers/codex/test/n0-guard-census-core.mjs +1372 -0
  60. package/providers/codex/test/n0-guard-census.json +35479 -0
  61. package/providers/codex/test/n0-guard-census.mjs +31 -0
  62. package/providers/codex/test/n0-guard-census.test.mjs +374 -0
  63. package/providers/codex/test/n0-guard-counterexamples.mjs +1068 -0
  64. package/providers/codex/test/n0-guard-independent-projection.mjs +70 -0
  65. package/providers/codex/test/n0-guard-independent-projection.test.mjs +53 -0
  66. package/providers/codex/test/n0-guard-manifest-author.mjs +424 -0
  67. package/providers/codex/test/n0-guard-mutation-operators.test.mjs +244 -0
  68. package/providers/codex/test/n0-guard-mutation-runner.mjs +599 -0
  69. package/providers/codex/test/n0-guard-remainder-classes.test.mjs +314 -0
  70. package/providers/codex/test/n0-guard-sanitizer-pairs.test.mjs +68 -0
  71. package/providers/codex/test/n0-guard-shared-context-predicates.test.mjs +84 -0
  72. package/providers/codex/test/n0-harness.test.mjs +553 -0
  73. package/providers/codex/test/n0-notable-regressions.test.mjs +70 -0
  74. package/providers/codex/test/n0-subject-boundary.mjs +97 -0
  75. package/providers/codex/test/n0-subject-boundary.test.mjs +72 -0
  76. package/providers/codex/test/same-chat-plan-preflight.sh +167 -0
  77. package/providers/codex/tools/refresh-manifest.mjs +81 -0
  78. package/providers/codex/wake-helper/TRANSPORT-NOTES.md +25 -0
  79. package/providers/codex/wake-helper/integration.test.mjs +285 -0
  80. package/providers/codex/wake-helper/kijito-wake-helper.mjs +488 -0
  81. package/providers/codex/wake-helper/kijito-wake-helper.test.mjs +128 -0
  82. package/providers/codex/wake-helper/mock-daemon.mjs +64 -0
  83. package/providers/codex/wake-helper/status-probe.mjs +45 -0
  84. package/providers/codex/wake-helper/ws-uds.mjs +153 -0
  85. package/providers/monitor/.github/workflows/publish-npm.yml +38 -0
  86. package/providers/monitor/.github/workflows/publish-pypi.yml +30 -0
  87. package/providers/monitor/CHANGELOG.md +465 -0
  88. package/providers/monitor/IMPORT-PROVENANCE.md +68 -0
  89. package/providers/monitor/LICENSE +202 -0
  90. package/providers/monitor/NOTICE +7 -0
  91. package/providers/monitor/OPAQUE-OUTPUT-ENFORCEMENT.md +45 -0
  92. package/providers/monitor/README.md +571 -0
  93. package/providers/monitor/RELEASING.md +125 -0
  94. package/providers/monitor/arm-hive-monitor.sh +13 -0
  95. package/providers/monitor/bin/cli.js +43 -0
  96. package/providers/monitor/com.kijito.inbox-monitor.plist.template +59 -0
  97. package/providers/monitor/docs/DESIGN.md +984 -0
  98. package/providers/monitor/kijito-inbox-monitor@.service.template +95 -0
  99. package/providers/monitor/kijito_inbox_monitor.py +3666 -0
  100. package/providers/monitor/package.json +35 -0
  101. package/providers/monitor/pyproject.toml +42 -0
  102. package/providers/monitor/scripts/mutation-check.py +399 -0
  103. package/providers/monitor/scripts/prepublish-gate.sh +284 -0
  104. package/providers/monitor/test_kijito_monitor.py +5609 -0
@@ -0,0 +1,64 @@
1
+ // Minimal mock of the codex app-server DAEMON control socket for helper integration tests.
2
+ // Speaks the measured 0.147 transport — WEBSOCKET over the unix socket (handshake, then
3
+ // JSON-RPC as one message per text frame) — and the measured methods: initialize,
4
+ // thread/resume, turn/start; pushes thread/status/changed notifications on demand.
5
+ // Test-only — the gate-4 battery runs against the REAL daemon; this exists so the run loop's
6
+ // logic (defer-until-idle, loud exits, gasp) is proven on the same transport first.
7
+ import net from "node:net";
8
+ import fs from "node:fs";
9
+ import { acceptWsUds } from "./ws-uds.mjs";
10
+
11
+ export class MockDaemon {
12
+ constructor(sockPath, { threadId = "T1", initialStatus = "idle", failResume = false } = {}) {
13
+ this.sockPath = sockPath;
14
+ this.threadId = threadId;
15
+ this.status = initialStatus;
16
+ this.failResume = failResume;
17
+ this.calls = [];
18
+ this.conns = new Set();
19
+ this.turnSeq = 0;
20
+ }
21
+
22
+ listen() {
23
+ try { fs.unlinkSync(this.sockPath); } catch { /* fresh */ }
24
+ this.server = net.createServer(async (sock) => {
25
+ const conn = await acceptWsUds(sock, {
26
+ onText: (text, c) => this.handle(c, JSON.parse(text)),
27
+ onClose: () => this.conns.delete(conn),
28
+ }).catch(() => null);
29
+ if (conn) this.conns.add(conn);
30
+ });
31
+ return new Promise((resolve) => this.server.listen(this.sockPath, resolve));
32
+ }
33
+
34
+ handle(conn, msg) {
35
+ this.calls.push(msg);
36
+ if (msg.id === undefined) return; // notification (e.g. initialized)
37
+ const reply = (result) => conn.sendText(JSON.stringify({ jsonrpc: "2.0", id: msg.id, result }));
38
+ const fail = (message) => conn.sendText(JSON.stringify({ jsonrpc: "2.0", id: msg.id, error: { code: -32000, message } }));
39
+ if (msg.method === "initialize") return reply({});
40
+ if (msg.method === "thread/resume") {
41
+ if (this.failResume) return fail("thread not found");
42
+ return reply({ thread: { id: this.threadId, status: { type: this.status } } });
43
+ }
44
+ if (msg.method === "turn/start") {
45
+ if (this.status !== "idle") return fail("thread busy");
46
+ this.turnSeq += 1;
47
+ return reply({ turn: { id: `turn-${this.turnSeq}` } });
48
+ }
49
+ return fail(`unknown method ${msg.method}`);
50
+ }
51
+
52
+ setStatus(type) {
53
+ this.status = type;
54
+ const note = JSON.stringify({ jsonrpc: "2.0", method: "thread/status/changed", params: { threadId: this.threadId, status: { type } } });
55
+ for (const conn of this.conns) conn.sendText(note);
56
+ }
57
+
58
+ turnStarts() { return this.calls.filter((c) => c.method === "turn/start"); }
59
+
60
+ close() {
61
+ for (const conn of this.conns) conn.destroy();
62
+ return new Promise((resolve) => this.server.close(resolve));
63
+ }
64
+ }
@@ -0,0 +1,45 @@
1
+ // One-shot leg-(ii) instrument: what does a SECOND client see when it resumes a thread
2
+ // mid-turn? Prints the status thread/resume returns, plus any thread/status/changed
3
+ // notifications observed in a short listen window. Read-only.
4
+ import { connectWsUds } from "./ws-uds.mjs";
5
+
6
+ const [sock, threadId, listenMsArg] = process.argv.slice(2);
7
+ if (!sock || !threadId) { process.stderr.write("usage: status-probe.mjs <sock> <threadId> [listenMs]\n"); process.exit(2); }
8
+ const listenMs = Number(listenMsArg ?? 0);
9
+
10
+ const pending = new Map();
11
+ let nextId = 1;
12
+ let conn;
13
+ const notes = [];
14
+
15
+ function send(method, params) {
16
+ return new Promise((resolve, reject) => {
17
+ const id = nextId++;
18
+ pending.set(id, { resolve, reject });
19
+ conn.sendText(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
20
+ setTimeout(() => { if (pending.has(id)) { pending.delete(id); reject(new Error(`timeout:${method}`)); } }, 10_000);
21
+ });
22
+ }
23
+
24
+ conn = await connectWsUds(sock, {
25
+ onText: (text) => {
26
+ let msg; try { msg = JSON.parse(text); } catch { return; }
27
+ if (msg.id !== undefined && pending.has(msg.id)) {
28
+ const p = pending.get(msg.id); pending.delete(msg.id);
29
+ if (msg.error) p.reject(new Error(msg.error.message ?? "rpc-error")); else p.resolve(msg.result);
30
+ } else if (msg.method === "thread/status/changed") {
31
+ notes.push({ at: new Date().toISOString(), params: msg.params });
32
+ }
33
+ },
34
+ onClose: () => {},
35
+ });
36
+
37
+ await send("initialize", { clientInfo: { name: "g4-status-probe", title: "g4 status probe", version: "1" }, capabilities: { experimentalApi: true } });
38
+ conn.sendText(JSON.stringify({ jsonrpc: "2.0", method: "initialized" }));
39
+ const opened = await send("thread/resume", { threadId });
40
+ process.stdout.write(JSON.stringify({ at: new Date().toISOString(), resumeStatus: opened?.thread?.status ?? null }) + "\n");
41
+ if (listenMs > 0) {
42
+ await new Promise((r) => setTimeout(r, listenMs));
43
+ process.stdout.write(JSON.stringify({ notificationsObserved: notes }) + "\n");
44
+ }
45
+ process.exit(0);
@@ -0,0 +1,153 @@
1
+ // Minimal RFC 6455 WebSocket over a unix-domain socket — zero dependencies, exactly what the
2
+ // codex app-server daemon control socket speaks (measured at source, rust-v0.147.0:
3
+ // tokio_tungstenite accept_async on the UnixStream; clients dial with handshake URL
4
+ // ws://localhost/rpc and exchange JSON-RPC as text frames; filesystem 0600 is the auth).
5
+ //
6
+ // Scope is deliberately narrow: text frames, unfragmented (our JSON-RPC messages are small),
7
+ // ping->pong, clean close. Anything outside that surface errors loudly rather than guessing.
8
+ import crypto from "node:crypto";
9
+ import net from "node:net";
10
+
11
+ const WS_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
12
+ export const HANDSHAKE_PATH = "/rpc";
13
+
14
+ function acceptKeyFor(key) {
15
+ return crypto.createHash("sha1").update(key + WS_MAGIC).digest("base64");
16
+ }
17
+
18
+ // ---------- framing ----------
19
+
20
+ export function encodeFrame(opcode, payload, mask) {
21
+ const len = payload.length;
22
+ let header;
23
+ if (len < 126) header = Buffer.from([0x80 | opcode, (mask ? 0x80 : 0) | len]);
24
+ else if (len < 65_536) {
25
+ header = Buffer.alloc(4);
26
+ header[0] = 0x80 | opcode; header[1] = (mask ? 0x80 : 0) | 126; header.writeUInt16BE(len, 2);
27
+ } else {
28
+ header = Buffer.alloc(10);
29
+ header[0] = 0x80 | opcode; header[1] = (mask ? 0x80 : 0) | 127; header.writeBigUInt64BE(BigInt(len), 2);
30
+ }
31
+ if (!mask) return Buffer.concat([header, payload]);
32
+ const key = crypto.randomBytes(4);
33
+ const masked = Buffer.from(payload);
34
+ for (let i = 0; i < masked.length; i += 1) masked[i] ^= key[i % 4];
35
+ return Buffer.concat([header, key, masked]);
36
+ }
37
+
38
+ // Incremental decoder: feed bytes, yields {opcode, payload} frames.
39
+ export class FrameDecoder {
40
+ constructor() { this.buf = Buffer.alloc(0); }
41
+ push(chunk) {
42
+ this.buf = Buffer.concat([this.buf, chunk]);
43
+ const frames = [];
44
+ for (;;) {
45
+ if (this.buf.length < 2) break;
46
+ const fin = (this.buf[0] & 0x80) !== 0;
47
+ const opcode = this.buf[0] & 0x0f;
48
+ const masked = (this.buf[1] & 0x80) !== 0;
49
+ let len = this.buf[1] & 0x7f;
50
+ let off = 2;
51
+ if (len === 126) { if (this.buf.length < 4) break; len = this.buf.readUInt16BE(2); off = 4; }
52
+ else if (len === 127) {
53
+ if (this.buf.length < 10) break;
54
+ const big = this.buf.readBigUInt64BE(2);
55
+ if (big > BigInt(64 * 1024 * 1024)) throw new Error("ws frame too large");
56
+ len = Number(big); off = 10;
57
+ }
58
+ const maskLen = masked ? 4 : 0;
59
+ if (this.buf.length < off + maskLen + len) break;
60
+ if (!fin) throw new Error("fragmented ws frames unsupported");
61
+ let payload = this.buf.subarray(off + maskLen, off + maskLen + len);
62
+ if (masked) {
63
+ const key = this.buf.subarray(off, off + 4);
64
+ payload = Buffer.from(payload);
65
+ for (let i = 0; i < payload.length; i += 1) payload[i] ^= key[i % 4];
66
+ }
67
+ frames.push({ opcode, payload: Buffer.from(payload) });
68
+ this.buf = this.buf.subarray(off + maskLen + len);
69
+ }
70
+ return frames;
71
+ }
72
+ }
73
+
74
+ // ---------- shared per-connection wiring ----------
75
+
76
+ function wire(sock, { isClient, onText, onClose }) {
77
+ const decoder = new FrameDecoder();
78
+ const conn = {
79
+ sendText: (text) => sock.write(encodeFrame(0x1, Buffer.from(text, "utf8"), isClient)),
80
+ close: () => { try { sock.write(encodeFrame(0x8, Buffer.alloc(0), isClient)); } catch { /* gone */ } sock.end(); },
81
+ destroy: () => sock.destroy(),
82
+ };
83
+ sock.on("data", (chunk) => {
84
+ let frames;
85
+ try { frames = decoder.push(chunk); } catch (error) { sock.destroy(error); return; }
86
+ for (const frame of frames) {
87
+ if (frame.opcode === 0x1) onText(frame.payload.toString("utf8"), conn);
88
+ else if (frame.opcode === 0x9) sock.write(encodeFrame(0xa, frame.payload, isClient)); // ping->pong
89
+ else if (frame.opcode === 0x8) { try { sock.write(encodeFrame(0x8, Buffer.alloc(0), isClient)); } catch { /* gone */ } sock.end(); }
90
+ // 0xa pong and binary 0x2 ignored — nothing on this surface sends them meaningfully.
91
+ }
92
+ });
93
+ sock.on("close", () => onClose?.());
94
+ sock.on("error", () => { /* close event follows; onClose reports */ });
95
+ return conn;
96
+ }
97
+
98
+ // ---------- client ----------
99
+
100
+ export function connectWsUds(sockPath, { path = HANDSHAKE_PATH, timeoutMs = 10_000, onText, onClose } = {}) {
101
+ return new Promise((resolve, reject) => {
102
+ const sock = net.createConnection(sockPath);
103
+ const timer = setTimeout(() => { sock.destroy(); reject(new Error("ws-handshake-timeout")); }, timeoutMs);
104
+ sock.once("error", (error) => { clearTimeout(timer); reject(error); });
105
+ sock.once("connect", () => {
106
+ const key = crypto.randomBytes(16).toString("base64");
107
+ sock.write(`GET ${path} HTTP/1.1\r\nHost: localhost\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: ${key}\r\nSec-WebSocket-Version: 13\r\n\r\n`);
108
+ let head = "";
109
+ const onHead = (chunk) => {
110
+ head += chunk.toString("latin1");
111
+ const end = head.indexOf("\r\n\r\n");
112
+ if (end === -1) {
113
+ if (head.length > 16_384) { sock.destroy(); clearTimeout(timer); reject(new Error("ws-handshake-oversized")); }
114
+ return;
115
+ }
116
+ sock.removeListener("data", onHead);
117
+ const header = head.slice(0, end);
118
+ const rest = Buffer.from(head.slice(end + 4), "latin1");
119
+ if (!/^HTTP\/1\.1 101 /.test(header)) { sock.destroy(); clearTimeout(timer); reject(new Error(`ws-handshake-rejected: ${header.split("\r\n")[0]}`)); return; }
120
+ const accept = /sec-websocket-accept: *([^\r\n]+)/i.exec(header)?.[1];
121
+ if (accept !== acceptKeyFor(key)) { sock.destroy(); clearTimeout(timer); reject(new Error("ws-accept-mismatch")); return; }
122
+ clearTimeout(timer);
123
+ const conn = wire(sock, { isClient: true, onText, onClose });
124
+ if (rest.length) sock.emit("data", rest);
125
+ resolve(conn);
126
+ };
127
+ sock.on("data", onHead);
128
+ });
129
+ });
130
+ }
131
+
132
+ // ---------- server-accept (mock daemon / tests) ----------
133
+
134
+ export function acceptWsUds(sock, { onText, onClose }) {
135
+ return new Promise((resolve, reject) => {
136
+ let head = "";
137
+ const onHead = (chunk) => {
138
+ head += chunk.toString("latin1");
139
+ const end = head.indexOf("\r\n\r\n");
140
+ if (end === -1) return;
141
+ sock.removeListener("data", onHead);
142
+ const key = /sec-websocket-key: *([^\r\n]+)/i.exec(head.slice(0, end))?.[1];
143
+ if (!key) { sock.destroy(); reject(new Error("ws-server: no key")); return; }
144
+ sock.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${acceptKeyFor(key)}\r\n\r\n`);
145
+ const rest = Buffer.from(head.slice(end + 4), "latin1");
146
+ const conn = wire(sock, { isClient: false, onText, onClose });
147
+ if (rest.length) sock.emit("data", rest);
148
+ resolve(conn);
149
+ };
150
+ sock.on("data", onHead);
151
+ sock.once("error", reject);
152
+ });
153
+ }
@@ -0,0 +1,38 @@
1
+ name: publish-npm
2
+
3
+ # Publishes the kijito-inbox-monitor npm SIGNPOST package on a version tag, using Trusted
4
+ # Publishing (OIDC). The npm package is not a real installer: it points users at the Python
5
+ # package on PyPI (see bin/cli.js). No NPM_TOKEN is stored; npm exchanges a short-lived
6
+ # workflow credential and attaches provenance automatically.
7
+ #
8
+ # Canonical form per docs.npmjs.com/trusted-publishers: Node 24 (bundles npm >= 11.5.1, which
9
+ # trusted publishing requires) + registry-url + `npm publish` with NO NODE_AUTH_TOKEN. There
10
+ # are zero deps and no lockfile, so we skip `npm ci`.
11
+ #
12
+ # Trusted Publisher config on npmjs.com (package -> Settings -> Trusted Publisher) must match
13
+ # EXACTLY (case-sensitive): Organization/user "KijitoAI", Repository "kijito-inbox-monitor",
14
+ # Workflow filename "publish-npm.yml" (filename only, WITH .yml, not a path), Environment blank,
15
+ # and Allowed actions must include "npm publish". A mismatch on any field fails with E404
16
+ # "could not be found or you do not have permission".
17
+ on:
18
+ push:
19
+ tags: ["v*"]
20
+ workflow_dispatch:
21
+
22
+ permissions:
23
+ id-token: write # required for Trusted Publishing
24
+ contents: read
25
+
26
+ jobs:
27
+ publish:
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ - uses: actions/setup-node@v4
32
+ with:
33
+ node-version: "24" # bundles npm >= 11.5.1, required for OIDC trusted publishing
34
+ registry-url: "https://registry.npmjs.org"
35
+ - run: npm --version
36
+ # Zero runtime deps and no lockfile, so no `npm ci`. There is no postinstall hook
37
+ # (npm v12 disables install lifecycle scripts by default, and that pattern is the worm).
38
+ - run: npm publish --access public
@@ -0,0 +1,30 @@
1
+ name: publish-pypi
2
+
3
+ # Publishes kijito-inbox-monitor to PyPI on a version tag, using Trusted Publishing (OIDC).
4
+ # No PyPI API token is stored: PyPI exchanges a short-lived OIDC credential and the publish
5
+ # action attaches PEP 740 attestations automatically. Configure the trusted publisher once
6
+ # on pypi.org first: pypi.org -> the project -> Publishing -> add this repo + this workflow
7
+ # file + the "pypi" environment.
8
+ on:
9
+ push:
10
+ tags: ["v*"]
11
+ workflow_dispatch:
12
+
13
+ permissions:
14
+ id-token: write # required for Trusted Publishing
15
+ contents: read
16
+
17
+ jobs:
18
+ publish:
19
+ runs-on: ubuntu-latest
20
+ environment:
21
+ name: pypi
22
+ url: https://pypi.org/p/kijito-inbox-monitor
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - uses: actions/setup-python@v5
26
+ with:
27
+ python-version: "3.12"
28
+ - run: pipx run build # builds sdist + wheel into dist/
29
+ - run: pipx run twine check dist/*
30
+ - uses: pypa/gh-action-pypi-publish@release/v1 # v1.11+ attaches attestations by default