@outerlayer/cli 0.1.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +238 -77
  3. package/dist/agent-setup-IFH3FB5X.js +197 -0
  4. package/dist/build-TZUUTRGH.js +9 -0
  5. package/dist/build-info.json +1 -1
  6. package/dist/check-WQP2CZDW.js +94 -0
  7. package/dist/chunk-2E54Z3P5.js +232 -0
  8. package/dist/chunk-3TT7RQYB.js +55 -0
  9. package/dist/chunk-3YPKXE5L.js +1683 -0
  10. package/dist/{chunk-HNCCD2IX.js → chunk-5DLBAXY5.js} +209 -61
  11. package/dist/{chunk-R5KBGVII.js → chunk-5QRP3MVS.js} +30 -14
  12. package/dist/chunk-5Y3QQRUH.js +300 -0
  13. package/dist/chunk-6Y63SXFO.js +383 -0
  14. package/dist/chunk-774HEQL6.js +178 -0
  15. package/dist/chunk-7A6RVXPZ.js +2233 -0
  16. package/dist/chunk-7YIOOFVJ.js +71 -0
  17. package/dist/chunk-ABIWDUMS.js +104 -0
  18. package/dist/chunk-AUFG23AA.js +534 -0
  19. package/dist/chunk-B2G7JHHB.js +42 -0
  20. package/dist/chunk-BCJNHMZT.js +34 -0
  21. package/dist/chunk-BCLJSQCV.js +56 -0
  22. package/dist/chunk-BFESLKPP.js +61 -0
  23. package/dist/chunk-BJ3KTMDH.js +63 -0
  24. package/dist/chunk-BKO6JEZI.js +47 -0
  25. package/dist/chunk-BOLTI6LR.js +37 -0
  26. package/dist/chunk-CBPPJSAR.js +89 -0
  27. package/dist/chunk-CBZB6XY6.js +102 -0
  28. package/dist/{chunk-RCQXYLMO.js → chunk-DVDEBNQJ.js} +18 -18
  29. package/dist/{work-pr-cmd-GOYSPEN2.js → chunk-DVYHCMB2.js} +10 -25
  30. package/dist/{chunk-TFUIDMOB.js → chunk-EABW6AJQ.js} +12 -1
  31. package/dist/{chunk-KFYJV2ZG.js → chunk-EMY4I27X.js} +1 -1
  32. package/dist/{chunk-NTTPJV35.js → chunk-F47JBIAW.js} +3 -1
  33. package/dist/chunk-F6GFZXZ3.js +60 -0
  34. package/dist/{chunk-YOOSBOKS.js → chunk-F7CU5ABH.js} +1 -1
  35. package/dist/chunk-FAJSS2WN.js +1357 -0
  36. package/dist/chunk-FS2VYGZT.js +1764 -0
  37. package/dist/chunk-FV2CCGXK.js +9 -0
  38. package/dist/{chunk-BQB3U2VD.js → chunk-G7FSV7JX.js} +5113 -3729
  39. package/dist/{emit-cmd-TWSYYEZI.js → chunk-H5NCNVDA.js} +68 -17
  40. package/dist/chunk-HE2D4EY5.js +83 -0
  41. package/dist/chunk-L2DBRKRA.js +94 -0
  42. package/dist/chunk-LF6MLJCY.js +38 -0
  43. package/dist/chunk-M5POMKKW.js +1051 -0
  44. package/dist/{mcp-install-cmd-FDQH6SEN.js → chunk-MQ3IPHIZ.js} +35 -18
  45. package/dist/{chunk-D77LS3UI.js → chunk-N5FOE5PS.js} +33 -4
  46. package/dist/chunk-N6LURUEF.js +64 -0
  47. package/dist/chunk-NYC54VBD.js +1084 -0
  48. package/dist/chunk-OGFGZQA3.js +23 -0
  49. package/dist/chunk-QOZKPLVJ.js +226 -0
  50. package/dist/chunk-RA3O54FC.js +30 -0
  51. package/dist/chunk-TBT347UY.js +41 -0
  52. package/dist/{chunk-3VQ4XXQA.js → chunk-UFFXNXLQ.js} +115 -71
  53. package/dist/chunk-USO2DKBD.js +421 -0
  54. package/dist/chunk-W4SQCOZU.js +206 -0
  55. package/dist/{chunk-A3WLZX2F.js → chunk-WIOZAJ2W.js} +15 -2
  56. package/dist/chunk-WO2BXCTQ.js +83 -0
  57. package/dist/chunk-WZBHBH3J.js +20 -0
  58. package/dist/{chunk-IWIUYLDR.js → chunk-XCCLFVXM.js} +118 -189
  59. package/dist/{context-materialize-J6CRQ7O7.js → chunk-XDDW4FRS.js} +134 -26
  60. package/dist/chunk-YYYXRJUW.js +101 -0
  61. package/dist/chunk-ZM2IMMYN.js +76 -0
  62. package/dist/chunk-ZMYPLFG3.js +71 -0
  63. package/dist/chunk-ZNA27WEV.js +470 -0
  64. package/dist/{cli-EFJIG2VT.js → cli-NHUXABYH.js} +597 -965
  65. package/dist/{paths-D2VGWWFI.js → cli-build-K56DK4DS.js} +1 -1
  66. package/dist/config-XVJYZ4IQ.js +9 -0
  67. package/dist/connect-cmd-OOG4TPU7.js +16 -0
  68. package/dist/context-adopt-UMA4O5NY.js +105 -0
  69. package/dist/context-materialize-G2FUFC72.js +19 -0
  70. package/dist/dist-RW4UPPC2.js +6 -0
  71. package/dist/docker-NEGDLU6D.js +7 -0
  72. package/dist/doctor-53F2PYY7.js +43 -0
  73. package/dist/doctor-UFVL4PZY.js +426 -0
  74. package/dist/{emit-artifact-cmd-KPPD3YLO.js → emit-artifact-cmd-KTYW2TN2.js} +25 -19
  75. package/dist/emit-cmd-DCERAU27.js +9 -0
  76. package/dist/emit-criteria-cmd-VAUUSAYD.js +166 -0
  77. package/dist/{emit-finding-cmd-VR5VEGXJ.js → emit-finding-cmd-UR4ID45Z.js} +35 -30
  78. package/dist/{emit-result-cmd-YOA5BJKL.js → emit-result-cmd-FDTUKGKE.js} +35 -61
  79. package/dist/exec-client-4XGXV3XN.js +8 -0
  80. package/dist/guest-init-GENRHNP7.js +8 -0
  81. package/dist/hook-fast-EYENPK6F.js +9 -0
  82. package/dist/{hook-wrap-fast-KXYNX3AD.js → hook-wrap-fast-NPLHJXFK.js} +2 -2
  83. package/dist/host-key-KDYZ5ECC.js +7 -0
  84. package/dist/import-capture-cmd-AVZ4VIVJ.js +68 -0
  85. package/dist/{import-ruler-cmd-7LH2QOMG.js → import-ruler-cmd-GUHL6IY5.js} +1 -1
  86. package/dist/index.js +4 -4
  87. package/dist/init-DXBCOAJK.js +36 -0
  88. package/dist/init-YEXK35CF.js +221 -0
  89. package/dist/init-cmd-NHR7PRSQ.js +135 -0
  90. package/dist/install-cmd-7WXOIFDO.js +55 -0
  91. package/dist/lima-XN65D7GN.js +55 -0
  92. package/dist/login-browser-7XGSV7MA.js +10 -0
  93. package/dist/{config-POF7DEQW.js → logout-cmd-QAUFDJ6J.js} +3 -1
  94. package/dist/{logs-TRNPQM42.js → logs-7RYUH5A6.js} +1 -1
  95. package/dist/loop-VZOLL4WQ.js +43 -0
  96. package/dist/machine-WSG52J75.js +35 -0
  97. package/dist/mcp-install-cmd-RLK4SO3N.js +9 -0
  98. package/dist/{mcp-serve-cmd-57EZZOTL.js → mcp-serve-cmd-JPTP5FMS.js} +20 -9
  99. package/dist/paths-OYKMVYJP.js +6 -0
  100. package/dist/{pidfile-PTW76F56.js → pidfile-UZRH774M.js} +2 -3
  101. package/dist/real-deps-SU24ZA2K.js +21 -0
  102. package/dist/relay-L76HDX72.js +46 -0
  103. package/dist/settings-J2652U5N.js +7 -0
  104. package/dist/starter-pack-LIZYMKYQ.js +10 -0
  105. package/dist/{status-JHVSZYC5.js → status-2UZKKNB7.js} +31 -12
  106. package/dist/{statusline-fast-3C5OXHDD.js → statusline-fast-SVTMBMD7.js} +3 -2
  107. package/dist/sync-cmd-VPPYCNNM.js +28 -0
  108. package/dist/version-BWM6VLDI.js +6 -0
  109. package/dist/{watch-NRLXJUST.js → watch-5C4ZBBGO.js} +20 -7
  110. package/dist/{work-claim-cmd-HXB7K27H.js → work-claim-cmd-LSO2B2EX.js} +34 -19
  111. package/dist/work-cmd-UOQO2AD4.js +17 -0
  112. package/dist/work-comment-cmd-WF3EOLAE.js +106 -0
  113. package/dist/{work-launch-YNNCKIH3.js → work-launch-YI4CJDAP.js} +2 -1
  114. package/dist/work-open-pr-cmd-4SSQZMYG.js +156 -0
  115. package/dist/work-pr-cmd-6AN6RRQJ.js +16 -0
  116. package/package.json +13 -3
  117. package/skill-pack/maintained/amend/SKILL.md +104 -0
  118. package/skill-pack/maintained/emitting-evidence/SKILL.md +99 -0
  119. package/skill-pack/maintained/emitting-evidence/references/agents-snippet.md +20 -0
  120. package/skill-pack/maintained/outerlayer/SKILL.md +55 -0
  121. package/skill-pack/maintained/reporting-findings/SKILL.md +148 -0
  122. package/skill-pack/template/build/SKILL.md +193 -0
  123. package/skill-pack/template/build/references/agent-briefs.md +243 -0
  124. package/skill-pack/template/build/references/criteria-judge.md +91 -0
  125. package/skill-pack/template/build/references/evidence.md +42 -0
  126. package/skill-pack/template/build/references/release.md +74 -0
  127. package/skill-pack/template/build/references/review-briefs.md +275 -0
  128. package/skill-pack/template/build/references/review-loop.md +158 -0
  129. package/skill-pack/template/build/scripts/record-criteria.mjs +235 -0
  130. package/skill-pack/template/spec/SKILL.md +84 -0
  131. package/skill-pack/template/writing-specs/SKILL.md +134 -0
  132. package/dist/chunk-JJP7YLMN.js +0 -25
  133. package/dist/chunk-KKEU6FFR.js +0 -589
  134. package/dist/chunk-OZ7C3XUE.js +0 -34
  135. package/dist/chunk-WQ6VGRGZ.js +0 -150
  136. package/dist/emit-commit-credit-cmd-656BRAQ4.js +0 -121
  137. package/dist/hook-fast-I2XTHDE6.js +0 -8
  138. package/dist/import-capture-cmd-EUMBGIV3.js +0 -176
  139. package/dist/init-PTBITAUO.js +0 -103
  140. package/dist/login-cmd-IRX6LZT7.js +0 -62
  141. package/dist/loop-BWE2BS6J.js +0 -646
  142. package/dist/sync-cmd-S7Q6DWU3.js +0 -15
  143. package/dist/work-cmd-ABP6XDBP.js +0 -16
@@ -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 };
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+
4
+ createRequire(import.meta.url);
5
+
6
+ // src/tracker-number.ts
7
+ var MAX_TRACKER_NUMBER = Number.MAX_SAFE_INTEGER;
8
+ function parseTrackerNumber(raw) {
9
+ if (raw === void 0) return void 0;
10
+ const text = typeof raw === "number" ? String(raw) : raw;
11
+ if (!/^\d+$/.test(text)) return void 0;
12
+ const n = Number(text);
13
+ if (!Number.isSafeInteger(n) || n <= 0) return void 0;
14
+ return n;
15
+ }
16
+ function trackerNumberError(flag, raw) {
17
+ return `invalid ${flag} "${raw}" \u2014 expected a positive integer no larger than ${MAX_TRACKER_NUMBER}`;
18
+ }
19
+
20
+ export { parseTrackerNumber, trackerNumberError };
@@ -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 { existsSync, readFileSync, unlinkSync, readdirSync, mkdirSync, writeFileSync, 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,138 +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 POST_REWRITE_MARKER = "# outerlayer post-rewrite journal";
78
- var POST_REWRITE_VERSION = `${POST_REWRITE_MARKER} v1`;
79
- var POST_REWRITE_HOOK_SCRIPT = `#!/bin/sh
80
- ${POST_REWRITE_VERSION}
81
- # Appends exact old->new commit pairs (amend/rebase, incl. squashes) to the
82
- # OuterLayer spool so rewritten history stays creditable to the recorded
83
- # sessions that produced it. Always exits 0: journaling must never break a
84
- # rebase.
85
- # On a machine without OuterLayer (this hook can be committed into a repo
86
- # and run for every contributor) there is nothing to journal and no reason
87
- # to create ~/.outerlayer: bow out before touching the filesystem.
88
- [ -n "$OUTERLAYER_SPOOL_DIR" ] || [ -d "$HOME/.outerlayer" ] || exit 0
89
- SPOOL_DIR="\${OUTERLAYER_SPOOL_DIR:-$HOME/.outerlayer/spool}"
90
- mkdir -p "$SPOOL_DIR" 2>/dev/null || exit 0
91
- FILE="$SPOOL_DIR/rewrites.jsonl"
92
- if [ -f "$FILE" ] && [ "$(wc -c < "$FILE" 2>/dev/null || echo 0)" -gt 5242880 ]; then
93
- mv -f "$FILE" "$FILE.1" 2>/dev/null || true
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"]);
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 $?
94
109
  fi
95
- REPO="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)"
96
- if [ -z "$REPO" ]; then
97
- REPO="$(git rev-parse --git-common-dir 2>/dev/null)" || exit 0
98
- case "$REPO" in /*) ;; *) REPO="$PWD/$REPO" ;; esac
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
99
117
  fi
100
- TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
101
- awk -v repo="$REPO" -v ts="$TS" -v src="$1" '
102
- NF >= 2 && $1 ~ /^[0-9a-f]+$/ && $2 ~ /^[0-9a-f]+$/ {
103
- pairs = pairs (n ? "," : "") "[\\"" $1 "\\",\\"" $2 "\\"]"; n++
104
- }
105
- END {
106
- if (n) {
107
- gsub(/\\\\/, "\\\\\\\\", repo); gsub(/"/, "\\\\\\"", repo)
108
- printf "{\\"t\\":\\"%s\\",\\"repo\\":\\"%s\\",\\"source\\":\\"%s\\",\\"pairs\\":[%s]}\\n", ts, repo, src, pairs
109
- }
110
- }
111
- ' >> "$FILE" 2>/dev/null || true
112
- exit 0
113
118
  `;
114
- var POST_COMMIT_HOOK_SCRIPT = `#!/bin/sh
115
- ${POST_REWRITE_VERSION}
116
- # Appends every commit's SHA to the OuterLayer spool the moment it exists,
117
- # so a session whose transcript lost git's porcelain line (piped or muted
118
- # output) can still be credited at sync time. Always exits 0.
119
- # On a machine without OuterLayer (this hook can be committed into a repo
120
- # and run for every contributor) there is nothing to journal and no reason
121
- # to create ~/.outerlayer: bow out before touching the filesystem.
122
- [ -n "$OUTERLAYER_SPOOL_DIR" ] || [ -d "$HOME/.outerlayer" ] || exit 0
123
- SPOOL_DIR="\${OUTERLAYER_SPOOL_DIR:-$HOME/.outerlayer/spool}"
124
- mkdir -p "$SPOOL_DIR" 2>/dev/null || exit 0
125
- FILE="$SPOOL_DIR/commits.jsonl"
126
- if [ -f "$FILE" ] && [ "$(wc -c < "$FILE" 2>/dev/null || echo 0)" -gt 5242880 ]; then
127
- mv -f "$FILE" "$FILE.1" 2>/dev/null || true
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
128
145
  fi
129
- REPO="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)"
130
- if [ -z "$REPO" ]; then
131
- REPO="$(git rev-parse --git-common-dir 2>/dev/null)" || exit 0
132
- case "$REPO" in /*) ;; *) REPO="$PWD/$REPO" ;; esac
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" "$@"`}
133
153
  fi
134
- SHA="$(git rev-parse HEAD 2>/dev/null)" || exit 0
135
- TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
136
- case "$SHA" in
137
- *[!0-9a-f]*) exit 0 ;;
138
- esac
139
- REPO_ESCAPED=$(printf '%s' "$REPO" | sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g')
140
- printf '{"t":"%s","repo":"%s","sha":"%s"}
141
- ' "$TS" "$REPO_ESCAPED" "$SHA" >> "$FILE" 2>/dev/null || true
142
- exit 0
154
+ ${adds ? `[ "$status" -eq 0 ] || exit "$status"
155
+ ${ADD_TRAILER}` : ""}exit 0
143
156
  `;
144
- var PREPARE_COMMIT_MSG_HOOK_SCRIPT = `#!/bin/sh
145
- ${POST_REWRITE_VERSION}
146
- # Appends "OuterLayer-Session: <id>" to commits made during a recorded agent
147
- # session, so the commit itself carries the session identity through squash
148
- # and cross-machine rebases (the server-side trailer tier parses it). Stamps
149
- # NOTHING outside a session: the agent env marker is required, and the id
150
- # comes from the shell's own session variable \u2014 exact, because the commit
151
- # process is a child of the session that issued it \u2014 with the spool journal
152
- # as the fallback for harnesses that don't export one. Always exits 0.
153
- # The journal-fallback selection rules mirror detectActiveSession in
154
- # packages/cli/src/session-detect.ts \u2014 keep them in step.
155
- [ -n "$OUTERLAYER_NO_TRAILER" ] && exit 0
156
- [ -n "$CLAUDECODE" ] || exit 0
157
- MSG_FILE="$1"
158
- [ -f "$MSG_FILE" ] || exit 0
159
- grep -qi '^outerlayer-session:' "$MSG_FILE" 2>/dev/null && exit 0
160
- SID="$CLAUDE_CODE_SESSION_ID"
161
- case "$SID" in
162
- *[!0-9a-fA-F-]*) SID="" ;;
163
- esac
164
- if [ -n "$SID" ]; then
165
- printf '\\nOuterLayer-Session: %s\\n' "$SID" >> "$MSG_FILE" 2>/dev/null || true
166
- exit 0
167
- fi
168
- SPOOL_DIR="\${OUTERLAYER_SPOOL_DIR:-$HOME/.outerlayer/spool}"
169
- EVENTS="$SPOOL_DIR/events.jsonl"
170
- [ -f "$EVENTS" ] || exit 0
171
- TOP="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
172
- [ -n "$TOP" ] || exit 0
173
- TOP_REAL="$(cd "$TOP" 2>/dev/null && pwd -P)" || TOP_REAL="$TOP"
174
- CUTOFF="$(date -u -v-24H +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo 1970-01-01T00:00:00Z)"
175
- SID="$(tail -c 65536 "$EVENTS" 2>/dev/null | awk -v top="$TOP" -v topr="$TOP_REAL" -v cutoff="$CUTOFF" '
176
- function field(line, key, re, v) {
177
- re = "\\"" key "\\":\\"[^\\"]*\\""
178
- if (match(line, re) == 0) return ""
179
- v = substr(line, RSTART, RLENGTH)
180
- sub("^\\"" key "\\":\\"", "", v); sub("\\"$", "", v)
181
- return v
182
- }
183
- # macOS spells one directory two ways (/var is a symlink to /private/var);
184
- # git reports the resolved form while the session recorded the launch
185
- # spelling, so both sides normalize before comparing.
186
- function norm(p) { sub(/^\\/private\\//, "/", p); return p }
187
- function cwdMatches(c, cn, tn, rn) {
188
- cn = norm(c); tn = norm(top); rn = norm(topr)
189
- return cn == tn || cn == rn || index(cn, tn "/") == 1 || index(cn, rn "/") == 1
190
- }
191
- /^\\{/ {
192
- sid = field($0, "sessionId"); if (sid == "") next
193
- ev = field($0, "event")
194
- if (ev == "SessionEnd") { ended[sid] = 1; next }
195
- t = field($0, "t"); c = field($0, "cwd")
196
- if (t >= cutoff && cwdMatches(c)) { lastAt[sid] = NR }
197
- }
198
- END {
199
- best = ""; bestNr = 0
200
- for (sid in lastAt) if (!ended[sid] && lastAt[sid] > bestNr) { best = sid; bestNr = lastAt[sid] }
201
- if (best != "" && best ~ /^[0-9a-fA-F-]+$/) print best
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);
202
166
  }
203
- ')"
204
- [ -n "$SID" ] || exit 0
205
- printf '\\nOuterLayer-Session: %s\\n' "$SID" >> "$MSG_FILE" 2>/dev/null || true
206
- exit 0
207
- `;
208
- var PRE_COMMIT_GUARD_MARKER = "# outerlayer materialized-context commit guard";
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";
209
174
  var PRE_COMMIT_GUARD_VERSION = `${PRE_COMMIT_GUARD_MARKER} v1`;
210
175
  var PRE_COMMIT_GUARD_BODY_SIGNATURE = "outerlayer: refusing to commit materialized control-plane context";
211
176
  var PRE_COMMIT_GUARD_SCRIPT = `#!/bin/sh
@@ -242,15 +207,10 @@ echo " $REMEDY" >&2
242
207
  exit 1
243
208
  `;
244
209
  function managedHookScripts() {
245
- return [
246
- ["post-rewrite", printableHookScript(POST_REWRITE_HOOK_SCRIPT)],
247
- ["post-commit", printableHookScript(POST_COMMIT_HOOK_SCRIPT)],
248
- ["prepare-commit-msg", printableHookScript(PREPARE_COMMIT_MSG_HOOK_SCRIPT)],
249
- ["pre-commit", printableHookScript(PRE_COMMIT_GUARD_SCRIPT)]
250
- ];
210
+ return [["pre-commit", printableHookScript(PRE_COMMIT_GUARD_SCRIPT)]];
251
211
  }
252
- function printableHookScript(script) {
253
- const lines = script.split("\n");
212
+ function printableHookScript(script2) {
213
+ const lines = script2.split("\n");
254
214
  return [lines[0], ...lines.slice(2)].join("\n");
255
215
  }
256
216
  function git(cwd, args) {
@@ -276,7 +236,7 @@ function commonHooksDir(cwd) {
276
236
  if (rel === void 0) return void 0;
277
237
  return join(isAbsolute(rel) ? rel : join(cwd, rel), "hooks");
278
238
  }
279
- function installHookFile(dir, name, script, marker) {
239
+ function installHookFile(dir, name, script2, marker) {
280
240
  const path = join(dir, name);
281
241
  let existing;
282
242
  try {
@@ -286,7 +246,7 @@ function installHookFile(dir, name, script, marker) {
286
246
  }
287
247
  if (existing !== void 0) {
288
248
  if (!isOwnRecognitionLine(existing, marker)) return "skipped-foreign";
289
- if (existing === script) {
249
+ if (existing === script2) {
290
250
  try {
291
251
  chmodSync(path, 493);
292
252
  } catch {
@@ -295,36 +255,10 @@ function installHookFile(dir, name, script, marker) {
295
255
  }
296
256
  }
297
257
  mkdirSync(dir, { recursive: true });
298
- writeFileSync(path, script);
258
+ writeFileSync(path, script2);
299
259
  chmodSync(path, 493);
300
260
  return existing === void 0 ? "installed" : "updated";
301
261
  }
302
- function installPostRewriteHook(cwd, opts) {
303
- const stampTrailers = opts?.stampTrailers ?? true;
304
- const inRepo = git(cwd, ["rev-parse", "--is-inside-work-tree"]);
305
- if (inRepo !== "true") return { status: "not-a-repo" };
306
- if ((git(cwd, ["config", "core.hooksPath"]) ?? "") !== "") {
307
- return { status: "skipped-custom-hooks-path" };
308
- }
309
- const dir = hooksDir(cwd);
310
- if (dir === void 0) return { status: "not-a-repo" };
311
- const pairOutcomes = [
312
- installHookFile(dir, "post-rewrite", POST_REWRITE_HOOK_SCRIPT, POST_REWRITE_MARKER),
313
- installHookFile(dir, "post-commit", POST_COMMIT_HOOK_SCRIPT, POST_REWRITE_MARKER)
314
- ];
315
- const outcomes = [...pairOutcomes];
316
- if (stampTrailers) {
317
- outcomes.push(installHookFile(dir, "prepare-commit-msg", PREPARE_COMMIT_MSG_HOOK_SCRIPT, POST_REWRITE_MARKER));
318
- } else {
319
- removeOurHookFile(join(dir, "prepare-commit-msg"), POST_REWRITE_MARKER);
320
- }
321
- const path = join(dir, "post-rewrite");
322
- if (pairOutcomes.every((o) => o === "skipped-foreign")) return { status: "skipped-foreign", path };
323
- if (outcomes.every((o) => o === "unchanged")) return { status: "unchanged", path };
324
- if (outcomes.includes("updated")) return { status: "updated", path };
325
- if (outcomes.includes("installed")) return { status: "installed", path };
326
- return { status: "skipped-foreign", path };
327
- }
328
262
  function removeOurHookFile(path, marker) {
329
263
  if (!existsSync(path)) return false;
330
264
  let content;
@@ -341,33 +275,14 @@ function isOwnRecognitionLine(content, marker) {
341
275
  const secondLine = content.split("\n", 2)[1];
342
276
  return secondLine !== void 0 && secondLine.startsWith(marker);
343
277
  }
344
- function removePostRewriteHook(cwd) {
278
+ function removeJournalHooks(cwd) {
345
279
  const dir = hooksDir(cwd);
346
280
  if (dir === void 0) return { removed: false };
347
281
  let removed = false;
348
282
  for (const name of ["post-rewrite", "post-commit", "prepare-commit-msg"]) {
349
- if (removeOurHookFile(join(dir, name), POST_REWRITE_MARKER)) removed = true;
283
+ if (removeOurHookFile(join(dir, name), JOURNAL_HOOK_MARKER)) removed = true;
350
284
  }
351
- return { removed, path: join(dir, "post-rewrite") };
352
- }
353
- function postRewriteHookPresence(cwd) {
354
- const inRepo = git(cwd, ["rev-parse", "--is-inside-work-tree"]);
355
- if (inRepo !== "true") return "not-a-repo";
356
- if ((git(cwd, ["config", "core.hooksPath"]) ?? "") !== "") return "custom-hooks-path";
357
- const dir = hooksDir(cwd);
358
- if (dir === void 0) return "not-a-repo";
359
- const states = ["post-rewrite", "post-commit"].map((name) => {
360
- let content;
361
- try {
362
- content = readFileSync(join(dir, name), "utf8");
363
- } catch {
364
- return "absent";
365
- }
366
- return content.includes(POST_REWRITE_MARKER) ? "ours" : "foreign";
367
- });
368
- if (states.every((st) => st === "ours")) return "ours";
369
- if (states.includes("foreign")) return "foreign";
370
- return "absent";
285
+ return { removed, path: dir };
371
286
  }
372
287
  function installPreCommitGuardHook(cwd) {
373
288
  const inRepo = git(cwd, ["rev-parse", "--is-inside-work-tree"]);
@@ -417,15 +332,29 @@ function expandHooksPathTilde(hooksPath) {
417
332
  }
418
333
  function ensurePreCommitGuardHook(repoRoot, gitCommonDir) {
419
334
  const hooksPath = git(repoRoot, ["config", "core.hooksPath"]) ?? "";
420
- if (hooksPath !== "") {
335
+ if (hooksPath !== "" && !isRunnerHookWrapper(hooksPath, repoRoot)) {
421
336
  const expanded = expandHooksPathTilde(hooksPath);
422
337
  const effectivePath = isAbsolute(expanded) ? join(expanded, "pre-commit") : join(repoRoot, expanded, "pre-commit");
423
338
  return { status: "skipped-custom-hooks-path", adopted: guardAdoptedAtEffectiveHook(effectivePath) };
424
339
  }
340
+ return installPreCommitGuardInGitDir(gitCommonDir);
341
+ }
342
+ function installPreCommitGuardInGitDir(gitCommonDir) {
425
343
  const path = join(gitCommonDir, "hooks", "pre-commit");
426
344
  const status = installHookFile(join(gitCommonDir, "hooks"), "pre-commit", PRE_COMMIT_GUARD_SCRIPT, PRE_COMMIT_GUARD_MARKER);
427
345
  return { status, adopted: status === "skipped-foreign" ? guardBodyPresent(path) : true };
428
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
+ }
429
358
  function deadHooksState(cwd) {
430
359
  const inRepo = git(cwd, ["rev-parse", "--is-inside-work-tree"]);
431
360
  if (inRepo !== "true") return { dead: false };
@@ -462,11 +391,11 @@ function preparePackageManagerCommand(repoRoot) {
462
391
  function huskyPrepareScript(repoRoot) {
463
392
  try {
464
393
  const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"));
465
- const script = pkg.scripts?.prepare;
466
- 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;
467
396
  } catch {
468
397
  }
469
398
  return null;
470
399
  }
471
400
 
472
- export { contextExcludesPresent, deadHooksState, ensureContextExcludes, ensurePreCommitGuardHook, huskyPrepareScript, installPostRewriteHook, installPreCommitGuardHook, managedHookScripts, postRewriteHookPresence, preparePackageManagerCommand, removePostRewriteHook, removePreCommitGuardHook };
401
+ export { HOOKS_DIR_NAME, contextExcludesPresent, deadHooksState, ensureContextExcludes, ensurePreCommitGuardHook, huskyPrepareScript, installPreCommitGuardHook, installPreCommitGuardInGitDir, managedHookScripts, preparePackageManagerCommand, removeJournalHooks, removePreCommitGuardHook, writeCommitHooks };