@agent-delivery-harness/cli 0.1.0 → 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.
@@ -0,0 +1,261 @@
1
+ /**
2
+ * What `emit`, `runs`, and the boundary wrap share: how the run store is
3
+ * found, how an event envelope is built, and how a store-derived string is
4
+ * made safe to print.
5
+ *
6
+ * WHY THE STORE IS RESOLVED HERE AND NOT WIRED. The record store is wired from
7
+ * the config, because a delivery record belongs to a configured gate. A run
8
+ * belongs to the *repository* — it outlives the worktree it ran in and it
9
+ * exists in repositories that have no harness config at all. So the run store
10
+ * is resolved from git and nothing else, through the kernel's namespace
11
+ * resolver, with a direct runner and the `GIT_` namespace cleared: an
12
+ * inherited `GIT_DIR` must never relocate the store into someone else's
13
+ * repository.
14
+ *
15
+ * WHY EVERY PRINTED STRING GOES THROUGH `oneLine`. A journal carries
16
+ * executor-written free text — rationales, decisions, blocker summaries. It is
17
+ * rendered to a terminal in a row-per-event table, so an escape sequence could
18
+ * repaint the screen and an embedded newline could forge a row that looks
19
+ * exactly like a CLI-written completion. Neutralizing strips the sequences;
20
+ * collapsing the whitespace is what removes the second attack, and it has to
21
+ * happen on every single-line field, not just the ones that look risky.
22
+ */
23
+ import { lstatSync } from "node:fs";
24
+ import path from "node:path";
25
+ import {
26
+ createBlocker,
27
+ createRunStore,
28
+ evaluateRunJournal,
29
+ gitNamespaceClearedEnvironment,
30
+ neutralizeForDisplay,
31
+ resolveRunStoreLocation,
32
+ runGitDirect,
33
+ type Blocker,
34
+ type RunEventInput,
35
+ type RunEventKind,
36
+ type RunJournalRow,
37
+ type RunStore,
38
+ } from "@agent-delivery-harness/kernel";
39
+
40
+ /** The config-free commands' own source id family. */
41
+ export const RUN_SURFACE_SOURCE = "delivery-harness.cli.run-surface";
42
+
43
+ export interface RunSurface {
44
+ readonly store: RunStore;
45
+ readonly commonDir: string;
46
+ readonly runsDir: string;
47
+ /** The invoking worktree's pointer key — the identity of "the current run" here. */
48
+ readonly worktreeKey: string;
49
+ }
50
+
51
+ export type RunSurfaceResolution =
52
+ | { readonly ok: true; readonly surface: RunSurface }
53
+ | { readonly ok: false; readonly reason: string };
54
+
55
+ /**
56
+ * Resolves the run store for the invoking worktree. Never throws: a config-free
57
+ * command turns a failure into a typed blocker, and the boundary wrap turns it
58
+ * into silence.
59
+ */
60
+ export async function resolveRunSurface(cwd: string): Promise<RunSurfaceResolution> {
61
+ const location = await resolveRunStoreLocation({
62
+ cwd,
63
+ run: runGitDirect,
64
+ env: gitNamespaceClearedEnvironment(),
65
+ });
66
+ if (!location.ok) return { ok: false, reason: location.reason };
67
+ return {
68
+ ok: true,
69
+ surface: {
70
+ store: createRunStore(location.commonDir),
71
+ commonDir: location.commonDir,
72
+ runsDir: location.runsDir,
73
+ worktreeKey: location.worktreeKey,
74
+ },
75
+ };
76
+ }
77
+
78
+ /**
79
+ * The worktree ROOT of a path, for the one thing the run surface needs it for:
80
+ * the config-presence note names a root, and `runs serve` renders a root the
81
+ * operator named rather than the one it was invoked in.
82
+ *
83
+ * `--show-toplevel` is a plumbing query that reads no index and runs no hook,
84
+ * alias, or pager, and it runs here with the `GIT_` namespace cleared for the
85
+ * same reason the store resolution does: a `GIT_DIR` inherited from the
86
+ * operator's shell must not decide which repository a `--repo` path names.
87
+ */
88
+ export async function resolveWorktreeRoot(
89
+ cwd: string,
90
+ ): Promise<{ readonly ok: true; readonly root: string } | { readonly ok: false; readonly reason: string }> {
91
+ const outcome = await runGitDirect({
92
+ cwd,
93
+ args: ["rev-parse", "--path-format=absolute", "--show-toplevel"],
94
+ env: gitNamespaceClearedEnvironment(),
95
+ });
96
+ const root = outcome.stdout.trim();
97
+ if (outcome.code !== 0 || root.length === 0) return { ok: false, reason: `not a git worktree: ${cwd}` };
98
+ return { ok: true, root };
99
+ }
100
+
101
+ /**
102
+ * The writing process's own instant, at the contract's second granularity.
103
+ * `at` is never settable through an argument surface: whoever writes the event
104
+ * is whoever is holding the clock.
105
+ */
106
+ export function runInstant(): string {
107
+ return `${new Date().toISOString().slice(0, 19)}Z`;
108
+ }
109
+
110
+ /** The two envelope members a payload may own; both are copied, never invented. */
111
+ const MIRRORED = ["ticket", "candidateTreeSha"] as const;
112
+
113
+ /**
114
+ * Builds the envelope around a payload. The mirrored members are copied
115
+ * verbatim — including a value the validator will reject — because the store
116
+ * requires the envelope and the payload to agree exactly, and a "helpful"
117
+ * coercion here would turn a malformed payload into a disagreement instead.
118
+ */
119
+ export function buildRunEvent(input: {
120
+ readonly runId: string;
121
+ readonly commonDir: string;
122
+ readonly kind: string;
123
+ readonly role: "cli" | "executor";
124
+ readonly payload: unknown;
125
+ }): RunEventInput {
126
+ const payload = typeof input.payload === "object" && input.payload !== null ? (input.payload as Record<string, unknown>) : undefined;
127
+ const mirrored: Record<string, unknown> = {};
128
+ if (payload !== undefined) {
129
+ for (const member of MIRRORED) {
130
+ if (Object.prototype.hasOwnProperty.call(payload, member) && payload[member] !== undefined) {
131
+ mirrored[member] = payload[member];
132
+ }
133
+ }
134
+ }
135
+ return {
136
+ version: "run-event/1",
137
+ runId: input.runId,
138
+ at: runInstant(),
139
+ repo: { commonDir: input.commonDir },
140
+ kind: input.kind as RunEventKind,
141
+ actor: { role: input.role },
142
+ ...mirrored,
143
+ attestation: "self",
144
+ payload: (input.payload ?? null) as Readonly<Record<string, unknown>>,
145
+ } as RunEventInput;
146
+ }
147
+
148
+ /**
149
+ * One row's worth of a string: neutralized, then whitespace-collapsed so no
150
+ * free-text member can end a row and start one of its own, then bounded.
151
+ */
152
+ export function oneLine(value: string, maximum = 240): string {
153
+ const collapsed = neutralizeForDisplay(value).replace(/\s+/g, " ").trim();
154
+ return collapsed.length <= maximum ? collapsed : `${collapsed.slice(0, Math.max(maximum - 1, 0))}…`;
155
+ }
156
+
157
+ /** The same treatment for anything that is not already a string. */
158
+ export function oneLineOf(value: unknown, maximum = 240): string {
159
+ if (typeof value === "string") return oneLine(value, maximum);
160
+ if (value === undefined) return "";
161
+ return oneLine(JSON.stringify(value) ?? String(value), maximum);
162
+ }
163
+
164
+ /**
165
+ * The labels the row carries wherever it is printed. `bound to the record` is
166
+ * the one that differs from the viewer's: `verify` always has a record's tree
167
+ * sha, so its round constraints were judged against THIS candidate rather than
168
+ * against any paired round.
169
+ */
170
+ export const RUN_JOURNAL_ROW_LABELS = "self-attested; observability, not evidence; bound to the record";
171
+
172
+ /** Nothing was found, so nothing was evaluated. The one shape `absent` takes. */
173
+ const ABSENT: RunJournalRow = { status: "absent", missing: [], attestation: "self" };
174
+
175
+ /**
176
+ * The self-attested completeness row for the candidate a delivery record binds.
177
+ *
178
+ * FOUND BY THE RECORD'S TREE SHA, NOT BY THE POINTER. The run whose journal
179
+ * describes this candidate has usually ended by the time anyone verifies it,
180
+ * and `run.ended` clears the worktree pointer — so "the current run" is exactly
181
+ * the wrong question. The store scans instead, which is affordable because it
182
+ * is unpruned by design and small, and which is what lets a journal outlive the
183
+ * worktree its run happened in.
184
+ *
185
+ * EVERY FAILURE IS `absent`. No store, no match, a journal that refuses the
186
+ * read discipline, a journal that vanished between the scan and the read: the
187
+ * row says nothing was found rather than inventing a verdict. A reader that
188
+ * treats `absent` as a failure does so behind its own opt-in — this function
189
+ * never decides that.
190
+ */
191
+ export async function resolveRunJournalRow(input: {
192
+ readonly cwd: string;
193
+ readonly treeSha: string;
194
+ readonly mandatedLensIds?: readonly string[];
195
+ }): Promise<RunJournalRow> {
196
+ const resolved = await resolveRunSurface(input.cwd);
197
+ if (!resolved.ok) return ABSENT;
198
+ const match = await resolved.surface.store.findByCandidateTreeSha(input.treeSha);
199
+ if (match === undefined) return ABSENT;
200
+ const read = await resolved.surface.store.read(match.runId);
201
+ if (!read.ok) return ABSENT;
202
+ const evaluation = evaluateRunJournal(read.events, input.treeSha, input.mandatedLensIds);
203
+ return {
204
+ runId: match.runId,
205
+ ...(match.alsoMatching.length === 0 ? {} : { alsoMatching: match.alsoMatching }),
206
+ status: evaluation.status,
207
+ missing: evaluation.missing,
208
+ ...(evaluation.violations.length === 0 ? {} : { violations: evaluation.violations }),
209
+ attestation: "self",
210
+ };
211
+ }
212
+
213
+ /**
214
+ * The row, rendered for a terminal. Every store-derived string goes through
215
+ * `oneLine` for the same reason the viewer's rows do: a run id or a constraint
216
+ * name printed raw is a string from a file anyone who can execute here may
217
+ * write, and this one is printed under a line an operator reads as a verdict.
218
+ */
219
+ export function runJournalRows(row: RunJournalRow): readonly string[] {
220
+ const rows = [` run journal: ${oneLine(row.status, 64)} (${RUN_JOURNAL_ROW_LABELS})`];
221
+ if (row.runId !== undefined) rows.push(` run: ${oneLine(row.runId, 128)}`);
222
+ if (row.alsoMatching !== undefined && row.alsoMatching.length > 0) {
223
+ rows.push(` also matching: ${row.alsoMatching.map((id) => oneLine(id, 128)).join(", ")}`);
224
+ }
225
+ rows.push(` missing: ${row.missing.length === 0 ? "(none)" : row.missing.map((entry) => oneLine(entry, 64)).join(", ")}`);
226
+ if (row.violations !== undefined && row.violations.length > 0) {
227
+ rows.push(` violations: ${row.violations.map((entry) => oneLine(entry, 64)).join(", ")}`);
228
+ }
229
+ return rows;
230
+ }
231
+
232
+ /** The typed refusal a config-free command returns when it has nothing to work with. */
233
+ export function runSurfaceBlocker(input: {
234
+ readonly code: string;
235
+ readonly summary: string;
236
+ readonly details?: string;
237
+ readonly remediation: { readonly id: string; readonly summary: string };
238
+ }): Blocker {
239
+ return createBlocker({
240
+ code: input.code,
241
+ source: { kind: "command", id: RUN_SURFACE_SOURCE },
242
+ summary: input.summary,
243
+ ...(input.details === undefined ? {} : { details: input.details }),
244
+ remediations: [{ id: input.remediation.id, kind: "manual_action", summary: input.remediation.summary }],
245
+ });
246
+ }
247
+
248
+ /**
249
+ * Whether a `harness.config.ts` sits at this worktree root. `lstat` only: the
250
+ * module is never imported, never read, never parsed. The note this answers is
251
+ * presentational, and the file it looks for is candidate-committed, so its
252
+ * absence is suppressible by anyone who can write the tree — it bounds
253
+ * accident, not tampering.
254
+ */
255
+ export function harnessConfigPresentAt(rootDir: string): boolean {
256
+ try {
257
+ return lstatSync(path.join(rootDir, "harness.config.ts")).isFile();
258
+ } catch {
259
+ return false;
260
+ }
261
+ }