@kleroterion/koine 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/dist/index.d.ts +157 -0
- package/dist/index.js +355 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Koine
|
|
2
|
+
|
|
3
|
+
> *The common tongue.* Shared building blocks for [Boule](https://github.com/kleroterionlabs/boule) and [Praktor](https://github.com/kleroterionlabs/praktor).
|
|
4
|
+
|
|
5
|
+
**Koine** (Greek κοινή, "the common [dialect]" — the shared standard Greek everyone spoke) is the library both Kleroterion tools depend on, so the contract between them can never drift. Boule writes the artifacts; Praktor reads them; **koine** is the single definition they share.
|
|
6
|
+
|
|
7
|
+
## What it provides
|
|
8
|
+
|
|
9
|
+
| Module | Exports |
|
|
10
|
+
|---|---|
|
|
11
|
+
| **taxonomy** | `ISSUE_TYPE_NAMES`, `kindLabel`, `STATUS_LABELS`, `PRIORITY_LABELS`, `OPERATIONAL_LABELS`, `PRAKTOR_LABELS`, `PROJECT_FIELDS`, `STATUS_OPTIONS`, `DISCUSSION_CATEGORIES`, `allBootstrapLabels()` — the GitHub label/field contract |
|
|
12
|
+
| **identity** | `bouleId`, `contentHash`, `idLabel`, `parseBouleBlock`/`renderBouleBlock`/`withBouleBlock`/`stripBouleBlock`, `parseVerifies` — the `boule:v1` block + content addressing |
|
|
13
|
+
| **github** | `createGitHubClient` (octokit + throttling + retry/backoff), `mintToken` (PAT or GitHub App), `AuthConfig`/`GitHubAuth` |
|
|
14
|
+
| **agent** | `runQuery` — the resilient Claude Agent SDK run-loop (survives subprocess teardown noise) |
|
|
15
|
+
| **security** | `cleanOutbound`, `scrubSecrets`, `sanitizeMentions` — redact credentials + neutralize @-mentions before anything reaches GitHub |
|
|
16
|
+
| **observability** | `createLogger` — structured pino logging with credential redaction |
|
|
17
|
+
|
|
18
|
+
## Design notes
|
|
19
|
+
|
|
20
|
+
- **Identity-agnostic.** koine never reads `process.env`. Each tool resolves its *own* credentials (Boule's App, Praktor's App) into an `AuthConfig` and passes it to `createGitHubClient`.
|
|
21
|
+
- **On-wire strings are stable.** Label values stay `boule:*` / `kind:*` / `status:*` — they're the format already on live GitHub issues. koine is their code home, not a rename.
|
|
22
|
+
- `@anthropic-ai/claude-agent-sdk` is a **peer dependency** — the consuming tool owns the version.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { createGitHubClient, parseBouleBlock, kindLabel, runQuery } from "@kleroterion/koine";
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## License
|
|
31
|
+
|
|
32
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Octokit } from '@octokit/rest';
|
|
2
|
+
import { Logger } from 'pino';
|
|
3
|
+
export { Logger } from 'pino';
|
|
4
|
+
import { Options } from '@anthropic-ai/claude-agent-sdk';
|
|
5
|
+
|
|
6
|
+
type ArtifactKind = "design" | "requirement" | "competitor" | "market" | "gap" | "epic" | "feature" | "task" | "spike";
|
|
7
|
+
/** Content-addressable fingerprint of an artifact body (e.g. "sha256:abcd1234..."). */
|
|
8
|
+
type Fingerprint = string;
|
|
9
|
+
|
|
10
|
+
declare const ISSUE_TYPE_NAMES: {
|
|
11
|
+
readonly design: "Design";
|
|
12
|
+
readonly requirement: "Requirement";
|
|
13
|
+
readonly competitor: "Competitor";
|
|
14
|
+
readonly market: "Market";
|
|
15
|
+
readonly gap: "Gap";
|
|
16
|
+
readonly epic: "Epic";
|
|
17
|
+
readonly feature: "Feature";
|
|
18
|
+
readonly task: "Task";
|
|
19
|
+
readonly spike: "Spike";
|
|
20
|
+
};
|
|
21
|
+
/** Fallback kind label used when native Issue Types are unavailable. */
|
|
22
|
+
declare const kindLabel: (kind: ArtifactKind) => string;
|
|
23
|
+
declare const OPERATIONAL_LABELS: {
|
|
24
|
+
readonly managed: "boule:managed";
|
|
25
|
+
readonly needsHuman: "boule:needs-human";
|
|
26
|
+
readonly superseded: "boule:superseded";
|
|
27
|
+
/** Kill-switch: an OPEN issue carrying this label halts all autonomous writes. */
|
|
28
|
+
readonly halt: "boule:halt";
|
|
29
|
+
};
|
|
30
|
+
/** An artifact's ACCEPTANCE lifecycle, carried on the Issue as a label. */
|
|
31
|
+
declare const STATUS_LABELS: readonly ["status:draft", "status:needs-review", "status:accepted", "status:superseded"];
|
|
32
|
+
type StatusLabel = (typeof STATUS_LABELS)[number];
|
|
33
|
+
declare const PRIORITY_LABELS: readonly ["priority:must", "priority:should", "priority:could", "priority:wont"];
|
|
34
|
+
/** Praktor's own progress labels — namespaced so they never collide with Boule's lifecycle. */
|
|
35
|
+
declare const PRAKTOR_LABELS: {
|
|
36
|
+
readonly inProgress: "praktor:in-progress";
|
|
37
|
+
readonly done: "praktor:done";
|
|
38
|
+
readonly blocked: "praktor:blocked";
|
|
39
|
+
};
|
|
40
|
+
/** Projects v2 custom field names (canonical keys for field values). */
|
|
41
|
+
declare const PROJECT_FIELDS: {
|
|
42
|
+
readonly status: "Status";
|
|
43
|
+
readonly kind: "Kind";
|
|
44
|
+
readonly priority: "Priority";
|
|
45
|
+
readonly rice: "RICE";
|
|
46
|
+
readonly wsjf: "WSJF";
|
|
47
|
+
readonly moscow: "MoSCoW";
|
|
48
|
+
readonly iteration: "Iteration";
|
|
49
|
+
};
|
|
50
|
+
/** Projects v2 Status column options (the board workflow state). */
|
|
51
|
+
declare const STATUS_OPTIONS: readonly ["Triage", "In Design", "In Review", "Ready", "In Progress", "Blocked", "Done"];
|
|
52
|
+
declare const DISCUSSION_CATEGORIES: {
|
|
53
|
+
readonly dailyStatus: "Daily Status";
|
|
54
|
+
readonly handoff: "Agent Handoffs";
|
|
55
|
+
readonly designReview: "Design Review";
|
|
56
|
+
};
|
|
57
|
+
/** Every repo label the tools bootstrap (kinds + operational + status + priority). */
|
|
58
|
+
declare function allBootstrapLabels(): string[];
|
|
59
|
+
|
|
60
|
+
interface BouleBlock {
|
|
61
|
+
kind: ArtifactKind;
|
|
62
|
+
bouleId: string;
|
|
63
|
+
contentHash: Fingerprint;
|
|
64
|
+
parent?: string;
|
|
65
|
+
runId?: string;
|
|
66
|
+
generatedBy?: string;
|
|
67
|
+
}
|
|
68
|
+
/** Stable, content-independent slug. Same natural key ⇒ same id (NOT random). */
|
|
69
|
+
declare function bouleId(kind: ArtifactKind, naturalKey: string): string;
|
|
70
|
+
/** sha256 over the normalized semantic body, EXCLUDING any boule block. */
|
|
71
|
+
declare function contentHash(body: string): Fingerprint;
|
|
72
|
+
/** A unique, deterministic dedup label (hashed to stay under GitHub's 50-char label limit). */
|
|
73
|
+
declare function idLabel(id: string): string;
|
|
74
|
+
declare function renderBouleBlock(b: BouleBlock): string;
|
|
75
|
+
/** Append the block to a body, recomputing the hash over the body sans-block. */
|
|
76
|
+
declare function withBouleBlock(body: string, meta: Omit<BouleBlock, "contentHash">): string;
|
|
77
|
+
declare function parseBouleBlock(body: string): BouleBlock | null;
|
|
78
|
+
declare function stripBouleBlock(body: string): string;
|
|
79
|
+
/** Requirement issue numbers referenced by a `Verifies: #110, #112` link line (Task → Requirement). */
|
|
80
|
+
declare function parseVerifies(body: string): number[];
|
|
81
|
+
|
|
82
|
+
interface ScrubResult {
|
|
83
|
+
clean: string;
|
|
84
|
+
found: string[];
|
|
85
|
+
}
|
|
86
|
+
/** Replace any credential-looking substrings with `[REDACTED:<kind>]`; report the kinds found. */
|
|
87
|
+
declare function scrubSecrets(text: string): ScrubResult;
|
|
88
|
+
|
|
89
|
+
interface MentionResult {
|
|
90
|
+
clean: string;
|
|
91
|
+
stripped: string[];
|
|
92
|
+
}
|
|
93
|
+
declare function sanitizeMentions(text: string): MentionResult;
|
|
94
|
+
|
|
95
|
+
interface Outbound {
|
|
96
|
+
clean: string;
|
|
97
|
+
secrets: string[];
|
|
98
|
+
mentions: string[];
|
|
99
|
+
}
|
|
100
|
+
declare function cleanOutbound(text: string): Outbound;
|
|
101
|
+
|
|
102
|
+
type GitHubAuth = {
|
|
103
|
+
kind: "pat";
|
|
104
|
+
token: string;
|
|
105
|
+
} | {
|
|
106
|
+
kind: "app";
|
|
107
|
+
appId: string;
|
|
108
|
+
installationId: string;
|
|
109
|
+
privateKey: string;
|
|
110
|
+
};
|
|
111
|
+
interface AuthConfig {
|
|
112
|
+
github: GitHubAuth;
|
|
113
|
+
}
|
|
114
|
+
/** Decode a base64-or-PEM private key (CI usually stores a single-line base64 blob). */
|
|
115
|
+
declare function decodePrivateKey(raw: string): string;
|
|
116
|
+
/** Mint a usable token: a PAT passes through; an App mints a short-lived installation token. */
|
|
117
|
+
declare function mintToken(auth: GitHubAuth): Promise<string>;
|
|
118
|
+
|
|
119
|
+
type OpKind = "read" | "write";
|
|
120
|
+
interface GitHubClient {
|
|
121
|
+
rest: Octokit;
|
|
122
|
+
/** Run a REST call through the retry/backoff gate. */
|
|
123
|
+
withRest<T>(op: OpKind, fn: (o: Octokit) => Promise<T>): Promise<T>;
|
|
124
|
+
/** Run a GraphQL document through the gate. */
|
|
125
|
+
graphql<T = unknown>(op: OpKind, query: string, vars?: Record<string, unknown>): Promise<T>;
|
|
126
|
+
}
|
|
127
|
+
interface ClientOptions {
|
|
128
|
+
maxRetries?: number;
|
|
129
|
+
}
|
|
130
|
+
declare function createGitHubClient(auth: AuthConfig, log: Logger, opts?: ClientOptions): Promise<GitHubClient>;
|
|
131
|
+
|
|
132
|
+
type StopReason = "success" | "error_max_turns" | "error_max_budget_usd" | "error_during_execution";
|
|
133
|
+
interface RunOutcome {
|
|
134
|
+
ok: boolean;
|
|
135
|
+
stopReason: StopReason;
|
|
136
|
+
sessionId: string;
|
|
137
|
+
numTurns: number;
|
|
138
|
+
costUsd: number;
|
|
139
|
+
modelUsage: Record<string, unknown>;
|
|
140
|
+
errors: string[];
|
|
141
|
+
}
|
|
142
|
+
interface RunHooks {
|
|
143
|
+
log: Logger;
|
|
144
|
+
/** Called once with the SDK session id (at init) — e.g. to checkpoint for resume. */
|
|
145
|
+
onSession?: (sessionId: string) => void;
|
|
146
|
+
}
|
|
147
|
+
/** Drive one query() to completion, returning a normalized outcome. Never throws on transport noise. */
|
|
148
|
+
declare function runQuery(prompt: string, options: Options, hooks: RunHooks): Promise<RunOutcome>;
|
|
149
|
+
|
|
150
|
+
interface LoggerOptions {
|
|
151
|
+
level?: string;
|
|
152
|
+
service?: string;
|
|
153
|
+
runId?: string;
|
|
154
|
+
}
|
|
155
|
+
declare function createLogger(opts?: LoggerOptions): Logger;
|
|
156
|
+
|
|
157
|
+
export { type ArtifactKind, type AuthConfig, type BouleBlock, type ClientOptions, DISCUSSION_CATEGORIES, type Fingerprint, type GitHubAuth, type GitHubClient, ISSUE_TYPE_NAMES, type LoggerOptions, type MentionResult, OPERATIONAL_LABELS, type Outbound, PRAKTOR_LABELS, PRIORITY_LABELS, PROJECT_FIELDS, type RunHooks, type RunOutcome, STATUS_LABELS, STATUS_OPTIONS, type ScrubResult, type StatusLabel, type StopReason, allBootstrapLabels, bouleId, cleanOutbound, contentHash, createGitHubClient, createLogger, decodePrivateKey, idLabel, kindLabel, mintToken, parseBouleBlock, parseVerifies, renderBouleBlock, runQuery, sanitizeMentions, scrubSecrets, stripBouleBlock, withBouleBlock };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// src/taxonomy.ts
|
|
2
|
+
var ISSUE_TYPE_NAMES = {
|
|
3
|
+
design: "Design",
|
|
4
|
+
requirement: "Requirement",
|
|
5
|
+
competitor: "Competitor",
|
|
6
|
+
market: "Market",
|
|
7
|
+
gap: "Gap",
|
|
8
|
+
epic: "Epic",
|
|
9
|
+
feature: "Feature",
|
|
10
|
+
task: "Task",
|
|
11
|
+
spike: "Spike"
|
|
12
|
+
};
|
|
13
|
+
var kindLabel = (kind) => `kind:${kind}`;
|
|
14
|
+
var OPERATIONAL_LABELS = {
|
|
15
|
+
managed: "boule:managed",
|
|
16
|
+
needsHuman: "boule:needs-human",
|
|
17
|
+
superseded: "boule:superseded",
|
|
18
|
+
/** Kill-switch: an OPEN issue carrying this label halts all autonomous writes. */
|
|
19
|
+
halt: "boule:halt"
|
|
20
|
+
};
|
|
21
|
+
var STATUS_LABELS = [
|
|
22
|
+
"status:draft",
|
|
23
|
+
"status:needs-review",
|
|
24
|
+
"status:accepted",
|
|
25
|
+
"status:superseded"
|
|
26
|
+
];
|
|
27
|
+
var PRIORITY_LABELS = [
|
|
28
|
+
"priority:must",
|
|
29
|
+
"priority:should",
|
|
30
|
+
"priority:could",
|
|
31
|
+
"priority:wont"
|
|
32
|
+
];
|
|
33
|
+
var PRAKTOR_LABELS = {
|
|
34
|
+
inProgress: "praktor:in-progress",
|
|
35
|
+
done: "praktor:done",
|
|
36
|
+
blocked: "praktor:blocked"
|
|
37
|
+
};
|
|
38
|
+
var PROJECT_FIELDS = {
|
|
39
|
+
status: "Status",
|
|
40
|
+
kind: "Kind",
|
|
41
|
+
priority: "Priority",
|
|
42
|
+
rice: "RICE",
|
|
43
|
+
wsjf: "WSJF",
|
|
44
|
+
moscow: "MoSCoW",
|
|
45
|
+
iteration: "Iteration"
|
|
46
|
+
};
|
|
47
|
+
var STATUS_OPTIONS = [
|
|
48
|
+
"Triage",
|
|
49
|
+
"In Design",
|
|
50
|
+
"In Review",
|
|
51
|
+
"Ready",
|
|
52
|
+
"In Progress",
|
|
53
|
+
"Blocked",
|
|
54
|
+
"Done"
|
|
55
|
+
];
|
|
56
|
+
var DISCUSSION_CATEGORIES = {
|
|
57
|
+
dailyStatus: "Daily Status",
|
|
58
|
+
handoff: "Agent Handoffs",
|
|
59
|
+
designReview: "Design Review"
|
|
60
|
+
};
|
|
61
|
+
function allBootstrapLabels() {
|
|
62
|
+
const kinds = Object.keys(ISSUE_TYPE_NAMES);
|
|
63
|
+
return [
|
|
64
|
+
...kinds.map(kindLabel),
|
|
65
|
+
...Object.values(OPERATIONAL_LABELS),
|
|
66
|
+
...STATUS_LABELS,
|
|
67
|
+
...PRIORITY_LABELS
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/identity.ts
|
|
72
|
+
import { createHash } from "crypto";
|
|
73
|
+
var BOULE_BEGIN = "<!-- boule:v1";
|
|
74
|
+
var BOULE_END = "-->";
|
|
75
|
+
function bouleId(kind, naturalKey) {
|
|
76
|
+
const slug = naturalKey.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
77
|
+
return `${kind}:${slug}`;
|
|
78
|
+
}
|
|
79
|
+
function contentHash(body) {
|
|
80
|
+
const normalized = stripBouleBlock(body).replace(/\r\n/g, "\n").replace(/[ \t]+$/gm, "").trim();
|
|
81
|
+
const hex = createHash("sha256").update(normalized, "utf8").digest("hex");
|
|
82
|
+
return `sha256:${hex.slice(0, 16)}`;
|
|
83
|
+
}
|
|
84
|
+
function idLabel(id) {
|
|
85
|
+
const hex = createHash("sha256").update(id, "utf8").digest("hex");
|
|
86
|
+
return `boule-id-${hex.slice(0, 12)}`;
|
|
87
|
+
}
|
|
88
|
+
function renderBouleBlock(b) {
|
|
89
|
+
return [
|
|
90
|
+
BOULE_BEGIN,
|
|
91
|
+
`kind: ${b.kind}`,
|
|
92
|
+
`boule-id: ${b.bouleId}`,
|
|
93
|
+
`content-hash: ${b.contentHash}`,
|
|
94
|
+
b.parent ? `parent: ${b.parent}` : "parent:",
|
|
95
|
+
b.runId ? `run-id: ${b.runId}` : null,
|
|
96
|
+
b.generatedBy ? `generated-by: ${b.generatedBy}` : null,
|
|
97
|
+
BOULE_END
|
|
98
|
+
].filter((l) => l !== null).join("\n");
|
|
99
|
+
}
|
|
100
|
+
function withBouleBlock(body, meta) {
|
|
101
|
+
const clean = stripBouleBlock(body).trimEnd();
|
|
102
|
+
const block = renderBouleBlock({ ...meta, contentHash: contentHash(clean) });
|
|
103
|
+
return `${clean}
|
|
104
|
+
|
|
105
|
+
${block}
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
function parseBouleBlock(body) {
|
|
109
|
+
const start = body.indexOf(BOULE_BEGIN);
|
|
110
|
+
if (start === -1) return null;
|
|
111
|
+
const end = body.indexOf(BOULE_END, start);
|
|
112
|
+
if (end === -1) return null;
|
|
113
|
+
const inner = body.slice(start + BOULE_BEGIN.length, end);
|
|
114
|
+
const get = (k) => {
|
|
115
|
+
const m = inner.match(new RegExp(`^${k}:\\s*(.+)$`, "m"));
|
|
116
|
+
return m?.[1]?.trim() || void 0;
|
|
117
|
+
};
|
|
118
|
+
const kind = get("kind");
|
|
119
|
+
const id = get("boule-id");
|
|
120
|
+
const hash = get("content-hash");
|
|
121
|
+
if (!kind || !id || !hash) return null;
|
|
122
|
+
return {
|
|
123
|
+
kind,
|
|
124
|
+
bouleId: id,
|
|
125
|
+
contentHash: hash,
|
|
126
|
+
parent: get("parent"),
|
|
127
|
+
runId: get("run-id"),
|
|
128
|
+
generatedBy: get("generated-by")
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function stripBouleBlock(body) {
|
|
132
|
+
const start = body.indexOf(BOULE_BEGIN);
|
|
133
|
+
if (start === -1) return body;
|
|
134
|
+
const end = body.indexOf(BOULE_END, start);
|
|
135
|
+
if (end === -1) return body;
|
|
136
|
+
return body.slice(0, start) + body.slice(end + BOULE_END.length);
|
|
137
|
+
}
|
|
138
|
+
function parseVerifies(body) {
|
|
139
|
+
const m = body.match(/^\s*Verifies:\s*(.+)$/im);
|
|
140
|
+
if (!m?.[1]) return [];
|
|
141
|
+
return [...m[1].matchAll(/#(\d+)/g)].map((x) => Number(x[1])).filter((n) => Number.isInteger(n));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/security/secrets.ts
|
|
145
|
+
var PATTERNS = [
|
|
146
|
+
{ name: "github-token", re: /\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/g },
|
|
147
|
+
{ name: "anthropic-key", re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
|
|
148
|
+
{ name: "aws-access-key", re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
149
|
+
{ name: "private-key", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g },
|
|
150
|
+
{ name: "openai-key", re: /\bsk-[A-Za-z0-9]{32,}\b/g }
|
|
151
|
+
];
|
|
152
|
+
function scrubSecrets(text) {
|
|
153
|
+
let clean = text;
|
|
154
|
+
const found = /* @__PURE__ */ new Set();
|
|
155
|
+
for (const { name, re } of PATTERNS) {
|
|
156
|
+
clean = clean.replace(re, () => {
|
|
157
|
+
found.add(name);
|
|
158
|
+
return `[REDACTED:${name}]`;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return { clean, found: [...found] };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/security/mentions.ts
|
|
165
|
+
var MENTION_RE = /(^|[^A-Za-z0-9_`@/])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?)\b/g;
|
|
166
|
+
function sanitizeMentions(text) {
|
|
167
|
+
const stripped = [];
|
|
168
|
+
const clean = text.replace(MENTION_RE, (_m, pre, handle) => {
|
|
169
|
+
stripped.push(handle);
|
|
170
|
+
return `${pre}\`@${handle}\``;
|
|
171
|
+
});
|
|
172
|
+
return { clean, stripped };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/security/outbound.ts
|
|
176
|
+
function cleanOutbound(text) {
|
|
177
|
+
const s = scrubSecrets(text);
|
|
178
|
+
const m = sanitizeMentions(s.clean);
|
|
179
|
+
return { clean: m.clean, secrets: s.found, mentions: m.stripped };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/github/auth.ts
|
|
183
|
+
import { createAppAuth } from "@octokit/auth-app";
|
|
184
|
+
function decodePrivateKey(raw) {
|
|
185
|
+
const v = raw.trim();
|
|
186
|
+
if (v.includes("BEGIN") && v.includes("PRIVATE KEY")) return v;
|
|
187
|
+
try {
|
|
188
|
+
return Buffer.from(v, "base64").toString("utf8");
|
|
189
|
+
} catch {
|
|
190
|
+
return v;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function mintToken(auth) {
|
|
194
|
+
if (auth.kind === "pat") return auth.token;
|
|
195
|
+
const appAuth = createAppAuth({
|
|
196
|
+
appId: auth.appId,
|
|
197
|
+
privateKey: auth.privateKey,
|
|
198
|
+
installationId: Number(auth.installationId)
|
|
199
|
+
});
|
|
200
|
+
const { token } = await appAuth({ type: "installation" });
|
|
201
|
+
return token;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/github/client.ts
|
|
205
|
+
import { graphql as octokitGraphql } from "@octokit/graphql";
|
|
206
|
+
import { throttling } from "@octokit/plugin-throttling";
|
|
207
|
+
import { Octokit } from "@octokit/rest";
|
|
208
|
+
import pRetry, { AbortError } from "p-retry";
|
|
209
|
+
var ThrottledOctokit = Octokit.plugin(throttling);
|
|
210
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
211
|
+
var jitter = (ms) => ms * (0.5 + Math.random());
|
|
212
|
+
function classifyWait(err, attempt) {
|
|
213
|
+
const e = err;
|
|
214
|
+
const status = e.status ?? e.response?.status;
|
|
215
|
+
const headers = e.response?.headers ?? {};
|
|
216
|
+
if (status === 403 || status === 429) {
|
|
217
|
+
const ra = Number(headers["retry-after"]);
|
|
218
|
+
if (Number.isFinite(ra)) return ra * 1e3;
|
|
219
|
+
const reset = Number(headers["x-ratelimit-reset"]);
|
|
220
|
+
if (Number.isFinite(reset)) return Math.max(0, reset * 1e3 - Date.now());
|
|
221
|
+
return Math.max(6e4, jitter(2 ** attempt * 1e3));
|
|
222
|
+
}
|
|
223
|
+
if (status && status >= 500) return jitter(2 ** attempt * 500);
|
|
224
|
+
throw new AbortError(err);
|
|
225
|
+
}
|
|
226
|
+
async function createGitHubClient(auth, log, opts = {}) {
|
|
227
|
+
const token = await mintToken(auth.github);
|
|
228
|
+
const rest = new ThrottledOctokit({
|
|
229
|
+
auth: token,
|
|
230
|
+
throttle: {
|
|
231
|
+
onRateLimit: (after, _o, _ok, retryCount) => {
|
|
232
|
+
log.warn({ after, retryCount }, "primary rate limit");
|
|
233
|
+
return retryCount < 3;
|
|
234
|
+
},
|
|
235
|
+
onSecondaryRateLimit: (after) => {
|
|
236
|
+
log.warn({ after }, "secondary rate limit; honoring retry-after");
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
const gql = octokitGraphql.defaults({
|
|
242
|
+
headers: {
|
|
243
|
+
authorization: `token ${token}`,
|
|
244
|
+
"GraphQL-Features": "issue_types,sub_issues"
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
const run = (_op, task) => pRetry(
|
|
248
|
+
async (attempt) => {
|
|
249
|
+
try {
|
|
250
|
+
return await task();
|
|
251
|
+
} catch (err) {
|
|
252
|
+
await sleep(classifyWait(err, attempt));
|
|
253
|
+
throw err;
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
{ retries: opts.maxRetries ?? 6, minTimeout: 1e3, factor: 2 }
|
|
257
|
+
);
|
|
258
|
+
return {
|
|
259
|
+
rest,
|
|
260
|
+
withRest: (op, fn) => run(op, () => fn(rest)),
|
|
261
|
+
graphql: (op, query2, vars) => run(op, () => gql(query2, vars))
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/agent/run.ts
|
|
266
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
267
|
+
function stopReasonOf(subtype) {
|
|
268
|
+
if (subtype === "success") return "success";
|
|
269
|
+
if (subtype === "error_max_turns") return "error_max_turns";
|
|
270
|
+
if (subtype === "error_max_budget_usd") return "error_max_budget_usd";
|
|
271
|
+
return "error_during_execution";
|
|
272
|
+
}
|
|
273
|
+
async function runQuery(prompt, options, hooks) {
|
|
274
|
+
const { log } = hooks;
|
|
275
|
+
let stopReason = "error_during_execution";
|
|
276
|
+
let numTurns = 0;
|
|
277
|
+
let costUsd = 0;
|
|
278
|
+
let sessionId = "";
|
|
279
|
+
let modelUsage = {};
|
|
280
|
+
const errors = [];
|
|
281
|
+
let gotResult = false;
|
|
282
|
+
try {
|
|
283
|
+
for await (const msg of query({ prompt, options })) {
|
|
284
|
+
if (msg.type === "system" && msg.subtype === "init") {
|
|
285
|
+
sessionId = msg.session_id;
|
|
286
|
+
log.info({ sessionId }, "agent run started");
|
|
287
|
+
hooks.onSession?.(sessionId);
|
|
288
|
+
}
|
|
289
|
+
if (msg.type === "result") {
|
|
290
|
+
stopReason = stopReasonOf(msg.subtype);
|
|
291
|
+
numTurns = msg.num_turns;
|
|
292
|
+
costUsd = msg.total_cost_usd;
|
|
293
|
+
modelUsage = msg.modelUsage ?? {};
|
|
294
|
+
if (msg.subtype !== "success") errors.push(...msg.errors ?? []);
|
|
295
|
+
gotResult = true;
|
|
296
|
+
log.info({ stopReason, costUsd, numTurns }, "agent run finished");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} catch (err) {
|
|
300
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
301
|
+
if (gotResult) {
|
|
302
|
+
log.warn({ err: message }, "agent transport error after result; keeping captured outcome");
|
|
303
|
+
} else {
|
|
304
|
+
log.error({ err: message }, "agent run failed before producing a result");
|
|
305
|
+
errors.push(message);
|
|
306
|
+
stopReason = "error_during_execution";
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return { ok: stopReason === "success", stopReason, sessionId, numTurns, costUsd, modelUsage, errors };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/observability/logger.ts
|
|
313
|
+
import pino from "pino";
|
|
314
|
+
function createLogger(opts = {}) {
|
|
315
|
+
const base = pino({
|
|
316
|
+
level: opts.level ?? "info",
|
|
317
|
+
redact: {
|
|
318
|
+
paths: ["token", "privateKey", "*.token", "*.privateKey", "headers.authorization"],
|
|
319
|
+
censor: "[redacted]"
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
const bindings = {};
|
|
323
|
+
if (opts.service) bindings.service = opts.service;
|
|
324
|
+
if (opts.runId) bindings.runId = opts.runId;
|
|
325
|
+
return Object.keys(bindings).length ? base.child(bindings) : base;
|
|
326
|
+
}
|
|
327
|
+
export {
|
|
328
|
+
DISCUSSION_CATEGORIES,
|
|
329
|
+
ISSUE_TYPE_NAMES,
|
|
330
|
+
OPERATIONAL_LABELS,
|
|
331
|
+
PRAKTOR_LABELS,
|
|
332
|
+
PRIORITY_LABELS,
|
|
333
|
+
PROJECT_FIELDS,
|
|
334
|
+
STATUS_LABELS,
|
|
335
|
+
STATUS_OPTIONS,
|
|
336
|
+
allBootstrapLabels,
|
|
337
|
+
bouleId,
|
|
338
|
+
cleanOutbound,
|
|
339
|
+
contentHash,
|
|
340
|
+
createGitHubClient,
|
|
341
|
+
createLogger,
|
|
342
|
+
decodePrivateKey,
|
|
343
|
+
idLabel,
|
|
344
|
+
kindLabel,
|
|
345
|
+
mintToken,
|
|
346
|
+
parseBouleBlock,
|
|
347
|
+
parseVerifies,
|
|
348
|
+
renderBouleBlock,
|
|
349
|
+
runQuery,
|
|
350
|
+
sanitizeMentions,
|
|
351
|
+
scrubSecrets,
|
|
352
|
+
stripBouleBlock,
|
|
353
|
+
withBouleBlock
|
|
354
|
+
};
|
|
355
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/taxonomy.ts","../src/identity.ts","../src/security/secrets.ts","../src/security/mentions.ts","../src/security/outbound.ts","../src/github/auth.ts","../src/github/client.ts","../src/agent/run.ts","../src/observability/logger.ts"],"sourcesContent":["// src/taxonomy.ts — THE shared label/field contract written to GitHub. Boule produces it; Praktor reads\n// it; both import it here so it can never drift. The string VALUES are the on-wire format (already on\n// live issues) — change them only with a migration.\nimport type { ArtifactKind } from \"./types.js\";\n\nexport const ISSUE_TYPE_NAMES = {\n design: \"Design\",\n requirement: \"Requirement\",\n competitor: \"Competitor\",\n market: \"Market\",\n gap: \"Gap\",\n epic: \"Epic\",\n feature: \"Feature\",\n task: \"Task\",\n spike: \"Spike\",\n} as const;\n\n/** Fallback kind label used when native Issue Types are unavailable. */\nexport const kindLabel = (kind: ArtifactKind): string => `kind:${kind}`;\n\nexport const OPERATIONAL_LABELS = {\n managed: \"boule:managed\",\n needsHuman: \"boule:needs-human\",\n superseded: \"boule:superseded\",\n /** Kill-switch: an OPEN issue carrying this label halts all autonomous writes. */\n halt: \"boule:halt\",\n} as const;\n\n/** An artifact's ACCEPTANCE lifecycle, carried on the Issue as a label. */\nexport const STATUS_LABELS = [\n \"status:draft\",\n \"status:needs-review\",\n \"status:accepted\",\n \"status:superseded\",\n] as const;\nexport type StatusLabel = (typeof STATUS_LABELS)[number];\n\nexport const PRIORITY_LABELS = [\n \"priority:must\",\n \"priority:should\",\n \"priority:could\",\n \"priority:wont\",\n] as const;\n\n/** Praktor's own progress labels — namespaced so they never collide with Boule's lifecycle. */\nexport const PRAKTOR_LABELS = {\n inProgress: \"praktor:in-progress\",\n done: \"praktor:done\",\n blocked: \"praktor:blocked\",\n} as const;\n\n/** Projects v2 custom field names (canonical keys for field values). */\nexport const PROJECT_FIELDS = {\n status: \"Status\",\n kind: \"Kind\",\n priority: \"Priority\",\n rice: \"RICE\",\n wsjf: \"WSJF\",\n moscow: \"MoSCoW\",\n iteration: \"Iteration\",\n} as const;\n\n/** Projects v2 Status column options (the board workflow state). */\nexport const STATUS_OPTIONS = [\n \"Triage\",\n \"In Design\",\n \"In Review\",\n \"Ready\",\n \"In Progress\",\n \"Blocked\",\n \"Done\",\n] as const;\n\nexport const DISCUSSION_CATEGORIES = {\n dailyStatus: \"Daily Status\",\n handoff: \"Agent Handoffs\",\n designReview: \"Design Review\",\n} as const;\n\n/** Every repo label the tools bootstrap (kinds + operational + status + priority). */\nexport function allBootstrapLabels(): string[] {\n const kinds = Object.keys(ISSUE_TYPE_NAMES) as ArtifactKind[];\n return [\n ...kinds.map(kindLabel),\n ...Object.values(OPERATIONAL_LABELS),\n ...STATUS_LABELS,\n ...PRIORITY_LABELS,\n ];\n}\n","// src/identity.ts — the boule:v1 identity block + content addressing. The crux of safe autonomy:\n// pure, deterministic, network-free. Same logical work ⇒ same id ⇒ idempotent re-runs.\nimport { createHash } from \"node:crypto\";\nimport type { ArtifactKind, Fingerprint } from \"./types.js\";\n\nconst BOULE_BEGIN = \"<!-- boule:v1\";\nconst BOULE_END = \"-->\";\n\nexport interface BouleBlock {\n kind: ArtifactKind;\n bouleId: string;\n contentHash: Fingerprint;\n parent?: string;\n runId?: string;\n generatedBy?: string;\n}\n\n/** Stable, content-independent slug. Same natural key ⇒ same id (NOT random). */\nexport function bouleId(kind: ArtifactKind, naturalKey: string): string {\n const slug = naturalKey\n .toLowerCase()\n .normalize(\"NFKD\")\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 64);\n return `${kind}:${slug}`;\n}\n\n/** sha256 over the normalized semantic body, EXCLUDING any boule block. */\nexport function contentHash(body: string): Fingerprint {\n const normalized = stripBouleBlock(body)\n .replace(/\\r\\n/g, \"\\n\")\n .replace(/[ \\t]+$/gm, \"\")\n .trim();\n const hex = createHash(\"sha256\").update(normalized, \"utf8\").digest(\"hex\");\n return `sha256:${hex.slice(0, 16)}`;\n}\n\n/** A unique, deterministic dedup label (hashed to stay under GitHub's 50-char label limit). */\nexport function idLabel(id: string): string {\n const hex = createHash(\"sha256\").update(id, \"utf8\").digest(\"hex\");\n return `boule-id-${hex.slice(0, 12)}`;\n}\n\nexport function renderBouleBlock(b: BouleBlock): string {\n return [\n BOULE_BEGIN,\n `kind: ${b.kind}`,\n `boule-id: ${b.bouleId}`,\n `content-hash: ${b.contentHash}`,\n b.parent ? `parent: ${b.parent}` : \"parent:\",\n b.runId ? `run-id: ${b.runId}` : null,\n b.generatedBy ? `generated-by: ${b.generatedBy}` : null,\n BOULE_END,\n ]\n .filter((l): l is string => l !== null)\n .join(\"\\n\");\n}\n\n/** Append the block to a body, recomputing the hash over the body sans-block. */\nexport function withBouleBlock(body: string, meta: Omit<BouleBlock, \"contentHash\">): string {\n const clean = stripBouleBlock(body).trimEnd();\n const block = renderBouleBlock({ ...meta, contentHash: contentHash(clean) });\n return `${clean}\\n\\n${block}\\n`;\n}\n\nexport function parseBouleBlock(body: string): BouleBlock | null {\n const start = body.indexOf(BOULE_BEGIN);\n if (start === -1) return null;\n const end = body.indexOf(BOULE_END, start);\n if (end === -1) return null;\n const inner = body.slice(start + BOULE_BEGIN.length, end);\n const get = (k: string): string | undefined => {\n const m = inner.match(new RegExp(`^${k}:\\\\s*(.+)$`, \"m\"));\n return m?.[1]?.trim() || undefined;\n };\n const kind = get(\"kind\") as ArtifactKind | undefined;\n const id = get(\"boule-id\");\n const hash = get(\"content-hash\");\n if (!kind || !id || !hash) return null;\n return {\n kind,\n bouleId: id,\n contentHash: hash,\n parent: get(\"parent\"),\n runId: get(\"run-id\"),\n generatedBy: get(\"generated-by\"),\n };\n}\n\nexport function stripBouleBlock(body: string): string {\n const start = body.indexOf(BOULE_BEGIN);\n if (start === -1) return body;\n const end = body.indexOf(BOULE_END, start);\n if (end === -1) return body;\n return body.slice(0, start) + body.slice(end + BOULE_END.length);\n}\n\n/** Requirement issue numbers referenced by a `Verifies: #110, #112` link line (Task → Requirement). */\nexport function parseVerifies(body: string): number[] {\n const m = body.match(/^\\s*Verifies:\\s*(.+)$/im);\n if (!m?.[1]) return [];\n return [...m[1].matchAll(/#(\\d+)/g)].map((x) => Number(x[1])).filter((n) => Number.isInteger(n));\n}\n","// src/security/secrets.ts — last-line defense: redact credential-looking strings before any\n// agent-authored text reaches GitHub (a public issue/discussion/PR must never leak a token).\nconst PATTERNS: ReadonlyArray<{ name: string; re: RegExp }> = [\n { name: \"github-token\", re: /\\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\\b/g },\n { name: \"anthropic-key\", re: /\\bsk-ant-[A-Za-z0-9_-]{20,}\\b/g },\n { name: \"aws-access-key\", re: /\\bAKIA[0-9A-Z]{16}\\b/g },\n { name: \"private-key\", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\\s\\S]*?-----END [A-Z ]*PRIVATE KEY-----/g },\n { name: \"openai-key\", re: /\\bsk-[A-Za-z0-9]{32,}\\b/g },\n];\n\nexport interface ScrubResult {\n clean: string;\n found: string[]; // distinct credential kinds redacted\n}\n\n/** Replace any credential-looking substrings with `[REDACTED:<kind>]`; report the kinds found. */\nexport function scrubSecrets(text: string): ScrubResult {\n let clean = text;\n const found = new Set<string>();\n for (const { name, re } of PATTERNS) {\n clean = clean.replace(re, () => {\n found.add(name);\n return `[REDACTED:${name}]`;\n });\n }\n return { clean, found: [...found] };\n}\n","// src/security/mentions.ts — neutralize @-mentions so autonomous artifacts never ping people.\n// Wrapping a handle in a backtick code span stops GitHub from sending a notification.\n\n// @handle not preceded by a word char/backtick and not part of an email; 1-39 chars, GitHub rules.\nconst MENTION_RE = /(^|[^A-Za-z0-9_`@/])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?)\\b/g;\n\nexport interface MentionResult {\n clean: string;\n stripped: string[]; // handles neutralized\n}\n\nexport function sanitizeMentions(text: string): MentionResult {\n const stripped: string[] = [];\n const clean = text.replace(MENTION_RE, (_m, pre: string, handle: string) => {\n stripped.push(handle);\n return `${pre}\\`@${handle}\\``;\n });\n return { clean, stripped };\n}\n","// src/security/outbound.ts — the single cleanup applied to every agent-authored string before it\n// reaches GitHub: redact credentials, then neutralize @-mentions.\nimport { sanitizeMentions } from \"./mentions.js\";\nimport { scrubSecrets } from \"./secrets.js\";\n\nexport interface Outbound {\n clean: string;\n secrets: string[]; // kinds of credential redacted\n mentions: string[]; // handles neutralized\n}\n\nexport function cleanOutbound(text: string): Outbound {\n const s = scrubSecrets(text);\n const m = sanitizeMentions(s.clean);\n return { clean: m.clean, secrets: s.found, mentions: m.stripped };\n}\n","// src/github/auth.ts — credential TYPES + token minting only. Each tool resolves its OWN env names\n// into an AuthConfig (Boule uses BOULE_APP_*, Praktor uses PRAKTOR_APP_*) and passes it to the client;\n// koine never reads process.env, so it stays identity-agnostic.\nimport { createAppAuth } from \"@octokit/auth-app\";\n\nexport type GitHubAuth =\n | { kind: \"pat\"; token: string }\n | { kind: \"app\"; appId: string; installationId: string; privateKey: string };\n\nexport interface AuthConfig {\n github: GitHubAuth;\n}\n\n/** Decode a base64-or-PEM private key (CI usually stores a single-line base64 blob). */\nexport function decodePrivateKey(raw: string): string {\n const v = raw.trim();\n if (v.includes(\"BEGIN\") && v.includes(\"PRIVATE KEY\")) return v;\n try {\n return Buffer.from(v, \"base64\").toString(\"utf8\");\n } catch {\n return v;\n }\n}\n\n/** Mint a usable token: a PAT passes through; an App mints a short-lived installation token. */\nexport async function mintToken(auth: GitHubAuth): Promise<string> {\n if (auth.kind === \"pat\") return auth.token;\n const appAuth = createAppAuth({\n appId: auth.appId,\n privateKey: auth.privateKey,\n installationId: Number(auth.installationId),\n });\n const { token } = await appAuth({ type: \"installation\" });\n return token;\n}\n","// src/github/client.ts — THE only path to the GitHub API. All backoff/retry + auth live here so both\n// tools share one hardened transport. Reads are concurrency-friendly; writes serialize via p-retry.\nimport { graphql as octokitGraphql } from \"@octokit/graphql\";\nimport { throttling } from \"@octokit/plugin-throttling\";\nimport { Octokit } from \"@octokit/rest\";\nimport pRetry, { AbortError } from \"p-retry\";\nimport type { Logger } from \"pino\";\nimport { type AuthConfig, mintToken } from \"./auth.js\";\n\nconst ThrottledOctokit = Octokit.plugin(throttling);\ntype OpKind = \"read\" | \"write\";\n\nexport interface GitHubClient {\n rest: Octokit;\n /** Run a REST call through the retry/backoff gate. */\n withRest<T>(op: OpKind, fn: (o: Octokit) => Promise<T>): Promise<T>;\n /** Run a GraphQL document through the gate. */\n graphql<T = unknown>(op: OpKind, query: string, vars?: Record<string, unknown>): Promise<T>;\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));\nconst jitter = (ms: number): number => ms * (0.5 + Math.random());\n\nfunction classifyWait(err: unknown, attempt: number): number {\n const e = err as { status?: number; response?: { status?: number; headers?: Record<string, string> } };\n const status = e.status ?? e.response?.status;\n const headers = e.response?.headers ?? {};\n if (status === 403 || status === 429) {\n const ra = Number(headers[\"retry-after\"]);\n if (Number.isFinite(ra)) return ra * 1000;\n const reset = Number(headers[\"x-ratelimit-reset\"]);\n if (Number.isFinite(reset)) return Math.max(0, reset * 1000 - Date.now());\n return Math.max(60_000, jitter(2 ** attempt * 1000));\n }\n if (status && status >= 500) return jitter(2 ** attempt * 500);\n throw new AbortError(err as Error); // 4xx (non-rate) ⇒ don't retry\n}\n\nexport interface ClientOptions {\n maxRetries?: number;\n}\n\nexport async function createGitHubClient(\n auth: AuthConfig,\n log: Logger,\n opts: ClientOptions = {},\n): Promise<GitHubClient> {\n const token = await mintToken(auth.github);\n const rest = new ThrottledOctokit({\n auth: token,\n throttle: {\n onRateLimit: (after, _o, _ok, retryCount) => {\n log.warn({ after, retryCount }, \"primary rate limit\");\n return retryCount < 3;\n },\n onSecondaryRateLimit: (after) => {\n log.warn({ after }, \"secondary rate limit; honoring retry-after\");\n return true;\n },\n },\n });\n const gql = octokitGraphql.defaults({\n headers: {\n authorization: `token ${token}`,\n \"GraphQL-Features\": \"issue_types,sub_issues\",\n },\n });\n\n const run = <T>(_op: OpKind, task: () => Promise<T>): Promise<T> =>\n pRetry(\n async (attempt) => {\n try {\n return await task();\n } catch (err) {\n await sleep(classifyWait(err, attempt));\n throw err;\n }\n },\n { retries: opts.maxRetries ?? 6, minTimeout: 1000, factor: 2 },\n );\n\n return {\n rest,\n withRest: (op, fn) => run(op, () => fn(rest)),\n graphql: <T>(op: OpKind, query: string, vars?: Record<string, unknown>) =>\n run<T>(op, () => gql(query, vars) as Promise<T>),\n };\n}\n","// src/agent/run.ts — the shared Claude Agent SDK run-loop. Resilient to transport noise: the SDK's\n// subprocess can exit non-zero on teardown AFTER emitting a terminal result; that must not override a\n// run whose outcome is already known. A failure BEFORE any result is real and reported as such.\nimport { type Options, query } from \"@anthropic-ai/claude-agent-sdk\";\nimport type { Logger } from \"pino\";\n\nexport type StopReason = \"success\" | \"error_max_turns\" | \"error_max_budget_usd\" | \"error_during_execution\";\n\nexport interface RunOutcome {\n ok: boolean;\n stopReason: StopReason;\n sessionId: string;\n numTurns: number;\n costUsd: number;\n modelUsage: Record<string, unknown>;\n errors: string[];\n}\n\nexport interface RunHooks {\n log: Logger;\n /** Called once with the SDK session id (at init) — e.g. to checkpoint for resume. */\n onSession?: (sessionId: string) => void;\n}\n\nfunction stopReasonOf(subtype: string): StopReason {\n if (subtype === \"success\") return \"success\";\n if (subtype === \"error_max_turns\") return \"error_max_turns\";\n if (subtype === \"error_max_budget_usd\") return \"error_max_budget_usd\";\n return \"error_during_execution\";\n}\n\n/** Drive one query() to completion, returning a normalized outcome. Never throws on transport noise. */\nexport async function runQuery(prompt: string, options: Options, hooks: RunHooks): Promise<RunOutcome> {\n const { log } = hooks;\n let stopReason: StopReason = \"error_during_execution\";\n let numTurns = 0;\n let costUsd = 0;\n let sessionId = \"\";\n let modelUsage: Record<string, unknown> = {};\n const errors: string[] = [];\n let gotResult = false;\n\n try {\n for await (const msg of query({ prompt, options })) {\n if (msg.type === \"system\" && msg.subtype === \"init\") {\n sessionId = msg.session_id;\n log.info({ sessionId }, \"agent run started\");\n hooks.onSession?.(sessionId);\n }\n if (msg.type === \"result\") {\n stopReason = stopReasonOf(msg.subtype);\n numTurns = msg.num_turns;\n costUsd = msg.total_cost_usd;\n modelUsage = (msg.modelUsage ?? {}) as Record<string, unknown>;\n if (msg.subtype !== \"success\") errors.push(...(msg.errors ?? []));\n gotResult = true;\n log.info({ stopReason, costUsd, numTurns }, \"agent run finished\");\n }\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (gotResult) {\n log.warn({ err: message }, \"agent transport error after result; keeping captured outcome\");\n } else {\n log.error({ err: message }, \"agent run failed before producing a result\");\n errors.push(message);\n stopReason = \"error_during_execution\";\n }\n }\n\n return { ok: stopReason === \"success\", stopReason, sessionId, numTurns, costUsd, modelUsage, errors };\n}\n","// src/observability/logger.ts — structured pino logger with credential redaction. Each tool passes its\n// own level + service name; a run-scoped child carries the runId.\nimport pino, { type Logger } from \"pino\";\n\nexport type { Logger };\n\nexport interface LoggerOptions {\n level?: string;\n service?: string;\n runId?: string;\n}\n\nexport function createLogger(opts: LoggerOptions = {}): Logger {\n const base = pino({\n level: opts.level ?? \"info\",\n redact: {\n paths: [\"token\", \"privateKey\", \"*.token\", \"*.privateKey\", \"headers.authorization\"],\n censor: \"[redacted]\",\n },\n });\n const bindings: Record<string, string> = {};\n if (opts.service) bindings.service = opts.service;\n if (opts.runId) bindings.runId = opts.runId;\n return Object.keys(bindings).length ? base.child(bindings) : base;\n}\n"],"mappings":"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","names":["query"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kleroterion/koine",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "The common tongue of the Kleroterion tools — shared building blocks for Boule and Praktor: GitHub-artifact taxonomy, the boule:v1 identity block, a gated GitHub client (App/PAT), the resilient Claude Agent SDK run-loop, structured logging, and outbound secret/mention scrubbing.",
|
|
5
|
+
"keywords": ["kleroterion", "boule", "praktor", "github", "claude", "agent-sdk", "shared", "library"],
|
|
6
|
+
"homepage": "https://github.com/kleroterionlabs/koine#readme",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/kleroterionlabs/koine/issues"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/kleroterionlabs/koine.git"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Bill Schumacher <34168009+BillSchumacher@users.noreply.github.com>",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20.11"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": ["dist"],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsup",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"lint": "biome ci .",
|
|
35
|
+
"prepublishOnly": "npm run build"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@anthropic-ai/claude-agent-sdk": "^0.1.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@octokit/rest": "^21.0.0",
|
|
42
|
+
"@octokit/graphql": "^8.1.0",
|
|
43
|
+
"@octokit/auth-app": "^7.1.0",
|
|
44
|
+
"@octokit/plugin-throttling": "^9.3.0",
|
|
45
|
+
"p-retry": "^6.2.0",
|
|
46
|
+
"pino": "^9.4.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@anthropic-ai/claude-agent-sdk": "^0.1.0",
|
|
50
|
+
"typescript": "^5.6.0",
|
|
51
|
+
"tsup": "^8.3.0",
|
|
52
|
+
"vitest": "^2.1.0",
|
|
53
|
+
"@vitest/coverage-v8": "^2.1.0",
|
|
54
|
+
"@biomejs/biome": "^1.9.0",
|
|
55
|
+
"@types/node": "^22.5.0"
|
|
56
|
+
}
|
|
57
|
+
}
|