@cheongfish/bouncer 0.0.0-stage → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +31 -2
  3. package/bin/bouncer +26 -0
  4. package/bin/bouncer-root +25 -0
  5. package/bin/build-stamp.js +78 -0
  6. package/bin/register-path.js +71 -0
  7. package/dist/.build-stamp +1 -0
  8. package/dist/approval-snapshot.js +99 -0
  9. package/dist/bouncer-root.js +80 -0
  10. package/dist/canonical-json.js +37 -0
  11. package/dist/cli-commit-guard-command.js +74 -0
  12. package/dist/cli-config-command.js +47 -0
  13. package/dist/cli-context-review-command.js +273 -0
  14. package/dist/cli-current-command.js +374 -0
  15. package/dist/cli-dispatch-command.js +119 -0
  16. package/dist/cli-doc-commands.js +352 -0
  17. package/dist/cli-epic-command.js +130 -0
  18. package/dist/cli-flags.js +50 -0
  19. package/dist/cli-git-commands.js +931 -0
  20. package/dist/cli-intent-command.js +491 -0
  21. package/dist/cli-metrics-command.js +134 -0
  22. package/dist/cli-ontology-command.js +109 -0
  23. package/dist/cli-project-commands.js +580 -0
  24. package/dist/cli-retro-command.js +138 -0
  25. package/dist/cli-review-command.js +251 -0
  26. package/dist/cli-review-dispatch-command.js +338 -0
  27. package/dist/cli-usage.js +72 -0
  28. package/dist/cli.js +70 -0
  29. package/dist/codex-agents.js +189 -0
  30. package/dist/command-argv.js +125 -0
  31. package/dist/commit-guard.js +72 -0
  32. package/dist/commit-hook.js +470 -0
  33. package/dist/commit-message.js +77 -0
  34. package/dist/commit-sha.js +107 -0
  35. package/dist/commit.js +519 -0
  36. package/dist/composite-gates.js +129 -0
  37. package/dist/comprehension.js +127 -0
  38. package/dist/config.js +298 -0
  39. package/dist/context-ids.js +288 -0
  40. package/dist/context-migration-rewrite.js +421 -0
  41. package/dist/context-ontology.js +746 -0
  42. package/dist/context-review-contracts.js +2 -0
  43. package/dist/context-review-projection.js +147 -0
  44. package/dist/context-review-state.js +824 -0
  45. package/dist/context-review-store.js +602 -0
  46. package/dist/context-review-transitions.js +412 -0
  47. package/dist/coordinate-advance.js +253 -0
  48. package/dist/coordinate-next.js +998 -0
  49. package/dist/coordinate-output.js +57 -0
  50. package/dist/coordinator-command-contracts.js +2 -0
  51. package/dist/coordinator-commands.js +1908 -0
  52. package/dist/coordinator-fanin.js +1021 -0
  53. package/dist/coordinator-git.js +121 -0
  54. package/dist/coordinator-input.js +37 -0
  55. package/dist/coordinator-ledger.js +381 -0
  56. package/dist/coordinator-prepare.js +1047 -0
  57. package/dist/coordinator-repair.js +1416 -0
  58. package/dist/coordinator-verification.js +42 -0
  59. package/dist/coordinator-worker.js +388 -0
  60. package/dist/coordinator.js +7 -0
  61. package/dist/cross-drive.js +85 -0
  62. package/dist/current.js +1148 -0
  63. package/dist/dag-shape.js +93 -0
  64. package/dist/doc-shape-contracts.js +241 -0
  65. package/dist/drive-metrics.js +691 -0
  66. package/dist/epic-index.js +347 -0
  67. package/dist/epic-registry.js +453 -0
  68. package/dist/execute-prepare.js +262 -0
  69. package/dist/execution-contracts.js +46 -0
  70. package/dist/execution-dag.js +148 -0
  71. package/dist/finalize-checks.js +177 -0
  72. package/dist/finalize-collect.js +343 -0
  73. package/dist/finalize-comprehension.js +116 -0
  74. package/dist/finalize-digest-collect.js +388 -0
  75. package/dist/finalize-digest-model.js +595 -0
  76. package/dist/finalize-digest.js +164 -0
  77. package/dist/finalize-explain.js +176 -0
  78. package/dist/finalize-pr.js +610 -0
  79. package/dist/finalize-release-main.js +652 -0
  80. package/dist/finalize.js +714 -0
  81. package/dist/frontmatter.js +22 -0
  82. package/dist/git-ops.js +49 -0
  83. package/dist/graph-exec.js +179 -0
  84. package/dist/graph-scope.js +268 -0
  85. package/dist/graphify.js +995 -0
  86. package/dist/guards.js +82 -0
  87. package/dist/import-git.js +78 -0
  88. package/dist/import-history.js +392 -0
  89. package/dist/import-render.js +59 -0
  90. package/dist/import-types.js +2 -0
  91. package/dist/init.js +571 -0
  92. package/dist/intent-bundle.js +936 -0
  93. package/dist/intent-provenance.js +927 -0
  94. package/dist/layout.js +131 -0
  95. package/dist/lease.js +145 -0
  96. package/dist/legacy-comments.js +71 -0
  97. package/dist/migrate-context-slugs.js +262 -0
  98. package/dist/migrate-merge-legacy-epics.js +392 -0
  99. package/dist/migrate-review-scope.js +144 -0
  100. package/dist/migrate-task-layout.js +194 -0
  101. package/dist/path-overlap.js +38 -0
  102. package/dist/paths.js +45 -0
  103. package/dist/plan-approve.js +218 -0
  104. package/dist/plan-inspect.js +575 -0
  105. package/dist/plan-prerequisites.js +197 -0
  106. package/dist/plan-snapshot-contract.js +98 -0
  107. package/dist/plan-snapshot.js +138 -0
  108. package/dist/plugin-root.js +237 -0
  109. package/dist/posix-path.js +14 -0
  110. package/dist/pre-commit-hook.js +170 -0
  111. package/dist/print-dispatch.js +285 -0
  112. package/dist/render.js +8 -0
  113. package/dist/retention-migration.js +595 -0
  114. package/dist/retro-read.js +259 -0
  115. package/dist/retro-sources.js +318 -0
  116. package/dist/review-dispatch.js +1054 -0
  117. package/dist/review-policy.js +28 -0
  118. package/dist/review-record.js +447 -0
  119. package/dist/review-repair-state.js +52 -0
  120. package/dist/review-snapshot.js +659 -0
  121. package/dist/run-detach.js +165 -0
  122. package/dist/run-preflight.js +368 -0
  123. package/dist/runtime-state.js +1038 -0
  124. package/dist/scaffold.js +533 -0
  125. package/dist/schema.js +294 -0
  126. package/dist/scope.js +1073 -0
  127. package/dist/seed-worktree.js +926 -0
  128. package/dist/session-graph.js +337 -0
  129. package/dist/subagents.js +86 -0
  130. package/dist/symbol-index.js +1390 -0
  131. package/dist/task-brief-hash.js +34 -0
  132. package/dist/task-commits.js +124 -0
  133. package/dist/tasks-docs.js +192 -0
  134. package/dist/templates.js +469 -0
  135. package/dist/time.js +21 -0
  136. package/dist/validate-docs.js +302 -0
  137. package/dist/validate-gates-finalize.js +303 -0
  138. package/dist/validate-gates-plan.js +720 -0
  139. package/dist/validate-gates-run.js +420 -0
  140. package/dist/validate-gates-shared.js +124 -0
  141. package/dist/validate-gates.js +87 -0
  142. package/dist/validate-sections.js +654 -0
  143. package/dist/validate-structural.js +347 -0
  144. package/dist/validate.js +417 -0
  145. package/dist/verification-contracts.js +26 -0
  146. package/dist/verification-evidence.js +233 -0
  147. package/dist/verification-execution.js +163 -0
  148. package/dist/verification-identity.js +413 -0
  149. package/dist/verification-target.js +51 -0
  150. package/dist/verification.js +383 -0
  151. package/dist/windows-npm.js +38 -0
  152. package/package.json +32 -4
  153. package/resources/graphify-compat.json +9 -0
  154. package/vendor/README.md +23 -0
  155. package/vendor/js-yaml.LICENSE +21 -0
  156. package/vendor/js-yaml.js +3022 -0
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+ const path = require("node:path");
3
+ /**
4
+ * ontology 명령의 argv를 검증하고, 유효한 입력에서만 메모리 색인을 만든다.
5
+ * 잘못된 argv는 core 적재 전에 exit 2로 반환하고, 코어 입력/I/O 오류는 각각
6
+ * 2/1로 전달하며 예상하지 않은 예외는 호출자에게 다시 던진다.
7
+ *
8
+ * @param {string[]} rest - `ontology` 뒤의 모드와 옵션
9
+ * @param {CliIo} io - stdout/stderr 싱크
10
+ * @returns {number} 명령 종료 코드
11
+ */
12
+ function run(rest, io) {
13
+ const parsed = parse(rest);
14
+ if ('error' in parsed) {
15
+ io.err(`ontology: ${parsed.error}\n`);
16
+ return 2;
17
+ }
18
+ const core = require('./context-ontology');
19
+ let index;
20
+ try {
21
+ index = core.loadContextOntology({ repoRoot: parsed.repoRoot });
22
+ }
23
+ catch (error) {
24
+ return reportCoreError(error, io);
25
+ }
26
+ try {
27
+ const result = core.queryContextOntology(index, {
28
+ mode: parsed.mode,
29
+ value: parsed.value,
30
+ limit: parsed.limit,
31
+ });
32
+ io.out(`${JSON.stringify(result)}\n`);
33
+ return 0;
34
+ }
35
+ catch (error) {
36
+ return reportCoreError(error, io);
37
+ }
38
+ }
39
+ /**
40
+ * 단일 selector와 공통 옵션을 검증하고 repo 경로를 절대 경로로 정규화한다.
41
+ *
42
+ * @param {string[]} args - ontology mode와 option 인자
43
+ * @returns {Parsed | { error: string }} 성공한 질의 또는 사용자 입력 오류
44
+ */
45
+ function parse(args) {
46
+ const mode = args[0];
47
+ if (mode !== 'entity' && mode !== 'file' && mode !== 'impact') {
48
+ return { error: 'expected entity, file, or impact mode' };
49
+ }
50
+ const selector = mode === 'entity' ? '--entity' : '--file';
51
+ let value;
52
+ let repo;
53
+ let limit = 20;
54
+ const seen = new Set();
55
+ for (let i = 1; i < args.length; i++) {
56
+ const option = args[i];
57
+ if (option !== selector && option !== '--repo' && option !== '--limit') {
58
+ return { error: `unknown or inapplicable option: ${option}` };
59
+ }
60
+ if (seen.has(option))
61
+ return { error: `${option} may be specified only once` };
62
+ seen.add(option);
63
+ const next = args[++i];
64
+ if (next === undefined || next === '' || (option !== selector && next.startsWith('--'))) {
65
+ return { error: `${option} requires a value` };
66
+ }
67
+ if (option === selector)
68
+ value = next;
69
+ else if (option === '--repo')
70
+ repo = next;
71
+ else {
72
+ if (!/^\d+$/.test(next) ||
73
+ !Number.isSafeInteger(Number(next)) ||
74
+ Number(next) < 1 ||
75
+ Number(next) > 100)
76
+ return { error: '--limit must be an integer from 1 to 100' };
77
+ limit = Number(next);
78
+ }
79
+ }
80
+ if (!value)
81
+ return { error: `${selector} is required` };
82
+ const hasNonAscii = [...value].some((char) => char.charCodeAt(0) > 0x7f);
83
+ if (mode === 'entity' && (/[\s]/.test(value) || hasNonAscii)) {
84
+ return { error: '--entity must be a non-empty ASCII token without whitespace' };
85
+ }
86
+ if (mode !== 'entity' &&
87
+ (!value || value.includes('\0') || value.includes('\\') || value.startsWith('/') ||
88
+ value.split('/').includes('..') || /[*?{}[\]]/.test(value))) {
89
+ return { error: '--file must be a safe repository-relative path' };
90
+ }
91
+ // 상대 경로는 process cwd 기준이다. 코어가 Git root를 검증하므로 하위 경로를 승격하지 않는다.
92
+ return { mode, value, repoRoot: path.resolve(repo ?? process.cwd()), limit };
93
+ }
94
+ /**
95
+ * 코어가 식별한 입력/I/O 오류만 CLI 종료 코드로 바꾸고 나머지는 보존한다.
96
+ *
97
+ * @param {unknown} error - 코어에서 발생한 예외
98
+ * @param {CliIo} io - stderr 출력 싱크
99
+ * @returns {number} 처리한 입력 오류 2 또는 I/O 오류 1
100
+ */
101
+ function reportCoreError(error, io) {
102
+ const code = error && typeof error === 'object' ? error.code : undefined;
103
+ if (code !== 'ontology-input-invalid' && code !== 'ontology-io-failed')
104
+ throw error;
105
+ // 코어는 두 오류만 코드로 보장한다. 그 외 예외를 삼키면 버그가 입력 오류로 위장된다.
106
+ io.err(`ontology: ${error.message}\n`);
107
+ return code === 'ontology-input-invalid' ? 2 : 1;
108
+ }
109
+ module.exports = { run };
@@ -0,0 +1,580 @@
1
+ 'use strict';
2
+ const cliUsage = require("./cli-usage");
3
+ function cmdInit(rest, io) {
4
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
5
+ const { parseFlags } = require('./cli-flags');
6
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
7
+ const { init } = require('./init');
8
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
9
+ const { nowIsoKst } = require('./time');
10
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
11
+ const { syncSessionGraphs } = require('./session-graph');
12
+ const f = parseFlags(rest);
13
+ const timestamp = typeof f.timestamp === 'string' ? f.timestamp : nowIsoKst();
14
+ // CLI 기본은 설치 on — 라이브러리 init() 기본(install:false)과 의도적으로 다르다.
15
+ // 테스트·프로그래밍 호출이 네트워크 pip을 타지 않게 라이브러리는 opt-in.
16
+ const install = f['no-graphify'] !== true;
17
+ const repoRoot = (f.repo || process.cwd());
18
+ // 파일을 쓰기 전에 거절한다 — 잘못된 값이 반쯤 쓴 .bouncer/를 남기지 않게.
19
+ const profile = f.profile === undefined ? 'standard' : f.profile;
20
+ if (profile !== 'standard' && profile !== 'domain-only') {
21
+ io.err('init: --profile must be standard or domain-only\n');
22
+ return 2;
23
+ }
24
+ const limitRaw = f['domain-epic-limit'];
25
+ if (limitRaw !== undefined && (typeof limitRaw !== 'string' || !/^\d+$/.test(limitRaw))) {
26
+ io.err('init: --domain-epic-limit must be a non-negative integer\n');
27
+ return 2;
28
+ }
29
+ const domainEpicLimit = limitRaw === undefined ? 0 : Number(limitRaw);
30
+ const result = init({
31
+ repoRoot,
32
+ timestamp,
33
+ profile,
34
+ domainEpicLimit,
35
+ graphify: {
36
+ install,
37
+ // --upgrade-graphify만 이 rebuild를 탄다. 기본 sync는 mtime skip-fresh라
38
+ // 패키지 schema만 바뀐 승격이 옛 graph.json을 stamp하는 것으로 끝난다.
39
+ // force여도 skip-version-incompatible·skip-graph-disabled 등은 failed[]
40
+ // 없이 돌아온다. 그 경우를 성공으로 치면 안 되는 검사는 upgradeGraphify가 한다.
41
+ rebuild: () => syncSessionGraphs({ repoRoot, force: true }),
42
+ },
43
+ promote: f['promote-graphify'] === true,
44
+ upgradeGraphify: f['upgrade-graphify'] === true,
45
+ writeGitignore: f['write-gitignore'] === true,
46
+ seedCodexAgents: f['seed-codex-agents'] === true,
47
+ preCommitHook: f['pre-commit-hook'] === true,
48
+ });
49
+ io.out(`${JSON.stringify(Object.assign({ ok: true }, result), null, 2)}\n`);
50
+ // created/skipped와 무관하게 result.ok만 본다. 부분 성공을 0으로 위장하지 않음.
51
+ return result.ok ? 0 : 1;
52
+ }
53
+ function cmdGraphSync(rest, io) {
54
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
55
+ const { parseFlags } = require('./cli-flags');
56
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
57
+ const { syncSessionGraphs, graphSyncWarnings } = require('./session-graph');
58
+ const f = parseFlags(rest);
59
+ const result = syncSessionGraphs({ repoRoot: (f.repo || process.cwd()) });
60
+ // missing은 상태가 아니라 오류가 아니다(그래프 부재). failed만 종료 코드를 가른다.
61
+ // warnings는 SessionStart 훅이 stderr에 그대로 쓰는 줄이다. 문구 정본을 graphSyncWarnings
62
+ // 한 곳에 두려고 훅이 아니라 여기서 계산해 JSON으로 넘긴다(끝 개행 포함).
63
+ const warnings = graphSyncWarnings(result);
64
+ io.out(`${JSON.stringify({ ok: result.failed.length === 0, ...result, warnings }, null, 2)}\n`);
65
+ return result.failed.length === 0 ? 0 : 1;
66
+ }
67
+ /**
68
+ * 첫 intent 실행에서만 전용 CommonJS command를 적재한다. usage 문자열은 여기
69
+ * 레지스트리에 남겨 help가 projectCommands만으로도 조립되게 하고,
70
+ * parser·resolver는 cli-intent-command 쪽으로 미룬다.
71
+ *
72
+ * @param {string[]} rest - `intent` 뒤 argv
73
+ * @param {CliIo} io - stdout/stderr 싱크
74
+ * @returns {number} 전용 handler의 종료 코드
75
+ */
76
+ function cmdIntentLazy(rest, io) {
77
+ const intentCommand = require('./cli-intent-command');
78
+ return intentCommand.run(rest, io);
79
+ }
80
+ /**
81
+ * ontology parser와 색인을 첫 명령 실행까지 지연한다. help 구성은 registry usage만
82
+ * 읽으므로 전체 CLI와 다른 project command에서 검색 코어를 적재하지 않는다.
83
+ *
84
+ * @param {string[]} rest - `ontology` 뒤의 모드와 옵션
85
+ * @param {CliIo} io - stdout/stderr 싱크
86
+ * @returns {number} ontology 전용 handler의 종료 코드
87
+ */
88
+ function cmdOntologyLazy(rest, io) {
89
+ const command = require('./cli-ontology-command');
90
+ return command.run(rest, io);
91
+ }
92
+ function cmdGraphifyBin(rest, io) {
93
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
94
+ const { parseFlags } = require('./cli-flags');
95
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
96
+ const { resolveGraphifyBin } = require('./graphify');
97
+ const f = parseFlags(rest);
98
+ const repoRoot = (f.repo || process.cwd());
99
+ const { bin } = resolveGraphifyBin({ repoRoot });
100
+ if (!bin) {
101
+ // stdout은 pipe-clean 유지 — 실패 사유는 stderr만.
102
+ io.err('graphify-bin: graphify executable not found (config.bin, venv, or PATH)\n');
103
+ // 환경 문제이지 사용법 오류가 아니므로 2가 아니라 1.
104
+ return 1;
105
+ }
106
+ io.out(`${bin}\n`);
107
+ return 0;
108
+ }
109
+ /**
110
+ * `--agent`가 NAMED_AGENTS 원소인지 검사한다. 목록을 핸들러마다 복제하면
111
+ * 거절 문구의 허용 이름이 seed 목록과 어긋난다.
112
+ *
113
+ * @param {Record<string, string | boolean>} flags - parseFlags 결과
114
+ * @param {string} command - stderr 접두 (subagent-model | codex-agents)
115
+ * @returns {{ error: string } | { agent: string }} 실패면 error, 성공이면 agent
116
+ */
117
+ function requireNamedAgent(flags, command) {
118
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
119
+ const { NAMED_AGENTS } = require('./codex-agents');
120
+ const allowed = NAMED_AGENTS.join(', ');
121
+ const agent = flags.agent;
122
+ // parseFlags는 값 없는 --agent를 true로 둔다. 빈 문자열과 같이 누락으로 본다.
123
+ if (typeof agent !== 'string' || agent.length === 0) {
124
+ return { error: `${command}: --agent <name> is required (${allowed})\n` };
125
+ }
126
+ if (!NAMED_AGENTS.includes(agent)) {
127
+ return { error: `${command}: unknown agent ${agent} (allowed: ${allowed})\n` };
128
+ }
129
+ return { agent };
130
+ }
131
+ /**
132
+ * 설정된 named-agent model slug를 한 줄로 낸다. JSON `{model, provider}`를
133
+ * 내면 지침이 provider를 dispatch 인자로 넘기는 경로가 생긴다.
134
+ *
135
+ * @param {string[]} rest - `subagent-model` 뒤 argv
136
+ * @param {CliIo} io - stdout/stderr 싱크
137
+ * @returns {number} 성공 0, 사용법 2
138
+ */
139
+ function cmdSubagentModel(rest, io) {
140
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
141
+ const { parseFlags } = require('./cli-flags');
142
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
143
+ const { resolveSubagentModel } = require('./subagents');
144
+ const f = parseFlags(rest);
145
+ const named = requireNamedAgent(f, 'subagent-model');
146
+ if ('error' in named) {
147
+ io.err(named.error);
148
+ return 2;
149
+ }
150
+ const provider = typeof f.provider === 'string' && f.provider.length > 0
151
+ ? f.provider
152
+ : undefined;
153
+ const result = resolveSubagentModel({
154
+ repoRoot: (f.repo || process.cwd()),
155
+ agentName: named.agent,
156
+ provider,
157
+ });
158
+ // null·inherit·비문자열은 라이브러리가 model:null로 수렴한다. stdout은
159
+ // inherit sentinel만 주어 호출자가 모델 인자를 생략하게 한다.
160
+ io.out(`${result.model ?? 'inherit'}\n`);
161
+ return 0;
162
+ }
163
+ /**
164
+ * 로컬 Codex TOML이 생성본과 같은지 JSON으로 보고한다. check 외 동사는
165
+ * 사용법(2)이고, 불일치는 런타임(1)이다. 파일을 쓰지 않는다.
166
+ *
167
+ * @param {string[]} rest - `codex-agents` 뒤 argv
168
+ * @param {CliIo} io - stdout/stderr 싱크
169
+ * @returns {number} 동기 0, 불일치 1, 사용법 2
170
+ */
171
+ function cmdCodexAgents(rest, io) {
172
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
173
+ const { parseFlags } = require('./cli-flags');
174
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
175
+ const { checkCodexAgent } = require('./codex-agents');
176
+ const command = rest[0];
177
+ // seed는 init --seed-codex-agents가 소유한다. 여기서 받으면 읽기 명령이
178
+ // 쓰기로 오해되고, 사용자 소유 TOML을 덮을 우회가 생긴다.
179
+ if (command !== 'check') {
180
+ io.err('codex-agents: command must be check\n');
181
+ return 2;
182
+ }
183
+ const f = parseFlags(rest.slice(1));
184
+ const named = requireNamedAgent(f, 'codex-agents');
185
+ if ('error' in named) {
186
+ io.err(named.error);
187
+ return 2;
188
+ }
189
+ const checked = checkCodexAgent({
190
+ repoRoot: (f.repo || process.cwd()),
191
+ agentName: named.agent,
192
+ });
193
+ if (checked.ok) {
194
+ io.out(`${JSON.stringify({
195
+ ok: true,
196
+ agent: named.agent,
197
+ in_sync: true,
198
+ path: checked.path,
199
+ })}\n`);
200
+ return 0;
201
+ }
202
+ io.out(`${JSON.stringify({
203
+ ok: false,
204
+ agent: named.agent,
205
+ in_sync: false,
206
+ reason: checked.reason,
207
+ next: 'bouncer init --seed-codex-agents',
208
+ path: checked.path,
209
+ })}\n`);
210
+ return 1;
211
+ }
212
+ function cmdProjectRoot(rest, io) {
213
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
214
+ const { parseFlags } = require('./cli-flags');
215
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
216
+ const { runtimePaths } = require('./runtime-state');
217
+ const f = parseFlags(rest);
218
+ const repoRoot = (f.repo || process.cwd());
219
+ // 현재 워크플로가 소비하는 정본은 main worktree다. linked cwd나
220
+ // plugin root로 대체하면 도그푸드 기준을 오독하므로 unavailable은
221
+ // 빈 stdout/cwd fallback 없이 stderr+1로 거절한다.
222
+ const paths = runtimePaths({ repoRoot });
223
+ if (paths.unavailable || !paths.projectRoot) {
224
+ io.err(`project-root: ${paths.reason || 'Bouncer requires a Git repository'}\n`);
225
+ return 1;
226
+ }
227
+ io.out(`${paths.projectRoot}\n`);
228
+ return 0;
229
+ }
230
+ /**
231
+ * `run preflight`와 `run detach`를 받는다. 정규화 JSON을 stdout에 내고, 거절은 같은
232
+ * 채널의 `{ok:false}`와 종료 코드 1이다. 알 수 없는 서브커맨드는 사용법(2).
233
+ * detach는 사용자 승인(`--yes`)을 detachRun이 확인한다 — 여기서 승인을 추정하지 않는다.
234
+ *
235
+ * @param {string[]} rest - 서브커맨드와 플래그
236
+ * @param {CliIo} io - stdout/stderr 싱크
237
+ * @returns {number} 성공 0, 거절 1, 사용법 2
238
+ */
239
+ function cmdRun(rest, io) {
240
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
241
+ const { catchMessage } = require('./guards');
242
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
243
+ const { parseFlags } = require('./cli-flags');
244
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
245
+ const { runPreflight } = require('./run-preflight');
246
+ const command = rest[0];
247
+ const f = parseFlags(rest.slice(1));
248
+ // preflight·detach만 공개한다. 다른 서브커맨드를 받으면 사용법(2) — 런타임 거절(1)과
249
+ // 구분해, 없는 동사를 drive 쓰기로 착각하지 않게 한다.
250
+ if (command !== 'preflight' && command !== 'detach') {
251
+ io.err('run: command must be preflight|detach\n');
252
+ return 2;
253
+ }
254
+ if (typeof f.blueprint !== 'string' || f.blueprint === '') {
255
+ io.err('run: --blueprint is required\n');
256
+ return 2;
257
+ }
258
+ if (command === 'detach') {
259
+ try {
260
+ const { detachRun } = require('./run-detach');
261
+ const result = detachRun({
262
+ repoRoot: (f.repo || process.cwd()),
263
+ blueprint: f.blueprint,
264
+ reason: typeof f.reason === 'string' ? f.reason : undefined,
265
+ yes: f.yes === true,
266
+ });
267
+ io.out(`${JSON.stringify(result, null, 2)}\n`);
268
+ return result.ok ? 0 : 1;
269
+ }
270
+ catch (error) {
271
+ io.err(`run detach: ${catchMessage(error)}\n`);
272
+ return 1;
273
+ }
274
+ }
275
+ try {
276
+ const result = runPreflight({
277
+ repoRoot: (f.repo || process.cwd()),
278
+ blueprintDir: f.blueprint,
279
+ });
280
+ io.out(`${JSON.stringify(result, null, 2)}\n`);
281
+ return result.ok ? 0 : 1;
282
+ }
283
+ catch (error) {
284
+ io.err(`run preflight: ${catchMessage(error)}\n`);
285
+ return 1;
286
+ }
287
+ }
288
+ /**
289
+ * migrate retention 전용 argv. parseFlags는 중복·알 수 없는 option을 조용히
290
+ * 삼키므로, --apply/--blueprint 조합을 여기서 닫아 부분 적용을 막는다.
291
+ *
292
+ * @param {string[]} flagArgs - `migrate retention` 뒤 argv
293
+ * @returns {{ error?: string, apply: boolean, blueprint: string | null, repo?: string }}
294
+ */
295
+ function parseRetentionMigrateArgs(flagArgs) {
296
+ let apply = false;
297
+ let applySeen = false;
298
+ let blueprint = null;
299
+ let blueprintSeen = false;
300
+ let repo;
301
+ let repoSeen = false;
302
+ const fail = (message) => ({
303
+ error: `migrate retention: ${message}\n`,
304
+ apply,
305
+ blueprint,
306
+ repo,
307
+ });
308
+ for (let i = 0; i < flagArgs.length; i += 1) {
309
+ const token = flagArgs[i];
310
+ if (token === '--apply') {
311
+ if (applySeen)
312
+ return fail('duplicate option: --apply');
313
+ applySeen = true;
314
+ apply = true;
315
+ continue;
316
+ }
317
+ if (token === '--blueprint') {
318
+ if (blueprintSeen)
319
+ return fail('duplicate option: --blueprint');
320
+ blueprintSeen = true;
321
+ const value = flagArgs[++i];
322
+ if (value === undefined || value.startsWith('--') || value.trim() === '') {
323
+ return fail('--blueprint requires a directory');
324
+ }
325
+ blueprint = value.trim();
326
+ continue;
327
+ }
328
+ if (token === '--repo') {
329
+ if (repoSeen)
330
+ return fail('duplicate option: --repo');
331
+ repoSeen = true;
332
+ const value = flagArgs[++i];
333
+ if (!value || value.startsWith('--'))
334
+ return fail('--repo requires a directory');
335
+ repo = value;
336
+ continue;
337
+ }
338
+ if (token.startsWith('--'))
339
+ return fail(`unknown option: ${token}`);
340
+ return fail(`unexpected argument: ${token}`);
341
+ }
342
+ // dry-run이 기본이다. --blueprint만 주면 단일 경로 적용처럼 보이므로 거절한다.
343
+ if (blueprintSeen && !applySeen) {
344
+ return fail('--blueprint requires --apply');
345
+ }
346
+ if (applySeen && !blueprintSeen) {
347
+ return fail('--apply requires --blueprint <dir>');
348
+ }
349
+ return { apply, blueprint, repo };
350
+ }
351
+ /**
352
+ * migrate review-scope 전용 argv. parseFlags는 알 수 없는 option을 조용히 삼키므로
353
+ * 여기서 엄격히 닫는다. 쓰기 명령이라 --blueprint 누락은 사용법 오류(exit 2)다.
354
+ *
355
+ * @param {string[]} flagArgs - `migrate review-scope` 뒤 argv
356
+ * @returns {{ error?: string, blueprint: string | null, repo?: string }}
357
+ */
358
+ function parseReviewScopeMigrateArgs(flagArgs) {
359
+ let blueprint = null;
360
+ let repo;
361
+ const fail = (message) => ({
362
+ error: `migrate review-scope: ${message}\n`,
363
+ blueprint,
364
+ repo,
365
+ });
366
+ for (let i = 0; i < flagArgs.length; i += 1) {
367
+ const token = flagArgs[i];
368
+ if (token === '--blueprint' || token === '--repo') {
369
+ const isBlueprint = token === '--blueprint';
370
+ if (isBlueprint ? blueprint !== null : repo !== undefined) {
371
+ return fail(`duplicate option: ${token}`);
372
+ }
373
+ const value = flagArgs[++i];
374
+ if (value === undefined || value.startsWith('--') || value.trim() === '') {
375
+ return fail(`${token} requires a directory`);
376
+ }
377
+ if (isBlueprint)
378
+ blueprint = value.trim();
379
+ else
380
+ repo = value;
381
+ continue;
382
+ }
383
+ if (token.startsWith('--'))
384
+ return fail(`unknown option: ${token}`);
385
+ return fail(`unexpected argument: ${token}`);
386
+ }
387
+ if (blueprint === null)
388
+ return fail('--blueprint <dir> is required');
389
+ return { blueprint, repo };
390
+ }
391
+ /**
392
+ * migrate context-slugs 전용 argv. parseFlags는 알 수 없는 option을 조용히 삼키는데, 이 명령은 쓰기
393
+ * 명령이라 `--yes` 오타가 dry-run으로 위장되면 안 되므로 여기서 엄격히 닫는다.
394
+ *
395
+ * @param {string[]} flagArgs - `migrate context-slugs` 뒤 argv
396
+ * @returns {{ error?: string, yes: boolean, repo?: string }}
397
+ */
398
+ function parseContextSlugsMigrateArgs(flagArgs) {
399
+ let yes = false;
400
+ let yesSeen = false;
401
+ let repo;
402
+ const fail = (message) => ({ error: `migrate context-slugs: ${message}\n`, yes, repo });
403
+ for (let i = 0; i < flagArgs.length; i += 1) {
404
+ const token = flagArgs[i];
405
+ if (token === '--yes') {
406
+ if (yesSeen)
407
+ return fail('duplicate option: --yes');
408
+ yesSeen = true;
409
+ yes = true;
410
+ continue;
411
+ }
412
+ if (token === '--repo') {
413
+ if (repo !== undefined)
414
+ return fail('duplicate option: --repo');
415
+ const value = flagArgs[++i];
416
+ if (!value || value.startsWith('--'))
417
+ return fail('--repo requires a directory');
418
+ repo = value;
419
+ continue;
420
+ }
421
+ if (token.startsWith('--'))
422
+ return fail(`unknown option: ${token}`);
423
+ return fail(`unexpected argument: ${token}`);
424
+ }
425
+ return { yes, repo };
426
+ }
427
+ /**
428
+ * migrate merge-legacy-epics 전용 argv. 쓰기 명령이라 `--yes` 오타가 dry-run으로 위장되지 않도록
429
+ * context-slugs와 같은 엄격 규칙(알 수 없는·중복 option, 값 없는 `--repo`, 남는 인자 거절)을 쓴다.
430
+ *
431
+ * @param {string[]} flagArgs - `migrate merge-legacy-epics` 뒤 argv
432
+ * @returns {{ error?: string, yes: boolean, repo?: string }}
433
+ */
434
+ function parseMergeLegacyEpicsArgs(flagArgs) {
435
+ const parsed = parseContextSlugsMigrateArgs(flagArgs);
436
+ return parsed.error === undefined
437
+ ? parsed : { ...parsed, error: parsed.error.replace('migrate context-slugs:', 'migrate merge-legacy-epics:') };
438
+ }
439
+ function cmdMigrate(rest, io) {
440
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
441
+ const { parseFlags } = require('./cli-flags');
442
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
443
+ const { migrateTaskLayout } = require('./migrate-task-layout');
444
+ // 명령을 선택한 뒤에만 기능을 읽어 다른 경로의 적재를 피한다.
445
+ const { auditRetention, migrateRetention } = require('./retention-migration');
446
+ const [kind, ...flagArgs] = rest;
447
+ // kind를 플래그보다 먼저 본다. 알 수 없는 kind에 --dry-run만 있어도
448
+ // task-layout으로 떨어지면 안 된다.
449
+ if (kind === 'retention') {
450
+ const parsed = parseRetentionMigrateArgs(flagArgs);
451
+ if (parsed.error) {
452
+ io.err(parsed.error);
453
+ return 2;
454
+ }
455
+ const repoRoot = (parsed.repo || process.cwd());
456
+ if (parsed.apply) {
457
+ const result = migrateRetention({
458
+ repoRoot,
459
+ blueprintDir: parsed.blueprint,
460
+ });
461
+ io.out(`${JSON.stringify(result, null, 2)}\n`);
462
+ return result.ok ? 0 : 1;
463
+ }
464
+ const result = auditRetention({ repoRoot });
465
+ io.out(`${JSON.stringify(result, null, 2)}\n`);
466
+ return result.ok ? 0 : 1;
467
+ }
468
+ if (kind === 'review-scope') {
469
+ const { migrateReviewScope } = require('./migrate-review-scope');
470
+ const parsed = parseReviewScopeMigrateArgs(flagArgs);
471
+ if (parsed.error) {
472
+ io.err(parsed.error);
473
+ return 2;
474
+ }
475
+ const result = migrateReviewScope({
476
+ repoRoot: (parsed.repo || process.cwd()),
477
+ blueprintDir: parsed.blueprint,
478
+ });
479
+ io.out(`${JSON.stringify(result, null, 2)}\n`);
480
+ return result.ok ? 0 : 1;
481
+ }
482
+ if (kind === 'context-slugs') {
483
+ const { migrateContextSlugs } = require('./migrate-context-slugs');
484
+ const { catchMessage } = require('./guards');
485
+ const parsed = parseContextSlugsMigrateArgs(flagArgs);
486
+ if (parsed.error) {
487
+ io.err(parsed.error);
488
+ return 2;
489
+ }
490
+ try {
491
+ const result = migrateContextSlugs({ repoRoot: (parsed.repo || process.cwd()), yes: parsed.yes });
492
+ io.out(`${JSON.stringify(result, null, 2)}\n`);
493
+ return result.ok ? 0 : 1;
494
+ }
495
+ catch (error) {
496
+ // 계산 단계(쓰기 전)의 예외도 JSON 거절로 보고한다. 이 시점에는 디스크가 바뀌지 않았다.
497
+ io.out(`${JSON.stringify({
498
+ ok: false, code: 'migrate-failed', message: catchMessage(error), next: 'fix the cause, then retry',
499
+ }, null, 2)}\n`);
500
+ return 1;
501
+ }
502
+ }
503
+ if (kind === 'merge-legacy-epics') {
504
+ const { migrateMergeLegacyEpics } = require('./migrate-merge-legacy-epics');
505
+ const { catchMessage } = require('./guards');
506
+ const parsed = parseMergeLegacyEpicsArgs(flagArgs);
507
+ if (parsed.error) {
508
+ io.err(parsed.error);
509
+ return 2;
510
+ }
511
+ try {
512
+ const result = migrateMergeLegacyEpics({ repoRoot: (parsed.repo || process.cwd()), yes: parsed.yes });
513
+ io.out(`${JSON.stringify(result, null, 2)}\n`);
514
+ return result.ok ? 0 : 1;
515
+ }
516
+ catch (error) {
517
+ // 계산 단계(쓰기 전)의 예외도 JSON 거절로 보고한다. 이 시점에는 디스크가 바뀌지 않았다.
518
+ io.out(`${JSON.stringify({
519
+ ok: false, code: 'migrate-failed', message: catchMessage(error), next: 'fix the cause, then retry',
520
+ }, null, 2)}\n`);
521
+ return 1;
522
+ }
523
+ }
524
+ if (kind !== 'task-layout') {
525
+ io.err(`unknown migrate kind: ${kind || '(missing)'}\n`);
526
+ return 2;
527
+ }
528
+ const f = parseFlags(flagArgs);
529
+ const repoRoot = (f.repo || process.cwd());
530
+ const result = migrateTaskLayout({ repoRoot, dryRun: f['dry-run'] === true });
531
+ io.out(`${JSON.stringify(result, null, 2)}\n`);
532
+ // kind는 위에서 이미 걸렀다. 라이브러리 거절(dirty/collision)은 실행 실패(1).
533
+ return result.ok ? 0 : 1;
534
+ }
535
+ module.exports = {
536
+ init: {
537
+ run: cmdInit,
538
+ usage: cliUsage.COMMAND_USAGE['init'],
539
+ },
540
+ 'graph-sync': {
541
+ run: cmdGraphSync,
542
+ usage: cliUsage.COMMAND_USAGE['graph-sync'],
543
+ },
544
+ intent: {
545
+ run: cmdIntentLazy,
546
+ usage: cliUsage.COMMAND_USAGE['intent'],
547
+ },
548
+ ontology: {
549
+ run: cmdOntologyLazy,
550
+ usage: ` ontology entity --entity <token> [--repo <dir>] [--limit <1..100>]
551
+ file --file <path> [--repo <dir>] [--limit <1..100>]
552
+ impact --file <path> [--repo <dir>] [--limit <1..100>]
553
+ Search canonical Bouncer context in memory (read-only JSON).
554
+ `,
555
+ },
556
+ 'graphify-bin': {
557
+ run: cmdGraphifyBin,
558
+ usage: cliUsage.COMMAND_USAGE['graphify-bin'],
559
+ },
560
+ 'project-root': {
561
+ run: cmdProjectRoot,
562
+ usage: cliUsage.COMMAND_USAGE['project-root'],
563
+ },
564
+ 'subagent-model': {
565
+ run: cmdSubagentModel,
566
+ usage: cliUsage.COMMAND_USAGE['subagent-model'],
567
+ },
568
+ 'codex-agents': {
569
+ run: cmdCodexAgents,
570
+ usage: cliUsage.COMMAND_USAGE['codex-agents'],
571
+ },
572
+ migrate: {
573
+ run: cmdMigrate,
574
+ usage: cliUsage.COMMAND_USAGE['migrate'],
575
+ },
576
+ run: {
577
+ run: cmdRun,
578
+ usage: cliUsage.COMMAND_USAGE['run'],
579
+ },
580
+ };