@outerlayer/cli 0.2.0 → 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 (141) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +226 -59
  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-U32VSRLO.js → chunk-3YPKXE5L.js} +90 -632
  10. package/dist/{chunk-Q6CL5THG.js → chunk-5DLBAXY5.js} +208 -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-65WXAANR.js → chunk-DVDEBNQJ.js} +17 -2
  29. package/dist/{work-pr-cmd-AZQPWH4H.js → chunk-DVYHCMB2.js} +9 -26
  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-4TNZMO7V.js → chunk-G7FSV7JX.js} +3452 -751
  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-NXLLURA4.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-I3ETLSNE.js → chunk-XCCLFVXM.js} +120 -11
  58. package/dist/{context-materialize-6WKT3RBQ.js → chunk-XDDW4FRS.js} +134 -26
  59. package/dist/chunk-YYYXRJUW.js +101 -0
  60. package/dist/chunk-ZM2IMMYN.js +76 -0
  61. package/dist/chunk-ZMYPLFG3.js +71 -0
  62. package/dist/chunk-ZNA27WEV.js +470 -0
  63. package/dist/{cli-G7DILYJY.js → cli-NHUXABYH.js} +590 -876
  64. package/dist/{paths-D2VGWWFI.js → cli-build-K56DK4DS.js} +1 -1
  65. package/dist/config-XVJYZ4IQ.js +9 -0
  66. package/dist/connect-cmd-OOG4TPU7.js +16 -0
  67. package/dist/context-adopt-UMA4O5NY.js +105 -0
  68. package/dist/context-materialize-G2FUFC72.js +19 -0
  69. package/dist/dist-RW4UPPC2.js +6 -0
  70. package/dist/docker-NEGDLU6D.js +7 -0
  71. package/dist/doctor-53F2PYY7.js +43 -0
  72. package/dist/doctor-UFVL4PZY.js +426 -0
  73. package/dist/{emit-artifact-cmd-UQVT3OKW.js → emit-artifact-cmd-KTYW2TN2.js} +24 -20
  74. package/dist/emit-cmd-DCERAU27.js +9 -0
  75. package/dist/emit-criteria-cmd-VAUUSAYD.js +166 -0
  76. package/dist/{emit-finding-cmd-FDOU2OPD.js → emit-finding-cmd-UR4ID45Z.js} +34 -31
  77. package/dist/{emit-result-cmd-H2T4C2K3.js → emit-result-cmd-FDTUKGKE.js} +34 -62
  78. package/dist/exec-client-4XGXV3XN.js +8 -0
  79. package/dist/guest-init-GENRHNP7.js +8 -0
  80. package/dist/hook-fast-EYENPK6F.js +9 -0
  81. package/dist/{hook-wrap-fast-KXYNX3AD.js → hook-wrap-fast-NPLHJXFK.js} +2 -2
  82. package/dist/host-key-KDYZ5ECC.js +7 -0
  83. package/dist/import-capture-cmd-AVZ4VIVJ.js +68 -0
  84. package/dist/{import-ruler-cmd-7LH2QOMG.js → import-ruler-cmd-GUHL6IY5.js} +1 -1
  85. package/dist/index.js +4 -4
  86. package/dist/init-DXBCOAJK.js +36 -0
  87. package/dist/init-YEXK35CF.js +221 -0
  88. package/dist/init-cmd-NHR7PRSQ.js +135 -0
  89. package/dist/install-cmd-7WXOIFDO.js +55 -0
  90. package/dist/lima-XN65D7GN.js +55 -0
  91. package/dist/login-browser-7XGSV7MA.js +10 -0
  92. package/dist/{config-POF7DEQW.js → logout-cmd-QAUFDJ6J.js} +3 -1
  93. package/dist/{logs-TRNPQM42.js → logs-7RYUH5A6.js} +1 -1
  94. package/dist/loop-VZOLL4WQ.js +43 -0
  95. package/dist/machine-WSG52J75.js +35 -0
  96. package/dist/mcp-install-cmd-RLK4SO3N.js +9 -0
  97. package/dist/{mcp-serve-cmd-57EZZOTL.js → mcp-serve-cmd-JPTP5FMS.js} +20 -9
  98. package/dist/paths-OYKMVYJP.js +6 -0
  99. package/dist/{pidfile-PTW76F56.js → pidfile-UZRH774M.js} +2 -3
  100. package/dist/real-deps-SU24ZA2K.js +21 -0
  101. package/dist/relay-L76HDX72.js +46 -0
  102. package/dist/settings-J2652U5N.js +7 -0
  103. package/dist/starter-pack-LIZYMKYQ.js +10 -0
  104. package/dist/{status-I27IST4L.js → status-2UZKKNB7.js} +30 -13
  105. package/dist/{statusline-fast-3C5OXHDD.js → statusline-fast-SVTMBMD7.js} +3 -2
  106. package/dist/sync-cmd-VPPYCNNM.js +28 -0
  107. package/dist/version-BWM6VLDI.js +6 -0
  108. package/dist/{watch-3DTPJETH.js → watch-5C4ZBBGO.js} +19 -8
  109. package/dist/{work-claim-cmd-5AUNCLS2.js → work-claim-cmd-LSO2B2EX.js} +33 -20
  110. package/dist/work-cmd-UOQO2AD4.js +17 -0
  111. package/dist/work-comment-cmd-WF3EOLAE.js +106 -0
  112. package/dist/{work-launch-LT663PB3.js → work-launch-YI4CJDAP.js} +1 -1
  113. package/dist/work-open-pr-cmd-4SSQZMYG.js +156 -0
  114. package/dist/work-pr-cmd-6AN6RRQJ.js +16 -0
  115. package/package.json +13 -3
  116. package/skill-pack/maintained/amend/SKILL.md +104 -0
  117. package/skill-pack/maintained/emitting-evidence/SKILL.md +99 -0
  118. package/skill-pack/maintained/emitting-evidence/references/agents-snippet.md +20 -0
  119. package/skill-pack/maintained/outerlayer/SKILL.md +55 -0
  120. package/skill-pack/maintained/reporting-findings/SKILL.md +148 -0
  121. package/skill-pack/template/build/SKILL.md +193 -0
  122. package/skill-pack/template/build/references/agent-briefs.md +243 -0
  123. package/skill-pack/template/build/references/criteria-judge.md +91 -0
  124. package/skill-pack/template/build/references/evidence.md +42 -0
  125. package/skill-pack/template/build/references/release.md +74 -0
  126. package/skill-pack/template/build/references/review-briefs.md +275 -0
  127. package/skill-pack/template/build/references/review-loop.md +158 -0
  128. package/skill-pack/template/build/scripts/record-criteria.mjs +235 -0
  129. package/skill-pack/template/spec/SKILL.md +84 -0
  130. package/skill-pack/template/writing-specs/SKILL.md +134 -0
  131. package/dist/chunk-DCNOXRMV.js +0 -589
  132. package/dist/chunk-JJP7YLMN.js +0 -25
  133. package/dist/chunk-OZ7C3XUE.js +0 -34
  134. package/dist/chunk-WQ6VGRGZ.js +0 -150
  135. package/dist/hook-fast-J5LCDHUJ.js +0 -8
  136. package/dist/import-capture-cmd-EUMBGIV3.js +0 -176
  137. package/dist/init-PTBITAUO.js +0 -103
  138. package/dist/login-cmd-IRX6LZT7.js +0 -62
  139. package/dist/loop-ASZRX3CZ.js +0 -648
  140. package/dist/sync-cmd-BBAUZ5JD.js +0 -17
  141. package/dist/work-cmd-2P4BVX47.js +0 -18
@@ -1,17 +1,61 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { detectActiveSession } from './chunk-U32VSRLO.js';
4
- import { resolveRepoIdentity } from './chunk-4TNZMO7V.js';
3
+ import { detectActiveSession } from './chunk-BFESLKPP.js';
4
+ import { sanitizeTerminal } from './chunk-6Y63SXFO.js';
5
+ import { resolveRepoIdentity } from './chunk-G7FSV7JX.js';
5
6
  import { parseTrackerNumber, trackerNumberError } from './chunk-WZBHBH3J.js';
6
- import { readCloudConfig } from './chunk-TFUIDMOB.js';
7
+ import { defaultFetch } from './chunk-BCLJSQCV.js';
8
+ import { resolveGatewayCredentials, unauthorizedMessage } from './chunk-BOLTI6LR.js';
9
+ import { signRunnerRequest } from './chunk-M5POMKKW.js';
7
10
  import { homeFromEnv } from './chunk-VNVZDWO3.js';
8
11
 
9
12
  createRequire(import.meta.url);
10
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
+
11
40
  // src/work-cmd.ts
12
41
  var GREEN = "\x1B[32m";
42
+ var YELLOW = "\x1B[33m";
13
43
  var DIM = "\x1B[2m";
14
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
+ }
15
59
  var WorkCommandError = class extends Error {
16
60
  /** True when the same request is worth sending again: the gateway was
17
61
  * unreachable, answered a body that never came from it, or failed in a way
@@ -22,27 +66,18 @@ var WorkCommandError = class extends Error {
22
66
  * reached a response. A caller that has to tell one refusal from another —
23
67
  * a claim lost to another host from a key that no longer works — reads it
24
68
  * rather than matching on the message. */
25
- constructor(message, retryable = false, status) {
69
+ constructor(message, retryable = false, status, code) {
26
70
  super(message);
27
71
  this.retryable = retryable;
28
72
  this.status = status;
73
+ this.code = code;
29
74
  }
30
75
  };
31
- var WORK_ADD_RETRY_DELAYS_MS = [1e3, 5e3, 15e3, 6e4];
76
+ var WORK_LINK_RETRY_DELAYS_MS = [1e3, 5e3, 15e3, 6e4];
32
77
  function resolveCredentials(opts) {
33
- const home = opts.home ?? homeFromEnv();
34
- const env = opts.env ?? process.env;
35
- const fileConfig = readCloudConfig(home);
36
- const url = opts.url ?? env.OUTERLAYER_URL ?? fileConfig.url;
37
- const apiKey = opts.apiKey ?? env.OUTERLAYER_API_KEY ?? fileConfig.apiKey;
38
- const appId = opts.appId ?? env.OUTERLAYER_APP_ID ?? fileConfig.appId;
39
- if (!url || !apiKey || !appId) {
40
- const missing = [!url && "--url", !apiKey && "--api-key", !appId && "--app-id"].filter(Boolean).join(", ");
41
- throw new WorkCommandError(
42
- `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.`
43
- );
44
- }
45
- 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 };
46
81
  }
47
82
  function resolveRepository(repoFlag, cwd) {
48
83
  if (repoFlag) return repoFlag;
@@ -60,7 +95,7 @@ function parsePositiveInt(raw, flag) {
60
95
  }
61
96
  function parseRetries(raw) {
62
97
  const n = parseTrackerNumber(raw);
63
- 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);
64
99
  }
65
100
  function resolveSubject(opts) {
66
101
  const issue = parsePositiveInt(opts.issue, "--issue");
@@ -69,33 +104,74 @@ function resolveSubject(opts) {
69
104
  }
70
105
  return { issue };
71
106
  }
72
- 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) {
73
138
  let response;
74
139
  const endpoint = new URL(path, url).toString();
140
+ const payload = JSON.stringify(body);
141
+ const signature = await signatureHeaders("POST", endpoint, payload ?? "", signer);
75
142
  try {
76
143
  response = await fetchImpl(endpoint, {
77
144
  method: "POST",
78
145
  headers: {
79
146
  "content-type": "application/json",
80
147
  authorization: `Bearer ${apiKey}`,
81
- "x-outerlayer-app-id": appId
148
+ "x-outerlayer-app-id": appId,
149
+ ...signature
82
150
  },
83
- body: JSON.stringify(body)
151
+ body: payload,
152
+ ...deadline ? { signal: deadline.signal } : {}
84
153
  });
85
154
  } catch (err) {
86
155
  throw new WorkCommandError(`network error reaching ${endpoint}: ${String(err)}`, true);
87
156
  }
88
157
  if (response.status === 401) {
89
- throw new WorkCommandError(
90
- "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),
91
160
  false,
92
161
  401
93
162
  );
94
163
  }
95
164
  let json;
96
165
  try {
97
- json = await response.json();
98
- } 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
+ }
99
175
  throw new WorkCommandError(
100
176
  `${endpoint} answered ${response.status} with a body that is not JSON \u2014 the request may not have reached the gateway`,
101
177
  true,
@@ -106,25 +182,48 @@ async function postJson(url, path, apiKey, appId, body, fetchImpl) {
106
182
  throw new WorkCommandError(
107
183
  json.error?.message ?? `request failed (${response.status})`,
108
184
  response.status >= 500 || response.status === 429,
109
- response.status
185
+ response.status,
186
+ json.error?.code
110
187
  );
111
188
  }
112
189
  return { status: response.status, data: json.data ?? json };
113
190
  }
114
- 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) {
115
209
  const endpoint = new URL(path, url).toString();
116
210
  let response;
211
+ const signature = await signatureHeaders("GET", endpoint, "", signer);
117
212
  try {
118
213
  response = await fetchImpl(endpoint, {
119
214
  method: "GET",
120
- headers: { authorization: `Bearer ${apiKey}`, "x-outerlayer-app-id": appId }
215
+ headers: {
216
+ authorization: `Bearer ${apiKey}`,
217
+ "x-outerlayer-app-id": appId,
218
+ ...signature
219
+ }
121
220
  });
122
221
  } catch (err) {
123
222
  throw new WorkCommandError(`network error reaching ${endpoint}: ${String(err)}`);
124
223
  }
125
224
  if (response.status === 401) {
126
- throw new WorkCommandError(
127
- "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),
128
227
  false,
129
228
  401
130
229
  );
@@ -143,7 +242,8 @@ async function getJson(url, path, apiKey, appId, fetchImpl) {
143
242
  throw new WorkCommandError(
144
243
  json.error?.message ?? `request failed (${response.status})`,
145
244
  response.status >= 500 || response.status === 429,
146
- response.status
245
+ response.status,
246
+ json.error?.code
147
247
  );
148
248
  }
149
249
  return { status: response.status, data: json.data ?? json, pagination: json.pagination ?? null };
@@ -174,33 +274,65 @@ function environmentFacts(opts) {
174
274
  ...ci ? { ci } : {}
175
275
  };
176
276
  }
177
- 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) {
178
299
  const cwd = opts.cwd ?? process.cwd();
179
300
  const env = opts.env ?? process.env;
180
301
  const home = opts.home ?? homeFromEnv(env);
181
302
  const { url, apiKey, appId } = resolveCredentials(opts);
182
- const subject = resolveSubject(opts);
183
- const repository = resolveRepository(opts.repo, cwd);
184
- const fetchImpl = opts.fetchImpl ?? fetch;
185
- const body = {
186
- subject: { repository, ...subject },
187
- note: opts.note,
188
- ...environmentFacts({ cwd, env, home, sessionIdOverride: opts.sessionId })
189
- };
303
+ const fetchImpl = opts.fetchImpl ?? defaultFetch;
304
+ const facts = environmentFacts({ cwd, env, home, sessionIdOverride: opts.sessionId });
190
305
  const retries = parseRetries(opts.retry);
191
306
  const sleep = opts.sleepImpl ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
192
- let data;
193
- for (let attempt = 0; ; attempt += 1) {
194
- try {
195
- data = (await postJson(url, "/v1/work-items", apiKey, appId, body, fetchImpl)).data;
196
- break;
197
- } catch (err) {
198
- if (attempt >= retries || !(err instanceof WorkCommandError) || !err.retryable) throw err;
199
- 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
+ );
200
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 };
201
328
  }
202
- const stageLine = data.recomputed === false ? `stage ${data.stage} (not yet derived \u2014 the hourly sweep settles it)` : `stage ${data.stage}`;
203
- 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);
204
336
  if (!opts.quiet) process.stdout.write(output + "\n");
205
337
  return { data, output };
206
338
  }
@@ -215,9 +347,14 @@ async function runWorkLinkSession(opts) {
215
347
  if (!sessionId) {
216
348
  throw new WorkCommandError("no session id detected \u2014 pass --session-id or run from a live session");
217
349
  }
218
- const fetchImpl = opts.fetchImpl ?? fetch;
350
+ const fetchImpl = opts.fetchImpl ?? defaultFetch;
219
351
  const identity = resolveRepoIdentity(cwd);
220
- 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
+ };
221
358
  const retries = parseRetries(opts.retry);
222
359
  const sleep = opts.sleepImpl ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
223
360
  let data;
@@ -227,10 +364,12 @@ async function runWorkLinkSession(opts) {
227
364
  break;
228
365
  } catch (err) {
229
366
  if (attempt >= retries || !(err instanceof WorkCommandError) || !err.retryable) throw err;
230
- await sleep(WORK_ADD_RETRY_DELAYS_MS[attempt]);
367
+ await sleep(WORK_LINK_RETRY_DELAYS_MS[attempt]);
231
368
  }
232
369
  }
233
- 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}`;
234
373
  if (!opts.quiet) process.stdout.write(output + "\n");
235
374
  return { data, output };
236
375
  }
@@ -242,7 +381,7 @@ async function runWorkRemove(opts) {
242
381
  if (!opts.reason) {
243
382
  throw new WorkCommandError("--reason is required \u2014 a removal records why");
244
383
  }
245
- const fetchImpl = opts.fetchImpl ?? fetch;
384
+ const fetchImpl = opts.fetchImpl ?? defaultFetch;
246
385
  const itemNumber = parsePositiveInt(opts.item, "--item");
247
386
  if (itemNumber !== void 0) {
248
387
  const { data: data2 } = await postJson(
@@ -289,7 +428,7 @@ async function runWorkStatus(opts) {
289
428
  `/v1/work-items/${fetchByItem}`,
290
429
  creds.apiKey,
291
430
  creds.appId,
292
- opts.fetchImpl ?? fetch
431
+ opts.fetchImpl ?? defaultFetch
293
432
  );
294
433
  const body = opts.json ? JSON.stringify(data2) : JSON.stringify(data2, null, 2);
295
434
  if (!opts.quiet) process.stdout.write(body + "\n");
@@ -297,7 +436,7 @@ async function runWorkStatus(opts) {
297
436
  }
298
437
  const subject = resolveSubject(opts);
299
438
  const repository = resolveRepository(opts.repo, cwd);
300
- const fetchImpl = opts.fetchImpl ?? fetch;
439
+ const fetchImpl = opts.fetchImpl ?? defaultFetch;
301
440
  const number = subject.issue;
302
441
  const item = await findLiveItem(creds, repository, subject, fetchImpl);
303
442
  if (!item) {
@@ -311,7 +450,7 @@ async function runWorkStatus(opts) {
311
450
  async function runWorkList(opts) {
312
451
  const cwd = opts.cwd ?? process.cwd();
313
452
  const { url, apiKey, appId } = resolveCredentials(opts);
314
- const fetchImpl = opts.fetchImpl ?? fetch;
453
+ const fetchImpl = opts.fetchImpl ?? defaultFetch;
315
454
  const repository = opts.repo ?? resolveRepoIdentity(cwd).gitRepo;
316
455
  if (opts.needs !== void 0 && opts.needs !== "amend") {
317
456
  throw new WorkCommandError('--needs only accepts "amend"');
@@ -321,6 +460,7 @@ async function runWorkList(opts) {
321
460
  if (opts.stage) query.set("stage", opts.stage);
322
461
  if (opts.section) query.set("section", opts.section);
323
462
  if (opts.unclaimed) query.set("unclaimed", "true");
463
+ if (opts.claimed) query.set("claimed", "true");
324
464
  if (opts.startable) query.set("startable", "true");
325
465
  if (opts.needs) query.set("needs", opts.needs);
326
466
  const items = [];
@@ -332,9 +472,16 @@ async function runWorkList(opts) {
332
472
  cursor = page.pagination?.cursor ?? null;
333
473
  } while (cursor);
334
474
  const data = items;
335
- 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");
336
483
  if (!opts.quiet) process.stdout.write(output + "\n");
337
484
  return { data, output };
338
485
  }
339
486
 
340
- 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 };