oh-my-customcode 0.73.0 → 0.75.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/README.md CHANGED
@@ -21,17 +21,16 @@ npm install -g oh-my-customcode && cd your-project && omcustom init
21
21
 
22
22
  ---
23
23
 
24
- ## What's New in v0.62.5
24
+ ## What's New in v0.74.0
25
25
 
26
26
  | Feature | Description |
27
27
  |---------|-------------|
28
- | **D3 Dependency Graph** | Interactive force-directed graph visualization at `/graph` zoom, pan, drag, search, type filters |
29
- | **Playwright E2E Tests** | 11 accessibility tests with axe-core audit, `.pw.ts` extension for test isolation |
30
- | **Graph Accessibility** | WCAG keyboard navigation, aria-live announcements, skip link, focus-visible, reduced-motion support |
31
- | **CI Lockfile-Sync Gate** | New CI job validates bun.lockb consistency before lint/test |
32
- | **Token Optimization** | HTML comment technique reduces CLAUDE.md from 550→286 lines (48% reduction) |
33
- | **Workflow Engine** | YAML-defined workflow pipelines with `auto-dev` 8-step release batch |
34
- | **CC v2.1.83–v2.1.87 Compat** | Conditional hook `if` field, CwdChanged/FileChanged events, managed-settings.d |
28
+ | **`omcustom sync`** | Drift detection for `.claude/` configurationcompare against lockfile, export team snapshots |
29
+ | **`omcustom init --from-snapshot`** | Team reproducibility install from pre-configured snapshot directory |
30
+ | **`analysis --interview`** | Interactive AI architecture interview before file-based project detection |
31
+ | **skill-extractor** | 100th skill analyze task trajectories to propose reusable SKILL.md candidates |
32
+ | **User Model** | Structured tracking of correction patterns, skill preferences, expertise profile |
33
+ | **Release Cleanup** | Auto-close linked issues and delete release branches on PR merge |
35
34
 
36
35
  ---
37
36
 
@@ -153,13 +152,13 @@ Each agent declares its tools, model, memory scope, and limitations in YAML fron
153
152
  | Best Practices | 24 | Go, Python, TypeScript, Kotlin, Rust, React, FastAPI, Spring Boot, Django, Flutter, Docker, AWS, Postgres, Redis, Kafka, dbt, Spark, Snowflake, Airflow, pipeline-architecture-patterns, alembic, and more |
154
153
  | Routing | 4 | secretary, dev-lead, de-lead, qa-lead |
155
154
  | Workflow | 13 | structured-dev-cycle, deep-plan, research, evaluator-optimizer, dag-orchestration, worker-reviewer-pipeline, reasoning-sandwich, pipeline, and more |
156
- | Development | 7 | dev-review, dev-refactor, analysis, create-agent, intent-detection, web-design-guidelines, omcustom-takeover |
155
+ | Development | 8 | dev-review, dev-refactor, analysis, create-agent, intent-detection, web-design-guidelines, omcustom-takeover, skill-extractor |
157
156
  | Operations | 9 | update-docs, audit-agents, sauron-watch, monitoring-setup, fix-refs, release-notes, and more |
158
157
  | Memory | 3 | memory-save, memory-recall, memory-management |
159
158
  | Package | 3 | npm-publish, npm-version, npm-audit |
160
159
  | Optimization | 3 | optimize-analyze, optimize-bundle, optimize-report |
161
160
  | Security | 3 | adversarial-review, cve-triage, jinja2-prompts |
162
- | Other | 9 | codex-exec, claude-native, vercel-deploy, skills-sh-search, result-aggregation, writing-clearly-and-concisely, and more |
161
+ | Other | 10 | codex-exec, claude-native, vercel-deploy, skills-sh-search, result-aggregation, writing-clearly-and-concisely, and more |
163
162
 
164
163
  Skills use a 3-tier scope system: `core` (universal), `harness` (agent/skill maintenance), `package` (project-specific).
165
164
 
@@ -262,6 +261,10 @@ Security hooks are advisory (exit 0). They warn but never block.
262
261
  ```bash
263
262
  omcustom init # Interactive setup wizard (language, framework, team mode)
264
263
  omcustom init --lang ko # Initialize with Korean
264
+ omcustom init --from-snapshot # Install from pre-configured team snapshot
265
+ omcustom sync # Detect drift between .claude/ state and lockfile
266
+ omcustom sync --check # Check for drift without applying changes
267
+ omcustom sync --export # Export current state as team snapshot
265
268
  omcustom update # Update to latest
266
269
  omcustom list # List components
267
270
  omcustom doctor # Verify installation
package/dist/cli/index.js CHANGED
@@ -9325,7 +9325,7 @@ var init_package = __esm(() => {
9325
9325
  workspaces: [
9326
9326
  "packages/*"
9327
9327
  ],
9328
- version: "0.73.0",
9328
+ version: "0.75.0",
9329
9329
  description: "Batteries-included agent harness for Claude Code",
9330
9330
  type: "module",
9331
9331
  bin: {
@@ -11185,13 +11185,13 @@ var PromisePolyfill;
11185
11185
  var init_promise_polyfill = __esm(() => {
11186
11186
  PromisePolyfill = class PromisePolyfill extends Promise {
11187
11187
  static withResolver() {
11188
- let resolve2;
11188
+ let resolve3;
11189
11189
  let reject;
11190
11190
  const promise = new Promise((res, rej) => {
11191
- resolve2 = res;
11191
+ resolve3 = res;
11192
11192
  reject = rej;
11193
11193
  });
11194
- return { promise, resolve: resolve2, reject };
11194
+ return { promise, resolve: resolve3, reject };
11195
11195
  }
11196
11196
  };
11197
11197
  });
@@ -11229,7 +11229,7 @@ function createPrompt(view) {
11229
11229
  output
11230
11230
  });
11231
11231
  const screen = new ScreenManager(rl);
11232
- const { promise, resolve: resolve2, reject } = PromisePolyfill.withResolver();
11232
+ const { promise, resolve: resolve3, reject } = PromisePolyfill.withResolver();
11233
11233
  const cancel = () => reject(new CancelPromptError);
11234
11234
  if (signal) {
11235
11235
  const abort = () => reject(new AbortPromptError({ cause: signal.reason }));
@@ -11257,7 +11257,7 @@ function createPrompt(view) {
11257
11257
  cycle(() => {
11258
11258
  try {
11259
11259
  const nextView = view(config, (value) => {
11260
- setImmediate(() => resolve2(value));
11260
+ setImmediate(() => resolve3(value));
11261
11261
  });
11262
11262
  if (nextView === undefined) {
11263
11263
  const callerFilename = callSites[1]?.getFileName();
@@ -17122,7 +17122,7 @@ var require_lib2 = __commonJS((exports) => {
17122
17122
  return matches;
17123
17123
  };
17124
17124
  exports.analyse = analyse;
17125
- var detectFile = (filepath, opts = {}) => new Promise((resolve2, reject) => {
17125
+ var detectFile = (filepath, opts = {}) => new Promise((resolve3, reject) => {
17126
17126
  let fd;
17127
17127
  const fs3 = (0, node_1.default)();
17128
17128
  const handler = (err, buffer) => {
@@ -17132,7 +17132,7 @@ var require_lib2 = __commonJS((exports) => {
17132
17132
  if (err) {
17133
17133
  reject(err);
17134
17134
  } else if (buffer) {
17135
- resolve2((0, exports.detect)(buffer));
17135
+ resolve3((0, exports.detect)(buffer));
17136
17136
  } else {
17137
17137
  reject(new Error("No error and no buffer received"));
17138
17138
  }
@@ -24757,6 +24757,9 @@ var en_default = {
24757
24757
  initializingTemplate: "Initializing with template: {{name}}",
24758
24758
  promptOverwrite: "Configuration already exists. Overwrite?",
24759
24759
  aborted: "Initialization aborted",
24760
+ snapshot: {
24761
+ installing: "Installing from team snapshot..."
24762
+ },
24760
24763
  wizard: {
24761
24764
  welcome: "Welcome to oh-my-customcode setup!",
24762
24765
  langPrompt: "Select your preferred language",
@@ -24970,6 +24973,9 @@ var en_default = {
24970
24973
  serveStop: "[Deprecated] `omcustom serve-stop` is deprecated. Use `omcustom web stop` instead."
24971
24974
  }
24972
24975
  },
24976
+ sync: {
24977
+ description: "Check .claude/ configuration drift or export snapshot"
24978
+ },
24973
24979
  security: {
24974
24980
  description: "Scan for security issues in hooks, configs, and templates",
24975
24981
  verboseOption: "Show detailed scan results",
@@ -25164,6 +25170,9 @@ var ko_default = {
25164
25170
  initializingTemplate: "템플릿으로 초기화 중: {{name}}",
25165
25171
  promptOverwrite: "설정 파일이 이미 존재합니다. 덮어쓰시겠습니까?",
25166
25172
  aborted: "초기화가 취소되었습니다",
25173
+ snapshot: {
25174
+ installing: "팀 스냅샷에서 설치 중..."
25175
+ },
25167
25176
  wizard: {
25168
25177
  welcome: "oh-my-customcode 설정을 시작합니다!",
25169
25178
  langPrompt: "사용할 언어를 선택하세요",
@@ -25377,6 +25386,9 @@ var ko_default = {
25377
25386
  serveStop: "[Deprecated] `omcustom serve-stop` is deprecated. Use `omcustom web stop` instead."
25378
25387
  }
25379
25388
  },
25389
+ sync: {
25390
+ description: ".claude/ 설정 드리프트 확인 또는 스냅샷 내보내기"
25391
+ },
25380
25392
  security: {
25381
25393
  description: "훅, 설정, 템플릿의 보안 문제 검사",
25382
25394
  verboseOption: "상세 검사 결과 표시",
@@ -26491,6 +26503,29 @@ async function generateAndWriteLockfileForDir(targetDir) {
26491
26503
  return { fileCount: 0, warning: `Lockfile generation failed: ${msg}` };
26492
26504
  }
26493
26505
  }
26506
+ function diffLockfiles(base, current) {
26507
+ const baseKeys = new Set(Object.keys(base.files));
26508
+ const currentKeys = new Set(Object.keys(current.files));
26509
+ const added = [];
26510
+ const removed = [];
26511
+ const modified = [];
26512
+ const unchanged = [];
26513
+ for (const key of currentKeys) {
26514
+ if (!baseKeys.has(key)) {
26515
+ added.push(key);
26516
+ } else if (base.files[key].templateHash !== current.files[key].templateHash) {
26517
+ modified.push(key);
26518
+ } else {
26519
+ unchanged.push(key);
26520
+ }
26521
+ }
26522
+ for (const key of baseKeys) {
26523
+ if (!currentKeys.has(key)) {
26524
+ removed.push(key);
26525
+ }
26526
+ }
26527
+ return { added, removed, modified, unchanged };
26528
+ }
26494
26529
 
26495
26530
  // src/core/rtk-installer.ts
26496
26531
  import { execSync as execSync4 } from "node:child_process";
@@ -27209,7 +27244,7 @@ async function doctorCommand(options = {}) {
27209
27244
 
27210
27245
  // src/cli/init.ts
27211
27246
  init_package();
27212
- import { join as join10 } from "node:path";
27247
+ import { join as join11 } from "node:path";
27213
27248
 
27214
27249
  // src/core/installer.ts
27215
27250
  init_fs();
@@ -28111,6 +28146,82 @@ async function checkUvAvailable() {
28111
28146
  }
28112
28147
  }
28113
28148
 
28149
+ // src/core/snapshot.ts
28150
+ init_package();
28151
+ init_projects();
28152
+ import { existsSync as existsSync2 } from "node:fs";
28153
+ import { copyFile as copyFile2, cp } from "node:fs/promises";
28154
+ import { join as join10 } from "node:path";
28155
+ init_fs();
28156
+ async function checkExistingInstallation(targetDir) {
28157
+ const layout = getProviderLayout();
28158
+ const rootDir = join10(targetDir, layout.rootDir);
28159
+ return fileExists(rootDir);
28160
+ }
28161
+ async function installFromSnapshot(targetDir, snapshotPath, options) {
28162
+ if (!existsSync2(snapshotPath)) {
28163
+ return {
28164
+ success: false,
28165
+ message: i18n.t("cli.init.failed"),
28166
+ errors: [`Snapshot path not found: ${snapshotPath}`]
28167
+ };
28168
+ }
28169
+ const layout = getProviderLayout();
28170
+ const snapshotClaude = join10(snapshotPath, layout.rootDir);
28171
+ if (!existsSync2(snapshotClaude)) {
28172
+ return {
28173
+ success: false,
28174
+ message: i18n.t("cli.init.failed"),
28175
+ errors: [`Invalid snapshot: missing ${layout.rootDir}/ directory in ${snapshotPath}`]
28176
+ };
28177
+ }
28178
+ console.log(`Installing from snapshot: ${snapshotPath}`);
28179
+ try {
28180
+ const exists2 = await checkExistingInstallation(targetDir);
28181
+ if (exists2 && !options.force) {
28182
+ console.log(i18n.t("cli.init.exists", { rootDir: layout.rootDir }));
28183
+ console.log(i18n.t("cli.init.backing_up"));
28184
+ const backupDir = join10(targetDir, `.claude-backup-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, -1)}`);
28185
+ await cp(join10(targetDir, layout.rootDir), backupDir, { recursive: true });
28186
+ console.log(` Backed up to: ${backupDir}`);
28187
+ }
28188
+ await cp(snapshotClaude, join10(targetDir, layout.rootDir), {
28189
+ recursive: true,
28190
+ force: true
28191
+ });
28192
+ const snapshotGuides = join10(snapshotPath, "guides");
28193
+ if (existsSync2(snapshotGuides)) {
28194
+ await cp(snapshotGuides, join10(targetDir, "guides"), {
28195
+ recursive: true,
28196
+ force: true
28197
+ });
28198
+ }
28199
+ const snapshotEntry = join10(snapshotPath, layout.entryFile);
28200
+ if (existsSync2(snapshotEntry)) {
28201
+ await copyFile2(snapshotEntry, join10(targetDir, layout.entryFile));
28202
+ }
28203
+ try {
28204
+ const existing = await readLockFile(targetDir);
28205
+ await writeLockFile(targetDir, package_default.version, existing);
28206
+ } catch {}
28207
+ console.log(i18n.t("cli.init.success"));
28208
+ console.log(`
28209
+ Installed from snapshot: ${snapshotPath}`);
28210
+ return {
28211
+ success: true,
28212
+ message: `Installed from snapshot: ${snapshotPath}`
28213
+ };
28214
+ } catch (error2) {
28215
+ const errorMessage = error2 instanceof Error ? error2.message : String(error2);
28216
+ console.error(i18n.t("cli.init.failed"), errorMessage);
28217
+ return {
28218
+ success: false,
28219
+ message: i18n.t("cli.init.failed"),
28220
+ errors: [errorMessage]
28221
+ };
28222
+ }
28223
+ }
28224
+
28114
28225
  // src/cli/init.ts
28115
28226
  init_fs();
28116
28227
  init_projects();
@@ -29090,9 +29201,9 @@ async function runInitWizard(options) {
29090
29201
  }
29091
29202
 
29092
29203
  // src/cli/init.ts
29093
- async function checkExistingInstallation(targetDir) {
29204
+ async function checkExistingInstallation2(targetDir) {
29094
29205
  const layout = getProviderLayout();
29095
- const rootDir = join10(targetDir, layout.rootDir);
29206
+ const rootDir = join11(targetDir, layout.rootDir);
29096
29207
  return fileExists(rootDir);
29097
29208
  }
29098
29209
  var PROVIDER_SUBDIR_COMPONENTS = new Set([
@@ -29106,13 +29217,13 @@ var PROVIDER_SUBDIR_COMPONENTS = new Set([
29106
29217
  function componentToPath(targetDir, component) {
29107
29218
  if (component === "entry-md") {
29108
29219
  const layout = getProviderLayout();
29109
- return join10(targetDir, layout.entryFile);
29220
+ return join11(targetDir, layout.entryFile);
29110
29221
  }
29111
29222
  if (PROVIDER_SUBDIR_COMPONENTS.has(component)) {
29112
29223
  const layout = getProviderLayout();
29113
- return join10(targetDir, layout.rootDir, component);
29224
+ return join11(targetDir, layout.rootDir, component);
29114
29225
  }
29115
- return join10(targetDir, component);
29226
+ return join11(targetDir, component);
29116
29227
  }
29117
29228
  function buildInstalledPaths(targetDir, components) {
29118
29229
  return components.map((component) => componentToPath(targetDir, component));
@@ -29178,6 +29289,9 @@ async function setupMcpConfig(targetDir) {
29178
29289
  }
29179
29290
  async function initCommand(options) {
29180
29291
  const targetDir = process.cwd();
29292
+ if (options.fromSnapshot) {
29293
+ return installFromSnapshot(targetDir, options.fromSnapshot, options);
29294
+ }
29181
29295
  const resolved = await resolveOptions(options);
29182
29296
  if (!resolved) {
29183
29297
  return { success: false, message: i18n.t("cli.init.wizard.cancelled") };
@@ -29185,7 +29299,7 @@ async function initCommand(options) {
29185
29299
  console.log(i18n.t("cli.init.start"));
29186
29300
  try {
29187
29301
  const layout = getProviderLayout();
29188
- const exists2 = await checkExistingInstallation(targetDir);
29302
+ const exists2 = await checkExistingInstallation2(targetDir);
29189
29303
  if (exists2) {
29190
29304
  console.log(i18n.t("cli.init.exists", { rootDir: layout.rootDir }));
29191
29305
  console.log(i18n.t("cli.init.backing_up"));
@@ -29231,7 +29345,7 @@ async function initCommand(options) {
29231
29345
  }
29232
29346
 
29233
29347
  // src/cli/list.ts
29234
- import { basename as basename4, dirname as dirname4, join as join11, relative as relative3 } from "node:path";
29348
+ import { basename as basename4, dirname as dirname4, join as join12, relative as relative3 } from "node:path";
29235
29349
  init_fs();
29236
29350
  var ALLOWED_TOP_LEVEL_KEYS = new Set(["name", "type", "description", "version", "category"]);
29237
29351
  function parseKeyValue(line) {
@@ -29296,12 +29410,12 @@ function extractAgentTypeFromFilename(filename) {
29296
29410
  return prefixMap[prefix] || "unknown";
29297
29411
  }
29298
29412
  function extractSkillCategoryFromPath(skillPath, baseDir, rootDir) {
29299
- const relativePath = relative3(join11(baseDir, rootDir, "skills"), skillPath);
29413
+ const relativePath = relative3(join12(baseDir, rootDir, "skills"), skillPath);
29300
29414
  const parts = relativePath.split("/").filter(Boolean);
29301
29415
  return parts[0] || "unknown";
29302
29416
  }
29303
29417
  function extractGuideCategoryFromPath(guidePath, baseDir) {
29304
- const relativePath = relative3(join11(baseDir, "guides"), guidePath);
29418
+ const relativePath = relative3(join12(baseDir, "guides"), guidePath);
29305
29419
  const parts = relativePath.split("/").filter(Boolean);
29306
29420
  return parts[0] || "unknown";
29307
29421
  }
@@ -29395,7 +29509,7 @@ async function tryExtractMarkdownDescription(mdPath, options = {}) {
29395
29509
  }
29396
29510
  }
29397
29511
  async function getAgents(targetDir, rootDir = ".claude", config) {
29398
- const agentsDir = join11(targetDir, rootDir, "agents");
29512
+ const agentsDir = join12(targetDir, rootDir, "agents");
29399
29513
  if (!await fileExists(agentsDir))
29400
29514
  return [];
29401
29515
  try {
@@ -29423,7 +29537,7 @@ async function getAgents(targetDir, rootDir = ".claude", config) {
29423
29537
  }
29424
29538
  }
29425
29539
  async function getSkills(targetDir, rootDir = ".claude", config) {
29426
- const skillsDir = join11(targetDir, rootDir, "skills");
29540
+ const skillsDir = join12(targetDir, rootDir, "skills");
29427
29541
  if (!await fileExists(skillsDir))
29428
29542
  return [];
29429
29543
  try {
@@ -29433,7 +29547,7 @@ async function getSkills(targetDir, rootDir = ".claude", config) {
29433
29547
  const skillMdFiles = await listFiles(skillsDir, { recursive: true, pattern: "SKILL.md" });
29434
29548
  const skills = await Promise.all(skillMdFiles.map(async (skillMdPath) => {
29435
29549
  const skillDir = dirname4(skillMdPath);
29436
- const indexYamlPath = join11(skillDir, "index.yaml");
29550
+ const indexYamlPath = join12(skillDir, "index.yaml");
29437
29551
  const { description, version } = await tryReadIndexYamlMetadata(indexYamlPath);
29438
29552
  const relativePath = relative3(targetDir, skillDir);
29439
29553
  return {
@@ -29452,7 +29566,7 @@ async function getSkills(targetDir, rootDir = ".claude", config) {
29452
29566
  }
29453
29567
  }
29454
29568
  async function getGuides(targetDir, config) {
29455
- const guidesDir = join11(targetDir, "guides");
29569
+ const guidesDir = join12(targetDir, "guides");
29456
29570
  if (!await fileExists(guidesDir))
29457
29571
  return [];
29458
29572
  try {
@@ -29479,7 +29593,7 @@ async function getGuides(targetDir, config) {
29479
29593
  }
29480
29594
  var RULE_PRIORITY_ORDER = { MUST: 0, SHOULD: 1, MAY: 2 };
29481
29595
  async function getRules(targetDir, rootDir = ".claude", config) {
29482
- const rulesDir = join11(targetDir, rootDir, "rules");
29596
+ const rulesDir = join12(targetDir, rootDir, "rules");
29483
29597
  if (!await fileExists(rulesDir))
29484
29598
  return [];
29485
29599
  try {
@@ -29551,7 +29665,7 @@ function formatAsJson(components) {
29551
29665
  console.log(JSON.stringify(components, null, 2));
29552
29666
  }
29553
29667
  async function getHooks(targetDir, rootDir = ".claude") {
29554
- const hooksDir = join11(targetDir, rootDir, "hooks");
29668
+ const hooksDir = join12(targetDir, rootDir, "hooks");
29555
29669
  if (!await fileExists(hooksDir))
29556
29670
  return [];
29557
29671
  try {
@@ -29569,7 +29683,7 @@ async function getHooks(targetDir, rootDir = ".claude") {
29569
29683
  }
29570
29684
  }
29571
29685
  async function getContexts(targetDir, rootDir = ".claude") {
29572
- const contextsDir = join11(targetDir, rootDir, "contexts");
29686
+ const contextsDir = join12(targetDir, rootDir, "contexts");
29573
29687
  if (!await fileExists(contextsDir))
29574
29688
  return [];
29575
29689
  try {
@@ -29962,22 +30076,22 @@ async function securityCommand(_options = {}) {
29962
30076
 
29963
30077
  // src/cli/serve-commands.ts
29964
30078
  import { spawnSync as spawnSync2 } from "node:child_process";
29965
- import { join as join13 } from "node:path";
30079
+ import { join as join14 } from "node:path";
29966
30080
 
29967
30081
  // src/cli/serve.ts
29968
30082
  import { spawn } from "node:child_process";
29969
- import { existsSync as existsSync2 } from "node:fs";
30083
+ import { existsSync as existsSync3 } from "node:fs";
29970
30084
  import { readFile as readFile2, unlink, writeFile as writeFile2 } from "node:fs/promises";
29971
- import { join as join12 } from "node:path";
30085
+ import { join as join13 } from "node:path";
29972
30086
  var DEFAULT_PORT = 4321;
29973
- var PID_FILE = join12(process.env.HOME ?? "~", ".omcustom-serve.pid");
30087
+ var PID_FILE = join13(process.env.HOME ?? "~", ".omcustom-serve.pid");
29974
30088
  function findServeBuildDir(projectRoot, options) {
29975
- const localBuild = join12(projectRoot, "packages", "serve", "build");
29976
- if (existsSync2(join12(localBuild, "index.js")))
30089
+ const localBuild = join13(projectRoot, "packages", "serve", "build");
30090
+ if (existsSync3(join13(localBuild, "index.js")))
29977
30091
  return localBuild;
29978
30092
  if (options?.skipNpmFallback !== true) {
29979
- const npmBuild = join12(import.meta.dirname, "..", "..", "packages", "serve", "build");
29980
- if (existsSync2(join12(npmBuild, "index.js")))
30093
+ const npmBuild = join13(import.meta.dirname, "..", "..", "packages", "serve", "build");
30094
+ if (existsSync3(join13(npmBuild, "index.js")))
29981
30095
  return npmBuild;
29982
30096
  }
29983
30097
  return null;
@@ -30005,7 +30119,7 @@ async function startServeBackground(projectRoot, port = DEFAULT_PORT, buildDirOp
30005
30119
  if (buildDir === null) {
30006
30120
  return;
30007
30121
  }
30008
- const child = spawn("node", [join12(buildDir, "index.js")], {
30122
+ const child = spawn("node", [join13(buildDir, "index.js")], {
30009
30123
  env: {
30010
30124
  ...process.env,
30011
30125
  OMCUSTOM_PORT: String(port),
@@ -30082,7 +30196,7 @@ function runForeground(projectRoot, port, buildDirOpts) {
30082
30196
  process.exit(1);
30083
30197
  }
30084
30198
  console.log(`Web UI: http://localhost:${port}`);
30085
- spawnSync2("node", [join13(buildDir, "index.js")], {
30199
+ spawnSync2("node", [join14(buildDir, "index.js")], {
30086
30200
  env: {
30087
30201
  ...process.env,
30088
30202
  OMCUSTOM_PORT: String(port),
@@ -30094,12 +30208,199 @@ function runForeground(projectRoot, port, buildDirOpts) {
30094
30208
  });
30095
30209
  }
30096
30210
 
30211
+ // src/cli/sync.ts
30212
+ import { resolve as resolve2 } from "node:path";
30213
+
30214
+ // src/core/sync.ts
30215
+ init_fs();
30216
+ import { existsSync as existsSync4 } from "node:fs";
30217
+ import { cp as cp2, mkdir } from "node:fs/promises";
30218
+ import { join as join15 } from "node:path";
30219
+ async function loadVersions() {
30220
+ try {
30221
+ const packageRoot = getPackageRoot();
30222
+ const manifest = await readJsonFile(join15(packageRoot, "templates", "manifest.json"));
30223
+ const pkg = await readJsonFile(join15(packageRoot, "package.json"));
30224
+ return { generatorVersion: pkg.version, templateVersion: manifest.version };
30225
+ } catch {
30226
+ return { generatorVersion: "0.0.0", templateVersion: "0.0.0" };
30227
+ }
30228
+ }
30229
+ async function generateCurrentLockfile(targetDir) {
30230
+ try {
30231
+ const { generatorVersion, templateVersion } = await loadVersions();
30232
+ return await generateLockfile(targetDir, generatorVersion, templateVersion);
30233
+ } catch {
30234
+ return null;
30235
+ }
30236
+ }
30237
+ async function syncCheck(targetDir, options) {
30238
+ const empty = {
30239
+ inSync: false,
30240
+ added: [],
30241
+ removed: [],
30242
+ modified: [],
30243
+ unchanged: 0,
30244
+ referenceVersion: null,
30245
+ currentVersion: null,
30246
+ totalTracked: 0
30247
+ };
30248
+ const referenceDir = options?.reference ?? targetDir;
30249
+ const reference = await readLockfile(referenceDir);
30250
+ if (!reference) {
30251
+ return empty;
30252
+ }
30253
+ const current = await generateCurrentLockfile(targetDir);
30254
+ if (!current) {
30255
+ return {
30256
+ ...empty,
30257
+ referenceVersion: reference.generatorVersion
30258
+ };
30259
+ }
30260
+ const diff = diffLockfiles(reference, current);
30261
+ return {
30262
+ inSync: diff.added.length === 0 && diff.removed.length === 0 && diff.modified.length === 0,
30263
+ added: diff.added,
30264
+ removed: diff.removed,
30265
+ modified: diff.modified,
30266
+ unchanged: diff.unchanged.length,
30267
+ referenceVersion: reference.generatorVersion,
30268
+ currentVersion: current.generatorVersion,
30269
+ totalTracked: Object.keys(current.files).length
30270
+ };
30271
+ }
30272
+ function isExportable(src) {
30273
+ const normalized = src.replace(/\\/g, "/");
30274
+ const excluded = ["/agent-memory", "/agent-memory-local", "/outputs", "settings.local"];
30275
+ return !excluded.some((segment) => {
30276
+ if (segment.startsWith("/")) {
30277
+ return normalized.includes(`${segment}/`) || normalized.endsWith(segment);
30278
+ }
30279
+ return normalized.includes(segment);
30280
+ });
30281
+ }
30282
+ async function countFiles(dir2) {
30283
+ const { readdir: readdir3, stat: stat3 } = await import("node:fs/promises");
30284
+ async function walk(current) {
30285
+ let total = 0;
30286
+ let entries;
30287
+ try {
30288
+ entries = await readdir3(current);
30289
+ } catch {
30290
+ return 0;
30291
+ }
30292
+ for (const entry of entries) {
30293
+ const full = join15(current, entry);
30294
+ try {
30295
+ const s = await stat3(full);
30296
+ if (s.isDirectory()) {
30297
+ total += await walk(full);
30298
+ } else if (s.isFile()) {
30299
+ total += 1;
30300
+ }
30301
+ } catch {}
30302
+ }
30303
+ return total;
30304
+ }
30305
+ return walk(dir2);
30306
+ }
30307
+ async function exportSnapshot(targetDir, outputPath) {
30308
+ const claudeDir = join15(targetDir, ".claude");
30309
+ const guidesDir = join15(targetDir, "guides");
30310
+ if (!existsSync4(claudeDir)) {
30311
+ return { success: false, exportPath: outputPath, fileCount: 0 };
30312
+ }
30313
+ await mkdir(outputPath, { recursive: true });
30314
+ const destClaude = join15(outputPath, ".claude");
30315
+ await cp2(claudeDir, destClaude, {
30316
+ recursive: true,
30317
+ filter: isExportable
30318
+ });
30319
+ if (existsSync4(guidesDir)) {
30320
+ await cp2(guidesDir, join15(outputPath, "guides"), { recursive: true });
30321
+ }
30322
+ const lockfile = await generateCurrentLockfile(targetDir);
30323
+ if (lockfile) {
30324
+ await writeLockfile(outputPath, lockfile);
30325
+ }
30326
+ const fileCount = await countFiles(outputPath);
30327
+ return { success: true, exportPath: outputPath, fileCount };
30328
+ }
30329
+
30330
+ // src/cli/sync.ts
30331
+ async function runExport(targetDir, outputPath) {
30332
+ const result = await exportSnapshot(targetDir, resolve2(outputPath));
30333
+ if (!result.success) {
30334
+ console.error(`
30335
+ Export failed — no .claude/ directory found in current project.`);
30336
+ process.exit(1);
30337
+ }
30338
+ console.log(`
30339
+ Snapshot exported: ${result.exportPath} (${result.fileCount} files)`);
30340
+ console.log(`Team members can install with: omcustom init --from-snapshot ${result.exportPath}`);
30341
+ }
30342
+ function printDriftDetails(result) {
30343
+ if (result.unchanged > 0) {
30344
+ console.log(` ✓ ${result.unchanged} files in sync`);
30345
+ }
30346
+ if (result.modified.length > 0) {
30347
+ console.log(` ⚠ ${result.modified.length} files modified since install:`);
30348
+ for (const f of result.modified) {
30349
+ console.log(` modified: ${f}`);
30350
+ }
30351
+ }
30352
+ if (result.removed.length > 0) {
30353
+ console.log(` ✗ ${result.removed.length} files removed:`);
30354
+ for (const f of result.removed) {
30355
+ console.log(` removed: ${f}`);
30356
+ }
30357
+ }
30358
+ if (result.added.length > 0) {
30359
+ console.log(` + ${result.added.length} files added (not in lockfile):`);
30360
+ for (const f of result.added) {
30361
+ console.log(` added: ${f}`);
30362
+ }
30363
+ }
30364
+ }
30365
+ async function runCheck(targetDir, options) {
30366
+ const result = await syncCheck(targetDir, { reference: options.reference });
30367
+ if (!result.referenceVersion) {
30368
+ console.error(`
30369
+ No lockfile found. Run omcustom init first.`);
30370
+ process.exit(1);
30371
+ }
30372
+ const label = options.reference ? `external snapshot at ${options.reference}` : `lockfile (v${result.referenceVersion})`;
30373
+ console.log(`
30374
+ Sync check — comparing against ${label}
30375
+ `);
30376
+ if (result.inSync) {
30377
+ console.log(` ✓ ${result.unchanged} files in sync`);
30378
+ } else {
30379
+ printDriftDetails(result);
30380
+ }
30381
+ console.log(`
30382
+ Summary: ${result.unchanged} unchanged, ${result.modified.length} modified, ${result.removed.length} removed, ${result.added.length} added`);
30383
+ if (!result.inSync) {
30384
+ process.exit(1);
30385
+ }
30386
+ }
30387
+ function syncCommand(program2) {
30388
+ program2.command("sync").description(i18n.t("cli.sync.description")).option("--check", "Compare current state against lockfile (default behavior)").option("--reference <path>", "Compare against an external snapshot instead of the lockfile").option("--export <path>", "Export current .claude/ state as a reusable snapshot").action(async (options) => {
30389
+ const targetDir = resolve2(".");
30390
+ if (options.export) {
30391
+ await runExport(targetDir, options.export);
30392
+ return;
30393
+ }
30394
+ await runCheck(targetDir, options);
30395
+ });
30396
+ }
30397
+
30097
30398
  // src/cli/update.ts
30098
30399
  init_package();
30099
30400
 
30100
30401
  // src/core/updater.ts
30101
30402
  init_package();
30102
- import { join as join14 } from "node:path";
30403
+ import { join as join16 } from "node:path";
30103
30404
  init_fs();
30104
30405
 
30105
30406
  // src/core/entry-merger.ts
@@ -30354,7 +30655,7 @@ function resolveCustomizations(customizations, configPreserveFiles, targetDir) {
30354
30655
  }
30355
30656
  async function updateEntryDoc(targetDir, config, options) {
30356
30657
  const layout = getProviderLayout();
30357
- const entryPath = join14(targetDir, layout.entryFile);
30658
+ const entryPath = join16(targetDir, layout.entryFile);
30358
30659
  const templateName = getEntryTemplateName2(config.language);
30359
30660
  const templatePath = resolveTemplatePath(templateName);
30360
30661
  if (!await fileExists(templatePath)) {
@@ -30455,7 +30756,7 @@ async function update(options) {
30455
30756
  result.error = `Downgrade prevented: project has v${result.previousVersion} but CLI is v${cliVersion}. Update the CLI first: npm install -g oh-my-customcode@latest`;
30456
30757
  return result;
30457
30758
  }
30458
- const targetPkgPath = join14(options.targetDir, "package.json");
30759
+ const targetPkgPath = join16(options.targetDir, "package.json");
30459
30760
  if (await fileExists(targetPkgPath)) {
30460
30761
  const targetPkg = await readJsonFile(targetPkgPath);
30461
30762
  if (targetPkg.name === "oh-my-customcode") {
@@ -30571,11 +30872,11 @@ async function collectProtectedSkipPaths(srcPath, destPath, componentPath, force
30571
30872
  const warnedPaths = [];
30572
30873
  const updatedPaths = [];
30573
30874
  for (const p of protectedRelative) {
30574
- const targetFilePath = join14(targetDir, componentPath, p);
30875
+ const targetFilePath = join16(targetDir, componentPath, p);
30575
30876
  const lockfileKey = `${componentPath}/${p}`.replace(/\\/g, "/");
30576
30877
  const shouldSkip = await shouldSkipProtectedFile(targetFilePath, lockfileKey, lockfile);
30577
30878
  if (shouldSkip) {
30578
- skipPaths.push(path3.relative(destPath, join14(destPath, p)));
30879
+ skipPaths.push(path3.relative(destPath, join16(destPath, p)));
30579
30880
  warnedPaths.push(p);
30580
30881
  } else {
30581
30882
  updatedPaths.push(p);
@@ -30621,7 +30922,7 @@ async function updateComponent(targetDir, component, customizations, options, co
30621
30922
  const preservedFiles = [];
30622
30923
  const componentPath = getComponentPath2(component);
30623
30924
  const srcPath = resolveTemplatePath(componentPath);
30624
- const destPath = join14(targetDir, componentPath);
30925
+ const destPath = join16(targetDir, componentPath);
30625
30926
  const customComponents = config.customComponents || [];
30626
30927
  const skipPaths = [];
30627
30928
  if (customizations && !options.forceOverwriteAll) {
@@ -30663,7 +30964,7 @@ async function updateComponent(targetDir, component, customizations, options, co
30663
30964
  }
30664
30965
  skipPaths.push(...protectedSkipPaths);
30665
30966
  const path3 = await import("node:path");
30666
- const normalizedSkipPaths = skipPaths.map((p) => path3.relative(destPath, join14(targetDir, p)));
30967
+ const normalizedSkipPaths = skipPaths.map((p) => path3.relative(destPath, join16(targetDir, p)));
30667
30968
  const uniqueSkipPaths = [...new Set(normalizedSkipPaths)];
30668
30969
  await copyDirectory(srcPath, destPath, {
30669
30970
  overwrite: true,
@@ -30685,12 +30986,12 @@ async function syncRootLevelFiles(targetDir, options) {
30685
30986
  const layout = getProviderLayout();
30686
30987
  const synced = [];
30687
30988
  for (const fileName of ROOT_LEVEL_FILES) {
30688
- const srcPath = resolveTemplatePath(join14(layout.rootDir, fileName));
30989
+ const srcPath = resolveTemplatePath(join16(layout.rootDir, fileName));
30689
30990
  if (!await fileExists(srcPath)) {
30690
30991
  continue;
30691
30992
  }
30692
- const destPath = join14(targetDir, layout.rootDir, fileName);
30693
- await ensureDirectory(join14(destPath, ".."));
30993
+ const destPath = join16(targetDir, layout.rootDir, fileName);
30994
+ await ensureDirectory(join16(destPath, ".."));
30694
30995
  await fs3.copyFile(srcPath, destPath);
30695
30996
  if (fileName.endsWith(".sh")) {
30696
30997
  await fs3.chmod(destPath, 493);
@@ -30725,7 +31026,7 @@ async function removeDeprecatedFiles(targetDir, options) {
30725
31026
  });
30726
31027
  continue;
30727
31028
  }
30728
- const fullPath = join14(targetDir, entry.path);
31029
+ const fullPath = join16(targetDir, entry.path);
30729
31030
  if (await fileExists(fullPath)) {
30730
31031
  await fs3.unlink(fullPath);
30731
31032
  removed.push(entry.path);
@@ -30766,7 +31067,7 @@ async function syncNamespaceInFile(targetFilePath, upstreamFilePath) {
30766
31067
  async function processNamespaceSyncEntry(entry, relPath, fullSrcPath, destPath, componentPath, lockfile) {
30767
31068
  if (!entry.isFile() || !entry.name.endsWith(".md"))
30768
31069
  return null;
30769
- const targetFilePath = join14(destPath, relPath);
31070
+ const targetFilePath = join16(destPath, relPath);
30770
31071
  const lockfileKey = `${componentPath}/${relPath}`.replace(/\\/g, "/");
30771
31072
  const shouldSkip = await shouldSkipProtectedFile(targetFilePath, lockfileKey, lockfile);
30772
31073
  if (shouldSkip)
@@ -30781,7 +31082,7 @@ async function applyNamespaceSync(targetDir, component, lockfile) {
30781
31082
  return [];
30782
31083
  const componentPath = getComponentPath2(component);
30783
31084
  const srcPath = resolveTemplatePath(componentPath);
30784
- const destPath = join14(targetDir, componentPath);
31085
+ const destPath = join16(targetDir, componentPath);
30785
31086
  const fs3 = await import("node:fs/promises");
30786
31087
  const synced = [];
30787
31088
  const queue = [{ dir: srcPath, relDir: "" }];
@@ -30795,7 +31096,7 @@ async function applyNamespaceSync(targetDir, component, lockfile) {
30795
31096
  }
30796
31097
  for (const entry of entries) {
30797
31098
  const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
30798
- const fullSrcPath = join14(dir2, entry.name);
31099
+ const fullSrcPath = join16(dir2, entry.name);
30799
31100
  if (entry.isDirectory()) {
30800
31101
  queue.push({ dir: fullSrcPath, relDir: relPath });
30801
31102
  continue;
@@ -30818,26 +31119,26 @@ function getComponentPath2(component) {
30818
31119
  }
30819
31120
  async function backupInstallation(targetDir) {
30820
31121
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
30821
- const backupDir = join14(targetDir, `.omcustom-backup-${timestamp}`);
31122
+ const backupDir = join16(targetDir, `.omcustom-backup-${timestamp}`);
30822
31123
  const fs3 = await import("node:fs/promises");
30823
31124
  await ensureDirectory(backupDir);
30824
31125
  const layout = getProviderLayout();
30825
31126
  const dirsToBackup = [layout.rootDir, "guides"];
30826
31127
  for (const dir2 of dirsToBackup) {
30827
- const srcPath = join14(targetDir, dir2);
31128
+ const srcPath = join16(targetDir, dir2);
30828
31129
  if (await fileExists(srcPath)) {
30829
- const destPath = join14(backupDir, dir2);
31130
+ const destPath = join16(backupDir, dir2);
30830
31131
  await copyDirectory(srcPath, destPath, { overwrite: true });
30831
31132
  }
30832
31133
  }
30833
- const entryPath = join14(targetDir, layout.entryFile);
31134
+ const entryPath = join16(targetDir, layout.entryFile);
30834
31135
  if (await fileExists(entryPath)) {
30835
- await fs3.copyFile(entryPath, join14(backupDir, layout.entryFile));
31136
+ await fs3.copyFile(entryPath, join16(backupDir, layout.entryFile));
30836
31137
  }
30837
31138
  return backupDir;
30838
31139
  }
30839
31140
  async function loadCustomizationManifest(targetDir) {
30840
- const manifestPath = join14(targetDir, CUSTOMIZATION_MANIFEST_FILE);
31141
+ const manifestPath = join16(targetDir, CUSTOMIZATION_MANIFEST_FILE);
30841
31142
  if (await fileExists(manifestPath)) {
30842
31143
  return readJsonFile(manifestPath);
30843
31144
  }
@@ -31078,7 +31379,7 @@ var packageJson = require2("../../package.json");
31078
31379
  function createProgram() {
31079
31380
  const program2 = new Command;
31080
31381
  program2.name("omcustom").description(i18n.t("cli.description")).version(packageJson.version, "-v, --version", i18n.t("cli.versionOption")).option("--skip-version-check", "Skip CLI version pre-flight check");
31081
- program2.command("init").description(i18n.t("cli.init.description")).option("-l, --lang <language>", i18n.t("cli.init.langOption")).option("--domain <domain>", "Install only agents/skills for specific domain (backend, frontend, data-engineering, devops)").option("--yes", "Skip interactive wizard, use defaults").action(async (options) => {
31382
+ program2.command("init").description(i18n.t("cli.init.description")).option("-l, --lang <language>", i18n.t("cli.init.langOption")).option("--domain <domain>", "Install only agents/skills for specific domain (backend, frontend, data-engineering, devops)").option("--yes", "Skip interactive wizard, use defaults").option("--from-snapshot <path>", "Install from a pre-configured team snapshot directory").action(async (options) => {
31082
31383
  await initCommand(options);
31083
31384
  });
31084
31385
  program2.command("update").description(i18n.t("cli.update.description")).option("--dry-run", i18n.t("cli.update.dryRunOption")).option("--force", i18n.t("cli.update.forceOption")).option("--force-overwrite-all", i18n.t("cli.update.forceOverwriteAllOption")).option("--hard", i18n.t("cli.update.hardOption")).option("--backup", i18n.t("cli.update.backupOption")).option("--agents", i18n.t("cli.update.agentsOption")).option("--skills", i18n.t("cli.update.skillsOption")).option("--rules", i18n.t("cli.update.rulesOption")).option("--guides", i18n.t("cli.update.guidesOption")).option("--hooks", i18n.t("cli.update.hooksOption")).option("--contexts", i18n.t("cli.update.contextsOption")).option("--all", i18n.t("cli.update.allOption")).action(async (options) => {
@@ -31090,6 +31391,7 @@ function createProgram() {
31090
31391
  verbose: options.verbose
31091
31392
  });
31092
31393
  });
31394
+ syncCommand(program2);
31093
31395
  program2.command("doctor").description(i18n.t("cli.doctor.description")).option("--fix", i18n.t("cli.doctor.fixOption")).option("--updates", i18n.t("cli.doctor.updatesOption")).action(async (options) => {
31094
31396
  await doctorCommand(options);
31095
31397
  });
package/dist/index.js CHANGED
@@ -1820,7 +1820,7 @@ var package_default = {
1820
1820
  workspaces: [
1821
1821
  "packages/*"
1822
1822
  ],
1823
- version: "0.73.0",
1823
+ version: "0.75.0",
1824
1824
  description: "Batteries-included agent harness for Claude Code",
1825
1825
  type: "module",
1826
1826
  bin: {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "workspaces": [
4
4
  "packages/*"
5
5
  ],
6
- "version": "0.73.0",
6
+ "version": "0.75.0",
7
7
  "description": "Batteries-included agent harness for Claude Code",
8
8
  "type": "module",
9
9
  "bin": {
@@ -2,7 +2,7 @@
2
2
  name: omcustom:analysis
3
3
  description: Analyze project and auto-configure agents, skills, rules, and guides
4
4
  scope: harness
5
- argument-hint: "[--dry-run] [--verbose]"
5
+ argument-hint: "[target-dir] [--interview]"
6
6
  user-invocable: true
7
7
  ---
8
8
 
@@ -13,12 +13,52 @@ Scan a project's tech stack, compare against installed agents/skills, and auto-c
13
13
  ## Options
14
14
 
15
15
  ```
16
- --dry-run Show what would be added without making changes
17
- --verbose Show detailed detection reasoning
16
+ --dry-run Show what would be added without making changes
17
+ --verbose Show detailed detection reasoning
18
+ --interview, -i Run interactive architecture interview before file-based detection
18
19
  ```
19
20
 
20
21
  ## Workflow
21
22
 
23
+ ### Step 0: Architecture Interview (--interview only)
24
+
25
+ When `--interview` flag is provided, conduct an interactive AI interview before file-based detection. This captures human context that file scanning cannot determine.
26
+
27
+ **Interview flow** (sequential, AI-guided):
28
+
29
+ 1. **프로젝트 유형**: "이 프로젝트는 어떤 종류입니까?"
30
+ → 옵션: web app, REST API, CLI tool, library, monorepo, data pipeline, mobile app
31
+
32
+ 2. **아키텍처 패턴**: "어떤 아키텍처를 따르고 있습니까?"
33
+ → 옵션: microservices, monolith, serverless, event-driven, layered, hexagonal
34
+
35
+ 3. **주요 언어**: "주로 사용하는 프로그래밍 언어는?"
36
+ → 자유 입력, 알려진 에이전트와 매칭
37
+
38
+ 4. **배포 대상**: "어디에 배포합니까?"
39
+ → 옵션: AWS, GCP, Azure, Vercel, on-premises, Docker/K8s, edge
40
+
41
+ 5. **팀 우선순위**: "팀의 주요 관심사는?"
42
+ → 옵션: performance, security, developer experience, cost, scalability
43
+
44
+ **Interview results feed into Step 1 as weighted detection hints:**
45
+ - File evidence + interview agreement = `confidence: high`
46
+ - File evidence only = `confidence: medium` (unchanged from current)
47
+ - Interview only (no file evidence) = `confidence: suggested`
48
+
49
+ **Integration with report:**
50
+ ```
51
+ Interview Insights (--interview):
52
+ Project type: REST API (user-specified, confirmed by file scan)
53
+ Architecture: microservices (user-specified)
54
+ Deployment: AWS + Docker (confirmed by file scan)
55
+ Team focus: security → sec-codeql-expert [suggested]
56
+
57
+ Suggested (from interview, no file evidence):
58
+ ~ sec-codeql-expert [suggested — no CodeQL config found]
59
+ ~ de-kafka-expert [suggested — no kafka deps found]
60
+ ```
61
+
22
62
  ### Step 1: Project Scan
23
63
 
24
64
  Detect tech stack by checking indicator files and dependency manifests.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.73.0",
2
+ "version": "0.75.0",
3
3
  "lastUpdated": "2026-03-24T00:00:00.000Z",
4
4
  "components": [
5
5
  {
@@ -7,16 +7,18 @@ mode: auto
7
7
  error: halt-and-report
8
8
 
9
9
  steps:
10
- - name: pre-triage
11
- skill: professor-triage
12
- description: Run professor-triage on open issues that lack verify-done label
13
- condition: "open issues without label:verify-done exist"
14
-
15
- - name: triage
16
- skill: professor-triage
17
- description: Analyze verify-done issues against current codebase and perform automated triage
10
+ - name: issue-analysis
11
+ parallel:
12
+ - name: pre-triage
13
+ skill: professor-triage
14
+ description: Run professor-triage on open issues that lack verify-done label
15
+ condition: "open issues without label:verify-done exist"
16
+ - name: triage
17
+ skill: professor-triage
18
+ description: Analyze verify-done issues against current codebase and perform automated triage
18
19
 
19
20
  - name: plan
21
+ depends_on: issue-analysis
20
22
  skill: release-plan
21
23
  description: Group triaged issues into release units by priority and size
22
24
  input: triage-results