@outerlayer/cli 0.1.1 → 0.4.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 (143) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +238 -77
  3. package/dist/agent-setup-IFH3FB5X.js +197 -0
  4. package/dist/build-TZUUTRGH.js +9 -0
  5. package/dist/build-info.json +1 -1
  6. package/dist/check-WQP2CZDW.js +94 -0
  7. package/dist/chunk-2E54Z3P5.js +232 -0
  8. package/dist/chunk-3TT7RQYB.js +55 -0
  9. package/dist/chunk-3YPKXE5L.js +1683 -0
  10. package/dist/{chunk-HNCCD2IX.js → chunk-5DLBAXY5.js} +209 -61
  11. package/dist/{chunk-R5KBGVII.js → chunk-5QRP3MVS.js} +30 -14
  12. package/dist/chunk-5Y3QQRUH.js +300 -0
  13. package/dist/chunk-6Y63SXFO.js +383 -0
  14. package/dist/chunk-774HEQL6.js +178 -0
  15. package/dist/chunk-7A6RVXPZ.js +2233 -0
  16. package/dist/chunk-7YIOOFVJ.js +71 -0
  17. package/dist/chunk-ABIWDUMS.js +104 -0
  18. package/dist/chunk-AUFG23AA.js +534 -0
  19. package/dist/chunk-B2G7JHHB.js +42 -0
  20. package/dist/chunk-BCJNHMZT.js +34 -0
  21. package/dist/chunk-BCLJSQCV.js +56 -0
  22. package/dist/chunk-BFESLKPP.js +61 -0
  23. package/dist/chunk-BJ3KTMDH.js +63 -0
  24. package/dist/chunk-BKO6JEZI.js +47 -0
  25. package/dist/chunk-BOLTI6LR.js +37 -0
  26. package/dist/chunk-CBPPJSAR.js +89 -0
  27. package/dist/chunk-CBZB6XY6.js +102 -0
  28. package/dist/{chunk-RCQXYLMO.js → chunk-DVDEBNQJ.js} +18 -18
  29. package/dist/{work-pr-cmd-GOYSPEN2.js → chunk-DVYHCMB2.js} +10 -25
  30. package/dist/{chunk-TFUIDMOB.js → chunk-EABW6AJQ.js} +12 -1
  31. package/dist/{chunk-KFYJV2ZG.js → chunk-EMY4I27X.js} +1 -1
  32. package/dist/{chunk-NTTPJV35.js → chunk-F47JBIAW.js} +3 -1
  33. package/dist/chunk-F6GFZXZ3.js +60 -0
  34. package/dist/{chunk-YOOSBOKS.js → chunk-F7CU5ABH.js} +1 -1
  35. package/dist/chunk-FAJSS2WN.js +1357 -0
  36. package/dist/chunk-FS2VYGZT.js +1764 -0
  37. package/dist/chunk-FV2CCGXK.js +9 -0
  38. package/dist/{chunk-BQB3U2VD.js → chunk-G7FSV7JX.js} +5113 -3729
  39. package/dist/{emit-cmd-TWSYYEZI.js → chunk-H5NCNVDA.js} +68 -17
  40. package/dist/chunk-HE2D4EY5.js +83 -0
  41. package/dist/chunk-L2DBRKRA.js +94 -0
  42. package/dist/chunk-LF6MLJCY.js +38 -0
  43. package/dist/chunk-M5POMKKW.js +1051 -0
  44. package/dist/{mcp-install-cmd-FDQH6SEN.js → chunk-MQ3IPHIZ.js} +35 -18
  45. package/dist/{chunk-D77LS3UI.js → chunk-N5FOE5PS.js} +33 -4
  46. package/dist/chunk-N6LURUEF.js +64 -0
  47. package/dist/chunk-NYC54VBD.js +1084 -0
  48. package/dist/chunk-OGFGZQA3.js +23 -0
  49. package/dist/chunk-QOZKPLVJ.js +226 -0
  50. package/dist/chunk-RA3O54FC.js +30 -0
  51. package/dist/chunk-TBT347UY.js +41 -0
  52. package/dist/{chunk-3VQ4XXQA.js → chunk-UFFXNXLQ.js} +115 -71
  53. package/dist/chunk-USO2DKBD.js +421 -0
  54. package/dist/chunk-W4SQCOZU.js +206 -0
  55. package/dist/{chunk-A3WLZX2F.js → chunk-WIOZAJ2W.js} +15 -2
  56. package/dist/chunk-WO2BXCTQ.js +83 -0
  57. package/dist/chunk-WZBHBH3J.js +20 -0
  58. package/dist/{chunk-IWIUYLDR.js → chunk-XCCLFVXM.js} +118 -189
  59. package/dist/{context-materialize-J6CRQ7O7.js → chunk-XDDW4FRS.js} +134 -26
  60. package/dist/chunk-YYYXRJUW.js +101 -0
  61. package/dist/chunk-ZM2IMMYN.js +76 -0
  62. package/dist/chunk-ZMYPLFG3.js +71 -0
  63. package/dist/chunk-ZNA27WEV.js +470 -0
  64. package/dist/{cli-EFJIG2VT.js → cli-NHUXABYH.js} +597 -965
  65. package/dist/{paths-D2VGWWFI.js → cli-build-K56DK4DS.js} +1 -1
  66. package/dist/config-XVJYZ4IQ.js +9 -0
  67. package/dist/connect-cmd-OOG4TPU7.js +16 -0
  68. package/dist/context-adopt-UMA4O5NY.js +105 -0
  69. package/dist/context-materialize-G2FUFC72.js +19 -0
  70. package/dist/dist-RW4UPPC2.js +6 -0
  71. package/dist/docker-NEGDLU6D.js +7 -0
  72. package/dist/doctor-53F2PYY7.js +43 -0
  73. package/dist/doctor-UFVL4PZY.js +426 -0
  74. package/dist/{emit-artifact-cmd-KPPD3YLO.js → emit-artifact-cmd-KTYW2TN2.js} +25 -19
  75. package/dist/emit-cmd-DCERAU27.js +9 -0
  76. package/dist/emit-criteria-cmd-VAUUSAYD.js +166 -0
  77. package/dist/{emit-finding-cmd-VR5VEGXJ.js → emit-finding-cmd-UR4ID45Z.js} +35 -30
  78. package/dist/{emit-result-cmd-YOA5BJKL.js → emit-result-cmd-FDTUKGKE.js} +35 -61
  79. package/dist/exec-client-4XGXV3XN.js +8 -0
  80. package/dist/guest-init-GENRHNP7.js +8 -0
  81. package/dist/hook-fast-EYENPK6F.js +9 -0
  82. package/dist/{hook-wrap-fast-KXYNX3AD.js → hook-wrap-fast-NPLHJXFK.js} +2 -2
  83. package/dist/host-key-KDYZ5ECC.js +7 -0
  84. package/dist/import-capture-cmd-AVZ4VIVJ.js +68 -0
  85. package/dist/{import-ruler-cmd-7LH2QOMG.js → import-ruler-cmd-GUHL6IY5.js} +1 -1
  86. package/dist/index.js +4 -4
  87. package/dist/init-DXBCOAJK.js +36 -0
  88. package/dist/init-YEXK35CF.js +221 -0
  89. package/dist/init-cmd-NHR7PRSQ.js +135 -0
  90. package/dist/install-cmd-7WXOIFDO.js +55 -0
  91. package/dist/lima-XN65D7GN.js +55 -0
  92. package/dist/login-browser-7XGSV7MA.js +10 -0
  93. package/dist/{config-POF7DEQW.js → logout-cmd-QAUFDJ6J.js} +3 -1
  94. package/dist/{logs-TRNPQM42.js → logs-7RYUH5A6.js} +1 -1
  95. package/dist/loop-VZOLL4WQ.js +43 -0
  96. package/dist/machine-WSG52J75.js +35 -0
  97. package/dist/mcp-install-cmd-RLK4SO3N.js +9 -0
  98. package/dist/{mcp-serve-cmd-57EZZOTL.js → mcp-serve-cmd-JPTP5FMS.js} +20 -9
  99. package/dist/paths-OYKMVYJP.js +6 -0
  100. package/dist/{pidfile-PTW76F56.js → pidfile-UZRH774M.js} +2 -3
  101. package/dist/real-deps-SU24ZA2K.js +21 -0
  102. package/dist/relay-L76HDX72.js +46 -0
  103. package/dist/settings-J2652U5N.js +7 -0
  104. package/dist/starter-pack-LIZYMKYQ.js +10 -0
  105. package/dist/{status-JHVSZYC5.js → status-2UZKKNB7.js} +31 -12
  106. package/dist/{statusline-fast-3C5OXHDD.js → statusline-fast-SVTMBMD7.js} +3 -2
  107. package/dist/sync-cmd-VPPYCNNM.js +28 -0
  108. package/dist/version-BWM6VLDI.js +6 -0
  109. package/dist/{watch-NRLXJUST.js → watch-5C4ZBBGO.js} +20 -7
  110. package/dist/{work-claim-cmd-HXB7K27H.js → work-claim-cmd-LSO2B2EX.js} +34 -19
  111. package/dist/work-cmd-UOQO2AD4.js +17 -0
  112. package/dist/work-comment-cmd-WF3EOLAE.js +106 -0
  113. package/dist/{work-launch-YNNCKIH3.js → work-launch-YI4CJDAP.js} +2 -1
  114. package/dist/work-open-pr-cmd-4SSQZMYG.js +156 -0
  115. package/dist/work-pr-cmd-6AN6RRQJ.js +16 -0
  116. package/package.json +13 -3
  117. package/skill-pack/maintained/amend/SKILL.md +104 -0
  118. package/skill-pack/maintained/emitting-evidence/SKILL.md +99 -0
  119. package/skill-pack/maintained/emitting-evidence/references/agents-snippet.md +20 -0
  120. package/skill-pack/maintained/outerlayer/SKILL.md +55 -0
  121. package/skill-pack/maintained/reporting-findings/SKILL.md +148 -0
  122. package/skill-pack/template/build/SKILL.md +193 -0
  123. package/skill-pack/template/build/references/agent-briefs.md +243 -0
  124. package/skill-pack/template/build/references/criteria-judge.md +91 -0
  125. package/skill-pack/template/build/references/evidence.md +42 -0
  126. package/skill-pack/template/build/references/release.md +74 -0
  127. package/skill-pack/template/build/references/review-briefs.md +275 -0
  128. package/skill-pack/template/build/references/review-loop.md +158 -0
  129. package/skill-pack/template/build/scripts/record-criteria.mjs +235 -0
  130. package/skill-pack/template/spec/SKILL.md +84 -0
  131. package/skill-pack/template/writing-specs/SKILL.md +134 -0
  132. package/dist/chunk-JJP7YLMN.js +0 -25
  133. package/dist/chunk-KKEU6FFR.js +0 -589
  134. package/dist/chunk-OZ7C3XUE.js +0 -34
  135. package/dist/chunk-WQ6VGRGZ.js +0 -150
  136. package/dist/emit-commit-credit-cmd-656BRAQ4.js +0 -121
  137. package/dist/hook-fast-I2XTHDE6.js +0 -8
  138. package/dist/import-capture-cmd-EUMBGIV3.js +0 -176
  139. package/dist/init-PTBITAUO.js +0 -103
  140. package/dist/login-cmd-IRX6LZT7.js +0 -62
  141. package/dist/loop-BWE2BS6J.js +0 -646
  142. package/dist/sync-cmd-S7Q6DWU3.js +0 -15
  143. package/dist/work-cmd-ABP6XDBP.js +0 -16
@@ -1,16 +1,61 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { resolveRepoIdentity, detectActiveSession } from './chunk-BQB3U2VD.js';
4
- import { parseTrackerNumber, trackerNumberError } from './chunk-RCQXYLMO.js';
5
- import { readCloudConfig } from './chunk-TFUIDMOB.js';
3
+ import { detectActiveSession } from './chunk-BFESLKPP.js';
4
+ import { sanitizeTerminal } from './chunk-6Y63SXFO.js';
5
+ import { resolveRepoIdentity } from './chunk-G7FSV7JX.js';
6
+ import { parseTrackerNumber, trackerNumberError } from './chunk-WZBHBH3J.js';
7
+ import { defaultFetch } from './chunk-BCLJSQCV.js';
8
+ import { resolveGatewayCredentials, unauthorizedMessage } from './chunk-BOLTI6LR.js';
9
+ import { signRunnerRequest } from './chunk-M5POMKKW.js';
6
10
  import { homeFromEnv } from './chunk-VNVZDWO3.js';
7
11
 
8
12
  createRequire(import.meta.url);
9
13
 
14
+ // src/runner/signature-refusals.ts
15
+ var MESSAGES = /* @__PURE__ */ new Map([
16
+ [
17
+ "runner_key_signature_required",
18
+ "the gateway wants a signature on every request from this runner key, and this one had none. Upgrade the CLI, and check that the runner's host key file exists"
19
+ ],
20
+ [
21
+ "runner_key_signature_invalid",
22
+ "this runner key is bound to another host's key, or the request was altered after it was signed. If the key moved to this machine, a member clears the old binding on the factory's API keys page (Clear host key), and the next claim binds this host"
23
+ ],
24
+ [
25
+ "runner_key_signature_stale",
26
+ "the signature's time is more than five minutes from the gateway's clock. The clock of this host is the likely cause: check that it is set correctly and synchronised"
27
+ ],
28
+ [
29
+ "runner_key_signature_replayed",
30
+ "the gateway already accepted this exact signed request, so it was refused as a replay. Something is sending copies of the runner's requests; restart the runner"
31
+ ]
32
+ ]);
33
+ function signatureRefusalMessage(code) {
34
+ return MESSAGES.get(code);
35
+ }
36
+ function isSignatureRefusal(code) {
37
+ return MESSAGES.has(code);
38
+ }
39
+
10
40
  // src/work-cmd.ts
11
41
  var GREEN = "\x1B[32m";
42
+ var YELLOW = "\x1B[33m";
12
43
  var DIM = "\x1B[2m";
13
44
  var RESET = "\x1B[0m";
45
+ async function signatureHeaders(method, endpoint, payload, signer) {
46
+ if (signer === void 0) return {};
47
+ return { ...await signRunnerRequest({ method, url: endpoint, body: new TextEncoder().encode(payload), signer }) };
48
+ }
49
+ async function signatureRefusalOf(response) {
50
+ let code;
51
+ try {
52
+ code = (await response.json()).error.code;
53
+ } catch {
54
+ return void 0;
55
+ }
56
+ const message = signatureRefusalMessage(code);
57
+ return message === void 0 ? void 0 : new WorkCommandError(`${code}: ${message}`, false, 401, code);
58
+ }
14
59
  var WorkCommandError = class extends Error {
15
60
  /** True when the same request is worth sending again: the gateway was
16
61
  * unreachable, answered a body that never came from it, or failed in a way
@@ -21,27 +66,18 @@ var WorkCommandError = class extends Error {
21
66
  * reached a response. A caller that has to tell one refusal from another —
22
67
  * a claim lost to another host from a key that no longer works — reads it
23
68
  * rather than matching on the message. */
24
- constructor(message, retryable = false, status) {
69
+ constructor(message, retryable = false, status, code) {
25
70
  super(message);
26
71
  this.retryable = retryable;
27
72
  this.status = status;
73
+ this.code = code;
28
74
  }
29
75
  };
30
- var WORK_ADD_RETRY_DELAYS_MS = [1e3, 5e3, 15e3, 6e4];
76
+ var WORK_LINK_RETRY_DELAYS_MS = [1e3, 5e3, 15e3, 6e4];
31
77
  function resolveCredentials(opts) {
32
- const home = opts.home ?? homeFromEnv();
33
- const env = opts.env ?? process.env;
34
- const fileConfig = readCloudConfig(home);
35
- const url = opts.url ?? env.OUTERLAYER_URL ?? fileConfig.url;
36
- const apiKey = opts.apiKey ?? env.OUTERLAYER_API_KEY ?? fileConfig.apiKey;
37
- const appId = opts.appId ?? env.OUTERLAYER_APP_ID ?? fileConfig.appId;
38
- if (!url || !apiKey || !appId) {
39
- const missing = [!url && "--url", !apiKey && "--api-key", !appId && "--app-id"].filter(Boolean).join(", ");
40
- throw new WorkCommandError(
41
- `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.`
42
- );
43
- }
44
- return { url, apiKey, appId };
78
+ const resolved = resolveGatewayCredentials(opts);
79
+ if (!resolved.ok) throw new WorkCommandError(resolved.message);
80
+ return { url: resolved.url, apiKey: resolved.credential, appId: resolved.appId };
45
81
  }
46
82
  function resolveRepository(repoFlag, cwd) {
47
83
  if (repoFlag) return repoFlag;
@@ -59,7 +95,7 @@ function parsePositiveInt(raw, flag) {
59
95
  }
60
96
  function parseRetries(raw) {
61
97
  const n = parseTrackerNumber(raw);
62
- return n === void 0 ? 0 : Math.min(n, WORK_ADD_RETRY_DELAYS_MS.length);
98
+ return n === void 0 ? 0 : Math.min(n, WORK_LINK_RETRY_DELAYS_MS.length);
63
99
  }
64
100
  function resolveSubject(opts) {
65
101
  const issue = parsePositiveInt(opts.issue, "--issue");
@@ -68,33 +104,74 @@ function resolveSubject(opts) {
68
104
  }
69
105
  return { issue };
70
106
  }
71
- async function postJson(url, path, apiKey, appId, body, fetchImpl) {
107
+ var TRACKER_FLAG_HELP = 'expected "linear" or "jira" \u2014 GitHub issues are named by number, with no --tracker';
108
+ function resolveWorkSubject(opts) {
109
+ if (opts.issue === void 0) {
110
+ throw new WorkCommandError("pass --issue");
111
+ }
112
+ const asNumber = parseTrackerNumber(opts.issue);
113
+ if (asNumber !== void 0) {
114
+ if (opts.tracker !== void 0) {
115
+ throw new WorkCommandError(`--tracker cannot be used with --issue ${opts.issue}, a plain number: ${TRACKER_FLAG_HELP}`);
116
+ }
117
+ return { repository: resolveRepository(opts.repo, opts.cwd), issue: asNumber };
118
+ }
119
+ if (opts.tracker !== void 0 && opts.tracker !== "linear" && opts.tracker !== "jira") {
120
+ throw new WorkCommandError(`invalid --tracker "${opts.tracker}": ${TRACKER_FLAG_HELP}`);
121
+ }
122
+ return { key: String(opts.issue), tracker: opts.tracker, repository: opts.repo };
123
+ }
124
+ async function postJson(url, path, apiKey, appId, body, fetchImpl, timeoutMs, signer) {
125
+ const deadline = timeoutMs === void 0 ? void 0 : startDeadline(timeoutMs);
126
+ try {
127
+ return await postJsonBefore(url, path, apiKey, appId, body, fetchImpl, deadline, signer);
128
+ } finally {
129
+ deadline?.clear();
130
+ }
131
+ }
132
+ function startDeadline(ms) {
133
+ const controller = new AbortController();
134
+ const timer = setTimeout(() => controller.abort(), ms);
135
+ return { signal: controller.signal, ms, clear: () => clearTimeout(timer) };
136
+ }
137
+ async function postJsonBefore(url, path, apiKey, appId, body, fetchImpl, deadline, signer) {
72
138
  let response;
73
139
  const endpoint = new URL(path, url).toString();
140
+ const payload = JSON.stringify(body);
141
+ const signature = await signatureHeaders("POST", endpoint, payload ?? "", signer);
74
142
  try {
75
143
  response = await fetchImpl(endpoint, {
76
144
  method: "POST",
77
145
  headers: {
78
146
  "content-type": "application/json",
79
147
  authorization: `Bearer ${apiKey}`,
80
- "x-outerlayer-app-id": appId
148
+ "x-outerlayer-app-id": appId,
149
+ ...signature
81
150
  },
82
- body: JSON.stringify(body)
151
+ body: payload,
152
+ ...deadline ? { signal: deadline.signal } : {}
83
153
  });
84
154
  } catch (err) {
85
155
  throw new WorkCommandError(`network error reaching ${endpoint}: ${String(err)}`, true);
86
156
  }
87
157
  if (response.status === 401) {
88
- throw new WorkCommandError(
89
- "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.",
158
+ throw await signatureRefusalOf(response) ?? new WorkCommandError(
159
+ unauthorizedMessage(apiKey),
90
160
  false,
91
161
  401
92
162
  );
93
163
  }
94
164
  let json;
95
165
  try {
96
- json = await response.json();
97
- } catch {
166
+ json = await readBodyBefore(response, deadline);
167
+ } catch (err) {
168
+ if (err instanceof BodyDeadlineError) {
169
+ throw new WorkCommandError(
170
+ `${endpoint} answered ${response.status} but did not finish its body within ${err.ms}ms`,
171
+ true,
172
+ response.status
173
+ );
174
+ }
98
175
  throw new WorkCommandError(
99
176
  `${endpoint} answered ${response.status} with a body that is not JSON \u2014 the request may not have reached the gateway`,
100
177
  true,
@@ -105,25 +182,48 @@ async function postJson(url, path, apiKey, appId, body, fetchImpl) {
105
182
  throw new WorkCommandError(
106
183
  json.error?.message ?? `request failed (${response.status})`,
107
184
  response.status >= 500 || response.status === 429,
108
- response.status
185
+ response.status,
186
+ json.error?.code
109
187
  );
110
188
  }
111
189
  return { status: response.status, data: json.data ?? json };
112
190
  }
113
- async function getJson(url, path, apiKey, appId, fetchImpl) {
191
+ var BodyDeadlineError = class extends Error {
192
+ constructor(ms) {
193
+ super(`the response body did not finish within ${ms}ms`);
194
+ this.ms = ms;
195
+ }
196
+ };
197
+ function readBodyBefore(response, deadline) {
198
+ const read = response.json();
199
+ if (deadline === void 0) return read;
200
+ const { signal } = deadline;
201
+ return new Promise((resolve, reject) => {
202
+ const expire = () => reject(new BodyDeadlineError(deadline.ms));
203
+ if (signal.aborted) expire();
204
+ signal.addEventListener("abort", expire);
205
+ read.then(resolve, reject);
206
+ });
207
+ }
208
+ async function getJson(url, path, apiKey, appId, fetchImpl, signer) {
114
209
  const endpoint = new URL(path, url).toString();
115
210
  let response;
211
+ const signature = await signatureHeaders("GET", endpoint, "", signer);
116
212
  try {
117
213
  response = await fetchImpl(endpoint, {
118
214
  method: "GET",
119
- headers: { authorization: `Bearer ${apiKey}`, "x-outerlayer-app-id": appId }
215
+ headers: {
216
+ authorization: `Bearer ${apiKey}`,
217
+ "x-outerlayer-app-id": appId,
218
+ ...signature
219
+ }
120
220
  });
121
221
  } catch (err) {
122
222
  throw new WorkCommandError(`network error reaching ${endpoint}: ${String(err)}`);
123
223
  }
124
224
  if (response.status === 401) {
125
- throw new WorkCommandError(
126
- "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.",
225
+ throw await signatureRefusalOf(response) ?? new WorkCommandError(
226
+ unauthorizedMessage(apiKey),
127
227
  false,
128
228
  401
129
229
  );
@@ -142,7 +242,8 @@ async function getJson(url, path, apiKey, appId, fetchImpl) {
142
242
  throw new WorkCommandError(
143
243
  json.error?.message ?? `request failed (${response.status})`,
144
244
  response.status >= 500 || response.status === 429,
145
- response.status
245
+ response.status,
246
+ json.error?.code
146
247
  );
147
248
  }
148
249
  return { status: response.status, data: json.data ?? json, pagination: json.pagination ?? null };
@@ -173,33 +274,65 @@ function environmentFacts(opts) {
173
274
  ...ci ? { ci } : {}
174
275
  };
175
276
  }
176
- async function runWorkAdd(opts) {
277
+ function isRetryableAnswer(err) {
278
+ const status = err instanceof WorkCommandError ? err.status ?? 0 : 0;
279
+ return status >= 500 || status === 429;
280
+ }
281
+ async function postJsonWithRetry(url, path, apiKey, appId, body, fetchImpl, retries, sleep) {
282
+ for (let attempt = 0; ; attempt += 1) {
283
+ try {
284
+ return await postJson(url, path, apiKey, appId, body, fetchImpl);
285
+ } catch (err) {
286
+ if (attempt >= retries || !isRetryableAnswer(err)) throw err;
287
+ await sleep(WORK_LINK_RETRY_DELAYS_MS[attempt]);
288
+ }
289
+ }
290
+ }
291
+ function tokenWarning(data) {
292
+ const tokens = data.repositoryTokens;
293
+ if (tokens === void 0 || tokens === null || tokens.earnable !== false) return "";
294
+ const why = typeof tokens.message === "string" ? sanitizeTerminal(tokens.message) : "the gateway named no reason";
295
+ return `
296
+ ${YELLOW}!${RESET} a container host will fail this build: ${why}`;
297
+ }
298
+ async function runWorkBuild(opts) {
177
299
  const cwd = opts.cwd ?? process.cwd();
178
300
  const env = opts.env ?? process.env;
179
301
  const home = opts.home ?? homeFromEnv(env);
180
302
  const { url, apiKey, appId } = resolveCredentials(opts);
181
- const subject = resolveSubject(opts);
182
- const repository = resolveRepository(opts.repo, cwd);
183
- const fetchImpl = opts.fetchImpl ?? fetch;
184
- const body = {
185
- subject: { repository, ...subject },
186
- note: opts.note,
187
- ...environmentFacts({ cwd, env, home, sessionIdOverride: opts.sessionId })
188
- };
303
+ const fetchImpl = opts.fetchImpl ?? defaultFetch;
304
+ const facts = environmentFacts({ cwd, env, home, sessionIdOverride: opts.sessionId });
189
305
  const retries = parseRetries(opts.retry);
190
306
  const sleep = opts.sleepImpl ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
191
- let data;
192
- for (let attempt = 0; ; attempt += 1) {
193
- try {
194
- data = (await postJson(url, "/v1/work-items", apiKey, appId, body, fetchImpl)).data;
195
- break;
196
- } catch (err) {
197
- if (attempt >= retries || !(err instanceof WorkCommandError) || !err.retryable) throw err;
198
- await sleep(WORK_ADD_RETRY_DELAYS_MS[attempt]);
307
+ const itemNumber = parsePositiveInt(opts.item, "--item");
308
+ if (opts.local) {
309
+ if (itemNumber !== void 0) {
310
+ throw new WorkCommandError(
311
+ `--local takes --issue, not --item: item #${itemNumber} is already on Work \u2014 start a session on it with OUTERLAYER_WORK=${itemNumber}`
312
+ );
199
313
  }
314
+ const body2 = {
315
+ subject: resolveWorkSubject({ issue: opts.issue, tracker: opts.tracker, repo: opts.repo, cwd }),
316
+ note: opts.note,
317
+ ...facts
318
+ };
319
+ const { data: data2 } = await postJsonWithRetry(url, "/v1/work-items", apiKey, appId, body2, fetchImpl, retries, sleep);
320
+ const label = data2.number != null ? `#${data2.number}` : String(data2.workItemId);
321
+ const launch = data2.number != null ? `OUTERLAYER_WORK=${data2.number} claude` : null;
322
+ const branch = typeof data2.branch === "string" && data2.branch !== "" ? data2.branch : null;
323
+ const output2 = opts.json ? JSON.stringify({ ...data2, ...launch !== null ? { launchCommand: launch } : {} }) : `${GREEN}\u2713${RESET} on Work for you to build ${DIM}\xB7${RESET} ${label} ${DIM}\xB7${RESET} no host will take it` + (branch !== null ? `
324
+ branch: ${branch}` : "") + (launch !== null ? `
325
+ start your session with: ${launch}` : "");
326
+ if (!opts.quiet) process.stdout.write(output2 + "\n");
327
+ return { data: data2, output: output2 };
200
328
  }
201
- const stageLine = data.recomputed === false ? `stage ${data.stage} (not yet derived \u2014 the hourly sweep settles it)` : `stage ${data.stage}`;
202
- const output = opts.json ? JSON.stringify(data) : `${GREEN}\u2713${RESET} added ${DIM}\xB7${RESET} ${data.number != null ? `#${data.number}` : data.workItemId} ${DIM}\xB7${RESET} attestor ${data.attestor} ${DIM}\xB7${RESET} ${stageLine}`;
329
+ const body = itemNumber !== void 0 ? { workItemId: String(itemNumber), note: opts.note, ...facts } : {
330
+ subject: resolveWorkSubject({ issue: opts.issue, tracker: opts.tracker, repo: opts.repo, cwd }),
331
+ note: opts.note,
332
+ ...facts
333
+ };
334
+ const { data } = await postJsonWithRetry(url, "/v1/work-items/build", apiKey, appId, body, fetchImpl, retries, sleep);
335
+ const output = opts.json ? JSON.stringify(data) : `${GREEN}\u2713${RESET} build requested ${DIM}\xB7${RESET} ${data.number != null ? `#${data.number}` : data.workItemId} ${DIM}\xB7${RESET} by ${data.attestor}` + tokenWarning(data);
203
336
  if (!opts.quiet) process.stdout.write(output + "\n");
204
337
  return { data, output };
205
338
  }
@@ -214,9 +347,14 @@ async function runWorkLinkSession(opts) {
214
347
  if (!sessionId) {
215
348
  throw new WorkCommandError("no session id detected \u2014 pass --session-id or run from a live session");
216
349
  }
217
- const fetchImpl = opts.fetchImpl ?? fetch;
350
+ const fetchImpl = opts.fetchImpl ?? defaultFetch;
218
351
  const identity = resolveRepoIdentity(cwd);
219
- const body = { sessionId, repository: identity.gitRepo ?? "" };
352
+ const body = {
353
+ sessionId,
354
+ repository: identity.gitRepo ?? "",
355
+ ...opts.noLease ? { noLease: true } : {},
356
+ ...opts.ended ? { ended: true } : {}
357
+ };
220
358
  const retries = parseRetries(opts.retry);
221
359
  const sleep = opts.sleepImpl ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
222
360
  let data;
@@ -226,10 +364,12 @@ async function runWorkLinkSession(opts) {
226
364
  break;
227
365
  } catch (err) {
228
366
  if (attempt >= retries || !(err instanceof WorkCommandError) || !err.retryable) throw err;
229
- await sleep(WORK_ADD_RETRY_DELAYS_MS[attempt]);
367
+ await sleep(WORK_LINK_RETRY_DELAYS_MS[attempt]);
230
368
  }
231
369
  }
232
- const output = opts.json ? JSON.stringify(data) : `${GREEN}\u2713${RESET} linked ${DIM}\xB7${RESET} session ${sessionId} ${DIM}\xB7${RESET} #${number}`;
370
+ const heldBy = typeof data.heldBy === "string" ? data.heldBy : void 0;
371
+ const linked = data.linked === true;
372
+ const output = opts.json ? JSON.stringify(data) : opts.ended ? linked ? `${GREEN}\u2713${RESET} ended ${DIM}\xB7${RESET} session ${sessionId} ${DIM}\xB7${RESET} #${number}` : `${DIM}\xB7${RESET} session ${sessionId} was not linked to #${number}` : heldBy ? `${DIM}\xB7${RESET} session ${sessionId} linked to #${number}, held by ${sanitizeTerminal(heldBy)} \u2014 no lease taken` : `${GREEN}\u2713${RESET} linked ${DIM}\xB7${RESET} session ${sessionId} ${DIM}\xB7${RESET} #${number}`;
233
373
  if (!opts.quiet) process.stdout.write(output + "\n");
234
374
  return { data, output };
235
375
  }
@@ -241,7 +381,7 @@ async function runWorkRemove(opts) {
241
381
  if (!opts.reason) {
242
382
  throw new WorkCommandError("--reason is required \u2014 a removal records why");
243
383
  }
244
- const fetchImpl = opts.fetchImpl ?? fetch;
384
+ const fetchImpl = opts.fetchImpl ?? defaultFetch;
245
385
  const itemNumber = parsePositiveInt(opts.item, "--item");
246
386
  if (itemNumber !== void 0) {
247
387
  const { data: data2 } = await postJson(
@@ -288,7 +428,7 @@ async function runWorkStatus(opts) {
288
428
  `/v1/work-items/${fetchByItem}`,
289
429
  creds.apiKey,
290
430
  creds.appId,
291
- opts.fetchImpl ?? fetch
431
+ opts.fetchImpl ?? defaultFetch
292
432
  );
293
433
  const body = opts.json ? JSON.stringify(data2) : JSON.stringify(data2, null, 2);
294
434
  if (!opts.quiet) process.stdout.write(body + "\n");
@@ -296,7 +436,7 @@ async function runWorkStatus(opts) {
296
436
  }
297
437
  const subject = resolveSubject(opts);
298
438
  const repository = resolveRepository(opts.repo, cwd);
299
- const fetchImpl = opts.fetchImpl ?? fetch;
439
+ const fetchImpl = opts.fetchImpl ?? defaultFetch;
300
440
  const number = subject.issue;
301
441
  const item = await findLiveItem(creds, repository, subject, fetchImpl);
302
442
  if (!item) {
@@ -310,7 +450,7 @@ async function runWorkStatus(opts) {
310
450
  async function runWorkList(opts) {
311
451
  const cwd = opts.cwd ?? process.cwd();
312
452
  const { url, apiKey, appId } = resolveCredentials(opts);
313
- const fetchImpl = opts.fetchImpl ?? fetch;
453
+ const fetchImpl = opts.fetchImpl ?? defaultFetch;
314
454
  const repository = opts.repo ?? resolveRepoIdentity(cwd).gitRepo;
315
455
  if (opts.needs !== void 0 && opts.needs !== "amend") {
316
456
  throw new WorkCommandError('--needs only accepts "amend"');
@@ -320,6 +460,7 @@ async function runWorkList(opts) {
320
460
  if (opts.stage) query.set("stage", opts.stage);
321
461
  if (opts.section) query.set("section", opts.section);
322
462
  if (opts.unclaimed) query.set("unclaimed", "true");
463
+ if (opts.claimed) query.set("claimed", "true");
323
464
  if (opts.startable) query.set("startable", "true");
324
465
  if (opts.needs) query.set("needs", opts.needs);
325
466
  const items = [];
@@ -331,9 +472,16 @@ async function runWorkList(opts) {
331
472
  cursor = page.pagination?.cursor ?? null;
332
473
  } while (cursor);
333
474
  const data = items;
334
- const output = opts.json ? JSON.stringify(data) : items.length === 0 ? "no live work items" : items.map((i) => `${i.trackerKey} ${DIM}\xB7${RESET} ${i.stage} ${DIM}\xB7${RESET} ${i.section} ${DIM}\xB7${RESET} ${i.title}`).join("\n");
475
+ const claimSuffix = (claim) => {
476
+ if (!claim) return "";
477
+ const holder = claim.holderKind === "session" ? `${claim.memberName || "a member"} (local session` : `${sanitizeTerminal(claim.host)} (${claim.kind}`;
478
+ return ` ${DIM}\xB7${RESET} claimed by ${holder}, since ${claim.createdAt}, until ${claim.expiresAt})`;
479
+ };
480
+ const output = opts.json ? JSON.stringify(data) : items.length === 0 ? "no live work items" : items.map(
481
+ (i) => `${i.trackerKey} ${DIM}\xB7${RESET} ${i.stage} ${DIM}\xB7${RESET} ${i.section} ${DIM}\xB7${RESET} ${i.title}${claimSuffix(i.claim)}`
482
+ ).join("\n");
335
483
  if (!opts.quiet) process.stdout.write(output + "\n");
336
484
  return { data, output };
337
485
  }
338
486
 
339
- export { WorkCommandError, findLiveItem, getJson, parsePositiveInt, postJson, resolveCredentials, resolveRepository, runWorkAdd, runWorkLinkSession, runWorkList, runWorkRemove, runWorkStatus };
487
+ export { WorkCommandError, findLiveItem, getJson, isSignatureRefusal, parsePositiveInt, postJson, resolveCredentials, resolveRepository, runWorkBuild, runWorkLinkSession, runWorkList, runWorkRemove, runWorkStatus, signatureRefusalMessage };
@@ -5,7 +5,7 @@ import { existsSync, readFileSync, mkdirSync, copyFileSync, writeFileSync } from
5
5
  import { join, dirname, isAbsolute } from 'path';
6
6
 
7
7
  createRequire(import.meta.url);
8
- var REGISTERED_EVENTS = ["SessionStart", "SessionEnd", "Stop"];
8
+ var REGISTERED_EVENTS = ["SessionStart", "SessionEnd", "Stop", "PostToolUse"];
9
9
  var MARKER = "_outerlayer";
10
10
  function settingsPath(scope, opts = {}) {
11
11
  const home = opts.home ?? homeFromEnv();
@@ -43,6 +43,12 @@ function tokenizeCommand(command) {
43
43
  function quoteBin(bin) {
44
44
  return /\s/.test(bin) ? `"${bin}"` : bin;
45
45
  }
46
+ function isScriptBin(bin) {
47
+ return /\.[cm]?js$/i.test(bin);
48
+ }
49
+ function cliCommand(cliBin) {
50
+ return isScriptBin(cliBin) ? `node ${quoteBin(cliBin)}` : quoteBin(cliBin);
51
+ }
46
52
  function isOurBin(bin, cliBin) {
47
53
  if (cliBin !== void 0 && normalizeBinPath(bin) === normalizeBinPath(cliBin)) return true;
48
54
  const parts = bin.replace(/\\/g, "/").split("/");
@@ -63,30 +69,40 @@ function splitCliInvocation(command, cliBin) {
63
69
  if (typeof command !== "string") return null;
64
70
  const tokens = tokenizeCommand(command.trim());
65
71
  if (!tokens) return null;
66
- let i = 0;
67
72
  const launcher = (tokens[0] ?? "").replace(/\\/g, "/").split("/").pop() ?? "";
68
- if (launcher === "node" || launcher === "node.exe" || launcher === "npx") i = 1;
73
+ const viaNode = launcher === "node" || launcher === "node.exe";
74
+ const viaNpx = launcher === "npx";
75
+ const i = viaNode || viaNpx ? 1 : 0;
69
76
  const bin = tokens[i];
70
77
  if (!bin || !isOurBin(bin, cliBin)) return null;
71
- return { bin, args: tokens.slice(i + 1), viaNpx: launcher === "npx" };
78
+ return { bin, args: tokens.slice(i + 1), viaNpx, viaNode };
72
79
  }
73
80
  function parseCliInvocation(command, cliBin) {
74
81
  return splitCliInvocation(command, cliBin)?.args ?? null;
75
82
  }
76
83
  function ourCommandBins(settings) {
77
- const bins = [];
84
+ return unique(ourCommandLaunches(settings).map((l) => l.bin));
85
+ }
86
+ function ourDirectScriptBins(settings) {
87
+ return unique(ourCommandLaunches(settings).filter((l) => !l.viaNode && isScriptBin(l.bin)).map((l) => l.bin));
88
+ }
89
+ function ourCommandLaunches(settings) {
90
+ const launches = [];
78
91
  const add = (command) => {
79
92
  const parsed = splitCliInvocation(command);
80
93
  if (!parsed || parsed.viaNpx) return;
81
94
  if (!["hook", "hook-wrap", "statusline"].includes(parsed.args[0] ?? "")) return;
82
95
  if (parsed.bin.includes("$") || !isAbsolute(parsed.bin)) return;
83
- if (!bins.includes(parsed.bin)) bins.push(parsed.bin);
96
+ launches.push({ bin: parsed.bin, viaNode: parsed.viaNode });
84
97
  };
85
98
  for (const matchers of Object.values(settings?.hooks ?? {})) {
86
99
  for (const matcher of matchers ?? []) for (const h of matcher.hooks ?? []) add(h.command);
87
100
  }
88
101
  add(settings?.statusLine?.command);
89
- return bins;
102
+ return launches;
103
+ }
104
+ function unique(values) {
105
+ return [...new Set(values)];
90
106
  }
91
107
  function parseLifecycleHookCommand(command, cliBin) {
92
108
  const args = parseCliInvocation(command, cliBin);
@@ -142,7 +158,7 @@ var SettingsParseError = class extends Error {
142
158
  };
143
159
  function ourEntry(cliBin, event) {
144
160
  return {
145
- hooks: [{ type: "command", command: `${quoteBin(cliBin)} hook ${event}`, [MARKER]: true }]
161
+ hooks: [{ type: "command", command: `${cliCommand(cliBin)} hook ${event}`, [MARKER]: true }]
146
162
  };
147
163
  }
148
164
  function pruneOurHooks(blocks, cliBin) {
@@ -229,7 +245,7 @@ function wrapHookCommand(cliBin, h, opts = {}) {
229
245
  if (isWrapped(h, cliBin)) return h;
230
246
  const id = encodeWrappedCommand(h.command);
231
247
  const flag = opts.captureOutput ? " --capture-output" : "";
232
- return { type: "command", command: `${quoteBin(cliBin)} hook-wrap --id ${id}${flag}`, [WRAP_MARKER]: h.command };
248
+ return { type: "command", command: `${cliCommand(cliBin)} hook-wrap --id ${id}${flag}`, [WRAP_MARKER]: h.command };
233
249
  }
234
250
  function unwrapHookCommand(h, cliBin) {
235
251
  const original = wrappedOriginalCommand(h, cliBin);
@@ -280,11 +296,11 @@ function repointWrappedHooks(settings, cliBin) {
280
296
  const h = matcher.hooks[hi];
281
297
  const parsed = splitCliInvocation(h.command, cliBin);
282
298
  if (!parsed || parsed.args[0] !== "hook-wrap") continue;
283
- if (normalizeBinPath(parsed.bin) === normalizeBinPath(cliBin)) continue;
299
+ if (normalizeBinPath(parsed.bin) === normalizeBinPath(cliBin) && parsed.viaNode === isScriptBin(cliBin)) continue;
284
300
  const original = wrappedOriginalCommand(h, cliBin);
285
301
  if (original === null) continue;
286
302
  const flag = parsed.args.includes("--capture-output") ? " --capture-output" : "";
287
- const desired = `${quoteBin(cliBin)} hook-wrap --id ${encodeWrappedCommand(original)}${flag}`;
303
+ const desired = `${cliCommand(cliBin)} hook-wrap --id ${encodeWrappedCommand(original)}${flag}`;
288
304
  matcher.hooks[hi] = { ...h, command: desired, [WRAP_MARKER]: original };
289
305
  changed = true;
290
306
  }
@@ -293,10 +309,10 @@ function repointWrappedHooks(settings, cliBin) {
293
309
  return { next, changed };
294
310
  }
295
311
  function statuslineCommand(cliBin) {
296
- return `${quoteBin(cliBin)} statusline`;
312
+ return `${cliCommand(cliBin)} statusline`;
297
313
  }
298
314
  function statuslineWrapCommand(cliBin, original) {
299
- return `${quoteBin(cliBin)} statusline --wrap-id ${encodeWrappedCommand(original)}`;
315
+ return `${cliCommand(cliBin)} statusline --wrap-id ${encodeWrappedCommand(original)}`;
300
316
  }
301
317
  function isOurStatuslineSlot(slot, cliBin) {
302
318
  if (slot[MARKER] === true) return true;
@@ -377,4 +393,4 @@ function unwrapHooks(settings, cliBin) {
377
393
  return { next, changed, unwrapped };
378
394
  }
379
395
 
380
- export { AUTO_WRAP_EVENTS, REGISTERED_EVENTS, SettingsParseError, cliBinResolvable, decodeWrappedCommand, isOurHookCommand, isOurStatuslineSlot, listHookCandidates, mergeHooks, mergeStatusline, ourCommandBins, readSettings, removeHooks, removeStatusline, repointWrappedHooks, settingsPath, statuslineWrappedOriginal, unwrapHooks, wrapHooks, wrappedOriginalCommand, writeSettings };
396
+ export { AUTO_WRAP_EVENTS, REGISTERED_EVENTS, SettingsParseError, cliBinResolvable, cliCommand, decodeWrappedCommand, encodeWrappedCommand, isOurHookCommand, isOurHookCommandFor, isOurStatuslineSlot, isWrapped, listHookCandidates, mergeHooks, mergeStatusline, ourCommandBins, ourDirectScriptBins, parseLifecycleHookCommand, readSettings, removeHooks, removeStatusline, repointWrappedHooks, settingsPath, statuslineCommand, statuslineWrappedOriginal, unwrapHooks, wrapHooks, wrappedOriginalCommand, writeSettings };