@outerlayer/cli 0.2.0 → 0.4.1

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 +132 -0
  2. package/README.md +229 -60
  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-7MFH6HNC.js +94 -0
  7. package/dist/chunk-3TFPDVTI.js +94 -0
  8. package/dist/{chunk-4TNZMO7V.js → chunk-4C4THABW.js} +5566 -754
  9. package/dist/{chunk-R5KBGVII.js → chunk-5QRP3MVS.js} +30 -14
  10. package/dist/chunk-5Y3QQRUH.js +300 -0
  11. package/dist/chunk-774HEQL6.js +178 -0
  12. package/dist/{work-pr-cmd-AZQPWH4H.js → chunk-7WERLFVR.js} +9 -26
  13. package/dist/chunk-ABIWDUMS.js +104 -0
  14. package/dist/chunk-AUFG23AA.js +534 -0
  15. package/dist/chunk-B2G7JHHB.js +42 -0
  16. package/dist/chunk-BCJNHMZT.js +34 -0
  17. package/dist/chunk-BCLJSQCV.js +56 -0
  18. package/dist/chunk-BFESLKPP.js +61 -0
  19. package/dist/chunk-BJ3KTMDH.js +63 -0
  20. package/dist/chunk-BKO6JEZI.js +47 -0
  21. package/dist/chunk-BOLTI6LR.js +37 -0
  22. package/dist/chunk-CBPPJSAR.js +89 -0
  23. package/dist/chunk-DM3VFDS3.js +1461 -0
  24. package/dist/{chunk-65WXAANR.js → chunk-DVDEBNQJ.js} +17 -2
  25. package/dist/{chunk-TFUIDMOB.js → chunk-EABW6AJQ.js} +12 -1
  26. package/dist/{chunk-KFYJV2ZG.js → chunk-EMY4I27X.js} +1 -1
  27. package/dist/{chunk-NTTPJV35.js → chunk-F47JBIAW.js} +3 -1
  28. package/dist/chunk-F6GFZXZ3.js +60 -0
  29. package/dist/{chunk-YOOSBOKS.js → chunk-F7CU5ABH.js} +1 -1
  30. package/dist/{emit-cmd-TWSYYEZI.js → chunk-H5NCNVDA.js} +68 -17
  31. package/dist/chunk-HZRNMGLF.js +2233 -0
  32. package/dist/chunk-LF6MLJCY.js +38 -0
  33. package/dist/{chunk-U32VSRLO.js → chunk-LT5TZHYH.js} +95 -634
  34. package/dist/chunk-LYCDNMOH.js +83 -0
  35. package/dist/chunk-M5POMKKW.js +1051 -0
  36. package/dist/{mcp-install-cmd-FDQH6SEN.js → chunk-MQ3IPHIZ.js} +35 -18
  37. package/dist/{chunk-D77LS3UI.js → chunk-N5FOE5PS.js} +33 -4
  38. package/dist/chunk-N6LURUEF.js +64 -0
  39. package/dist/chunk-OGFGZQA3.js +23 -0
  40. package/dist/chunk-QDQEUVUF.js +9 -0
  41. package/dist/chunk-QOZKPLVJ.js +226 -0
  42. package/dist/chunk-QY22NZUW.js +1764 -0
  43. package/dist/chunk-RA3O54FC.js +30 -0
  44. package/dist/chunk-RFUPN6KX.js +71 -0
  45. package/dist/chunk-SY4GK4SP.js +55 -0
  46. package/dist/chunk-TBT347UY.js +41 -0
  47. package/dist/chunk-TWNWNS2Q.js +1084 -0
  48. package/dist/{chunk-NXLLURA4.js → chunk-UFFXNXLQ.js} +115 -71
  49. package/dist/chunk-USO2DKBD.js +421 -0
  50. package/dist/chunk-W4SQCOZU.js +206 -0
  51. package/dist/{chunk-Q6CL5THG.js → chunk-W5FVUZXV.js} +208 -61
  52. package/dist/chunk-WFJ65NUD.js +383 -0
  53. package/dist/{chunk-A3WLZX2F.js → chunk-WIOZAJ2W.js} +15 -2
  54. package/dist/chunk-WO2BXCTQ.js +83 -0
  55. package/dist/{chunk-I3ETLSNE.js → chunk-XCCLFVXM.js} +120 -11
  56. package/dist/{context-materialize-6WKT3RBQ.js → chunk-XDDW4FRS.js} +134 -26
  57. package/dist/chunk-Y7KJLXYN.js +232 -0
  58. package/dist/chunk-YFHIIH4B.js +102 -0
  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-W62AFRSW.js} +616 -878
  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-ZTJONP37.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-44CQVWGT.js +6 -0
  70. package/dist/docker-NEGDLU6D.js +7 -0
  71. package/dist/doctor-53F2PYY7.js +43 -0
  72. package/dist/doctor-ZLGZYMDK.js +426 -0
  73. package/dist/{emit-artifact-cmd-UQVT3OKW.js → emit-artifact-cmd-AKZP7TMF.js} +87 -23
  74. package/dist/emit-cmd-DCERAU27.js +9 -0
  75. package/dist/emit-criteria-cmd-7MXQVCKD.js +166 -0
  76. package/dist/{emit-finding-cmd-FDOU2OPD.js → emit-finding-cmd-N2FTGCVC.js} +34 -31
  77. package/dist/{emit-result-cmd-H2T4C2K3.js → emit-result-cmd-4CPDBMN2.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-55Y4LHWF.js +36 -0
  87. package/dist/init-XXEKWZQP.js +221 -0
  88. package/dist/init-cmd-WLST2F7G.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-JFIBP4B7.js +43 -0
  95. package/dist/machine-ZBPT2J3R.js +35 -0
  96. package/dist/mcp-install-cmd-RLK4SO3N.js +9 -0
  97. package/dist/{mcp-serve-cmd-57EZZOTL.js → mcp-serve-cmd-N6UD2KBX.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-UJXWZRN5.js} +30 -13
  105. package/dist/{statusline-fast-3C5OXHDD.js → statusline-fast-SVTMBMD7.js} +3 -2
  106. package/dist/sync-cmd-4G4A7EVN.js +28 -0
  107. package/dist/version-BWM6VLDI.js +6 -0
  108. package/dist/{watch-3DTPJETH.js → watch-7SKJKGAA.js} +19 -8
  109. package/dist/{work-claim-cmd-5AUNCLS2.js → work-claim-cmd-5F7VZ42N.js} +34 -21
  110. package/dist/work-cmd-QAUZ7MTD.js +17 -0
  111. package/dist/work-comment-cmd-BNCSBTLH.js +106 -0
  112. package/dist/{work-launch-LT663PB3.js → work-launch-YI4CJDAP.js} +1 -1
  113. package/dist/work-open-pr-cmd-VFYZHIIW.js +156 -0
  114. package/dist/work-pr-cmd-PV32ZLSI.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 +108 -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
@@ -0,0 +1,383 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { ArtifactTombstoneRecordSchema, ArtifactSpoolRecordSchema } from './chunk-4C4THABW.js';
4
+ import { homeFromEnv } from './chunk-VNVZDWO3.js';
5
+ import { mkdirSync, appendFileSync, writeFileSync, renameSync, existsSync, unlinkSync, readdirSync, readFileSync } from 'fs';
6
+ import { join, dirname } from 'path';
7
+
8
+ createRequire(import.meta.url);
9
+ function artifactsSpoolPath(home = homeFromEnv()) {
10
+ return join(home, ".outerlayer", "spool", "artifacts.jsonl");
11
+ }
12
+ function artifactBlobsDir(home = homeFromEnv()) {
13
+ return join(home, ".outerlayer", "spool", "artifact-blobs");
14
+ }
15
+ function artifactsStatePath(home = homeFromEnv()) {
16
+ return join(home, ".outerlayer", "spool", "artifacts.state.json");
17
+ }
18
+ var ARTIFACT_RETRY_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
19
+ function appendArtifactRecord(record, home = homeFromEnv()) {
20
+ const path = artifactsSpoolPath(home);
21
+ mkdirSync(dirname(path), { recursive: true });
22
+ appendFileSync(path, JSON.stringify(record) + "\n");
23
+ }
24
+ function appendArtifactTombstone(artifactId, home = homeFromEnv(), now = () => /* @__PURE__ */ new Date()) {
25
+ const path = artifactsSpoolPath(home);
26
+ mkdirSync(dirname(path), { recursive: true });
27
+ const record = { rec: "artifact-tombstone", artifactId, t: now().toISOString() };
28
+ appendFileSync(path, JSON.stringify(record) + "\n");
29
+ }
30
+ function walkArtifactSpoolLines(home, visit) {
31
+ let buf;
32
+ try {
33
+ buf = readFileSync(artifactsSpoolPath(home));
34
+ } catch {
35
+ return;
36
+ }
37
+ let pos = 0;
38
+ while (pos < buf.length) {
39
+ const nl = buf.indexOf(10, pos);
40
+ const textEnd = nl === -1 ? buf.length : nl;
41
+ const text = buf.subarray(pos, textEnd).toString("utf8").trim();
42
+ if (text) visit(text, pos);
43
+ if (nl === -1) break;
44
+ pos = nl + 1;
45
+ }
46
+ }
47
+ function isArtifactTombstoned(artifactId, home = homeFromEnv()) {
48
+ let found = false;
49
+ walkArtifactSpoolLines(home, (text) => {
50
+ if (found) return;
51
+ try {
52
+ const parsed = ArtifactTombstoneRecordSchema.safeParse(JSON.parse(text));
53
+ if (parsed.success && parsed.data.artifactId === artifactId) found = true;
54
+ } catch {
55
+ }
56
+ });
57
+ return found;
58
+ }
59
+ var KNOWN_SPOOL_RECORD_TYPES = /* @__PURE__ */ new Set(["artifact", "artifact-tombstone"]);
60
+ var SHA256_HEX = /^[0-9a-f]{64}$/;
61
+ var MAX_DISPLAYED_REC_TYPE_LENGTH = 100;
62
+ var CONTROL_CHAR_PATTERN = new RegExp(
63
+ `[${String.fromCharCode(0)}-${String.fromCharCode(31)}${String.fromCharCode(127)}-${String.fromCharCode(159)}]`,
64
+ "g"
65
+ );
66
+ function sanitizeTerminal(text) {
67
+ return text.replace(CONTROL_CHAR_PATTERN, "");
68
+ }
69
+ function sanitizeRecType(rec) {
70
+ const stripped = sanitizeTerminal(rec);
71
+ return stripped.length > MAX_DISPLAYED_REC_TYPE_LENGTH ? `${stripped.slice(0, MAX_DISPLAYED_REC_TYPE_LENGTH)}\u2026` : stripped;
72
+ }
73
+ function readUnknownRecordEnvelope(json) {
74
+ if (typeof json !== "object" || json === null) return void 0;
75
+ const rec = json.rec;
76
+ const t = json.t;
77
+ if (typeof rec !== "string" || typeof t !== "string") return void 0;
78
+ const shaRaw = json.sha256;
79
+ const sha256 = typeof shaRaw === "string" && SHA256_HEX.test(shaRaw) ? shaRaw : void 0;
80
+ return { rec, t, sha256 };
81
+ }
82
+ function classifySpoolLine(text) {
83
+ let json;
84
+ try {
85
+ json = JSON.parse(text);
86
+ } catch {
87
+ return { kind: "skipped", reason: "not valid JSON" };
88
+ }
89
+ const tombstone = ArtifactTombstoneRecordSchema.safeParse(json);
90
+ if (tombstone.success) return { kind: "tombstone", artifactId: tombstone.data.artifactId };
91
+ const parsed = ArtifactSpoolRecordSchema.safeParse(json);
92
+ if (parsed.success) return { kind: "record", record: parsed.data };
93
+ const envelope = readUnknownRecordEnvelope(json);
94
+ if (envelope !== void 0 && !KNOWN_SPOOL_RECORD_TYPES.has(envelope.rec)) {
95
+ const sanitizedRecType = sanitizeRecType(envelope.rec);
96
+ const recType = sanitizedRecType === "" ? "(empty)" : `"${sanitizedRecType}"`;
97
+ return {
98
+ kind: "held",
99
+ reason: `unrecognized record type ${recType} \u2014 this build's schemas do not recognize it`,
100
+ ...envelope.sha256 !== void 0 ? { sha256: envelope.sha256 } : {}
101
+ };
102
+ }
103
+ const issue = parsed.error.issues[0];
104
+ const issueDetail = issue ? ` (${issue.path.length > 0 ? `${issue.path.join(".")}: ` : ""}${sanitizeTerminal(issue.message)})` : "";
105
+ return { kind: "skipped", reason: `does not match the artifact spool record shape${issueDetail}` };
106
+ }
107
+ function legacySidecarPaths(home) {
108
+ const dir = join(home, ".outerlayer", "spool");
109
+ return [
110
+ join(dir, "artifacts.watermark"),
111
+ join(dir, "artifacts.uploaded.json"),
112
+ join(dir, "artifacts.dropped.json"),
113
+ join(dir, "artifacts.held-offsets.json"),
114
+ join(dir, "artifacts.pending-first-seen.json")
115
+ ];
116
+ }
117
+ function readLegacySidecars(home) {
118
+ const dir = join(home, ".outerlayer", "spool");
119
+ let watermark = 0;
120
+ try {
121
+ const n = parseInt(readFileSync(join(dir, "artifacts.watermark"), "utf8").trim(), 10);
122
+ if (Number.isFinite(n) && n >= 0) watermark = n;
123
+ } catch {
124
+ }
125
+ const readIdSet = (name) => {
126
+ try {
127
+ const raw = JSON.parse(readFileSync(join(dir, name), "utf8"));
128
+ return new Set(Array.isArray(raw) ? raw.filter((v) => typeof v === "string") : []);
129
+ } catch {
130
+ return /* @__PURE__ */ new Set();
131
+ }
132
+ };
133
+ let pendingFirstSeen = {};
134
+ try {
135
+ const raw = JSON.parse(readFileSync(join(dir, "artifacts.pending-first-seen.json"), "utf8"));
136
+ if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
137
+ const out = {};
138
+ for (const [key, value] of Object.entries(raw)) {
139
+ if (typeof value === "string") out[key] = value;
140
+ }
141
+ pendingFirstSeen = out;
142
+ }
143
+ } catch {
144
+ }
145
+ return {
146
+ watermark,
147
+ uploadedIds: readIdSet("artifacts.uploaded.json"),
148
+ droppedIds: readIdSet("artifacts.dropped.json"),
149
+ pendingFirstSeen
150
+ };
151
+ }
152
+ function computeMigratedState(home) {
153
+ const legacy = readLegacySidecars(home);
154
+ const state = /* @__PURE__ */ new Map();
155
+ const tombstoned = /* @__PURE__ */ new Set();
156
+ const rawRecords = [];
157
+ walkArtifactSpoolLines(home, (text, offset) => {
158
+ const classified = classifySpoolLine(text);
159
+ if (classified.kind === "tombstone") {
160
+ tombstoned.add(classified.artifactId);
161
+ } else if (classified.kind === "record") {
162
+ rawRecords.push({
163
+ artifactId: classified.record.artifactId,
164
+ offset,
165
+ sha256: classified.record.sha256,
166
+ t: classified.record.t
167
+ });
168
+ }
169
+ });
170
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
171
+ for (const { artifactId, offset, sha256, t } of rawRecords) {
172
+ if (offset >= legacy.watermark) continue;
173
+ if (legacy.uploadedIds.has(artifactId)) {
174
+ state.set(artifactId, { state: "uploaded", attempts: 1, firstSeen: t || nowIso, sha256, reachedServer: true });
175
+ continue;
176
+ }
177
+ if (legacy.droppedIds.has(artifactId)) {
178
+ state.set(artifactId, {
179
+ state: "dropped",
180
+ attempts: 1,
181
+ firstSeen: legacy.pendingFirstSeen[artifactId] ?? t ?? nowIso,
182
+ sha256,
183
+ reachedServer: false
184
+ });
185
+ continue;
186
+ }
187
+ if (tombstoned.has(artifactId)) continue;
188
+ state.set(artifactId, { state: "dropped", attempts: 1, firstSeen: t || nowIso, sha256, reachedServer: true });
189
+ }
190
+ return state;
191
+ }
192
+ function readPersistedArtifactSpoolState(home) {
193
+ let raw;
194
+ try {
195
+ raw = readFileSync(artifactsStatePath(home), "utf8");
196
+ } catch {
197
+ return { state: /* @__PURE__ */ new Map(), corrupt: false };
198
+ }
199
+ try {
200
+ const parsed = JSON.parse(raw);
201
+ const records = parsed !== null && typeof parsed === "object" && Array.isArray(parsed.records) ? parsed.records : void 0;
202
+ if (records === void 0) return { state: /* @__PURE__ */ new Map(), corrupt: true };
203
+ const state = /* @__PURE__ */ new Map();
204
+ for (const entry of records) {
205
+ if (entry === null || typeof entry !== "object") continue;
206
+ const e = entry;
207
+ if (typeof e.artifactId !== "string") continue;
208
+ if (e.state !== "pending" && e.state !== "uploaded" && e.state !== "held" && e.state !== "dropped") continue;
209
+ state.set(e.artifactId, {
210
+ state: e.state,
211
+ attempts: typeof e.attempts === "number" && Number.isFinite(e.attempts) ? e.attempts : 0,
212
+ firstSeen: typeof e.firstSeen === "string" ? e.firstSeen : (/* @__PURE__ */ new Date(0)).toISOString(),
213
+ sha256: typeof e.sha256 === "string" ? e.sha256 : "",
214
+ reachedServer: e.reachedServer === true
215
+ });
216
+ }
217
+ return { state, corrupt: false };
218
+ } catch {
219
+ return { state: /* @__PURE__ */ new Map(), corrupt: true };
220
+ }
221
+ }
222
+ var ARTIFACT_STATE_RESOLVED_CAP = 5e3;
223
+ function writeArtifactSpoolState(state, home = homeFromEnv()) {
224
+ try {
225
+ const path = artifactsStatePath(home);
226
+ mkdirSync(dirname(path), { recursive: true });
227
+ const tmp = `${path}.tmp`;
228
+ const pending = [];
229
+ const resolved = [];
230
+ for (const pair of state.entries()) {
231
+ (pair[1].state === "uploaded" || pair[1].state === "dropped" ? resolved : pending).push(pair);
232
+ }
233
+ const cappedResolved = resolved.length > ARTIFACT_STATE_RESOLVED_CAP ? resolved.slice(-ARTIFACT_STATE_RESOLVED_CAP) : resolved;
234
+ const records = [...pending, ...cappedResolved].map(([artifactId, entry]) => ({ artifactId, ...entry }));
235
+ writeFileSync(tmp, JSON.stringify({ version: 1, records }));
236
+ renameSync(tmp, path);
237
+ return true;
238
+ } catch {
239
+ return false;
240
+ }
241
+ }
242
+ function migrateLegacyArtifactSidecars(home = homeFromEnv()) {
243
+ if (existsSync(artifactsStatePath(home))) return;
244
+ const paths = legacySidecarPaths(home);
245
+ if (!paths.some((p) => existsSync(p))) return;
246
+ writeArtifactSpoolState(computeMigratedState(home), home);
247
+ for (const p of paths) {
248
+ try {
249
+ unlinkSync(p);
250
+ } catch {
251
+ }
252
+ }
253
+ }
254
+ function readEffectiveArtifactSpoolState(home = homeFromEnv()) {
255
+ const { state: persisted, corrupt } = existsSync(artifactsStatePath(home)) ? readPersistedArtifactSpoolState(home) : { state: computeMigratedState(home), corrupt: false };
256
+ const state = new Map(persisted);
257
+ const tombstonedIds = /* @__PURE__ */ new Set();
258
+ walkArtifactSpoolLines(home, (text) => {
259
+ const classified = classifySpoolLine(text);
260
+ if (classified.kind === "tombstone") tombstonedIds.add(classified.artifactId);
261
+ });
262
+ if (tombstonedIds.size > 0) {
263
+ walkArtifactSpoolLines(home, (text) => {
264
+ const classified = classifySpoolLine(text);
265
+ if (classified.kind !== "record") return;
266
+ const { record } = classified;
267
+ if (!tombstonedIds.has(record.artifactId)) return;
268
+ const existing = state.get(record.artifactId);
269
+ if (existing === void 0 || existing.state === "held" || existing.state === "pending") {
270
+ state.set(record.artifactId, {
271
+ state: "dropped",
272
+ attempts: existing?.attempts ?? 0,
273
+ firstSeen: existing?.firstSeen ?? record.t,
274
+ sha256: record.sha256,
275
+ reachedServer: false
276
+ });
277
+ }
278
+ });
279
+ }
280
+ return { state, corrupt };
281
+ }
282
+ function planArtifactUpload(opts = {}) {
283
+ const home = opts.home ?? homeFromEnv();
284
+ const { state: effective, corrupt } = readEffectiveArtifactSpoolState(home);
285
+ const state = new Map(effective);
286
+ const candidates = [];
287
+ const skipped = [];
288
+ const held = [];
289
+ const tombstonedIds = /* @__PURE__ */ new Set();
290
+ walkArtifactSpoolLines(home, (text) => {
291
+ const classified = classifySpoolLine(text);
292
+ if (classified.kind === "tombstone") tombstonedIds.add(classified.artifactId);
293
+ });
294
+ walkArtifactSpoolLines(home, (text, offset) => {
295
+ const classified = classifySpoolLine(text);
296
+ if (classified.kind === "tombstone") return;
297
+ if (classified.kind === "skipped") {
298
+ skipped.push({ offset, reason: classified.reason });
299
+ return;
300
+ }
301
+ if (classified.kind === "held") {
302
+ held.push({ offset, reason: classified.reason, ...classified.sha256 !== void 0 ? { sha256: classified.sha256 } : {} });
303
+ return;
304
+ }
305
+ const { record } = classified;
306
+ if (tombstonedIds.has(record.artifactId)) return;
307
+ const existing = state.get(record.artifactId);
308
+ if (existing === void 0 || existing.state === "held" || existing.state === "pending") {
309
+ candidates.push({ record });
310
+ }
311
+ });
312
+ return { candidates, state, skipped, held, corrupt };
313
+ }
314
+ function neededArtifactShas(home, finalState) {
315
+ const needed = /* @__PURE__ */ new Set();
316
+ walkArtifactSpoolLines(home, (text) => {
317
+ const classified = classifySpoolLine(text);
318
+ if (classified.kind === "held") {
319
+ if (classified.sha256 !== void 0) needed.add(classified.sha256);
320
+ return;
321
+ }
322
+ if (classified.kind !== "record") return;
323
+ const entry = finalState.get(classified.record.artifactId);
324
+ if (entry === void 0 || entry.state === "held" || entry.state === "pending") needed.add(classified.record.sha256);
325
+ });
326
+ return needed;
327
+ }
328
+ function reclaimUnneededArtifactBlobs(home, neededShas) {
329
+ let names;
330
+ try {
331
+ names = readdirSync(artifactBlobsDir(home));
332
+ } catch {
333
+ return 0;
334
+ }
335
+ let reclaimed = 0;
336
+ for (const name of names) {
337
+ if (!SHA256_HEX.test(name) || neededShas.has(name)) continue;
338
+ try {
339
+ unlinkSync(join(artifactBlobsDir(home), name));
340
+ reclaimed += 1;
341
+ } catch {
342
+ }
343
+ }
344
+ return reclaimed;
345
+ }
346
+ function localArtifactIds(home = homeFromEnv()) {
347
+ const ids = /* @__PURE__ */ new Set();
348
+ walkArtifactSpoolLines(home, (text) => {
349
+ const classified = classifySpoolLine(text);
350
+ if (classified.kind === "record") ids.add(classified.record.artifactId);
351
+ });
352
+ return ids;
353
+ }
354
+ function findPendingArtifactRecord(artifactId, home = homeFromEnv()) {
355
+ let found;
356
+ walkArtifactSpoolLines(home, (text) => {
357
+ if (found !== void 0) return;
358
+ const classified = classifySpoolLine(text);
359
+ if (classified.kind === "record" && classified.record.artifactId === artifactId) found = classified.record;
360
+ });
361
+ if (found === void 0) return void 0;
362
+ const { state } = readEffectiveArtifactSpoolState(home);
363
+ const entry = state.get(artifactId);
364
+ if (entry !== void 0 && entry.state !== "held" && entry.state !== "pending") return void 0;
365
+ return { record: found };
366
+ }
367
+ function resolveArtifactTurnIndex(turns, filename) {
368
+ let found;
369
+ for (const turn of turns) {
370
+ for (const call of turn.toolCalls ?? []) {
371
+ const haystack = [
372
+ call.name,
373
+ typeof call.input === "string" ? call.input : call.input === void 0 ? "" : JSON.stringify(call.input),
374
+ typeof call.output === "string" ? call.output : call.output === void 0 ? "" : JSON.stringify(call.output),
375
+ call.file ?? ""
376
+ ].join("\n");
377
+ if (haystack.includes("emit artifact") && haystack.includes(filename)) found = turn.index;
378
+ }
379
+ }
380
+ return found;
381
+ }
382
+
383
+ export { ARTIFACT_RETRY_MAX_AGE_MS, appendArtifactRecord, appendArtifactTombstone, artifactBlobsDir, artifactsSpoolPath, artifactsStatePath, findPendingArtifactRecord, isArtifactTombstoned, localArtifactIds, migrateLegacyArtifactSidecars, neededArtifactShas, planArtifactUpload, readEffectiveArtifactSpoolState, reclaimUnneededArtifactBlobs, resolveArtifactTurnIndex, sanitizeTerminal, writeArtifactSpoolState };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { CONTEXT_PIN_SCHEMA_VERSION, CONTEXT_PIN_FILENAME } from './chunk-KFYJV2ZG.js';
3
+ import { CONTEXT_PIN_SCHEMA_VERSION, CONTEXT_PIN_FILENAME } from './chunk-EMY4I27X.js';
4
4
  import { readFileSync, existsSync, writeFileSync, mkdirSync, utimesSync, statSync, unlinkSync } from 'fs';
5
5
  import { join, dirname } from 'path';
6
6
 
@@ -116,5 +116,18 @@ function writeSourceCache(gitCommonDir, cache) {
116
116
  function isSourceCacheFresh(cache, now) {
117
117
  return now - Date.parse(cache.checkedAt) < cache.ttlMs;
118
118
  }
119
+ function buildHoldPath(gitCommonDir) {
120
+ return join(stateDir(gitCommonDir), "build-hold.json");
121
+ }
122
+ function readBuildHold(gitCommonDir) {
123
+ const parsed = readJson(buildHoldPath(gitCommonDir));
124
+ if (parsed && typeof parsed.sha === "string" && typeof parsed.repository === "string" && typeof parsed.ref === "string" && typeof parsed.at === "string") {
125
+ return parsed;
126
+ }
127
+ return void 0;
128
+ }
129
+ function writeBuildHold(gitCommonDir, hold) {
130
+ writeJson(buildHoldPath(gitCommonDir), hold);
131
+ }
119
132
 
120
- export { SOURCE_CACHE_TTL_RETRY_MS, SOURCE_CACHE_TTL_SUCCESS_MS, clearDegraded, isLastCheckFresh, isSourceCacheFresh, manifestFilesPresent, pinExists, readContextManifest, readDegraded, readPin, readSourceCache, touchLastCheck, writeContextManifest, writeDegraded, writePinIfChanged, writeSourceCache };
133
+ export { SOURCE_CACHE_TTL_RETRY_MS, SOURCE_CACHE_TTL_SUCCESS_MS, clearDegraded, isLastCheckFresh, isSourceCacheFresh, manifestFilesPresent, pinExists, readBuildHold, readContextManifest, readDegraded, readPin, readSourceCache, touchLastCheck, writeBuildHold, writeContextManifest, writeDegraded, writePinIfChanged, writeSourceCache };
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { splitRawFile } from './chunk-EMY4I27X.js';
4
+ import { resolvePackageVersion } from './chunk-RA3O54FC.js';
5
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync } from 'fs';
6
+ import { join, dirname } from 'path';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ createRequire(import.meta.url);
10
+ var TEMPLATE_SKILLS = ["spec", "writing-specs", "build"];
11
+ var MAINTAINED_SKILLS = ["emitting-evidence", "amend", "reporting-findings", "outerlayer"];
12
+ var TEMPLATE_CLASS_LINE = "<!-- yours to edit. Copied once from the default template by outerlayer init; never overwritten. -->";
13
+ function maintainedClassLine(version) {
14
+ return `<!-- maintained by OuterLayer ${version}. Edits are overwritten on the next outerlayer context emit. -->`;
15
+ }
16
+ function resolvePackRoot(entryPath = fileURLToPath(import.meta.url)) {
17
+ return join(dirname(dirname(entryPath)), "skill-pack");
18
+ }
19
+ function skillDir(skill) {
20
+ return `.outerlayer/skills/${skill}`;
21
+ }
22
+ function renderSkillFile(skillClass, content, version) {
23
+ const line = skillClass === "maintained" ? maintainedClassLine(version) : TEMPLATE_CLASS_LINE;
24
+ const { frontmatterInner, body } = splitRawFile(content);
25
+ if (frontmatterInner === null) return `${line}
26
+ ${content}`;
27
+ const block = content.slice(0, content.length - body.length);
28
+ return `${block}${block.endsWith("\n") ? "" : "\n"}${line}
29
+ ${body}`;
30
+ }
31
+ function filesUnder(dir, base = dir) {
32
+ const out = [];
33
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
34
+ const abs = join(dir, entry.name);
35
+ if (entry.isDirectory()) out.push(...filesUnder(abs, base));
36
+ else if (entry.isFile()) out.push(abs.slice(base.length + 1).split("\\").join("/"));
37
+ }
38
+ return out.sort();
39
+ }
40
+ function orderSkillFiles(relPaths) {
41
+ return [...relPaths.filter((p) => p === "SKILL.md"), ...relPaths.filter((p) => p !== "SKILL.md")];
42
+ }
43
+ function loadClass(packRoot, skillClass, names, version) {
44
+ const files = [];
45
+ for (const skill of names) {
46
+ const dir = join(packRoot, skillClass, skill);
47
+ if (!existsSync(dir)) throw new Error(`skill pack is missing ${skillClass}/${skill} under ${packRoot}`);
48
+ for (const rel of orderSkillFiles(filesUnder(dir))) {
49
+ const raw = readFileSync(join(dir, rel), "utf8");
50
+ files.push({
51
+ skill,
52
+ skillClass,
53
+ path: `${skillDir(skill)}/${rel}`,
54
+ content: rel === "SKILL.md" ? renderSkillFile(skillClass, raw, version) : raw
55
+ });
56
+ }
57
+ }
58
+ return files;
59
+ }
60
+ function loadTemplateFiles(packRoot = resolvePackRoot(), version = resolvePackageVersion()) {
61
+ return loadClass(packRoot, "template", TEMPLATE_SKILLS, version);
62
+ }
63
+ function loadMaintainedFiles(packRoot = resolvePackRoot(), version = resolvePackageVersion()) {
64
+ return loadClass(packRoot, "maintained", MAINTAINED_SKILLS, version);
65
+ }
66
+ function writeMaintainedFiles(repoRoot, files) {
67
+ const created = [];
68
+ const rewritten = [];
69
+ for (const file of files) {
70
+ const abs = join(repoRoot, file.path);
71
+ const exists = existsSync(abs);
72
+ if (exists && readFileSync(abs, "utf8") === file.content) continue;
73
+ mkdirSync(dirname(abs), { recursive: true });
74
+ writeFileSync(abs, file.content);
75
+ (exists ? rewritten : created).push(file.path);
76
+ }
77
+ return { created, rewritten };
78
+ }
79
+ function maintainedFilesForExistingSkills(repoRoot, files) {
80
+ return files.filter((file) => existsSync(join(repoRoot, skillDir(file.skill))));
81
+ }
82
+
83
+ export { MAINTAINED_SKILLS, TEMPLATE_SKILLS, loadMaintainedFiles, loadTemplateFiles, maintainedFilesForExistingSkills, resolvePackRoot, skillDir, writeMaintainedFiles };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
3
  import { homeFromEnv } from './chunk-VNVZDWO3.js';
4
- import { readFileSync, readdirSync, existsSync, mkdirSync, writeFileSync, unlinkSync, chmodSync } from 'fs';
4
+ import { mkdirSync, chmodSync, writeFileSync, readFileSync, readdirSync, existsSync, unlinkSync } from 'fs';
5
5
  import { join, isAbsolute, dirname, basename, resolve } from 'path';
6
6
  import { execFileSync } from 'child_process';
7
7
 
@@ -74,8 +74,103 @@ function contextExcludesPresent(gitCommonDir) {
74
74
  if (beginIdx === -1) return false;
75
75
  return BLOCK_LINES.every((l, i) => lines[beginIdx + i] === l);
76
76
  }
77
- var JOURNAL_HOOK_MARKER = "# outerlayer post-rewrite journal";
77
+ var HOOKS_DIR_NAME = "git-hooks";
78
+ var CO_AUTHOR_FILE = "co-author";
79
+ var HOOKS = [
80
+ "applypatch-msg",
81
+ "pre-applypatch",
82
+ "post-applypatch",
83
+ "pre-commit",
84
+ "pre-merge-commit",
85
+ "prepare-commit-msg",
86
+ "commit-msg",
87
+ "post-commit",
88
+ "pre-rebase",
89
+ "post-checkout",
90
+ "post-merge",
91
+ "pre-push",
92
+ "pre-receive",
93
+ "update",
94
+ "post-receive",
95
+ "post-update",
96
+ "reference-transaction",
97
+ "pre-auto-gc",
98
+ "post-rewrite",
99
+ "sendemail-validate",
100
+ "post-index-change"
101
+ ];
102
+ var READS_STDIN = /* @__PURE__ */ new Set(["pre-push", "pre-receive", "post-receive", "reference-transaction", "post-rewrite"]);
78
103
  var PRE_COMMIT_GUARD_MARKER = "# outerlayer materialized-context commit guard";
104
+ var RUN_GUARD = `common=$(git rev-parse --git-common-dir 2>/dev/null)
105
+ guard=$common/hooks/pre-commit
106
+ if [ "$guard" != "$hook" ] && [ -f "$guard" ] && [ -x "$guard" ] &&
107
+ sed -n 2p "$guard" | grep -q '^${PRE_COMMIT_GUARD_MARKER}'; then
108
+ "$guard" "$@" || exit $?
109
+ fi
110
+ `;
111
+ var ADD_TRAILER = `
112
+ # Every commit of the build names the requester once: a trailer already there is left alone.
113
+ file=\${1-}
114
+ if [ -n "$file" ] && [ -f "$file" ]; then
115
+ git interpret-trailers --in-place --if-exists addIfDifferent --trailer "Co-authored-by: $(cat "$here/${CO_AUTHOR_FILE}")" "$file" ||
116
+ echo "outerlayer: could not add the co-author line to the commit message" >&2
117
+ fi
118
+ `;
119
+ var HOOK_WRAPPER_MARKER = "# Written by the OuterLayer runner.";
120
+ function script(name) {
121
+ const adds = name === "prepare-commit-msg";
122
+ return `#!/bin/sh
123
+ ${HOOK_WRAPPER_MARKER} Runs the repository's own ${name} hook, then
124
+ # ${adds ? "adds the co-author line to the commit message" : "hands its status on"}.
125
+ here=$(dirname "$0")
126
+
127
+ # The hooks path git would use without this directory: cut the list of configuration
128
+ # entries the runner passes just before its own core.hooksPath entry, then ask git.
129
+ n=\${GIT_CONFIG_COUNT-0}
130
+ case $n in '' | *[!0-9]*) n=0 ;; esac
131
+ cut=$n
132
+ i=$n
133
+ while [ "$i" -gt 0 ]; do
134
+ i=$((i - 1))
135
+ eval "key=\\\${GIT_CONFIG_KEY_$i-}"
136
+ eval "value=\\\${GIT_CONFIG_VALUE_$i-}"
137
+ if [ "$key" = core.hooksPath ] && { [ "$value" = "$here" ] || [ "$value" -ef "$here" ]; }; then
138
+ cut=$i
139
+ break
140
+ fi
141
+ done
142
+ path=$(GIT_CONFIG_COUNT=$cut git config --type=path core.hooksPath 2>/dev/null) || path=
143
+ if [ -z "$path" ]; then
144
+ path=$(git rev-parse --git-common-dir 2>/dev/null)/hooks
145
+ fi
146
+
147
+ hook=$path/${name}
148
+ ${name === "pre-commit" ? RUN_GUARD : ""}status=0
149
+ if [ "$path" -ef "$here" ] || [ ! -f "$hook" ] || [ ! -x "$hook" ]; then
150
+ ${READS_STDIN.has(name) ? " cat >/dev/null\n" : ""} :
151
+ else
152
+ ${adds ? ` "$hook" "$@" || status=$?` : ` exec "$hook" "$@"`}
153
+ fi
154
+ ${adds ? `[ "$status" -eq 0 ] || exit "$status"
155
+ ${ADD_TRAILER}` : ""}exit 0
156
+ `;
157
+ }
158
+ function writeCommitHooks(buildDir, coAuthor) {
159
+ const dir = join(buildDir, HOOKS_DIR_NAME);
160
+ mkdirSync(dir, { recursive: true });
161
+ chmodSync(dir, 493);
162
+ for (const name of HOOKS) {
163
+ const path = join(dir, name);
164
+ writeFileSync(path, script(name));
165
+ chmodSync(path, 493);
166
+ }
167
+ const data = join(dir, CO_AUTHOR_FILE);
168
+ writeFileSync(data, `${coAuthor.name} <${coAuthor.email}>
169
+ `);
170
+ chmodSync(data, 420);
171
+ return dir;
172
+ }
173
+ var JOURNAL_HOOK_MARKER = "# outerlayer post-rewrite journal";
79
174
  var PRE_COMMIT_GUARD_VERSION = `${PRE_COMMIT_GUARD_MARKER} v1`;
80
175
  var PRE_COMMIT_GUARD_BODY_SIGNATURE = "outerlayer: refusing to commit materialized control-plane context";
81
176
  var PRE_COMMIT_GUARD_SCRIPT = `#!/bin/sh
@@ -114,8 +209,8 @@ exit 1
114
209
  function managedHookScripts() {
115
210
  return [["pre-commit", printableHookScript(PRE_COMMIT_GUARD_SCRIPT)]];
116
211
  }
117
- function printableHookScript(script) {
118
- const lines = script.split("\n");
212
+ function printableHookScript(script2) {
213
+ const lines = script2.split("\n");
119
214
  return [lines[0], ...lines.slice(2)].join("\n");
120
215
  }
121
216
  function git(cwd, args) {
@@ -141,7 +236,7 @@ function commonHooksDir(cwd) {
141
236
  if (rel === void 0) return void 0;
142
237
  return join(isAbsolute(rel) ? rel : join(cwd, rel), "hooks");
143
238
  }
144
- function installHookFile(dir, name, script, marker) {
239
+ function installHookFile(dir, name, script2, marker) {
145
240
  const path = join(dir, name);
146
241
  let existing;
147
242
  try {
@@ -151,7 +246,7 @@ function installHookFile(dir, name, script, marker) {
151
246
  }
152
247
  if (existing !== void 0) {
153
248
  if (!isOwnRecognitionLine(existing, marker)) return "skipped-foreign";
154
- if (existing === script) {
249
+ if (existing === script2) {
155
250
  try {
156
251
  chmodSync(path, 493);
157
252
  } catch {
@@ -160,7 +255,7 @@ function installHookFile(dir, name, script, marker) {
160
255
  }
161
256
  }
162
257
  mkdirSync(dir, { recursive: true });
163
- writeFileSync(path, script);
258
+ writeFileSync(path, script2);
164
259
  chmodSync(path, 493);
165
260
  return existing === void 0 ? "installed" : "updated";
166
261
  }
@@ -237,15 +332,29 @@ function expandHooksPathTilde(hooksPath) {
237
332
  }
238
333
  function ensurePreCommitGuardHook(repoRoot, gitCommonDir) {
239
334
  const hooksPath = git(repoRoot, ["config", "core.hooksPath"]) ?? "";
240
- if (hooksPath !== "") {
335
+ if (hooksPath !== "" && !isRunnerHookWrapper(hooksPath, repoRoot)) {
241
336
  const expanded = expandHooksPathTilde(hooksPath);
242
337
  const effectivePath = isAbsolute(expanded) ? join(expanded, "pre-commit") : join(repoRoot, expanded, "pre-commit");
243
338
  return { status: "skipped-custom-hooks-path", adopted: guardAdoptedAtEffectiveHook(effectivePath) };
244
339
  }
340
+ return installPreCommitGuardInGitDir(gitCommonDir);
341
+ }
342
+ function installPreCommitGuardInGitDir(gitCommonDir) {
245
343
  const path = join(gitCommonDir, "hooks", "pre-commit");
246
344
  const status = installHookFile(join(gitCommonDir, "hooks"), "pre-commit", PRE_COMMIT_GUARD_SCRIPT, PRE_COMMIT_GUARD_MARKER);
247
345
  return { status, adopted: status === "skipped-foreign" ? guardBodyPresent(path) : true };
248
346
  }
347
+ function isRunnerHookWrapper(hooksPath, repoRoot) {
348
+ const dir = resolve(repoRoot, expandHooksPathTilde(hooksPath));
349
+ if (basename(dir) !== HOOKS_DIR_NAME) return false;
350
+ try {
351
+ const script2 = readFileSync(join(dir, "pre-commit"), "utf8");
352
+ const secondLine = script2.indexOf("\n") + 1;
353
+ return secondLine > 0 && script2.startsWith(HOOK_WRAPPER_MARKER, secondLine);
354
+ } catch {
355
+ return false;
356
+ }
357
+ }
249
358
  function deadHooksState(cwd) {
250
359
  const inRepo = git(cwd, ["rev-parse", "--is-inside-work-tree"]);
251
360
  if (inRepo !== "true") return { dead: false };
@@ -282,11 +391,11 @@ function preparePackageManagerCommand(repoRoot) {
282
391
  function huskyPrepareScript(repoRoot) {
283
392
  try {
284
393
  const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"));
285
- const script = pkg.scripts?.prepare;
286
- if (typeof script === "string" && /husky/i.test(script)) return script;
394
+ const script2 = pkg.scripts?.prepare;
395
+ if (typeof script2 === "string" && /husky/i.test(script2)) return script2;
287
396
  } catch {
288
397
  }
289
398
  return null;
290
399
  }
291
400
 
292
- export { contextExcludesPresent, deadHooksState, ensureContextExcludes, ensurePreCommitGuardHook, huskyPrepareScript, installPreCommitGuardHook, managedHookScripts, preparePackageManagerCommand, removeJournalHooks, removePreCommitGuardHook };
401
+ export { HOOKS_DIR_NAME, contextExcludesPresent, deadHooksState, ensureContextExcludes, ensurePreCommitGuardHook, huskyPrepareScript, installPreCommitGuardHook, installPreCommitGuardInGitDir, managedHookScripts, preparePackageManagerCommand, removeJournalHooks, removePreCommitGuardHook, writeCommitHooks };