ai-ops-cli 1.0.2 → 1.1.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.
package/dist/bin/index.js CHANGED
@@ -866,14 +866,45 @@ var parseProjectLayerDocument = (path, rawContent) => {
866
866
  content
867
867
  };
868
868
  };
869
+ var parseMarkdownTableCells = (line) => {
870
+ const trimmed = line.trim();
871
+ if (!trimmed.startsWith("|") || !trimmed.endsWith("|")) {
872
+ return null;
873
+ }
874
+ return trimmed.slice(1, -1).split("|").map((cell) => cell.trim());
875
+ };
876
+ var isDocsStatusHeaderLine = (line) => {
877
+ const cells = parseMarkdownTableCells(line);
878
+ return cells !== null && cells.length === 3 && cells[0] === "path" && cells[1] === "status" && cells[2] === "owner";
879
+ };
880
+ var isMarkdownDividerCell = (cell) => /^:?-{3,}:?$/.test(cell);
881
+ var isDocsStatusDividerLine = (line) => {
882
+ const cells = parseMarkdownTableCells(line);
883
+ return cells !== null && cells.length === 3 && cells.every(isMarkdownDividerCell);
884
+ };
885
+ var findDocsStatusTableBounds = (lines) => {
886
+ const headerIndex = lines.findIndex(isDocsStatusHeaderLine);
887
+ const dividerIndex = headerIndex + 1;
888
+ if (headerIndex < 0 || !isDocsStatusDividerLine(lines[dividerIndex] ?? "")) {
889
+ return null;
890
+ }
891
+ let tableEndIndex = dividerIndex + 1;
892
+ while (tableEndIndex < lines.length && parseMarkdownTableCells(lines[tableEndIndex] ?? "") !== null) {
893
+ tableEndIndex += 1;
894
+ }
895
+ return { headerIndex, dividerIndex, tableEndIndex };
896
+ };
869
897
  var parseDocsStatusEntries = (content) => {
870
898
  const document = parseProjectLayerDocument("docs/docs-status.md", content);
871
- const rows = document.content.split("\n").filter((line) => line.trim().startsWith("|")).map((line) => line.trim());
899
+ const lines = document.content.split("\n");
900
+ const tableBounds = findDocsStatusTableBounds(lines);
901
+ if (tableBounds === null) {
902
+ return [];
903
+ }
904
+ const rows = lines.slice(tableBounds.dividerIndex + 1, tableBounds.tableEndIndex);
872
905
  return rows.flatMap((line) => {
873
- const cells = line.split("|").map((cell) => cell.trim()).filter((cell) => cell.length > 0);
874
- if (cells.length < 3) return [];
875
- if (cells[0] === "path") return [];
876
- if (cells[0].startsWith("---")) return [];
906
+ const cells = parseMarkdownTableCells(line);
907
+ if (cells === null || cells.length < 3) return [];
877
908
  return [
878
909
  {
879
910
  path: cells[0],
@@ -1050,16 +1081,11 @@ var buildDocsStatusRowsFromDisk = (params) => params.documentPaths.map((path) =>
1050
1081
  });
1051
1082
  var replaceDocsStatusRows = (content, rows) => {
1052
1083
  const lines = content.trimEnd().split("\n");
1053
- const headerIndex = lines.findIndex((line) => line.trim() === "| path | status | owner |");
1054
- const dividerIndex = headerIndex + 1;
1055
- if (headerIndex < 0 || !lines[dividerIndex]?.trim().startsWith("| ---")) {
1084
+ const tableBounds = findDocsStatusTableBounds(lines);
1085
+ if (tableBounds === null) {
1056
1086
  throw new Error("docs/docs-status.md table header not found");
1057
1087
  }
1058
- let tableEndIndex = dividerIndex + 1;
1059
- while (tableEndIndex < lines.length && lines[tableEndIndex]?.trim().startsWith("|")) {
1060
- tableEndIndex += 1;
1061
- }
1062
- return [...lines.slice(0, dividerIndex + 1), ...rows, ...lines.slice(tableEndIndex)].join("\n") + "\n";
1088
+ return [...lines.slice(0, tableBounds.dividerIndex + 1), ...rows, ...lines.slice(tableBounds.tableEndIndex)].join("\n") + "\n";
1063
1089
  };
1064
1090
  var updateDocsStatusTable = (basePath, documentPaths) => {
1065
1091
  const docsStatusPath = "docs/docs-status.md";
@@ -1793,11 +1819,714 @@ var diffProjectLayerPack = (params) => {
1793
1819
  // src/core/uninstall-plan.ts
1794
1820
  import { join as join12 } from "path";
1795
1821
 
1822
+ // src/core/context-promotion.ts
1823
+ import { createHash as createHash2 } from "crypto";
1824
+ import { execFileSync } from "child_process";
1825
+ import { existsSync as existsSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync8, statSync, writeFileSync as writeFileSync6 } from "fs";
1826
+ import { dirname as dirname8, join as join13, resolve as resolve7 } from "path";
1827
+ import { z as z12 } from "zod";
1828
+ var CONTEXT_PROMOTION_DECISION = {
1829
+ PROMOTED: "promoted",
1830
+ NO_PROMOTION: "no-promotion"
1831
+ };
1832
+ var CONTEXT_PROMOTION_SCOPE = {
1833
+ CORE: "core",
1834
+ PROJECT_LOCAL: "project-local",
1835
+ GLOBAL: "global"
1836
+ };
1837
+ var ContextPromotionDecisionSchema = z12.union([
1838
+ z12.literal(CONTEXT_PROMOTION_DECISION.PROMOTED),
1839
+ z12.literal(CONTEXT_PROMOTION_DECISION.NO_PROMOTION)
1840
+ ]);
1841
+ var ContextPromotionScopeSchema = z12.union([
1842
+ z12.literal(CONTEXT_PROMOTION_SCOPE.CORE),
1843
+ z12.literal(CONTEXT_PROMOTION_SCOPE.PROJECT_LOCAL),
1844
+ z12.literal(CONTEXT_PROMOTION_SCOPE.GLOBAL)
1845
+ ]);
1846
+ var ContextPromotionReceiptSchema = z12.object({
1847
+ fingerprint: z12.string().regex(/^[a-f0-9]{16}$/),
1848
+ commitHash: z12.string().regex(/^(NO_HEAD|[a-f0-9]{40})$/).optional(),
1849
+ decision: ContextPromotionDecisionSchema,
1850
+ scopes: z12.array(ContextPromotionScopeSchema),
1851
+ targets: z12.array(z12.string().min(1)),
1852
+ summary: z12.string().min(1),
1853
+ resolvedAt: z12.string().min(1)
1854
+ }).strict();
1855
+ var ContextPromotionReceiptIndexSchema = z12.object({
1856
+ schemaVersion: z12.literal(1),
1857
+ kind: z12.literal("context-promotion-receipts"),
1858
+ projectKey: z12.string().regex(/^[a-f0-9]{12}$/),
1859
+ projectRoot: z12.string().min(1),
1860
+ receipts: z12.array(ContextPromotionReceiptSchema)
1861
+ }).strict();
1862
+ var RECEIPT_INDEX_FILENAME = "receipts-index.json";
1863
+ var DEFAULT_PRUNE_MAX = 50;
1864
+ var hashHex = (parts, length) => createHash2("sha256").update(parts.join("\0")).digest("hex").slice(0, length);
1865
+ var buildContextPromotionProjectKey = (gitRoot) => hashHex([resolve7(gitRoot)], 12);
1866
+ var runGit = (cwd, args) => execFileSync("git", [...args], {
1867
+ cwd,
1868
+ encoding: "utf-8",
1869
+ maxBuffer: 20 * 1024 * 1024,
1870
+ stdio: ["ignore", "pipe", "ignore"]
1871
+ });
1872
+ var resolveContextPromotionGitRoot = (cwd) => {
1873
+ try {
1874
+ return runGit(cwd, ["rev-parse", "--show-toplevel"]).trim();
1875
+ } catch {
1876
+ return null;
1877
+ }
1878
+ };
1879
+ var readGitHead = (gitRoot) => {
1880
+ try {
1881
+ return runGit(gitRoot, ["rev-parse", "--verify", "HEAD"]).trim();
1882
+ } catch {
1883
+ return "NO_HEAD";
1884
+ }
1885
+ };
1886
+ var readUntrackedFingerprintParts = (gitRoot) => {
1887
+ const raw = runGit(gitRoot, ["ls-files", "--others", "--exclude-standard", "-z"]);
1888
+ const paths = raw.split("\0").filter((path) => path.length > 0).sort((a, b) => a.localeCompare(b));
1889
+ return paths.map((relativePath) => {
1890
+ const absolutePath = join13(gitRoot, relativePath);
1891
+ try {
1892
+ const stat = statSync(absolutePath);
1893
+ if (!stat.isFile()) {
1894
+ return `${relativePath}:non-file`;
1895
+ }
1896
+ const content = readFileSync8(absolutePath);
1897
+ return `${relativePath}:${createHash2("sha256").update(content).digest("hex")}`;
1898
+ } catch {
1899
+ throw new Error(`Unable to read untracked path for context promotion fingerprint: ${relativePath}`);
1900
+ }
1901
+ });
1902
+ };
1903
+ var readTrackedWorkingTreeFingerprintParts = (gitRoot) => {
1904
+ const rawDiff = runGit(gitRoot, ["diff", "--raw", "-z"]);
1905
+ const rawNames = runGit(gitRoot, ["diff", "--name-only", "-z"]);
1906
+ const paths = rawNames.split("\0").filter((path) => path.length > 0).sort((a, b) => a.localeCompare(b));
1907
+ return [
1908
+ `raw:${rawDiff}`,
1909
+ ...paths.map((relativePath) => {
1910
+ const absolutePath = join13(gitRoot, relativePath);
1911
+ if (!existsSync3(absolutePath)) {
1912
+ return `${relativePath}:deleted`;
1913
+ }
1914
+ const stat = statSync(absolutePath);
1915
+ if (!stat.isFile()) {
1916
+ return `${relativePath}:non-file`;
1917
+ }
1918
+ const content = readFileSync8(absolutePath);
1919
+ return `${relativePath}:${createHash2("sha256").update(content).digest("hex")}`;
1920
+ })
1921
+ ];
1922
+ };
1923
+ var readGitIndexFingerprintParts = (gitRoot) => [
1924
+ `index:${runGit(gitRoot, ["ls-files", "-s", "-z"])}`,
1925
+ `staged-raw:${runGit(gitRoot, ["diff", "--cached", "--raw", "-z"])}`
1926
+ ];
1927
+ var computeContextPromotionFingerprint = (gitRoot) => hashHex(
1928
+ [
1929
+ `head:${readGitHead(gitRoot)}`,
1930
+ ...readGitIndexFingerprintParts(gitRoot),
1931
+ ...readTrackedWorkingTreeFingerprintParts(gitRoot).map((part) => `tracked-working-tree:${part}`),
1932
+ ...readUntrackedFingerprintParts(gitRoot).map((part) => `untracked:${part}`)
1933
+ ],
1934
+ 16
1935
+ );
1936
+ var resolveContextPromotionReceiptIndexPath = (params) => join13(
1937
+ params.userBasePath,
1938
+ ".ai-ops",
1939
+ "context-promotion",
1940
+ "projects",
1941
+ params.projectKey,
1942
+ RECEIPT_INDEX_FILENAME
1943
+ );
1944
+ var parseContextPromotionReceiptIndex = (json) => ContextPromotionReceiptIndexSchema.parse(JSON.parse(json));
1945
+ var serializeContextPromotionReceiptIndex = (index) => JSON.stringify(index, null, 2) + "\n";
1946
+ var readContextPromotionReceiptIndex = (indexPath) => {
1947
+ try {
1948
+ return parseContextPromotionReceiptIndex(readFileSync8(indexPath, "utf-8"));
1949
+ } catch {
1950
+ return null;
1951
+ }
1952
+ };
1953
+ var writeContextPromotionReceiptIndex = (indexPath, index) => {
1954
+ mkdirSync6(dirname8(indexPath), { recursive: true });
1955
+ writeFileSync6(indexPath, serializeContextPromotionReceiptIndex(index), "utf-8");
1956
+ };
1957
+ var buildEmptyReceiptIndex = (params) => ({
1958
+ schemaVersion: 1,
1959
+ kind: "context-promotion-receipts",
1960
+ projectKey: params.projectKey,
1961
+ projectRoot: params.projectRoot,
1962
+ receipts: []
1963
+ });
1964
+ var sortReceiptsByResolvedAtDesc = (receipts) => [...receipts].sort((a, b) => b.resolvedAt.localeCompare(a.resolvedAt));
1965
+ var findContextPromotionReceipt = (params) => {
1966
+ const receipts = params.index?.receipts ?? [];
1967
+ return receipts.find((receipt) => receipt.commitHash === params.commitHash) ?? receipts.find((receipt) => receipt.fingerprint === params.fingerprint) ?? null;
1968
+ };
1969
+ var upsertContextPromotionReceipt = (params) => {
1970
+ const previous = readContextPromotionReceiptIndex(params.indexPath);
1971
+ const index = previous?.projectKey === params.projectKey ? previous : buildEmptyReceiptIndex({ projectKey: params.projectKey, projectRoot: params.projectRoot });
1972
+ const remaining = index.receipts.filter((receipt) => {
1973
+ if (receipt.fingerprint === params.receipt.fingerprint) {
1974
+ return false;
1975
+ }
1976
+ if (params.receipt.commitHash && receipt.commitHash === params.receipt.commitHash) {
1977
+ return false;
1978
+ }
1979
+ return true;
1980
+ });
1981
+ const maxReceipts = params.maxReceipts ?? DEFAULT_PRUNE_MAX;
1982
+ const receipts = sortReceiptsByResolvedAtDesc([params.receipt, ...remaining]).slice(0, maxReceipts);
1983
+ const nextIndex = {
1984
+ ...index,
1985
+ projectRoot: params.projectRoot,
1986
+ receipts
1987
+ };
1988
+ writeContextPromotionReceiptIndex(params.indexPath, nextIndex);
1989
+ return nextIndex;
1990
+ };
1991
+ var pruneContextPromotionReceipts = (params) => {
1992
+ const index = readContextPromotionReceiptIndex(params.indexPath);
1993
+ if (!index) {
1994
+ return null;
1995
+ }
1996
+ const nextIndex = {
1997
+ ...index,
1998
+ receipts: sortReceiptsByResolvedAtDesc(index.receipts).slice(0, params.maxReceipts)
1999
+ };
2000
+ writeContextPromotionReceiptIndex(params.indexPath, nextIndex);
2001
+ return nextIndex;
2002
+ };
2003
+ var hasContextPromotionLayer = (gitRoot) => existsSync3(join13(gitRoot, PROJECT_LAYER_CONTEXT_INDEX_RELATIVE_PATH));
2004
+ var getContextPromotionStatus = (params) => {
2005
+ const cwd = resolve7(params.cwd);
2006
+ const gitRoot = resolveContextPromotionGitRoot(cwd);
2007
+ if (!gitRoot) {
2008
+ return {
2009
+ cwd,
2010
+ gitRoot: null,
2011
+ hasContextLayer: false,
2012
+ projectKey: null,
2013
+ commitHash: null,
2014
+ fingerprint: null,
2015
+ receipt: null,
2016
+ receiptIndexPath: null
2017
+ };
2018
+ }
2019
+ const hasContextLayer = hasContextPromotionLayer(gitRoot);
2020
+ const projectKey = buildContextPromotionProjectKey(gitRoot);
2021
+ const commitHash = readGitHead(gitRoot);
2022
+ const receiptIndexPath = resolveContextPromotionReceiptIndexPath({
2023
+ userBasePath: params.userBasePath,
2024
+ projectKey
2025
+ });
2026
+ if (!hasContextLayer) {
2027
+ return {
2028
+ cwd,
2029
+ gitRoot,
2030
+ hasContextLayer,
2031
+ projectKey,
2032
+ commitHash,
2033
+ fingerprint: null,
2034
+ receipt: null,
2035
+ receiptIndexPath
2036
+ };
2037
+ }
2038
+ const fingerprint = computeContextPromotionFingerprint(gitRoot);
2039
+ const index = readContextPromotionReceiptIndex(receiptIndexPath);
2040
+ return {
2041
+ cwd,
2042
+ gitRoot,
2043
+ hasContextLayer,
2044
+ projectKey,
2045
+ commitHash,
2046
+ fingerprint,
2047
+ receipt: findContextPromotionReceipt({ index, fingerprint, commitHash }),
2048
+ receiptIndexPath
2049
+ };
2050
+ };
2051
+ var buildContextPromotionReceipt = (params) => {
2052
+ const summary = params.input.summary.trim();
2053
+ if (summary.length === 0) {
2054
+ throw new Error("summary is required");
2055
+ }
2056
+ if (params.input.decision === CONTEXT_PROMOTION_DECISION.PROMOTED && params.input.scopes.length === 0) {
2057
+ throw new Error("at least one scope is required for promoted decisions");
2058
+ }
2059
+ return ContextPromotionReceiptSchema.parse({
2060
+ fingerprint: params.fingerprint,
2061
+ commitHash: params.commitHash,
2062
+ decision: params.input.decision,
2063
+ scopes: [...params.input.scopes],
2064
+ targets: [...params.input.targets],
2065
+ summary,
2066
+ resolvedAt: params.resolvedAt ?? (/* @__PURE__ */ new Date()).toISOString()
2067
+ });
2068
+ };
2069
+ var resolveContextPromotion = (params) => {
2070
+ const status = getContextPromotionStatus({ cwd: params.cwd, userBasePath: params.userBasePath });
2071
+ if (!status.gitRoot || !status.projectKey || !status.commitHash || !status.fingerprint || !status.receiptIndexPath) {
2072
+ throw new Error("git repository is required for context promotion receipts");
2073
+ }
2074
+ if (!status.hasContextLayer) {
2075
+ throw new Error("ai-ops context layer is required for context promotion receipts");
2076
+ }
2077
+ const receipt = buildContextPromotionReceipt({
2078
+ fingerprint: status.fingerprint,
2079
+ commitHash: status.commitHash,
2080
+ input: params.input
2081
+ });
2082
+ upsertContextPromotionReceipt({
2083
+ indexPath: status.receiptIndexPath,
2084
+ projectKey: status.projectKey,
2085
+ projectRoot: status.gitRoot,
2086
+ receipt
2087
+ });
2088
+ return getContextPromotionStatus({ cwd: params.cwd, userBasePath: params.userBasePath });
2089
+ };
2090
+ var HookToolInputSchema = z12.object({
2091
+ command: z12.string().optional()
2092
+ }).passthrough();
2093
+ var ToolUseHookInputSchema = z12.object({
2094
+ hook_event_name: z12.string(),
2095
+ cwd: z12.string(),
2096
+ tool_name: z12.string().optional(),
2097
+ tool_input: z12.unknown().optional(),
2098
+ tool_response: z12.unknown().optional()
2099
+ }).passthrough();
2100
+ var SHELL_CONTROL_TOKENS = /* @__PURE__ */ new Set(["&&", "||", ";", "|", "(", ")"]);
2101
+ var SHELL_SCRIPT_FLAGS = /* @__PURE__ */ new Set(["-c", "-lc"]);
2102
+ var GIT_GLOBAL_OPTIONS_WITH_VALUE = /* @__PURE__ */ new Set([
2103
+ "-C",
2104
+ "-c",
2105
+ "--git-dir",
2106
+ "--work-tree",
2107
+ "--namespace",
2108
+ "--config-env",
2109
+ "--exec-path"
2110
+ ]);
2111
+ var basename = (token) => token.replace(/\\/g, "/").split("/").at(-1) ?? token;
2112
+ var isAssignmentToken = (token) => /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);
2113
+ var tokenizeShellLike = (command) => {
2114
+ const tokens = [];
2115
+ let current = "";
2116
+ let quote = null;
2117
+ const pushCurrent = () => {
2118
+ if (current.length > 0) {
2119
+ tokens.push(current);
2120
+ current = "";
2121
+ }
2122
+ };
2123
+ for (let index = 0; index < command.length; index += 1) {
2124
+ const char = command[index];
2125
+ const nextChar = command[index + 1];
2126
+ if (quote) {
2127
+ if (char === quote) {
2128
+ quote = null;
2129
+ continue;
2130
+ }
2131
+ current += char;
2132
+ continue;
2133
+ }
2134
+ if (char === '"' || char === "'") {
2135
+ quote = char;
2136
+ continue;
2137
+ }
2138
+ if (/\s/.test(char)) {
2139
+ pushCurrent();
2140
+ continue;
2141
+ }
2142
+ if (char === "&" && nextChar === "&" || char === "|" && nextChar === "|") {
2143
+ pushCurrent();
2144
+ tokens.push(`${char}${nextChar}`);
2145
+ index += 1;
2146
+ continue;
2147
+ }
2148
+ if (char === ";" || char === "|" || char === "(" || char === ")") {
2149
+ pushCurrent();
2150
+ tokens.push(char);
2151
+ continue;
2152
+ }
2153
+ current += char;
2154
+ }
2155
+ pushCurrent();
2156
+ return tokens;
2157
+ };
2158
+ var splitCommandSegments = (tokens) => {
2159
+ const segments = [];
2160
+ let current = [];
2161
+ for (const token of tokens) {
2162
+ if (SHELL_CONTROL_TOKENS.has(token)) {
2163
+ if (current.length > 0) {
2164
+ segments.push(current);
2165
+ current = [];
2166
+ }
2167
+ continue;
2168
+ }
2169
+ current.push(token);
2170
+ }
2171
+ if (current.length > 0) {
2172
+ segments.push(current);
2173
+ }
2174
+ return segments;
2175
+ };
2176
+ var firstExecutableIndex = (segment) => {
2177
+ let index = 0;
2178
+ while (index < segment.length && isAssignmentToken(segment[index])) {
2179
+ index += 1;
2180
+ }
2181
+ if (segment[index] === "env") {
2182
+ index += 1;
2183
+ while (index < segment.length && isAssignmentToken(segment[index])) {
2184
+ index += 1;
2185
+ }
2186
+ }
2187
+ if (segment[index] === "command" || segment[index] === "sudo") {
2188
+ index += 1;
2189
+ }
2190
+ return index;
2191
+ };
2192
+ var segmentInvokesGitCommit = (segment) => {
2193
+ const executableIndex = firstExecutableIndex(segment);
2194
+ if (executableIndex >= segment.length || basename(segment[executableIndex]) !== "git") {
2195
+ return false;
2196
+ }
2197
+ for (let index = executableIndex + 1; index < segment.length; index += 1) {
2198
+ const token = segment[index];
2199
+ if (GIT_GLOBAL_OPTIONS_WITH_VALUE.has(token)) {
2200
+ index += 1;
2201
+ continue;
2202
+ }
2203
+ if (token.startsWith("-")) {
2204
+ continue;
2205
+ }
2206
+ return token === "commit";
2207
+ }
2208
+ return false;
2209
+ };
2210
+ var segmentInvokesShellScriptWithGitCommit = (segment) => {
2211
+ const executableIndex = firstExecutableIndex(segment);
2212
+ const executable = segment[executableIndex];
2213
+ if (!executable || !["bash", "sh", "zsh"].includes(basename(executable))) {
2214
+ return false;
2215
+ }
2216
+ for (let index = executableIndex + 1; index < segment.length - 1; index += 1) {
2217
+ if (SHELL_SCRIPT_FLAGS.has(segment[index]) && isGitCommitCommand(segment[index + 1])) {
2218
+ return true;
2219
+ }
2220
+ }
2221
+ return false;
2222
+ };
2223
+ var isGitCommitCommand = (command) => {
2224
+ const segments = splitCommandSegments(tokenizeShellLike(command));
2225
+ return segments.some((segment) => segmentInvokesGitCommit(segment) || segmentInvokesShellScriptWithGitCommit(segment));
2226
+ };
2227
+ var buildContextPromotionReviewPrompt = (status) => {
2228
+ const projectRoot = status.gitRoot ?? status.cwd;
2229
+ const cdCommand = `cd ${JSON.stringify(projectRoot)}`;
2230
+ return [
2231
+ "Context Promotion Review should run for the completed work commit.",
2232
+ "",
2233
+ `Project root: ${projectRoot}`,
2234
+ "This project root is authoritative for this review.",
2235
+ "",
2236
+ "Use the `context-promotion-review` skill to review the just-created HEAD commit for reusable operating knowledge.",
2237
+ "",
2238
+ "Scope boundary:",
2239
+ `- Before inspecting files, anchor shell work in the project root above. If needed, run \`${cdCommand}\` first.`,
2240
+ "- Do not inspect other repositories, parent directories, or earlier conversation workspaces.",
2241
+ "- Do not search the web or external documentation for this review.",
2242
+ "- If `AGENTS.md`, `docs/agent/*`, `docs/docs-status.md`, or other context-layer files are absent, report them as absent; do not substitute files from another repo.",
2243
+ "- Use only the just-created `HEAD` commit, this conversation, and files under the project root.",
2244
+ "",
2245
+ "Review requirements:",
2246
+ "- Do not amend, rewrite, or mix changes into the work commit.",
2247
+ "- Inspect the completed commit before deciding: run `git show --stat HEAD`, `git show --name-only HEAD`, and `git show HEAD` when detail is needed.",
2248
+ "- Cross-check existing `AGENTS.md`, `docs/agent/*`, `docs/docs-status.md`, and `.ai-ops/context-layer.json` first.",
2249
+ "- Classify candidates as `core`, `project-local`, `global`, `already-covered`, or `no-promotion`.",
2250
+ "- Ask the user before editing any file.",
2251
+ "- If promotion is approved, edit only the approved context/global files, then stop for user inspection without committing.",
2252
+ '- After approved updates or a no-promotion/already-covered decision, run `ai-ops context-promotion resolve --decision <promoted|no-promotion> --summary "<summary>"` with any approved `--scope` and `--target` values.',
2253
+ "- Re-run `ai-ops context-promotion status` and confirm a receipt exists for the current HEAD.",
2254
+ "",
2255
+ `Project: ${projectRoot}`,
2256
+ `HEAD: ${status.commitHash ?? "unknown"}`,
2257
+ `Fingerprint: ${status.fingerprint ?? "unknown"}`
2258
+ ].join("\n");
2259
+ };
2260
+ var buildContextPromotionStatusFailurePrompt = (cwd, error) => {
2261
+ const message = error instanceof Error ? error.message : "unknown error";
2262
+ return [
2263
+ "Context Promotion Review could not inspect the completed work commit.",
2264
+ "",
2265
+ "The work command has already finished; do not amend or rewrite it for this hook.",
2266
+ "Do not search the web or inspect another repository to repair this hook review.",
2267
+ "",
2268
+ "Run `ai-ops context-promotion status` to inspect the failure, then decide whether a manual promotion review is needed.",
2269
+ "",
2270
+ `Project cwd: ${cwd}`,
2271
+ `Error: ${message}`
2272
+ ].join("\n");
2273
+ };
2274
+ var buildPostToolUseOutput = (prompt) => ({
2275
+ decision: "block",
2276
+ reason: prompt,
2277
+ hookSpecificOutput: {
2278
+ hookEventName: "PostToolUse",
2279
+ additionalContext: prompt
2280
+ }
2281
+ });
2282
+ var isJsonRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2283
+ var numberField = (record, keys) => {
2284
+ for (const key of keys) {
2285
+ const value = record[key];
2286
+ if (typeof value === "number") {
2287
+ return value;
2288
+ }
2289
+ }
2290
+ return null;
2291
+ };
2292
+ var booleanField = (record, keys) => {
2293
+ for (const key of keys) {
2294
+ const value = record[key];
2295
+ if (typeof value === "boolean") {
2296
+ return value;
2297
+ }
2298
+ }
2299
+ return null;
2300
+ };
2301
+ var GIT_COMMIT_FAILURE_OUTPUT_PATTERNS = [
2302
+ /(^|\n)\s*fatal:/i,
2303
+ /(^|\n)\s*error:/i,
2304
+ /(^|\n)\s*nothing to commit\b/i,
2305
+ /(^|\n)\s*no changes added to commit\b/i,
2306
+ /(^|\n).*aborting commit\b/i,
2307
+ /(^|\n).*commit failed\b/i,
2308
+ /(^|\n).*failed to .*commit\b/i,
2309
+ /(^|\n).*command failed\b/i,
2310
+ /(^|\n).*non-zero exit\b/i,
2311
+ /(^|\n).*exit (code|status)\s+[1-9]\d*\b/i,
2312
+ /(^|\n).*exited with code\s+[1-9]\d*\b/i,
2313
+ /(^|\n).*hook.*(failed|declined|error|exit(?:ed)? with code|non-zero)/i
2314
+ ];
2315
+ var GIT_COMMIT_SUCCESS_OUTPUT_PATTERN = /(^|\n)\[[^\]\n]+ [a-f0-9]{7,40}\]/i;
2316
+ var stringIndicatesGitCommitSuccess = (output) => GIT_COMMIT_SUCCESS_OUTPUT_PATTERN.test(output);
2317
+ var stringIndicatesGitCommitFailureOrSuccess = (output) => stringIndicatesGitCommitSuccess(output) ? false : GIT_COMMIT_FAILURE_OUTPUT_PATTERNS.some((pattern) => pattern.test(output)) ? true : null;
2318
+ var recordStringFieldsIndicateGitCommitFailure = (record) => ["message", "output", "stdout", "stderr", "error", "combinedOutput"].some((key) => {
2319
+ const value = record[key];
2320
+ return typeof value === "string" && stringIndicatesGitCommitFailureOrSuccess(value) === true;
2321
+ });
2322
+ var toolResponseIndicatesFailure = (toolResponse) => {
2323
+ if (typeof toolResponse === "string") {
2324
+ return stringIndicatesGitCommitFailureOrSuccess(toolResponse) === true;
2325
+ }
2326
+ if (!isJsonRecord(toolResponse)) {
2327
+ return false;
2328
+ }
2329
+ const success = booleanField(toolResponse, ["success", "ok"]);
2330
+ if (success === false) {
2331
+ return true;
2332
+ }
2333
+ const exitCode = numberField(toolResponse, ["exit_code", "exitCode", "status", "code"]);
2334
+ if (exitCode !== null && exitCode !== 0) {
2335
+ return true;
2336
+ }
2337
+ return recordStringFieldsIndicateGitCommitFailure(toolResponse);
2338
+ };
2339
+ var evaluateContextPromotionPostToolUseHook = (params) => {
2340
+ const hookInput = ToolUseHookInputSchema.safeParse(params.hookInput);
2341
+ if (!hookInput.success) {
2342
+ return null;
2343
+ }
2344
+ if (hookInput.data.hook_event_name !== "PostToolUse" || hookInput.data.tool_name !== "Bash") {
2345
+ return null;
2346
+ }
2347
+ const toolInput = HookToolInputSchema.safeParse(hookInput.data.tool_input);
2348
+ const command = toolInput.success ? toolInput.data.command ?? "" : "";
2349
+ if (!isGitCommitCommand(command) || toolResponseIndicatesFailure(hookInput.data.tool_response)) {
2350
+ return null;
2351
+ }
2352
+ let status;
2353
+ try {
2354
+ status = getContextPromotionStatus({
2355
+ cwd: hookInput.data.cwd,
2356
+ userBasePath: params.userBasePath
2357
+ });
2358
+ } catch (error) {
2359
+ return buildPostToolUseOutput(buildContextPromotionStatusFailurePrompt(hookInput.data.cwd, error));
2360
+ }
2361
+ if (!status.hasContextLayer || status.receipt) {
2362
+ return null;
2363
+ }
2364
+ return buildPostToolUseOutput(buildContextPromotionReviewPrompt(status));
2365
+ };
2366
+
2367
+ // src/core/codex-hook.ts
2368
+ import { existsSync as existsSync4, mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
2369
+ import { dirname as dirname9, join as join14 } from "path";
2370
+ var CONTEXT_PROMOTION_HOOK_ID = "context-promotion";
2371
+ var CONTEXT_PROMOTION_HOOK_COMMAND_MARKER = "context-promotion hook post-tool-use";
2372
+ var CONTEXT_PROMOTION_LEGACY_HOOK_COMMAND_MARKER = "context-promotion hook pre-tool-use";
2373
+ var CONTEXT_PROMOTION_DEFAULT_HOOK_COMMAND = `ai-ops ${CONTEXT_PROMOTION_HOOK_COMMAND_MARKER}`;
2374
+ var PRE_TOOL_USE_EVENT = "PreToolUse";
2375
+ var POST_TOOL_USE_EVENT = "PostToolUse";
2376
+ var BASH_MATCHER = "^Bash$";
2377
+ var isJsonRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2378
+ var readJsonRecord = (filePath) => {
2379
+ if (!existsSync4(filePath)) {
2380
+ return {};
2381
+ }
2382
+ const parsed = JSON.parse(readFileSync9(filePath, "utf-8"));
2383
+ if (!isJsonRecord2(parsed)) {
2384
+ throw new Error("hooks.json must contain a JSON object");
2385
+ }
2386
+ return parsed;
2387
+ };
2388
+ var writeJsonRecord = (filePath, value) => {
2389
+ mkdirSync7(dirname9(filePath), { recursive: true });
2390
+ writeFileSync7(filePath, JSON.stringify(value, null, 2) + "\n", "utf-8");
2391
+ };
2392
+ var getOrCreateRecord = (record, key) => {
2393
+ const existing = record[key];
2394
+ if (isJsonRecord2(existing)) {
2395
+ return existing;
2396
+ }
2397
+ const next = {};
2398
+ record[key] = next;
2399
+ return next;
2400
+ };
2401
+ var getArray = (record, key) => {
2402
+ const existing = record[key];
2403
+ return Array.isArray(existing) ? existing : [];
2404
+ };
2405
+ var handlerMatchesContextPromotion = (handler) => isJsonRecord2(handler) && typeof handler.command === "string" && (handler.command.includes(CONTEXT_PROMOTION_HOOK_COMMAND_MARKER) || handler.command.includes(CONTEXT_PROMOTION_LEGACY_HOOK_COMMAND_MARKER));
2406
+ var handlerMatchesCommand = (handler, command) => isJsonRecord2(handler) && handler.command === command;
2407
+ var groupHasContextPromotionHook = (group) => isJsonRecord2(group) && getArray(group, "hooks").some(handlerMatchesContextPromotion);
2408
+ var groupHasCurrentContextPromotionHook = (group, command) => isJsonRecord2(group) && getArray(group, "hooks").some((handler) => handlerMatchesCommand(handler, command));
2409
+ var countContextPromotionHandlers = (groups) => groups.reduce((count, group) => {
2410
+ if (!isJsonRecord2(group)) {
2411
+ return count;
2412
+ }
2413
+ return count + getArray(group, "hooks").filter(handlerMatchesContextPromotion).length;
2414
+ }, 0);
2415
+ var configHasContextPromotionHook = (config) => {
2416
+ const hooks = config.hooks;
2417
+ if (!isJsonRecord2(hooks)) {
2418
+ return false;
2419
+ }
2420
+ return getArray(hooks, POST_TOOL_USE_EVENT).some(groupHasContextPromotionHook);
2421
+ };
2422
+ var configHasOnlyCurrentContextPromotionHook = (config, command) => {
2423
+ const hooks = config.hooks;
2424
+ if (!isJsonRecord2(hooks)) {
2425
+ return false;
2426
+ }
2427
+ const hasLegacy = getArray(hooks, PRE_TOOL_USE_EVENT).some(groupHasContextPromotionHook);
2428
+ const postGroups = getArray(hooks, POST_TOOL_USE_EVENT);
2429
+ const hasCurrent = postGroups.some((group) => groupHasCurrentContextPromotionHook(group, command));
2430
+ return hasCurrent && !hasLegacy && countContextPromotionHandlers(postGroups) === 1;
2431
+ };
2432
+ var removeContextPromotionHooksFromEvent = (hooks, eventName) => {
2433
+ const previousGroups = getArray(hooks, eventName);
2434
+ let removed = false;
2435
+ const nextGroups = previousGroups.map((group) => {
2436
+ if (!isJsonRecord2(group)) {
2437
+ return group;
2438
+ }
2439
+ const previousHandlers = getArray(group, "hooks");
2440
+ const nextHandlers = previousHandlers.filter((handler) => {
2441
+ const matches = handlerMatchesContextPromotion(handler);
2442
+ if (matches) {
2443
+ removed = true;
2444
+ }
2445
+ return !matches;
2446
+ });
2447
+ if (nextHandlers.length === 0) {
2448
+ return null;
2449
+ }
2450
+ return {
2451
+ ...group,
2452
+ hooks: nextHandlers
2453
+ };
2454
+ }).filter((group) => group !== null);
2455
+ if (!removed) {
2456
+ return false;
2457
+ }
2458
+ if (nextGroups.length > 0) {
2459
+ hooks[eventName] = nextGroups;
2460
+ } else {
2461
+ delete hooks[eventName];
2462
+ }
2463
+ return true;
2464
+ };
2465
+ var resolveCodexHooksPath = (codexHomePath) => join14(codexHomePath, "hooks.json");
2466
+ var buildContextPromotionHookCommand = (overrideCommand) => {
2467
+ const command = overrideCommand?.trim() ?? CONTEXT_PROMOTION_DEFAULT_HOOK_COMMAND;
2468
+ if (!command.includes(CONTEXT_PROMOTION_HOOK_COMMAND_MARKER)) {
2469
+ throw new Error(`context promotion hook command must include: ${CONTEXT_PROMOTION_HOOK_COMMAND_MARKER}`);
2470
+ }
2471
+ return command;
2472
+ };
2473
+ var inspectContextPromotionHook = (hooksPath) => ({
2474
+ hooksPath,
2475
+ installed: configHasContextPromotionHook(readJsonRecord(hooksPath))
2476
+ });
2477
+ var installContextPromotionHook = (params) => {
2478
+ const config = readJsonRecord(params.hooksPath);
2479
+ if (configHasOnlyCurrentContextPromotionHook(config, params.command)) {
2480
+ return {
2481
+ hooksPath: params.hooksPath,
2482
+ installed: true,
2483
+ changed: false
2484
+ };
2485
+ }
2486
+ const hooks = getOrCreateRecord(config, "hooks");
2487
+ removeContextPromotionHooksFromEvent(hooks, PRE_TOOL_USE_EVENT);
2488
+ removeContextPromotionHooksFromEvent(hooks, POST_TOOL_USE_EVENT);
2489
+ const existingGroups = getArray(hooks, POST_TOOL_USE_EVENT);
2490
+ const nextGroup = {
2491
+ matcher: BASH_MATCHER,
2492
+ hooks: [
2493
+ {
2494
+ type: "command",
2495
+ command: params.command,
2496
+ timeout: 30,
2497
+ statusMessage: "Checking context promotion review"
2498
+ }
2499
+ ]
2500
+ };
2501
+ hooks[POST_TOOL_USE_EVENT] = [...existingGroups, nextGroup];
2502
+ writeJsonRecord(params.hooksPath, config);
2503
+ return {
2504
+ hooksPath: params.hooksPath,
2505
+ installed: true,
2506
+ changed: true
2507
+ };
2508
+ };
2509
+ var uninstallContextPromotionHook = (hooksPath) => {
2510
+ const config = readJsonRecord(hooksPath);
2511
+ const hooks = config.hooks;
2512
+ if (!isJsonRecord2(hooks)) {
2513
+ return { hooksPath, removed: false, changed: false };
2514
+ }
2515
+ const removedLegacy = removeContextPromotionHooksFromEvent(hooks, PRE_TOOL_USE_EVENT);
2516
+ const removedCurrent = removeContextPromotionHooksFromEvent(hooks, POST_TOOL_USE_EVENT);
2517
+ const removed = removedLegacy || removedCurrent;
2518
+ if (!removed) {
2519
+ return { hooksPath, removed: false, changed: false };
2520
+ }
2521
+ writeJsonRecord(hooksPath, config);
2522
+ return { hooksPath, removed: true, changed: true };
2523
+ };
2524
+
1796
2525
  // src/lib/paths.ts
1797
- import { join as join13 } from "path";
1798
- var resolveSkillsDir = () => join13(COMPILER_DATA_DIR, "skills");
1799
- var resolveSubagentsDir = () => join13(COMPILER_DATA_DIR, "subagents");
1800
- var resolvePacksDir = () => join13(COMPILER_DATA_DIR, "packs");
2526
+ import { join as join15 } from "path";
2527
+ var resolveSkillsDir = () => join15(COMPILER_DATA_DIR, "skills");
2528
+ var resolveSubagentsDir = () => join15(COMPILER_DATA_DIR, "subagents");
2529
+ var resolvePacksDir = () => join15(COMPILER_DATA_DIR, "packs");
1801
2530
  var resolveBasePath = () => process.cwd();
1802
2531
  var resolveUserBasePath = () => {
1803
2532
  const userBasePath = process.env.AI_OPS_HOME ?? process.env.HOME;
@@ -2081,19 +2810,19 @@ var findInstalledSkill = (installedSkills, skillId) => {
2081
2810
  };
2082
2811
 
2083
2812
  // src/lib/skill-install.ts
2084
- import { existsSync as existsSync3, mkdirSync as mkdirSync6, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
2085
- import { dirname as dirname8, resolve as resolve7 } from "path";
2813
+ import { existsSync as existsSync5, mkdirSync as mkdirSync8, rmSync as rmSync3, writeFileSync as writeFileSync8 } from "fs";
2814
+ import { dirname as dirname10, resolve as resolve8 } from "path";
2086
2815
  var installSkillPackages = (basePath, packages) => {
2087
2816
  const writtenRoots = [];
2088
2817
  for (const skillPackage of packages) {
2089
- const absRoot = resolve7(basePath, skillPackage.rootDir);
2090
- if (existsSync3(absRoot)) {
2818
+ const absRoot = resolve8(basePath, skillPackage.rootDir);
2819
+ if (existsSync5(absRoot)) {
2091
2820
  rmSync3(absRoot, { recursive: true, force: true });
2092
2821
  }
2093
2822
  for (const file of skillPackage.files) {
2094
- const absPath = resolve7(basePath, file.relativePath);
2095
- mkdirSync6(dirname8(absPath), { recursive: true });
2096
- writeFileSync6(absPath, file.content + "\n", "utf-8");
2823
+ const absPath = resolve8(basePath, file.relativePath);
2824
+ mkdirSync8(dirname10(absPath), { recursive: true });
2825
+ writeFileSync8(absPath, file.content + "\n", "utf-8");
2097
2826
  }
2098
2827
  writtenRoots.push(skillPackage.rootDir);
2099
2828
  }
@@ -2102,8 +2831,8 @@ var installSkillPackages = (basePath, packages) => {
2102
2831
  var removeDirectories = (basePath, relativeDirs) => {
2103
2832
  const removed = [];
2104
2833
  for (const relativeDir of relativeDirs) {
2105
- const absPath = resolve7(basePath, relativeDir);
2106
- if (!existsSync3(absPath)) continue;
2834
+ const absPath = resolve8(basePath, relativeDir);
2835
+ if (!existsSync5(absPath)) continue;
2107
2836
  rmSync3(absPath, { recursive: true, force: true });
2108
2837
  removed.push(relativeDir);
2109
2838
  }
@@ -2282,14 +3011,14 @@ var skillUninstallCommand = async (skillId) => {
2282
3011
 
2283
3012
  // src/commands/subagent.ts
2284
3013
  import * as p8 from "@clack/prompts";
2285
- import { existsSync as existsSync5, rmSync as rmSync6 } from "fs";
3014
+ import { existsSync as existsSync7, rmSync as rmSync6 } from "fs";
2286
3015
 
2287
3016
  // src/lib/subagent-install.ts
2288
- import { existsSync as existsSync4, mkdirSync as mkdirSync7, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
2289
- import { dirname as dirname9, isAbsolute as isAbsolute3, relative as relative3, resolve as resolve8 } from "path";
3017
+ import { existsSync as existsSync6, mkdirSync as mkdirSync9, rmSync as rmSync5, writeFileSync as writeFileSync9 } from "fs";
3018
+ import { dirname as dirname11, isAbsolute as isAbsolute3, relative as relative3, resolve as resolve9 } from "path";
2290
3019
  var resolveInsideBasePath = (basePath, relativePath) => {
2291
- const absBasePath = resolve8(basePath);
2292
- const absPath = resolve8(absBasePath, relativePath);
3020
+ const absBasePath = resolve9(basePath);
3021
+ const absPath = resolve9(absBasePath, relativePath);
2293
3022
  const fromBase = relative3(absBasePath, absPath);
2294
3023
  if (fromBase.length === 0 || fromBase.startsWith("..") || isAbsolute3(fromBase)) {
2295
3024
  throw new Error(`Subagent path escapes AI_OPS_HOME: ${relativePath}`);
@@ -2301,11 +3030,11 @@ var installSubagentPackages = (basePath, packages) => {
2301
3030
  for (const subagentPackage of packages) {
2302
3031
  for (const file of subagentPackage.files) {
2303
3032
  const absPath = resolveInsideBasePath(basePath, file.relativePath);
2304
- if (existsSync4(absPath)) {
3033
+ if (existsSync6(absPath)) {
2305
3034
  rmSync5(absPath, { recursive: true, force: true });
2306
3035
  }
2307
- mkdirSync7(dirname9(absPath), { recursive: true });
2308
- writeFileSync7(absPath, file.content.trimEnd() + "\n", "utf-8");
3036
+ mkdirSync9(dirname11(absPath), { recursive: true });
3037
+ writeFileSync9(absPath, file.content.trimEnd() + "\n", "utf-8");
2309
3038
  written.push(file.relativePath);
2310
3039
  }
2311
3040
  }
@@ -2315,7 +3044,7 @@ var removeSubagentFiles = (basePath, relativePaths) => {
2315
3044
  const removed = [];
2316
3045
  for (const relativePath of relativePaths) {
2317
3046
  const absPath = resolveInsideBasePath(basePath, relativePath);
2318
- if (!existsSync4(absPath)) continue;
3047
+ if (!existsSync6(absPath)) continue;
2319
3048
  rmSync5(absPath, { recursive: true, force: true });
2320
3049
  removed.push(relativePath);
2321
3050
  }
@@ -2377,7 +3106,7 @@ var writeUserSubagentState = (params) => {
2377
3106
  };
2378
3107
  var readInstalledSubagents = (basePath) => readSubagentManifest(resolveSubagentManifestPath(basePath))?.subagents ?? [];
2379
3108
  var warnMissingSkills = (requiredSkills) => {
2380
- const missing = requiredSkills.filter((skill) => !existsSync5(skill.path));
3109
+ const missing = requiredSkills.filter((skill) => !existsSync7(skill.path));
2381
3110
  if (missing.length === 0) {
2382
3111
  return;
2383
3112
  }
@@ -2650,6 +3379,265 @@ ${result.preserved.map((file) => ` ${file}`).join("\n")}`);
2650
3379
  p9.outro("ai-ops pack uninstall \uC644\uB8CC");
2651
3380
  };
2652
3381
 
3382
+ // src/commands/context-promotion.ts
3383
+ import * as p10 from "@clack/prompts";
3384
+ var VALID_DECISIONS = [
3385
+ CONTEXT_PROMOTION_DECISION.PROMOTED,
3386
+ CONTEXT_PROMOTION_DECISION.NO_PROMOTION
3387
+ ];
3388
+ var VALID_SCOPES = [
3389
+ CONTEXT_PROMOTION_SCOPE.CORE,
3390
+ CONTEXT_PROMOTION_SCOPE.PROJECT_LOCAL,
3391
+ CONTEXT_PROMOTION_SCOPE.GLOBAL
3392
+ ];
3393
+ var parseDecision = (decision) => {
3394
+ const parsed = VALID_DECISIONS.find((candidate) => candidate === decision);
3395
+ if (parsed) {
3396
+ return parsed;
3397
+ }
3398
+ throw new Error(`decision must be one of: ${VALID_DECISIONS.join(", ")}`);
3399
+ };
3400
+ var parseScopes = (scopes) => {
3401
+ const requestedScopes = scopes ?? [];
3402
+ return requestedScopes.map((scope) => {
3403
+ const parsed = VALID_SCOPES.find((candidate) => candidate === scope);
3404
+ if (!parsed) {
3405
+ throw new Error(`scope must be one of: ${VALID_SCOPES.join(", ")}`);
3406
+ }
3407
+ return parsed;
3408
+ });
3409
+ };
3410
+ var parseMax = (max) => {
3411
+ if (max === void 0) {
3412
+ return 50;
3413
+ }
3414
+ const parsed = Number.parseInt(max, 10);
3415
+ if (!Number.isFinite(parsed) || parsed < 1) {
3416
+ throw new Error("max must be a positive integer");
3417
+ }
3418
+ return parsed;
3419
+ };
3420
+ var readStdin = async () => new Promise((resolve10, reject) => {
3421
+ let raw = "";
3422
+ process.stdin.setEncoding("utf-8");
3423
+ process.stdin.on("data", (chunk) => {
3424
+ raw += chunk;
3425
+ });
3426
+ process.stdin.on("end", () => resolve10(raw));
3427
+ process.stdin.on("error", reject);
3428
+ });
3429
+ var reportContextPromotionError = (error) => {
3430
+ const message = error instanceof Error ? error.message : "unknown error";
3431
+ p10.log.error(message);
3432
+ process.exitCode = 1;
3433
+ };
3434
+ var contextPromotionStatusCommand = async (opts) => {
3435
+ try {
3436
+ const status = getContextPromotionStatus({
3437
+ cwd: resolveBasePath(),
3438
+ userBasePath: resolveUserBasePath()
3439
+ });
3440
+ if (opts.json) {
3441
+ process.stdout.write(JSON.stringify(status, null, 2) + "\n");
3442
+ return;
3443
+ }
3444
+ p10.intro("ai-ops context-promotion status");
3445
+ p10.log.info(
3446
+ [
3447
+ `git root: ${status.gitRoot ?? "not found"}`,
3448
+ `context layer: ${status.hasContextLayer ? "found" : "not found"}`,
3449
+ `HEAD: ${status.commitHash ?? "not available"}`,
3450
+ `fingerprint: ${status.fingerprint ?? "not available"}`,
3451
+ `receipt: ${status.receipt ? "found" : "missing"}`,
3452
+ `receipt store: ${status.receiptIndexPath ?? "not available"}`
3453
+ ].join("\n")
3454
+ );
3455
+ p10.outro("ai-ops context-promotion status \uC644\uB8CC");
3456
+ } catch (error) {
3457
+ reportContextPromotionError(error);
3458
+ }
3459
+ };
3460
+ var contextPromotionResolveCommand = async (opts) => {
3461
+ p10.intro("ai-ops context-promotion resolve");
3462
+ try {
3463
+ const nextStatus = resolveContextPromotion({
3464
+ cwd: resolveBasePath(),
3465
+ userBasePath: resolveUserBasePath(),
3466
+ input: {
3467
+ decision: parseDecision(opts.decision),
3468
+ summary: opts.summary ?? "",
3469
+ scopes: parseScopes(opts.scope),
3470
+ targets: opts.target ?? []
3471
+ }
3472
+ });
3473
+ p10.log.success(`receipt \uAE30\uB85D \uC644\uB8CC: ${nextStatus.commitHash ?? nextStatus.fingerprint ?? "unknown"}`);
3474
+ } catch (error) {
3475
+ reportContextPromotionError(error);
3476
+ }
3477
+ p10.outro("ai-ops context-promotion resolve \uC644\uB8CC");
3478
+ };
3479
+ var contextPromotionPruneCommand = async (opts) => {
3480
+ p10.intro("ai-ops context-promotion prune");
3481
+ try {
3482
+ const status = getContextPromotionStatus({
3483
+ cwd: resolveBasePath(),
3484
+ userBasePath: resolveUserBasePath()
3485
+ });
3486
+ if (!status.receiptIndexPath) {
3487
+ p10.log.warn("prune\uD560 receipt store\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.");
3488
+ p10.outro("ai-ops context-promotion prune \uC644\uB8CC");
3489
+ return;
3490
+ }
3491
+ const maxReceipts = parseMax(opts.max);
3492
+ const before = readContextPromotionReceiptIndex(status.receiptIndexPath)?.receipts.length ?? 0;
3493
+ const next = pruneContextPromotionReceipts({ indexPath: status.receiptIndexPath, maxReceipts });
3494
+ const after = next?.receipts.length ?? 0;
3495
+ p10.log.success(`receipt prune \uC644\uB8CC: ${before} -> ${after}`);
3496
+ } catch (error) {
3497
+ reportContextPromotionError(error);
3498
+ }
3499
+ p10.outro("ai-ops context-promotion prune \uC644\uB8CC");
3500
+ };
3501
+ var contextPromotionPreToolUseHookCommand = async () => {
3502
+ await readStdin();
3503
+ };
3504
+ var contextPromotionPostToolUseHookCommand = async () => {
3505
+ try {
3506
+ const raw = await readStdin();
3507
+ const hookInput = raw.trim().length > 0 ? JSON.parse(raw) : {};
3508
+ const output = evaluateContextPromotionPostToolUseHook({
3509
+ hookInput,
3510
+ userBasePath: resolveUserBasePath()
3511
+ });
3512
+ if (output) {
3513
+ process.stdout.write(JSON.stringify(output) + "\n");
3514
+ }
3515
+ } catch (error) {
3516
+ const message = error instanceof Error ? error.message : "unknown error";
3517
+ process.stdout.write(
3518
+ JSON.stringify({
3519
+ systemMessage: `ai-ops context promotion hook skipped: ${message}`
3520
+ }) + "\n"
3521
+ );
3522
+ }
3523
+ };
3524
+
3525
+ // src/commands/codex-hook.ts
3526
+ import * as p11 from "@clack/prompts";
3527
+ import { existsSync as existsSync8 } from "fs";
3528
+ import { join as join16 } from "path";
3529
+ var CONTEXT_PROMOTION_REVIEW_SKILL_ID = "context-promotion-review";
3530
+ var resolveCodexHomePath = () => {
3531
+ const codexHome = process.env.CODEX_HOME;
3532
+ if (codexHome && codexHome.length > 0) {
3533
+ return codexHome;
3534
+ }
3535
+ const home = process.env.HOME;
3536
+ if (!home) {
3537
+ throw new Error("CODEX_HOME or HOME is required for Codex hook commands");
3538
+ }
3539
+ return `${home}/.codex`;
3540
+ };
3541
+ var assertContextPromotionHookId = (hookId) => {
3542
+ if (hookId !== CONTEXT_PROMOTION_HOOK_ID) {
3543
+ throw new Error(`Unknown Codex hook: ${hookId}`);
3544
+ }
3545
+ };
3546
+ var reportCodexHookError = (error) => {
3547
+ const message = error instanceof Error ? error.message : "unknown error";
3548
+ p11.log.error(message);
3549
+ process.exitCode = 1;
3550
+ };
3551
+ var readInstalledSkills2 = (basePath) => (readSkillRegistry(resolveSkillRegistryPath(basePath))?.skills ?? []).map((installedSkill) => ({
3552
+ ...installedSkill,
3553
+ id: resolveCanonicalSkillId(installedSkill.id)
3554
+ }));
3555
+ var resolveContextPromotionReviewSkill = () => {
3556
+ const skill = loadAllSkills(resolveSkillsDir()).find(
3557
+ (candidate) => candidate.id === CONTEXT_PROMOTION_REVIEW_SKILL_ID
3558
+ );
3559
+ if (!skill) {
3560
+ throw new Error(`Unknown skill: ${CONTEXT_PROMOTION_REVIEW_SKILL_ID}`);
3561
+ }
3562
+ return skill;
3563
+ };
3564
+ var hasInstalledContextPromotionReviewSkill = (basePath) => {
3565
+ const installedSkill = findInstalledSkill(readInstalledSkills2(basePath), CONTEXT_PROMOTION_REVIEW_SKILL_ID);
3566
+ return installedSkill?.tools.includes(SKILL_TOOL.CODEX) === true && existsSync8(join16(basePath, ".agents/skills/context-promotion-review/SKILL.md"));
3567
+ };
3568
+ var ensureContextPromotionReviewSkill = (basePath) => {
3569
+ const skill = resolveContextPromotionReviewSkill();
3570
+ const installedSkills = readInstalledSkills2(basePath);
3571
+ const existingInstalledSkill = findInstalledSkill(installedSkills, skill.id);
3572
+ const requestedTools = mergeSkillTools({
3573
+ existing: existingInstalledSkill?.tools,
3574
+ requested: [SKILL_TOOL.CODEX]
3575
+ });
3576
+ const { packages, installedSkill } = buildSkillInstallPlan({
3577
+ skill,
3578
+ requestedTools
3579
+ });
3580
+ const alreadyInstalled = existingInstalledSkill?.sourceHash === installedSkill.sourceHash && existingInstalledSkill.tools.includes(SKILL_TOOL.CODEX) && existsSync8(join16(basePath, ".agents/skills/context-promotion-review/SKILL.md"));
3581
+ if (alreadyInstalled) {
3582
+ return { changed: false, installedSkill };
3583
+ }
3584
+ installSkillPackages(basePath, packages);
3585
+ writeSkillRegistry(resolveSkillRegistryPath(basePath), {
3586
+ skills: upsertInstalledSkill(installedSkills, installedSkill),
3587
+ cliVersion: getCliVersion(),
3588
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
3589
+ });
3590
+ return { changed: true, installedSkill };
3591
+ };
3592
+ var codexHookInstallCommand = async (hookId, opts = {}) => {
3593
+ p11.intro(`ai-ops codex-hook install ${hookId}`);
3594
+ try {
3595
+ assertContextPromotionHookId(hookId);
3596
+ const skillResult = ensureContextPromotionReviewSkill(resolveUserBasePath());
3597
+ const hooksPath = resolveCodexHooksPath(resolveCodexHomePath());
3598
+ const result = installContextPromotionHook({
3599
+ hooksPath,
3600
+ command: buildContextPromotionHookCommand(opts.command)
3601
+ });
3602
+ p11.log.success(
3603
+ skillResult.changed ? `skill \uC124\uCE58 \uC644\uB8CC: ${skillResult.installedSkill.id}` : `skill \uC774\uBBF8 \uC124\uCE58\uB428: ${skillResult.installedSkill.id}`
3604
+ );
3605
+ p11.log.success(result.changed ? `hook \uC124\uCE58 \uC644\uB8CC: ${result.hooksPath}` : `hook \uC774\uBBF8 \uC124\uCE58\uB428: ${result.hooksPath}`);
3606
+ } catch (error) {
3607
+ reportCodexHookError(error);
3608
+ }
3609
+ p11.outro("ai-ops codex-hook install \uC644\uB8CC");
3610
+ };
3611
+ var codexHookStatusCommand = async (hookId) => {
3612
+ p11.intro(`ai-ops codex-hook status ${hookId}`);
3613
+ try {
3614
+ assertContextPromotionHookId(hookId);
3615
+ const result = inspectContextPromotionHook(resolveCodexHooksPath(resolveCodexHomePath()));
3616
+ const skillInstalled = hasInstalledContextPromotionReviewSkill(resolveUserBasePath());
3617
+ p11.log.info(
3618
+ [
3619
+ `hooks file: ${result.hooksPath}`,
3620
+ `hook installed: ${result.installed ? "yes" : "no"}`,
3621
+ `skill installed: ${skillInstalled ? "yes" : "no"}`
3622
+ ].join("\n")
3623
+ );
3624
+ } catch (error) {
3625
+ reportCodexHookError(error);
3626
+ }
3627
+ p11.outro("ai-ops codex-hook status \uC644\uB8CC");
3628
+ };
3629
+ var codexHookUninstallCommand = async (hookId) => {
3630
+ p11.intro(`ai-ops codex-hook uninstall ${hookId}`);
3631
+ try {
3632
+ assertContextPromotionHookId(hookId);
3633
+ const result = uninstallContextPromotionHook(resolveCodexHooksPath(resolveCodexHomePath()));
3634
+ p11.log.success(result.removed ? `hook \uC81C\uAC70 \uC644\uB8CC: ${result.hooksPath}` : `\uC124\uCE58\uB41C hook \uC5C6\uC74C: ${result.hooksPath}`);
3635
+ } catch (error) {
3636
+ reportCodexHookError(error);
3637
+ }
3638
+ p11.outro("ai-ops codex-hook uninstall \uC644\uB8CC");
3639
+ };
3640
+
2653
3641
  // src/bin/index.ts
2654
3642
  var program = new Command();
2655
3643
  program.name("ai-ops").description("AI agent operating layer manager").version("0.1.0");
@@ -2682,5 +3670,18 @@ packCommand.command("install <packId>").description("pack \uC124\uCE58").action(
2682
3670
  packCommand.command("diff [packId]").description("pack \uBCC0\uACBD \uBE44\uAD50").action((packId) => packDiffCommand(packId));
2683
3671
  packCommand.command("update [packId]").description("pack \uAC31\uC2E0").action((packId) => packUpdateCommand(packId));
2684
3672
  packCommand.command("uninstall <packId>").description("pack \uC81C\uAC70").action((packId) => packUninstallCommand(packId));
3673
+ var contextPromotionCommand = program.command("context-promotion").description("context promotion review receipt \uAD00\uB9AC");
3674
+ contextPromotionCommand.command("status").description("\uD604\uC7AC context promotion receipt \uC0C1\uD0DC \uD655\uC778").option("--json", "JSON\uC73C\uB85C \uCD9C\uB825", false).action((opts) => contextPromotionStatusCommand(opts));
3675
+ contextPromotionCommand.command("resolve").description("\uD604\uC7AC HEAD \uCEE4\uBC0B\uC5D0 \uB300\uD55C context promotion review receipt \uAE30\uB85D").requiredOption("--decision <decision>", "promoted|no-promotion").requiredOption("--summary <summary>", "review \uACB0\uC815 \uC694\uC57D").option("--scope <scope...>", "\uC2B9\uACA9 scope (core|project-local|global)").option("--target <path...>", "\uC2B9\uACA9 \uB300\uC0C1 \uD30C\uC77C \uB610\uB294 \uC790\uC0B0").action(
3676
+ (opts) => contextPromotionResolveCommand(opts)
3677
+ );
3678
+ contextPromotionCommand.command("prune").description("user-local context promotion receipts \uC815\uB9AC").option("--max <number>", "\uC720\uC9C0\uD560 receipt \uC218", "50").action((opts) => contextPromotionPruneCommand(opts));
3679
+ var contextPromotionHookCommand = contextPromotionCommand.command("hook").description("Codex hook \uB0B4\uBD80 \uBA85\uB839");
3680
+ contextPromotionHookCommand.command("pre-tool-use").description("Deprecated no-op Codex PreToolUse hook entrypoint").action(() => contextPromotionPreToolUseHookCommand());
3681
+ contextPromotionHookCommand.command("post-tool-use").description("Codex PostToolUse hook entrypoint").action(() => contextPromotionPostToolUseHookCommand());
3682
+ var codexHookCommand = program.command("codex-hook").description("Codex hooks \uC124\uC815 \uAD00\uB9AC");
3683
+ codexHookCommand.command("install <hookId>").description("Codex hook \uC124\uCE58").option("--command <command>", "hook\uC5D0 \uC800\uC7A5\uD560 context-promotion \uC2E4\uD589 \uBA85\uB839").action((hookId, opts) => codexHookInstallCommand(hookId, opts));
3684
+ codexHookCommand.command("status <hookId>").description("Codex hook \uC124\uCE58 \uC0C1\uD0DC \uD655\uC778").action((hookId) => codexHookStatusCommand(hookId));
3685
+ codexHookCommand.command("uninstall <hookId>").description("Codex hook \uC81C\uAC70").action((hookId) => codexHookUninstallCommand(hookId));
2685
3686
  program.parse();
2686
3687
  //# sourceMappingURL=index.js.map