@outerlayer/cli 0.18.0 → 0.19.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 (102) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +21 -2
  3. package/dist/{agent-setup-RBTEACYU.js → agent-setup-TPR4BBFV.js} +15 -16
  4. package/dist/{artifact-link-cmd-AVM2EKTV.js → artifact-link-cmd-W7XF5BFB.js} +2 -2
  5. package/dist/build-info.json +1 -1
  6. package/dist/{builder-DUI2ZH2H.js → builder-C4QNTYMS.js} +3 -3
  7. package/dist/{check-5C4YDZYV.js → check-UWQAL2MN.js} +44 -43
  8. package/dist/{chunk-S26M3YBJ.js → chunk-3UZTIRLF.js} +1 -1
  9. package/dist/{chunk-W5VPCONO.js → chunk-452TM7J4.js} +33 -227
  10. package/dist/{chunk-UYOYTKE2.js → chunk-5GHGAF6P.js} +3 -3
  11. package/dist/{chunk-QQM2UJ6T.js → chunk-5MYFOW5V.js} +182 -28
  12. package/dist/{chunk-Y5GE76XC.js → chunk-5TN7GLYX.js} +33 -81
  13. package/dist/{chunk-GOBOJMOJ.js → chunk-6D7GT22T.js} +4 -4
  14. package/dist/{chunk-3OFZM4X6.js → chunk-6U3MV7TZ.js} +151 -20
  15. package/dist/{chunk-F4XSVQJ4.js → chunk-6X4SQ5MY.js} +2 -2
  16. package/dist/{chunk-Y5MXET4M.js → chunk-7KSSBQTS.js} +11 -10
  17. package/dist/{chunk-JF42URPU.js → chunk-7SJPQWBM.js} +33 -9
  18. package/dist/{chunk-N3CUDFCW.js → chunk-7VUK5RFS.js} +15 -2
  19. package/dist/{chunk-SWCTGCSC.js → chunk-7YP7T6GV.js} +1 -1
  20. package/dist/{chunk-KDVIU23F.js → chunk-BWSUPRDK.js} +1 -1
  21. package/dist/{chunk-S76AYB4M.js → chunk-CG36IONK.js} +5 -5
  22. package/dist/chunk-CJJDJ7AT.js +56 -0
  23. package/dist/{chunk-FUB7LRUE.js → chunk-DADWEUH3.js} +5 -5
  24. package/dist/{chunk-RAZTWYZN.js → chunk-DJ3ZUSC3.js} +7 -8
  25. package/dist/{chunk-MQJSXHV7.js → chunk-G3IJ3N6F.js} +5 -3
  26. package/dist/{chunk-KK4VBOH6.js → chunk-HPJNLETG.js} +10 -11
  27. package/dist/{chunk-IP7BIBXJ.js → chunk-IF2OSI5T.js} +35 -1
  28. package/dist/{chunk-T4Y6SCG3.js → chunk-IXYZUUYQ.js} +2 -2
  29. package/dist/chunk-KYS3TQAG.js +296 -0
  30. package/dist/chunk-LLM3I5YU.js +89 -0
  31. package/dist/{chunk-B3UKB46N.js → chunk-LRG3ZMZD.js} +4 -5
  32. package/dist/{chunk-ULSIJGLS.js → chunk-NFIR3GEE.js} +56 -14
  33. package/dist/{chunk-MNTSH4CL.js → chunk-NNLA7LLQ.js} +1 -1
  34. package/dist/{chunk-WGAUY5LE.js → chunk-QEFKA2QD.js} +1 -1
  35. package/dist/{chunk-BMYGAGBJ.js → chunk-QNLX477Q.js} +155 -23
  36. package/dist/chunk-TUSYOL2O.js +113 -0
  37. package/dist/{chunk-ESC6UR76.js → chunk-TWF3RICI.js} +6 -6
  38. package/dist/{chunk-NL4XPC7W.js → chunk-UVG35MWF.js} +1 -1
  39. package/dist/{chunk-L2USZT6T.js → chunk-VMJPFQNE.js} +1 -1
  40. package/dist/chunk-VRRGENS4.js +144 -0
  41. package/dist/{chunk-X5N6KC6V.js → chunk-WFSUUUAN.js} +2 -3
  42. package/dist/{chunk-6CHIYEZZ.js → chunk-XSZD4AZ6.js} +9 -83
  43. package/dist/{chunk-LXAY4A2G.js → chunk-YQR6E6OV.js} +34 -49
  44. package/dist/{chunk-47TTLEDX.js → chunk-ZHAUSEEN.js} +35 -13
  45. package/dist/{claude-launcher-YVXI76NX.js → claude-launcher-FPPHZPMR.js} +20 -7
  46. package/dist/{cli-C77H54BV.js → cli-HROW5T7X.js} +203 -228
  47. package/dist/{command-H5CILO4S.js → command-OEAV7BVX.js} +29 -30
  48. package/dist/{config-FXDG4NPJ.js → config-CLSGABD5.js} +2 -2
  49. package/dist/{connect-cmd-ZW5FV3ZW.js → connect-cmd-UNYKGDAL.js} +7 -7
  50. package/dist/context-cleanup-HSVYDGMI.js +9 -0
  51. package/dist/control-plane-launch-NYVDJESK.js +216 -0
  52. package/dist/{dist-QTX4RSCL.js → dist-WO7NOJNG.js} +2 -2
  53. package/dist/{docker-RC3PRX3Q.js → docker-BFU3AQGY.js} +4 -4
  54. package/dist/{doctor-2NQQML6P.js → doctor-23BBJWBX.js} +32 -33
  55. package/dist/{emit-artifact-cmd-7MP63KXU.js → emit-artifact-cmd-O6SCM7TV.js} +17 -17
  56. package/dist/{emit-cmd-PPN4QQF2.js → emit-cmd-7SP3I6UZ.js} +6 -6
  57. package/dist/{emit-criteria-cmd-3RXPDLNK.js → emit-criteria-cmd-LQNERZRV.js} +11 -12
  58. package/dist/{emit-finding-cmd-OL7JB6H3.js → emit-finding-cmd-AJPKHIOV.js} +12 -13
  59. package/dist/{emit-result-cmd-UZVMOCF4.js → emit-result-cmd-RUICOKWQ.js} +17 -17
  60. package/dist/emit-step-cmd-347XNGPQ.js +26 -0
  61. package/dist/{governing-source-BUU4IABS.js → governing-source-V5EEGPTA.js} +21 -22
  62. package/dist/{hook-fast-W5MKHMJT.js → hook-fast-YAZG5MDX.js} +3 -3
  63. package/dist/{import-capture-cmd-ZO6KGHI2.js → import-capture-cmd-KKFFOGWP.js} +4 -4
  64. package/dist/{import-ruler-cmd-KJMWL5V6.js → import-ruler-cmd-5P6JOXQN.js} +2 -2
  65. package/dist/index.js +4 -4
  66. package/dist/{init-WNXXYMO7.js → init-LWHDMZQG.js} +30 -31
  67. package/dist/{init-VWXIZEAN.js → init-MOINY2DD.js} +30 -31
  68. package/dist/{init-cmd-WS6TIRYT.js → init-cmd-HSMRID6A.js} +67 -26
  69. package/dist/{init-update-E3J6TVIN.js → init-update-LWX5MPPR.js} +21 -22
  70. package/dist/{install-CBWIEF3R.js → install-4SEFIIW2.js} +3 -3
  71. package/dist/{login-browser-O63QZN7D.js → login-browser-DX35MARZ.js} +2 -2
  72. package/dist/{loop-UGI2BJOT.js → loop-SYYPVFYW.js} +44 -43
  73. package/dist/{machine-JOW4RNZ7.js → machine-6X63G2KH.js} +26 -27
  74. package/dist/{mcp-serve-cmd-DOTY5FKY.js → mcp-serve-cmd-N3UTY6EE.js} +5 -5
  75. package/dist/{runner-env-4NB2GA7O.js → runner-env-FRZFFHDF.js} +4 -4
  76. package/dist/{runner-key-TC3NFWFV.js → runner-key-276SJU5R.js} +30 -31
  77. package/dist/{session-sender-NNVPSPY7.js → session-sender-GGIY5OAX.js} +19 -15
  78. package/dist/shell-function-NRD4KVUK.js +6 -0
  79. package/dist/{spec-cmd-OWVTBB4J.js → spec-cmd-SIZBRN7D.js} +12 -13
  80. package/dist/{starter-pack-OPWUDOWX.js → starter-pack-6LQVLBIA.js} +5 -5
  81. package/dist/{status-TKCLLKFG.js → status-NAK6DYSF.js} +33 -32
  82. package/dist/{sync-cmd-Q6VYNPWG.js → sync-cmd-G7GF4UTW.js} +18 -14
  83. package/dist/{watch-ARAGADTF.js → watch-EVM5GSMP.js} +18 -14
  84. package/dist/{work-claim-cmd-34FERLYX.js → work-claim-cmd-3J3QW56U.js} +16 -17
  85. package/dist/{work-cmd-7S5B2H2P.js → work-cmd-5EPPD33Y.js} +12 -13
  86. package/dist/{work-comment-cmd-SZJSPM5S.js → work-comment-cmd-2ACOZQAE.js} +12 -13
  87. package/dist/work-logs-cmd-QDD2NMJL.js +60 -0
  88. package/dist/{work-open-pr-cmd-RPXKCBWS.js → work-open-pr-cmd-XNCEI4ZM.js} +13 -14
  89. package/dist/{work-pr-cmd-5LGANL32.js → work-pr-cmd-H4OQR26N.js} +12 -13
  90. package/package.json +2 -2
  91. package/skill-pack/maintained/outerlayer-cli/SKILL.md +9 -2
  92. package/skill-pack/maintained/outerlayer-cli/references/find-out-why-a-build-ended.md +76 -0
  93. package/skill-pack/shipped.json +1 -0
  94. package/skill-pack/template/build/references/release.md +3 -1
  95. package/dist/chunk-WBT5X3NH.js +0 -40
  96. package/dist/chunk-XCCLFVXM.js +0 -401
  97. package/dist/context-adopt-O5OFXMKI.js +0 -109
  98. package/dist/context-materialize-Y5IJ4MQT.js +0 -23
  99. package/dist/{chunk-NACDBXUG.js → chunk-BM63HB2M.js} +1 -1
  100. package/dist/{chunk-ZRL6HKBY.js → chunk-FVMYKL6L.js} +1 -1
  101. package/dist/{chunk-52NC2FIU.js → chunk-NTRCK4NH.js} +1 -1
  102. package/dist/{chunk-XGP3VYOG.js → chunk-P53VEAOF.js} +1 -1
@@ -1,17 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { autoConnect } from './chunk-WBT5X3NH.js';
4
- import { detectActiveSession } from './chunk-IP7BIBXJ.js';
5
- import { workPageUrl } from './chunk-S76AYB4M.js';
6
- import { sanitizeTerminal } from './chunk-MQJSXHV7.js';
7
- import { resolveRepoIdentity } from './chunk-BMYGAGBJ.js';
3
+ import { detectActiveSession, autoConnect } from './chunk-IF2OSI5T.js';
4
+ import { workPageUrl } from './chunk-CG36IONK.js';
5
+ import { sanitizeTerminal } from './chunk-G3IJ3N6F.js';
6
+ import { resolveRepoIdentity } from './chunk-QNLX477Q.js';
8
7
  import { C } from './chunk-ANPNT6PM.js';
9
8
  import { parseTrackerNumber, trackerNumberError } from './chunk-WZBHBH3J.js';
10
- import { defaultFetch } from './chunk-ZRL6HKBY.js';
11
- import { resolveGatewayCredentials, unauthorizedMessage } from './chunk-52NC2FIU.js';
12
- import { signRunnerRequest } from './chunk-M2ZWWTWZ.js';
9
+ import { defaultFetch } from './chunk-FVMYKL6L.js';
10
+ import { resolveGatewayCredentials, unauthorizedMessage } from './chunk-NTRCK4NH.js';
13
11
  import { readRepoConnection } from './chunk-BJ3KTMDH.js';
14
12
  import { homeFromEnv } from './chunk-VNVZDWO3.js';
13
+ import { signRunnerRequest } from './chunk-M2ZWWTWZ.js';
15
14
 
16
15
  createRequire(import.meta.url);
17
16
 
@@ -44,8 +43,8 @@ var TRACKERS = {
44
43
  scopeNoun: "project",
45
44
  connect: {
46
45
  kind: "form",
47
- summary: "Read the projects you pick on a site, with a service account. One webhook is created in Jira by an admin.",
48
- intro: "Uses a service account's OAuth client credential. Create one under Atlassian administration, then paste it here. Read scope only.",
46
+ summary: "Read the projects you pick on a site, and file specs as issues, with a service account. One webhook is created in Jira by an admin.",
47
+ intro: "Uses a service account's OAuth client credential. Create one under Atlassian administration, then paste it here. Give it read:jira-work, and write:jira-work to file specs. A credential's scopes are fixed when it is created.",
49
48
  notice: "After connecting, you get a webhook URL and secret to put on one webhook in Jira. That is the only manual step, and the only time the secret is shown.",
50
49
  fields: [
51
50
  { name: "siteUrl", label: "Site URL", placeholder: "https://your-team.atlassian.net" },
@@ -422,7 +421,7 @@ async function runWorkLinkSession(opts) {
422
421
  return await linkSession(opts);
423
422
  } catch (err) {
424
423
  if (opts.logFailures) {
425
- const { logHookError } = await import('./hook-fast-W5MKHMJT.js');
424
+ const { logHookError } = await import('./hook-fast-YAZG5MDX.js');
426
425
  const reason = err instanceof Error ? err.message : String(err);
427
426
  logHookError(opts.home ?? homeFromEnv(opts.env ?? process.env), new Error(`work link-session #${opts.number} failed: ${reason}`));
428
427
  }
@@ -1,5 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
+ import { fetchFactories, selectFactory, saveFactoryConnection, autoConnectedLine } from './chunk-CG36IONK.js';
4
+ import { resolveRepoIdentity } from './chunk-QNLX477Q.js';
5
+ import { resolveGatewayCredentials } from './chunk-NTRCK4NH.js';
6
+ import { findGitDirs, readRepoConnection } from './chunk-BJ3KTMDH.js';
7
+ import { cliInvocation } from './chunk-CRB3POP4.js';
8
+ import { homeFromEnv } from './chunk-VNVZDWO3.js';
3
9
  import { openSync, fstatSync, readSync, closeSync } from 'fs';
4
10
  import { join } from 'path';
5
11
 
@@ -76,4 +82,32 @@ function transcriptPathForSession(sessionId, home, readTailImpl = readTail) {
76
82
  return found;
77
83
  }
78
84
 
79
- export { detectActiveSession, transcriptPathForSession };
85
+ // src/auto-connect.ts
86
+ async function autoConnect(opts) {
87
+ const env = opts.env ?? process.env;
88
+ const home = opts.home ?? homeFromEnv(env);
89
+ const cwd = opts.cwd ?? process.cwd();
90
+ const resolved = resolveGatewayCredentials({ url: opts.url, apiKey: opts.apiKey, appId: opts.appId, home, env, cwd });
91
+ if (resolved.ok || resolved.kind !== "login" || !resolved.credential || !resolved.url) return void 0;
92
+ const dirs = findGitDirs(cwd);
93
+ const repository = opts.repository ?? resolveRepoIdentity(cwd).gitRepo;
94
+ if (dirs === void 0 || repository === void 0) return void 0;
95
+ const listed = await fetchFactories(opts.fetchImpl ?? fetch, resolved.url, resolved.credential, repository);
96
+ if (!listed.ok || listed.factories.length === 0) return void 0;
97
+ const chosen = selectFactory(listed.factories, readRepoConnection(cwd), repository);
98
+ if (chosen.factory === void 0) {
99
+ return `you are logged in, but no factory is chosen for this repository, and more than one fits. Run \`${cliInvocation()} connect --factory <org>/<factory>\` with one of:
100
+ ${chosen.text}
101
+ Or pass --app-id / set OUTERLAYER_APP_ID.`;
102
+ }
103
+ try {
104
+ saveFactoryConnection(dirs.gitCommonDir, void 0, chosen.factory, /* @__PURE__ */ new Date());
105
+ } catch {
106
+ return void 0;
107
+ }
108
+ process.stderr.write(`${autoConnectedLine(repository, chosen.factory, chosen.reason)}
109
+ `);
110
+ return void 0;
111
+ }
112
+
113
+ export { autoConnect, detectActiveSession, transcriptPathForSession };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { skillDir, FORMER_MAINTAINED_NAMES, isMaintainedCopy } from './chunk-MNTSH4CL.js';
4
- import { emitTree, ALL_TARGET_IDS, classifyTree } from './chunk-X5N6KC6V.js';
3
+ import { skillDir, FORMER_MAINTAINED_NAMES, isMaintainedCopy } from './chunk-NNLA7LLQ.js';
4
+ import { emitTree, ALL_TARGET_IDS, classifyTree } from './chunk-WFSUUUAN.js';
5
5
  import { cliInvocation } from './chunk-CRB3POP4.js';
6
6
  import { readFileSync, existsSync, unlinkSync, rmdirSync, readdirSync } from 'fs';
7
7
  import { join, dirname, relative, sep } from 'path';
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { launchRefusalForSession } from './chunk-ESVW4MJY.js';
4
+ import { autoConnect, detectActiveSession } from './chunk-IF2OSI5T.js';
5
+ import { acquireSpoolLockAt } from './chunk-G3IJ3N6F.js';
6
+ import { resolveRepoIdentity } from './chunk-QNLX477Q.js';
7
+ import { C } from './chunk-ANPNT6PM.js';
8
+ import { launchedItemForSession } from './chunk-T2Q7ISPE.js';
9
+ import { parseTrackerNumber, trackerNumberError } from './chunk-WZBHBH3J.js';
10
+ import { defaultFetch } from './chunk-FVMYKL6L.js';
11
+ import { resolveGatewayCredentials, unauthorizedMessage } from './chunk-NTRCK4NH.js';
12
+ import { StepNameSchema, StepBodySchema } from './chunk-7VUK5RFS.js';
13
+ import { homeFromEnv } from './chunk-VNVZDWO3.js';
14
+ import { randomUUID } from 'crypto';
15
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, renameSync, appendFileSync } from 'fs';
16
+ import { join, dirname } from 'path';
17
+
18
+ createRequire(import.meta.url);
19
+ var STEP_RETRY_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
20
+ var STEP_STATE_RESOLVED_CAP = 5e3;
21
+ function stepsSpoolPath(home = homeFromEnv()) {
22
+ return join(home, ".outerlayer", "spool", "steps.jsonl");
23
+ }
24
+ function stepsStatePath(home = homeFromEnv()) {
25
+ return join(home, ".outerlayer", "spool", "steps.state.json");
26
+ }
27
+ function stepsLockPath(home = homeFromEnv()) {
28
+ return join(home, ".outerlayer", "spool", "steps.lock");
29
+ }
30
+ function appendStepRecord(record, home = homeFromEnv()) {
31
+ const path = stepsSpoolPath(home);
32
+ mkdirSync(dirname(path), { recursive: true });
33
+ appendFileSync(path, JSON.stringify(record) + "\n");
34
+ }
35
+ function isRecord(value) {
36
+ if (value === null || typeof value !== "object") return false;
37
+ const r = value;
38
+ return typeof r.clientStepId === "string" && typeof r.name === "string" && typeof r.startedAt === "string" && typeof r.itemNumber === "number" && typeof r.appId === "string" && typeof r.cwd === "string" && typeof r.spooledAt === "string";
39
+ }
40
+ function readStepRecords(home = homeFromEnv()) {
41
+ const path = stepsSpoolPath(home);
42
+ if (!existsSync(path)) return { records: [], skipped: 0 };
43
+ const records = [];
44
+ let skipped = 0;
45
+ for (const line of readFileSync(path, "utf8").split("\n")) {
46
+ if (line.trim() === "") continue;
47
+ try {
48
+ const parsed = JSON.parse(line);
49
+ if (isRecord(parsed)) records.push(parsed);
50
+ else skipped += 1;
51
+ } catch {
52
+ skipped += 1;
53
+ }
54
+ }
55
+ return { records, skipped };
56
+ }
57
+ function readStepState(home = homeFromEnv()) {
58
+ const state = /* @__PURE__ */ new Map();
59
+ let parsed;
60
+ try {
61
+ parsed = JSON.parse(readFileSync(stepsStatePath(home), "utf8"));
62
+ } catch {
63
+ return state;
64
+ }
65
+ const entries = parsed?.records;
66
+ if (!Array.isArray(entries)) return state;
67
+ for (const entry of entries) {
68
+ const e = entry;
69
+ if (typeof e?.clientStepId !== "string") continue;
70
+ if (e.state === "pending" || e.state === "uploaded" || e.state === "dropped") state.set(e.clientStepId, e.state);
71
+ }
72
+ return state;
73
+ }
74
+ function writeStepState(state, home = homeFromEnv()) {
75
+ try {
76
+ const path = stepsStatePath(home);
77
+ mkdirSync(dirname(path), { recursive: true });
78
+ const pending = [];
79
+ const resolved = [];
80
+ for (const pair of state.entries()) (pair[1] === "pending" ? pending : resolved).push(pair);
81
+ const kept = resolved.length > STEP_STATE_RESOLVED_CAP ? resolved.slice(-STEP_STATE_RESOLVED_CAP) : resolved;
82
+ const records = [...pending, ...kept].map(([clientStepId, disposition]) => ({ clientStepId, state: disposition }));
83
+ const tmp = `${path}.tmp`;
84
+ writeFileSync(tmp, JSON.stringify({ version: 1, records }));
85
+ renameSync(tmp, path);
86
+ return true;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ // src/emit-step-cmd.ts
93
+ var EmitStepError = class extends Error {
94
+ };
95
+ function firstIssue(error) {
96
+ const issue = error.issues[0];
97
+ if (!issue) return "malformed";
98
+ const path = issue.path.join(".");
99
+ return path === "" || path === "name" ? issue.message : `${path}: ${issue.message}`;
100
+ }
101
+ function detectSessionId(opts) {
102
+ const identity = resolveRepoIdentity(opts.cwd);
103
+ return detectActiveSession({
104
+ env: opts.env,
105
+ home: opts.home,
106
+ cwds: identity.repoRoot !== void 0 ? [identity.repoRoot, opts.cwd] : [opts.cwd],
107
+ nowMs: opts.now().getTime()
108
+ });
109
+ }
110
+ function resolveItem(opts) {
111
+ if (opts.sessionId !== void 0) {
112
+ const refusal = launchRefusalForSession(opts.sessionId, opts.home);
113
+ if (refusal !== void 0) throw new EmitStepError(refusal);
114
+ }
115
+ if (opts.item !== void 0) {
116
+ const number = parseTrackerNumber(opts.item);
117
+ if (number === void 0) throw new EmitStepError(trackerNumberError("--item", opts.item));
118
+ return number;
119
+ }
120
+ const launched = opts.sessionId !== void 0 ? launchedItemForSession(opts.sessionId, opts.home) : void 0;
121
+ if (launched !== void 0) return launched;
122
+ throw new EmitStepError(
123
+ "no work item to report this step on. Pass --item <number>, or run inside a recorded session launched with OUTERLAYER_WORK, whose launch record names the item."
124
+ );
125
+ }
126
+ async function refusalReason(response) {
127
+ let text = "";
128
+ try {
129
+ text = await response.text();
130
+ } catch {
131
+ }
132
+ try {
133
+ const message = JSON.parse(text).error?.message;
134
+ if (typeof message === "string" && message !== "") return message;
135
+ } catch {
136
+ }
137
+ return text.replace(/\s+/g, " ").trim().slice(0, 160);
138
+ }
139
+ function stepUrl(base) {
140
+ return new URL("/v1/work-items/steps", base).toString();
141
+ }
142
+ function stepHeaders(apiKey, appId) {
143
+ return { "content-type": "application/json", authorization: `Bearer ${apiKey}`, "x-outerlayer-app-id": appId };
144
+ }
145
+ async function runEmitStep(opts) {
146
+ const cwd = opts.cwd ?? process.cwd();
147
+ const home = opts.home ?? homeFromEnv();
148
+ const env = opts.env ?? process.env;
149
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
150
+ const name = StepNameSchema.safeParse(opts.name);
151
+ if (!name.success) throw new EmitStepError(`invalid step name: ${firstIssue(name.error)}`);
152
+ const sessionId = detectSessionId({ cwd, home, env, now });
153
+ const itemNumber = resolveItem({ item: opts.item, sessionId, home });
154
+ const parsed = StepBodySchema.safeParse({
155
+ clientStepId: randomUUID(),
156
+ name: opts.name,
157
+ item: { number: itemNumber },
158
+ startedAt: now().toISOString()
159
+ });
160
+ if (!parsed.success) throw new EmitStepError(`invalid step: ${firstIssue(parsed.error)}`);
161
+ const body = parsed.data;
162
+ const refusal = await autoConnect({ ...opts, cwd, home, env });
163
+ if (refusal !== void 0) throw new EmitStepError(refusal);
164
+ const resolved = resolveGatewayCredentials({ ...opts, cwd, home, env });
165
+ if (!resolved.ok) throw new EmitStepError(resolved.message);
166
+ const { url, credential: apiKey, appId } = resolved;
167
+ const fetchImpl = opts.fetchImpl ?? defaultFetch;
168
+ const endpoint = stepUrl(url);
169
+ const spool = (why) => {
170
+ appendStepRecord(
171
+ {
172
+ clientStepId: body.clientStepId,
173
+ name: body.name,
174
+ startedAt: body.startedAt,
175
+ itemNumber,
176
+ appId,
177
+ cwd,
178
+ spooledAt: now().toISOString()
179
+ },
180
+ home
181
+ );
182
+ const output2 = `${C.yellow}!${C.reset} step "${body.name}" queued (${why}); \`outerlayer sync\` will send it`;
183
+ if (!opts.quiet) process.stdout.write(output2 + "\n");
184
+ return { data: { clientStepId: body.clientStepId, queued: true }, output: output2, exitCode: 0, queued: true };
185
+ };
186
+ let response;
187
+ try {
188
+ response = await fetchImpl(endpoint, { method: "POST", headers: stepHeaders(apiKey, appId), body: JSON.stringify(body) });
189
+ } catch (err) {
190
+ return spool(`could not reach ${new URL(endpoint).host}: ${String(err)}`);
191
+ }
192
+ if (response.status >= 500) return spool(`the gateway answered ${response.status}`);
193
+ if (response.status === 401) throw new EmitStepError(unauthorizedMessage(apiKey));
194
+ if (!response.ok) {
195
+ const reason = await refusalReason(response);
196
+ throw new EmitStepError(`step refused (${response.status})${reason ? `: ${reason}` : ""}`);
197
+ }
198
+ let data = {};
199
+ try {
200
+ data = (await response.json()).data ?? {};
201
+ } catch {
202
+ }
203
+ const output = opts.json ? JSON.stringify(data) : `${C.green}\u2713${C.reset} step started: ${body.name} ${C.dim}\xB7${C.reset} item #${itemNumber}`;
204
+ if (!opts.quiet) process.stdout.write(output + "\n");
205
+ return { data, output, exitCode: 0, queued: false };
206
+ }
207
+ async function uploadSpooledSteps(opts) {
208
+ const { home } = opts;
209
+ const waiting = () => {
210
+ const state = readStepState(home);
211
+ return readStepRecords(home).records.filter((record) => {
212
+ const disposition = state.get(record.clientStepId);
213
+ return disposition !== "uploaded" && disposition !== "dropped";
214
+ });
215
+ };
216
+ if (waiting().length === 0) return void 0;
217
+ const lock = await acquireSpoolLockAt(stepsLockPath(home), { waitMs: opts.lockWaitMs });
218
+ if (!lock.ok) {
219
+ if (!opts.quiet) {
220
+ process.stderr.write(`${C.yellow}!${C.reset} steps not sent: ${lock.reason}; they stay queued for the next sync
221
+ `);
222
+ }
223
+ return { uploaded: 0, dropped: 0, pending: waiting().length };
224
+ }
225
+ try {
226
+ const fetchImpl = opts.fetchImpl ?? defaultFetch;
227
+ const nowMs = opts.nowMs ?? Date.now();
228
+ const state = readStepState(home);
229
+ const result = { uploaded: 0, dropped: 0, pending: 0 };
230
+ let stopped = false;
231
+ for (const record of readStepRecords(home).records) {
232
+ const saved = state.get(record.clientStepId);
233
+ if (saved === "uploaded" || saved === "dropped") continue;
234
+ const spooledMs = Date.parse(record.spooledAt);
235
+ if (Number.isFinite(spooledMs) && nowMs - spooledMs > STEP_RETRY_MAX_AGE_MS) {
236
+ state.set(record.clientStepId, "dropped");
237
+ result.dropped += 1;
238
+ continue;
239
+ }
240
+ const foreign = opts.kind === "key" && record.appId !== opts.appId || opts.onlyItem !== void 0 && record.itemNumber !== opts.onlyItem;
241
+ if (stopped || foreign) {
242
+ result.pending += 1;
243
+ continue;
244
+ }
245
+ const body = {
246
+ clientStepId: record.clientStepId,
247
+ name: record.name,
248
+ item: { number: record.itemNumber },
249
+ startedAt: record.startedAt
250
+ };
251
+ let response;
252
+ try {
253
+ response = await fetchImpl(stepUrl(opts.url), {
254
+ method: "POST",
255
+ headers: stepHeaders(opts.apiKey, opts.kind === "login" ? record.appId : opts.appId),
256
+ body: JSON.stringify(body)
257
+ });
258
+ } catch {
259
+ stopped = true;
260
+ result.pending += 1;
261
+ continue;
262
+ }
263
+ if (response.ok) {
264
+ state.set(record.clientStepId, "uploaded");
265
+ result.uploaded += 1;
266
+ } else if (response.status === 401) {
267
+ result.unauthorized = unauthorizedMessage(opts.apiKey);
268
+ stopped = true;
269
+ result.pending += 1;
270
+ } else if (response.status >= 500 || response.status === 408 || response.status === 429) {
271
+ result.pending += 1;
272
+ } else {
273
+ state.set(record.clientStepId, "dropped");
274
+ result.dropped += 1;
275
+ if (!opts.quiet) {
276
+ const reason = await refusalReason(response);
277
+ process.stderr.write(
278
+ `${C.yellow}!${C.reset} step "${record.name}" on item #${record.itemNumber} refused (${response.status}) and dropped${reason ? `: ${reason}` : ""}
279
+ `
280
+ );
281
+ }
282
+ }
283
+ }
284
+ if (!writeStepState(state, opts.home) && !opts.quiet) {
285
+ process.stderr.write(
286
+ `${C.yellow}!${C.reset} failed to save ${stepsStatePath(home)}; steps sent this run may be sent again, which is harmless
287
+ `
288
+ );
289
+ }
290
+ return result;
291
+ } finally {
292
+ lock.release();
293
+ }
294
+ }
295
+
296
+ export { EmitStepError, runEmitStep, uploadSpooledSteps };
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { rewriteControlPlanePaths, rewriteHookCommand } from './chunk-G2YNJIGF.js';
4
+ import { MAINTAINED_SKILLS } from './chunk-NNLA7LLQ.js';
5
+ import { existsSync, mkdirSync, copyFileSync, writeFileSync, readdirSync, lstatSync, readFileSync } from 'fs';
6
+ import { join, dirname } from 'path';
7
+
8
+ createRequire(import.meta.url);
9
+ function deliverySection(dir) {
10
+ return `This repository's control plane is not in the checkout. Its files are in \`${dir}\`. When an instruction or skill names a path under \`.outerlayer/\` or \`.claude/\`, read it under \`${dir}\`.`;
11
+ }
12
+ function isFile(path) {
13
+ try {
14
+ return lstatSync(path).isFile();
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+ function filesUnder(dir, base = dir) {
20
+ if (!existsSync(dir) || !lstatSync(dir).isDirectory()) return [];
21
+ const out = [];
22
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
23
+ const abs = join(dir, entry.name);
24
+ if (entry.isDirectory()) out.push(...filesUnder(abs, base));
25
+ else if (entry.isFile()) out.push(abs.slice(base.length + 1));
26
+ }
27
+ return out.sort();
28
+ }
29
+ function readText(path) {
30
+ if (!isFile(path)) return void 0;
31
+ const content = readFileSync(path);
32
+ return content.includes(0) ? void 0 : content.toString("utf8");
33
+ }
34
+ function deliveredSettings(raw, dir) {
35
+ if (raw === void 0) return "{}\n";
36
+ const rewrite = (value) => {
37
+ if (typeof value === "string") return rewriteHookCommand(value, dir);
38
+ if (Array.isArray(value)) return value.map(rewrite);
39
+ if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).map(([key, inner]) => [key, rewrite(inner)]));
40
+ return value;
41
+ };
42
+ try {
43
+ return `${JSON.stringify(rewrite(JSON.parse(raw)), null, 2)}
44
+ `;
45
+ } catch {
46
+ return rewriteHookCommand(raw, dir);
47
+ }
48
+ }
49
+ function composeDelivery(input) {
50
+ const { tree, dir } = input;
51
+ const added = [];
52
+ const put = (rel, content) => {
53
+ mkdirSync(dirname(join(tree, rel)), { recursive: true });
54
+ writeFileSync(join(tree, rel), content);
55
+ added.push(rel);
56
+ };
57
+ for (const skill of MAINTAINED_SKILLS) {
58
+ for (const rel of filesUnder(join(tree, ".outerlayer/skills", skill))) {
59
+ const target = `.claude/skills/${skill}/${rel}`;
60
+ if (existsSync(join(tree, target))) continue;
61
+ mkdirSync(dirname(join(tree, target)), { recursive: true });
62
+ copyFileSync(join(tree, ".outerlayer/skills", skill, rel), join(tree, target));
63
+ added.push(target);
64
+ }
65
+ }
66
+ for (const rel of [...filesUnder(join(tree, ".claude/skills"), tree), ...filesUnder(join(tree, ".claude/agents"), tree)]) {
67
+ const text = readText(join(tree, rel));
68
+ if (text === void 0) continue;
69
+ const rewritten = rewriteControlPlanePaths(text, dir);
70
+ if (rewritten !== text) writeFileSync(join(tree, rel), rewritten);
71
+ }
72
+ const agents = readText(join(tree, "AGENTS.md"));
73
+ put("CLAUDE.md", agents === void 0 ? `${deliverySection(dir)}
74
+ ` : `${deliverySection(dir)}
75
+
76
+ ${rewriteControlPlanePaths(agents, dir)}`);
77
+ put("settings.json", deliveredSettings(readText(join(tree, ".claude/settings.json")), dir));
78
+ return added;
79
+ }
80
+ function sharedSkillNames(tree) {
81
+ const dir = join(tree, ".claude/skills");
82
+ if (!existsSync(dir)) return [];
83
+ return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
84
+ }
85
+ function skillConflictMessage(input) {
86
+ return `.claude/skills/${input.skill} in ${input.repository} has the same name as the shared skill ${input.skill} from ${input.controlPlane}`;
87
+ }
88
+
89
+ export { composeDelivery, sharedSkillNames, skillConflictMessage };
@@ -1,13 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { autoConnect } from './chunk-WBT5X3NH.js';
4
- import { detectActiveSession } from './chunk-IP7BIBXJ.js';
5
- import { resolveRepoIdentity } from './chunk-BMYGAGBJ.js';
3
+ import { autoConnect, detectActiveSession } from './chunk-IF2OSI5T.js';
4
+ import { resolveRepoIdentity } from './chunk-QNLX477Q.js';
6
5
  import { C } from './chunk-ANPNT6PM.js';
7
6
  import { parseWorkItemEnv } from './chunk-T2Q7ISPE.js';
8
7
  import { parseTrackerNumber, trackerNumberError } from './chunk-WZBHBH3J.js';
9
- import { defaultFetch } from './chunk-ZRL6HKBY.js';
10
- import { unauthorizedMessage, resolveGatewayCredentials } from './chunk-52NC2FIU.js';
8
+ import { defaultFetch } from './chunk-FVMYKL6L.js';
9
+ import { unauthorizedMessage, resolveGatewayCredentials } from './chunk-NTRCK4NH.js';
11
10
  import { homeFromEnv } from './chunk-VNVZDWO3.js';
12
11
 
13
12
  createRequire(import.meta.url);
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { CONTROL_PLANE_DIR_VARIABLE } from './chunk-G2YNJIGF.js';
4
- import { deadHooksState, huskyPrepareScript, preparePackageManagerCommand } from './chunk-XCCLFVXM.js';
5
3
  import { appendContextProvenance } from './chunk-ZJSNLVCE.js';
6
4
  import { readRepoConnection, findGitDirs } from './chunk-BJ3KTMDH.js';
5
+ import { CONTROL_PLANE_DIR_VARIABLE } from './chunk-G2YNJIGF.js';
7
6
  import { cliInvocation } from './chunk-CRB3POP4.js';
7
+ import { deadHooksState, huskyPrepareScript, preparePackageManagerCommand } from './chunk-TUSYOL2O.js';
8
8
  import { homeFromEnv } from './chunk-VNVZDWO3.js';
9
9
  import { readFileSync, mkdirSync, writeFileSync, renameSync, existsSync, statSync, appendFileSync, opendirSync, unlinkSync } from 'fs';
10
10
  import { spawn, execFileSync } from 'child_process';
@@ -502,15 +502,16 @@ function installedCliStalenessContext(home, cwd) {
502
502
  return `The installed OuterLayer CLI (~/.outerlayer/cli) is behind the workspace build: installed ${installedLabel}, workspace ${workspaceLabel}.
503
503
  ${howToRefresh}`;
504
504
  }
505
- function writeSessionStartContext(event, home, cwd, write, extraParts = []) {
505
+ function writeSessionStartContext(event, home, cwd, write, systemMessage, extraParts = []) {
506
506
  try {
507
507
  if (event !== "SessionStart") return;
508
508
  const parts = [syncHealthContext(home), installedCliStalenessContext(home, cwd), ...extraParts].filter(
509
509
  (s) => s !== null
510
510
  );
511
- if (parts.length === 0) return;
511
+ if (parts.length === 0 && systemMessage === void 0) return;
512
512
  write(
513
513
  JSON.stringify({
514
+ ...systemMessage === void 0 ? {} : { systemMessage },
514
515
  hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: parts.join("\n\n") }
515
516
  })
516
517
  );
@@ -571,7 +572,6 @@ function deadHooksAutoHealContext(cwd, home) {
571
572
  clearHealFailureMarker(home, state.repoRoot);
572
573
  return null;
573
574
  }
574
- var PIN_FILENAME = ".outerlayer-pin.json";
575
575
  function sourceCachePath(gitCommonDir) {
576
576
  return join(gitCommonDir, "outerlayer", "source.json");
577
577
  }
@@ -579,8 +579,8 @@ function contextPossiblyGoverned(cwd, home = homeFromEnv()) {
579
579
  try {
580
580
  const found = findGitDirs(cwd);
581
581
  if (found === void 0) return false;
582
- const { repoRoot, gitCommonDir } = found;
583
- if (existsSync(join(repoRoot, PIN_FILENAME))) return true;
582
+ const { gitCommonDir } = found;
583
+ if (existsSync(join(gitCommonDir, "outerlayer", "delivery.json"))) return true;
584
584
  if (!existsSync(join(home, ".outerlayer", "config.json"))) return false;
585
585
  let cache;
586
586
  try {
@@ -620,6 +620,50 @@ function recordDeliveredContext(cwd, sessionId, home) {
620
620
  home
621
621
  );
622
622
  }
623
+ async function missingControlPlane(cwd, sessionId, home, env) {
624
+ const dirs = findGitDirs(cwd);
625
+ if (dirs === void 0) return void 0;
626
+ if (existsSync(join(dirs.gitCommonDir, "outerlayer", "materialized.json")) || hasExcludeBlock(dirs.gitCommonDir)) {
627
+ const { cleanupEarlierCopies } = await import('./context-cleanup-HSVYDGMI.js');
628
+ cleanupEarlierCopies(dirs);
629
+ }
630
+ if (!contextPossiblyGoverned(cwd, home)) return void 0;
631
+ const { governanceOf } = await import('./control-plane-launch-NYVDJESK.js');
632
+ if (await governanceOf({ cwd, env, home }) !== "governed") return void 0;
633
+ recordMissingContext(dirs, sessionId, home);
634
+ const launcher = `${cliInvocation()} claude`;
635
+ return {
636
+ person: `This repository's shared instructions and skills are missing from this session, because it did not start through \`${launcher}\`. Start Claude Code with \`${launcher}\`, or run \`${cliInvocation()} init\` once to make plain \`claude\` do the same.`,
637
+ agent: `This repository is governed by a control plane, and this session did not start through \`${launcher}\`, so the control plane's shared instructions and skills are not loaded. Tell the person to start Claude Code with \`${launcher}\`.`
638
+ };
639
+ }
640
+ function recordMissingContext(dirs, sessionId, home) {
641
+ let cache;
642
+ try {
643
+ cache = JSON.parse(readFileSync(sourceCachePath(dirs.gitCommonDir), "utf8"));
644
+ } catch {
645
+ cache = {};
646
+ }
647
+ appendContextProvenance(
648
+ {
649
+ t: (/* @__PURE__ */ new Date()).toISOString(),
650
+ sessionId,
651
+ repoRoot: dirs.repoRoot,
652
+ ...typeof cache.repository === "string" ? { repository: cache.repository } : {},
653
+ ...typeof cache.ref === "string" ? { ref: cache.ref } : {},
654
+ sha: null,
655
+ verification: "none"
656
+ },
657
+ home
658
+ );
659
+ }
660
+ function hasExcludeBlock(gitCommonDir) {
661
+ try {
662
+ return readFileSync(join(gitCommonDir, "info", "exclude"), "utf8").includes("# BEGIN outerlayer context materialization");
663
+ } catch {
664
+ return false;
665
+ }
666
+ }
623
667
  async function runHookFast(event, home = homeFromEnv(), read = readStdin, env = process.env, spawnImpl = spawnDetached) {
624
668
  try {
625
669
  const stdin = read();
@@ -649,7 +693,7 @@ async function runHookFast(event, home = homeFromEnv(), read = readStdin, env =
649
693
  const hookSessionId = typeof payload.session_id === "string" ? payload.session_id : void 0;
650
694
  await maybeRenewWorkLease(resolvedEvent, hookSessionId, home, env, spawnImpl, Date.now(), sessionCwd);
651
695
  await handleWorkSessionEnd(resolvedEvent, hookSessionId, home, env, spawnImpl, sessionCwd);
652
- let materializeMessage;
696
+ let missingPlane;
653
697
  let deadHooksMessage;
654
698
  let workLaunchMessage;
655
699
  let hookErrorsMessage;
@@ -664,12 +708,9 @@ async function runHookFast(event, home = homeFromEnv(), read = readStdin, env =
664
708
  } catch (err) {
665
709
  logHookError(home, err);
666
710
  }
667
- } else if (contextPossiblyGoverned(sessionCwd, home)) {
711
+ } else {
668
712
  try {
669
- const { materializeContext: materialize } = await import('./context-materialize-Y5IJ4MQT.js');
670
- const sessionId = typeof payload.session_id === "string" ? payload.session_id : "unknown";
671
- const result = await materialize({ cwd: sessionCwd, sessionId, home });
672
- materializeMessage = result.message;
713
+ missingPlane = await missingControlPlane(sessionCwd, typeof payload.session_id === "string" ? payload.session_id : "unknown", home, env);
673
714
  } catch (err) {
674
715
  logHookError(home, err);
675
716
  }
@@ -697,7 +738,8 @@ async function runHookFast(event, home = homeFromEnv(), read = readStdin, env =
697
738
  home,
698
739
  sessionCwd,
699
740
  (out) => process.stdout.write(out),
700
- [materializeMessage, deadHooksMessage, workLaunchMessage, hookErrorsMessage, renewalHookMessage].filter(
741
+ missingPlane?.person,
742
+ [missingPlane?.agent, deadHooksMessage, workLaunchMessage, hookErrorsMessage, renewalHookMessage].filter(
701
743
  (s) => s !== void 0
702
744
  )
703
745
  );
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { splitRawFile } from './chunk-X5N6KC6V.js';
3
+ import { splitRawFile } from './chunk-WFSUUUAN.js';
4
4
  import { resolvePackageVersion } from './chunk-RA3O54FC.js';
5
5
  import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync } from 'fs';
6
6
  import { join, dirname } from 'path';
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { builtinKind } from './chunk-XGP3VYOG.js';
3
+ import { builtinKind } from './chunk-P53VEAOF.js';
4
4
  import { parseSize } from './chunk-7RQ73DLO.js';
5
5
  import { runnerDir } from './chunk-N2PXGQRL.js';
6
6
  import { dirname } from 'path';