oh-my-customcode 0.46.1 → 0.47.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.
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  **[한국어 문서 (Korean)](./README_ko.md)**
15
15
 
16
- 45 agents. 82 skills. 21 rules. One command.
16
+ 45 agents. 83 skills. 21 rules. One command.
17
17
 
18
18
  ```bash
19
19
  npm install -g oh-my-customcode && cd your-project && omcustom init
@@ -138,7 +138,7 @@ Each agent declares its tools, model, memory scope, and limitations in YAML fron
138
138
 
139
139
  ---
140
140
 
141
- ### Skills (82)
141
+ ### Skills (83)
142
142
 
143
143
  | Category | Count | Includes |
144
144
  |----------|-------|----------|
@@ -180,11 +180,11 @@ All commands are invoked inside the Claude Code conversation.
180
180
  |---------|-------------|
181
181
  | `/omcustom:analysis` | Analyze project, auto-configure agents and skills |
182
182
  | `/omcustom:create-agent` | Create a new agent |
183
- | `/omcustom:takeover` | Extract canonical spec from existing agent or skill |
183
+ | `/omcustom-takeover` | Extract canonical spec from existing agent or skill |
184
184
  | `/omcustom:audit-agents` | Audit agent dependencies |
185
185
  | `/omcustom:update-docs` | Sync project structure and documentation |
186
186
  | `/omcustom:sauron-watch` | Full structural verification (5+3 rounds) |
187
- | `/omcustom:feedback` | Submit feedback as GitHub issue |
187
+ | `/omcustom-feedback` | Submit feedback as GitHub issue |
188
188
 
189
189
  ### Web UI
190
190
 
@@ -199,7 +199,7 @@ All commands are invoked inside the Claude Code conversation.
199
199
  | `/omcustom:npm-publish` | Publish to npm |
200
200
  | `/omcustom:npm-version` | Semantic versioning |
201
201
  | `/omcustom:npm-audit` | Dependency security audit |
202
- | `/omcustom:release-notes` | Generate release notes from git history |
202
+ | `/omcustom-release-notes` | Generate release notes from git history |
203
203
 
204
204
  ### Memory & System
205
205
 
@@ -208,6 +208,7 @@ All commands are invoked inside the Claude Code conversation.
208
208
  | `/memory-save` | Save session context |
209
209
  | `/memory-recall` | Search and recall memories |
210
210
  | `/omcustom:monitoring-setup` | OTel monitoring toggle |
211
+ | `/omcustom:loop` | Auto-continue background agent workflows (3-continue safety limit) |
211
212
  | `/omcustom:lists` | Show all commands |
212
213
  | `/omcustom:status` | System health check |
213
214
 
@@ -271,7 +272,7 @@ your-project/
271
272
  ├── CLAUDE.md # Entry point
272
273
  ├── .claude/
273
274
  │ ├── agents/ # 45 agent definitions
274
- │ ├── skills/ # 82 skill modules
275
+ │ ├── skills/ # 83 skill modules
275
276
  │ ├── rules/ # 21 governance rules (R000-R021)
276
277
  │ ├── hooks/ # 15 lifecycle hook scripts
277
278
  │ ├── schemas/ # Tool input validation schemas
package/dist/cli/index.js CHANGED
@@ -9323,7 +9323,7 @@ var init_package = __esm(() => {
9323
9323
  package_default = {
9324
9324
  name: "oh-my-customcode",
9325
9325
  workspaces: ["packages/*"],
9326
- version: "0.46.1",
9326
+ version: "0.47.1",
9327
9327
  description: "Batteries-included agent harness for Claude Code",
9328
9328
  type: "module",
9329
9329
  bin: {
@@ -9353,9 +9353,9 @@ var init_package = __esm(() => {
9353
9353
  "test:integration": "bun test tests/integration",
9354
9354
  "test:e2e": "bun test tests/e2e",
9355
9355
  "test:coverage": "bun test --coverage",
9356
- lint: "biome check .",
9357
- "lint:fix": "biome check --write .",
9358
- format: "biome format --write .",
9356
+ lint: "biome check src/ tests/ scripts/",
9357
+ "lint:fix": "biome check --write src/ tests/ scripts/",
9358
+ format: "biome format --write src/ tests/ scripts/",
9359
9359
  typecheck: "tsc --noEmit",
9360
9360
  "docs:dev": "vitepress dev docs",
9361
9361
  "docs:build": "vitepress build docs",
@@ -9419,7 +9419,7 @@ __export(exports_projects, {
9419
9419
  default: () => projects_default
9420
9420
  });
9421
9421
  import { homedir as homedir2 } from "node:os";
9422
- import { basename as basename3, join as join9 } from "node:path";
9422
+ import { basename as basename3, dirname as dirname3, join as join9 } from "node:path";
9423
9423
  async function readLockFile(projectDir) {
9424
9424
  const lockFilePath = join9(projectDir, ".omcustom.lock.json");
9425
9425
  try {
@@ -9518,6 +9518,14 @@ async function findProjects(options = {}) {
9518
9518
  for (const dir2 of DEFAULT_SEARCH_DIRS) {
9519
9519
  searchPaths.push(join9(home, dir2));
9520
9520
  }
9521
+ if (!options.paths) {
9522
+ const cwd = process.cwd();
9523
+ if (!searchPaths.includes(cwd))
9524
+ searchPaths.push(cwd);
9525
+ const parent = dirname3(cwd);
9526
+ if (parent !== cwd && !searchPaths.includes(parent))
9527
+ searchPaths.push(parent);
9528
+ }
9521
9529
  if (options.paths) {
9522
9530
  searchPaths.push(...options.paths);
9523
9531
  }
@@ -9546,7 +9554,7 @@ function formatProjectsTable(projects, currentVersion) {
9546
9554
  if (projects.length === 0) {
9547
9555
  console.log(`
9548
9556
  oh-my-customcode가 적용된 프로젝트를 찾을 수 없습니다.`);
9549
- console.log(` 검색 경로: ~/workspace, ~/projects, ~/dev, ~/src, ~/code
9557
+ console.log(` 검색 경로: ~/workspace, ~/projects, ~/dev, ~/src, ~/code, ~/repos, ~/work, (현재 디렉토리 및 부모)
9550
9558
  `);
9551
9559
  return;
9552
9560
  }
@@ -24896,6 +24904,37 @@ var en_default = {
24896
24904
  }
24897
24905
  }
24898
24906
  },
24907
+ web: {
24908
+ description: "Manage the Web UI server (start, stop, status, open)",
24909
+ start: {
24910
+ description: "Start the Web UI server",
24911
+ portOption: "Port number",
24912
+ openOption: "Open browser automatically after start",
24913
+ foregroundOption: "Run in foreground (not detached)",
24914
+ started: "Web UI started: http://localhost:{{port}}",
24915
+ failed: "Failed to start Web UI server"
24916
+ },
24917
+ stop: {
24918
+ description: "Stop the Web UI server",
24919
+ stopped: "Web UI server stopped",
24920
+ notRunning: "Web UI server is not running"
24921
+ },
24922
+ status: {
24923
+ description: "Show Web UI server status",
24924
+ running: "Web UI is running: http://localhost:{{port}}",
24925
+ notRunning: "Web UI is not running",
24926
+ startHint: " Start with: omcustom web start"
24927
+ },
24928
+ open: {
24929
+ description: "Open the Web UI in the default browser",
24930
+ portOption: "Port number",
24931
+ notRunningWarn: "Web UI does not appear to be running. Start it with: omcustom web start"
24932
+ },
24933
+ deprecated: {
24934
+ serve: "[Deprecated] `omcustom serve` is deprecated. Use `omcustom web start` instead.",
24935
+ serveStop: "[Deprecated] `omcustom serve-stop` is deprecated. Use `omcustom web stop` instead."
24936
+ }
24937
+ },
24899
24938
  security: {
24900
24939
  description: "Scan for security issues in hooks, configs, and templates",
24901
24940
  verboseOption: "Show detailed scan results",
@@ -25247,6 +25286,37 @@ var ko_default = {
25247
25286
  }
25248
25287
  }
25249
25288
  },
25289
+ web: {
25290
+ description: "Web UI 서버 관리 (시작, 중지, 상태, 열기)",
25291
+ start: {
25292
+ description: "Web UI 서버 시작",
25293
+ portOption: "포트 번호",
25294
+ openOption: "시작 후 브라우저 자동 열기",
25295
+ foregroundOption: "포그라운드 실행 (백그라운드 분리 안 함)",
25296
+ started: "Web UI 시작됨: http://localhost:{{port}}",
25297
+ failed: "Web UI 서버 시작 실패"
25298
+ },
25299
+ stop: {
25300
+ description: "Web UI 서버 중지",
25301
+ stopped: "Web UI 서버가 중지되었습니다",
25302
+ notRunning: "Web UI 서버가 실행 중이 아닙니다"
25303
+ },
25304
+ status: {
25305
+ description: "Web UI 서버 상태 표시",
25306
+ running: "Web UI 실행 중: http://localhost:{{port}}",
25307
+ notRunning: "Web UI가 실행 중이 아닙니다",
25308
+ startHint: " 시작하려면: omcustom web start"
25309
+ },
25310
+ open: {
25311
+ description: "기본 브라우저에서 Web UI 열기",
25312
+ portOption: "포트 번호",
25313
+ notRunningWarn: "Web UI가 실행 중이지 않은 것 같습니다. 먼저 시작하세요: omcustom web start"
25314
+ },
25315
+ deprecated: {
25316
+ serve: "[Deprecated] `omcustom serve` is deprecated. Use `omcustom web start` instead.",
25317
+ serveStop: "[Deprecated] `omcustom serve-stop` is deprecated. Use `omcustom web stop` instead."
25318
+ }
25319
+ },
25250
25320
  security: {
25251
25321
  description: "훅, 설정, 템플릿의 보안 문제 검사",
25252
25322
  verboseOption: "상세 검사 결과 표시",
@@ -26874,7 +26944,7 @@ async function doctorCommand(options = {}) {
26874
26944
 
26875
26945
  // src/cli/init.ts
26876
26946
  init_package();
26877
- import { join as join11 } from "node:path";
26947
+ import { join as join10 } from "node:path";
26878
26948
 
26879
26949
  // src/core/installer.ts
26880
26950
  init_fs();
@@ -27731,83 +27801,6 @@ async function checkUvAvailable() {
27731
27801
  init_fs();
27732
27802
  init_projects();
27733
27803
 
27734
- // src/cli/serve.ts
27735
- import { spawn } from "node:child_process";
27736
- import { existsSync as existsSync2 } from "node:fs";
27737
- import { readFile as readFile2, unlink, writeFile as writeFile2 } from "node:fs/promises";
27738
- import { join as join10 } from "node:path";
27739
- var DEFAULT_PORT = 4321;
27740
- var PID_FILE = join10(process.env.HOME ?? "~", ".omcustom-serve.pid");
27741
- function findServeBuildDir(projectRoot) {
27742
- const localBuild = join10(projectRoot, "packages", "serve", "build");
27743
- if (existsSync2(join10(localBuild, "index.js")))
27744
- return localBuild;
27745
- const npmBuild = join10(import.meta.dirname, "..", "..", "packages", "serve", "build");
27746
- if (existsSync2(join10(npmBuild, "index.js")))
27747
- return npmBuild;
27748
- return null;
27749
- }
27750
- async function isServeRunning() {
27751
- try {
27752
- const raw = await readFile2(PID_FILE, "utf-8");
27753
- const pid = Number(raw.trim());
27754
- if (!Number.isFinite(pid) || pid <= 0) {
27755
- await cleanupPidFile();
27756
- return false;
27757
- }
27758
- process.kill(pid, 0);
27759
- return true;
27760
- } catch {
27761
- await cleanupPidFile();
27762
- return false;
27763
- }
27764
- }
27765
- async function startServeBackground(projectRoot, port = DEFAULT_PORT) {
27766
- if (await isServeRunning()) {
27767
- return;
27768
- }
27769
- const buildDir = findServeBuildDir(projectRoot);
27770
- if (buildDir === null) {
27771
- return;
27772
- }
27773
- const child = spawn("node", [join10(buildDir, "index.js")], {
27774
- env: {
27775
- ...process.env,
27776
- OMCUSTOM_PORT: String(port),
27777
- OMCUSTOM_HOST: "localhost",
27778
- OMCUSTOM_ORIGIN: `http://localhost:${port}`,
27779
- OMX_PROJECT_ROOT: projectRoot
27780
- },
27781
- stdio: "ignore",
27782
- detached: true
27783
- });
27784
- child.unref();
27785
- if (child.pid !== undefined) {
27786
- await writeFile2(PID_FILE, String(child.pid), "utf-8");
27787
- }
27788
- }
27789
- async function stopServe() {
27790
- try {
27791
- const raw = await readFile2(PID_FILE, "utf-8");
27792
- const pid = Number(raw.trim());
27793
- if (!Number.isFinite(pid) || pid <= 0) {
27794
- await cleanupPidFile();
27795
- return false;
27796
- }
27797
- process.kill(pid, "SIGTERM");
27798
- await cleanupPidFile();
27799
- return true;
27800
- } catch {
27801
- await cleanupPidFile();
27802
- return false;
27803
- }
27804
- }
27805
- async function cleanupPidFile() {
27806
- try {
27807
- await unlink(PID_FILE);
27808
- } catch {}
27809
- }
27810
-
27811
27804
  // node_modules/.bun/@clack+core@1.1.0/node_modules/@clack/core/dist/index.mjs
27812
27805
  import { styleText as D } from "node:util";
27813
27806
  import { stdout as R, stdin as q } from "node:process";
@@ -28785,7 +28778,7 @@ async function runInitWizard(options) {
28785
28778
  // src/cli/init.ts
28786
28779
  async function checkExistingInstallation(targetDir) {
28787
28780
  const layout = getProviderLayout();
28788
- const rootDir = join11(targetDir, layout.rootDir);
28781
+ const rootDir = join10(targetDir, layout.rootDir);
28789
28782
  return fileExists(rootDir);
28790
28783
  }
28791
28784
  var PROVIDER_SUBDIR_COMPONENTS = new Set([
@@ -28799,13 +28792,13 @@ var PROVIDER_SUBDIR_COMPONENTS = new Set([
28799
28792
  function componentToPath(targetDir, component) {
28800
28793
  if (component === "entry-md") {
28801
28794
  const layout = getProviderLayout();
28802
- return join11(targetDir, layout.entryFile);
28795
+ return join10(targetDir, layout.entryFile);
28803
28796
  }
28804
28797
  if (PROVIDER_SUBDIR_COMPONENTS.has(component)) {
28805
28798
  const layout = getProviderLayout();
28806
- return join11(targetDir, layout.rootDir, component);
28799
+ return join10(targetDir, layout.rootDir, component);
28807
28800
  }
28808
- return join11(targetDir, component);
28801
+ return join10(targetDir, component);
28809
28802
  }
28810
28803
  function buildInstalledPaths(targetDir, components) {
28811
28804
  return components.map((component) => componentToPath(targetDir, component));
@@ -28911,8 +28904,6 @@ async function initCommand(options) {
28911
28904
  console.log(" /plugin install context7");
28912
28905
  console.log("");
28913
28906
  console.log('See CLAUDE.md "외부 의존성" section for details.');
28914
- await startServeBackground(targetDir).catch(() => {});
28915
- console.log(`Web UI: http://127.0.0.1:${DEFAULT_PORT}`);
28916
28907
  return {
28917
28908
  success: true,
28918
28909
  message: i18n.t("cli.init.success"),
@@ -28926,7 +28917,7 @@ async function initCommand(options) {
28926
28917
  }
28927
28918
 
28928
28919
  // src/cli/list.ts
28929
- import { basename as basename4, dirname as dirname3, join as join12, relative as relative3 } from "node:path";
28920
+ import { basename as basename4, dirname as dirname4, join as join11, relative as relative3 } from "node:path";
28930
28921
  init_fs();
28931
28922
  var ALLOWED_TOP_LEVEL_KEYS = new Set(["name", "type", "description", "version", "category"]);
28932
28923
  function parseKeyValue(line) {
@@ -28991,12 +28982,12 @@ function extractAgentTypeFromFilename(filename) {
28991
28982
  return prefixMap[prefix] || "unknown";
28992
28983
  }
28993
28984
  function extractSkillCategoryFromPath(skillPath, baseDir, rootDir) {
28994
- const relativePath = relative3(join12(baseDir, rootDir, "skills"), skillPath);
28985
+ const relativePath = relative3(join11(baseDir, rootDir, "skills"), skillPath);
28995
28986
  const parts = relativePath.split("/").filter(Boolean);
28996
28987
  return parts[0] || "unknown";
28997
28988
  }
28998
28989
  function extractGuideCategoryFromPath(guidePath, baseDir) {
28999
- const relativePath = relative3(join12(baseDir, "guides"), guidePath);
28990
+ const relativePath = relative3(join11(baseDir, "guides"), guidePath);
29000
28991
  const parts = relativePath.split("/").filter(Boolean);
29001
28992
  return parts[0] || "unknown";
29002
28993
  }
@@ -29090,7 +29081,7 @@ async function tryExtractMarkdownDescription(mdPath, options = {}) {
29090
29081
  }
29091
29082
  }
29092
29083
  async function getAgents(targetDir, rootDir = ".claude", config) {
29093
- const agentsDir = join12(targetDir, rootDir, "agents");
29084
+ const agentsDir = join11(targetDir, rootDir, "agents");
29094
29085
  if (!await fileExists(agentsDir))
29095
29086
  return [];
29096
29087
  try {
@@ -29118,7 +29109,7 @@ async function getAgents(targetDir, rootDir = ".claude", config) {
29118
29109
  }
29119
29110
  }
29120
29111
  async function getSkills(targetDir, rootDir = ".claude", config) {
29121
- const skillsDir = join12(targetDir, rootDir, "skills");
29112
+ const skillsDir = join11(targetDir, rootDir, "skills");
29122
29113
  if (!await fileExists(skillsDir))
29123
29114
  return [];
29124
29115
  try {
@@ -29127,8 +29118,8 @@ async function getSkills(targetDir, rootDir = ".claude", config) {
29127
29118
  const customSkillPaths = new Set(customComponents.filter((c) => c.type === "skill").map((c) => c.path));
29128
29119
  const skillMdFiles = await listFiles(skillsDir, { recursive: true, pattern: "SKILL.md" });
29129
29120
  const skills = await Promise.all(skillMdFiles.map(async (skillMdPath) => {
29130
- const skillDir = dirname3(skillMdPath);
29131
- const indexYamlPath = join12(skillDir, "index.yaml");
29121
+ const skillDir = dirname4(skillMdPath);
29122
+ const indexYamlPath = join11(skillDir, "index.yaml");
29132
29123
  const { description, version } = await tryReadIndexYamlMetadata(indexYamlPath);
29133
29124
  const relativePath = relative3(targetDir, skillDir);
29134
29125
  return {
@@ -29147,7 +29138,7 @@ async function getSkills(targetDir, rootDir = ".claude", config) {
29147
29138
  }
29148
29139
  }
29149
29140
  async function getGuides(targetDir, config) {
29150
- const guidesDir = join12(targetDir, "guides");
29141
+ const guidesDir = join11(targetDir, "guides");
29151
29142
  if (!await fileExists(guidesDir))
29152
29143
  return [];
29153
29144
  try {
@@ -29174,7 +29165,7 @@ async function getGuides(targetDir, config) {
29174
29165
  }
29175
29166
  var RULE_PRIORITY_ORDER = { MUST: 0, SHOULD: 1, MAY: 2 };
29176
29167
  async function getRules(targetDir, rootDir = ".claude", config) {
29177
- const rulesDir = join12(targetDir, rootDir, "rules");
29168
+ const rulesDir = join11(targetDir, rootDir, "rules");
29178
29169
  if (!await fileExists(rulesDir))
29179
29170
  return [];
29180
29171
  try {
@@ -29246,7 +29237,7 @@ function formatAsJson(components) {
29246
29237
  console.log(JSON.stringify(components, null, 2));
29247
29238
  }
29248
29239
  async function getHooks(targetDir, rootDir = ".claude") {
29249
- const hooksDir = join12(targetDir, rootDir, "hooks");
29240
+ const hooksDir = join11(targetDir, rootDir, "hooks");
29250
29241
  if (!await fileExists(hooksDir))
29251
29242
  return [];
29252
29243
  try {
@@ -29264,7 +29255,7 @@ async function getHooks(targetDir, rootDir = ".claude") {
29264
29255
  }
29265
29256
  }
29266
29257
  async function getContexts(targetDir, rootDir = ".claude") {
29267
- const contextsDir = join12(targetDir, rootDir, "contexts");
29258
+ const contextsDir = join11(targetDir, rootDir, "contexts");
29268
29259
  if (!await fileExists(contextsDir))
29269
29260
  return [];
29270
29261
  try {
@@ -29656,46 +29647,127 @@ async function securityCommand(_options = {}) {
29656
29647
  }
29657
29648
 
29658
29649
  // src/cli/serve-commands.ts
29659
- import { execFile, spawnSync as spawnSync2 } from "node:child_process";
29650
+ import { spawnSync as spawnSync2 } from "node:child_process";
29660
29651
  import { join as join13 } from "node:path";
29652
+
29653
+ // src/cli/serve.ts
29654
+ import { spawn } from "node:child_process";
29655
+ import { existsSync as existsSync2 } from "node:fs";
29656
+ import { readFile as readFile2, unlink, writeFile as writeFile2 } from "node:fs/promises";
29657
+ import { join as join12 } from "node:path";
29658
+ var DEFAULT_PORT = 4321;
29659
+ var PID_FILE = join12(process.env.HOME ?? "~", ".omcustom-serve.pid");
29660
+ function findServeBuildDir(projectRoot, options) {
29661
+ const localBuild = join12(projectRoot, "packages", "serve", "build");
29662
+ if (existsSync2(join12(localBuild, "index.js")))
29663
+ return localBuild;
29664
+ if (options?.skipNpmFallback !== true) {
29665
+ const npmBuild = join12(import.meta.dirname, "..", "..", "packages", "serve", "build");
29666
+ if (existsSync2(join12(npmBuild, "index.js")))
29667
+ return npmBuild;
29668
+ }
29669
+ return null;
29670
+ }
29671
+ async function isServeRunning() {
29672
+ try {
29673
+ const raw = await readFile2(PID_FILE, "utf-8");
29674
+ const pid = Number(raw.trim());
29675
+ if (!Number.isFinite(pid) || pid <= 0) {
29676
+ await cleanupPidFile();
29677
+ return false;
29678
+ }
29679
+ process.kill(pid, 0);
29680
+ return true;
29681
+ } catch {
29682
+ await cleanupPidFile();
29683
+ return false;
29684
+ }
29685
+ }
29686
+ async function startServeBackground(projectRoot, port = DEFAULT_PORT, buildDirOpts) {
29687
+ if (await isServeRunning()) {
29688
+ return;
29689
+ }
29690
+ const buildDir = findServeBuildDir(projectRoot, buildDirOpts);
29691
+ if (buildDir === null) {
29692
+ return;
29693
+ }
29694
+ const child = spawn("node", [join12(buildDir, "index.js")], {
29695
+ env: {
29696
+ ...process.env,
29697
+ OMCUSTOM_PORT: String(port),
29698
+ OMCUSTOM_HOST: "localhost",
29699
+ OMCUSTOM_ORIGIN: `http://localhost:${port}`,
29700
+ OMX_PROJECT_ROOT: projectRoot
29701
+ },
29702
+ stdio: "ignore",
29703
+ detached: true
29704
+ });
29705
+ child.unref();
29706
+ if (child.pid !== undefined) {
29707
+ await writeFile2(PID_FILE, String(child.pid), "utf-8");
29708
+ }
29709
+ }
29710
+ async function stopServe() {
29711
+ try {
29712
+ const raw = await readFile2(PID_FILE, "utf-8");
29713
+ const pid = Number(raw.trim());
29714
+ if (!Number.isFinite(pid) || pid <= 0) {
29715
+ await cleanupPidFile();
29716
+ return false;
29717
+ }
29718
+ process.kill(pid, "SIGTERM");
29719
+ await cleanupPidFile();
29720
+ return true;
29721
+ } catch {
29722
+ await cleanupPidFile();
29723
+ return false;
29724
+ }
29725
+ }
29726
+ async function cleanupPidFile() {
29727
+ try {
29728
+ await unlink(PID_FILE);
29729
+ } catch {}
29730
+ }
29731
+
29732
+ // src/cli/serve-commands.ts
29661
29733
  async function serveCommand(options) {
29662
29734
  const port = options.port !== undefined ? Number(options.port) : DEFAULT_PORT;
29663
29735
  if (!Number.isFinite(port) || port < 1 || port > 65535) {
29664
29736
  console.error(`Invalid port: ${options.port}`);
29665
29737
  process.exit(1);
29666
29738
  }
29667
- const cwd = process.cwd();
29739
+ const cwd = options._projectRoot ?? process.cwd();
29740
+ const buildDirOpts = {
29741
+ skipNpmFallback: options._projectRoot !== undefined
29742
+ };
29668
29743
  if (options.foreground === true) {
29669
- runForeground(cwd, port);
29744
+ runForeground(cwd, port, buildDirOpts);
29670
29745
  return;
29671
29746
  }
29672
- await startServeBackground(cwd, port);
29747
+ await startServeBackground(cwd, port, buildDirOpts);
29673
29748
  const running = await isServeRunning();
29674
29749
  if (running) {
29675
- console.log(`Web UI started: http://127.0.0.1:${port}`);
29676
- if (options.open === true) {
29677
- openBrowser(port);
29678
- }
29750
+ console.log(i18n.t("cli.web.start.started", { port }));
29679
29751
  } else {
29680
- console.error("Failed to start Web UI server");
29752
+ console.error(i18n.t("cli.web.start.failed"));
29681
29753
  process.exit(1);
29682
29754
  }
29683
29755
  }
29684
29756
  async function serveStopCommand() {
29685
29757
  const stopped = await stopServe();
29686
29758
  if (stopped) {
29687
- console.log("Web UI server stopped");
29759
+ console.log(i18n.t("cli.web.stop.stopped"));
29688
29760
  } else {
29689
- console.log("Web UI server is not running");
29761
+ console.log(i18n.t("cli.web.stop.notRunning"));
29690
29762
  }
29691
29763
  }
29692
- function runForeground(projectRoot, port) {
29693
- const buildDir = findServeBuildDir(projectRoot);
29764
+ function runForeground(projectRoot, port, buildDirOpts) {
29765
+ const buildDir = findServeBuildDir(projectRoot, buildDirOpts);
29694
29766
  if (buildDir === null) {
29695
29767
  console.error("Web UI build not found. Run: cd packages/serve && bun run build");
29696
29768
  process.exit(1);
29697
29769
  }
29698
- console.log(`Web UI: http://127.0.0.1:${port}`);
29770
+ console.log(`Web UI: http://localhost:${port}`);
29699
29771
  spawnSync2("node", [join13(buildDir, "index.js")], {
29700
29772
  env: {
29701
29773
  ...process.env,
@@ -29707,17 +29779,6 @@ function runForeground(projectRoot, port) {
29707
29779
  stdio: "inherit"
29708
29780
  });
29709
29781
  }
29710
- function openBrowser(port) {
29711
- const url = `http://127.0.0.1:${port}`;
29712
- const platform = process.platform;
29713
- if (platform === "darwin") {
29714
- execFile("open", [url], () => {});
29715
- } else if (platform === "win32") {
29716
- execFile("cmd", ["/c", "start", url], () => {});
29717
- } else {
29718
- execFile("xdg-open", [url], () => {});
29719
- }
29720
- }
29721
29782
 
29722
29783
  // src/cli/update.ts
29723
29784
  init_package();
@@ -30493,6 +30554,35 @@ function printUpdateResults(result) {
30493
30554
  }
30494
30555
  }
30495
30556
 
30557
+ // src/cli/web-commands.ts
30558
+ async function webStartCommand(options) {
30559
+ await serveCommand(options);
30560
+ }
30561
+ async function webStopCommand() {
30562
+ await serveStopCommand();
30563
+ }
30564
+ async function webStatusCommand() {
30565
+ const running = await isServeRunning();
30566
+ if (running) {
30567
+ const port = process.env.OMCUSTOM_PORT ?? String(DEFAULT_PORT);
30568
+ console.log(i18n.t("cli.web.status.running", { port }));
30569
+ } else {
30570
+ console.log(i18n.t("cli.web.status.notRunning"));
30571
+ console.log(i18n.t("cli.web.status.startHint"));
30572
+ }
30573
+ }
30574
+ async function webOpenCommand(options) {
30575
+ const port = options.port !== undefined ? Number(options.port) : DEFAULT_PORT;
30576
+ if (!Number.isFinite(port) || port < 1 || port > 65535) {
30577
+ console.error(`Invalid port: ${options.port}`);
30578
+ process.exit(1);
30579
+ }
30580
+ const running = await isServeRunning();
30581
+ if (!running) {
30582
+ console.warn(i18n.t("cli.web.open.notRunningWarn"));
30583
+ }
30584
+ }
30585
+
30496
30586
  // src/cli/index.ts
30497
30587
  var require2 = createRequire2(import.meta.url);
30498
30588
  var packageJson = require2("../../package.json");
@@ -30518,10 +30608,28 @@ function createProgram() {
30518
30608
  const result = await securityCommand(options);
30519
30609
  process.exitCode = result.success ? 0 : 1;
30520
30610
  });
30521
- program2.command("serve").description("Start the web UI server").option("-p, --port <port>", "Port number", "4321").option("--open", "Open browser automatically").option("--foreground", "Run in foreground (not detached)").action(async (options) => {
30611
+ const web = program2.command("web").description(i18n.t("cli.web.description"));
30612
+ web.command("start").description(i18n.t("cli.web.start.description")).option("-p, --port <port>", i18n.t("cli.web.start.portOption"), "4321").option("--foreground", i18n.t("cli.web.start.foregroundOption")).action(async (options) => {
30613
+ await webStartCommand(options);
30614
+ });
30615
+ web.command("stop").description(i18n.t("cli.web.stop.description")).action(async () => {
30616
+ await webStopCommand();
30617
+ });
30618
+ web.command("status").description(i18n.t("cli.web.status.description")).action(async () => {
30619
+ await webStatusCommand();
30620
+ });
30621
+ web.command("open").description(i18n.t("cli.web.open.description")).option("-p, --port <port>", i18n.t("cli.web.open.portOption"), "4321").action(async (options) => {
30622
+ await webOpenCommand(options);
30623
+ });
30624
+ web.action(async () => {
30625
+ await webStatusCommand();
30626
+ });
30627
+ program2.command("serve").description("(Deprecated) Start the Web UI server — use `omcustom web start` instead").option("-p, --port <port>", i18n.t("cli.web.start.portOption"), "4321").option("--foreground", i18n.t("cli.web.start.foregroundOption")).action(async (options) => {
30628
+ console.warn(i18n.t("cli.web.deprecated.serve"));
30522
30629
  await serveCommand(options);
30523
30630
  });
30524
- program2.command("serve-stop").description("Stop the web UI server").action(async () => {
30631
+ program2.command("serve-stop").description("(Deprecated) Stop the Web UI server — use `omcustom web stop` instead").action(async () => {
30632
+ console.warn(i18n.t("cli.web.deprecated.serveStop"));
30525
30633
  await serveStopCommand();
30526
30634
  });
30527
30635
  program2.command("projects").description("List all projects on this machine where oh-my-customcode is installed").option("-f, --format <format>", "Output format: table, json, or simple", "table").option("--path <dir>", "Additional search directory (can be specified multiple times)", (val, prev) => [...prev, val], []).action(async (options) => {
@@ -30530,7 +30638,14 @@ function createProgram() {
30530
30638
  program2.hook("preAction", async (thisCommand, actionCommand) => {
30531
30639
  const opts = thisCommand.optsWithGlobals();
30532
30640
  const skipCheck = opts.skipVersionCheck || false;
30533
- if (actionCommand.name() === "init") {
30641
+ const cmdName = actionCommand.name();
30642
+ const parentName = actionCommand.parent?.name();
30643
+ const isServeCmd = cmdName === "serve" || cmdName === "serve-stop";
30644
+ const isWebCmd = cmdName === "web" || parentName === "web";
30645
+ if (isServeCmd || isWebCmd) {
30646
+ return;
30647
+ }
30648
+ if (cmdName === "init") {
30534
30649
  await maybeHandleSelfUpdateForInit({
30535
30650
  currentVersion: packageJson.version,
30536
30651
  skip: skipCheck
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "oh-my-customcode",
3
3
  "workspaces": ["packages/*"],
4
- "version": "0.46.1",
4
+ "version": "0.47.1",
5
5
  "description": "Batteries-included agent harness for Claude Code",
6
6
  "type": "module",
7
7
  "bin": {
@@ -31,9 +31,9 @@
31
31
  "test:integration": "bun test tests/integration",
32
32
  "test:e2e": "bun test tests/e2e",
33
33
  "test:coverage": "bun test --coverage",
34
- "lint": "biome check .",
35
- "lint:fix": "biome check --write .",
36
- "format": "biome format --write .",
34
+ "lint": "biome check src/ tests/ scripts/",
35
+ "lint:fix": "biome check --write src/ tests/ scripts/",
36
+ "format": "biome format --write src/ tests/ scripts/",
37
37
  "typecheck": "tsc --noEmit",
38
38
  "docs:dev": "vitepress dev docs",
39
39
  "docs:build": "vitepress build docs",
@@ -136,9 +136,17 @@
136
136
  {
137
137
  "type": "command",
138
138
  "command": "bash .claude/hooks/scripts/task-outcome-recorder.sh"
139
+ },
140
+ {
141
+ "type": "command",
142
+ "command": "count_file=\"/tmp/.claude-loop-count-$PPID\"; if [ -f \"$count_file\" ]; then last_mod=$(stat -f%m \"$count_file\" 2>/dev/null || echo 0); now=$(date +%s); if [ $((now - last_mod)) -gt 60 ]; then echo 0 > \"$count_file\"; fi; fi; count=$(cat \"$count_file\" 2>/dev/null || echo 0); count=$((count + 1)); echo \"$count\" > \"$count_file\"; if [ \"$count\" -ge 4 ]; then echo '[AutoContinue] SAFETY: auto-continue limit (3) reached. Pausing.' >&2; fi; cat"
143
+ },
144
+ {
145
+ "type": "prompt",
146
+ "prompt": "A background subagent just completed. Check if there are pending workflow steps that depend on this result. If the previous subagent FAILED, do NOT auto-continue — report the failure and wait for user input. If the previous step succeeded and there are pending steps, proceed automatically. If no pending steps, report results and wait. Safety: The file /tmp/.claude-loop-count-$PPID tracks auto-continue count. After 3 consecutive auto-continues without user interaction, pause and ask the user before proceeding."
139
147
  }
140
148
  ],
141
- "description": "Record agent outcomes on subagent completion (complements PostToolUse Agent matcher)"
149
+ "description": "Record agent outcomes + auto-continue workflow on subagent completion"
142
150
  }
143
151
  ],
144
152
  "PostCompact": [
@@ -205,8 +205,8 @@ When the `--spec` flag is present, refactoring is guided by the target's canonic
205
205
 
206
206
  ### Workflow
207
207
 
208
- 1. **Load spec**: Read `.claude/specs/<agent-name>.spec.md` (generated by `/omcustom:takeover`)
209
- - If spec doesn't exist, run takeover first: `/omcustom:takeover <name>`
208
+ 1. **Load spec**: Read `.claude/specs/<agent-name>.spec.md` (generated by `/omcustom-takeover`)
209
+ - If spec doesn't exist, run takeover first: `/omcustom-takeover <name>`
210
210
  2. **Extract invariants**: Parse the spec's `## Invariants` section as pre-flight guard constraints
211
211
  3. **Refactor**: Perform normal refactoring (per existing workflow)
212
212
  4. **Verify invariants**: After refactoring, check each invariant still holds:
@@ -216,7 +216,7 @@ When the `--spec` flag is present, refactoring is guided by the target's canonic
216
216
  ├── ✓ Invariant 2: {description} — PASS
217
217
  └── ✗ Invariant 3: {description} — FAIL (reason)
218
218
  ```
219
- 5. **Regenerate spec**: If refactoring changed the contract, run `/omcustom:takeover <name>` to update
219
+ 5. **Regenerate spec**: If refactoring changed the contract, run `/omcustom-takeover <name>` to update
220
220
 
221
221
  ### When to Use
222
222
 
@@ -1,53 +1,82 @@
1
1
  ---
2
2
  name: omcustom-feedback
3
- description: Submit feedback about oh-my-customcode as a GitHub issue
3
+ description: Submit feedback about oh-my-customcode (supports anonymous submission)
4
4
  scope: harness
5
5
  user-invocable: true
6
6
  disable-model-invocation: true
7
- argument-hint: "[description or leave empty for interactive]"
7
+ argument-hint: "[description or leave empty for interactive] [--anonymous]"
8
8
  ---
9
9
 
10
10
  # Feedback Submitter
11
11
 
12
- Submit feedback about oh-my-customcode (bugs, features, improvements, questions) directly as a GitHub issue from the CLI session.
12
+ Submit feedback about oh-my-customcode (bugs, features, improvements, questions) directly from the CLI session. Supports anonymous submission via Airflow DAG when gh CLI is unavailable or when anonymity is requested.
13
13
 
14
14
  ## Purpose
15
15
 
16
- Lowers the barrier for submitting feedback by allowing users to create GitHub issues without leaving their terminal session. All feedback is filed to the `baekenough/oh-my-customcode` repository.
16
+ Lowers the barrier for submitting feedback by allowing users to create GitHub issues or submit anonymously — without leaving their terminal session. All feedback is filed to the `baekenough/oh-my-customcode` repository.
17
17
 
18
18
  ## Usage
19
19
 
20
20
  ```
21
21
  # Inline feedback
22
- /omcustom:feedback HUD display is missing during parallel agent spawn
22
+ /omcustom-feedback HUD display is missing during parallel agent spawn
23
+
24
+ # Anonymous submission
25
+ /omcustom:feedback --anonymous Something feels off with the routing
23
26
 
24
27
  # Interactive (no arguments)
25
- /omcustom:feedback
28
+ /omcustom-feedback
26
29
  ```
27
30
 
28
31
  ## Workflow
29
32
 
30
33
  ### Phase 1: Input Parsing
31
34
 
32
- If arguments are provided:
35
+ Check for `--anonymous` flag in the arguments:
36
+ - If `--anonymous` is present, set `ANONYMOUS=true` and strip the flag from the content
37
+ - Otherwise, set `ANONYMOUS=false`
38
+
39
+ If remaining arguments are provided:
33
40
  1. Analyze the content to auto-detect category (`bug`, `feature`, `improvement`, `question`)
34
41
  2. Use the content as the issue title (truncate to 80 chars if needed)
35
42
  3. Use the full content as the description body
36
43
 
37
- If no arguments:
44
+ If no arguments (or only `--anonymous`):
38
45
  1. Ask the user for category using AskUserQuestion: `[bug / feature / improvement / question]`
39
46
  2. Ask for title and optional detailed description (combine into a single prompt when possible)
40
47
 
41
- ### Phase 2: Preflight Check
48
+ ### Phase 2: Route Decision
42
49
 
43
- ```bash
44
- # Verify gh CLI is installed
45
- command -v gh >/dev/null 2>&1 || { echo "Error: gh CLI not installed. Install from https://cli.github.com/"; exit 1; }
50
+ Check environment and user intent:
46
51
 
47
- # Verify authentication
48
- gh auth status 2>&1 | grep -q "Logged in" || { echo "Error: Not authenticated. Run 'gh auth login' first."; exit 1; }
52
+ ```bash
53
+ # Check gh CLI availability
54
+ command -v gh >/dev/null 2>&1 && GH_AVAILABLE=true || GH_AVAILABLE=false
55
+
56
+ # Check gh authentication (only if gh is available)
57
+ if [ "$GH_AVAILABLE" = "true" ]; then
58
+ gh auth status >/dev/null 2>&1 && GH_AUTHED=true || GH_AUTHED=false
59
+ else
60
+ GH_AUTHED=false
61
+ fi
62
+
63
+ # Check curl availability
64
+ command -v curl >/dev/null 2>&1 && CURL_AVAILABLE=true || CURL_AVAILABLE=false
49
65
  ```
50
66
 
67
+ **Route A**: `gh` available + authenticated + NOT anonymous
68
+ - Use GitHub Issue creation (see Phase 4A)
69
+
70
+ **Route B**: anonymous OR not authenticated (gh available)
71
+ - Use `curl` to Airflow REST API (see Phase 4C)
72
+ - Phase 4B (workflow_dispatch) reserved for future use
73
+
74
+ **Route C**: `gh` NOT available (or Route B curl fallback)
75
+ - Use `curl` to Airflow REST API (see Phase 4C)
76
+
77
+ **Fallback**: Neither `gh` nor `curl` available
78
+ - Save feedback locally and inform the user (see Phase 4D)
79
+
51
80
  ### Phase 3: Environment Collection
52
81
 
53
82
  Collect environment info via Bash:
@@ -64,9 +93,16 @@ OS_INFO=$(uname -s 2>/dev/null || echo "unknown")
64
93
 
65
94
  # Project name
66
95
  PROJECT_NAME=$(basename "$(pwd)")
96
+
97
+ # Build project context string
98
+ PROJECT_CONTEXT="omcustom v${OMCUSTOM_VERSION}, Claude Code ${CLAUDE_VERSION}, ${OS_INFO}"
67
99
  ```
68
100
 
69
- ### Phase 4: Confirmation and Create
101
+ For anonymous submissions, do NOT include the project name. Offer to include project context as opt-in:
102
+ - Ask: "Include environment info (version, OS) in the anonymous report? [Y/n]"
103
+ - If declined, set `PROJECT_CONTEXT=""`
104
+
105
+ ### Phase 4A: GitHub Issue Creation (Route A — gh + authenticated + not anonymous)
70
106
 
71
107
  1. Show the user a preview of the issue to be created:
72
108
  ```
@@ -80,7 +116,7 @@ PROJECT_NAME=$(basename "$(pwd)")
80
116
 
81
117
  3. Ensure labels exist (defensive):
82
118
  ```bash
83
- gh label create feedback --description "User feedback via /omcustom:feedback" --color 0E8A16 --repo baekenough/oh-my-customcode 2>/dev/null || true
119
+ gh label create feedback --description "User feedback via /omcustom-feedback" --color 0E8A16 --repo baekenough/oh-my-customcode 2>/dev/null || true
84
120
  ```
85
121
 
86
122
  4. Create the issue using `--body-file` for safe markdown handling:
@@ -102,7 +138,7 @@ PROJECT_NAME=$(basename "$(pwd)")
102
138
  - Project: {project_name}
103
139
 
104
140
  ---
105
- *Submitted via `/omcustom:feedback`*
141
+ *Submitted via `/omcustom-feedback`*
106
142
  FEEDBACK_EOF
107
143
 
108
144
  # Create issue
@@ -120,18 +156,91 @@ PROJECT_NAME=$(basename "$(pwd)")
120
156
 
121
157
  6. Return the issue URL to the user
122
158
 
159
+ ### Phase 4B: Anonymous via GitHub Actions (Route B — FUTURE)
160
+
161
+ > **Note**: This route is reserved for future implementation. The `feedback-submission.yml` workflow does not yet exist. All anonymous/unauthenticated submissions currently fall through to Route C (Airflow REST API).
162
+
163
+ ```bash
164
+ gh workflow run feedback-submission.yml \
165
+ --repo baekenough/oh-my-customcode \
166
+ -f title="$TITLE" \
167
+ -f body="$BODY" \
168
+ -f feedback_type="$TYPE" \
169
+ -f anonymous=true \
170
+ -f project_context="$PROJECT_CONTEXT"
171
+ ```
172
+
173
+ On success, inform the user:
174
+ ```
175
+ [Done] Anonymous feedback submitted via GitHub Actions workflow.
176
+ ```
177
+
178
+ If `gh workflow run` fails (e.g., permissions), fall through to Route C.
179
+
180
+ ### Phase 4C: Anonymous via Airflow REST API (Route C — no gh)
181
+
182
+ ```bash
183
+ # Build JSON payload safely with jq
184
+ PAYLOAD=$(jq -n --arg title "$TITLE" --arg body "$BODY" --arg type "$TYPE" --arg ctx "$PROJECT_CONTEXT" \
185
+ '{"conf":{"title":$title,"body":$body,"feedback_type":$type,"anonymous":true,"submitter":"","project_context":$ctx}}')
186
+
187
+ curl -X POST "https://airflow.baekenough.com/api/v2/dags/omc_feedback_collector/dagRuns" \
188
+ -H "Content-Type: application/json" \
189
+ -d "$PAYLOAD" \
190
+ --silent --show-error \
191
+ --max-time 15
192
+ ```
193
+
194
+ On HTTP 200/201, inform the user:
195
+ ```
196
+ [Done] Anonymous feedback submitted via Airflow.
197
+ ```
198
+
199
+ On failure (non-2xx or network error), fall through to Fallback.
200
+
201
+ ### Phase 4D: Local Fallback (no gh, no curl, or all routes failed)
202
+
203
+ ```bash
204
+ mkdir -p ~/.omcustom/feedback
205
+ TIMESTAMP=$(date +%Y%m%dT%H%M%S)
206
+ FEEDBACK_FILE=~/.omcustom/feedback/${TIMESTAMP}.json
207
+
208
+ cat > "$FEEDBACK_FILE" << EOF
209
+ {
210
+ "title": "$TITLE",
211
+ "body": "$BODY",
212
+ "feedback_type": "$TYPE",
213
+ "anonymous": $ANONYMOUS,
214
+ "project_context": "$PROJECT_CONTEXT",
215
+ "saved_at": "$TIMESTAMP"
216
+ }
217
+ EOF
218
+ ```
219
+
220
+ Inform the user:
221
+ ```
222
+ [Saved] Feedback saved locally to ~/.omcustom/feedback/{timestamp}.json
223
+ Submit manually when connectivity is available:
224
+ - GitHub Issues: https://github.com/baekenough/oh-my-customcode/issues/new
225
+ - Or run /omcustom:feedback again when gh or curl is available
226
+ ```
227
+
123
228
  ### Category-to-Label Mapping
124
229
 
125
- | Category | GitHub Label |
126
- |----------|-------------|
127
- | bug | bug |
128
- | feature | enhancement |
129
- | improvement | enhancement |
130
- | question | question |
230
+ | Category | GitHub Label | Airflow feedback_type |
231
+ |----------|--------------|-----------------------|
232
+ | bug | bug | bug |
233
+ | feature | enhancement | feature |
234
+ | improvement | enhancement | improvement |
235
+ | question | question | question |
236
+ | (auto-detect fails) | (none) | general |
131
237
 
132
238
  ## Notes
133
239
 
134
- - Target repo is hardcoded to `baekenough/oh-my-customcode` feedback is always about omcustom itself
135
- - Requires `gh` CLI with authentication
136
- - Uses `--body-file` instead of `--body` to safely handle markdown with special characters
240
+ - Route A creates a visible GitHub issue attributed to the user's gh account
241
+ - Routes B and C submit anonymously — submitter identity is not recorded
242
+ - Route B requires `gh` but NOT authentication to the repo (public workflow)
243
+ - Route C requires `curl` (available on macOS/Linux by default)
244
+ - Fallback ensures no feedback is silently lost even in offline environments
137
245
  - `disable-model-invocation: true` ensures this skill only runs when explicitly invoked by the user
246
+ - Target repo is hardcoded to `baekenough/oh-my-customcode` — feedback is always about omcustom itself
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: omcustom-loop
3
+ description: Prevent session idle during background agent work via SubagentStop prompt hook auto-continuation
4
+ scope: core
5
+ version: 1.0.0
6
+ user-invocable: true
7
+ ---
8
+
9
+ # /omcustom:loop — Session Auto-Continuation
10
+
11
+ ## Overview
12
+
13
+ Prevents session idle when background subagents complete by using a `SubagentStop` prompt hook that nudges the orchestrator to check for pending workflow steps.
14
+
15
+ ## How It Works
16
+
17
+ 1. When a background subagent completes, Claude Code fires the `SubagentStop` event
18
+ 2. The prompt hook injects a message asking the orchestrator to check for pending steps
19
+ 3. If pending steps exist, the orchestrator proceeds automatically
20
+ 4. If no pending steps, it reports results and waits for user input
21
+
22
+ ## Safety Limits
23
+
24
+ - **3 consecutive auto-continues max**: After 3 automatic progressions without user interaction, the system pauses and asks the user before proceeding
25
+ - **stuck-detector integration**: If the same action repeats 3+ times, stuck-detector intervenes
26
+ - **cost-cap-advisor**: Cost monitoring continues during auto-continuation
27
+
28
+ ## Configuration
29
+
30
+ The hook is configured in `.claude/hooks/hooks.json` under `SubagentStop`. It works alongside the existing `task-outcome-recorder.sh` command hook.
31
+
32
+ ## Limitations
33
+
34
+ - **Platform constraint**: Claude Code's turn-based model means the prompt hook only fires when a subagent completes — it cannot wake the model from true idle state
35
+ - **Foreground agents preferred**: For guaranteed continuation, use foreground parallel agents (R009) instead of background agents
36
+ - **PoC status**: This is an experimental feature. If the prompt hook doesn't reliably trigger in all scenarios, fall back to foreground agent patterns
37
+
38
+ ## Usage
39
+
40
+ ```bash
41
+ /omcustom:loop # Show current auto-continuation status
42
+ /omcustom:loop status # Same as above
43
+ ```
44
+
45
+ The feature is active by default via hooks.json. No explicit activation needed.
@@ -17,8 +17,8 @@ Replaces the CI-based `release-notes.yml` workflow that previously used Claude A
17
17
  ## Usage
18
18
 
19
19
  ```
20
- /omcustom:release-notes 0.36.0
21
- /omcustom:release-notes 0.36.0 --previous-tag v0.35.3
20
+ /omcustom-release-notes 0.36.0
21
+ /omcustom-release-notes 0.36.0 --previous-tag v0.35.3
22
22
  ```
23
23
 
24
24
  ## Workflow
@@ -105,7 +105,7 @@ This skill is designed to be used during the release process:
105
105
 
106
106
  ```
107
107
  /omcustom:npm-version patch|minor|major -> version bump
108
- /omcustom:release-notes {version} -> generate notes
108
+ /omcustom-release-notes {version} -> generate notes
109
109
  mgr-gitnerd: gh release create -> create release with notes
110
110
  ```
111
111
 
@@ -17,8 +17,8 @@ When an agent or skill has evolved organically without a formal spec, `takeover`
17
17
  ## Usage
18
18
 
19
19
  ```
20
- /omcustom:takeover <agent-name>
21
- /omcustom:takeover <skill-name>
20
+ /omcustom-takeover <agent-name>
21
+ /omcustom-takeover <skill-name>
22
22
  ```
23
23
 
24
24
  ## Workflow
@@ -110,6 +110,6 @@ generated: <ISO-8601 timestamp>
110
110
  ## Notes
111
111
 
112
112
  - Specs are git-untracked (under `.claude/`)
113
- - Regenerate anytime with `/omcustom:takeover <name>`
113
+ - Regenerate anytime with `/omcustom-takeover <name>`
114
114
  - Used by `/dev-refactor --spec` for invariant-preserving refactoring
115
115
  - Advisory output — human review recommended before using as contract
@@ -59,6 +59,11 @@ if [[ -z "$json" ]]; then
59
59
  exit 0
60
60
  fi
61
61
 
62
+ # Debug logging for CTX investigation
63
+ if [[ -n "${STATUSLINE_DEBUG}" ]]; then
64
+ printf '%s\n' "$json" >> "/tmp/.claude-statusline-debug-${PPID}.jsonl"
65
+ fi
66
+
62
67
  # ---------------------------------------------------------------------------
63
68
  # 4. Single jq call — extract all fields as TSV
64
69
  # Fields: model_name, project_dir, ctx_pct, ctx_size, cost_usd, rl_5h_pct
@@ -67,7 +72,7 @@ IFS=$'\t' read -r model_name project_dir ctx_pct ctx_size cost_usd rl_5h_pct <<<
67
72
  printf '%s' "$json" | jq -r '[
68
73
  (.model.display_name // "unknown"),
69
74
  (.workspace.current_dir // ""),
70
- (.context_window.used_percentage // 0),
75
+ (if .context_window.used != null and .context_window.total != null and .context_window.total > 0 then (.context_window.used / .context_window.total * 100) elif .context_window.used_percentage != null then .context_window.used_percentage else 0 end),
71
76
  (.context_window.context_window_size // 0),
72
77
  (.cost.total_cost_usd // 0),
73
78
  (.rate_limits.five_hour.used_percentage // -1)
@@ -78,7 +83,8 @@ IFS=$'\t' read -r model_name project_dir ctx_pct ctx_size cost_usd rl_5h_pct <<<
78
83
  # 4b. Cost & context data bridge — write to temp file for hooks
79
84
  # ---------------------------------------------------------------------------
80
85
  COST_BRIDGE_FILE="/tmp/.claude-cost-${PPID}"
81
- printf '%s\t%s\t%s\t%s\n' "$cost_usd" "$ctx_pct" "$(date +%s)" "$rl_5h_pct" > "$COST_BRIDGE_FILE" 2>/dev/null || true
86
+ _tmp="${COST_BRIDGE_FILE}.tmp.$$"
87
+ printf '%s\t%s\t%s\t%s\n' "$cost_usd" "$ctx_pct" "$(date +%s)" "$rl_5h_pct" > "$_tmp" 2>/dev/null && mv -f "$_tmp" "$COST_BRIDGE_FILE" 2>/dev/null || true
82
88
 
83
89
  # ---------------------------------------------------------------------------
84
90
  # 5. Model display name + color (bash 3.2 compatible case pattern matching)
@@ -101,7 +101,7 @@ oh-my-customcode로 구동됩니다.
101
101
  | `/omcustom:update-external` | 외부 소스에서 에이전트 업데이트 |
102
102
  | `/omcustom:audit-agents` | 에이전트 의존성 감사 |
103
103
  | `/omcustom:fix-refs` | 깨진 참조 수정 |
104
- | `/omcustom:takeover` | 기존 에이전트/스킬에서 canonical spec 추출 |
104
+ | `/omcustom-takeover` | 기존 에이전트/스킬에서 canonical spec 추출 |
105
105
  | `/adversarial-review` | 공격자 관점 보안 코드 리뷰 |
106
106
  | `/ambiguity-gate` | 요청 모호성 분석 및 명확화 질문 (ouroboros 패턴) |
107
107
  | `/dev-review` | 코드 베스트 프랙티스 리뷰 |
@@ -112,8 +112,8 @@ oh-my-customcode로 구동됩니다.
112
112
  | `/omcustom:npm-publish` | npm 레지스트리에 패키지 배포 |
113
113
  | `/omcustom:npm-version` | 시맨틱 버전 관리 |
114
114
  | `/omcustom:npm-audit` | 의존성 감사 |
115
- | `/omcustom:release-notes` | 릴리즈 노트 생성 (git 히스토리 기반) |
116
- | `/omcustom:feedback` | 사용자 피드백을 GitHub Issue로 등록 |
115
+ | `/omcustom-release-notes` | 릴리즈 노트 생성 (git 히스토리 기반) |
116
+ | `/omcustom-feedback` | 사용자 피드백을 GitHub Issue로 등록 |
117
117
  | `/codex-exec` | Codex CLI 프롬프트 실행 |
118
118
  | `/optimize-analyze` | 번들 및 성능 분석 |
119
119
  | `/optimize-bundle` | 번들 크기 최적화 |
@@ -122,6 +122,7 @@ oh-my-customcode로 구동됩니다.
122
122
  | `/deep-plan` | 연구 검증 기반 계획 수립 (research → plan → verify) |
123
123
  | `/omcustom:sauron-watch` | 전체 R017 검증 |
124
124
  | `/structured-dev-cycle` | 6단계 구조적 개발 사이클 (Plan → Verify → Implement → Verify → Compound → Done) |
125
+ | `/omcustom:loop` | 백그라운드 에이전트 자동 계속 실행 |
125
126
  | `/omcustom:lists` | 모든 사용 가능한 커맨드 표시 |
126
127
  | `/omcustom:status` | 시스템 상태 표시 |
127
128
  | `/omcustom:help` | 도움말 표시 |
@@ -132,12 +133,12 @@ oh-my-customcode로 구동됩니다.
132
133
  project/
133
134
  +-- CLAUDE.md # 진입점
134
135
  +-- .claude/
135
- | +-- agents/ # 서브에이전트 정의 (44 파일)
136
- | +-- skills/ # 스킬 (78 디렉토리)
136
+ | +-- agents/ # 서브에이전트 정의 (45 파일)
137
+ | +-- skills/ # 스킬 (83 디렉토리)
137
138
  | +-- rules/ # 전역 규칙 (R000-R021)
138
139
  | +-- hooks/ # 훅 스크립트 (보안, 검증, HUD)
139
140
  | +-- contexts/ # 컨텍스트 파일 (ecomode)
140
- +-- guides/ # 레퍼런스 문서 (27 토픽)
141
+ +-- guides/ # 레퍼런스 문서 (28 토픽)
141
142
  ```
142
143
 
143
144
  ## 오케스트레이션
@@ -185,14 +186,14 @@ oh-my-customcode는 소프트웨어 컴파일과 동일한 구조를 따릅니
185
186
  | SW Engineer/Frontend | 4 | fe-vercel-agent, fe-vuejs-agent, fe-svelte-agent, fe-flutter-agent |
186
187
  | SW Engineer/Tooling | 3 | tool-npm-expert, tool-optimizer, tool-bun-expert |
187
188
  | DE Engineer | 6 | de-airflow-expert, de-dbt-expert, de-spark-expert, de-kafka-expert, de-snowflake-expert, de-pipeline-expert |
188
- | SW Engineer/Database | 3 | db-supabase-expert, db-postgres-expert, db-redis-expert |
189
+ | SW Engineer/Database | 4 | db-supabase-expert, db-postgres-expert, db-redis-expert, db-alembic-expert |
189
190
  | Security | 1 | sec-codeql-expert |
190
191
  | SW Architect | 2 | arch-documenter, arch-speckit-agent |
191
192
  | Infra Engineer | 2 | infra-docker-expert, infra-aws-expert |
192
193
  | QA Team | 3 | qa-planner, qa-writer, qa-engineer |
193
194
  | Manager | 6 | mgr-creator, mgr-updater, mgr-supplier, mgr-gitnerd, mgr-sauron, mgr-claude-code-bible |
194
195
  | System | 2 | sys-memory-keeper, sys-naggy |
195
- | **총계** | **44** | |
196
+ | **총계** | **45** | |
196
197
 
197
198
  ## Agent Teams (MUST when enabled)
198
199
 
@@ -100,7 +100,7 @@ NO EXCEPTIONS. NO EXCUSES.
100
100
  | `/omcustom:update-external` | Update agents from external sources |
101
101
  | `/omcustom:audit-agents` | Audit agent dependencies |
102
102
  | `/omcustom:fix-refs` | Fix broken references |
103
- | `/omcustom:takeover` | Extract canonical spec from existing agent/skill |
103
+ | `/omcustom-takeover` | Extract canonical spec from existing agent/skill |
104
104
  | `/dev-review` | Review code for best practices |
105
105
  | `/dev-refactor` | Refactor code |
106
106
  | `/memory-save` | Save session context to claude-mem |
@@ -109,7 +109,7 @@ NO EXCEPTIONS. NO EXCUSES.
109
109
  | `/omcustom:npm-publish` | Publish package to npm registry |
110
110
  | `/omcustom:npm-version` | Manage semantic versions |
111
111
  | `/omcustom:npm-audit` | Audit dependencies |
112
- | `/omcustom:release-notes` | Generate release notes from git history |
112
+ | `/omcustom-release-notes` | Generate release notes from git history |
113
113
  | `/codex-exec` | Execute Codex CLI prompts |
114
114
  | `/optimize-analyze` | Analyze bundle and performance |
115
115
  | `/optimize-bundle` | Optimize bundle size |
@@ -100,7 +100,7 @@ oh-my-customcode로 구동됩니다.
100
100
  | `/omcustom:update-external` | 외부 소스에서 에이전트 업데이트 |
101
101
  | `/omcustom:audit-agents` | 에이전트 의존성 감사 |
102
102
  | `/omcustom:fix-refs` | 깨진 참조 수정 |
103
- | `/omcustom:takeover` | 기존 에이전트/스킬에서 canonical spec 추출 |
103
+ | `/omcustom-takeover` | 기존 에이전트/스킬에서 canonical spec 추출 |
104
104
  | `/dev-review` | 코드 베스트 프랙티스 리뷰 |
105
105
  | `/dev-refactor` | 코드 리팩토링 |
106
106
  | `/memory-save` | 세션 컨텍스트를 claude-mem에 저장 |
@@ -109,7 +109,7 @@ oh-my-customcode로 구동됩니다.
109
109
  | `/omcustom:npm-publish` | npm 레지스트리에 패키지 배포 |
110
110
  | `/omcustom:npm-version` | 시맨틱 버전 관리 |
111
111
  | `/omcustom:npm-audit` | 의존성 감사 |
112
- | `/omcustom:release-notes` | 릴리즈 노트 생성 (git 히스토리 기반) |
112
+ | `/omcustom-release-notes` | 릴리즈 노트 생성 (git 히스토리 기반) |
113
113
  | `/codex-exec` | Codex CLI 프롬프트 실행 |
114
114
  | `/optimize-analyze` | 번들 및 성능 분석 |
115
115
  | `/optimize-bundle` | 번들 크기 최적화 |
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.46.1",
2
+ "version": "0.47.1",
3
3
  "lastUpdated": "2026-03-16T00:00:00.000Z",
4
4
  "components": [
5
5
  {
@@ -18,7 +18,7 @@
18
18
  "name": "skills",
19
19
  "path": ".claude/skills",
20
20
  "description": "Reusable skill modules (includes slash commands)",
21
- "files": 82
21
+ "files": 83
22
22
  },
23
23
  {
24
24
  "name": "guides",