leerness 1.36.184 → 1.36.185
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/CHANGELOG.md +4 -0
- package/README.ko.md +15 -8
- package/README.md +25 -7
- package/bin/leerness.js +1088 -179
- package/docs/role-agent-routing-v2.md +411 -0
- package/lib/agents.js +1236 -100
- package/lib/file-leases.js +845 -0
- package/lib/io.js +152 -1
- package/lib/mcp-tools.js +19 -2
- package/lib/pure-utils.js +52 -4
- package/lib/role-agent-schema.js +1179 -0
- package/lib/role-catalog.js +31 -1
- package/lib/role-fallback.js +1611 -0
- package/lib/role-store.js +730 -0
- package/lib/routing.js +109 -21
- package/lib/session-close.js +49 -1
- package/package.json +10 -5
- package/scripts/command-flags-probe.js +40 -3
- package/scripts/e2e.js +94 -36
- package/scripts/file-lease-probe.js +1139 -0
- package/scripts/installed-cleanroom-probe.js +39 -12
- package/scripts/provider-capacity-probe.js +425 -0
- package/scripts/release-runtime-probe.js +16 -1
- package/scripts/role-agent-schema-probe.js +834 -0
- package/scripts/role-fallback-probe.js +2010 -0
- package/scripts/role-store-loader-probe.js +882 -0
package/bin/leerness.js
CHANGED
|
@@ -11,7 +11,7 @@ const { scanShellScriptsEncoding, applyShellScriptUtf8Bom } = require('../lib/sh
|
|
|
11
11
|
const os = require('os'); // 1.9.178: _publishToNpm 에서 os.tmpdir() 사용 (전역 import)
|
|
12
12
|
const readline = require('readline');
|
|
13
13
|
// 1.9.274 (UR-0025 1단계): 순수 유틸 함수 모듈 분리 (require-based, 비파괴). selftest 7종이 동작 검증.
|
|
14
|
-
const { _isSecretKey, _isPlaceholderSecret, _looksSecretLike, _mergeLines, _mergeEnvLines, _mergeReadmeSection, _managedMerge, _parseSkillsValue, _parseArchiveBlocks, _parseSkillCatalog, _renderTeamsMd, _composeTeamPlan, _teamHandoffReminders, _cadenceAssessment, _teamDeployGate, _renderWorkspaceReferenceGuide, _memorySurface, _renderPulseLine, compareVer, parseHarnessVersion, _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp,
|
|
14
|
+
const { _isSecretKey, _isPlaceholderSecret, _looksSecretLike, redactSecrets, stripDefaultIgnorables, _mergeLines, _mergeEnvLines, _mergeReadmeSection, _managedMerge, _parseSkillsValue, _parseArchiveBlocks, _parseSkillCatalog, _renderTeamsMd, _composeTeamPlan, _teamHandoffReminders, _cadenceAssessment, _teamDeployGate, _renderWorkspaceReferenceGuide, _memorySurface, _renderPulseLine, compareVer, parseHarnessVersion, _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp,
|
|
15
15
|
PERMISSION_TIERS, _tierRank, _requiredTier, _policyAllows, _resolveNpmTag, _mcpJsonContent, _newRunRecord,
|
|
16
16
|
_htmlToText, _extractTitle, _extractLinks,
|
|
17
17
|
_countDatedBlocks, _extractDecisionBlocks, _classifyIntent,
|
|
@@ -49,7 +49,7 @@ const {
|
|
|
49
49
|
migrateLegacyWorkspace,
|
|
50
50
|
} = require('../lib/workspace-dir');
|
|
51
51
|
|
|
52
|
-
const VERSION = '1.36.
|
|
52
|
+
const VERSION = '1.36.185';
|
|
53
53
|
|
|
54
54
|
// MCP lifecycle 주소 표식은 현재 CLI 호출 한 번에만 유효하다. CLI bootstrap에서 즉시 env에서
|
|
55
55
|
// 떼어 두어 `--no-record`/hook처럼 presence 기록 함수에 도달하지 않는 경로도 후속 child에 유출하지 않는다.
|
|
@@ -661,17 +661,22 @@ function _getAutoLoopRule(root) {
|
|
|
661
661
|
return readRules(root).find(r => r.status === 'active' && /every-round/i.test(r.trigger || '')) || null;
|
|
662
662
|
} catch { return null; }
|
|
663
663
|
}
|
|
664
|
+
function _optionArgv() {
|
|
665
|
+
const end = process.argv.indexOf('--');
|
|
666
|
+
return end >= 0 ? process.argv.slice(0, end) : process.argv;
|
|
667
|
+
}
|
|
664
668
|
function arg(name, def = null) {
|
|
665
|
-
const
|
|
669
|
+
const optionArgs = _optionArgv();
|
|
670
|
+
const i = optionArgs.indexOf(name);
|
|
666
671
|
if (i >= 0) {
|
|
667
672
|
// 1.10.5 (13th 버그헌트 P3, UR-0169): 값 없음(끝) 또는 다음 토큰이 --장식플래그면 def 반환 — 기존 `argv[i+1] || true` 는 다음 --flag 를 값으로 흡수하거나 boolean true 를 누출(예: --reason 가 'true' 기록)했음. 음수값(-5 등 단일 -)은 값으로 보존.
|
|
668
|
-
const next =
|
|
673
|
+
const next = optionArgs[i + 1];
|
|
669
674
|
return (next != null && !next.startsWith('--')) ? next : def;
|
|
670
675
|
}
|
|
671
|
-
const eq =
|
|
676
|
+
const eq = optionArgs.find(a => a.startsWith(name + '=')); // --name=value 형태 (외부리뷰 CV-1/UR-0076)
|
|
672
677
|
return eq ? eq.slice(name.length + 1) : def;
|
|
673
678
|
}
|
|
674
|
-
function has(name) { return
|
|
679
|
+
function has(name) { return _optionArgv().includes(name); }
|
|
675
680
|
// 1.36.82: 반복 지정 값 플래그를 `--flag=v` 형태까지 포함해 수집하고, **값 누락도 보고**한다.
|
|
676
681
|
// (검수 High#4) 값 없는 `--require-referee` 나 `--require-referee=` 를 조용히 버리면
|
|
677
682
|
// CI 의 `--require-referee=$EMPTY_ENV` 가 아무 게이팅 없이 exit 0 — fail-open 이다.
|
|
@@ -679,7 +684,7 @@ function has(name) { return process.argv.includes(name); }
|
|
|
679
684
|
function _argAllStrict(name) {
|
|
680
685
|
const values = [];
|
|
681
686
|
let missing = 0;
|
|
682
|
-
const a =
|
|
687
|
+
const a = _optionArgv();
|
|
683
688
|
for (let i = 0; i < a.length; i++) {
|
|
684
689
|
if (a[i] === name) {
|
|
685
690
|
const next = a[i + 1];
|
|
@@ -717,7 +722,13 @@ const _VALUE_FLAGS = new Set(['--language','--skills','--path','--status','--pro
|
|
|
717
722
|
'--files-changed','--files-read','--members','--only','--personas','--priority','--purpose','--schedule','--mode',
|
|
718
723
|
// --approved-by 는 `agents route` 가 값으로 쓴다(자체 인라인 목록에만 있었다) — 부울로 오분류하면 값이 토큰으로 샌다.
|
|
719
724
|
// --title 도 같다(_TASK_VALUE_FLAGS 소속). 둘 다 완전성 가드가 잡아냈다 — 손으로 분류한 것은 손으로 틀린다.
|
|
720
|
-
'--approved-by', '--title'
|
|
725
|
+
'--approved-by', '--title',
|
|
726
|
+
// T-0172 역할 폴백·실행 provenance: 값 플래그를 중앙 파서에도 등록한다.
|
|
727
|
+
'--session-id','--session-provider','--session-model','--session-model-family',
|
|
728
|
+
'--model-family','--agent-model','--agent-model-family','--agent-session',
|
|
729
|
+
'--candidate','--candidate-family','--remove-candidate','--policy','--fallback-policy',
|
|
730
|
+
'--auth-state','--entitlement','--quota-state','--policy-state','--reachability',
|
|
731
|
+
'--retry-after','--expires-at','--ttl-min','--reason','--session','--ttl']);
|
|
721
732
|
// 1.36.103 (codex 검수 #10/#11): `brief set` 은 플래그를 정적 리터럴이 아니라 레지스트리에서 만든다
|
|
722
733
|
// (`arg('--' + f.flag)`). 정적 추출로는 보이지 않아 `brief set --intro hello` 가 "알 수 없는 플래그" 오탐을 냈다.
|
|
723
734
|
// 손으로 10개를 베껴 적으면 그 목록이 또 뒤처진다 — 레지스트리에서 **유도**한다(이 라운드의 요지 그대로).
|
|
@@ -728,7 +739,7 @@ const _BOOL_FLAGS = new Set(['--ai','--all','--all-apps','--baseline','--all-pre
|
|
|
728
739
|
// 소스에 리터럴로만 존재해 소비 형태를 기계 추출하지 못한 것들 — 실재하는 플래그이므로 오타로 오인하면 안 된다
|
|
729
740
|
'--commands','--decision','--errors','--fix','--list','--notes','--print','--raw','--select-all','--skip-git-repo-check','--tests','--verbose','--yolo',
|
|
730
741
|
// lib/*.js 의 부울 소비자 — bin/ 만 훑었다면 `agents route --confirm` 같은 실재 문법에 오탐 경고를 냈을 것이다
|
|
731
|
-
'--confirm','--dispatch','--execute','--ladder','--log','--multi','--no-env-check','--no-feature-check','--no-gitignore-check','--no-npm-audit','--no-secret-scan','--readonly','--write']);
|
|
742
|
+
'--confirm','--dispatch','--execute','--ladder','--log','--multi','--no-env-check','--no-feature-check','--no-gitignore-check','--no-npm-audit','--no-secret-scan','--readonly','--write','--clear-candidates','--rate-limited','--not-rate-limited']);
|
|
732
743
|
// 1.36.103: 하위명령이 '선택' 인 명령에서 `<cmd> <경로>` 의 경로가 하위명령 자리로 읽히던 것 차단.
|
|
733
744
|
// tech/library 는 경로가 args[1] 인데 root 해석이 _taskPositionalPath(args, 2) 로 한 칸 늦게 시작해
|
|
734
745
|
// positional 을 통째로 건너뛰고 cwd 에 썼다(실측: `tech <TARGET>` 이 CWD 에 tech-profile.json 생성).
|
|
@@ -759,6 +770,10 @@ function _nonFlagArgsPass(booleanValueFlags = new Set()) {
|
|
|
759
770
|
const a = process.argv.slice(2);
|
|
760
771
|
for (let i = 0; i < a.length; i++) {
|
|
761
772
|
const x = a[i];
|
|
773
|
+
if (x === '--') {
|
|
774
|
+
for (let j = i + 1; j < a.length; j++) out.push(a[j]);
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
762
777
|
if (x.startsWith('-')) {
|
|
763
778
|
const name = x.split('=')[0];
|
|
764
779
|
if (withValue.has(name) && !booleanValueFlags.has(name) && !x.includes('=') && a[i+1] && !a[i+1].startsWith('-')) i++;
|
|
@@ -792,7 +807,7 @@ function nonFlagArgs() {
|
|
|
792
807
|
return overrides.size ? _nonFlagArgsPass(overrides) : initial;
|
|
793
808
|
}
|
|
794
809
|
function argAll(name) {
|
|
795
|
-
const out = []; const a =
|
|
810
|
+
const out = []; const a = _optionArgv();
|
|
796
811
|
for (let i = 0; i < a.length; i++) if (a[i] === name && a[i+1] && !a[i+1].startsWith('-')) out.push(a[++i]);
|
|
797
812
|
return out;
|
|
798
813
|
}
|
|
@@ -1262,7 +1277,7 @@ leerness drift check . # 4 신호 + 4단계 레벨
|
|
|
1262
1277
|
## Step 3. 업무 분배 — sub-agent 매핑
|
|
1263
1278
|
\`\`\`bash
|
|
1264
1279
|
leerness agents list # ready CLI 확인
|
|
1265
|
-
leerness agents quota #
|
|
1280
|
+
leerness agents quota # 공급자 상태 관측(잔여량은 검증된 공식 어댑터가 있을 때만)
|
|
1266
1281
|
leerness agents dispatch "<task>" --to <id> # 작업 유형 추천 자동
|
|
1267
1282
|
\`\`\`
|
|
1268
1283
|
- 작업 유형별 기본 sub-agent 매핑 (leerness 의 **기본 관례**이며 측정된 성능·품질 순위가 아닙니다):
|
|
@@ -1435,7 +1450,7 @@ leerness memory restore <surface> <target> # archive → active 복귀 (DELETE
|
|
|
1435
1450
|
_files['.leerness/feature-contracts.md'] = fm('feature-contracts', ['before implementing/changing features'], ['feature IO/state/error changes'], `# Feature Contracts\n\n## Template\n- Feature:\n- Input:\n- Output:\n- States:\n- Errors:\n- Related files:\n- Test evidence ID:\n`, 'en');
|
|
1436
1451
|
_files['.cursor/rules/leerness.mdc'] = `${MARK}\n---\nalwaysApply: true\n---\nFollow AGENTS.md and .leerness/context-routing.md.\nRun: \`leerness handoff .\` at session start.\nRun: \`leerness session close .\` at session end.\nPreserve Leerness protected files.\n`;
|
|
1437
1452
|
_files['.github/copilot-instructions.md'] = `${MARK}\n# Copilot Instructions\n\nUse AGENTS.md and .leerness/ as project memory.\nDo not remove protected Leerness files.\nBefore completion, ensure plan.md, progress-tracker.md, current-state.md, session-handoff.md are updated.\n`;
|
|
1438
|
-
_files['.leerness/session-workflow.md'] = fm('session-workflow', ['session start', 'new user request arrives', 'before distributing complex work'], ['workflow step changes'], `# Session Workflow — 6 steps of AI harness engineering\n\n> **At the start of every session the main agent reads this document first and follows the 6 steps as written.**\n> Same flow regardless of round length/complexity — that is what prevents drift.\n\n## Step 1. Analyze the request + check the environment\n\`\`\`bash\nleerness handoff . # load context + automatic drift warning\nleerness drift check . # signals + 4-level verdict\n\`\`\`\n- Decompose the request (5W1H). If ambiguous, ask clarifying questions (except autonomous mode).\n- On drift critical, run \`leerness session close .\` or \`drift check --auto-fix\` first.\n\n## Step 2. Plan\n- 3+ steps → use TodoWrite or \`leerness plan add\`.\n- New capability → search existing assets first via \`leerness reuse-map\` / \`reuse find <query>\`.\n- Multiple modules → define an integration spec up front (e.g. TICK_SPEC.md).\n\n## Step 3. Distribute — sub-agent mapping\n\`\`\`bash\nleerness agents list # ready CLIs\nleerness agents quota #
|
|
1453
|
+
_files['.leerness/session-workflow.md'] = fm('session-workflow', ['session start', 'new user request arrives', 'before distributing complex work'], ['workflow step changes'], `# Session Workflow — 6 steps of AI harness engineering\n\n> **At the start of every session the main agent reads this document first and follows the 6 steps as written.**\n> Same flow regardless of round length/complexity — that is what prevents drift.\n\n## Step 1. Analyze the request + check the environment\n\`\`\`bash\nleerness handoff . # load context + automatic drift warning\nleerness drift check . # signals + 4-level verdict\n\`\`\`\n- Decompose the request (5W1H). If ambiguous, ask clarifying questions (except autonomous mode).\n- On drift critical, run \`leerness session close .\` or \`drift check --auto-fix\` first.\n\n## Step 2. Plan\n- 3+ steps → use TodoWrite or \`leerness plan add\`.\n- New capability → search existing assets first via \`leerness reuse-map\` / \`reuse find <query>\`.\n- Multiple modules → define an integration spec up front (e.g. TICK_SPEC.md).\n\n## Step 3. Distribute — sub-agent mapping\n\`\`\`bash\nleerness agents list # ready CLIs\nleerness agents quota # provider status; remaining capacity only via a verified official adapter\nleerness agents dispatch "<task>" --to <id>\n\`\`\`\n- Best sub-agent by task type: text/translation/analysis → claude · deep code reasoning → codex · direct file edits → agy --yolo · security review → \`leerness review --persona security\`.\n- **Conflict-prevention rules (mandatory)**: give each sub-agent *the exact files only it may modify*; require mtime-verification reports (concurrent writes risk last-writer-wins); verify against the spec afterwards with \`leerness contract verify\`.\n\n## Step 4. Sub-agent work + individual self-verification\n- Each sub-agent reports only after passing its own module tests.\n- Report format: line counts, tests N/N PASS, issues found, mtime verification.\n\n## Step 5. Integrated verification\n\`\`\`bash\nleerness contract verify SPEC.md src/<mod>.js\nleerness verify-claim T-XXX --run-tests --strict-claims\nleerness review <file> --persona security,performance,ux\n\`\`\`\n- The main agent writes and runs its own integration scenario (independent verification).\n- Cross-check that sub-agent verification and main verification *agree*.\n\n## Step 6. Close + handoff + next-round suggestion\n\`\`\`bash\nleerness session close . # --suggest on by default\nleerness session close . --no-suggest\n\n# or separately:\nleerness skill suggest .\nleerness drift check .\nleerness audit . --fix\n\`\`\`\n\n## 🧠 Memory CRUD Quick Reference\n\n| Surface | CREATE | READ | DELETE | RESTORE |\n|---|---|---|---|---|\n| **tasks** | task add | task list --json | task drop | task update |\n| **decisions** | decision add | decision list --json | decision drop | memory restore decisions |\n| **lessons** | lesson save | lesson list [--tag] | lesson drop | memory restore lessons |\n| **plan** | plan add | plan list --json | plan remove | memory restore plan |\n| **rules** | rule add | rule list --json | rule remove | (rule pause/resume) |\n\n\`\`\`bash\nleerness memory status [--json]\nleerness memory archive list [--surface s]\nleerness memory restore <surface> <target>\n\`\`\`\n\n**Recovering a wrongly saved item**:\n1. \`memory archive list\` — list restore candidates\n2. \`memory restore decisions "PostgreSQL"\` — archive → active\n3. handoff automatically notes archive activity within 24h every session\n\n## Auto-recovery & security\n- A skipped session close triggers drift critical at the next session start — recover with \`drift check --auto-fix\` (runs session close automatically on critical).\n- handoff auto-recalls past lessons (by current task keywords) and auto-suggests installed skills.\n- handoff prints a 1–2 line security summary (.env ↔ .env.example sync + .gitignore secret gaps); a missing .env in .gitignore is 🚨 CRITICAL — \`LEERNESS_AUTO_SECURITY_FIX=1\` runs \`audit --fix\` automatically.\n- \`leerness health\` gives a one-line combined check (drift + security + skills + usage + tasks).\n\n---\n\n## Quick checklist (before the session ends)\n- [ ] This round's tasks are registered in plan/progress-tracker (or task sync)\n- [ ] Every done item has evidence attached (verify-claim PASS)\n- [ ] contract verify PASS when sub-agents were used\n- [ ] drift score ≤ 30 — \`leerness drift check\`\n- [ ] session close was run\n- [ ] \`leerness health\` overall check\n- [ ] If .env is used: .gitignore secret patterns OK + .env.example synced\n- [ ] On security critical: \`LEERNESS_AUTO_SECURITY_FIX=1\` or \`audit --fix\`\n\n## Anti-patterns (drift signals)\n- ⚠ "work is done, just report and stop" → missed session close → drift critical next session\n- ⚠ "only TodoWrite updated, leerness unused" → \`task sync --from\` or \`task add\` is mandatory\n- ⚠ distributing to sub-agents without explicit file paths → concurrent-write conflicts\n- ⚠ self-reported "tests ran, PASS" only → verify-claim --run-tests not executed\n- ⚠ skipping contract verify → spec-mismatch bugs reach the user\n`, 'en');
|
|
1439
1454
|
}
|
|
1440
1455
|
// T-0136: handoff is a tracked-read boundary. The localized templates predate that contract,
|
|
1441
1456
|
// so normalize the one stale sentence after language selection from a single place.
|
|
@@ -4350,8 +4365,8 @@ function _selfTestCases() {
|
|
|
4350
4365
|
&& JSON.stringify(codexBench.args) === JSON.stringify(['exec', '--skip-git-repo-check', 'probe']);
|
|
4351
4366
|
// (4) 1.36.29 (사용자 보고): multi 도 브리프 접두 — --execute 실 spawn(_cliChat) + 명령 목록/JSON 3지점 전부 briefTask.
|
|
4352
4367
|
const multiWired = agentsSrc.includes('_cliChat(root, def.id, briefTask,')
|
|
4353
|
-
&& agentsSrc.includes('_dispatchCommand(x.def.id, briefTask, writeMode)')
|
|
4354
|
-
&& agentsSrc.includes('_dispatchCommand(def.id, briefTask, writeMode)');
|
|
4368
|
+
&& agentsSrc.includes('_dispatchCommand(x.def.id, briefTask, writeMode, null, x.def)')
|
|
4369
|
+
&& agentsSrc.includes('_dispatchCommand(def.id, briefTask, writeMode, null, def)');
|
|
4355
4370
|
return briefOk && wired && stdinFixed && multiWired;
|
|
4356
4371
|
} },
|
|
4357
4372
|
{ name: '19th GPT5.5평가 (UR-0013): _parseTestStdout 러너 인식(jest/tap/spec/비율) + declared-pass 부풀림 3경로 게이팅 + json ok/reasons (1.35.7)', run: () => {
|
|
@@ -5140,7 +5155,7 @@ function _selfTestCases() {
|
|
|
5140
5155
|
{ name: 'UR-0025 심층(Codex 위임·검증): anti-laziness catalog→lib/catalogs(OPTIMISM_PATTERNS) + optimism 순수로직→pure-utils 분리 (1.9.336)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = Array.isArray(c.OPTIMISM_PATTERNS) && c.OPTIMISM_PATTERNS.length === 10 && c.OPTIMISM_PATTERNS[0].kind === 'API'; const ev = 'API 호출 완료, POST /users'; const sus = m._detectOptimism(c.OPTIMISM_PATTERNS, ev, 'function x(){}'); const conf = m._computeConfidence(c.OPTIMISM_PATTERNS, ev, 'function x(){}'); const work = sus.some(s => s.kind === 'API' && s.severity === 'high') && conf < 0.5 && m._computeConfidence(c.OPTIMISM_PATTERNS, '정리함', 'x') === 1 && m._detectOptimism(null, ev, 'x').length === 0 && m._extractUrlClaims('POST /a').length === 1 && m._verifyUrlClaim({ path: '/a' }, 'has /a') === true; const src = read(__filename); const moved = OPTIMISM_PATTERNS === c.OPTIMISM_PATTERNS && _puDetectOptimism === m._detectOptimism && !/const OPTIMISM_PATTERNS = \[/.test(src) && !/function _extractUrlClaims\(/.test(src); return catOk && work && moved; } },
|
|
5141
5156
|
{ name: 'UR-0025 심층: persona catalog→lib/catalogs(BUILT_IN_PERSONAS) + _personaSummaries→pure-utils 분리 (1.9.337)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c.BUILT_IN_PERSONAS && Object.keys(c.BUILT_IN_PERSONAS).length === 5 && c.BUILT_IN_PERSONAS.security && typeof c.BUILT_IN_PERSONAS.security.body === 'string'; const sm = m._personaSummaries(c.BUILT_IN_PERSONAS); const work = Array.isArray(sm) && sm.length === 5 && sm[0].id === 'security' && sm[0].body === undefined && typeof sm[0].name === 'string' && m._personaSummaries(null).length === 0; const src = read(__filename); const moved = BUILT_IN_PERSONAS === c.BUILT_IN_PERSONAS && _personaSummaries === m._personaSummaries && !/const BUILT_IN_PERSONAS = \{/.test(src); return catOk && work && moved; } },
|
|
5142
5157
|
{ name: 'UR-0025 심층: i18n STRINGS catalog→lib/catalogs + _translate→pure-utils 분리 (1.9.338)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c.STRINGS && typeof c.STRINGS['common.ready'] === 'object' && c.STRINGS['common.ready'].en === 'Ready'; const work = m._translate(c.STRINGS, 'common.ready', 'en') === 'Ready' && m._translate(c.STRINGS, 'common.ready', 'ko') === '준비 완료' && m._translate(c.STRINGS, 'no.such.key', 'en') === 'no.such.key' && m._translate(null, 'x', 'ko') === 'x' && m._translate({ k: { ko: '케이' } }, 'k', 'en') === '케이'; const src = read(__filename); const moved = STRINGS === c.STRINGS && _translate === m._translate && !/const STRINGS = \{/.test(src); return catOk && work && moved; } },
|
|
5143
|
-
{ name: 'UR-0053: decisions canonical JSON 레이어(_loadDecisions/_saveDecisions/decisionsJsonPath) + pure 파서/렌더 round-trip (1.9.339)', run: () => { const m = require('../lib/pure-utils'); const md = '# Decisions\n\n### 2026-06-05 — A\n- Decision: a\n- Reason: r\n- Alternatives: alt\n- Impact: imp\n\n### 2026-06-04 — B\n- Decision: b\n- Alternatives:\n'; const objs = m._decisionsFromMd(md); const parseOk = objs.length === 2 && objs[0].alternatives === 'alt' && objs[0].impact === 'imp' && objs[1].alternatives === null && objs[1].title === 'B'; const rt = m._decisionsFromMd(m._renderDecisionsMd(objs)); const rtOk = JSON.stringify(rt) === JSON.stringify(objs); const tplOk = m._decisionsFromMd(m._renderDecisionsMd([])).length === 0; const layerOk = typeof _loadDecisions === 'function' && typeof _saveDecisions === 'function' && typeof decisionsJsonPath === 'function' && _decisionsFromMd === m._decisionsFromMd; return parseOk && rtOk && tplOk && layerOk; } },
|
|
5158
|
+
{ name: 'UR-0053: decisions canonical JSON 레이어(_loadDecisions/_saveDecisions/decisionsJsonPath) + pure 파서/렌더 round-trip (1.9.339)', run: () => { const m = require('../lib/pure-utils'); const md = '# Decisions\n\n### 2026-06-05 — A\n- Decision: a\n- Reason: r\n- Alternatives: alt\n- Impact: imp\n\n### 2026-06-04 — B\n- Decision: b\n- Alternatives:\n'; const objs = m._decisionsFromMd(md); const parseOk = objs.length === 2 && objs[0].alternatives === 'alt' && objs[0].impact === 'imp' && objs[1].alternatives === null && objs[1].title === 'B'; const rt = m._decisionsFromMd(m._renderDecisionsMd(objs)); const rtOk = JSON.stringify(rt) === JSON.stringify(objs); const tplOk = m._decisionsFromMd(m._renderDecisionsMd([])).length === 0; const noTrailing = !/[ \t]+$/m.test(m._renderDecisionsMd(objs)); const layerOk = typeof _loadDecisions === 'function' && typeof _saveDecisions === 'function' && typeof decisionsJsonPath === 'function' && _decisionsFromMd === m._decisionsFromMd; return parseOk && rtOk && tplOk && noTrailing && layerOk; } },
|
|
5144
5159
|
{ name: 'UR-0058: lessons canonical JSON 레이어(_loadLessons/_saveLessons/lessonsJsonPath) + pure 파서/렌더 round-trip', run: () => { const m = require('../lib/pure-utils'); const objs = [{ date: '2026-06-05', text: 'A', tag: 't' }, { date: '2026-06-04', text: 'B', tag: null }]; const rt = m._parseLessonEntries(m._renderLessonsMd(objs)); const rtOk = JSON.stringify(rt) === JSON.stringify(objs); const tplOk = m._parseLessonEntries(m._renderLessonsMd([])).length === 0; const layerOk = typeof _loadLessons === 'function' && typeof _saveLessons === 'function' && typeof lessonsJsonPath === 'function' && _renderLessonsMd === m._renderLessonsMd; return rtOk && tplOk && layerOk; } },
|
|
5145
5160
|
{ name: 'UR-0025 심층: BUILTIN_CATALOG→lib/catalogs + _withBuiltinSource→pure-utils 분리 (1.9.341)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c.BUILTIN_CATALOG && Object.keys(c.BUILTIN_CATALOG).length === 2 && c.BUILTIN_CATALOG['feature-implementation'] && c.BUILTIN_CATALOG['project-roadmap-generator'] && Object.values(c.BUILTIN_CATALOG).every(v => v.verification !== 'passed'); const out = m._withBuiltinSource(c.BUILTIN_CATALOG); const work = Object.keys(out).length === 2 && Object.values(out).every(v => v._source === 'builtin') && out['feature-implementation'].version === '1.0.0' && Array.isArray(out['feature-implementation'].capabilities) && Object.keys(m._withBuiltinSource(null)).length === 0; const src = read(__filename); const moved = BUILTIN_CATALOG === c.BUILTIN_CATALOG && _withBuiltinSource === m._withBuiltinSource && !/const BUILTIN_CATALOG = \{/.test(src); return catOk && work && moved; } },
|
|
5146
5161
|
{ name: 'UR-0025 심층: ROADMAP_STATUS_LABEL/COLOR→lib/catalogs 분리 (1.9.342)', run: () => { const c = require('../lib/catalogs'); const lblOk = c.ROADMAP_STATUS_LABEL && Object.keys(c.ROADMAP_STATUS_LABEL).length === 11 && c.ROADMAP_STATUS_LABEL.done === '완료' && c.ROADMAP_STATUS_LABEL.blocked === '오류'; const colOk = c.ROADMAP_STATUS_COLOR && Object.keys(c.ROADMAP_STATUS_COLOR).length === 11 && c.ROADMAP_STATUS_COLOR.done === '#16a34a' && c.ROADMAP_STATUS_COLOR.skill === '#8b5cf6'; const src = read(__filename); const moved = ROADMAP_STATUS_LABEL === c.ROADMAP_STATUS_LABEL && ROADMAP_STATUS_COLOR === c.ROADMAP_STATUS_COLOR && !/const ROADMAP_STATUS_LABEL = \{/.test(src); return lblOk && colOk && moved; } },
|
|
@@ -5622,9 +5637,12 @@ function _selfTestCases() {
|
|
|
5622
5637
|
const src = read(__filename);
|
|
5623
5638
|
return guard(hSrc) && guard(dSrc) && guard(src); // status(bin) 도 동일 가드
|
|
5624
5639
|
} },
|
|
5625
|
-
{ name: '11th 외부평가 Codex P2 (UR-0137): agents
|
|
5626
|
-
const
|
|
5627
|
-
return
|
|
5640
|
+
{ name: '11th 외부평가 Codex P2 (UR-0137): agents task parser 가 flag 값과 사용자 본문을 분리 (1.9.435/T-0165)', run: () => {
|
|
5641
|
+
const parse = require('../lib/agents')._parseAgentPositional;
|
|
5642
|
+
return parse(['fix auth', '--to', 'codex', '--model', 'm1']) === 'fix auth'
|
|
5643
|
+
&& parse(['--to', 'codex', 'fix auth', '--role', 'coder']) === 'fix auth'
|
|
5644
|
+
&& parse(['provider', 'fix auth', '--approved-by', 'owner'], 1) === 'fix auth'
|
|
5645
|
+
&& parse(['--to=codex', '--json', '--', '-literal task']) === '-literal task';
|
|
5628
5646
|
} },
|
|
5629
5647
|
{ name: '11th 외부평가 Opus P3 (UR-0139): 시크릿 all-X/0 placeholder FP — 동일문자 8+연속은 prefix 무관 더미 (1.9.436)', run: () => {
|
|
5630
5648
|
const m = require('../lib/pure-utils');
|
|
@@ -11465,8 +11483,8 @@ function commandsCmd(root) {
|
|
|
11465
11483
|
{ cmd: 'session close [path] [--json] [--auto-apply-delivered]', desc: '세션 마감 + 9 카테고리 + 자동 통합', descEn: 'close the session + 9 categories + auto integration' },
|
|
11466
11484
|
{ cmd: 'resume [path]', desc: 'auto-resume-plan 적용 (1.9.203)', descEn: 'apply the auto-resume plan (1.9.203)' },
|
|
11467
11485
|
{ cmd: 'route <task-type>', desc: '작업 유형 분류 (11종)', descEn: 'classify the task type (11 kinds)' },
|
|
11468
|
-
{ cmd: 'agents list|check|quota|dispatch|multi|recommend', desc: '외부 AI CLI 오케스트레이션
|
|
11469
|
-
{ cmd: 'roles list|set|unset|catalog|suggest|verify', desc: '모델별 역할 부여
|
|
11486
|
+
{ cmd: 'agents list|check|quota|resolve|fallback|record|history|dispatch|multi|recommend', desc: '외부 AI CLI 오케스트레이션 + 역할 가용성·폴백 선택·실행 provenance', descEn: 'orchestrate external AI CLIs + role availability, fallback selection, and execution provenance' },
|
|
11487
|
+
{ cmd: 'roles list|set|unset|catalog|suggest|verify|validate', desc: '모델별 역할 부여 + fail-closed 저장소 검증 — 1.9.270/T-0171', descEn: 'assign roles and validate the role store fail-closed — 1.9.270/T-0171' },
|
|
11470
11488
|
{ cmd: 'capabilities [--json]', desc: '권한·보안 표면 공개 (무엇을 하는지 + opt-out + 주의 명령) — 1.9.272', descEn: 'disclose the permission/security surface (what it does + opt-outs + commands to watch) — 1.9.272' },
|
|
11471
11489
|
{ cmd: 'state show|start|record|verify|handoff', desc: '.leerness/ JSON 상태 substrate (에이전트 간 인수인계 표준) — 1.9.278', descEn: '.leerness/ JSON state substrate (cross-agent handoff standard) — 1.9.278' },
|
|
11472
11490
|
{ cmd: 'adapter <tool>|list [--dry-run]', desc: '도구별 지침/.mcp.json 선택 생성 (claude/cursor/codex/goose/...) — 1.9.280', descEn: 'generate per-tool instructions/.mcp.json selectively (claude/cursor/codex/goose/...) — 1.9.280' },
|
|
@@ -11485,7 +11503,7 @@ function commandsCmd(root) {
|
|
|
11485
11503
|
{ cmd: 'referee add|verify|list|show|drop', desc: '검증기 캘리브레이션 — 신뢰 전에 탐지력 증명(known-good 통과 + 망가뜨린 known-bad 를 기대 사유로 거부). verify-claim --referee / gate 게이팅 — 1.36.80 P-0001', descEn: 'verifier calibration — prove detection power before trusting it (passes known-good, rejects a broken known-bad for the expected reason). Gates verify-claim --referee / gate — 1.36.80 P-0001' },
|
|
11486
11504
|
{ cmd: 'preview add|list|show|approve|revise|mockup|verify', desc: '신규 기능 미리보기 승인 워크플로 — approve 전 코드 작성 금지 계약. mockup <P-ID> [--target <파일>] [--force]: 프로젝트의 실제 토큰·컴포넌트·대상 화면 클래스를 심은 시안 · approve 시 그 시안이 계약으로 고정됨 · verify <P-ID> --files a,b: 구현이 계약을 지켰는지 존재 대조(시각적 일치는 보증하지 않음) — 1.36.51 UR-0061 · 1.36.75 UR-0066 · 1.36.99/1.36.100 P-0012', descEn: 'preview-approval workflow for new features — contract: no code before approve. mockup <P-ID> [--target <file>] [--force]: a draft seeded with the project\'s real tokens/components/target-screen classes · approve freezes that draft as the contract · verify <P-ID> --files a,b: existence check that the implementation kept the contract (visual match is NOT guaranteed) — 1.36.51 UR-0061 · 1.36.75 UR-0066 · 1.36.99/1.36.100 P-0012' },
|
|
11487
11505
|
{ cmd: 'dashboard [path] [--port N] [--timeout SEC] [--json]', desc: '.leerness 읽기 전용 대시보드 — 토글·task·마일스톤·미리보기·라우팅기록·bugfix probe 를 localhost 에서 열람. 설정 변경·게이트 판정은 하지 않는다(CLI 가 판정). --json 은 서버 없이 같은 스냅샷 출력. 정적 단일파일 뷰(leerness.html)는 leerness graph --html 로 그대로 — 1.36.90 P-0006', descEn: 'read-only .leerness dashboard — browse toggles/tasks/milestones/previews/routing history/bugfix probes on localhost. It never changes settings or makes gate verdicts (the CLI does). --json prints the same snapshot without a server. The static single-file view (leerness.html) is still leerness graph --html — 1.36.90 P-0006' },
|
|
11488
|
-
{ cmd: 'agents route "<작업>" [--tier tiny|normal|high-risk] [--confirm --approved-by "<승인자>"] [--log]', cmdEn: 'agents route "<task>" [--tier tiny|normal|high-risk] [--confirm --approved-by "<approver>"] [--log]', desc: '작업 난이도 판정(결정적 규칙) → 추상 역할
|
|
11506
|
+
{ cmd: 'agents route "<작업>" [--tier tiny|normal|high-risk] [--confirm --approved-by "<승인자>"] [--reason "<사유>"] [--log]', cmdEn: 'agents route "<task>" [--tier tiny|normal|high-risk] [--confirm --approved-by "<approver>"] [--reason "<reason>"] [--log]', desc: '작업 난이도 판정(결정적 규칙) → 추상 역할 배치 제안. 감지된 고위험 등급 하향은 승인자+사유가 모두 필요하고, 실행은 명시 확인 필요. 토글 기본 OFF: leerness toggle set difficulty-routing on — 1.36.89 P-0007', descEn: 'judge task difficulty (deterministic rules) → propose abstract-role assignments. Lowering detected high risk requires both approver and reason, and execution needs explicit confirmation. Toggle is OFF by default: leerness toggle set difficulty-routing on — 1.36.89 P-0007' },
|
|
11489
11507
|
{ cmd: 'bugfix start|receipt|drop|list', desc: 'bugfix 완료 영수증 — 수정 전 재현 probe 를 등록(2회 연속 실패해야 등록)하고, done 전이에서 통과 + 근본원인/형제범위 영수증을 요구. 토글 기본 OFF: leerness toggle set bugfix-receipt on — 1.36.87 P-0005', descEn: 'bugfix completion receipt — register a reproduction probe before the fix (it must fail twice in a row to register), then require it to pass at the done transition plus a root-cause/sibling-scope receipt. Toggle is OFF by default: leerness toggle set bugfix-receipt on — 1.36.87 P-0005' },
|
|
11490
11508
|
{ cmd: 'review <file> --persona <ids>', desc: '페르소나 리뷰 (1.9.29)', descEn: 'persona-based review (1.9.29)' },
|
|
11491
11509
|
{ cmd: 'brainstorm "<topic>" [--include-code]', desc: '워크스페이스 회수 + 코드 grep', descEn: 'recall from the workspace + grep the code' }
|
|
@@ -11512,6 +11530,7 @@ function commandsCmd(root) {
|
|
|
11512
11530
|
{ cmd: 'update [--check|--yes|--force]', desc: '자가 업데이트', descEn: 'self-update' },
|
|
11513
11531
|
{ cmd: 'wakeup-interval get|set|auto|history|record', desc: 'adaptive wakeup (1.9.210)', descEn: 'adaptive wakeup interval (1.9.210)' },
|
|
11514
11532
|
{ cmd: 'sessions [path] [--json]', desc: '이 프로젝트에 남은 **다른 AI 세션 기록** (1.36.129, P-0016 P1) — 기록만 보여 주고 프로세스 상태는 판정하지 않는다', descEn: 'session records left by other AI sessions in this project (1.36.129, P-0016 P1) — records only; process state is never judged' },
|
|
11533
|
+
{ cmd: 'lease acquire|release|list|check [file|lease-id] [--session KEY] [--ttl SEC] [--json]', desc: '명시적 exact-file TTL lease — 동기식 충돌만 반환하는 opt-in 협업 힌트(외부 편집기 강제 잠금·ambient 경고 없음)', descEn: 'explicit exact-file TTL lease — opt-in synchronous coordination hint (no external-editor lock and no ambient warnings)' },
|
|
11515
11534
|
{ cmd: 'workspace-dir get|guide', desc: `워크스페이스 디렉토리 확인 — ${CANONICAL_WORKSPACE_DIR} canonical + 기존 ${LEGACY_WORKSPACE_DIR} 안전 마이그레이션 (T-0107)`, descEn: `show the workspace directory — canonical ${CANONICAL_WORKSPACE_DIR} + safe legacy ${LEGACY_WORKSPACE_DIR} migration (T-0107)` },
|
|
11516
11535
|
{ cmd: 'parent detect|adopt [--select <kinds>] [--apply]', desc: '상위 leerness 부모 탐지 + 자산 게이트형 adopt (1.30.2~3)', descEn: 'detect a parent leerness above + gated asset adoption (1.30.2~3)' },
|
|
11517
11536
|
{ cmd: 'intent classify|expand|domains "<request>"', desc: '의도 파악 + scope (1.9.213)', descEn: 'infer intent + scope (1.9.213)' },
|
|
@@ -12159,6 +12178,9 @@ const _OBSERVATION_ONLY_SUBCOMMANDS = new Map([
|
|
|
12159
12178
|
['release', new Set(['cadence'])],
|
|
12160
12179
|
['auto-update', new Set(['status'])],
|
|
12161
12180
|
['preview', new Set(['list', 'show'])],
|
|
12181
|
+
['lease', new Set(['list', 'check'])],
|
|
12182
|
+
['roles', new Set(['validate'])],
|
|
12183
|
+
['role', new Set(['validate'])],
|
|
12162
12184
|
]);
|
|
12163
12185
|
function _cliMutationClass(args, cmd) {
|
|
12164
12186
|
if (cmd === 'mcp') return 'observation-only'; // server startup must not mutate its incidental cwd
|
|
@@ -12167,6 +12189,7 @@ function _cliMutationClass(args, cmd) {
|
|
|
12167
12189
|
// Classify the alias before auto-migration/telemetry so adding --json/--path
|
|
12168
12190
|
// cannot turn a read into a write merely because no child token exists.
|
|
12169
12191
|
if (cmd === 'preview' && (!args || !args[1] || String(args[1]).startsWith('-'))) return 'observation-only';
|
|
12192
|
+
if (cmd === 'lease' && (!args || !args[1] || String(args[1]).startsWith('-'))) return 'observation-only';
|
|
12170
12193
|
const children = _OBSERVATION_ONLY_SUBCOMMANDS.get(cmd);
|
|
12171
12194
|
return children && args && children.has(args[1]) ? 'observation-only' : 'stateful';
|
|
12172
12195
|
}
|
|
@@ -13954,6 +13977,122 @@ function _agentReminderSignal(root) {
|
|
|
13954
13977
|
String(s).replace(new RegExp('[\u0000-\u001f\u007f]', 'g'), ' ').replace(/^[=\-]{3,}/, ''), 200).trim();
|
|
13955
13978
|
return { path: rel(root, p), lines: lines.length, truncated, head: lines.slice(0, 6).map(l => _clean(l).slice(0, 200)).filter(Boolean) };
|
|
13956
13979
|
}
|
|
13980
|
+
// Exact-file lease is deliberately separate from `sessions scope`. The discarded ambient
|
|
13981
|
+
// scope advisory exceeded its declared noise ceiling; this command acts only after an
|
|
13982
|
+
// explicit acquire/check/release request for one exact file and never injects warnings
|
|
13983
|
+
// into handoff, status, or dashboards.
|
|
13984
|
+
function fileLeaseCmd(root, action, subject, extraOperands) {
|
|
13985
|
+
const L = require('../lib/file-leases');
|
|
13986
|
+
const SP = require('../lib/session-presence');
|
|
13987
|
+
root = absRoot(root || process.cwd());
|
|
13988
|
+
action = action == null ? 'list' : action;
|
|
13989
|
+
const ui = _uiLang(root);
|
|
13990
|
+
const t = (ko, en) => ui === 'en' ? en : ko;
|
|
13991
|
+
const json = has('--json');
|
|
13992
|
+
const safeArg = (value) => _lineSafe(String(value == null ? '' : value)).slice(0, 120);
|
|
13993
|
+
const common = ['--path', '--json', '--language', '--no-stale-check', '--no-banner'];
|
|
13994
|
+
const allowed = {
|
|
13995
|
+
acquire: ['--session', '--ttl', '--note'],
|
|
13996
|
+
release: ['--session'],
|
|
13997
|
+
list: ['--session', '--all'],
|
|
13998
|
+
check: ['--session'],
|
|
13999
|
+
}[action];
|
|
14000
|
+
if (!allowed) {
|
|
14001
|
+
const badAction = safeArg(action || '(missing)');
|
|
14002
|
+
const payload = { ok: false, code: 'unknown_subcommand', error: `Unknown lease subcommand: ${badAction}`, choices: ['acquire', 'release', 'list', 'check'] };
|
|
14003
|
+
if (json) log(JSON.stringify(payload, null, 2));
|
|
14004
|
+
else fail(t(`알 수 없는 lease 하위명령: ${badAction} (가능: acquire, release, list, check)`, `unknown lease subcommand: ${badAction} (choices: acquire, release, list, check)`));
|
|
14005
|
+
process.exitCode = 1;
|
|
14006
|
+
return payload;
|
|
14007
|
+
}
|
|
14008
|
+
if (!_rejectUnknownFlags(allowed, 'lease acquire|release|list|check', { globals: common })) return;
|
|
14009
|
+
if (Array.isArray(extraOperands) && extraOperands.length > 0) {
|
|
14010
|
+
const stray = safeArg(extraOperands[0]);
|
|
14011
|
+
const payload = { ok: false, code: 'too_many_arguments', error: `Unexpected lease argument: ${stray}` };
|
|
14012
|
+
if (json) log(JSON.stringify(payload, null, 2));
|
|
14013
|
+
else fail(t(`불필요한 lease 인자: ${stray}`, `unexpected lease argument: ${stray}`));
|
|
14014
|
+
process.exitCode = 1;
|
|
14015
|
+
return payload;
|
|
14016
|
+
}
|
|
14017
|
+
if ((action === 'list' && subject) || ((action === 'acquire' || action === 'release' || action === 'check') && !subject)) {
|
|
14018
|
+
const code = action === 'list' ? 'too_many_arguments' : (action === 'release' ? 'missing_lease_id' : 'missing_file');
|
|
14019
|
+
const payload = { ok: false, code, error: action === 'list' ? `Unexpected lease argument: ${safeArg(subject)}` : `lease ${action} requires ${action === 'release' ? 'a lease ID' : 'a file path'}.` };
|
|
14020
|
+
if (json) log(JSON.stringify(payload, null, 2));
|
|
14021
|
+
else fail(t(`lease ${action} 인자 오류`, `invalid lease ${action} arguments`));
|
|
14022
|
+
process.exitCode = 1;
|
|
14023
|
+
return payload;
|
|
14024
|
+
}
|
|
14025
|
+
|
|
14026
|
+
const sessionKey = (required) => {
|
|
14027
|
+
const explicit = arg('--session', null);
|
|
14028
|
+
if (explicit != null) return L.normalizeSessionKey(explicit);
|
|
14029
|
+
if (!required) return null;
|
|
14030
|
+
if (typeof SP.invalidExplicitKey === 'function' && SP.invalidExplicitKey(process.env) != null) {
|
|
14031
|
+
throw new L.LeaseError('invalid_session_key', 'LEERNESS_SESSION_ID is explicitly set but invalid; refusing ambient fallback.');
|
|
14032
|
+
}
|
|
14033
|
+
return L.normalizeSessionKey(SP.deriveSessionKey(process.env));
|
|
14034
|
+
};
|
|
14035
|
+
const errorPayload = (error) => {
|
|
14036
|
+
if (error instanceof L.LeaseError) {
|
|
14037
|
+
return { ok: false, code: error.code, error: error.message, ...(error.details ? { details: error.details } : {}) };
|
|
14038
|
+
}
|
|
14039
|
+
return { ok: false, code: 'lease_internal_error', error: String(error && error.message || error || 'unknown error').slice(0, 300) };
|
|
14040
|
+
};
|
|
14041
|
+
try {
|
|
14042
|
+
let result;
|
|
14043
|
+
if (action === 'acquire') {
|
|
14044
|
+
if (!subject) throw new L.LeaseError('missing_file', 'lease acquire requires a file path.');
|
|
14045
|
+
result = L.acquire(root, subject, sessionKey(true), {
|
|
14046
|
+
ttlSeconds: arg('--ttl', null), note: arg('--note', null), withLock: _withLock,
|
|
14047
|
+
});
|
|
14048
|
+
} else if (action === 'release') {
|
|
14049
|
+
if (!subject) throw new L.LeaseError('missing_lease_id', 'lease release requires a lease ID.');
|
|
14050
|
+
result = L.release(root, subject, sessionKey(true), { withLock: _withLock });
|
|
14051
|
+
} else if (action === 'check') {
|
|
14052
|
+
if (!subject) throw new L.LeaseError('missing_file', 'lease check requires a file path.');
|
|
14053
|
+
result = L.check(root, subject, { sessionKey: sessionKey(false) });
|
|
14054
|
+
} else {
|
|
14055
|
+
result = L.list(root, { sessionKey: sessionKey(false), all: has('--all') });
|
|
14056
|
+
}
|
|
14057
|
+
|
|
14058
|
+
if (json) {
|
|
14059
|
+
log(JSON.stringify(result, null, 2));
|
|
14060
|
+
} else if (action === 'acquire') {
|
|
14061
|
+
log(t(`✓ 파일 lease ${result.action === 'renewed' ? '갱신' : '획득'}: ${result.lease.file}`, `✓ file lease ${result.action}: ${result.lease.file}`));
|
|
14062
|
+
log(` ${result.lease.leaseId} · ${result.lease.sessionKey} · ${result.ttlSeconds}s · ${result.lease.expiresAt}`);
|
|
14063
|
+
log(t(' Leerness-aware 세션 사이의 협업 힌트이며 외부 편집기를 강제로 잠그지 않습니다.', ' Coordination hint between Leerness-aware sessions; external editors are not locked.'));
|
|
14064
|
+
} else if (action === 'release') {
|
|
14065
|
+
log(t(`✓ 파일 lease 해제: ${result.lease.file}`, `✓ file lease released: ${result.lease.file}`));
|
|
14066
|
+
log(` ${result.lease.leaseId} · ${result.lease.sessionKey}`);
|
|
14067
|
+
} else if (action === 'check') {
|
|
14068
|
+
if (result.available && result.ownedBySession) log(t(`✓ 사용 가능: ${result.target.file} (이 세션 소유)`, `✓ available: ${result.target.file} (owned by this session)`));
|
|
14069
|
+
else if (result.available) log(t(`✓ 사용 가능: ${result.target.file}`, `✓ available: ${result.target.file}`));
|
|
14070
|
+
else {
|
|
14071
|
+
fail(t(`다른 세션 lease: ${result.target.file}`, `leased by another session: ${result.target.file}`));
|
|
14072
|
+
log(` ${result.lease.leaseId} · ${result.lease.sessionKey} · ${result.lease.expiresAt}`);
|
|
14073
|
+
}
|
|
14074
|
+
} else {
|
|
14075
|
+
log(t('# 파일 lease', '# file leases'));
|
|
14076
|
+
log(t(`저장 ${result.totalStored} · 활성 ${result.active} · 만료 ${result.expired} · 표시 ${result.shown}`, `stored ${result.totalStored} · active ${result.active} · expired ${result.expired} · shown ${result.shown}`));
|
|
14077
|
+
for (const lease of result.leases) log(` ${lease.status === 'active' ? '●' : '○'} ${lease.file} · ${lease.sessionKey} · ${lease.leaseId} · ${lease.expiresAt}`);
|
|
14078
|
+
if (!result.leases.length) log(t(' lease 없음.', ' no leases.'));
|
|
14079
|
+
}
|
|
14080
|
+
if (action === 'check' && result.available === false) process.exitCode = 1;
|
|
14081
|
+
return result;
|
|
14082
|
+
} catch (error) {
|
|
14083
|
+
const payload = errorPayload(error);
|
|
14084
|
+
if (json) log(JSON.stringify(payload, null, 2));
|
|
14085
|
+
else if (payload.code === 'lease_conflict' && payload.details && payload.details.conflict) {
|
|
14086
|
+
const c = payload.details.conflict;
|
|
14087
|
+
fail(t(`파일 lease 충돌: ${c.file} · 소유 세션 ${c.sessionKey} · 만료 ${c.expiresAt}`, `file lease conflict: ${c.file} · owner ${c.sessionKey} · expires ${c.expiresAt}`));
|
|
14088
|
+
} else {
|
|
14089
|
+
fail(t(`lease 오류(${payload.code}): ${payload.error}`, `lease error (${payload.code}): ${payload.error}`));
|
|
14090
|
+
}
|
|
14091
|
+
process.exitCode = 1;
|
|
14092
|
+
return payload;
|
|
14093
|
+
}
|
|
14094
|
+
}
|
|
14095
|
+
|
|
13957
14096
|
// 1.36.129 (UR-0068 / P-0016 P1): `leerness sessions` — 이 프로젝트에 남은 세션 **기록**을 보여 준다.
|
|
13958
14097
|
// ⚠ 문구 원칙: 검증하지 않은 것을 주장하지 않는다. "활성/active" 라는 말을 쓰지 않고,
|
|
13959
14098
|
// close 기록이 없다는 사실만 말한다. 그리고 **못 보는 것을 항상 함께 적는다** —
|
|
@@ -15293,8 +15432,11 @@ function _argvLongFlagNames() {
|
|
|
15293
15432
|
}
|
|
15294
15433
|
|
|
15295
15434
|
// T-0144: 모든 명령에서 이름 자체가 없는 플래그는 migration/usage/write 이전에 fail-closed.
|
|
15296
|
-
function _rejectUnregisteredFlags() {
|
|
15297
|
-
const
|
|
15435
|
+
function _rejectUnregisteredFlags(cmd) {
|
|
15436
|
+
const seen = _argvLongFlagNames();
|
|
15437
|
+
const unknown = seen.filter(name => !_VALUE_FLAGS.has(name) && !_BOOL_FLAGS.has(name));
|
|
15438
|
+
const leaseOnlyForeign = cmd === 'lease' ? [] : seen.filter(name => name === '--session' || name === '--ttl');
|
|
15439
|
+
for (const name of leaseOnlyForeign) if (!unknown.includes(name)) unknown.push(name);
|
|
15298
15440
|
if (!unknown.length) return true;
|
|
15299
15441
|
const en = _errorUiLang(process.cwd()) === 'en';
|
|
15300
15442
|
const human = en
|
|
@@ -15340,9 +15482,20 @@ function _rejectMalformedFlagForms(cmd, args) {
|
|
|
15340
15482
|
if (cmd === 'gate' && name === '--require-referee') continue;
|
|
15341
15483
|
errors.push({ kind: 'missing_value', name, token });
|
|
15342
15484
|
}
|
|
15343
|
-
else
|
|
15485
|
+
else {
|
|
15344
15486
|
const value = eqAt >= 0 ? token.slice(eqAt + 1) : String(raw[i + 1]);
|
|
15345
|
-
if (!valueChoices[name].has(value.toLowerCase()))
|
|
15487
|
+
if (valueChoices[name] && !valueChoices[name].has(value.toLowerCase())) {
|
|
15488
|
+
errors.push({ kind: 'invalid_value', name, token, value, choices: [...valueChoices[name]] });
|
|
15489
|
+
}
|
|
15490
|
+
else if (cmd === 'lease' && name === '--ttl') {
|
|
15491
|
+
const ttl = Number(value);
|
|
15492
|
+
if (!Number.isInteger(ttl) || ttl < 30 || ttl > 1800) errors.push({ kind: 'invalid_value', name, token, value, choices: ['integer 30..1800'] });
|
|
15493
|
+
}
|
|
15494
|
+
else if (cmd === 'lease' && name === '--session') {
|
|
15495
|
+
let valid = false;
|
|
15496
|
+
try { valid = require('../lib/session-presence').isValidKey(value); } catch { valid = false; }
|
|
15497
|
+
if (!valid) errors.push({ kind: 'invalid_value', name, token, value: _lineSafe(value), choices: ['stable 8..64 character session key'] });
|
|
15498
|
+
}
|
|
15346
15499
|
}
|
|
15347
15500
|
}
|
|
15348
15501
|
}
|
|
@@ -15367,6 +15520,27 @@ function _rejectMalformedFlagForms(cmd, args) {
|
|
|
15367
15520
|
return false;
|
|
15368
15521
|
}
|
|
15369
15522
|
|
|
15523
|
+
function _rejectDuplicateValueFlags(groups) {
|
|
15524
|
+
const raw = process.argv.slice(2);
|
|
15525
|
+
for (const groupValue of groups || []) {
|
|
15526
|
+
const group = Array.isArray(groupValue) ? groupValue : [groupValue];
|
|
15527
|
+
let count = 0;
|
|
15528
|
+
for (let i = 0; i < raw.length; i++) {
|
|
15529
|
+
const token = String(raw[i] == null ? '' : raw[i]);
|
|
15530
|
+
if (token === '--') break;
|
|
15531
|
+
const flag = group.find(name => token === name || token.startsWith(name + '='));
|
|
15532
|
+
if (!flag) continue;
|
|
15533
|
+
count += 1;
|
|
15534
|
+
if (token === flag && i + 1 < raw.length && !String(raw[i + 1]).startsWith('--')) i += 1;
|
|
15535
|
+
}
|
|
15536
|
+
if (count <= 1) continue;
|
|
15537
|
+
const label = group.join('/');
|
|
15538
|
+
failJson(has('--json'), 'duplicate_flag', `${label} may be specified only once`);
|
|
15539
|
+
return false;
|
|
15540
|
+
}
|
|
15541
|
+
return true;
|
|
15542
|
+
}
|
|
15543
|
+
|
|
15370
15544
|
// 전역 registry는 "값을 갖는가"만 답한다. 아래 audit 완료 route는 현재 명령이 그 플래그를
|
|
15371
15545
|
// 실제 소비하는지도 검사한다. 확인하지 않은 97개 표면을 추측 allowlist로 막지 않고 검증된 route부터 확장한다.
|
|
15372
15546
|
// --path 는 기존 공용 호출 래퍼가 모든 명령에 붙이는 호환 플래그다. 명령이 별도 positional 파일을
|
|
@@ -15384,11 +15558,104 @@ const _STRICT_COMMAND_FLAGS = {
|
|
|
15384
15558
|
'verify-code': { allowed: ['--build', '--bench', '--strict'], globals: [..._COMMON_COMMAND_FLAGS], usage: 'verify-code [path] [--build] [--bench] [--strict]' },
|
|
15385
15559
|
'contract verify': { allowed: ['--allow-empty'], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'contract verify <spec.md> <impl.js> [--allow-empty] [--json]' },
|
|
15386
15560
|
'intent expand': { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'intent expand "<request>" [--json]' },
|
|
15561
|
+
'lease acquire': { allowed: ['--session', '--ttl', '--note'], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'lease acquire <file> --session <key> [--ttl 30..1800] [--note <text>] [--json]' },
|
|
15562
|
+
'lease release': { allowed: ['--session'], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'lease release <lease-id> --session <key> [--json]' },
|
|
15563
|
+
'lease list': { allowed: ['--session', '--all'], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'lease list [--session <key>] [--all] [--json]' },
|
|
15564
|
+
'lease check': { allowed: ['--session'], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'lease check <file> [--session <key>] [--json]' },
|
|
15565
|
+
'roles validate': { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'roles validate [--path .] [--json]' },
|
|
15566
|
+
'roles set': { allowed: ['--provider', '--to', '--model', '--model-family', '--policy', '--fallback-policy', '--candidate', '--candidate-family', '--remove-candidate', '--clear-candidates', '--persona', '--force'], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'roles set <role> --provider <id> [--model <id>] [--policy strict|balanced|continuity]' },
|
|
15567
|
+
'roles unset': { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'roles unset <role> [--path .] [--json]' },
|
|
15568
|
+
'roles suggest': { allowed: ['--apply'], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'roles suggest [--apply] [--path .] [--json]' },
|
|
15569
|
+
'roles list': { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'roles list [--path .] [--json]' },
|
|
15570
|
+
'roles catalog': { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'roles catalog [--json]' },
|
|
15571
|
+
'roles verify': { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'roles verify [--path .] [--json]' },
|
|
15387
15572
|
};
|
|
15573
|
+
function _validateLeaseCommandShape(cmd, args) {
|
|
15574
|
+
if (cmd !== 'lease') return true;
|
|
15575
|
+
const rawAction = args[1];
|
|
15576
|
+
const action = rawAction == null ? 'list' : rawAction;
|
|
15577
|
+
const choices = new Set(['acquire', 'release', 'list', 'check']);
|
|
15578
|
+
const en = _errorUiLang(process.cwd(), cmd) === 'en';
|
|
15579
|
+
if (!choices.has(action)) {
|
|
15580
|
+
const safe = _lineSafe(String(action == null ? '' : action)).slice(0, 120);
|
|
15581
|
+
failJson(has('--json'), 'unknown_subcommand', en
|
|
15582
|
+
? `Unknown lease subcommand: ${safe} (choices: acquire, release, list, check)`
|
|
15583
|
+
: `알 수 없는 lease 하위명령: ${safe} (가능: acquire, release, list, check)`);
|
|
15584
|
+
return false;
|
|
15585
|
+
}
|
|
15586
|
+
const expected = action === 'list' ? (rawAction == null ? 1 : 2) : 3;
|
|
15587
|
+
if (args.length < expected || (action !== 'list' && (args[2] == null || args[2] === ''))) {
|
|
15588
|
+
const release = action === 'release';
|
|
15589
|
+
failJson(has('--json'), release ? 'missing_lease_id' : 'missing_file', en
|
|
15590
|
+
? `lease ${action} requires ${release ? 'a lease ID' : 'a file path'}.`
|
|
15591
|
+
: `lease ${action}에는 ${release ? 'lease ID' : '파일 경로'}가 필요합니다.`);
|
|
15592
|
+
return false;
|
|
15593
|
+
}
|
|
15594
|
+
if (args.length > expected) {
|
|
15595
|
+
const stray = _lineSafe(String(args[expected])).slice(0, 120);
|
|
15596
|
+
failJson(has('--json'), 'too_many_arguments', en
|
|
15597
|
+
? `Unexpected lease argument: ${stray}`
|
|
15598
|
+
: `불필요한 lease 인자: ${stray}`);
|
|
15599
|
+
return false;
|
|
15600
|
+
}
|
|
15601
|
+
try {
|
|
15602
|
+
const L = require('../lib/file-leases');
|
|
15603
|
+
if (action === 'acquire' || action === 'check') {
|
|
15604
|
+
L.resolveTarget(absRoot(arg('--path', process.cwd())), args[2]);
|
|
15605
|
+
}
|
|
15606
|
+
if (action === 'release' && !/^lease-[0-9a-f]{16}$/.test(String(args[2] || ''))) {
|
|
15607
|
+
throw new L.LeaseError('invalid_lease_id', 'A canonical lease ID is required.');
|
|
15608
|
+
}
|
|
15609
|
+
if (action === 'acquire' || action === 'release') {
|
|
15610
|
+
const SP = require('../lib/session-presence');
|
|
15611
|
+
const explicit = arg('--session', null);
|
|
15612
|
+
if (explicit != null) L.normalizeSessionKey(explicit);
|
|
15613
|
+
else {
|
|
15614
|
+
if (typeof SP.invalidExplicitKey === 'function' && SP.invalidExplicitKey(process.env) != null) {
|
|
15615
|
+
throw new L.LeaseError('invalid_session_key', 'LEERNESS_SESSION_ID is explicitly set but invalid; refusing ambient fallback.');
|
|
15616
|
+
}
|
|
15617
|
+
L.normalizeSessionKey(SP.deriveSessionKey(process.env));
|
|
15618
|
+
}
|
|
15619
|
+
}
|
|
15620
|
+
} catch (error) {
|
|
15621
|
+
const code = error && error.code ? String(error.code) : 'invalid_lease_input';
|
|
15622
|
+
const message = error && error.message ? String(error.message) : 'Invalid lease input.';
|
|
15623
|
+
failJson(has('--json'), code, _lineSafe(message).slice(0, 300));
|
|
15624
|
+
return false;
|
|
15625
|
+
}
|
|
15626
|
+
return true;
|
|
15627
|
+
}
|
|
15628
|
+
|
|
15629
|
+
function _validateRolesCommandShape(cmd, args) {
|
|
15630
|
+
if (cmd !== 'roles' && cmd !== 'role') return true;
|
|
15631
|
+
const sub = String(args[1] || 'list').trim().toLowerCase();
|
|
15632
|
+
const choices = new Set(['list', 'set', 'unset', 'remove', 'rm', 'catalog', 'suggest', 'verify', 'validate']);
|
|
15633
|
+
if (!choices.has(sub)) {
|
|
15634
|
+
failJson(has('--json'), 'unknown_subcommand', `Unknown roles subcommand: ${_lineSafe(sub).slice(0, 80)}`);
|
|
15635
|
+
return false;
|
|
15636
|
+
}
|
|
15637
|
+
const roleOperand = new Set(['set', 'unset', 'remove', 'rm']);
|
|
15638
|
+
const maximum = roleOperand.has(sub) ? 3 : (args[1] == null ? 1 : 2);
|
|
15639
|
+
if (args.length > maximum) {
|
|
15640
|
+
failJson(has('--json'), 'too_many_arguments', `Unexpected roles argument: ${_lineSafe(String(args[maximum])).slice(0, 120)}`);
|
|
15641
|
+
return false;
|
|
15642
|
+
}
|
|
15643
|
+
return true;
|
|
15644
|
+
}
|
|
15645
|
+
|
|
15388
15646
|
function _validateCommandFlags(cmd, args) {
|
|
15647
|
+
if (cmd === 'lease' && !_rejectDuplicateValueFlags(['--session', '--ttl', '--note', '--path'])) return false;
|
|
15648
|
+
if (cmd === 'agents' && args[1] === 'route'
|
|
15649
|
+
&& !_rejectDuplicateValueFlags(['--tier', '--approved-by', '--reason', '--path'])) return false;
|
|
15650
|
+
const roleSub = (cmd === 'roles' || cmd === 'role') ? String(args[1] || 'list').toLowerCase() : null;
|
|
15651
|
+
if (roleSub === 'set' && !_rejectDuplicateValueFlags([['--provider', '--to'], '--model', '--model-family', ['--policy', '--fallback-policy'], '--persona', '--path'])) return false;
|
|
15652
|
+
if (!_validateLeaseCommandShape(cmd, args)) return false;
|
|
15653
|
+
if (!_validateRolesCommandShape(cmd, args)) return false;
|
|
15389
15654
|
let route = cmd === 'contract' && args[1] === 'verify' ? 'contract verify' : cmd;
|
|
15390
15655
|
if (cmd === 'verify-claim' && args[1] === 'baseline') route = `verify-claim baseline ${args[2] || ''}`.trim();
|
|
15391
15656
|
if (cmd === 'intent' && args[1] === 'expand') route = 'intent expand';
|
|
15657
|
+
if (cmd === 'lease') route = `lease ${args[1] == null ? 'list' : args[1]}`;
|
|
15658
|
+
if (roleSub) route = `roles ${roleSub === 'remove' || roleSub === 'rm' ? 'unset' : roleSub}`;
|
|
15392
15659
|
const cfg = _STRICT_COMMAND_FLAGS[route];
|
|
15393
15660
|
if (!cfg) return true;
|
|
15394
15661
|
return _rejectUnknownFlags(cfg.allowed, cfg.usage, { globals: cfg.globals });
|
|
@@ -20720,9 +20987,11 @@ function _allProviders(root) {
|
|
|
20720
20987
|
const userOverrides = new Map();
|
|
20721
20988
|
for (const u of userList) {
|
|
20722
20989
|
if (!u || !u.id) continue;
|
|
20990
|
+
const providerId = canonicalLegacyProviderIdentity(u.id);
|
|
20991
|
+
if (!providerId) continue;
|
|
20723
20992
|
// 정상화 — 누락 필드는 빌트인에서 fallback
|
|
20724
|
-
const _bi = EXTERNAL_AGENTS.find(a => a.id ===
|
|
20725
|
-
const _bin = u.bin ||
|
|
20993
|
+
const _bi = EXTERNAL_AGENTS.find(a => a.id === providerId);
|
|
20994
|
+
const _bin = u.bin || providerId;
|
|
20726
20995
|
// 1.36.94 (헌트 #16/#21): 정규화가 6개 필드만 복사해 빌트인 `authCheck` 를 **조용히 버렸다** —
|
|
20727
20996
|
// `provider add codex --bin codex`(사실상 무변경 등록) 한 번으로 인증 축이 영원히 unknown 이 되고,
|
|
20728
20997
|
// 고위험 fail-closed(provider_not_authenticated)가 경고 없이 꺼졌다(실측: 로그아웃 codex 로 exit 0 확정).
|
|
@@ -20730,7 +20999,8 @@ function _allProviders(root) {
|
|
|
20730
20999
|
// **같은 bin 일 때만** 빌트인 확인 명령을 물려받는다(다른 실행파일에 남의 하위명령을 쏘면 안 된다).
|
|
20731
21000
|
// 사용자 JSON 의 authCheck 는 절대 신뢰하지 않는다(임의 명령 주입 경로) — 빌트인에서만 온다.
|
|
20732
21001
|
// 같은 실행파일이면 물려받는다(별칭 정규화) — 다른 실행파일에 남의 하위명령을 쏘지는 않는다.
|
|
20733
|
-
const
|
|
21002
|
+
const _trustedBin = !!(_bi && _trustedBuiltinBinAlias(_bin, _bi.bin));
|
|
21003
|
+
const _inherited = (_bi && _bi.authCheck && _trustedBin) ? _bi.authCheck : undefined;
|
|
20734
21004
|
// versionArgs 도 **같은 규율**로 다룬다. 종전엔 사용자 값을 그대로 실어 `shell:true` 로 돌렸는데,
|
|
20735
21005
|
// 메타문자가 있으면 두 번째 명령이 됐다(실측: `agents list` 한 번으로 마커 파일 생성).
|
|
20736
21006
|
// 한때 "플래그만 허용"으로 좁혔다가 되돌렸다 — `kubectl version --client` · `gh copilot --version` ·
|
|
@@ -20740,18 +21010,18 @@ function _allProviders(root) {
|
|
|
20740
21010
|
const _uvaOk = !!_uva && _argsSafe(_uva);
|
|
20741
21011
|
// 폴백은 **같은 실행파일일 때만** 빌트인 값을 쓴다 — 아니면 남의 하위명령(`gh copilot --version`)을
|
|
20742
21012
|
// 엉뚱한 bin 에 쏜다(검수 실측: `kubectl copilot --version`).
|
|
20743
|
-
const _fallbackVa = (_bi &&
|
|
21013
|
+
const _fallbackVa = (_bi && _trustedBin && _bi.versionArgs) || ['--version'];
|
|
20744
21014
|
// bin 이 명령줄이면 실행 자체를 하지 않는다 — 이 provider 는 '미설치'로 보이고 사유를 말한다.
|
|
20745
21015
|
const _binOk = _binSafe(_bin);
|
|
20746
|
-
userOverrides.set(
|
|
20747
|
-
id:
|
|
21016
|
+
userOverrides.set(providerId, {
|
|
21017
|
+
id: providerId,
|
|
20748
21018
|
bin: _bin,
|
|
20749
21019
|
...(_binOk ? {} : { binRejectedReason: `providers.json 의 bin 이 실행파일 이름/경로가 아니어서 실행하지 않았습니다(선행 -·공백·셸 메타문자 불가): ${String(_bin).slice(0, 80)}` }),
|
|
20750
|
-
envFlag: u.envFlag || (_bi && _bi.envFlag) || `LEERNESS_ENABLE_${
|
|
21020
|
+
envFlag: u.envFlag || (_bi && _bi.envFlag) || `LEERNESS_ENABLE_${providerId.toUpperCase()}`,
|
|
20751
21021
|
versionArgs: _uvaOk ? _uva : _fallbackVa,
|
|
20752
21022
|
...((_uva && !_uvaOk) ? { versionArgsRejectedReason: `providers.json 의 versionArgs 에 셸 메타문자가 있어 실행하지 않았습니다: ${JSON.stringify(_uva).slice(0, 120)}` } : {}),
|
|
20753
|
-
desc: u.desc || (_bi && _bi.desc) || `(user) ${
|
|
20754
|
-
descEn: u.desc || (_bi && _bi.descEn) || (_bi && _bi.desc) || `(user) ${
|
|
21023
|
+
desc: u.desc || (_bi && _bi.desc) || `(user) ${providerId}`,
|
|
21024
|
+
descEn: u.desc || (_bi && _bi.descEn) || (_bi && _bi.desc) || `(user) ${providerId}`,
|
|
20755
21025
|
installHint: u.installHint || (_bi && _bi.installHint) || '',
|
|
20756
21026
|
installCmd: u.installCmd || (_bi && _bi.installCmd) || '',
|
|
20757
21027
|
...(_inherited ? { authCheck: _inherited } : {}),
|
|
@@ -20782,7 +21052,7 @@ function providerCmd(root, sub, ...args) {
|
|
|
20782
21052
|
if (!sub || sub === 'list') {
|
|
20783
21053
|
const all = _allProviders(root);
|
|
20784
21054
|
const userList = _readUserProviders(root);
|
|
20785
|
-
const userIds = new Set(userList.map(u => u.id));
|
|
21055
|
+
const userIds = new Set(userList.map(u => canonicalLegacyProviderIdentity(u && u.id)).filter(Boolean));
|
|
20786
21056
|
if (has('--json')) {
|
|
20787
21057
|
log(JSON.stringify({
|
|
20788
21058
|
total: all.length,
|
|
@@ -20822,9 +21092,10 @@ function providerCmd(root, sub, ...args) {
|
|
|
20822
21092
|
return;
|
|
20823
21093
|
}
|
|
20824
21094
|
if (sub === 'add') {
|
|
20825
|
-
const
|
|
20826
|
-
|
|
20827
|
-
if (
|
|
21095
|
+
const rawId = String(args[0] || arg('--id', '')).trim();
|
|
21096
|
+
const id = canonicalProviderIdentity(rawId);
|
|
21097
|
+
if (!rawId) return fail('provider add <id> 필요 (예: openrouter)');
|
|
21098
|
+
if (!id || !/^[a-z][a-z0-9_-]*$/.test(id)) return fail(`잘못된 id: ${rawId} (정규 ASCII 영문자/숫자/_- 만 허용)`);
|
|
20828
21099
|
const bin = arg('--bin', id);
|
|
20829
21100
|
const envFlag = arg('--env-flag', `LEERNESS_ENABLE_${id.toUpperCase()}`);
|
|
20830
21101
|
const versionArgs = (arg('--version-args', '--version') || '--version').split(/\s+/).filter(Boolean);
|
|
@@ -20832,7 +21103,7 @@ function providerCmd(root, sub, ...args) {
|
|
|
20832
21103
|
const installHint = arg('--install-hint', '');
|
|
20833
21104
|
const userList = _readUserProviders(root);
|
|
20834
21105
|
// 중복 id 처리: 빌트인 override 또는 user 갱신
|
|
20835
|
-
const existingIdx = userList.findIndex(u => u.id === id);
|
|
21106
|
+
const existingIdx = userList.findIndex(u => canonicalLegacyProviderIdentity(u && u.id) === id);
|
|
20836
21107
|
const entry = { id, bin, envFlag, versionArgs, desc, installHint };
|
|
20837
21108
|
if (existingIdx >= 0) userList[existingIdx] = entry;
|
|
20838
21109
|
else userList.push(entry);
|
|
@@ -20842,14 +21113,15 @@ function providerCmd(root, sub, ...args) {
|
|
|
20842
21113
|
return;
|
|
20843
21114
|
}
|
|
20844
21115
|
if (sub === 'remove') {
|
|
20845
|
-
const
|
|
20846
|
-
|
|
20847
|
-
if (
|
|
21116
|
+
const rawId = String(args[0] || arg('--id', '')).trim();
|
|
21117
|
+
const id = canonicalLegacyProviderIdentity(rawId);
|
|
21118
|
+
if (!rawId || !id) return fail('provider remove <id> 필요');
|
|
21119
|
+
if (EXTERNAL_AGENTS.some(b => b.id === id) && !_readUserProviders(root).some(u => canonicalLegacyProviderIdentity(u && u.id) === id)) {
|
|
20848
21120
|
return fail(`${id} 는 빌트인 — 제거 불가 (override 만 제거 가능)`);
|
|
20849
21121
|
}
|
|
20850
21122
|
const userList = _readUserProviders(root);
|
|
20851
21123
|
const before = userList.length;
|
|
20852
|
-
const filtered = userList.filter(u => u.id !== id);
|
|
21124
|
+
const filtered = userList.filter(u => canonicalLegacyProviderIdentity(u && u.id) !== id);
|
|
20853
21125
|
if (filtered.length === before) return fail(`사용자 정의 provider ${id} 없음`);
|
|
20854
21126
|
_writeUserProviders(root, filtered);
|
|
20855
21127
|
ok(`provider 제거: ${id}`);
|
|
@@ -20910,11 +21182,12 @@ function providerCmd(root, sub, ...args) {
|
|
|
20910
21182
|
log(`발견 ${entries.length}개 후보`);
|
|
20911
21183
|
log('');
|
|
20912
21184
|
const userList = _readUserProviders(root);
|
|
20913
|
-
const existingIds = new Set(userList.map(u => u.id));
|
|
20914
|
-
const validIdRegex = /^[a-z][a-z0-9_-]
|
|
21185
|
+
const existingIds = new Set(userList.map(u => canonicalLegacyProviderIdentity(u && u.id)).filter(Boolean));
|
|
21186
|
+
const validIdRegex = /^[a-z][a-z0-9_-]*$/;
|
|
20915
21187
|
let added = 0, updated = 0, skipped = 0;
|
|
20916
21188
|
for (const e of entries) {
|
|
20917
|
-
|
|
21189
|
+
const providerId = e && canonicalProviderIdentity(e.id);
|
|
21190
|
+
if (!providerId || !e.bin || !validIdRegex.test(providerId)) { skipped++; continue; }
|
|
20918
21191
|
// 1.36.94: **원격 catalog 는 실행 인자를 정하지 못한다.** 여기 저장된 값은 나중에 `agents list/check`
|
|
20919
21192
|
// 같은 읽기성 명령에서 `shell:true` 로 실행된다 — 즉 원격 문자열이 곧 로컬 실행이다.
|
|
20920
21193
|
// 메타문자만 막는 것으로는 부족하다: `npm exec --yes <패키지>` 처럼 **메타문자 없이도** 임의 코드를
|
|
@@ -20923,32 +21196,35 @@ function providerCmd(root, sub, ...args) {
|
|
|
20923
21196
|
// bin 은 형식 검증을 통과한 것만 받는다 — 실행파일 이름/경로가 아니면 그 자체가 명령줄이 된다.
|
|
20924
21197
|
if (!_binSafe(e.bin)) { skipped++; log(` ⚠ ${e.id} 무시 — bin 이 실행파일 이름/경로가 아닙니다: ${String(e.bin).slice(0, 60)}`); continue; }
|
|
20925
21198
|
const entry = {
|
|
20926
|
-
id:
|
|
21199
|
+
id: providerId,
|
|
20927
21200
|
bin: e.bin,
|
|
20928
|
-
envFlag: e.envFlag || `LEERNESS_ENABLE_${
|
|
21201
|
+
envFlag: e.envFlag || `LEERNESS_ENABLE_${providerId.toUpperCase()}`,
|
|
20929
21202
|
versionArgs: ['--version'],
|
|
20930
|
-
desc: e.desc || `(synced) ${
|
|
21203
|
+
desc: e.desc || `(synced) ${providerId}`,
|
|
20931
21204
|
installHint: e.installHint || ''
|
|
20932
21205
|
};
|
|
20933
21206
|
if (e.versionArgs) log(` ⓘ ${e.id}: 원격 versionArgs 는 사용하지 않습니다(--version 고정) — 원격 값이 로컬 실행 인자가 되지 않게 합니다`);
|
|
20934
|
-
if (existingIds.has(
|
|
20935
|
-
const idx = userList.findIndex(u => u.id ===
|
|
21207
|
+
if (existingIds.has(providerId)) {
|
|
21208
|
+
const idx = userList.findIndex(u => canonicalLegacyProviderIdentity(u && u.id) === providerId);
|
|
20936
21209
|
if (!dryRun) userList[idx] = entry;
|
|
20937
21210
|
updated++;
|
|
20938
|
-
log(` ↺ ${
|
|
21211
|
+
log(` ↺ ${providerId} (갱신)`);
|
|
20939
21212
|
} else {
|
|
20940
21213
|
if (!dryRun) userList.push(entry);
|
|
20941
|
-
existingIds.add(
|
|
21214
|
+
existingIds.add(providerId);
|
|
20942
21215
|
added++;
|
|
20943
|
-
log(` + ${
|
|
21216
|
+
log(` + ${providerId} (신규)`);
|
|
20944
21217
|
}
|
|
20945
21218
|
}
|
|
20946
21219
|
// 1.36.108 (T-0097): 네트워크는 락 밖에서 끝났다. 저장만 락 안에서 하되 **다시 읽어** 병합한다 —
|
|
20947
21220
|
// 위의 userList 는 fetch 전 스냅샷이라, 그 사이 `provider add` 한 항목을 그대로 덮으면 지운다.
|
|
20948
21221
|
if (!dryRun) _withLock(_providersFile(root), () => {
|
|
20949
21222
|
const fresh = _readUserProviders(root);
|
|
20950
|
-
const byId = new Map(fresh.map(u => [u.id, u]));
|
|
20951
|
-
for (const u of userList)
|
|
21223
|
+
const byId = new Map(fresh.map(u => [canonicalLegacyProviderIdentity(u && u.id), u]).filter(([id]) => id));
|
|
21224
|
+
for (const u of userList) {
|
|
21225
|
+
const persistedId = canonicalLegacyProviderIdentity(u && u.id);
|
|
21226
|
+
if (persistedId) byId.set(persistedId, u); // 이번 sync 결과가 이긴다(그게 sync 의 의미다)
|
|
21227
|
+
}
|
|
20952
21228
|
_writeUserProviders(root, [...byId.values()]);
|
|
20953
21229
|
});
|
|
20954
21230
|
log('');
|
|
@@ -21166,10 +21442,93 @@ function _binKey(b) {
|
|
|
21166
21442
|
return process.platform === 'win32' ? s.toLowerCase() : s;
|
|
21167
21443
|
}
|
|
21168
21444
|
|
|
21445
|
+
function _trustedBuiltinBinAlias(candidate, builtin) {
|
|
21446
|
+
const raw = String(candidate || '');
|
|
21447
|
+
// A workspace-relative/absolute path is a different executable authority even
|
|
21448
|
+
// when its basename looks like a built-in CLI. Bare Windows shim extensions and
|
|
21449
|
+
// casing remain compatible because PATH would resolve those as the same command.
|
|
21450
|
+
if (!raw || /[\\/]/.test(raw) || /^[A-Za-z]:/.test(raw)) return false;
|
|
21451
|
+
return _binKey(raw) === _binKey(builtin);
|
|
21452
|
+
}
|
|
21453
|
+
|
|
21454
|
+
function _providerOutputSafe(value, max = 80) {
|
|
21455
|
+
// Provider output is untrusted display text. Compatibility-normalize it before
|
|
21456
|
+
// redaction so full-width credential labels cannot evade the ASCII secret
|
|
21457
|
+
// patterns, then remove terminal/default-ignorable formatting.
|
|
21458
|
+
const visible = stripDefaultIgnorables(value, { nfkc: true, visibleIdentity: true });
|
|
21459
|
+
const withoutAccounts = _lineSafe(visible)
|
|
21460
|
+
.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '***')
|
|
21461
|
+
// Provider auth output has no stable delimiter for display names or orgs.
|
|
21462
|
+
// Once an identity label appears, redact the remainder of that flattened
|
|
21463
|
+
// line instead of leaking every token after the first space.
|
|
21464
|
+
.replace(/\b((?:logged|signed)\s+in(?:\s+as)?\s*[:=]?)\s*[^\r\n]*/gi, '$1 ***')
|
|
21465
|
+
.replace(/\b(account|user(?:name)?|profile|tenant|org(?:anization)?)(?:\s+id)?\s*[:=]\s*[^\r\n]*/gi, '$1=***');
|
|
21466
|
+
return redactSecrets(withoutAccounts, max).trim().slice(0, max) || null;
|
|
21467
|
+
}
|
|
21468
|
+
|
|
21469
|
+
// Presence-only provider inspection. Disabled providers must not be executed merely to
|
|
21470
|
+
// populate an observation table. Windows reuses the shell-free portable resolver; POSIX
|
|
21471
|
+
// searches PATH and checks executable permission without spawning the candidate.
|
|
21472
|
+
function _commandPresentWithoutExecution(bin, options = {}) {
|
|
21473
|
+
if (!_binSafe(bin)) return false;
|
|
21474
|
+
const env = options.env || process.env;
|
|
21475
|
+
const cwd = options.cwd || process.cwd();
|
|
21476
|
+
const isFileExecutable = (candidate) => {
|
|
21477
|
+
try {
|
|
21478
|
+
const stat = fs.statSync(candidate);
|
|
21479
|
+
if (!stat.isFile()) return false;
|
|
21480
|
+
if (process.platform !== 'win32') fs.accessSync(candidate, fs.constants.X_OK);
|
|
21481
|
+
return true;
|
|
21482
|
+
} catch { return false; }
|
|
21483
|
+
};
|
|
21484
|
+
if (process.platform === 'win32') {
|
|
21485
|
+
try {
|
|
21486
|
+
const launch = resolvePortableLaunch(bin, { env, cwd, execPath: process.execPath });
|
|
21487
|
+
if (launch.kind === 'unresolved' || launch.kind === 'unsupported-shell-script') return false;
|
|
21488
|
+
return isFileExecutable(launch.source || launch.file);
|
|
21489
|
+
} catch { return false; }
|
|
21490
|
+
}
|
|
21491
|
+
const value = String(bin);
|
|
21492
|
+
if (/[\/]/.test(value)) {
|
|
21493
|
+
return isFileExecutable(path.isAbsolute(value) ? value : path.resolve(cwd, value));
|
|
21494
|
+
}
|
|
21495
|
+
const pathKey = Object.keys(env).find(key => key.toLowerCase() === 'path');
|
|
21496
|
+
const pathValue = pathKey ? String(env[pathKey] || '') : '';
|
|
21497
|
+
for (const rawDir of pathValue.split(path.delimiter)) {
|
|
21498
|
+
const dir = rawDir.trim().replace(/^"|"$/g, '');
|
|
21499
|
+
if (!dir) continue;
|
|
21500
|
+
const absoluteDir = path.isAbsolute(dir) ? dir : path.resolve(cwd, dir);
|
|
21501
|
+
if (isFileExecutable(path.join(absoluteDir, value))) return true;
|
|
21502
|
+
}
|
|
21503
|
+
return false;
|
|
21504
|
+
}
|
|
21505
|
+
|
|
21506
|
+
function _providerProbeEnv() {
|
|
21507
|
+
const out = _scrubTestEnv();
|
|
21508
|
+
// `_scrubTestEnv` is designed for arbitrary project test runners and therefore keeps
|
|
21509
|
+
// explicitly allowed provider/release environment variables. Provider inspection has a
|
|
21510
|
+
// stricter contract: no project credential/profile/endpoint setting is needed for PATH,
|
|
21511
|
+
// version, or local-auth observation. Remove the entire explicit dotenv surface plus
|
|
21512
|
+
// Leerness/NPM configuration before invoking a third-party provider executable.
|
|
21513
|
+
const explicit = new Set([..._DOTENV_EXPLICIT_KEYS].map(key => key.toUpperCase()));
|
|
21514
|
+
for (const key of Object.keys(out)) {
|
|
21515
|
+
const canonical = key.toUpperCase();
|
|
21516
|
+
if (explicit.has(canonical) || canonical.startsWith('LEERNESS_') || canonical.startsWith('NPM_CONFIG_')) delete out[key];
|
|
21517
|
+
}
|
|
21518
|
+
return out;
|
|
21519
|
+
}
|
|
21520
|
+
|
|
21169
21521
|
function _checkAgent(agent, opts = {}) {
|
|
21170
21522
|
const enabled = process.env[agent.envFlag] === '1';
|
|
21171
21523
|
// PATH 존재 확인 (which / where)
|
|
21172
21524
|
let installed = false, version = null, error = null;
|
|
21525
|
+
let versionCheckAttempted = false, versionCheckSucceeded = false, presenceCheckOnly = false;
|
|
21526
|
+
// Provider probes inherit only execution essentials. Project API keys, profiles, endpoints,
|
|
21527
|
+
// Leerness flags, and npm configuration are removed before third-party CLI invocation.
|
|
21528
|
+
const probeEnv = _providerProbeEnv();
|
|
21529
|
+
// Disabled providers are presence-only. Merely listing/resolving roles must
|
|
21530
|
+
// never execute a workspace-configured binary whose opt-in flag is unset.
|
|
21531
|
+
const executeVersion = enabled && opts.executeVersion !== false;
|
|
21173
21532
|
// 안전하지 않은 versionArgs 는 **실행하지 않고** 빌트인 기본값으로 되돌린다(거부보다 낫다 — 설치 확인 자체는 해야 한다).
|
|
21174
21533
|
const _vaRaw = agent.versionArgs;
|
|
21175
21534
|
const _vaSafe = _argsSafe(_vaRaw);
|
|
@@ -21178,25 +21537,36 @@ function _checkAgent(agent, opts = {}) {
|
|
|
21178
21537
|
const _binOk = _binSafe(agent.bin);
|
|
21179
21538
|
try {
|
|
21180
21539
|
if (!_binOk) throw new Error('bin 형식 거부');
|
|
21181
|
-
|
|
21182
|
-
if (
|
|
21183
|
-
|
|
21184
|
-
|
|
21185
|
-
|
|
21186
|
-
error = r.error.code || r.error.message;
|
|
21540
|
+
installed = _commandPresentWithoutExecution(agent.bin, { env: probeEnv, cwd: opts.cwd });
|
|
21541
|
+
if (!installed) {
|
|
21542
|
+
error = 'not found on PATH';
|
|
21543
|
+
} else if (!executeVersion) {
|
|
21544
|
+
presenceCheckOnly = true;
|
|
21187
21545
|
} else {
|
|
21188
|
-
|
|
21546
|
+
versionCheckAttempted = true;
|
|
21547
|
+
const r = _spawnPortableArgvSync(agent.bin, _versionArgs, {
|
|
21548
|
+
encoding: 'utf8', timeout: 5000, env: probeEnv, cwd: opts.cwd || undefined,
|
|
21549
|
+
});
|
|
21550
|
+
if (r.status === 0) {
|
|
21551
|
+
versionCheckSucceeded = true;
|
|
21552
|
+
version = _providerOutputSafe((r.stdout || r.stderr || '').split('\n')[0], 80);
|
|
21553
|
+
} else if (r.error) {
|
|
21554
|
+
error = r.error.code || r.error.message;
|
|
21555
|
+
} else {
|
|
21556
|
+
error = `exit ${r.status}`;
|
|
21557
|
+
}
|
|
21189
21558
|
}
|
|
21190
21559
|
} catch (e) { error = e.message; }
|
|
21191
21560
|
// 1.36.91: **설치됨 ≠ 사용가능**. 위 확인은 바이너리가 뜨는지만 본다 — 인증·모델 사용권한·쿼터·과금 권한은
|
|
21192
21561
|
// 아무것도 증명하지 않는다. 무료·비대화형·부작용 없음이 확인된 authCheck 를 가진 provider 만 실제로 판정하고,
|
|
21193
21562
|
// 나머지는 'unknown' 으로 **남긴다**(모르는 것을 안다고 말하지 않는다). opts.auth 일 때만 실행 — 지연 비용이 있다.
|
|
21194
21563
|
let auth = 'unknown', authEvidence = null, authSource = null;
|
|
21195
|
-
if (installed && opts.auth && agent.authCheck && _argsSafe(agent.authCheck.args)) {
|
|
21196
|
-
authSource = `${agent.bin} ${agent.authCheck.args.join(' ')}`;
|
|
21564
|
+
if (enabled && installed && opts.auth && agent.authCheck && _argsSafe(agent.authCheck.args)) {
|
|
21565
|
+
authSource = `${_binKey(agent.bin)} ${agent.authCheck.args.join(' ')}`;
|
|
21197
21566
|
try {
|
|
21198
21567
|
const ar = _spawnPortableArgvSync(agent.bin, agent.authCheck.args, {
|
|
21199
21568
|
encoding: 'utf8', timeout: agent.authCheck.timeoutMs || 8000, stdio: ['ignore', 'pipe', 'pipe'],
|
|
21569
|
+
env: probeEnv, cwd: opts.cwd || undefined,
|
|
21200
21570
|
});
|
|
21201
21571
|
const aout = ((ar.stdout || '') + (ar.stderr || '')).trim();
|
|
21202
21572
|
// **"실행하지 못함"을 "확실히 미인증"으로 둔갑시키지 않는다.** `no` 는 고위험 작업을 차단하므로,
|
|
@@ -21207,7 +21577,7 @@ function _checkAgent(agent, opts = {}) {
|
|
|
21207
21577
|
|| /\n\s+at .+:\d+:\d+/.test(aout)
|
|
21208
21578
|
|| ar.status === 127 || ar.status === 9009;
|
|
21209
21579
|
// evidence 는 사람이 보는 출력·JSON·CI 로그에 실린다 — 토큰 마스킹 + 제어문자(터미널 이스케이프) 제거.
|
|
21210
|
-
const _clean = (s) =>
|
|
21580
|
+
const _clean = (s) => _providerOutputSafe(s, 80);
|
|
21211
21581
|
if (ar.error || ar.status == null) { auth = 'unknown'; authEvidence = `확인 실패(${(ar.error && ar.error.code) || 'timeout'})`; }
|
|
21212
21582
|
else if (launcherish) { auth = 'unknown'; authEvidence = `확인 명령을 실행하지 못함(exit ${ar.status})`; }
|
|
21213
21583
|
else if (ar.status === 0) { auth = 'ok'; authEvidence = _clean(aout.split('\n')[0]) || 'exit 0'; }
|
|
@@ -21226,6 +21596,7 @@ function _checkAgent(agent, opts = {}) {
|
|
|
21226
21596
|
return {
|
|
21227
21597
|
id: agent.id, bin: agent.bin, desc: agent.desc, envFlag: agent.envFlag,
|
|
21228
21598
|
enabled, installed, version, error,
|
|
21599
|
+
versionCheckAttempted, versionCheckSucceeded, presenceCheckOnly, probeEnvironmentSanitized: true,
|
|
21229
21600
|
auth, authEvidence, authSource,
|
|
21230
21601
|
// 확인 명령이 없는 사유를 구분해 전달한다 — "원래 없음"과 "override 로 물려받지 못함"은 다른 말이다(헌트 #21).
|
|
21231
21602
|
authCheckDroppedReason: agent.authCheckDroppedReason || null,
|
|
@@ -21745,26 +22116,75 @@ function _harnessBrief() {
|
|
|
21745
22116
|
return "[leerness 위임 프로토콜] 시작: 'leerness handoff .' 로 컨텍스트/active rules 적재 후 'leerness task add' 로 이 작업 등록. 완료 전: evidence(수정 파일 경로 + 실제 테스트 결과)를 'leerness task update <T-ID> --status done --evidence ...' 로 기록하고 'leerness verify-claim <T-ID> --run-tests' 로 자기검증. 종료: 'leerness session close .' 실행. --- 작업: ";
|
|
21746
22117
|
}
|
|
21747
22118
|
|
|
21748
|
-
|
|
22119
|
+
// Dispatch prepares a copyable shell command, even for providers whose direct
|
|
22120
|
+
// execution path has a structured argv contract. That representation cannot
|
|
22121
|
+
// safely carry every string across cmd, PowerShell, and POSIX shells. Keep it
|
|
22122
|
+
// only for portable plain text; callers with shell syntax must use a structured
|
|
22123
|
+
// execution path instead of receiving an executable-looking unsafe string.
|
|
22124
|
+
function _portableDispatchTaskSafe(value) {
|
|
22125
|
+
const text = String(value == null ? '' : value);
|
|
22126
|
+
if (!text) return false;
|
|
22127
|
+
// PowerShell treats typographic quotes as real delimiters, including U+201C
|
|
22128
|
+
// and U+201D. ASCII apostrophe is the sole quotation mark safe inside the
|
|
22129
|
+
// emitted ASCII double-quoted argument; reject every other Unicode quote.
|
|
22130
|
+
if (/\p{Quotation_Mark}/u.test(text.replace(/'/g, ''))) return false;
|
|
22131
|
+
// NFKC exposes full-width shell metacharacters before applying the same deny
|
|
22132
|
+
// set. Cc covers the C1 range too; Cf/Zl/Zp prevent invisible line/bidi tricks.
|
|
22133
|
+
return [text, text.normalize('NFKC')].every(view =>
|
|
22134
|
+
!/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}"\\`$%!^]/u.test(view));
|
|
22135
|
+
}
|
|
22136
|
+
|
|
22137
|
+
function _dispatchCommand(agentId, task, writeMode, model, providerDefinition) {
|
|
21749
22138
|
const q = String(task || '').replace(/"/g, '\\"');
|
|
21750
|
-
const m = model ? String(model).
|
|
21751
|
-
if (
|
|
21752
|
-
|
|
21753
|
-
|
|
21754
|
-
|
|
22139
|
+
const m = model ? String(model).trim() : '';
|
|
22140
|
+
if (m && !isValidModelIdentifier(m)) {
|
|
22141
|
+
const error = new Error('model is not a safe model identifier');
|
|
22142
|
+
error.code = 'invalid_model_identifier';
|
|
22143
|
+
throw error;
|
|
22144
|
+
}
|
|
22145
|
+
const definition = providerDefinition && providerDefinition.id === agentId ? providerDefinition : null;
|
|
22146
|
+
const defaultBin = agentId === 'copilot' ? 'gh' : agentId;
|
|
22147
|
+
const executable = definition && definition.bin ? definition.bin : defaultBin;
|
|
22148
|
+
if (!_binSafe(executable) || (definition && definition.binRejectedReason)) {
|
|
22149
|
+
const error = new Error(`provider ${agentId} has no safe executable definition`);
|
|
22150
|
+
error.code = 'provider_dispatch_definition_invalid';
|
|
22151
|
+
throw error;
|
|
22152
|
+
}
|
|
22153
|
+
if (!_portableDispatchTaskSafe(task)) {
|
|
22154
|
+
const error = new Error(`provider ${agentId} cannot safely represent this task as a portable shell command; use a structured argv execution path`);
|
|
22155
|
+
error.code = 'provider_dispatch_task_unsafe';
|
|
22156
|
+
throw error;
|
|
22157
|
+
}
|
|
22158
|
+
if (agentId === 'claude') return `${executable} ${writeMode ? '--print --dangerously-skip-permissions' : '--print'}${m ? ` --model ${m}` : ''} "${q}"`;
|
|
22159
|
+
if (agentId === 'codex') return `${executable} ${writeMode ? 'exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox' : 'exec --skip-git-repo-check'}${m ? ` -m ${m}` : ''} "${q}"`;
|
|
22160
|
+
if (agentId === 'agy') return `${executable} ${writeMode ? '-p --yolo' : '-p'}${m ? ` --model ${m}` : ''} "${q}"`;
|
|
22161
|
+
if (agentId === 'grok') return `${executable}${writeMode ? ' --yolo' : ''}${m ? ` --model ${m}` : ''} "${q}"`;
|
|
21755
22162
|
// 1.9.277: 신규 provider 4종 — best-effort 비대화형 호출 (실제 실행은 사용자/메인)
|
|
21756
|
-
if (agentId === 'opencode') return
|
|
21757
|
-
if (agentId === 'qwen') return
|
|
21758
|
-
if (agentId === 'aider') return
|
|
21759
|
-
if (agentId === 'goose') return
|
|
21760
|
-
if (agentId === 'copilot') return
|
|
21761
|
-
if (agentId === 'ollama') return `#
|
|
21762
|
-
|
|
22163
|
+
if (agentId === 'opencode') return `${executable} run${m ? ` --model ${m}` : ''} "${q}"`;
|
|
22164
|
+
if (agentId === 'qwen') return `${executable} -p "${q}"${m ? ` # --model ${m} (env/설정)` : ''}`;
|
|
22165
|
+
if (agentId === 'aider') return `${executable} --message "${q}"${writeMode ? ' --yes' : ' --no-auto-commits'}${m ? ` --model ${m}` : ''}`;
|
|
22166
|
+
if (agentId === 'goose') return `${executable} run -t "${q}"${m && m !== 'default' ? ` # provider/model: goose configure` : ''}`;
|
|
22167
|
+
if (agentId === 'copilot') return `${executable} copilot suggest "${q}"`;
|
|
22168
|
+
if (agentId === 'ollama') return `# ${executable} — leerness agent "${q}" --provider ollama${m ? ` --model ${m}` : ''} 로 직접 호출 (REPL: leerness agent)`;
|
|
22169
|
+
if (!definition) {
|
|
22170
|
+
const error = new Error(`provider ${agentId} has no registered dispatch definition`);
|
|
22171
|
+
error.code = 'provider_dispatch_definition_missing';
|
|
22172
|
+
throw error;
|
|
22173
|
+
}
|
|
22174
|
+
// Legacy/user registries define an executable and use the registry's historic
|
|
22175
|
+
// generic positional-prompt/--model convention. They do not define a trusted
|
|
22176
|
+
// permission-widening flag, so an explicit write request fails closed.
|
|
22177
|
+
if (writeMode) {
|
|
22178
|
+
const error = new Error(`provider ${agentId} does not define ${writeMode ? 'write-mode' : 'model'} invocation semantics`);
|
|
22179
|
+
error.code = 'provider_dispatch_semantics_undefined';
|
|
22180
|
+
throw error;
|
|
22181
|
+
}
|
|
22182
|
+
return `${executable}${m ? ` --model ${m}` : ''} "${q}"`;
|
|
21763
22183
|
}
|
|
21764
22184
|
|
|
21765
22185
|
const _agents = require('../lib/agents');
|
|
21766
22186
|
// 1.9.424 (UR-0025/UR-0125 큰 핸들러 모듈화 9번째): agentsCmd → lib/agents.js (DI 위임, rest→array)
|
|
21767
|
-
function agentsCmd(root, sub, ...args) { return _agents.agentsCmd(root, sub, args, { VERSION, has, arg, uiLang: _uiLang(root), _agentSlashHint, _allProviders, _checkAgent, _cliChat, _dispatchCommand, _harnessBrief: _tgl.toggleOn(root, 'delegation-brief') ? _harnessBrief : undefined, _loadEnvFile, _normalizeRole, _policyEnforce, _readUserProviders, _recommendAgent, _recordRun, _resolveRole, lessonsPath, taskLogPath }); } // 1.36.30: delegation-brief 토글 OFF 면 브리프 미접두(=--raw 경로)
|
|
22187
|
+
function agentsCmd(root, sub, ...args) { return _agents.agentsCmd(root, sub, args, { VERSION, has, arg, rawArgv: process.argv.slice(2), uiLang: _uiLang(root), _agentSlashHint, _allProviders, _checkAgent, _cliChat, _dispatchCommand, _harnessBrief: _tgl.toggleOn(root, 'delegation-brief') ? _harnessBrief : undefined, _loadEnvFile, _normalizeRole, _policyEnforce, _readUserProviders, _recommendAgent, _recordRun, _resolveRole, _withLock, lessonsPath, taskLogPath }); } // 1.36.30: delegation-brief 토글 OFF 면 브리프 미접두(=--raw 경로)
|
|
21768
22188
|
|
|
21769
22189
|
function personaCmd(root, sub, idOrName, ...rest) {
|
|
21770
22190
|
root = absRoot(root || process.cwd());
|
|
@@ -26087,6 +26507,33 @@ function skillExportAllCmd(root) {
|
|
|
26087
26507
|
|
|
26088
26508
|
// 1.9.322 (UR-0044, 설치리뷰): MCP tools/call name->cliArgs 매핑을 단일 함수로 통합 (이전: mcpServeCmd 인라인 83-case switch).
|
|
26089
26509
|
// 일치성 가드(selftest: 모든 도구 def 가 case 보유) + e2e(대표 도구 dispatch) 가 안전망. 미지 도구는 null 반환.
|
|
26510
|
+
function _fileLeaseMcpParamError(name, args) {
|
|
26511
|
+
if (name !== 'leerness_file_lease_read' && name !== 'leerness_file_lease_write') return null;
|
|
26512
|
+
const own = key => Object.prototype.hasOwnProperty.call(args, key);
|
|
26513
|
+
const action = args.action;
|
|
26514
|
+
const allowedActions = name === 'leerness_file_lease_read' ? ['list', 'check'] : ['acquire', 'release'];
|
|
26515
|
+
if (!allowedActions.includes(action)) return `Invalid params: 'action' must be one of ${allowedActions.join('|')}`;
|
|
26516
|
+
const common = new Set(['action', 'path', 'sessionKey']);
|
|
26517
|
+
if (name === 'leerness_file_lease_read') {
|
|
26518
|
+
common.add('_cursor');
|
|
26519
|
+
common.add('_chunkSize');
|
|
26520
|
+
}
|
|
26521
|
+
const allowed = new Set(common);
|
|
26522
|
+
if (action === 'list') allowed.add('all');
|
|
26523
|
+
if (action === 'check') allowed.add('file');
|
|
26524
|
+
if (action === 'acquire') { allowed.add('file'); allowed.add('ttl'); allowed.add('note'); }
|
|
26525
|
+
if (action === 'release') allowed.add('leaseId');
|
|
26526
|
+
const extra = Object.keys(args).find(key => !allowed.has(key));
|
|
26527
|
+
if (extra) return `Invalid params: '${extra}' is not accepted for lease action '${action}'`;
|
|
26528
|
+
if ((action === 'check' || action === 'acquire') && (!own('file') || typeof args.file !== 'string' || !args.file.trim())) {
|
|
26529
|
+
return `Invalid params: lease action '${action}' requires a non-empty 'file'`;
|
|
26530
|
+
}
|
|
26531
|
+
if (action === 'release' && (!own('leaseId') || typeof args.leaseId !== 'string' || !args.leaseId)) {
|
|
26532
|
+
return "Invalid params: lease action 'release' requires a non-empty 'leaseId'";
|
|
26533
|
+
}
|
|
26534
|
+
return null;
|
|
26535
|
+
}
|
|
26536
|
+
|
|
26090
26537
|
function _mcpToCliArgs(name, args, targetPath) {
|
|
26091
26538
|
let cliArgs;
|
|
26092
26539
|
switch (name) {
|
|
@@ -26096,7 +26543,83 @@ function _mcpToCliArgs(name, args, targetPath) {
|
|
|
26096
26543
|
case 'leerness_verify_claim': cliArgs = ['verify-claim', args.taskId, '--path', targetPath, ...(args.refereeId ? ['--referee', String(args.refereeId)] : []), ...(args.runTests ? ['--run-tests'] : []), ...(args.strictClaims ? ['--strict-claims'] : []), ...(args.lenient ? ['--lenient'] : [])]; break;
|
|
26097
26544
|
case 'leerness_verify_claim_all': cliArgs = ['verify-claim', '--all', '--path', targetPath, '--json', ...(args.runTests ? ['--run-tests'] : []), ...(args.strictClaims ? ['--strict-claims'] : []), ...(args.lenient ? ['--lenient'] : []), ...(args.raw ? ['--raw'] : [])]; break; // 1.33.3/1.36.167: 일괄검증 + 원판정 감사
|
|
26098
26545
|
case 'leerness_contract_verify': cliArgs = ['contract', 'verify', args.spec, args.impl]; break;
|
|
26099
|
-
case 'leerness_agents_list': cliArgs = ['agents', 'list', '--json']; break;
|
|
26546
|
+
case 'leerness_agents_list': cliArgs = ['agents', 'list', '--path', targetPath, '--json']; break;
|
|
26547
|
+
case 'leerness_agents_resolve':
|
|
26548
|
+
cliArgs = ['agents', 'resolve', String(args.task || ''), '--role', String(args.role || ''), '--path', targetPath, '--json'];
|
|
26549
|
+
if (args.tier) cliArgs.push('--tier', String(args.tier));
|
|
26550
|
+
if (args.preset) cliArgs.push('--preset', String(args.preset));
|
|
26551
|
+
if (args.approvedBy) cliArgs.push('--approved-by', String(args.approvedBy));
|
|
26552
|
+
if (args.reason) cliArgs.push('--reason', String(args.reason));
|
|
26553
|
+
if (args.sessionProvider) cliArgs.push('--session-provider', String(args.sessionProvider));
|
|
26554
|
+
if (args.sessionModel) cliArgs.push('--session-model', String(args.sessionModel));
|
|
26555
|
+
if (args.sessionModelFamily) cliArgs.push('--session-model-family', String(args.sessionModelFamily));
|
|
26556
|
+
if (args.implementerProvider) cliArgs.push('--agent', String(args.implementerProvider));
|
|
26557
|
+
if (args.implementerModel) cliArgs.push('--agent-model', String(args.implementerModel));
|
|
26558
|
+
if (args.implementerModelFamily) cliArgs.push('--agent-model-family', String(args.implementerModelFamily));
|
|
26559
|
+
if (args.implementerSession) cliArgs.push('--agent-session', String(args.implementerSession));
|
|
26560
|
+
break;
|
|
26561
|
+
case 'leerness_agents_fallback':
|
|
26562
|
+
cliArgs = ['agents', 'fallback', String(args.action || ''), String(args.task || ''), '--role', String(args.role || ''), '--path', targetPath, '--json'];
|
|
26563
|
+
if (args.taskId) cliArgs.push('--id', String(args.taskId));
|
|
26564
|
+
if (args.provider) cliArgs.push('--provider', String(args.provider));
|
|
26565
|
+
if (args.model) cliArgs.push('--model', String(args.model));
|
|
26566
|
+
if (args.approvedBy) cliArgs.push('--approved-by', String(args.approvedBy));
|
|
26567
|
+
if (args.reason) cliArgs.push('--reason', String(args.reason));
|
|
26568
|
+
if (args.tier) cliArgs.push('--tier', String(args.tier));
|
|
26569
|
+
if (args.preset) cliArgs.push('--preset', String(args.preset));
|
|
26570
|
+
if (args.write === true) cliArgs.push('--write');
|
|
26571
|
+
if (args.sessionProvider) cliArgs.push('--session-provider', String(args.sessionProvider));
|
|
26572
|
+
if (args.sessionModel) cliArgs.push('--session-model', String(args.sessionModel));
|
|
26573
|
+
if (args.sessionModelFamily) cliArgs.push('--session-model-family', String(args.sessionModelFamily));
|
|
26574
|
+
if (args.implementerProvider) cliArgs.push('--agent', String(args.implementerProvider));
|
|
26575
|
+
if (args.implementerModel) cliArgs.push('--agent-model', String(args.implementerModel));
|
|
26576
|
+
if (args.implementerModelFamily) cliArgs.push('--agent-model-family', String(args.implementerModelFamily));
|
|
26577
|
+
if (args.implementerSession) cliArgs.push('--agent-session', String(args.implementerSession));
|
|
26578
|
+
break;
|
|
26579
|
+
case 'leerness_agents_availability': {
|
|
26580
|
+
const action = String(args.action || 'list');
|
|
26581
|
+
cliArgs = ['agents', 'availability', action];
|
|
26582
|
+
if ((action === 'mark' || action === 'clear') && args.provider) cliArgs.push(String(args.provider));
|
|
26583
|
+
cliArgs.push('--path', targetPath, '--json');
|
|
26584
|
+
if (args.model) cliArgs.push('--model', String(args.model));
|
|
26585
|
+
if (args.modelFamily) cliArgs.push('--model-family', String(args.modelFamily));
|
|
26586
|
+
if (args.reason) cliArgs.push('--reason', String(args.reason));
|
|
26587
|
+
if (args.authenticated) cliArgs.push('--auth-state', String(args.authenticated === 'yes' ? 'ok' : args.authenticated));
|
|
26588
|
+
if (args.entitlement) cliArgs.push('--entitlement', String(args.entitlement));
|
|
26589
|
+
if (args.quota) cliArgs.push('--quota-state', String(args.quota));
|
|
26590
|
+
if (args.policy) cliArgs.push('--policy-state', String(args.policy));
|
|
26591
|
+
if (args.reachability) cliArgs.push('--reachability', String(args.reachability));
|
|
26592
|
+
if (args.rateLimited === true) cliArgs.push('--rate-limited');
|
|
26593
|
+
if (args.rateLimited === false) cliArgs.push('--not-rate-limited');
|
|
26594
|
+
if (args.retryAfter) cliArgs.push('--retry-after', String(args.retryAfter));
|
|
26595
|
+
if (args.expiresAt) cliArgs.push('--expires-at', String(args.expiresAt));
|
|
26596
|
+
if (typeof args.ttlMin === 'number') cliArgs.push('--ttl-min', String(args.ttlMin));
|
|
26597
|
+
if (typeof args.limit === 'number') cliArgs.push('--limit', String(args.limit));
|
|
26598
|
+
break;
|
|
26599
|
+
}
|
|
26600
|
+
case 'leerness_agents_record':
|
|
26601
|
+
cliArgs = ['agents', 'record', String(args.event || ''), String(args.summary || ''), '--path', targetPath, '--json'];
|
|
26602
|
+
if (args.taskId) cliArgs.push('--task', String(args.taskId));
|
|
26603
|
+
if (args.attemptId) cliArgs.push('--id', String(args.attemptId));
|
|
26604
|
+
if (args.targetAttemptId) cliArgs.push('--target', String(args.targetAttemptId));
|
|
26605
|
+
if (args.role) cliArgs.push('--role', String(args.role));
|
|
26606
|
+
if (args.provider) cliArgs.push('--provider', String(args.provider));
|
|
26607
|
+
if (args.model) cliArgs.push('--model', String(args.model));
|
|
26608
|
+
if (args.modelFamily) cliArgs.push('--model-family', String(args.modelFamily));
|
|
26609
|
+
if (args.status) cliArgs.push('--status', String(args.status));
|
|
26610
|
+
if (args.evidence) cliArgs.push('--evidence', String(args.evidence));
|
|
26611
|
+
if (args.implementerProvider) cliArgs.push('--agent', String(args.implementerProvider));
|
|
26612
|
+
if (args.implementerModel) cliArgs.push('--agent-model', String(args.implementerModel));
|
|
26613
|
+
if (args.implementerModelFamily) cliArgs.push('--agent-model-family', String(args.implementerModelFamily));
|
|
26614
|
+
if (args.implementerSession) cliArgs.push('--agent-session', String(args.implementerSession));
|
|
26615
|
+
break;
|
|
26616
|
+
case 'leerness_agents_history':
|
|
26617
|
+
cliArgs = ['agents', 'history', '--path', targetPath, '--json'];
|
|
26618
|
+
if (typeof args.limit === 'number') cliArgs.push('--limit', String(args.limit));
|
|
26619
|
+
break;
|
|
26620
|
+
case 'leerness_agents_quota':
|
|
26621
|
+
cliArgs = ['agents', 'quota', '--path', targetPath, '--json'];
|
|
26622
|
+
break;
|
|
26100
26623
|
case 'leerness_reuse_map': cliArgs = ['reuse-map', targetPath, ...(args.allApps ? ['--all-apps'] : []), ...(args.strictElements ? ['--strict-elements'] : []), '--json']; break;
|
|
26101
26624
|
case 'leerness_whats_new': cliArgs = ['whats-new', '--path', targetPath, ...(args.from ? ['--from', args.from] : []), ...(args.to ? ['--to', args.to] : []), ...(args.all ? ['--all'] : []), ...(args.limit ? ['--limit', String(args.limit)] : []), '--json']; break; // 1.36.69 (검수 #1): limit/all 전달 — MCP 소비자도 상한 조절 가능
|
|
26102
26625
|
case 'leerness_usage_stats': cliArgs = ['usage', 'stats', targetPath, '--json']; break;
|
|
@@ -26290,11 +26813,24 @@ function _mcpToCliArgs(name, args, targetPath) {
|
|
|
26290
26813
|
if (args.refresh === true) cliArgs.push('--refresh');
|
|
26291
26814
|
if (args.dryRun === true) cliArgs.push('--dry-run');
|
|
26292
26815
|
break;
|
|
26816
|
+
case 'leerness_roles_validate':
|
|
26817
|
+
cliArgs = ['roles', 'validate', '--path', targetPath, '--json'];
|
|
26818
|
+
break;
|
|
26293
26819
|
case 'leerness_roles':
|
|
26294
|
-
//
|
|
26820
|
+
// 역할별 primary/candidate/fallback policy — CLI 단일 구현을 그대로 재사용.
|
|
26295
26821
|
cliArgs = ['roles', String(args.sub || 'list'), ...(args.role ? [String(args.role)] : []), '--path', targetPath, '--json'];
|
|
26296
26822
|
if (args.provider) cliArgs.push('--provider', String(args.provider));
|
|
26297
26823
|
if (args.model) cliArgs.push('--model', String(args.model));
|
|
26824
|
+
if (args.modelFamily) cliArgs.push('--model-family', String(args.modelFamily));
|
|
26825
|
+
const _roleCandidates = Array.isArray(args.candidates) ? args.candidates : (args.candidate ? [args.candidate] : []);
|
|
26826
|
+
const _roleCandidateFamilies = Array.isArray(args.candidateFamilies) ? args.candidateFamilies : (args.candidateFamily ? [args.candidateFamily] : []);
|
|
26827
|
+
const _roleRemoveCandidates = Array.isArray(args.removeCandidates) ? args.removeCandidates : [];
|
|
26828
|
+
for (const candidate of _roleCandidates) cliArgs.push('--candidate', String(candidate));
|
|
26829
|
+
for (const family of _roleCandidateFamilies) cliArgs.push('--candidate-family', String(family));
|
|
26830
|
+
for (const candidate of _roleRemoveCandidates) cliArgs.push('--remove-candidate', String(candidate));
|
|
26831
|
+
if (args.fallbackPolicy) cliArgs.push('--policy', String(args.fallbackPolicy));
|
|
26832
|
+
if (args.persona) cliArgs.push('--persona', String(args.persona));
|
|
26833
|
+
if (args.clearCandidates === true) cliArgs.push('--clear-candidates');
|
|
26298
26834
|
if (args.apply === true) cliArgs.push('--apply');
|
|
26299
26835
|
break;
|
|
26300
26836
|
// 1.9.279 (UR-0031): 상태 substrate MCP 시맨틱 verb
|
|
@@ -26319,6 +26855,22 @@ function _mcpToCliArgs(name, args, targetPath) {
|
|
|
26319
26855
|
}
|
|
26320
26856
|
break;
|
|
26321
26857
|
}
|
|
26858
|
+
case 'leerness_file_lease_read': {
|
|
26859
|
+
const _la = String(args.action || 'list');
|
|
26860
|
+
cliArgs = ['lease', _la, '--path', targetPath, '--json'];
|
|
26861
|
+
if (_la === 'check') cliArgs.splice(2, 0, String(args.file || ''));
|
|
26862
|
+
if (args.sessionKey) cliArgs.push('--session', String(args.sessionKey));
|
|
26863
|
+
if (args.all) cliArgs.push('--all');
|
|
26864
|
+
break;
|
|
26865
|
+
}
|
|
26866
|
+
case 'leerness_file_lease_write': {
|
|
26867
|
+
const _la = String(args.action || 'acquire');
|
|
26868
|
+
cliArgs = ['lease', _la, '--path', targetPath, '--json', '--session', String(args.sessionKey || '')];
|
|
26869
|
+
cliArgs.splice(2, 0, _la === 'release' ? String(args.leaseId || '') : String(args.file || ''));
|
|
26870
|
+
if (_la === 'acquire' && args.ttl !== undefined) cliArgs.push('--ttl', String(args.ttl));
|
|
26871
|
+
if (_la === 'acquire' && args.note) cliArgs.push('--note', String(args.note));
|
|
26872
|
+
break;
|
|
26873
|
+
}
|
|
26322
26874
|
case 'leerness_preview': {
|
|
26323
26875
|
const _pa = String(args.action || 'list');
|
|
26324
26876
|
cliArgs = ['preview', _pa, '--path', targetPath, '--json'];
|
|
@@ -26448,6 +27000,9 @@ function mcpServeCmd(root) {
|
|
|
26448
27000
|
// (검수) 오타 프로필이 무언 full 로 새지 않게 — 제한 요청이 조용히 무력화되는 fail-open 차단
|
|
26449
27001
|
if (_profile !== 'core' && _profile !== 'full') { fail(`알 수 없는 MCP 프로필: ${_profile} (가능: core, full)`); return process.exit(1); }
|
|
26450
27002
|
const TOOLS = _profile === 'core' ? _ALL_TOOLS.filter(t => _ALL_TOOLS.CORE.includes(t.name)) : _ALL_TOOLS;
|
|
27003
|
+
// A stdio MCP process represents one connection. Per-call sessionKey remains
|
|
27004
|
+
// a routing address, but it must not manufacture an independent reviewer.
|
|
27005
|
+
const _mcpConnectionId = `mcp-${require('crypto').randomBytes(16).toString('hex')}`;
|
|
26451
27006
|
|
|
26452
27007
|
function send(obj) {
|
|
26453
27008
|
process.stdout.write(JSON.stringify(obj) + '\n');
|
|
@@ -26465,6 +27020,10 @@ function mcpServeCmd(root) {
|
|
|
26465
27020
|
// MCP 서버 프로세스가 바깥 호출의 일회성 presence marker를 물려받았더라도 다음 호출로
|
|
26466
27021
|
// 재사용하지 않는다. 검증된 direct lifecycle 호출만 아래 extraEnv에서 다시 부여한다.
|
|
26467
27022
|
_deleteEnvKeyCaseInsensitive(childEnv, 'LEERNESS_MCP_ADDRESS_EXPLICIT');
|
|
27023
|
+
// Connection identity is minted by this stdio server. Never let an inherited
|
|
27024
|
+
// differently-cased Windows environment key compete with the minted value.
|
|
27025
|
+
_deleteEnvKeyCaseInsensitive(childEnv, 'LEERNESS_MCP_CONNECTION_ID');
|
|
27026
|
+
_deleteEnvKeyCaseInsensitive(childEnv, 'LEERNESS_SESSION_IDENTITY_SOURCE');
|
|
26468
27027
|
// Windows child_process는 env 이름을 대소문자 비구분으로 하나만 전달한다. 호출별 주소를
|
|
26469
27028
|
// 덮을 때 상속된 다른 casing이 남아 있으면 어느 값이 선택될지 정렬 순서에 의존한다.
|
|
26470
27029
|
if (extraEnv && Object.prototype.hasOwnProperty.call(extraEnv, 'LEERNESS_SESSION_ID')) {
|
|
@@ -26539,18 +27098,31 @@ function mcpServeCmd(root) {
|
|
|
26539
27098
|
{
|
|
26540
27099
|
const _props = _toolDef && _toolDef.inputSchema && _toolDef.inputSchema.properties || null;
|
|
26541
27100
|
if (_props) {
|
|
27101
|
+
const _invalidParam = message => send({ jsonrpc: '2.0', id, error: { code: -32602, message: `Invalid params: ${message}` } });
|
|
27102
|
+
if (_toolDef.inputSchema.additionalProperties === false) {
|
|
27103
|
+
const extra = Object.keys(args).find(key => !Object.prototype.hasOwnProperty.call(_props, key));
|
|
27104
|
+
if (extra) return _invalidParam(`unknown property '${extra}'`);
|
|
27105
|
+
}
|
|
26542
27106
|
for (const [k, spec] of Object.entries(_props)) {
|
|
26543
27107
|
const v = args[k];
|
|
26544
27108
|
if (v === undefined || v === null) continue;
|
|
26545
|
-
if (spec.type === 'string' && typeof v !== 'string') return
|
|
26546
|
-
if (spec.type === 'number' && typeof v !== 'number') return
|
|
26547
|
-
if (spec.type === 'boolean' && typeof v !== 'boolean') return
|
|
27109
|
+
if (spec.type === 'string' && typeof v !== 'string') return _invalidParam(`'${k}' must be a string`);
|
|
27110
|
+
if (spec.type === 'number' && typeof v !== 'number') return _invalidParam(`'${k}' must be a number`);
|
|
27111
|
+
if (spec.type === 'boolean' && typeof v !== 'boolean') return _invalidParam(`'${k}' must be a boolean`);
|
|
27112
|
+
if (spec.type === 'array') {
|
|
27113
|
+
if (!Array.isArray(v)) return _invalidParam(`'${k}' must be an array`);
|
|
27114
|
+
if (Number.isInteger(spec.maxItems) && v.length > spec.maxItems) return _invalidParam(`'${k}' must contain at most ${spec.maxItems} item(s)`);
|
|
27115
|
+
if (spec.items && spec.items.type === 'string' && v.some(item => typeof item !== 'string')) return _invalidParam(`'${k}' items must be strings`);
|
|
27116
|
+
}
|
|
27117
|
+
if (Array.isArray(spec.enum) && !spec.enum.includes(v)) return _invalidParam(`'${k}' must be one of ${spec.enum.join('|')}`);
|
|
26548
27118
|
}
|
|
26549
27119
|
for (const rk of (_toolDef.inputSchema.required || [])) {
|
|
26550
|
-
if (args[rk] === undefined || args[rk] === null) return
|
|
27120
|
+
if (args[rk] === undefined || args[rk] === null) return _invalidParam(`missing required '${rk}'`);
|
|
26551
27121
|
}
|
|
26552
27122
|
}
|
|
26553
27123
|
}
|
|
27124
|
+
const _leaseParamError = _fileLeaseMcpParamError(name, args);
|
|
27125
|
+
if (_leaseParamError) return send({ jsonrpc: '2.0', id, error: { code: -32602, message: _leaseParamError } });
|
|
26554
27126
|
// 1.36.171 (외부 Codex P1/P2): sessionKey는 광고한 도구에서만 받으며, 명시된 값은
|
|
26555
27127
|
// MCP 경계에서 완전히 검증한다. 잘못된 값을 CLI가 무시해 서버의 CODEX_THREAD_ID나 전역
|
|
26556
27128
|
// currentRunId로 떨어뜨리면 서로 다른 에이전트의 증거가 다시 섞인다.
|
|
@@ -26601,8 +27173,13 @@ function mcpServeCmd(root) {
|
|
|
26601
27173
|
// 실제로 받은 **직접 handoff/close**에만 붙인다. state/agent-mode/일반 내부 스폰은
|
|
26602
27174
|
// presence 억제 예외가 아니며, sessionKey 자체는 상태 귀속용 env로만 전달한다.
|
|
26603
27175
|
const _lifecycle = name === 'leerness_handoff' || name === 'leerness_session_close';
|
|
26604
|
-
const _mcpEnv = {
|
|
26605
|
-
|
|
27176
|
+
const _mcpEnv = {
|
|
27177
|
+
LEERNESS_MCP_CONNECTION_ID: _mcpConnectionId,
|
|
27178
|
+
LEERNESS_SESSION_IDENTITY_SOURCE: 'mcp-connection',
|
|
27179
|
+
};
|
|
27180
|
+
if (_sk) {
|
|
27181
|
+
_mcpEnv.LEERNESS_SESSION_ID = _sk;
|
|
27182
|
+
}
|
|
26606
27183
|
if (_sk && _lifecycle) _mcpEnv.LEERNESS_MCP_ADDRESS_EXPLICIT = '1';
|
|
26607
27184
|
if (_mcpReadOnly) _mcpEnv.LEERNESS_NO_AUTO_WORKSPACE_MIGRATION = '1';
|
|
26608
27185
|
const r = callLeerness(cliArgs, _mcpEnv);
|
|
@@ -26614,9 +27191,16 @@ function mcpServeCmd(root) {
|
|
|
26614
27191
|
const cursor = (args._cursor && /^\d+$/.test(String(args._cursor))) ? parseInt(args._cursor, 10) : 0;
|
|
26615
27192
|
const chunk = fullText.slice(cursor, cursor + CHUNK_SIZE);
|
|
26616
27193
|
const nextCursor = (cursor + CHUNK_SIZE) < fullText.length ? String(cursor + CHUNK_SIZE) : null;
|
|
27194
|
+
let successfulDomainResponse = false;
|
|
27195
|
+
if (name === 'leerness_file_lease_read' && args.action === 'check') {
|
|
27196
|
+
try { successfulDomainResponse = JSON.parse(fullText).ok === true; } catch {}
|
|
27197
|
+
}
|
|
26617
27198
|
const result = {
|
|
26618
27199
|
content: [{ type: 'text', text: chunk }],
|
|
26619
|
-
|
|
27200
|
+
// A peer-owned lease is a successful check result with
|
|
27201
|
+
// available=false. Keep CLI exit status 1 for shell gating without
|
|
27202
|
+
// mislabeling that normal domain result as an MCP transport/tool error.
|
|
27203
|
+
isError: !r.ok && !successfulDomainResponse
|
|
26620
27204
|
};
|
|
26621
27205
|
if (nextCursor) {
|
|
26622
27206
|
result.nextCursor = nextCursor;
|
|
@@ -27120,10 +27704,10 @@ async function _ollamaListModels() {
|
|
|
27120
27704
|
async function _cliChat(root, provider, prompt, opts) {
|
|
27121
27705
|
opts = opts || {};
|
|
27122
27706
|
const agent = EXTERNAL_AGENTS.find(a => a.id === provider);
|
|
27123
|
-
if (!agent) return { ok: false, error: `unknown provider: ${provider}`, provider };
|
|
27707
|
+
if (!agent) return { ok: false, error: `unknown provider: ${provider}`, provider, executed: false };
|
|
27124
27708
|
const status = _checkAgent(agent);
|
|
27125
27709
|
if (status.status !== 'ready') {
|
|
27126
|
-
return { ok: false, error: `${provider} 비활성 (${status.status}) — .env 에서 ${agent.envFlag}=1 + CLI 설치 필요`, provider };
|
|
27710
|
+
return { ok: false, error: `${provider} 비활성 (${status.status}) — .env 에서 ${agent.envFlag}=1 + CLI 설치 필요`, provider, executed: false };
|
|
27127
27711
|
}
|
|
27128
27712
|
// CLI 별 비-인터랙티브 호출 인자 매핑 (read-only 모드 — REPL 안에서 파일 수정 X)
|
|
27129
27713
|
// 1.9.188 (사용자 명시 fix): 한글/특수문자 prompt 시 shell escape 실패 회피 → stdin 으로 전달.
|
|
@@ -27132,7 +27716,7 @@ async function _cliChat(root, provider, prompt, opts) {
|
|
|
27132
27716
|
else if (provider === 'codex') { cmd = 'codex'; args = ['exec', '--skip-git-repo-check', '-']; stdinInput = prompt; }
|
|
27133
27717
|
else if (provider === 'agy') { cmd = 'agy'; args = ['-p', prompt]; } // agy (Antigravity) 는 인자 only
|
|
27134
27718
|
else if (provider === 'copilot') { cmd = 'gh'; args = ['copilot', 'suggest', prompt]; }
|
|
27135
|
-
else return { ok: false, error: `provider ${provider} 미지원`, provider };
|
|
27719
|
+
else return { ok: false, error: `provider ${provider} 미지원`, provider, executed: false };
|
|
27136
27720
|
// runCommandSafe — env scrub + observability 자동
|
|
27137
27721
|
const r = runCommandSafe(cmd, args, {
|
|
27138
27722
|
cwd: process.cwd(), root,
|
|
@@ -27142,12 +27726,13 @@ async function _cliChat(root, provider, prompt, opts) {
|
|
|
27142
27726
|
input: stdinInput // 1.9.188: claude/codex 는 stdin 전달
|
|
27143
27727
|
});
|
|
27144
27728
|
if (r.status === 0) {
|
|
27145
|
-
return { ok: true, response: (r.stdout || '').trim(), provider, model: provider };
|
|
27729
|
+
return { ok: true, response: (r.stdout || '').trim(), provider, model: provider, executed: true };
|
|
27146
27730
|
}
|
|
27147
27731
|
return {
|
|
27148
27732
|
ok: false,
|
|
27149
27733
|
error: `exit=${r.status} ${(r.stderr || r.stdout || '').slice(0, 200)}`,
|
|
27150
|
-
provider
|
|
27734
|
+
provider,
|
|
27735
|
+
executed: true,
|
|
27151
27736
|
};
|
|
27152
27737
|
}
|
|
27153
27738
|
|
|
@@ -27587,65 +28172,189 @@ function ciInitCmd(root, opts = {}) {
|
|
|
27587
28172
|
log(' ⮕ 다음 단계(가드레일 완성): GitHub branch protection 에서 leerness-gate 를 "required" 체크로 지정 — 그래야 우회 불가.');
|
|
27588
28173
|
}
|
|
27589
28174
|
// 1.9.294 (UR-0025 3단계): 역할/모델 카탈로그(_PROVIDER_MODEL_CATALOG + _AGENT_ROLE_PROMPTS + ROLE_CATALOG + _ROLE_ALIASES) 데이터 모듈 분리 (비파괴, require-based).
|
|
27590
|
-
const { _PROVIDER_MODEL_CATALOG, _AGENT_ROLE_PROMPTS, ROLE_CATALOG, _ROLE_ALIASES } = require('../lib/role-catalog');
|
|
28175
|
+
const { _PROVIDER_MODEL_CATALOG, _AGENT_ROLE_PROMPTS, ROLE_CATALOG, ROLE_REQUIREMENTS, _ROLE_ALIASES, pickCatalogModel } = require('../lib/role-catalog');
|
|
28176
|
+
const { FALLBACK_POLICIES, validateRoleDefinitionShape, normalizeRoleDefinition, normalizeProviderIdentifier, canonicalProviderIdentity, canonicalLegacyProviderIdentity, isValidModelIdentifier, inferModelFamily, normalizeAvailability, readAvailabilityObservations, availabilityExtrasForCandidate } = require('../lib/role-fallback');
|
|
28177
|
+
const _roleStore = require('../lib/role-store');
|
|
27591
28178
|
function _normalizeRole(name) {
|
|
27592
28179
|
const raw = String(name || '').trim();
|
|
27593
|
-
if (_ROLE_ALIASES
|
|
28180
|
+
if (_roleStore.hasOwn(_ROLE_ALIASES, raw)) return _ROLE_ALIASES[raw];
|
|
27594
28181
|
const low = raw.toLowerCase();
|
|
27595
|
-
return _ROLE_ALIASES[low]
|
|
28182
|
+
return _roleStore.hasOwn(_ROLE_ALIASES, low) ? _ROLE_ALIASES[low] : low;
|
|
27596
28183
|
}
|
|
28184
|
+
function _roleCatalogDef(role) { return _roleStore.hasOwn(ROLE_CATALOG, role) ? ROLE_CATALOG[role] : null; }
|
|
27597
28185
|
// 역할 modelKind 에 맞는 모델 선택 — top(최상위) / code(코드 특화) / fast(빠른). 순수 함수(selftest).
|
|
27598
|
-
function _pickModel(provider, kind) {
|
|
27599
|
-
|
|
27600
|
-
|
|
27601
|
-
|
|
27602
|
-
if (
|
|
27603
|
-
return
|
|
27604
|
-
}
|
|
27605
|
-
function
|
|
27606
|
-
function
|
|
27607
|
-
const
|
|
27608
|
-
|
|
27609
|
-
|
|
28186
|
+
function _pickModel(provider, kind) { return pickCatalogModel(provider, kind); }
|
|
28187
|
+
function _rolesFile(root) { return _roleStore.rolesFile(root); }
|
|
28188
|
+
function _loadRoleState(root) {
|
|
28189
|
+
const state = _roleStore.readRoleStore(root);
|
|
28190
|
+
if (!state.ok) throw new _roleStore.RoleStoreError(state);
|
|
28191
|
+
return state;
|
|
28192
|
+
}
|
|
28193
|
+
function _loadRoles(root) { return _loadRoleState(root).roles; }
|
|
28194
|
+
function _loadRolesChecked(root) {
|
|
28195
|
+
const state = _roleStore.readRoleStore(root);
|
|
28196
|
+
return {
|
|
28197
|
+
...state,
|
|
28198
|
+
invalid: !state.ok,
|
|
28199
|
+
roles: state.ok ? state.roles : {},
|
|
28200
|
+
reason: state.ok ? null : _roleStore.messageFor(state),
|
|
28201
|
+
schemaVersion: state.ok && state.document ? (state.document.schemaVersion || null) : null,
|
|
28202
|
+
};
|
|
27610
28203
|
}
|
|
27611
28204
|
// 1.36.137 (동시성 감사 실측): `roles set/unset/suggest --apply` 는 load→mutate→save 를 **락 없이** 했다 —
|
|
27612
28205
|
// 4 프로세스만으로 28.8%, 16 프로세스에서 11~15/16 만 살아남았다(exit 0 · 경고 0 · 손상 0).
|
|
27613
28206
|
// 저장만 잠그면 소용이 없다(읽기와 쓰기 사이가 창이다). 읽기·변경·쓰기를 **한 락 안**에 묶는다 —
|
|
27614
28207
|
// 1.36.108 이 rules.md 에서 만든 형태 그대로다.
|
|
27615
|
-
function _updateRoles(root, mutate) {
|
|
27616
|
-
|
|
27617
|
-
|
|
27618
|
-
|
|
27619
|
-
|
|
27620
|
-
|
|
27621
|
-
|
|
28208
|
+
function _updateRoles(root, mutate) { return _roleStore.updateRoles(root, mutate, _withLock); }
|
|
28209
|
+
function _saveRoles(root, roles, validatedState) { return _roleStore.saveRoles(root, roles, validatedState); }
|
|
28210
|
+
// 역할 → {role, provider, model, persona, source} 해석 (사용자 설정 전용; 미설정 시 null).
|
|
28211
|
+
function _resolveRoleFromState(state, name) {
|
|
28212
|
+
const role = _normalizeRole(name);
|
|
28213
|
+
const roles = state.roles;
|
|
28214
|
+
const def = _roleStore.hasOwn(roles, role) ? roles[role] : null;
|
|
28215
|
+
if (!def || typeof def !== 'object') return null;
|
|
28216
|
+
const normalized = normalizeRoleDefinition(role, def, { allowLegacyProviderIds: true });
|
|
28217
|
+
const primary = normalized.primary;
|
|
28218
|
+
const provider = primary && typeof primary.provider === 'string' ? primary.provider.trim() : '';
|
|
28219
|
+
if (!provider) return null;
|
|
28220
|
+
return {
|
|
28221
|
+
role,
|
|
28222
|
+
provider,
|
|
28223
|
+
model: primary.model || null,
|
|
28224
|
+
modelFamily: primary.modelFamily || inferModelFamily(primary.model),
|
|
28225
|
+
persona: normalized.persona || '',
|
|
28226
|
+
candidates: normalized.candidates,
|
|
28227
|
+
fallbackPolicy: normalized.fallbackPolicy,
|
|
28228
|
+
requirements: normalized.requirements,
|
|
28229
|
+
primary: { provider, model: primary.model || null, modelFamily: primary.modelFamily || inferModelFamily(primary.model) },
|
|
28230
|
+
source: 'user',
|
|
28231
|
+
storeRevision: state.revision,
|
|
28232
|
+
};
|
|
27622
28233
|
}
|
|
27623
|
-
function
|
|
27624
|
-
const
|
|
27625
|
-
|
|
27626
|
-
writeUtf8(f, JSON.stringify({ schemaVersion: 1, updatedAt: new Date().toISOString(), roles }, null, 2) + '\n');
|
|
27627
|
-
return f;
|
|
28234
|
+
function _resolveRoles(root, names) {
|
|
28235
|
+
const state = _loadRoleState(root);
|
|
28236
|
+
return (names || []).map(name => _resolveRoleFromState(state, name));
|
|
27628
28237
|
}
|
|
27629
|
-
// 역할 → {role, provider, model, persona, source} 해석 (사용자 설정 전용; 미설정 시 null).
|
|
27630
28238
|
function _resolveRole(root, name) {
|
|
27631
|
-
|
|
27632
|
-
|
|
27633
|
-
|
|
27634
|
-
|
|
28239
|
+
return _resolveRoles(root, [name])[0];
|
|
28240
|
+
}
|
|
28241
|
+
function _emitRoleStoreFailure(json, state, language = 'ko') {
|
|
28242
|
+
const payload = { ok: false, ..._roleStore.publicError(state) };
|
|
28243
|
+
if (json) { log(JSON.stringify(payload, null, 2)); process.exitCode = 1; return payload; }
|
|
28244
|
+
fail(_roleStore.messageFor(state, language));
|
|
28245
|
+
for (const problem of payload.problems.slice(0, 5)) log(` - ${problem.code}: ${problem.path}`);
|
|
28246
|
+
log(language === 'en'
|
|
28247
|
+
? ' After recovery: leerness roles validate --path .'
|
|
28248
|
+
: ' 복구 후 확인: leerness roles validate --path .');
|
|
28249
|
+
return payload;
|
|
28250
|
+
}
|
|
28251
|
+
function _guardRoleStore(json, action) {
|
|
28252
|
+
try { return { ok: true, value: action() }; }
|
|
28253
|
+
catch (error) {
|
|
28254
|
+
if (error instanceof _roleStore.RoleStoreError) {
|
|
28255
|
+
_emitRoleStoreFailure(json, error.roleStoreState, _uiLang(process.cwd()));
|
|
28256
|
+
return { ok: false, value: null };
|
|
28257
|
+
}
|
|
28258
|
+
throw error;
|
|
28259
|
+
}
|
|
28260
|
+
}
|
|
28261
|
+
function _validateRolesStoreCmd(root, json) {
|
|
28262
|
+
const language = _uiLang(root);
|
|
28263
|
+
const en = language === 'en';
|
|
28264
|
+
const result = _roleStore.validationSummary(root);
|
|
28265
|
+
if (!result.ok) { _emitRoleStoreFailure(json, result, language); return; }
|
|
28266
|
+
if (json) { log(JSON.stringify(result, null, 2)); return result; }
|
|
28267
|
+
log('# leerness roles validate');
|
|
28268
|
+
log(en
|
|
28269
|
+
? ` state: ${result.state} · roles ${result.legacy.roleCount} · file ${result.fileExists ? result.bytes + ' bytes' : 'absent'}`
|
|
28270
|
+
: ` 상태: ${result.state} · 역할 ${result.legacy.roleCount}개 · 파일 ${result.fileExists ? result.bytes + ' bytes' : '없음'}`);
|
|
28271
|
+
log(en
|
|
28272
|
+
? ` v2 preview projection: Agents ${result.projection.agentCount} · pipelines ${result.projection.pipelineCount} · assignmentReady=${result.projection.assignmentReady}`
|
|
28273
|
+
: ` v2 미리보기 projection: Agent ${result.projection.agentCount}개 · pipeline ${result.projection.pipelineCount}개 · assignmentReady=${result.projection.assignmentReady}`);
|
|
28274
|
+
log(en
|
|
28275
|
+
? ' This command did not migrate files or enable v2 runtime routing.'
|
|
28276
|
+
: ' 이 명령은 파일을 마이그레이션하거나 v2 런타임 라우팅을 활성화하지 않았습니다.');
|
|
28277
|
+
log(en
|
|
28278
|
+
? ' Native role-definitions/agent-instances/routing-policy files were not read; this is a legacy projection preview.'
|
|
28279
|
+
: ' native role-definitions/agent-instances/routing-policy 파일은 읽지 않았으며 legacy 투영 미리보기입니다.');
|
|
28280
|
+
log(en
|
|
28281
|
+
? ' Provider readiness, authentication, entitlement, quota, and live callability were not checked.'
|
|
28282
|
+
: ' provider 준비·인증·모델 권한·quota·실제 호출 가능성은 확인하지 않았습니다.');
|
|
28283
|
+
return result;
|
|
27635
28284
|
}
|
|
27636
28285
|
// 활성(ready) 에이전트 기반 최적 역할 배치 제안 — "올바른 방향성 판단" (사용자 요청).
|
|
27637
28286
|
function _suggestRoles(root) {
|
|
27638
|
-
const
|
|
28287
|
+
const availabilityState = readAvailabilityObservations(root, 2000);
|
|
28288
|
+
if (availabilityState.ok === false || availabilityState.partial) {
|
|
28289
|
+
return { ready: [], suggestions: [], error: { code: availabilityState.code || 'availability_history_partial', message: availabilityState.error || 'availability observation history is incomplete' } };
|
|
28290
|
+
}
|
|
28291
|
+
const providers = _allProviders(root);
|
|
28292
|
+
const inspected = providers.map(definition => {
|
|
28293
|
+
let raw;
|
|
28294
|
+
try { raw = _checkAgent(definition); }
|
|
28295
|
+
catch (error) { raw = { status: 'check-error', error: error && error.message }; }
|
|
28296
|
+
const availability = normalizeAvailability(definition, raw, {
|
|
28297
|
+
provider: definition.id,
|
|
28298
|
+
...availabilityExtrasForCandidate(availabilityState, { provider: definition.id, model: null }),
|
|
28299
|
+
});
|
|
28300
|
+
return { id: definition.id, definition, availability };
|
|
28301
|
+
});
|
|
28302
|
+
const eligible = inspected.filter(item => item.availability.eligible);
|
|
28303
|
+
const ready = eligible.map(item => item.id);
|
|
27639
28304
|
const readySet = new Set(ready);
|
|
28305
|
+
const providerOrder = (def) => [...new Set([...(def.prefer || []), ...ready])].filter(id => readySet.has(id));
|
|
28306
|
+
const coderDef = ROLE_CATALOG.coder || { prefer: [] };
|
|
28307
|
+
const coderProvider = providerOrder(coderDef)[0] || null;
|
|
27640
28308
|
const suggestions = [];
|
|
27641
28309
|
for (const [role, def] of Object.entries(ROLE_CATALOG)) {
|
|
27642
|
-
|
|
27643
|
-
|
|
27644
|
-
|
|
28310
|
+
let ordered = providerOrder(def);
|
|
28311
|
+
if (role === 'reviewer' && coderProvider && ordered.some(id => id !== coderProvider)) {
|
|
28312
|
+
ordered = ordered.filter(id => id !== coderProvider).concat(ordered.filter(id => id === coderProvider));
|
|
28313
|
+
}
|
|
28314
|
+
const provider = role === 'coder' && coderProvider ? coderProvider : (ordered[0] || null);
|
|
28315
|
+
const availability = provider ? inspected.find(item => item.id === provider).availability : null;
|
|
28316
|
+
const candidates = ordered.filter(id => id !== provider).map(id => ({ provider: id, model: null, modelFamily: null }));
|
|
28317
|
+
suggestions.push({
|
|
28318
|
+
role, ko: def.ko, desc: def.desc, provider, model: null, modelFamily: null,
|
|
28319
|
+
ready: !!provider, candidates, availability,
|
|
28320
|
+
modelVerified: false,
|
|
28321
|
+
why: provider ? `${def.why} · provider 실행 준비만 확인; 실제 모델 ID/권한/쿼터는 미검증` : def.why,
|
|
28322
|
+
prefer: def.prefer,
|
|
28323
|
+
});
|
|
28324
|
+
}
|
|
28325
|
+
return { ready, inspected: inspected.map(item => ({ id: item.id, availability: item.availability })), suggestions, availabilityObservationState: { activeCount: availabilityState.activeCount, partial: availabilityState.partial } };
|
|
28326
|
+
}
|
|
28327
|
+
function _roleFlagValues(argv, name) {
|
|
28328
|
+
const list = Array.isArray(argv) ? argv : [];
|
|
28329
|
+
const out = [];
|
|
28330
|
+
for (let i = 0; i < list.length; i++) {
|
|
28331
|
+
const token = String(list[i] || '');
|
|
28332
|
+
if (token === name && i + 1 < list.length && !String(list[i + 1]).startsWith('-')) out.push(String(list[++i]));
|
|
28333
|
+
else if (token.startsWith(name + '=')) out.push(token.slice(name.length + 1));
|
|
27645
28334
|
}
|
|
27646
|
-
return
|
|
28335
|
+
return out;
|
|
28336
|
+
}
|
|
28337
|
+
function _parseRoleCandidateSpec(value, family) {
|
|
28338
|
+
const raw = String(value || '').trim();
|
|
28339
|
+
const at = raw.indexOf(':');
|
|
28340
|
+
const provider = canonicalProviderIdentity(at >= 0 ? raw.slice(0, at) : raw);
|
|
28341
|
+
const model = (at >= 0 ? raw.slice(at + 1) : '').trim();
|
|
28342
|
+
if (!provider) return null;
|
|
28343
|
+
return { provider, model: model || null, modelFamily: String(family || '').trim().toLowerCase() || inferModelFamily(model) };
|
|
28344
|
+
}
|
|
28345
|
+
function _roleCandidatesFromFlags(argv) {
|
|
28346
|
+
const specs = _roleFlagValues(argv, '--candidate');
|
|
28347
|
+
const families = _roleFlagValues(argv, '--candidate-family');
|
|
28348
|
+
if (families.length > specs.length) return { error: '--candidate-family 수가 --candidate 수보다 많습니다' };
|
|
28349
|
+
const candidates = specs.map((v, i) => _parseRoleCandidateSpec(v, families[i])).filter(Boolean);
|
|
28350
|
+
const removals = _roleFlagValues(argv, '--remove-candidate').map(v => _parseRoleCandidateSpec(v, null)).filter(Boolean);
|
|
28351
|
+
if (candidates.length !== specs.length || removals.length !== _roleFlagValues(argv, '--remove-candidate').length) {
|
|
28352
|
+
return { error: 'candidate provider는 정규 ASCII provider ID여야 합니다' };
|
|
28353
|
+
}
|
|
28354
|
+
return { candidates, removals, touched: specs.length > 0 || removals.length > 0 || argv.includes('--clear-candidates') };
|
|
27647
28355
|
}
|
|
27648
|
-
|
|
28356
|
+
|
|
28357
|
+
// leerness roles <list|set|unset|catalog|suggest|verify|validate>
|
|
27649
28358
|
function rolesCmd(root, sub, ...args) {
|
|
27650
28359
|
root = absRoot(root || process.cwd());
|
|
27651
28360
|
// 1.36.140 (재검수 P3, 재현): 오타 난 `--path` 로 `roles unset` 하면 **실패하면서 그 경로와 `.leerness/` 를 만들었다**.
|
|
@@ -27658,9 +28367,17 @@ function rolesCmd(root, sub, ...args) {
|
|
|
27658
28367
|
failJson(has('--json'), 'path_not_found', `경로가 없습니다: ${root} — 디렉토리를 새로 만들지 않았습니다`);
|
|
27659
28368
|
return true;
|
|
27660
28369
|
};
|
|
27661
|
-
try { _loadEnvFile(root); _loadEnvFile(path.join(root, '..')); } catch {}
|
|
27662
28370
|
const json = has('--json');
|
|
27663
|
-
|
|
28371
|
+
const _storeFailure = (error) => {
|
|
28372
|
+
if (error instanceof _roleStore.RoleStoreError) {
|
|
28373
|
+
_emitRoleStoreFailure(json, error.roleStoreState, _uiLang(root));
|
|
28374
|
+
return;
|
|
28375
|
+
}
|
|
28376
|
+
failJson(json, (error && error.code) || 'role_store_error', (error && error.message) || 'agent-roles.json을 읽거나 쓸 수 없습니다');
|
|
28377
|
+
};
|
|
28378
|
+
sub = String(sub || 'list').trim().toLowerCase();
|
|
28379
|
+
|
|
28380
|
+
if (sub === 'validate') return _validateRolesStoreCmd(root, json);
|
|
27664
28381
|
|
|
27665
28382
|
if (sub === 'catalog') {
|
|
27666
28383
|
if (json) { log(JSON.stringify({ roles: ROLE_CATALOG }, null, 2)); return; }
|
|
@@ -27669,29 +28386,113 @@ function rolesCmd(root, sub, ...args) {
|
|
|
27669
28386
|
log(` ${role.padEnd(11)} ${d.ko.padEnd(8)} — ${d.desc}`);
|
|
27670
28387
|
log(` 선호 provider: ${d.prefer.join(' > ')} · 모델 등급: ${d.modelKind} · 근거: ${d.why}`);
|
|
27671
28388
|
}
|
|
27672
|
-
log(`\n 설정: leerness roles set <role> --provider <id> [--model <m>] [--
|
|
28389
|
+
log(`\n 설정: leerness roles set <role> --provider <id> [--model <m>] [--model-family <f>] [--policy strict|balanced|continuity] [--candidate <provider[:model]> ...]`);
|
|
27673
28390
|
log(` 자동 제안: leerness roles suggest [--apply]`);
|
|
27674
28391
|
return;
|
|
27675
28392
|
}
|
|
27676
28393
|
|
|
27677
28394
|
if (sub === 'set') {
|
|
28395
|
+
const preflight = _guardRoleStore(json, () => _loadRoles(root));
|
|
28396
|
+
if (!preflight.ok) return;
|
|
28397
|
+
try { _loadEnvFile(root); _loadEnvFile(path.join(root, '..')); } catch {}
|
|
27678
28398
|
const roleRaw = args.find(a => a && !a.startsWith('-'));
|
|
27679
28399
|
const role = _normalizeRole(roleRaw);
|
|
27680
28400
|
if (!role) return fail('roles set <role> 필요 (예: coder, reviewer, commander)');
|
|
27681
|
-
if (!
|
|
27682
|
-
const
|
|
28401
|
+
if (!_roleCatalogDef(role) && !has('--force')) return fail(`알 수 없는 역할: ${role} (catalog: ${Object.keys(ROLE_CATALOG).join(', ')}) — 커스텀 역할은 --force`);
|
|
28402
|
+
const providerRaw = arg('--provider', null) || arg('--to', null);
|
|
28403
|
+
const provider = canonicalProviderIdentity(providerRaw);
|
|
27683
28404
|
if (!provider) return fail('--provider <id> 필요 (claude/codex/agy/grok/copilot/ollama)');
|
|
27684
28405
|
if (!_allProviders(root).some(p => p.id === provider) && !has('--force')) return fail(`알 수 없는 provider: ${provider} — 커스텀은 --force 또는 provider add`);
|
|
27685
|
-
|
|
27686
|
-
|
|
27687
|
-
const
|
|
28406
|
+
const model = arg('--model', null) || null;
|
|
28407
|
+
const modelFamily = String(arg('--model-family', '') || '').trim().toLowerCase() || inferModelFamily(model);
|
|
28408
|
+
const requestedPolicy = arg('--policy', null) || arg('--fallback-policy', null);
|
|
28409
|
+
if (requestedPolicy && !FALLBACK_POLICIES.includes(String(requestedPolicy).toLowerCase())) return fail(`--policy 는 ${FALLBACK_POLICIES.join('|')} (받음: ${requestedPolicy})`);
|
|
28410
|
+
const rawRoleArgv = process.argv.slice(2);
|
|
28411
|
+
const parsedCandidates = _roleCandidatesFromFlags(rawRoleArgv);
|
|
28412
|
+
if (parsedCandidates.error) return fail(parsedCandidates.error);
|
|
28413
|
+
const allProviders = _allProviders(root);
|
|
28414
|
+
const knownProviders = new Set(allProviders.map(p => p.id));
|
|
28415
|
+
const unknownCandidates = parsedCandidates.candidates.concat(parsedCandidates.removals).filter(c => !knownProviders.has(c.provider));
|
|
28416
|
+
if (unknownCandidates.length && !has('--force')) return fail(`알 수 없는 candidate provider: ${[...new Set(unknownCandidates.map(c => c.provider))].join(', ')} — provider add 또는 --force`);
|
|
28417
|
+
const persona = arg('--persona', null) || (_roleCatalogDef(role) ? _roleCatalogDef(role).desc : '');
|
|
27688
28418
|
if (_requireExistingPath()) return;
|
|
27689
|
-
|
|
27690
|
-
|
|
28419
|
+
let f, savedRole;
|
|
28420
|
+
try {
|
|
28421
|
+
f = _updateRoles(root, (roles) => {
|
|
28422
|
+
const previous = _roleStore.hasOwn(roles, role) && roles[role] && typeof roles[role] === 'object' ? roles[role] : {};
|
|
28423
|
+
const previousForUpdate = { ...previous };
|
|
28424
|
+
delete previousForUpdate.policy;
|
|
28425
|
+
const previousNormalized = normalizeRoleDefinition(role, previous, { allowLegacyProviderIds: true });
|
|
28426
|
+
const previousCandidateExtensions = new Map(
|
|
28427
|
+
(Array.isArray(previous.candidates) ? previous.candidates : [])
|
|
28428
|
+
.filter(candidate => candidate && typeof candidate === 'object' && !Array.isArray(candidate))
|
|
28429
|
+
.map(candidate => {
|
|
28430
|
+
const candidateProvider = canonicalProviderIdentity(candidate.provider || candidate.id || '');
|
|
28431
|
+
const candidateModel = candidate.model == null ? '' : String(candidate.model).trim();
|
|
28432
|
+
return [`${candidateProvider || ''}\u0000${candidateModel}`, candidate];
|
|
28433
|
+
}),
|
|
28434
|
+
);
|
|
28435
|
+
let candidates = previousNormalized.candidates.map(candidate => ({
|
|
28436
|
+
provider: candidate.provider,
|
|
28437
|
+
model: candidate.model || null,
|
|
28438
|
+
modelFamily: candidate.modelFamily || null,
|
|
28439
|
+
}));
|
|
28440
|
+
if (has('--clear-candidates')) candidates = [];
|
|
28441
|
+
if (parsedCandidates.candidates.length) candidates = parsedCandidates.candidates;
|
|
28442
|
+
if (parsedCandidates.removals.length) {
|
|
28443
|
+
const removed = new Set(parsedCandidates.removals.map(c => `${c.provider}\u0000${c.model || ''}`));
|
|
28444
|
+
candidates = candidates.filter(c => !removed.has(`${c.provider}\u0000${c.model || ''}`));
|
|
28445
|
+
}
|
|
28446
|
+
const normalized = normalizeRoleDefinition(role, {
|
|
28447
|
+
...previousForUpdate,
|
|
28448
|
+
primary: { provider, model: model || null, modelFamily },
|
|
28449
|
+
candidates,
|
|
28450
|
+
fallbackPolicy: requestedPolicy || previous.fallbackPolicy || previous.policy || 'balanced',
|
|
28451
|
+
requirements: previous.requirements || ROLE_REQUIREMENTS[role] || {},
|
|
28452
|
+
persona,
|
|
28453
|
+
}, { allowLegacyProviderIds: true });
|
|
28454
|
+
savedRole = {
|
|
28455
|
+
...previousForUpdate,
|
|
28456
|
+
provider,
|
|
28457
|
+
model: model || null,
|
|
28458
|
+
modelFamily: modelFamily || null,
|
|
28459
|
+
persona,
|
|
28460
|
+
primary: {
|
|
28461
|
+
...(previous.primary && typeof previous.primary === 'object' && !Array.isArray(previous.primary) ? previous.primary : {}),
|
|
28462
|
+
provider,
|
|
28463
|
+
model: model || null,
|
|
28464
|
+
modelFamily: modelFamily || null,
|
|
28465
|
+
},
|
|
28466
|
+
candidates: normalized.candidates.map(c => ({
|
|
28467
|
+
...(previousCandidateExtensions.get(`${c.provider}\u0000${c.model || ''}`) || {}),
|
|
28468
|
+
provider: c.provider,
|
|
28469
|
+
model: c.model || null,
|
|
28470
|
+
modelFamily: c.modelFamily || null,
|
|
28471
|
+
})),
|
|
28472
|
+
fallbackPolicy: normalized.fallbackPolicy,
|
|
28473
|
+
requirements: normalized.requirements,
|
|
28474
|
+
};
|
|
28475
|
+
const savedShape = validateRoleDefinitionShape(role, savedRole, { allowLegacyProviderIds: true });
|
|
28476
|
+
if (!savedShape.ok) {
|
|
28477
|
+
const error = new Error(savedShape.error);
|
|
28478
|
+
error.code = savedShape.code || 'role_definition_invalid';
|
|
28479
|
+
error.field = savedShape.field || null;
|
|
28480
|
+
throw error;
|
|
28481
|
+
}
|
|
28482
|
+
_roleStore.defineOwn(roles, role, savedRole);
|
|
28483
|
+
});
|
|
28484
|
+
} catch (error) { _storeFailure(error); return; }
|
|
28485
|
+
// 비활성 provider 경고 — validation과 같은 사용자 provider 정의를 사용한다.
|
|
27691
28486
|
let warnMsg = '';
|
|
27692
|
-
try {
|
|
27693
|
-
|
|
27694
|
-
|
|
28487
|
+
try {
|
|
28488
|
+
const providerDef = allProviders.find(a => a.id === provider);
|
|
28489
|
+
const st = providerDef ? _checkAgent(providerDef) : { status: 'unknown-provider' };
|
|
28490
|
+
if (st.status !== 'ready') warnMsg = `⚠ ${provider} 현재 비활성(${st.status})${providerDef && providerDef.envFlag ? ` — 활성화: ${providerDef.envFlag}=1 + CLI 설치` : ''}`;
|
|
28491
|
+
} catch {}
|
|
28492
|
+
if (json) { log(JSON.stringify({ set: role, provider, model: model || null, modelFamily: modelFamily || null, persona, fallbackPolicy: savedRole.fallbackPolicy, candidates: savedRole.candidates, file: f, warning: warnMsg || null }, null, 2)); return; }
|
|
28493
|
+
ok(`역할 설정: ${role} (${_roleCatalogDef(role) ? _roleCatalogDef(role).ko : 'custom'}) → ${provider}${model ? ' / ' + model : ''}${modelFamily ? ` [family=${modelFamily}]` : ''}`);
|
|
28494
|
+
log(` policy: ${savedRole.fallbackPolicy} · candidates: ${savedRole.candidates.length}`);
|
|
28495
|
+
savedRole.candidates.forEach((c, i) => log(` ${i + 1}. ${c.provider}${c.model ? ' / ' + c.model : ''}${c.modelFamily ? ` [family=${c.modelFamily}]` : ''}`));
|
|
27695
28496
|
if (persona) log(` persona: ${persona}`);
|
|
27696
28497
|
if (warnMsg) warn(warnMsg);
|
|
27697
28498
|
log(` 파일: ${f} · 사용: leerness agents dispatch "<task>" --role ${role}`);
|
|
@@ -27705,10 +28506,13 @@ function rolesCmd(root, sub, ...args) {
|
|
|
27705
28506
|
// 사유는 바깥의 `_unsetErr` 로 전달한다(락 안에서 출력하지 않는다).
|
|
27706
28507
|
if (_requireExistingPath()) return;
|
|
27707
28508
|
let _unsetErr = null;
|
|
27708
|
-
|
|
27709
|
-
|
|
27710
|
-
|
|
27711
|
-
|
|
28509
|
+
let f;
|
|
28510
|
+
try {
|
|
28511
|
+
f = _updateRoles(root, (roles) => {
|
|
28512
|
+
if (!_roleStore.hasOwn(roles, role)) { _unsetErr = `설정되지 않은 역할: ${role}`; return false; }
|
|
28513
|
+
delete roles[role];
|
|
28514
|
+
});
|
|
28515
|
+
} catch (error) { _storeFailure(error); return; }
|
|
27712
28516
|
if (_unsetErr) return fail(_unsetErr);
|
|
27713
28517
|
if (json) { log(JSON.stringify({ removed: role, file: f }, null, 2)); return; }
|
|
27714
28518
|
ok(`역할 제거: ${role}`);
|
|
@@ -27716,28 +28520,58 @@ function rolesCmd(root, sub, ...args) {
|
|
|
27716
28520
|
}
|
|
27717
28521
|
|
|
27718
28522
|
if (sub === 'suggest') {
|
|
27719
|
-
const
|
|
28523
|
+
const preflight = _guardRoleStore(json, () => _loadRoles(root));
|
|
28524
|
+
if (!preflight.ok) return;
|
|
28525
|
+
try { _loadEnvFile(root); _loadEnvFile(path.join(root, '..')); } catch {}
|
|
28526
|
+
const suggestionResult = _suggestRoles(root);
|
|
28527
|
+
if (suggestionResult.error) { failJson(json, suggestionResult.error.code, suggestionResult.error.message); return; }
|
|
28528
|
+
const { ready, suggestions } = suggestionResult;
|
|
27720
28529
|
if (has('--apply')) {
|
|
27721
28530
|
if (_requireExistingPath()) return;
|
|
27722
28531
|
let applied = 0;
|
|
27723
|
-
|
|
27724
|
-
|
|
27725
|
-
|
|
27726
|
-
|
|
27727
|
-
|
|
28532
|
+
let f;
|
|
28533
|
+
try {
|
|
28534
|
+
f = _updateRoles(root, (roles) => {
|
|
28535
|
+
applied = 0;
|
|
28536
|
+
for (const s of suggestions) {
|
|
28537
|
+
if (!s.ready) continue;
|
|
28538
|
+
const previous = _roleStore.hasOwn(roles, s.role) && roles[s.role] && typeof roles[s.role] === 'object' ? roles[s.role] : {};
|
|
28539
|
+
const candidates = Array.isArray(previous.candidates) && previous.candidates.length ? previous.candidates : s.candidates;
|
|
28540
|
+
_roleStore.defineOwn(roles, s.role, {
|
|
28541
|
+
...previous,
|
|
28542
|
+
provider: s.provider,
|
|
28543
|
+
model: null,
|
|
28544
|
+
modelFamily: null,
|
|
28545
|
+
persona: ROLE_CATALOG[s.role].desc,
|
|
28546
|
+
primary: {
|
|
28547
|
+
...(previous.primary && typeof previous.primary === 'object' && !Array.isArray(previous.primary)
|
|
28548
|
+
? previous.primary : {}),
|
|
28549
|
+
provider: s.provider,
|
|
28550
|
+
model: null,
|
|
28551
|
+
modelFamily: null,
|
|
28552
|
+
},
|
|
28553
|
+
candidates,
|
|
28554
|
+
fallbackPolicy: previous.fallbackPolicy || 'balanced',
|
|
28555
|
+
requirements: previous.requirements || ROLE_REQUIREMENTS[s.role] || {},
|
|
28556
|
+
});
|
|
28557
|
+
applied++;
|
|
28558
|
+
}
|
|
28559
|
+
});
|
|
28560
|
+
} catch (error) { _storeFailure(error); return; }
|
|
28561
|
+
if (json) { log(JSON.stringify({ applied, ready, file: f, suggestions, availabilityObservationState: suggestionResult.availabilityObservationState }, null, 2)); return; }
|
|
27728
28562
|
ok(`제안 적용: ${applied}개 역할 → ${f}`);
|
|
27729
|
-
for (const s of suggestions.filter(x => x.ready)) log(` ${s.role.padEnd(11)} → ${s.provider} /
|
|
28563
|
+
for (const s of suggestions.filter(x => x.ready)) log(` ${s.role.padEnd(11)} → ${s.provider} / (provider default; model unverified)`);
|
|
27730
28564
|
const unready = suggestions.filter(x => !x.ready);
|
|
27731
28565
|
if (unready.length) log(` (미배정 ${unready.length}: ${unready.map(x => x.role).join(', ')} — 활성 provider 없음)`);
|
|
27732
28566
|
return;
|
|
27733
28567
|
}
|
|
27734
|
-
if (json) { log(JSON.stringify({ ready, suggestions }, null, 2)); return; }
|
|
28568
|
+
if (json) { log(JSON.stringify({ ready, suggestions, inspected: suggestionResult.inspected, availabilityObservationState: suggestionResult.availabilityObservationState }, null, 2)); return; }
|
|
27735
28569
|
log(`# leerness roles suggest (1.9.270) — 활성 에이전트 기반 최적 역할 배치`);
|
|
27736
28570
|
log(`활성(ready): ${ready.join(', ') || '(없음 — LEERNESS_ENABLE_* + CLI 설치 필요)'}`);
|
|
27737
28571
|
log('');
|
|
27738
28572
|
for (const s of suggestions) {
|
|
27739
28573
|
const mark = s.ready ? '✓' : '·';
|
|
27740
|
-
log(` ${mark} ${s.role.padEnd(11)} ${s.ko.padEnd(8)} → ${s.ready ? `${s.provider} /
|
|
28574
|
+
log(` ${mark} ${s.role.padEnd(11)} ${s.ko.padEnd(8)} → ${s.ready ? `${s.provider} / (provider default; model unverified)` : '(미배정 — 선호 ' + s.prefer.join('>') + ' 비활성)'}`);
|
|
27741
28575
|
log(` 근거: ${s.why}`);
|
|
27742
28576
|
}
|
|
27743
28577
|
log(`\n 적용: leerness roles suggest --apply · 개별: leerness roles set <role> --provider <id>`);
|
|
@@ -27745,39 +28579,111 @@ function rolesCmd(root, sub, ...args) {
|
|
|
27745
28579
|
}
|
|
27746
28580
|
|
|
27747
28581
|
if (sub === 'verify') {
|
|
27748
|
-
const
|
|
28582
|
+
const loaded = _guardRoleStore(json, () => _loadRoleState(root));
|
|
28583
|
+
if (!loaded.ok) return;
|
|
28584
|
+
try { _loadEnvFile(root); _loadEnvFile(path.join(root, '..')); } catch {}
|
|
28585
|
+
const roleState = loaded.value;
|
|
28586
|
+
const roles = roleState.roles;
|
|
27749
28587
|
const entries = Object.entries(roles);
|
|
27750
|
-
const
|
|
27751
|
-
|
|
27752
|
-
|
|
27753
|
-
return
|
|
28588
|
+
const availabilityState = readAvailabilityObservations(root, 2000);
|
|
28589
|
+
if (availabilityState.ok === false || availabilityState.partial) {
|
|
28590
|
+
failJson(json, availabilityState.code || 'availability_history_partial', availabilityState.error || 'availability observation history is incomplete');
|
|
28591
|
+
return;
|
|
28592
|
+
}
|
|
28593
|
+
const allProviders = _allProviders(root);
|
|
28594
|
+
const providerMap = new Map(allProviders.map(p => [p.id, p]));
|
|
28595
|
+
const inspect = (candidate) => {
|
|
28596
|
+
const def = candidate && providerMap.get(candidate.provider);
|
|
28597
|
+
let raw = { status: def ? 'unknown' : 'unknown-provider' };
|
|
28598
|
+
try { if (def) raw = _checkAgent(def); } catch (error) { raw = { status: 'check-error', error: error && error.message }; }
|
|
28599
|
+
const availability = normalizeAvailability(def || null, raw, {
|
|
28600
|
+
provider: candidate && candidate.provider,
|
|
28601
|
+
...availabilityExtrasForCandidate(availabilityState, candidate || {}),
|
|
28602
|
+
});
|
|
28603
|
+
return {
|
|
28604
|
+
provider: candidate && candidate.provider || null,
|
|
28605
|
+
model: candidate && candidate.model || null,
|
|
28606
|
+
modelFamily: candidate && candidate.modelFamily || inferModelFamily(candidate && candidate.model),
|
|
28607
|
+
status: availability.status,
|
|
28608
|
+
ready: availability.eligible,
|
|
28609
|
+
availability,
|
|
28610
|
+
};
|
|
28611
|
+
};
|
|
28612
|
+
const results = entries.map(([role]) => {
|
|
28613
|
+
const resolved = _resolveRoleFromState(roleState, role);
|
|
28614
|
+
const primary = inspect(resolved && resolved.primary);
|
|
28615
|
+
const candidateResults = resolved ? resolved.candidates.map(inspect) : [];
|
|
28616
|
+
const fallbackReady = !!(resolved && resolved.fallbackPolicy !== 'strict' && candidateResults.some(c => c.ready));
|
|
28617
|
+
const routable = primary.ready || fallbackReady;
|
|
28618
|
+
return {
|
|
28619
|
+
role,
|
|
28620
|
+
provider: primary.provider,
|
|
28621
|
+
model: primary.model,
|
|
28622
|
+
modelFamily: resolved && resolved.modelFamily || null,
|
|
28623
|
+
fallbackPolicy: resolved ? resolved.fallbackPolicy : null,
|
|
28624
|
+
status: primary.status,
|
|
28625
|
+
primaryReady: primary.ready,
|
|
28626
|
+
fallbackReady,
|
|
28627
|
+
routable,
|
|
28628
|
+
availability: primary.availability,
|
|
28629
|
+
candidates: candidateResults,
|
|
28630
|
+
};
|
|
27754
28631
|
});
|
|
27755
|
-
const bad = results.filter(r => !r.
|
|
27756
|
-
|
|
27757
|
-
|
|
28632
|
+
const bad = results.filter(r => !r.routable);
|
|
28633
|
+
const degraded = results.filter(r => !r.primaryReady && r.routable);
|
|
28634
|
+
if (json) {
|
|
28635
|
+
log(JSON.stringify({
|
|
28636
|
+
total: entries.length,
|
|
28637
|
+
primaryReady: results.filter(r => r.primaryReady).length,
|
|
28638
|
+
routable: results.filter(r => r.routable).length,
|
|
28639
|
+
degraded: degraded.length,
|
|
28640
|
+
issues: bad,
|
|
28641
|
+
availabilityObservationState: { activeCount: availabilityState.activeCount, partial: availabilityState.partial },
|
|
28642
|
+
results,
|
|
28643
|
+
}, null, 2));
|
|
28644
|
+
if (bad.length) process.exitCode = 1;
|
|
28645
|
+
return;
|
|
28646
|
+
}
|
|
28647
|
+
log(`# leerness roles verify (1.9.270/T-0172)`);
|
|
27758
28648
|
if (!entries.length) { log(' (설정된 역할 없음 — leerness roles suggest --apply 또는 roles set)'); return; }
|
|
27759
|
-
for (const r of results)
|
|
27760
|
-
|
|
27761
|
-
|
|
28649
|
+
for (const r of results) {
|
|
28650
|
+
const mark = r.primaryReady ? '🟢' : r.routable ? '🟡' : '🔴';
|
|
28651
|
+
const state = r.primaryReady ? 'primary ready' : r.routable ? 'fallback ready' : 'unroutable';
|
|
28652
|
+
log(` ${mark} ${r.role.padEnd(11)} → ${r.provider}${r.model ? ' / ' + r.model : ''} [${r.status}; ${state}; policy=${r.fallbackPolicy}]`);
|
|
28653
|
+
r.candidates.forEach((c, i) => log(` ${i + 1}. ${c.ready ? '✓' : '×'} ${c.provider}${c.model ? ' / ' + c.model : ''} [${c.status}]`));
|
|
28654
|
+
}
|
|
28655
|
+
if (bad.length) { log(`\n ⚠ ${bad.length}개 역할은 primary와 허용된 fallback 모두 사용할 수 없습니다`); process.exitCode = 1; }
|
|
28656
|
+
else if (degraded.length) log(`\n ⚠ ${degraded.length}개 역할은 primary가 비활성이지만 fallback으로 라우팅 가능합니다`);
|
|
28657
|
+
else log(`\n ✓ 모든 역할의 primary가 활성 provider에 배정됨`);
|
|
27762
28658
|
return;
|
|
27763
28659
|
}
|
|
27764
28660
|
|
|
27765
28661
|
// default: list
|
|
27766
|
-
const
|
|
28662
|
+
const loaded = _guardRoleStore(json, () => _loadRoleState(root));
|
|
28663
|
+
if (!loaded.ok) return;
|
|
28664
|
+
const roleState = loaded.value;
|
|
28665
|
+
const roles = roleState.roles;
|
|
27767
28666
|
const entries = Object.entries(roles);
|
|
27768
|
-
if (json) {
|
|
28667
|
+
if (json) {
|
|
28668
|
+
const configurations = entries.map(([role]) => _resolveRoleFromState(roleState, role)).filter(Boolean);
|
|
28669
|
+
log(JSON.stringify({ roles, configurations, count: entries.length }, null, 2)); return;
|
|
28670
|
+
}
|
|
27769
28671
|
log(`# leerness roles (1.9.270) — 설정된 역할 ${entries.length}개`);
|
|
27770
28672
|
if (!entries.length) {
|
|
27771
28673
|
log(` (없음) — leerness roles suggest 로 활성 에이전트 기반 제안 확인 → roles suggest --apply`);
|
|
27772
|
-
log(` 또는: leerness roles set coder --provider codex --model
|
|
28674
|
+
log(` 또는: leerness roles set coder --provider codex [--model <verified-model-id>]`);
|
|
27773
28675
|
return;
|
|
27774
28676
|
}
|
|
27775
28677
|
for (const [role, def] of entries) {
|
|
27776
|
-
const ko =
|
|
27777
|
-
|
|
28678
|
+
const ko = _roleCatalogDef(role) ? _roleCatalogDef(role).ko : 'custom';
|
|
28679
|
+
const resolved = _resolveRoleFromState(roleState, role);
|
|
28680
|
+
const provider = resolved ? resolved.provider : '(invalid)';
|
|
28681
|
+
const model = resolved ? resolved.model : null;
|
|
28682
|
+
log(` ${role.padEnd(11)} ${ko.padEnd(8)} → ${provider}${model ? ' / ' + model : ''}${resolved && resolved.modelFamily ? ` [family=${resolved.modelFamily}]` : ''}${resolved ? ` [policy=${resolved.fallbackPolicy} · candidates=${resolved.candidates.length}]` : ''}`);
|
|
28683
|
+
if (resolved) resolved.candidates.forEach((c, i) => log(` ${i + 1}. ${c.provider}${c.model ? ' / ' + c.model : ''}${c.modelFamily ? ` [family=${c.modelFamily}]` : ''}`));
|
|
27778
28684
|
if (def.persona) log(` ${def.persona}`);
|
|
27779
28685
|
}
|
|
27780
|
-
log(`\n 사용: leerness agents dispatch "<task>" --role <role> · 검증: leerness roles verify`);
|
|
28686
|
+
log(`\n 사용: leerness agents dispatch "<task>" --role <role> · 저장소 검증: leerness roles validate · provider 확인: leerness roles verify`);
|
|
27781
28687
|
}
|
|
27782
28688
|
|
|
27783
28689
|
// ===== 1.9.272: Capability / Security Surface 공개 (GPT-5.5 외부 리뷰 반영) =====
|
|
@@ -30915,6 +31821,7 @@ SECURITY & HYGIENE
|
|
|
30915
31821
|
HANDOFF & SESSION
|
|
30916
31822
|
handoff [path] [--compact] [--all-apps] [--json] [--writeback] Session context; tracked writeback is explicit
|
|
30917
31823
|
session close [path] Closing report + auto handoff
|
|
31824
|
+
lease acquire|release|list|check [file|lease-id] [--session KEY] [--ttl SEC] Exact-file opt-in coordination lease
|
|
30918
31825
|
context [path] [--json] Agent onboarding context
|
|
30919
31826
|
retro | insights [path] [--json] [--limit N|--all] Retrospective / cumulative stats (JSON rows: newest 30 by default; rowsTotal = lifetime count, not the --days window)
|
|
30920
31827
|
|
|
@@ -30964,8 +31871,9 @@ Docs: https://www.npmjs.com/package/leerness | https://leerness.pages.dev
|
|
|
30964
31871
|
function help() {
|
|
30965
31872
|
// 1.23.1 (UR-0010 Phase 6): 영어 opt-in 시 큐레이트 영어판. 기본(ko) 은 아래 한국어 help 그대로.
|
|
30966
31873
|
if (_uiLang(arg('--path', process.cwd())) === 'en') { _helpEn(); return; }
|
|
30967
|
-
log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness update [path] [--check|--yes|--force|--from <tarball>]\n leerness auto-update install [path]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness audit [path]\n leerness check [path]\n leerness scan secrets [path]\n leerness encoding check [path]\n leerness lazy detect [path]\n leerness memory search "query" [--limit 5]\n leerness handoff [path] [--all-apps] [--include p1,p2] [--since 24h|3d] [--compact] [--json] # 1.9.17-22 워크스페이스 (--compact: LLM 시스템 프롬프트용 1줄 요약)\n leerness orchestrate "<목표>" [--agents N] [--model qwen2.5:7b-instruct] [--retry-on-fail K] # 1.9.22 Ollama opt-in (LEERNESS_OLLAMA_BASE_URL 필요)\n leerness llm-bench record --score N --model X [--label L] [--tokens T] # 1.9.22 LLM 벤치 히스토리 누적\n leerness deps <capability> [--run-tests] [--json] # 1.9.24 depends-on 역방향 추적 + 자동 회귀 sweep\n leerness memory search "키" [--include-code] # 1.9.25 소스 코드 본문도 검색 (모순 감지 핵심)\n leerness brainstorm "주제" [--include-code] # 1.9.25 코드 본문 hits 포함\n leerness register-pending "<요청>" [--agent X] [--note Y] # 1.9.25 다중 세션 in-progress 즉시 등록\n leerness optimism-check <T-ID> [--json] # 1.9.26/27 낙관적 표시 감지 (1.9.27: 10 카테고리 + URL/메서드 매핑 + 신뢰도 점수)\n leerness persona list|show <id>|add <id> # 1.9.29 페르소나 카탈로그 (보안/성능/UX/testing/docs 5종 내장)\n leerness review <file> --persona <id1,id2,...> # 1.9.29 도메인 페르소나 리뷰 프롬프트 자동 생성\n leerness agents list|check|quota #
|
|
31874
|
+
log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness update [path] [--check|--yes|--force|--from <tarball>]\n leerness auto-update install [path]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness audit [path]\n leerness check [path]\n leerness scan secrets [path]\n leerness encoding check [path]\n leerness lazy detect [path]\n leerness memory search "query" [--limit 5]\n leerness handoff [path] [--all-apps] [--include p1,p2] [--since 24h|3d] [--compact] [--json] # 1.9.17-22 워크스페이스 (--compact: LLM 시스템 프롬프트용 1줄 요약)\n leerness orchestrate "<목표>" [--agents N] [--model qwen2.5:7b-instruct] [--retry-on-fail K] # 1.9.22 Ollama opt-in (LEERNESS_OLLAMA_BASE_URL 필요)\n leerness llm-bench record --score N --model X [--label L] [--tokens T] # 1.9.22 LLM 벤치 히스토리 누적\n leerness deps <capability> [--run-tests] [--json] # 1.9.24 depends-on 역방향 추적 + 자동 회귀 sweep\n leerness memory search "키" [--include-code] # 1.9.25 소스 코드 본문도 검색 (모순 감지 핵심)\n leerness brainstorm "주제" [--include-code] # 1.9.25 코드 본문 hits 포함\n leerness register-pending "<요청>" [--agent X] [--note Y] # 1.9.25 다중 세션 in-progress 즉시 등록\n leerness optimism-check <T-ID> [--json] # 1.9.26/27 낙관적 표시 감지 (1.9.27: 10 카테고리 + URL/메서드 매핑 + 신뢰도 점수)\n leerness persona list|show <id>|add <id> # 1.9.29 페르소나 카탈로그 (보안/성능/UX/testing/docs 5종 내장)\n leerness review <file> --persona <id1,id2,...> # 1.9.29 도메인 페르소나 리뷰 프롬프트 자동 생성\n leerness agents list|check|quota # 외부 AI CLI 다축 가용성 확인\n leerness agents resolve \"<task>\" --role <role> [--preset strict|balanced|continuity] # 역할 유지 + 대체 선택지\n leerness agents fallback provider|session|direct|hold \"<task>\" --role <role> [--session-provider P --session-model M --session-model-family F] # 명시 선택·기록\n leerness agents record completed|failed|reviewed|validated \"<summary>\" [--task T-ID --role R --to P --model M --model-family F --agent P --agent-model M --agent-model-family F --evidence E]\n leerness agents history [--limit 20] # 실행자·검수자·결과 provenance\n leerness agents dispatch \"<task>\" --to <id> # 활성 CLI 대상 실행 명령 생성 (실 호출 X, 사용자 실행)\n leerness agents multi "<task>" [--only c1,c2] [--write] [--execute] [--timeout 60] # 1.9.152/156 활성 N개 일괄 dispatch (--execute: 실 spawn + consensus)\n leerness provider list|add|remove [args] # 1.9.157 Provider Registry — 사용자 정의 CLI provider 동적 추가 (OpenRouter/Bedrock 흡수)\n leerness agents dispatch "<task>" --multi # 1.9.152 multi 모드 alias (또는 --to all)\n leerness setup-agents [path] [--yes|--no-setup-agents] # 1.9.32 sub-agent CLI 인터랙티브 설정 (.env + 미설치 자동 설치)\n leerness init [path] [--no-stale-check] # 1.9.33 npx 캐시 함정 — 옛 버전 자동 경고 (끄려면 --no-stale-check)\n leerness which [--json] # 1.9.164 진단: 현재 실행 경로/버전 + npm 캐시 + PATH 후보 (구버전 충돌 해결)\n leerness selftest [--json] # 1.9.258 코어 함수 무결성 자가 검증 (설치 손상/부분설치 감지, CI 친화 exit 1)\n leerness shell-guard "<command>" [--json] # 1.9.260 터미널 명령 셸 호환성 린터 (PowerShell 5.1 && 미지원 등 실행 전 감지, UR-0020)\n leerness shell-guard --record --cmd "..." --exit N # 1.9.260 실패한 터미널 명령 기록 → 다음 분석 시 회수\n leerness path-setup [--apply] [--json] # 1.9.254 leerness CLI PATH 자동 등록 (npm global bin 미등록 시)\n leerness web check|screenshot|extract <url> [--out file.png] [--selector "css"] # 1.9.165 playwright bridge (opt-in: npm i -g playwright + permissions.browser)\n leerness pc check|click|type|screenshot [--x N --y N] [--text "s"] [--out f.png] # 1.9.166 robotjs/nut-tree bridge (opt-in: npm i -g robotjs + permissions.mouse/keyboard, ⚠ full 모드 권장)\n leerness lsp check|symbols|references <file/name> [--in dir] [--json] # 1.9.167 LSP 어댑터 MVP (typescript opt-in + regex fallback, 코드 인텔리전스)\n leerness review-request "<request>" [--json] # 1.9.176 사용자 요청 사전 검토 (충돌/재사용/효율/권장 단계 — 사용자 명시)\n leerness contract verify <spec.md> <impl.js> [--json] # 1.9.35 명세 ↔ 구현 일치 검사 (함수/필드)\n leerness reuse autodetect [path] [--apply] [--json] # 1.9.35 src/*.js의 module.exports → reuse-map 후보 등록\n leerness audit [path] [--fix] # 1.9.35 --fix: session-handoff/current-state 자동 갱신\n leerness verify-claim <T-ID> ... [--strict-claims] # 1.9.26 verify-claim에 낙관적 표시 자동 검사 통합
|
|
30968
31875
|
leerness lens [${_lensDomainList()}] [--json] # 1.18.3/1.36.97 분야별 자기질문 품질 렌즈 (database·contract·recovery·observability 심화 · axes 8축 경량 — 완료 선언 전 자가 점검)\n leerness library [show|page] [path] [--json] [--ai] # 1.36.98 재사용 인벤토리 — 컴포넌트·디자인 토큰 추출 (page: 오프라인 HTML · --ai: 에이전트용 압축)\n leerness reuse-map [path] [--all-apps] [--include p1,p2] [--strict-elements] [--json] # 1.9.18 중복/잠재중복/depends-on\n leerness verify-claim <T-ID> [--path .] [--run-tests] [--json] # 1.9.18-20 evidence 자동 검증 (1.9.20: scenes/scripts 등 도메인 폴더 + jest/mocha 파싱)\n leerness verify-code [path] [--build] [--bench] # 1.9.20 --bench: scripts.bench 추가 실행 + evidence 누적\n leerness session close [path]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop|fix-evidence|relink [args]\n leerness skill list|info <name>\n leerness skill learn <id> --doc <url> --command "..." --capability "..." [--note ...]\n leerness skill use <id> [--note ...]\n leerness skill optimize <id> --before "..." --after "..." [--note ...]\n leerness skill remove <id>\n leerness skill consolidate [--threshold 0.3]\n leerness gate [path] # verify+audit+scan+encoding+lazy
|
|
31876
|
+
leerness lease acquire|release|list|check [file|lease-id] [--session KEY] [--ttl SEC] [--json] # exact-file opt-in 협업 lease
|
|
30969
31877
|
leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
|
|
30970
31878
|
leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
|
|
30971
31879
|
leerness brainstorm "<주제>" [--all-apps] [--include p1,p2] [--json] # 브레인스토밍 (1.9.13~1.9.16)
|
|
@@ -30999,7 +31907,7 @@ async function main() {
|
|
|
30999
31907
|
}
|
|
31000
31908
|
if (has('--help') || has('-h')) return help();
|
|
31001
31909
|
// 입력 오류는 자동 legacy migration·usage 기록보다 먼저 거부해야 실패 호출이 상태를 바꾸지 않는다.
|
|
31002
|
-
if (!_rejectUnregisteredFlags()) return;
|
|
31910
|
+
if (!_rejectUnregisteredFlags(cmd)) return;
|
|
31003
31911
|
if (!_rejectMalformedFlagForms(cmd, args)) return;
|
|
31004
31912
|
if (!_validateCommandFlags(cmd, args)) return;
|
|
31005
31913
|
// 값이 존재하지만 의미가 잘못된 --keep 도 legacy migration/write 전에 거부한다.
|
|
@@ -31595,7 +32503,7 @@ async function main() {
|
|
|
31595
32503
|
return v;
|
|
31596
32504
|
};
|
|
31597
32505
|
return require('../lib/routing').routeCmd(arg('--path', process.cwd()), _rtToks.join(' '),
|
|
31598
|
-
{ has, arg, _withLock, _resolveRole, _providerAuth, _toggleOn: (r, id) => _tgl.toggleOn(r, id) });
|
|
32506
|
+
{ has, arg, _withLock, _resolveRole, _resolveRoles, _providerAuth, _toggleOn: (r, id) => _tgl.toggleOn(r, id) });
|
|
31599
32507
|
}
|
|
31600
32508
|
if (cmd === 'agents') return agentsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
31601
32509
|
// 1.9.157: Provider Registry — 사용자 정의 provider 동적 추가
|
|
@@ -31819,6 +32727,7 @@ async function main() {
|
|
|
31819
32727
|
}
|
|
31820
32728
|
// 1.36.51 (사용자 요청 UR-0061): 모호성 질문 + 미리보기 승인 워크플로
|
|
31821
32729
|
if (cmd === 'clarify') return _clar.clarifyCmd(arg('--path', process.cwd()), args.slice(1).filter(x => !x.startsWith('-')).join(' '), { has });
|
|
32730
|
+
if (cmd === 'lease') return fileLeaseCmd(arg('--path', process.cwd()), args[1], args[2], args.slice(3));
|
|
31822
32731
|
if (cmd === 'preview') {
|
|
31823
32732
|
// 전역 2-pass 파서가 command별 arity까지 반영한 positional 단일출처다.
|
|
31824
32733
|
// preview 전용 raw 재파싱은 --keep 을 다시 value로 오인해 뒤의 P-ID를 삼켰다(T-0144 교차검토).
|
|
@@ -32247,7 +33156,7 @@ module.exports = {
|
|
|
32247
33156
|
// (실제 CLI 설치 여부에 의존하는 단언은 그 CLI 가 없는 머신에서 공허해진다).
|
|
32248
33157
|
_checkAgent,
|
|
32249
33158
|
// 1.9.270: agent roles — 모델별 역할 부여 (사용자 명시) — 단위 테스트
|
|
32250
|
-
ROLE_CATALOG, _normalizeRole, _pickModel, _rolesFile, _loadRoles, _saveRoles, _resolveRole, _suggestRoles, rolesCmd, _dispatchCommand,
|
|
33159
|
+
ROLE_CATALOG, _normalizeRole, _pickModel, _rolesFile, _loadRoleState, _loadRoles, _loadRolesChecked, _saveRoles, _resolveRoleFromState, _resolveRoles, _resolveRole, _suggestRoles, rolesCmd, _dispatchCommand,
|
|
32251
33160
|
// 1.9.272: 권한/보안 표면 공개 (GPT-5.5 리뷰) — 단위 테스트
|
|
32252
33161
|
CAPABILITY_SURFACE, POWERFUL_COMMANDS, capabilitiesCmd,
|
|
32253
33162
|
// 1.9.275: 릴리스 채널 (npm dist-tag, GPT-5.5 리뷰) — 단위 테스트
|