@outerlayer/cli 0.1.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 (55) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +202 -0
  3. package/README.md +320 -0
  4. package/dist/build-info.json +1 -0
  5. package/dist/chunk-4H56Y7O6.js +589 -0
  6. package/dist/chunk-A3WLZX2F.js +120 -0
  7. package/dist/chunk-A6NNKRJU.js +650 -0
  8. package/dist/chunk-D77LS3UI.js +42 -0
  9. package/dist/chunk-DEDV5V4U.js +87324 -0
  10. package/dist/chunk-ECSYRPCC.js +334 -0
  11. package/dist/chunk-IWIUYLDR.js +472 -0
  12. package/dist/chunk-JJP7YLMN.js +25 -0
  13. package/dist/chunk-KFYJV2ZG.js +7978 -0
  14. package/dist/chunk-OZ7C3XUE.js +34 -0
  15. package/dist/chunk-RCQXYLMO.js +96 -0
  16. package/dist/chunk-TFUIDMOB.js +34 -0
  17. package/dist/chunk-VNVZDWO3.js +10 -0
  18. package/dist/chunk-VU34RWDU.js +339 -0
  19. package/dist/chunk-WQ6VGRGZ.js +150 -0
  20. package/dist/chunk-XN2XZFTK.js +165 -0
  21. package/dist/chunk-YMTXR7PJ.js +200 -0
  22. package/dist/chunk-YRYMNBGQ.js +54 -0
  23. package/dist/chunk-Z5GEFMRW.js +45 -0
  24. package/dist/chunk-ZJSNLVCE.js +58 -0
  25. package/dist/cli-3JYGL2OB.js +5311 -0
  26. package/dist/config-POF7DEQW.js +7 -0
  27. package/dist/context-materialize-J6CRQ7O7.js +589 -0
  28. package/dist/emit-artifact-cmd-23F4YKQ3.js +339 -0
  29. package/dist/emit-cmd-TWSYYEZI.js +208 -0
  30. package/dist/emit-commit-credit-cmd-2GX5M2BR.js +121 -0
  31. package/dist/emit-finding-cmd-6K7ERPKG.js +235 -0
  32. package/dist/emit-result-cmd-NDFHMJX7.js +225 -0
  33. package/dist/hook-fast-CBW4ZETX.js +8 -0
  34. package/dist/hook-wrap-fast-36WGE6EX.js +8 -0
  35. package/dist/import-capture-cmd-EUMBGIV3.js +176 -0
  36. package/dist/import-ruler-cmd-7LH2QOMG.js +173 -0
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +25 -0
  39. package/dist/init-PTBITAUO.js +103 -0
  40. package/dist/login-cmd-IRX6LZT7.js +62 -0
  41. package/dist/logs-TRNPQM42.js +63 -0
  42. package/dist/loop-NFSOBUUJ.js +646 -0
  43. package/dist/mcp-install-cmd-FDQH6SEN.js +75 -0
  44. package/dist/mcp-serve-cmd-57EZZOTL.js +123 -0
  45. package/dist/paths-D2VGWWFI.js +6 -0
  46. package/dist/pidfile-PTW76F56.js +8 -0
  47. package/dist/status-WGOJTXZF.js +94 -0
  48. package/dist/statusline-fast-Z5U5CEC6.js +8 -0
  49. package/dist/sync-cmd-C5UGI4SF.js +15 -0
  50. package/dist/watch-V3K4PESQ.js +70 -0
  51. package/dist/work-claim-cmd-FQDWQVT2.js +98 -0
  52. package/dist/work-cmd-CW26EZZO.js +16 -0
  53. package/dist/work-launch-YNNCKIH3.js +6 -0
  54. package/dist/work-pr-cmd-W2QNMWXP.js +115 -0
  55. package/package.json +58 -0
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { FindingBatchSchema, resolveRepoIdentity, detectActiveSession, launchRefusalForSession } from './chunk-DEDV5V4U.js';
4
+ import './chunk-YRYMNBGQ.js';
5
+ import { launchedItemForSession, parseTrackerNumber, trackerNumberError } from './chunk-RCQXYLMO.js';
6
+ import { readLatestProvenanceForSession } from './chunk-ZJSNLVCE.js';
7
+ import './chunk-A6NNKRJU.js';
8
+ import './chunk-IWIUYLDR.js';
9
+ import './chunk-YMTXR7PJ.js';
10
+ import './chunk-ECSYRPCC.js';
11
+ import { readCloudConfig } from './chunk-TFUIDMOB.js';
12
+ import { homeFromEnv } from './chunk-VNVZDWO3.js';
13
+ import './chunk-Z5GEFMRW.js';
14
+ import { readFileSync } from 'fs';
15
+
16
+ createRequire(import.meta.url);
17
+ var GREEN = "\x1B[32m";
18
+ var DIM = "\x1B[2m";
19
+ var RESET = "\x1B[0m";
20
+ var EmitFindingError = class extends Error {
21
+ };
22
+ function parseItemFlag(raw) {
23
+ if (raw === void 0) return void 0;
24
+ const number = parseTrackerNumber(raw);
25
+ if (number === void 0) {
26
+ throw new EmitFindingError(trackerNumberError("--item", raw));
27
+ }
28
+ return { number };
29
+ }
30
+ function parseIntFlag(raw, flag, opts) {
31
+ if (raw === void 0) return void 0;
32
+ if (!/^-?\d+$/.test(raw)) {
33
+ throw new EmitFindingError(`invalid ${flag} "${raw}" \u2014 expected a whole number`);
34
+ }
35
+ const n = Number(raw);
36
+ if (!Number.isSafeInteger(n) || n < opts.min) {
37
+ throw new EmitFindingError(`invalid ${flag} "${raw}" \u2014 expected a whole number >= ${opts.min}`);
38
+ }
39
+ return n;
40
+ }
41
+ async function readAllStdin() {
42
+ const chunks = [];
43
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
44
+ return Buffer.concat(chunks).toString("utf8");
45
+ }
46
+ function recordFromFlags(opts) {
47
+ const line = parseIntFlag(opts.line, "--line", { min: 1 });
48
+ const ruleLine = parseIntFlag(opts.ruleLine, "--rule-line", { min: 1 });
49
+ const turnIndex = parseIntFlag(opts.turn, "--turn", { min: 0 });
50
+ const round = parseIntFlag(opts.round, "--round", { min: 0 });
51
+ return {
52
+ id: opts.id,
53
+ subject: opts.subject,
54
+ title: opts.title,
55
+ file: opts.file,
56
+ line: line ?? null,
57
+ severity: opts.severity ?? null,
58
+ kind: opts.kind,
59
+ verdict: opts.verdict,
60
+ fixed: opts.fixed ?? false,
61
+ rule: opts.rulePath !== void 0 ? {
62
+ path: opts.rulePath,
63
+ line: ruleLine ?? null,
64
+ quote: opts.ruleQuote ?? "",
65
+ relation: opts.ruleRelation
66
+ } : null,
67
+ source: opts.source,
68
+ where: {
69
+ label: opts.where,
70
+ ...opts.traceId !== void 0 ? { traceId: opts.traceId } : {},
71
+ ...turnIndex !== void 0 ? { turnIndex } : {},
72
+ ...round !== void 0 ? { round } : {}
73
+ }
74
+ };
75
+ }
76
+ function firstIssueMessage(result) {
77
+ const issue = result.error.issues[0];
78
+ return issue ? `${issue.path.join(".")}: ${issue.message}` : "malformed";
79
+ }
80
+ function detectUploadSessionId(opts) {
81
+ const identity = resolveRepoIdentity(opts.cwd);
82
+ return detectActiveSession({
83
+ env: opts.env,
84
+ home: opts.home,
85
+ cwds: identity.repoRoot !== void 0 ? [identity.repoRoot, opts.cwd] : [opts.cwd],
86
+ nowMs: opts.now().getTime()
87
+ });
88
+ }
89
+ function resolveUploadTarget(opts) {
90
+ const item = parseItemFlag(opts.item);
91
+ const sessionId = opts.sessionId;
92
+ if (sessionId !== void 0) {
93
+ const refusal = launchRefusalForSession(sessionId, opts.home);
94
+ if (refusal !== void 0) throw new EmitFindingError(refusal);
95
+ }
96
+ if (item !== void 0) {
97
+ return sessionId !== void 0 ? { item, sessionId } : { item };
98
+ }
99
+ if (sessionId === void 0) {
100
+ throw new EmitFindingError(
101
+ "nothing to attach this to \u2014 no work item. Pass --item <number>, or run inside a recorded session launched with OUTERLAYER_WORK \u2014 its own launch record names the item automatically."
102
+ );
103
+ }
104
+ const launchedNumber = launchedItemForSession(sessionId, opts.home);
105
+ if (launchedNumber === void 0) {
106
+ throw new EmitFindingError(
107
+ "nothing to attach this to \u2014 no work item. Pass --item <number>; this recorded session's own launch record names no item either."
108
+ );
109
+ }
110
+ return { item: { number: launchedNumber }, sessionId };
111
+ }
112
+ async function upload(batch, target, opts) {
113
+ const fileConfig = readCloudConfig(opts.home);
114
+ const url = opts.url ?? opts.env.OUTERLAYER_URL ?? fileConfig.url;
115
+ const apiKey = opts.apiKey ?? opts.env.OUTERLAYER_API_KEY ?? fileConfig.apiKey;
116
+ const appId = opts.appId ?? opts.env.OUTERLAYER_APP_ID ?? fileConfig.appId;
117
+ if (!url || !apiKey || !appId) {
118
+ const missing = [!url && "--url", !apiKey && "--api-key", !appId && "--app-id"].filter(Boolean).join(", ");
119
+ throw new EmitFindingError(
120
+ `missing ${missing}. Run \`outerlayer login --url <your gateway>\` and paste the key at the prompt, or set OUTERLAYER_URL / OUTERLAYER_API_KEY / OUTERLAYER_APP_ID. Mint a key in the dashboard: Settings \u2192 API keys.`
121
+ );
122
+ }
123
+ const request = {
124
+ ...batch,
125
+ item: target.item,
126
+ ...target.sessionId !== void 0 ? { sessionId: target.sessionId } : {}
127
+ };
128
+ const fetchImpl = opts.fetchImpl ?? fetch;
129
+ const endpoint = new URL("/v1/findings", url).toString();
130
+ let response;
131
+ try {
132
+ response = await fetchImpl(endpoint, {
133
+ method: "POST",
134
+ headers: {
135
+ "content-type": "application/json",
136
+ authorization: `Bearer ${apiKey}`,
137
+ "x-outerlayer-app-id": appId
138
+ },
139
+ body: JSON.stringify(request)
140
+ });
141
+ } catch (err) {
142
+ throw new EmitFindingError(`network error reaching ${endpoint}: ${String(err)}`);
143
+ }
144
+ if (response.status === 401) {
145
+ throw new EmitFindingError(
146
+ "not authorized \u2014 the API key is unknown, expired, or bound to a different app. Mint a fresh key: dashboard \u2192 Settings \u2192 API keys."
147
+ );
148
+ }
149
+ if (!response.ok) {
150
+ let detail = "";
151
+ try {
152
+ detail = (await response.text()).replace(/\s+/g, " ").trim().slice(0, 160);
153
+ } catch {
154
+ }
155
+ throw new EmitFindingError(`emit failed (${response.status})${detail ? `: ${detail}` : ""}`);
156
+ }
157
+ let data = {};
158
+ try {
159
+ data = (await response.json()).data ?? {};
160
+ } catch {
161
+ }
162
+ const count = typeof data.count === "number" ? data.count : batch.findings.length;
163
+ const output = opts.json ? JSON.stringify(data) : `${GREEN}\u2713${RESET} recorded ${count} ${count === 1 ? "finding" : "findings"} ${DIM}\xB7${RESET} item #${target.item.number}`;
164
+ if (!opts.quiet) process.stdout.write(output + "\n");
165
+ return { data, output, exitCode: 0 };
166
+ }
167
+ async function runEmitFinding(opts) {
168
+ const cwd = opts.cwd ?? process.cwd();
169
+ const home = opts.home ?? homeFromEnv();
170
+ const env = opts.env ?? process.env;
171
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
172
+ const sessionId = detectUploadSessionId({ cwd, home, env, now });
173
+ const contextSourceSha = opts.contextSourceSha ?? (sessionId !== void 0 ? readLatestProvenanceForSession(sessionId, home)?.sha ?? void 0 : void 0);
174
+ const rawBatch = {
175
+ schemaVersion: 1,
176
+ ...opts.headSha !== void 0 ? { headSha: opts.headSha } : {},
177
+ ...contextSourceSha !== void 0 ? { contextSourceSha } : {},
178
+ findings: [recordFromFlags(opts)]
179
+ };
180
+ const parsed = FindingBatchSchema.safeParse(rawBatch);
181
+ if (!parsed.success) {
182
+ throw new EmitFindingError(`invalid finding \u2014 ${firstIssueMessage(parsed)}`);
183
+ }
184
+ const target = resolveUploadTarget({ item: opts.item, sessionId, home });
185
+ return upload(parsed.data, target, {
186
+ json: opts.json,
187
+ url: opts.url,
188
+ apiKey: opts.apiKey,
189
+ appId: opts.appId,
190
+ quiet: opts.quiet,
191
+ home,
192
+ env,
193
+ fetchImpl: opts.fetchImpl
194
+ });
195
+ }
196
+ async function runEmitFindings(opts) {
197
+ const cwd = opts.cwd ?? process.cwd();
198
+ const home = opts.home ?? homeFromEnv();
199
+ const env = opts.env ?? process.env;
200
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
201
+ let text;
202
+ if (opts.file === "-") {
203
+ text = await (opts.readStdin ?? readAllStdin)();
204
+ } else {
205
+ try {
206
+ text = readFileSync(opts.file, "utf8");
207
+ } catch (err) {
208
+ throw new EmitFindingError(`cannot read "${opts.file}": ${String(err)}`);
209
+ }
210
+ }
211
+ let raw;
212
+ try {
213
+ raw = JSON.parse(text);
214
+ } catch (err) {
215
+ throw new EmitFindingError(`"${opts.file}" is not valid JSON: ${String(err)}`);
216
+ }
217
+ const parsed = FindingBatchSchema.safeParse(raw);
218
+ if (!parsed.success) {
219
+ throw new EmitFindingError(`invalid finding batch \u2014 ${firstIssueMessage(parsed)}`);
220
+ }
221
+ const sessionId = detectUploadSessionId({ cwd, home, env, now });
222
+ const target = resolveUploadTarget({ item: opts.item, sessionId, home });
223
+ return upload(parsed.data, target, {
224
+ json: opts.json,
225
+ url: opts.url,
226
+ apiKey: opts.apiKey,
227
+ appId: opts.appId,
228
+ quiet: opts.quiet,
229
+ home,
230
+ env,
231
+ fetchImpl: opts.fetchImpl
232
+ });
233
+ }
234
+
235
+ export { EmitFindingError, runEmitFinding, runEmitFindings };
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { EmittedNameSchema, ARTIFACT_REVIEW_CHECK_NAME, EMITTED_RESULT_MAX_LINK_LENGTH, EMITTED_RESULTS, EMITTED_RESULT_MAX_BODY_LENGTH, resolveRepoIdentity, detectActiveSession, launchRefusalForSession, detectCi } from './chunk-DEDV5V4U.js';
4
+ import './chunk-YRYMNBGQ.js';
5
+ import { parseTrackerNumber, trackerNumberError } from './chunk-RCQXYLMO.js';
6
+ import './chunk-ZJSNLVCE.js';
7
+ import './chunk-A6NNKRJU.js';
8
+ import './chunk-IWIUYLDR.js';
9
+ import './chunk-YMTXR7PJ.js';
10
+ import './chunk-ECSYRPCC.js';
11
+ import { readCloudConfig } from './chunk-TFUIDMOB.js';
12
+ import { homeFromEnv } from './chunk-VNVZDWO3.js';
13
+ import './chunk-Z5GEFMRW.js';
14
+ import { randomUUID } from 'crypto';
15
+ import { readFileSync } from 'fs';
16
+
17
+ createRequire(import.meta.url);
18
+ var GREEN = "\x1B[32m";
19
+ var DIM = "\x1B[2m";
20
+ var RESET = "\x1B[0m";
21
+ var EmitResultError = class extends Error {
22
+ };
23
+ function parseItemFlag(raw) {
24
+ if (raw === void 0) return void 0;
25
+ const number = parseTrackerNumber(raw);
26
+ if (number === void 0) {
27
+ throw new EmitResultError(trackerNumberError("--item", raw));
28
+ }
29
+ return { number };
30
+ }
31
+ async function readAllStdin() {
32
+ const chunks = [];
33
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
34
+ return Buffer.concat(chunks).toString("utf8");
35
+ }
36
+ async function readBodyFile(path, readStdin) {
37
+ if (path === "-") return (readStdin ?? readAllStdin)();
38
+ try {
39
+ return readFileSync(path, "utf8");
40
+ } catch (err) {
41
+ throw new EmitResultError(`cannot read --body-file "${path}": ${String(err)}`);
42
+ }
43
+ }
44
+ async function runEmitResult(opts) {
45
+ const cwd = opts.cwd ?? process.cwd();
46
+ const home = opts.home ?? homeFromEnv();
47
+ const env = opts.env ?? process.env;
48
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
49
+ if (!EmittedNameSchema.safeParse(opts.name).success) {
50
+ throw new EmitResultError(
51
+ `invalid name "${opts.name}" \u2014 an emit name is 1-64 characters: a lowercase letter followed by lowercase letters, digits, ".", "_" or "-" (e.g. smoke.pass)`
52
+ );
53
+ }
54
+ const isArtifactReview = opts.name === ARTIFACT_REVIEW_CHECK_NAME;
55
+ const artifactId = opts.artifact === "" ? void 0 : opts.artifact;
56
+ if (isArtifactReview) {
57
+ if (artifactId === void 0) {
58
+ throw new EmitResultError(
59
+ `--artifact <id> is required for ${ARTIFACT_REVIEW_CHECK_NAME} \u2014 it names which artifact this verdict is on`
60
+ );
61
+ }
62
+ } else if (artifactId !== void 0) {
63
+ throw new EmitResultError(`--artifact is only accepted with ${ARTIFACT_REVIEW_CHECK_NAME}`);
64
+ }
65
+ const link = opts.link === "" ? void 0 : opts.link;
66
+ if (isArtifactReview && link !== void 0) {
67
+ throw new EmitResultError(
68
+ `--link is refused for ${ARTIFACT_REVIEW_CHECK_NAME} \u2014 an artifact verdict is a judgment on evidence already on file, never a run to point at; pass --body / --body-file instead`
69
+ );
70
+ }
71
+ if (link !== void 0) {
72
+ if (!link.startsWith("http://") && !link.startsWith("https://")) {
73
+ throw new EmitResultError(`invalid --link "${link}" \u2014 expected an http:// or https:// URL`);
74
+ }
75
+ if (/\s/.test(link)) {
76
+ throw new EmitResultError(`invalid --link \u2014 URLs cannot contain whitespace; percent-encode it`);
77
+ }
78
+ if (link.length > EMITTED_RESULT_MAX_LINK_LENGTH) {
79
+ throw new EmitResultError(
80
+ `link is ${link.length} characters \u2014 the cap is ${EMITTED_RESULT_MAX_LINK_LENGTH}`
81
+ );
82
+ }
83
+ }
84
+ let result = "pass";
85
+ if (opts.result !== void 0) {
86
+ if (!EMITTED_RESULTS.includes(opts.result)) {
87
+ throw new EmitResultError(`invalid --result "${opts.result}" \u2014 expected "pass" or "fail"`);
88
+ }
89
+ result = opts.result;
90
+ }
91
+ if (opts.body !== void 0 && opts.bodyFile !== void 0) {
92
+ throw new EmitResultError(
93
+ `--body and --body-file both name the sentence \u2014 pass one, not both`
94
+ );
95
+ }
96
+ let body;
97
+ if (opts.body !== void 0) {
98
+ body = opts.body;
99
+ } else if (opts.bodyFile !== void 0) {
100
+ body = await readBodyFile(opts.bodyFile, opts.readStdin);
101
+ } else if (isArtifactReview && result === "fail") {
102
+ body = await (opts.readStdin ?? readAllStdin)();
103
+ }
104
+ if (body !== void 0) {
105
+ body = body.trim();
106
+ if (body === "") {
107
+ throw new EmitResultError(
108
+ `the sentence is empty \u2014 --body and --body-file need something to say`
109
+ );
110
+ }
111
+ if (body.length > EMITTED_RESULT_MAX_BODY_LENGTH) {
112
+ throw new EmitResultError(
113
+ `the sentence is ${body.length} characters \u2014 the cap is ${EMITTED_RESULT_MAX_BODY_LENGTH}`
114
+ );
115
+ }
116
+ }
117
+ if (isArtifactReview) {
118
+ if (result === "fail" && body === void 0) {
119
+ throw new EmitResultError(
120
+ `a failing artifact verdict needs a sentence \u2014 pass --body or --body-file with one sentence saying what is wrong`
121
+ );
122
+ }
123
+ } else if (result === "fail" && link === void 0 && body === void 0) {
124
+ throw new EmitResultError(
125
+ `a failing check needs something to act on \u2014 pass --link with the run URL, or --body / --body-file with one sentence saying what is wrong`
126
+ );
127
+ }
128
+ const item = parseItemFlag(opts.item);
129
+ const identity = resolveRepoIdentity(cwd);
130
+ const sessionId = detectActiveSession({
131
+ env,
132
+ home,
133
+ cwds: identity.repoRoot !== void 0 ? [identity.repoRoot, cwd] : [cwd],
134
+ nowMs: now().getTime()
135
+ });
136
+ if (isArtifactReview && sessionId !== void 0) {
137
+ throw new EmitResultError(
138
+ "an artifact verdict is a person's act \u2014 record it with a person's API key, outside a recorded session."
139
+ );
140
+ }
141
+ if (!isArtifactReview) {
142
+ if (item === void 0 && sessionId === void 0) {
143
+ throw new EmitResultError(
144
+ "nothing to attach this to \u2014 no work item. Pass --item <number>; a recorded session alone does not name one, and a session with no --item is always refused as self-judgment."
145
+ );
146
+ }
147
+ if (sessionId !== void 0) {
148
+ const refusal = launchRefusalForSession(sessionId, home);
149
+ if (refusal !== void 0) throw new EmitResultError(refusal);
150
+ }
151
+ if (item === void 0 && sessionId !== void 0) {
152
+ throw new EmitResultError(
153
+ "a session records a check only on a work item it was not launched for, named with --item \u2014 the item this session was launched for needs a machine key (an API key run outside the recorded session) or CI."
154
+ );
155
+ }
156
+ }
157
+ const fileConfig = readCloudConfig(home);
158
+ const url = opts.url ?? env.OUTERLAYER_URL ?? fileConfig.url;
159
+ const apiKey = opts.apiKey ?? env.OUTERLAYER_API_KEY ?? fileConfig.apiKey;
160
+ const appId = opts.appId ?? env.OUTERLAYER_APP_ID ?? fileConfig.appId;
161
+ if (!url || !apiKey || !appId) {
162
+ const missing = [!url && "--url", !apiKey && "--api-key", !appId && "--app-id"].filter(Boolean).join(", ");
163
+ throw new EmitResultError(
164
+ `missing ${missing}. Run \`outerlayer login --url <your gateway>\` and paste the key at the prompt, or set OUTERLAYER_URL / OUTERLAYER_API_KEY / OUTERLAYER_APP_ID. Mint a key in the dashboard: Settings \u2192 API keys.`
165
+ );
166
+ }
167
+ const ci = detectCi(env);
168
+ const request = {
169
+ schemaVersion: 1,
170
+ emit: {
171
+ clientEmitId: randomUUID(),
172
+ name: opts.name,
173
+ result,
174
+ ...link !== void 0 ? { link } : {},
175
+ ...body !== void 0 ? { body } : {},
176
+ emittedAt: now().toISOString(),
177
+ ...ci ? { ci: true } : {},
178
+ ...sessionId !== void 0 ? { sessionId } : {},
179
+ ...item !== void 0 ? { item } : {},
180
+ ...isArtifactReview ? { artifactId } : {}
181
+ }
182
+ };
183
+ const fetchImpl = opts.fetchImpl ?? fetch;
184
+ const endpoint = new URL("/v1/emitted-results", url).toString();
185
+ let response;
186
+ try {
187
+ response = await fetchImpl(endpoint, {
188
+ method: "POST",
189
+ headers: {
190
+ "content-type": "application/json",
191
+ authorization: `Bearer ${apiKey}`,
192
+ "x-outerlayer-app-id": appId
193
+ },
194
+ body: JSON.stringify(request)
195
+ });
196
+ } catch (err) {
197
+ throw new EmitResultError(`network error reaching ${endpoint}: ${String(err)}`);
198
+ }
199
+ if (response.status === 401) {
200
+ throw new EmitResultError(
201
+ "not authorized \u2014 the API key is unknown, expired, or bound to a different app. Mint a fresh key: dashboard \u2192 Settings \u2192 API keys."
202
+ );
203
+ }
204
+ if (!response.ok) {
205
+ let detail = "";
206
+ try {
207
+ detail = (await response.text()).replace(/\s+/g, " ").trim().slice(0, 160);
208
+ } catch {
209
+ }
210
+ throw new EmitResultError(`emit failed (${response.status})${detail ? `: ${detail}` : ""}`);
211
+ }
212
+ let data = {};
213
+ try {
214
+ data = (await response.json()).data ?? {};
215
+ } catch {
216
+ }
217
+ const anchorLabel = isArtifactReview ? `artifact ${artifactId}` : `item #${item.number}`;
218
+ const recordedId = typeof data.id === "string" ? data.id : "";
219
+ const idSuffix = recordedId ? ` ${DIM}\xB7${RESET} ${recordedId}` : "";
220
+ const output = opts.json ? JSON.stringify(data) : `${GREEN}\u2713${RESET} emitted ${opts.name} (${result}) ${DIM}\xB7${RESET} ${anchorLabel}${idSuffix}`;
221
+ if (!opts.quiet) process.stdout.write(output + "\n");
222
+ return { data, output, exitCode: 0 };
223
+ }
224
+
225
+ export { EmitResultError, runEmitResult };
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ export { AUTO_SYNC_DEBOUNCE_MS, WORK_ADD_RETRIES, contextPossiblyGoverned, handleWorkLaunch, hookFiredMarkerPath, maybeAutoSync, runHookFast, spoolDir } from './chunk-A6NNKRJU.js';
4
+ import './chunk-IWIUYLDR.js';
5
+ import './chunk-VNVZDWO3.js';
6
+ import './chunk-Z5GEFMRW.js';
7
+
8
+ createRequire(import.meta.url);
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ export { hookExecSpoolPath, hookWrapStdio, installAbortForwarding, makeTailCollector, parseHookWrapArgs, runHookWrapFast, spoolDir } from './chunk-YMTXR7PJ.js';
4
+ import './chunk-ECSYRPCC.js';
5
+ import './chunk-VNVZDWO3.js';
6
+ import './chunk-Z5GEFMRW.js';
7
+
8
+ createRequire(import.meta.url);
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import './chunk-Z5GEFMRW.js';
4
+ import { statSync, existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
5
+ import { join, dirname } from 'path';
6
+
7
+ createRequire(import.meta.url);
8
+ var GREEN = "\x1B[32m";
9
+ var DIM = "\x1B[2m";
10
+ var YELLOW = "\x1B[33m";
11
+ var RESET = "\x1B[0m";
12
+ var ImportCaptureError = class extends Error {
13
+ };
14
+ var SKILL_PATH = ".outerlayer/skills/emitting-evidence/SKILL.md";
15
+ var SNIPPET_PATH = ".outerlayer/skills/emitting-evidence/references/agents-snippet.md";
16
+ var AGENTS_MD_PATH = ".outerlayer/AGENTS.md";
17
+ var AGENTS_MD_MARKER = "<!-- outerlayer:capture-pack -->";
18
+ var SKILL_CONTENT = `---
19
+ name: emitting-evidence
20
+ description: >
21
+ Emit artifacts \u2014 screenshots, recordings, reports, logs \u2014 as proof that a
22
+ change works, bound to the pull request via \`outerlayer emit artifact\`.
23
+ Use when finishing work a spec criterion or tracked requirement covers,
24
+ when asked to "provide evidence", "attach a screenshot", or "prove it
25
+ works", and in CI steps that produce verifiable output.
26
+ ---
27
+
28
+ An artifact is an exhibit: evidence *of* a specific change, anchored to the
29
+ pull request and rendered in its evidence comment. Emit the few exhibits the
30
+ spec asks for \u2014 a reviewer should see the change working, not a gallery.
31
+
32
+ ## When to capture
33
+
34
+ Capture AFTER the state exists, never before: run the app, the test, or the
35
+ flow first, and shoot the working result. A screenshot of code is not
36
+ evidence; a screenshot of the rendered page is. If the state takes setup
37
+ (seed data, a logged-in user), finish the setup, verify by eye, then capture.
38
+
39
+ ## Per-kind mechanics
40
+
41
+ Kind is inferred from the file's media type \u2014 name files honestly. Every
42
+ artifact shares one upload cap of 8 MiB, whatever its kind:
43
+
44
+ - **screenshot** (\`.png\`, \`.jpg\`) \u2014 one focused window or region showing the
45
+ proven state. Crop noise; keep enough chrome (URL bar, test summary line)
46
+ to show it is real.
47
+ - **video** (\`.webm\`, \`.mp4\`) \u2014 a short recording of the flow, start to
48
+ outcome. Video is the kind most likely to hit the 8 MiB cap: trim dead
49
+ time, prefer webm.
50
+ - **report** (\`.html\`, \`.pdf\`) \u2014 generated reports: coverage, benchmark,
51
+ audit output. Emit the file the tool produced, unedited.
52
+ - **log** (\`.txt\`, \`.log\`) \u2014 command output proving a run happened: test
53
+ runs, migrations, gate output. Pipe to a file and emit that file.
54
+
55
+ Anything else uploads as plain \`file\` \u2014 it is never guessed into a stronger
56
+ kind, so a \`.mov\` will NOT count where a video is required; convert first.
57
+
58
+ ## Captions
59
+
60
+ One sentence, present tense, saying what the exhibit shows and what that
61
+ proves: "Signup blocked for a disallowed domain \u2014 the 403 page renders."
62
+ Never put secrets, tokens, or personal data in the caption \u2014 or the pixels.
63
+
64
+ ## Binding with --for
65
+
66
+ When a criterion is the reason you captured, bind it:
67
+
68
+ outerlayer emit artifact shot.png --caption "\u2026" --for LOGIN-01
69
+
70
+ The id comes from your team's own spec or tracker \u2014 wherever criteria for
71
+ this change are written down. The PR comment's Evidence section shows the
72
+ id next to the artifact, and the dashboard's Floor lists it against the
73
+ work item \u2014 but deciding whether a given criterion still needs proof, and
74
+ of what kind, is your team's own check to write, typically a custom
75
+ validator recorded with \`outerlayer emit\`.
76
+
77
+ ## The noise rule
78
+
79
+ Satisfy the declared proofs; don't document everything. One exhibit per
80
+ criterion is the norm. An unrequested artifact is worth emitting only when
81
+ it would change how a reviewer reads the diff. When in doubt, leave it out \u2014
82
+ evidence works because there is little of it.
83
+
84
+ ## Mechanics
85
+
86
+ Inside a recorded session, just run the command \u2014 the artifact spools
87
+ locally and uploads with the next \`outerlayer sync\`, bound to this session
88
+ and turn. In CI, run it after the step that produced the file (repo and PR
89
+ come from the CI environment). From a plain machine it anchors through the
90
+ git checkout, or pass \`--pr <n>\` explicitly. If there is nothing to attach
91
+ to, the command refuses \u2014 emit from the work, not from nowhere.
92
+ `;
93
+ var AGENTS_SNIPPET_CONTENT = `${AGENTS_MD_MARKER}
94
+ ## Emitting evidence
95
+
96
+ When you finish work a spec criterion or tracked requirement covers,
97
+ capture the working state and emit it:
98
+
99
+ outerlayer emit artifact <file> --caption "what it shows" [--for <criterion-id>] [--pr <n>]
100
+
101
+ Rules: capture AFTER the state exists (run it, then shoot it); kind comes
102
+ from the file type (png/jpg screenshot, webm/mp4 video, html/pdf report,
103
+ txt/log log \u2014 anything else is a plain file); every artifact caps at 8
104
+ MiB, and video is the kind most likely to hit it; captions are one
105
+ present-tense sentence with no secrets; bind an artifact to the id of the
106
+ requirement it proves \u2014 \`--for <id>\`, from the team's own spec or
107
+ tracker \u2014 and pick the kind that shows the state best; the platform
108
+ records the kind and the id and shows them next to the artifact; satisfy
109
+ what's asked for and stop \u2014 don't document everything. Inside a recorded
110
+ session the artifact uploads on the next \`outerlayer sync\`; in CI it
111
+ anchors via the CI environment; otherwise the git checkout or \`--pr\`
112
+ anchors it, and with nothing to attach to the command refuses.
113
+ `;
114
+ function writeOutFile(repoRoot, relPath, content) {
115
+ const abs = join(repoRoot, relPath);
116
+ mkdirSync(dirname(abs), { recursive: true });
117
+ writeFileSync(abs, content);
118
+ }
119
+ function runImportCapture(opts = {}) {
120
+ const cwd = opts.cwd ?? process.cwd();
121
+ let cwdStat;
122
+ try {
123
+ cwdStat = statSync(cwd);
124
+ } catch {
125
+ throw new ImportCaptureError(`no such directory: ${cwd}`);
126
+ }
127
+ if (!cwdStat.isDirectory()) throw new ImportCaptureError(`not a directory: ${cwd}`);
128
+ const skillAbs = join(cwd, SKILL_PATH);
129
+ if (existsSync(skillAbs) && readFileSync(skillAbs, "utf8") !== SKILL_CONTENT) {
130
+ throw new ImportCaptureError(
131
+ `refusing to import \u2014 ${SKILL_PATH} already exists with local edits (never overwritten; remove it first to reinstall)`
132
+ );
133
+ }
134
+ const files = [];
135
+ for (const [relPath, content] of [
136
+ [SKILL_PATH, SKILL_CONTENT],
137
+ [SNIPPET_PATH, AGENTS_SNIPPET_CONTENT]
138
+ ]) {
139
+ if (existsSync(join(cwd, relPath))) continue;
140
+ writeOutFile(cwd, relPath, content);
141
+ files.push(relPath);
142
+ }
143
+ let appendedAgentsMd = false;
144
+ const agentsMdAbs = join(cwd, AGENTS_MD_PATH);
145
+ if (existsSync(agentsMdAbs)) {
146
+ const existing = readFileSync(agentsMdAbs, "utf8");
147
+ if (!existing.includes(AGENTS_MD_MARKER)) {
148
+ writeFileSync(agentsMdAbs, existing + "\n" + AGENTS_SNIPPET_CONTENT);
149
+ appendedAgentsMd = true;
150
+ }
151
+ }
152
+ let output;
153
+ if (opts.json) {
154
+ output = JSON.stringify({ files, appendedAgentsMd });
155
+ } else {
156
+ const lines = files.map((f) => `${GREEN}\u2713${RESET} wrote ${f}`);
157
+ if (appendedAgentsMd) lines.push(`${GREEN}\u2713${RESET} appended the evidence snippet to ${AGENTS_MD_PATH}`);
158
+ if (files.length === 0 && !appendedAgentsMd) {
159
+ lines.push(`${DIM}capture pack already installed \u2014 nothing to write${RESET}`);
160
+ }
161
+ lines.push(`Run ${YELLOW}\`outerlayer emit\`${RESET} to compile the pack into your agent's native files.`);
162
+ output = lines.join("\n");
163
+ }
164
+ if (!opts.quiet) {
165
+ process.stdout.write(output + "\n");
166
+ if (!opts.json && !appendedAgentsMd && !existsSync(agentsMdAbs)) {
167
+ process.stdout.write(
168
+ `${DIM} no ${AGENTS_MD_PATH} to append to \u2014 paste ${SNIPPET_PATH} into your instructions file if your harness reads no skills${RESET}
169
+ `
170
+ );
171
+ }
172
+ }
173
+ return { files, appendedAgentsMd, output };
174
+ }
175
+
176
+ export { ImportCaptureError, runImportCapture };