@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.
- package/LICENSE +202 -0
- package/README.md +31 -2
- package/bin/bouncer +26 -0
- package/bin/bouncer-root +25 -0
- package/bin/build-stamp.js +78 -0
- package/bin/register-path.js +71 -0
- package/dist/.build-stamp +1 -0
- package/dist/approval-snapshot.js +99 -0
- package/dist/bouncer-root.js +80 -0
- package/dist/canonical-json.js +37 -0
- package/dist/cli-commit-guard-command.js +74 -0
- package/dist/cli-config-command.js +47 -0
- package/dist/cli-context-review-command.js +273 -0
- package/dist/cli-current-command.js +374 -0
- package/dist/cli-dispatch-command.js +119 -0
- package/dist/cli-doc-commands.js +352 -0
- package/dist/cli-epic-command.js +130 -0
- package/dist/cli-flags.js +50 -0
- package/dist/cli-git-commands.js +931 -0
- package/dist/cli-intent-command.js +491 -0
- package/dist/cli-metrics-command.js +134 -0
- package/dist/cli-ontology-command.js +109 -0
- package/dist/cli-project-commands.js +580 -0
- package/dist/cli-retro-command.js +138 -0
- package/dist/cli-review-command.js +251 -0
- package/dist/cli-review-dispatch-command.js +338 -0
- package/dist/cli-usage.js +72 -0
- package/dist/cli.js +70 -0
- package/dist/codex-agents.js +189 -0
- package/dist/command-argv.js +125 -0
- package/dist/commit-guard.js +72 -0
- package/dist/commit-hook.js +470 -0
- package/dist/commit-message.js +77 -0
- package/dist/commit-sha.js +107 -0
- package/dist/commit.js +519 -0
- package/dist/composite-gates.js +129 -0
- package/dist/comprehension.js +127 -0
- package/dist/config.js +298 -0
- package/dist/context-ids.js +288 -0
- package/dist/context-migration-rewrite.js +421 -0
- package/dist/context-ontology.js +746 -0
- package/dist/context-review-contracts.js +2 -0
- package/dist/context-review-projection.js +147 -0
- package/dist/context-review-state.js +824 -0
- package/dist/context-review-store.js +602 -0
- package/dist/context-review-transitions.js +412 -0
- package/dist/coordinate-advance.js +253 -0
- package/dist/coordinate-next.js +998 -0
- package/dist/coordinate-output.js +57 -0
- package/dist/coordinator-command-contracts.js +2 -0
- package/dist/coordinator-commands.js +1908 -0
- package/dist/coordinator-fanin.js +1021 -0
- package/dist/coordinator-git.js +121 -0
- package/dist/coordinator-input.js +37 -0
- package/dist/coordinator-ledger.js +381 -0
- package/dist/coordinator-prepare.js +1047 -0
- package/dist/coordinator-repair.js +1416 -0
- package/dist/coordinator-verification.js +42 -0
- package/dist/coordinator-worker.js +388 -0
- package/dist/coordinator.js +7 -0
- package/dist/cross-drive.js +85 -0
- package/dist/current.js +1148 -0
- package/dist/dag-shape.js +93 -0
- package/dist/doc-shape-contracts.js +241 -0
- package/dist/drive-metrics.js +691 -0
- package/dist/epic-index.js +347 -0
- package/dist/epic-registry.js +453 -0
- package/dist/execute-prepare.js +262 -0
- package/dist/execution-contracts.js +46 -0
- package/dist/execution-dag.js +148 -0
- package/dist/finalize-checks.js +177 -0
- package/dist/finalize-collect.js +343 -0
- package/dist/finalize-comprehension.js +116 -0
- package/dist/finalize-digest-collect.js +388 -0
- package/dist/finalize-digest-model.js +595 -0
- package/dist/finalize-digest.js +164 -0
- package/dist/finalize-explain.js +176 -0
- package/dist/finalize-pr.js +610 -0
- package/dist/finalize-release-main.js +652 -0
- package/dist/finalize.js +714 -0
- package/dist/frontmatter.js +22 -0
- package/dist/git-ops.js +49 -0
- package/dist/graph-exec.js +179 -0
- package/dist/graph-scope.js +268 -0
- package/dist/graphify.js +995 -0
- package/dist/guards.js +82 -0
- package/dist/import-git.js +78 -0
- package/dist/import-history.js +392 -0
- package/dist/import-render.js +59 -0
- package/dist/import-types.js +2 -0
- package/dist/init.js +571 -0
- package/dist/intent-bundle.js +936 -0
- package/dist/intent-provenance.js +927 -0
- package/dist/layout.js +131 -0
- package/dist/lease.js +145 -0
- package/dist/legacy-comments.js +71 -0
- package/dist/migrate-context-slugs.js +262 -0
- package/dist/migrate-merge-legacy-epics.js +392 -0
- package/dist/migrate-review-scope.js +144 -0
- package/dist/migrate-task-layout.js +194 -0
- package/dist/path-overlap.js +38 -0
- package/dist/paths.js +45 -0
- package/dist/plan-approve.js +218 -0
- package/dist/plan-inspect.js +575 -0
- package/dist/plan-prerequisites.js +197 -0
- package/dist/plan-snapshot-contract.js +98 -0
- package/dist/plan-snapshot.js +138 -0
- package/dist/plugin-root.js +237 -0
- package/dist/posix-path.js +14 -0
- package/dist/pre-commit-hook.js +170 -0
- package/dist/print-dispatch.js +285 -0
- package/dist/render.js +8 -0
- package/dist/retention-migration.js +595 -0
- package/dist/retro-read.js +259 -0
- package/dist/retro-sources.js +318 -0
- package/dist/review-dispatch.js +1054 -0
- package/dist/review-policy.js +28 -0
- package/dist/review-record.js +447 -0
- package/dist/review-repair-state.js +52 -0
- package/dist/review-snapshot.js +659 -0
- package/dist/run-detach.js +165 -0
- package/dist/run-preflight.js +368 -0
- package/dist/runtime-state.js +1038 -0
- package/dist/scaffold.js +533 -0
- package/dist/schema.js +294 -0
- package/dist/scope.js +1073 -0
- package/dist/seed-worktree.js +926 -0
- package/dist/session-graph.js +337 -0
- package/dist/subagents.js +86 -0
- package/dist/symbol-index.js +1390 -0
- package/dist/task-brief-hash.js +34 -0
- package/dist/task-commits.js +124 -0
- package/dist/tasks-docs.js +192 -0
- package/dist/templates.js +469 -0
- package/dist/time.js +21 -0
- package/dist/validate-docs.js +302 -0
- package/dist/validate-gates-finalize.js +303 -0
- package/dist/validate-gates-plan.js +720 -0
- package/dist/validate-gates-run.js +420 -0
- package/dist/validate-gates-shared.js +124 -0
- package/dist/validate-gates.js +87 -0
- package/dist/validate-sections.js +654 -0
- package/dist/validate-structural.js +347 -0
- package/dist/validate.js +417 -0
- package/dist/verification-contracts.js +26 -0
- package/dist/verification-evidence.js +233 -0
- package/dist/verification-execution.js +163 -0
- package/dist/verification-identity.js +413 -0
- package/dist/verification-target.js +51 -0
- package/dist/verification.js +383 -0
- package/dist/windows-npm.js +38 -0
- package/package.json +32 -4
- package/resources/graphify-compat.json +9 -0
- package/vendor/README.md +23 -0
- package/vendor/js-yaml.LICENSE +21 -0
- package/vendor/js-yaml.js +3022 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const guards = require("./guards");
|
|
3
|
+
const { isRecord } = guards;
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const frontmatter = require("./frontmatter");
|
|
7
|
+
const { parseFrontmatter } = frontmatter;
|
|
8
|
+
const layout = require("./layout");
|
|
9
|
+
const { CODEX_AGENTS_DIR } = layout;
|
|
10
|
+
const pluginRoot = require("./plugin-root");
|
|
11
|
+
const { resolvePluginAssetRoot } = pluginRoot;
|
|
12
|
+
const NAMED_AGENTS = [
|
|
13
|
+
'bouncer-reviewer',
|
|
14
|
+
'bouncer-implementer',
|
|
15
|
+
'bouncer-debugger',
|
|
16
|
+
'bouncer-context-reviewer',
|
|
17
|
+
'bouncer-coordinator',
|
|
18
|
+
];
|
|
19
|
+
// 이 마커가 첫 줄이면 init이 플러그인 md와 다시 맞춘다. 마커 없는 파일은
|
|
20
|
+
// 사용자 소유로 보고 덮지 않는다 — Codex는 플러그인 디렉터리를 스캔하지
|
|
21
|
+
// 않으므로 이 사본이 유일한 로드 경로다.
|
|
22
|
+
const GENERATED_MARKER = '# bouncer-generated';
|
|
23
|
+
/**
|
|
24
|
+
* 플러그인 루트의 `agents/` 절대 경로를 돌려준다. print 디스패치와 Codex
|
|
25
|
+
* 시드가 같은 기준을 써야 역할 문서와 TOML 소스가 갈라지지 않는다.
|
|
26
|
+
*
|
|
27
|
+
* @returns {string} 검증된 자산 루트(`resolvePluginAssetRoot`) 아래의 `agents/`
|
|
28
|
+
* @throws 자산 루트를 해석하지 못하면 resolver의 Error를 그대로 던진다
|
|
29
|
+
*/
|
|
30
|
+
function pluginAgentsDir() {
|
|
31
|
+
return path.join(resolvePluginAssetRoot(), 'agents');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Claude/Cursor/Antigravity용 `agents/*.md`를 Codex custom-agent TOML로 바꾼다.
|
|
35
|
+
* `model: inherit`는 키를 생략해 부모 세션 모델을 쓰게 한다.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} markdown - YAML frontmatter가 있는 에이전트 문서
|
|
38
|
+
* @returns {string} Codex가 `.codex/agents/`에서 읽는 TOML
|
|
39
|
+
*/
|
|
40
|
+
function mdToCodexToml(markdown) {
|
|
41
|
+
const { data, body } = parseFrontmatter(markdown);
|
|
42
|
+
if (!isRecord(data)) {
|
|
43
|
+
throw new Error('agent frontmatter must be a mapping');
|
|
44
|
+
}
|
|
45
|
+
const name = data.name;
|
|
46
|
+
const description = data.description;
|
|
47
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
48
|
+
throw new Error('agent name required');
|
|
49
|
+
}
|
|
50
|
+
if (typeof description !== 'string' || description.length === 0) {
|
|
51
|
+
throw new Error('agent description required');
|
|
52
|
+
}
|
|
53
|
+
const lines = [
|
|
54
|
+
GENERATED_MARKER,
|
|
55
|
+
`# Source: agents/${name}.md`,
|
|
56
|
+
'',
|
|
57
|
+
`name = ${JSON.stringify(name)}`,
|
|
58
|
+
`description = ${JSON.stringify(description)}`,
|
|
59
|
+
];
|
|
60
|
+
if (data.readonly === true) {
|
|
61
|
+
lines.push('sandbox_mode = "read-only"');
|
|
62
|
+
}
|
|
63
|
+
const instructions = String(body).replace(/^\n+/, '').replace(/\n+$/, '');
|
|
64
|
+
lines.push('developer_instructions = """');
|
|
65
|
+
lines.push(instructions.replace(/"""/g, '\\"""'));
|
|
66
|
+
lines.push('"""');
|
|
67
|
+
lines.push('');
|
|
68
|
+
return lines.join('\n');
|
|
69
|
+
}
|
|
70
|
+
function shouldRefresh(existing, next) {
|
|
71
|
+
const first = existing.split(/\r?\n/, 1)[0];
|
|
72
|
+
if (first !== GENERATED_MARKER)
|
|
73
|
+
return false;
|
|
74
|
+
return existing !== next;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 로컬 `.codex/agents/<name>.toml`이 플러그인 md 생성본과 바이트 일치하는지
|
|
78
|
+
* 읽기만으로 판정한다. 파일을 만들지 않고, 마커 없는 사용자 파일은
|
|
79
|
+
* mismatch가 아니라 not-generated로 돌려 seed가 덮을 대상이 아님을 드러낸다.
|
|
80
|
+
*
|
|
81
|
+
* @param {{ repoRoot: string, agentName: string, agentsDir?: string }} opts
|
|
82
|
+
* @param {string} opts.repoRoot - 소비 저장소 루트
|
|
83
|
+
* @param {string} opts.agentName - `.toml` 파일 stem (예: bouncer-implementer)
|
|
84
|
+
* @param {string} [opts.agentsDir] - md 소스 디렉터리. 생략 시 플러그인 agents/
|
|
85
|
+
* @returns {CodexAgentCheckOk | CodexAgentCheckFail} ok면 in_sync·path, 아니면 reason·path
|
|
86
|
+
*/
|
|
87
|
+
function checkCodexAgent({ repoRoot, agentName, agentsDir, }) {
|
|
88
|
+
// 1. 상대 posix 경로를 JSON 계약의 path로 고정한다. OS 구분자로 바꾸면
|
|
89
|
+
// 에이전트가 `.codex/agents/<name>.toml` 리터럴을 찾지 못한다.
|
|
90
|
+
const rel = `${CODEX_AGENTS_DIR}/${agentName}.toml`;
|
|
91
|
+
const dest = path.join(repoRoot, rel);
|
|
92
|
+
if (!fs.existsSync(dest)) {
|
|
93
|
+
return { ok: false, in_sync: false, reason: 'missing', path: rel };
|
|
94
|
+
}
|
|
95
|
+
const existing = fs.readFileSync(dest, 'utf8');
|
|
96
|
+
// 2. 첫 줄만 본다. shouldRefresh와 같이 마커 없는 파일은 사용자 소유라
|
|
97
|
+
// 생성본과 비교하지 않는다 — 내용이 같아도 seed 대상이 아니다.
|
|
98
|
+
const first = existing.split(/\r?\n/, 1)[0];
|
|
99
|
+
if (first !== GENERATED_MARKER) {
|
|
100
|
+
return { ok: false, in_sync: false, reason: 'not-generated', path: rel };
|
|
101
|
+
}
|
|
102
|
+
// 3. 생성 기준은 ensureCodexAgents와 같은 mdToCodexToml이다. 변환을
|
|
103
|
+
// 여기서 다시 구현하면 seed와 check가 다른 바이트를 정본으로 삼는다.
|
|
104
|
+
const srcDir = agentsDir ?? pluginAgentsDir();
|
|
105
|
+
const next = mdToCodexToml(fs.readFileSync(path.join(srcDir, `${agentName}.md`), 'utf8'));
|
|
106
|
+
if (existing !== next) {
|
|
107
|
+
return { ok: false, in_sync: false, reason: 'mismatch', path: rel };
|
|
108
|
+
}
|
|
109
|
+
return { ok: true, in_sync: true, path: rel };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* init이 `.codex/agents/*.toml`을 심을지. 호스트 이름·환경 변수는 신호가
|
|
113
|
+
* 아니다 — Claude-only 저장소에 빈 `.codex/`가 생기는 일을 막기 위해
|
|
114
|
+
* 디렉터리 존재 또는 명시적 opt-in만 본다.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} repoRoot - 소비 저장소 루트
|
|
117
|
+
* @param {boolean} [optIn=false] - CLI `--seed-codex-agents` 등 명시 요청
|
|
118
|
+
* @returns {boolean}
|
|
119
|
+
*/
|
|
120
|
+
function shouldEnsureCodexAgents(repoRoot, optIn = false) {
|
|
121
|
+
if (optIn === true)
|
|
122
|
+
return true;
|
|
123
|
+
try {
|
|
124
|
+
return fs.statSync(path.join(repoRoot, '.codex')).isDirectory();
|
|
125
|
+
}
|
|
126
|
+
catch (_e) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 소비 저장소 `.codex/agents/<name>.toml`을 심거나, 생성본이면 md와 맞춘다.
|
|
132
|
+
* 호출 측이 조건을 통과한 뒤에만 부른다. 조건 없이 호출하면 named agent
|
|
133
|
+
* 전부의 TOML을 심거나 생성본을 맞춘다. 플러그인 agents 디렉터리가 없으면 no-op —
|
|
134
|
+
* init 부트스트랩을 막지 않는다.
|
|
135
|
+
*
|
|
136
|
+
* @param {{ repoRoot: string, created: string[], agentsDir?: string }} opts
|
|
137
|
+
* @returns {void}
|
|
138
|
+
*/
|
|
139
|
+
function ensureCodexAgents({ repoRoot, created, agentsDir, }) {
|
|
140
|
+
let srcDir;
|
|
141
|
+
if (agentsDir !== undefined) {
|
|
142
|
+
srcDir = agentsDir;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
try {
|
|
146
|
+
srcDir = pluginAgentsDir();
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
// resolver가 던지는 Error(무효 BOUNCER_HOME, 유효 루트 없음)만 흡수한다. 자산이 없으면
|
|
150
|
+
// 아래 existsSync 분기처럼 시드를 건너뛰는 것이 원래 계약이다. 그 외는 다시 던진다.
|
|
151
|
+
if (!(error instanceof Error))
|
|
152
|
+
throw error;
|
|
153
|
+
if ('code' in error && error.code === 'BOUNCER_PLUGIN_VERSION_MISMATCH')
|
|
154
|
+
throw error;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (!fs.existsSync(srcDir))
|
|
159
|
+
return;
|
|
160
|
+
const destDir = path.join(repoRoot, CODEX_AGENTS_DIR);
|
|
161
|
+
for (const name of NAMED_AGENTS) {
|
|
162
|
+
const src = path.join(srcDir, `${name}.md`);
|
|
163
|
+
if (!fs.existsSync(src))
|
|
164
|
+
continue;
|
|
165
|
+
const next = mdToCodexToml(fs.readFileSync(src, 'utf8'));
|
|
166
|
+
const rel = `${CODEX_AGENTS_DIR}/${name}.toml`;
|
|
167
|
+
const dest = path.join(repoRoot, rel);
|
|
168
|
+
if (fs.existsSync(dest)) {
|
|
169
|
+
const existing = fs.readFileSync(dest, 'utf8');
|
|
170
|
+
if (!shouldRefresh(existing, next))
|
|
171
|
+
continue;
|
|
172
|
+
fs.writeFileSync(dest, next);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
176
|
+
fs.writeFileSync(dest, next);
|
|
177
|
+
created.push(rel);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
module.exports = {
|
|
181
|
+
NAMED_AGENTS,
|
|
182
|
+
GENERATED_MARKER,
|
|
183
|
+
CODEX_AGENTS_DIR,
|
|
184
|
+
pluginAgentsDir,
|
|
185
|
+
mdToCodexToml,
|
|
186
|
+
checkCodexAgent,
|
|
187
|
+
ensureCodexAgents,
|
|
188
|
+
shouldEnsureCodexAgents,
|
|
189
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// argv 검사 leaf 모듈. verification.ts가 의존 모듈이 많아, 같은 검사를 쓰려는
|
|
3
|
+
// seed-worktree가 verification을 가져오면 import 순환 위험이 있다. 그래서 node
|
|
4
|
+
// 내장 모듈과 config의 기본 허용 목록만 가져오는 이 모듈로 분리했다.
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const config = require("./config");
|
|
7
|
+
const { DEFAULT_VERIFY_ALLOWLIST } = config;
|
|
8
|
+
// 실행 가능한 argv 문자열 하나만: 셸 체이닝, 리다이렉션, `cd` 접두사 없음.
|
|
9
|
+
// Plan S12와 runtime VERIFY_COMMAND_INVALID가 parseVerifyArgv·allowlist를
|
|
10
|
+
// 공유하므로 두 표면이 어긋날 수 없습니다.
|
|
11
|
+
/**
|
|
12
|
+
* 검증 명령 문자열을 argv 배열로 파싱한다. 공백으로 나누되 작은따옴표·
|
|
13
|
+
* 큰따옴표 안의 공백은 한 인자로 유지한다. 셸 확장은 하지 않는다 —
|
|
14
|
+
* `shell: false` 실행과 같은 경계를 파싱 단계에서도 맞춘다.
|
|
15
|
+
*
|
|
16
|
+
* 메타문자 검사는 인용 **밖**에서만 한다. `node -e "a; b"`처럼 인자 안의
|
|
17
|
+
* `;`는 데이터이고, 인용 밖 `a; b`만 체이닝으로 거절해야 예전 config.verify
|
|
18
|
+
* 문자열과 실행 의미가 어긋나지 않는다.
|
|
19
|
+
*
|
|
20
|
+
* @param {unknown} command - `tasks.bouncer.verify` 또는 `config.verify` 문자열
|
|
21
|
+
* @returns {string[] | null} 성공 시 argv. 빈 값·메타문자·미종료 인용·`cd` 접두면 null
|
|
22
|
+
*/
|
|
23
|
+
function parseVerifyArgv(command) {
|
|
24
|
+
if (typeof command !== 'string')
|
|
25
|
+
return null;
|
|
26
|
+
const trimmed = command.trim();
|
|
27
|
+
if (!trimmed)
|
|
28
|
+
return null;
|
|
29
|
+
const argv = [];
|
|
30
|
+
let current = '';
|
|
31
|
+
let quote = null;
|
|
32
|
+
let sawToken = false;
|
|
33
|
+
for (let i = 0; i < trimmed.length; i += 1) {
|
|
34
|
+
const ch = trimmed[i];
|
|
35
|
+
if (quote) {
|
|
36
|
+
if (ch === quote) {
|
|
37
|
+
quote = null;
|
|
38
|
+
sawToken = true;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
current += ch;
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (ch === '"' || ch === "'") {
|
|
46
|
+
quote = ch;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// 인용 밖의 셸 메타·개행·$( 만 거절. 인자 값에 든 동일 문자는 허용.
|
|
50
|
+
if (ch === '$' && trimmed[i + 1] === '(')
|
|
51
|
+
return null;
|
|
52
|
+
if (/[&|;`<>\n]/.test(ch))
|
|
53
|
+
return null;
|
|
54
|
+
if (/\s/.test(ch)) {
|
|
55
|
+
if (sawToken || current.length > 0) {
|
|
56
|
+
argv.push(current);
|
|
57
|
+
current = '';
|
|
58
|
+
sawToken = false;
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
current += ch;
|
|
63
|
+
sawToken = true;
|
|
64
|
+
}
|
|
65
|
+
if (quote)
|
|
66
|
+
return null;
|
|
67
|
+
if (sawToken || current.length > 0) {
|
|
68
|
+
argv.push(current);
|
|
69
|
+
}
|
|
70
|
+
if (argv.length === 0)
|
|
71
|
+
return null;
|
|
72
|
+
// basename이 아니라 원본 argv0가 cd면 거절한다. `cd`는 셸 내장이고
|
|
73
|
+
// shell:false로도 의미가 없으며, 예전 S12 계약(`cd` 접두 금지)을 유지한다.
|
|
74
|
+
if (argv[0] === 'cd')
|
|
75
|
+
return null;
|
|
76
|
+
return argv;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* argv0를 허용 목록 비교용 이름으로 정규화한다. basename만 남기고,
|
|
80
|
+
* Windows는 PATH가 `npm.cmd`·`node.exe`를 고르므로 관용 확장자를 벗긴다 —
|
|
81
|
+
* 목록은 `npm`/`node`처럼 확장자 없는 이름만 둔다.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} argv0 - 파싱된 첫 인자
|
|
84
|
+
* @returns {string} 허용 목록과 비교할 이름
|
|
85
|
+
*/
|
|
86
|
+
function verifyExecutableName(argv0) {
|
|
87
|
+
const base = path.basename(argv0);
|
|
88
|
+
if (process.platform === 'win32') {
|
|
89
|
+
return base.replace(/\.(cmd|exe|bat)$/i, '');
|
|
90
|
+
}
|
|
91
|
+
return base;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 활성 포인터·config가 고른 검증 명령이 실행 가능한지 판정한다.
|
|
95
|
+
* 파싱 성공과 허용 목록(argv0 실행 파일명)을 함께 본다. allowlist를 생략하면
|
|
96
|
+
* 기본 목록을 쓴다. 저장소 정책을 이미 읽은 호출자는 그 목록을 넘겨
|
|
97
|
+
* S12와 runtime이 다른 답을 내지 않게 한다.
|
|
98
|
+
*
|
|
99
|
+
* @param {unknown} command - 검증 명령 문자열
|
|
100
|
+
* @param {readonly string[]} [allowlist] - argv0 실행 파일명 허용 목록
|
|
101
|
+
* @returns {boolean} 실행해도 되는 명령이면 true
|
|
102
|
+
*/
|
|
103
|
+
function isValidVerifyCommand(command, allowlist = DEFAULT_VERIFY_ALLOWLIST) {
|
|
104
|
+
const argv = parseVerifyArgv(command);
|
|
105
|
+
if (!argv)
|
|
106
|
+
return false;
|
|
107
|
+
return allowlist.includes(verifyExecutableName(argv[0]));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* verify 명령을 비교용으로 정규화한다. 앞뒤 공백을 자르고 연속 공백을 하나로
|
|
111
|
+
* 접는다. G20(종단 전체 명령 금지)과 `coordinate repair --verify` 검사가 같은
|
|
112
|
+
* 함수를 써야 한 표면이 통과시킨 값을 다른 표면이 종단 명령으로 오인하지 않는다.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} cmd - verify 명령 문자열
|
|
115
|
+
* @returns {string} 정규화된 문자열. 문자열이 아니면 빈 문자열
|
|
116
|
+
*/
|
|
117
|
+
function normalizeVerifyCommand(cmd) {
|
|
118
|
+
return typeof cmd === 'string' ? cmd.trim().replace(/\s+/g, ' ') : '';
|
|
119
|
+
}
|
|
120
|
+
module.exports = {
|
|
121
|
+
normalizeVerifyCommand,
|
|
122
|
+
parseVerifyArgv,
|
|
123
|
+
verifyExecutableName,
|
|
124
|
+
isValidVerifyCommand,
|
|
125
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// scripts/lib/commit-guard.js
|
|
2
|
+
'use strict';
|
|
3
|
+
const scope = require("./scope");
|
|
4
|
+
const { makeAllowed, isRuntimeArtifact } = scope;
|
|
5
|
+
const crossDrive = require("./cross-drive");
|
|
6
|
+
const { crossDriveConflicts } = crossDrive;
|
|
7
|
+
/**
|
|
8
|
+
* 커밋 범위 안전 판정의 단일 구현.
|
|
9
|
+
* `commitTask`(CLI)와 호스트 commit hook(`evaluateCommit`)이 같은
|
|
10
|
+
* `allow`·`violations`를 쓰기 위한 계약이다. 허용·거부 로직을 어댑터에
|
|
11
|
+
* 복제하지 말고 여기만 호출한다.
|
|
12
|
+
* 기존 worktree·revision 경계와 현재 scope를 먼저 확정한 뒤 다른 drive를 본다.
|
|
13
|
+
* 그래야 잘못된 checkout이나 범위 밖 쓰기가 상대 ledger 상태로 가려지지 않는다.
|
|
14
|
+
*
|
|
15
|
+
* @param {object} opts
|
|
16
|
+
* @param {unknown[]|null|undefined} [opts.files] - 호출자가 선택한 실제 경로.
|
|
17
|
+
* G17 호출자는 staged 파일만 전달하며, 이 함수는 working tree를 다시 읽지 않는다.
|
|
18
|
+
* null/undefined는 빈 목록으로 취급한다.
|
|
19
|
+
* @param {unknown} [opts.affectedPaths] - 활성 task의 `affected_paths`
|
|
20
|
+
* @param {unknown} [opts.blueprintDir] - blueprint 상대 경로(번들 문서 예외용)
|
|
21
|
+
* @param {unknown} [opts.executionKind] - verification이면 commit 흐름 자체를 거절
|
|
22
|
+
* @param {object} [opts.coordinator] - `scope.coordinatorContext` 결과.
|
|
23
|
+
* `active`면 ledger의 현재 scope가 승인된 `affected_paths`를 대신하고,
|
|
24
|
+
* `reason`이 있으면 worktree·revision 경계 위반이므로 범위 판정 전에 거절한다.
|
|
25
|
+
* @param {object} [opts.otherDrives] - 현재 scope 통과 뒤 비교할 다른 drive 원장 또는 읽기 실패
|
|
26
|
+
* @returns {object} allow·violations·code와, 충돌이면 conflicts 또는 ledgerFile
|
|
27
|
+
* `allow === true`면 위반 없음(`violations`는 `[]`).
|
|
28
|
+
* `allow === false`면 `code`가 거절 사유이고, `out-of-scope`일 때만
|
|
29
|
+
* `violations`가 범위 밖 경로다. 경계 위반은 staged 경로 전체를 담는다.
|
|
30
|
+
* runtime artifact(`isRuntimeArtifact`)는 위반으로 치지 않는다 —
|
|
31
|
+
* `makeAllowed` 예외와 함께 이 모듈이 소유한다.
|
|
32
|
+
*/
|
|
33
|
+
function checkCommitSafety({ files, affectedPaths, blueprintDir, coordinator, executionKind, otherDrives, }) {
|
|
34
|
+
const candidates = (files || []).filter((f) => !isRuntimeArtifact(f));
|
|
35
|
+
// verification node는 빈 scope를 이용해 "변경 없음" commit으로 들어가는 것도
|
|
36
|
+
// 금지한다. 파일 수와 무관하게 종류 자체가 commit 권한을 갖지 않는다.
|
|
37
|
+
if (executionKind === 'verification') {
|
|
38
|
+
return { allow: false, code: 'verification-task-no-commit', violations: candidates };
|
|
39
|
+
}
|
|
40
|
+
const active = Boolean(coordinator && coordinator.active);
|
|
41
|
+
// worktree 경계와 revision 불일치는 범위 계산 이전의 거절이다. 여기서
|
|
42
|
+
// scope로 다시 판정하면 main worktree 커밋이 "범위 안"으로 통과한다.
|
|
43
|
+
if (active && coordinator && coordinator.reason) {
|
|
44
|
+
return { allow: false, code: coordinator.reason, violations: candidates };
|
|
45
|
+
}
|
|
46
|
+
// coordinator 실행에서 승인된 affected_paths는 초기 예상치다 — 현재 정본은
|
|
47
|
+
// ledger scope. ledger가 아직 scope를 기록하지 않았으면 승인값을 그대로 쓴다.
|
|
48
|
+
const scope = active && coordinator && Array.isArray(coordinator.scope)
|
|
49
|
+
? coordinator.scope
|
|
50
|
+
: affectedPaths;
|
|
51
|
+
const allowed = makeAllowed({ affectedPaths: scope, blueprintDir });
|
|
52
|
+
const violations = candidates.filter((f) => !allowed(f));
|
|
53
|
+
if (violations.length > 0)
|
|
54
|
+
return { allow: false, code: 'out-of-scope', violations };
|
|
55
|
+
if (active && otherDrives) {
|
|
56
|
+
// 범위 검사를 먼저 끝내므로 cross-drive 결과는 승인된 후보만 비교한다.
|
|
57
|
+
if (!otherDrives.ok) {
|
|
58
|
+
return { allow: false, code: otherDrives.reason, violations: [], ledgerFile: otherDrives.ledgerFile };
|
|
59
|
+
}
|
|
60
|
+
const candidate = {
|
|
61
|
+
id: coordinator && coordinator.taskId ? coordinator.taskId : 'commit',
|
|
62
|
+
affected_paths: candidates.filter((file) => typeof file === 'string'),
|
|
63
|
+
exclusive_resources: coordinator && Array.isArray(coordinator.resources) ? coordinator.resources : [],
|
|
64
|
+
};
|
|
65
|
+
const conflicts = crossDriveConflicts([candidate], otherDrives.drives);
|
|
66
|
+
if (conflicts.length > 0) {
|
|
67
|
+
return { allow: false, code: 'cross-drive-conflict', violations: [], conflicts };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { allow: true, violations: [] };
|
|
71
|
+
}
|
|
72
|
+
module.exports = { checkCommitSafety };
|