leerness 1.36.165 → 1.36.166
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.md +4 -4
- package/bin/leerness.js +191 -53
- package/package.json +2 -2
- package/scripts/command-flags-probe.js +324 -0
- package/scripts/e2e-concurrency.js +2 -2
- package/scripts/e2e.js +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.166 — 2026-08-26
|
|
4
|
+
|
|
5
|
+
- T-0144: unknown/foreign/malformed CLI 플래그를 migration과 쓰기 전에 exit 1로 거부하고 commands 카탈로그의 verify-code·contract verify 누락을 보완했습니다. 명령별 strict route와 값 누락·부울 equals·language 선택값 회귀 probe를 npm test에 추가했습니다.
|
|
6
|
+
|
|
3
7
|
## 1.36.165 — 2026-08-26
|
|
4
8
|
|
|
5
9
|
- verify-code가 fresh basic 권한에서도 Python/Go/Rust 러너를 제한적으로 실행하고, 혼합 Node/Python 테스트와 npm placeholder를 올바르게 판별하며, 실패·차단된 최신 증거가 lazy/gate를 거짓 통과하지 않도록 수정 (T-0143)
|
package/README.md
CHANGED
|
@@ -128,7 +128,7 @@ MIT
|
|
|
128
128
|
<!-- leerness:project-readme:start -->
|
|
129
129
|
## Leerness Project Harness
|
|
130
130
|
|
|
131
|
-
이 프로젝트는 Leerness v1.36.
|
|
131
|
+
이 프로젝트는 Leerness v1.36.166 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
132
132
|
|
|
133
133
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
134
134
|
|
|
@@ -182,7 +182,7 @@ leerness memory restore decision <date|title>
|
|
|
182
182
|
|
|
183
183
|
### MCP server (외부 AI 통합)
|
|
184
184
|
|
|
185
|
-
Leerness v1.36.
|
|
185
|
+
Leerness v1.36.166는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **89개 도구**를 노출:
|
|
186
186
|
|
|
187
187
|
```jsonc
|
|
188
188
|
// 카테고리별
|
|
@@ -203,7 +203,7 @@ Leerness v1.36.165는 stdio JSON-RPC MCP server를 내장합니다 — Claude Co
|
|
|
203
203
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
204
204
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
205
205
|
|
|
206
|
-
현재 누적: **v1.9.x → 1.36.
|
|
206
|
+
현재 누적: **v1.9.x → 1.36.166 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
207
207
|
|
|
208
208
|
### 성능 가이드
|
|
209
209
|
|
|
@@ -241,5 +241,5 @@ leerness release pack --close --auto-main-push
|
|
|
241
241
|
- `.leerness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
242
242
|
- `.leerness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
243
243
|
|
|
244
|
-
Last synced by Leerness v1.36.
|
|
244
|
+
Last synced by Leerness v1.36.166: 2026-08-26
|
|
245
245
|
<!-- leerness:project-readme:end -->
|
package/bin/leerness.js
CHANGED
|
@@ -47,7 +47,7 @@ const {
|
|
|
47
47
|
migrateLegacyWorkspace,
|
|
48
48
|
} = require('../lib/workspace-dir');
|
|
49
49
|
|
|
50
|
-
const VERSION = '1.36.
|
|
50
|
+
const VERSION = '1.36.166';
|
|
51
51
|
|
|
52
52
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
53
53
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -711,17 +711,57 @@ function _optSub(a) {
|
|
|
711
711
|
if (/^([A-Za-z]:[\\/]|\/|\.\.?[\\/])/.test(v)) return undefined; // 선행 구분자 path-like → 하위명령 아님
|
|
712
712
|
return v;
|
|
713
713
|
}
|
|
714
|
-
|
|
714
|
+
// 같은 이름이 route 에 따라 boolean/value 두 계약을 갖는 역사적 표면.
|
|
715
|
+
// update --check (boolean)
|
|
716
|
+
// referee add --check "command" (value)
|
|
717
|
+
// preview serve --keep (boolean)
|
|
718
|
+
// init/release cleanup --keep N (value)
|
|
719
|
+
// 전역 registry 하나로 arity 를 추측하면 정상 명령을 거부하거나 positional 값을 삼키므로,
|
|
720
|
+
// 명령/하위명령이 확인된 뒤 두 번째 pass 에서만 좁게 override 한다.
|
|
721
|
+
function _commandBooleanValueFlags(cmd, sub) {
|
|
722
|
+
const out = new Set();
|
|
723
|
+
if (cmd === 'update') out.add('--check');
|
|
724
|
+
if (cmd === 'preview' && sub === 'serve') out.add('--keep');
|
|
725
|
+
return out;
|
|
726
|
+
}
|
|
727
|
+
function _nonFlagArgsPass(booleanValueFlags = new Set()) {
|
|
715
728
|
const out = [];
|
|
716
729
|
const withValue = _VALUE_FLAGS;
|
|
717
730
|
const a = process.argv.slice(2);
|
|
718
731
|
for (let i = 0; i < a.length; i++) {
|
|
719
732
|
const x = a[i];
|
|
720
|
-
if (x.startsWith('-')) {
|
|
733
|
+
if (x.startsWith('-')) {
|
|
734
|
+
const name = x.split('=')[0];
|
|
735
|
+
if (withValue.has(name) && !booleanValueFlags.has(name) && !x.includes('=') && a[i+1] && !a[i+1].startsWith('-')) i++;
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
721
738
|
out.push(x);
|
|
722
739
|
}
|
|
723
740
|
return out;
|
|
724
741
|
}
|
|
742
|
+
function _commandSubHint(cmd) {
|
|
743
|
+
const raw = process.argv.slice(2);
|
|
744
|
+
const at = raw.indexOf(cmd);
|
|
745
|
+
if (at < 0) return undefined;
|
|
746
|
+
for (let i = at + 1; i < raw.length; i++) {
|
|
747
|
+
const token = raw[i];
|
|
748
|
+
if (token.startsWith('-')) {
|
|
749
|
+
const name = token.split('=')[0];
|
|
750
|
+
// preview 의 --keep 은 serve 를 알아야 boolean 임을 알 수 있고, serve 는 --keep 뒤에도 올 수 있다.
|
|
751
|
+
// 이 1개만 subcommand 탐색에서 provisional boolean 으로 보고 실제 arity 는 route 확정 후 검증한다.
|
|
752
|
+
const ambiguousBeforeSubcommand = cmd === 'preview' && name === '--keep';
|
|
753
|
+
if (!ambiguousBeforeSubcommand && _VALUE_FLAGS.has(name) && !token.includes('=') && raw[i + 1] && !raw[i + 1].startsWith('-')) i++;
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
return token;
|
|
757
|
+
}
|
|
758
|
+
return undefined;
|
|
759
|
+
}
|
|
760
|
+
function nonFlagArgs() {
|
|
761
|
+
const initial = _nonFlagArgsPass();
|
|
762
|
+
const overrides = _commandBooleanValueFlags(initial[0], _commandSubHint(initial[0]) || initial[1]);
|
|
763
|
+
return overrides.size ? _nonFlagArgsPass(overrides) : initial;
|
|
764
|
+
}
|
|
725
765
|
function argAll(name) {
|
|
726
766
|
const out = []; const a = process.argv;
|
|
727
767
|
for (let i = 0; i < a.length; i++) if (a[i] === name && a[i+1] && !a[i+1].startsWith('-')) out.push(a[++i]);
|
|
@@ -4823,7 +4863,7 @@ function _selfTestCases() {
|
|
|
4823
4863
|
{ name: 'CV-2/UR-0077: fetchNpmLatest 신형 Node win EINVAL 회피 (npm JS 진입점 + try/catch)', run: () => { if (typeof fetchNpmLatest !== 'function') return false; const src = read(__filename); const i = src.indexOf('function fetchNpmLatest'); if (i < 0) return false; const body = src.slice(i, i + 1600); return body.includes("spawnNpmSync(['view', pkg, 'version']") && /try \{/.test(body) && !/execFile\(|cmd\.exe \/d/.test(body); } },
|
|
4824
4864
|
{ name: 'CV-1/UR-0076: arg() --path=값 파싱 + _resolveRoot(--path>positional>cwd) 행위', run: () => { if (typeof _resolveRoot !== 'function') return false; const save = process.argv; try { process.argv = ['node', 'h', 'context', '--path=/tmp/eqform']; const eq = arg('--path', null) === '/tmp/eqform'; process.argv = ['node', 'h', 'context', 'X', '--path', '/tmp/flag']; const flagWins = _resolveRoot('X') === '/tmp/flag'; process.argv = ['node', 'h', 'context', '/tmp/pos']; const posWins = _resolveRoot('/tmp/pos') === '/tmp/pos'; process.argv = ['node', 'h', 'context']; const cwdFb = _resolveRoot(undefined) === process.cwd(); return eq && flagWins && posWins && cwdFb; } finally { process.argv = save; } } },
|
|
4825
4865
|
{ name: 'CV-4/UR-0079: _pruneArchives archive retention (최신 keep 유지, 오래된 prune) 행위', run: () => { if (typeof _pruneArchives !== 'function') return false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_prune_')); try { const adir = path.join(tmp, '.leerness', 'archive'); fs.mkdirSync(adir, { recursive: true }); for (let i = 0; i < 5; i++) fs.mkdirSync(path.join(adir, 'leerness-1.9.' + i + '-stamp')); const pruned = _pruneArchives(tmp, 2); const left = fs.readdirSync(adir).filter(n => /^leerness-/.test(n)).length; return pruned === 3 && left === 2; } finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } } },
|
|
4826
|
-
{ name: 'CV-7/UR-0082: commands 카탈로그 + help 에 누락 명령군 등재 (표면 drift 가드)', run: () => { const src = read(__filename); const ci = src.indexOf('function commandsCmd'); const hi = src.indexOf('function help('); if (ci < 0 || hi < 0) return false; const cbody = src.slice(ci, ci + 8000); const hbody = src.slice(hi, hi + 7000); const must = ['install-safety', 'feature add', 'creds list', 'incident list', 'webhook serve', 'deploy auto', 'runs list', 'permissions list', 'whats-new', 'migrate audit']; return must.every(c => cbody.includes(c)) && hbody.includes('install-safety') && hbody.includes('feature add'); } },
|
|
4866
|
+
{ name: 'CV-7/UR-0082: commands 카탈로그 + help 에 누락 명령군 등재 (표면 drift 가드)', run: () => { const src = read(__filename); const ci = src.indexOf('function commandsCmd'); const hi = src.indexOf('function help('); if (ci < 0 || hi < 0) return false; const cbody = src.slice(ci, ci + 8000); const hbody = src.slice(hi, hi + 7000); const must = ['verify-code', 'contract verify', 'install-safety', 'feature add', 'creds list', 'incident list', 'webhook serve', 'deploy auto', 'runs list', 'permissions list', 'whats-new', 'migrate audit']; return must.every(c => cbody.includes(c)) && hbody.includes('install-safety') && hbody.includes('feature add'); } },
|
|
4827
4867
|
{ name: 'UR-0083(4th외부평가 9.3): auto-update hook 비침투 (update --quiet 모드 + hook --check --quiet + 업그레이드)', run: () => { const src = read(__filename); const quietMode = /const quiet = !!opts\.quiet \|\| has\('--quiet'\)/.test(src); const hookQuiet = src.includes("command: 'leerness update --check --quiet'"); const upgrade = /includes\('leerness update --check'\) && !h\.command\.includes\('--quiet'\)/.test(src); return quietMode && hookQuiet && upgrade; } },
|
|
4828
4868
|
{ name: 'CV-6/UR-0081: 시크릿 스캐너 FP/FN — _isPlaceholderSecret + _looksSecretLike 행위', run: () => { if (typeof _isPlaceholderSecret !== 'function' || typeof _looksSecretLike !== 'function') return false; const fp = _isPlaceholderSecret('change-me') && _isPlaceholderSecret('your-api-key-here') && _isPlaceholderSecret('<token>') && _isPlaceholderSecret('') && !_isPlaceholderSecret('hunter2realpass'); const fn = _looksSecretLike('secret123') && _looksSecretLike('a'.repeat(24)) && !_looksSecretLike('processEnv') && !_looksSecretLike('reqBodyPassword'); return fp && fn; } },
|
|
4829
4869
|
{ name: 'UR-0025: _mergeLines/_mergeEnvLines 순수 코어 모듈 분리 + 행위 (1.9.367)', run: () => { if (typeof _mergeLines !== 'function' || typeof _mergeEnvLines !== 'function') return false; const m = require('../lib/pure-utils'); const moved = m._mergeLines === _mergeLines && m._mergeEnvLines === _mergeEnvLines; const ml = _mergeLines('a\n', ['a', 'b']) === 'a\nb\n'; const meKeep = _mergeEnvLines('FOO=keep\n', ['FOO=new']) === 'FOO=keep\n'; const meAdd = _mergeEnvLines('FOO=keep\n', ['BAR=add']).includes('BAR=add'); return moved && ml && meKeep && meAdd; } },
|
|
@@ -5755,7 +5795,7 @@ function _selfTestCases() {
|
|
|
5755
5795
|
} },
|
|
5756
5796
|
{ name: '범용성 P2 (UR-0048): task update unknown flag 거부 + did-you-mean(인수인계 유실 차단) (1.17.5)', run: () => {
|
|
5757
5797
|
const src = read(__filename);
|
|
5758
|
-
const wired = src.includes('function _rejectUnknownFlags(allowed, usageHint)') && src.includes("'unknown_flag'") && src.includes("_rejectUnknownFlags(['--status', '--evidence', '--next', '--note']");
|
|
5798
|
+
const wired = src.includes('function _rejectUnknownFlags(allowed, usageHint, options = {})') && src.includes("'unknown_flag'") && src.includes("_rejectUnknownFlags(['--status', '--evidence', '--next', '--note']");
|
|
5759
5799
|
// did-you-mean prefix 로직: --next-action 은 --next 를 prefix 로 가짐
|
|
5760
5800
|
const dymOk = '--next-action'.startsWith('--next');
|
|
5761
5801
|
return wired && dymOk;
|
|
@@ -10481,21 +10521,24 @@ function _p0103FlagsOk() {
|
|
|
10481
10521
|
const eqOk = pv2.includes('equals form title') && !pv2.includes('ZZEQ');
|
|
10482
10522
|
const rtEq = R(['agents', 'route', '--tier=normal', 'fix the parser']);
|
|
10483
10523
|
const eqRoute = !/비어 있|empty_task/.test((rtEq.stdout || '') + (rtEq.stderr || ''));
|
|
10484
|
-
// ④ 동적 레지스트리 플래그(brief set 은 arg('--'+f.flag) 로 만든다)에 오탐
|
|
10524
|
+
// ④ 동적 레지스트리 플래그(brief set 은 arg('--'+f.flag) 로 만든다)에 오탐 실패가 없어야 한다 — 판별 대조군 포함
|
|
10485
10525
|
const W2 = /알 수 없는 플래그|Unknown flag/;
|
|
10486
|
-
const
|
|
10487
|
-
|
|
10488
|
-
|
|
10526
|
+
const dynGood = R(['brief', 'set', '--intro', 'hello']);
|
|
10527
|
+
const dynBad = R(['brief', 'set', '--introo', 'x']);
|
|
10528
|
+
const dynOk = !W2.test((dynGood.stdout || '') + (dynGood.stderr || ''))
|
|
10529
|
+
&& dynBad.status !== 0 && W2.test((dynBad.stdout || '') + (dynBad.stderr || ''));
|
|
10530
|
+
// ⑤ 오타 fail-closed — 판별 대조군: 실재 플래그는 조용하고, 없는 플래그만 구조화 실패한다.
|
|
10489
10531
|
const good = R(['pulse', '--json']);
|
|
10490
10532
|
const typo = R(['pulse', '--json', '--nosuchflagzz']);
|
|
10491
10533
|
const quietOnReal = !/알 수 없는 플래그|Unknown flag/.test(good.stderr || '');
|
|
10492
10534
|
// 프로젝트가 OS locale 에 따라 en 으로 초기화돼도 같은 계약을 검증한다.
|
|
10493
10535
|
// 경고 정규식은 바로 위의 ko/en 공용 단일출처를 재사용해 CI locale 에 종속되지 않게 한다.
|
|
10494
|
-
|
|
10495
|
-
|
|
10496
|
-
|
|
10497
|
-
|
|
10498
|
-
|
|
10536
|
+
let typoJson = null; try { typoJson = JSON.parse((typo.stdout || '').trim()); } catch { typoJson = null; }
|
|
10537
|
+
const rejectsTypo = typo.status !== 0 && typoJson?.code === 'unknown_flag'
|
|
10538
|
+
&& String(typoJson?.error || '').includes('--nosuchflagzz');
|
|
10539
|
+
// JSON 오류는 stdout 단일 객체, stderr 무오염이다.
|
|
10540
|
+
const stderrClean = !(typo.stderr || '').trim();
|
|
10541
|
+
return titleClean && pathClean && eqOk && eqRoute && dynOk && quietOnReal && rejectsTypo && stderrClean;
|
|
10499
10542
|
} catch { return false; } finally { try { fs.rmSync(arena, { recursive: true, force: true }); } catch { /* 정리 실패는 판정과 무관 */ } }
|
|
10500
10543
|
}
|
|
10501
10544
|
// 1.36.102 (명령 표면 감사 P1 2건): 회귀 가드.
|
|
@@ -10925,6 +10968,8 @@ function commandsCmd(root) {
|
|
|
10925
10968
|
{ cmd: 'encoding check [path]', desc: '인코딩 검증', descEn: 'encoding verification' },
|
|
10926
10969
|
{ cmd: 'lazy detect [path] [--json]', desc: '게으른 작업 감지 (1.9.101)', descEn: 'detect lazy/incomplete work (1.9.101)' },
|
|
10927
10970
|
{ cmd: 'verify-claim <T-ID|--all> [--run-tests] [--test-cmd "<명령>"] [--strict-claims] [--require-evidence]', cmdEn: 'verify-claim <T-ID|--all> [--run-tests] [--test-cmd "<command>"] [--strict-claims] [--require-evidence]', desc: '주장 검증 (1.9.18~26) — --all: 모든 done 주장 일괄 검증(CI·스케일, 1.33.2) · --require-evidence: done 주장에 파일+테스트 근거 강제 (1.9.287) · --test-cmd: 비-JS 테스트 명령 (1.17.2)', descEn: 'verify a claim (1.9.18~26) — --all: verify every done claim at once (CI/scale, 1.33.2) · --require-evidence: require file + test evidence on done claims (1.9.287) · --test-cmd: non-JS test command (1.17.2)' },
|
|
10971
|
+
{ cmd: 'verify-code [path] [--build] [--bench] [--strict]', desc: '프로젝트 테스트·린트·타입검사 자동 감지 실행 + 검증 증거 기록', descEn: 'auto-detect and run project tests/lint/typecheck, then record verification evidence' },
|
|
10972
|
+
{ cmd: 'contract verify <spec.md> <impl.js> [--allow-empty] [--json]', desc: '명세의 함수·필드 선언과 구현 export 정적 대조', descEn: 'statically compare functions and fields declared in a spec with implementation exports' },
|
|
10928
10973
|
{ cmd: `lens [${_lensDomainList()}] [--json]`, desc: '분야별 자기질문 품질 렌즈 + 분야간 인과관계 (1.18.3 · database/contract/recovery/observability 는 심화, axes 는 8축 경량 점검)', descEn: 'per-domain self-question quality lenses + cross-domain causality (1.18.3 · database/contract/recovery/observability are deep dives, axes is a light 8-axis check)' },
|
|
10929
10974
|
// 1.36.101 (명령 표면 감사): 1.36.98 에서 디스패처에만 붙이고 commands/--help 에 등재하지 않아 '있는데 안 보이는' 명령이었다.
|
|
10930
10975
|
{ cmd: 'library [show|page] [path] [--json] [--ai]', desc: '재사용 인벤토리 — 컴포넌트·디자인 토큰을 저장소에서 추출. page: 오프라인 단일 HTML · --ai: 에이전트용 압축 JSON (1.36.98 P-0013)', descEn: 'reuse inventory — extracts components and design tokens from the repo. page: offline single HTML · --ai: compact JSON for agents (1.36.98 P-0013)' },
|
|
@@ -14473,16 +14518,126 @@ function taskAdd(root, text) {
|
|
|
14473
14518
|
// 1.17.5 (UR-0048, 5축 실증 P2): 모르는 옵션 조용히 무시 차단 — `task update --next-action "x"` 처럼 오타/미존재 플래그가
|
|
14474
14519
|
// "✓ task updated" 와 함께 값을 버려, 쓴 에이전트는 기록됐다고 믿고 다음 에이전트는 placeholder 를 받는(인수인계 유실) 최악의 실패 양식.
|
|
14475
14520
|
// prefix 기반 did-you-mean(--next-action → --next) + exit 1. 전역 플래그(--path/--json/--force/--lenient)는 항상 허용.
|
|
14476
|
-
|
|
14477
|
-
|
|
14478
|
-
|
|
14521
|
+
// T-0144: 명령별 검증은 options.globals 로 그 명령이 실제 지원하는 공통 플래그만 지정한다.
|
|
14522
|
+
// 기본값은 기존 task update 계약을 보존하고, exact 명령만 좁혀 과거의 묵시적 무시를 차단한다.
|
|
14523
|
+
function _rejectUnknownFlags(allowed, usageHint, options = {}) {
|
|
14524
|
+
const globals = Array.isArray(options.globals) ? options.globals : ['--path', '--json', '--force', '--lenient'];
|
|
14525
|
+
const all = new Set([...allowed, ...globals]);
|
|
14526
|
+
const seen = _argvLongFlagNames();
|
|
14479
14527
|
const unknown = [...new Set(seen.filter(f => !all.has(f)))];
|
|
14480
14528
|
if (!unknown.length) return true;
|
|
14481
14529
|
const dym = (u) => { const c = [...all].find(k => u.startsWith(k) || k.startsWith(u)); return c ? ` — 혹시 ${c}?` : ''; };
|
|
14482
|
-
|
|
14530
|
+
const detail = unknown.map(u => u + dym(u)).join(', ');
|
|
14531
|
+
const supported = [...all].join(' ');
|
|
14532
|
+
const en = _uiLang(process.cwd()) === 'en';
|
|
14533
|
+
failJson(has('--json'), 'unknown_flag', en
|
|
14534
|
+
? `Unsupported option for this command: ${detail} (supported: ${supported}${usageHint ? ' · ' + usageHint : ''}) — rejected instead of silently discarding its value`
|
|
14535
|
+
: `이 명령에서 지원하지 않는 옵션: ${detail} (지원: ${supported}${usageHint ? ' · ' + usageHint : ''}) — 값이 조용히 버려지는 것을 방지하기 위해 거부`);
|
|
14536
|
+
return false;
|
|
14537
|
+
}
|
|
14538
|
+
|
|
14539
|
+
function _argvLongFlagNames() {
|
|
14540
|
+
return [...new Set(process.argv.slice(2)
|
|
14541
|
+
.filter(a => typeof a === 'string' && a.startsWith('--') && a !== '--')
|
|
14542
|
+
.map(a => a.split('=')[0]))];
|
|
14543
|
+
}
|
|
14544
|
+
|
|
14545
|
+
// T-0144: 모든 명령에서 이름 자체가 없는 플래그는 migration/usage/write 이전에 fail-closed.
|
|
14546
|
+
function _rejectUnregisteredFlags() {
|
|
14547
|
+
const unknown = _argvLongFlagNames().filter(name => !_VALUE_FLAGS.has(name) && !_BOOL_FLAGS.has(name));
|
|
14548
|
+
if (!unknown.length) return true;
|
|
14549
|
+
const en = _uiLang(process.cwd()) === 'en';
|
|
14550
|
+
const human = en
|
|
14551
|
+
? `Unknown flag: ${unknown.join(' ')} — no such flag in leerness. Its value could otherwise be absorbed into text or a path. Check: leerness --help`
|
|
14552
|
+
: `알 수 없는 플래그: ${unknown.join(' ')} — leerness 에 없는 이름입니다. 값이 본문이나 경로 인자에 섞이지 않도록 거부했습니다. 확인: leerness --help`;
|
|
14553
|
+
if (has('--json')) {
|
|
14554
|
+
// JSON 본문 자체는 stdout 의 단일 문서다. 예전 경고 문구를 그대로 넣으면 소비자가 이를
|
|
14555
|
+
// "JSON 앞에 섞인 warning" 으로 오인하므로 code를 단일출처로 두고 메시지는 구조화 용어를 쓴다.
|
|
14556
|
+
failJson(true, 'unknown_flag', en
|
|
14557
|
+
? `Unregistered option: ${unknown.join(' ')} — rejected before its value could alter text or path arguments`
|
|
14558
|
+
: `등록되지 않은 옵션: ${unknown.join(' ')} — 값이 본문이나 경로 인자에 섞이기 전에 거부했습니다`);
|
|
14559
|
+
} else {
|
|
14560
|
+
// 사람용 계약은 기존 stderr 가시성을 보존하되, T-0144부터 exit 1로 fail-closed 한다.
|
|
14561
|
+
console.error('⚠ ' + human);
|
|
14562
|
+
process.exitCode = 1;
|
|
14563
|
+
}
|
|
14483
14564
|
return false;
|
|
14484
14565
|
}
|
|
14485
14566
|
|
|
14567
|
+
// 알려진 플래그도 형태가 틀리면 소비자가 기본값으로 조용히 폴백할 수 있다.
|
|
14568
|
+
// `--strict=true`는 has('--strict')가 false이고, `--language --json`은 language 값이 비어도 둘 다 성공했다.
|
|
14569
|
+
function _rejectMalformedFlagForms(cmd, args) {
|
|
14570
|
+
const raw = process.argv.slice(2);
|
|
14571
|
+
const errors = [];
|
|
14572
|
+
const valueChoices = { '--language': new Set(['auto', 'ko', 'en']) };
|
|
14573
|
+
const commandBooleans = _commandBooleanValueFlags(cmd, args && args[1]);
|
|
14574
|
+
for (let i = 0; i < raw.length; i++) {
|
|
14575
|
+
const token = raw[i];
|
|
14576
|
+
if (typeof token !== 'string' || !token.startsWith('--') || token === '--') continue;
|
|
14577
|
+
const eqAt = token.indexOf('=');
|
|
14578
|
+
const name = eqAt >= 0 ? token.slice(0, eqAt) : token;
|
|
14579
|
+
const booleanHere = _BOOL_FLAGS.has(name) || commandBooleans.has(name);
|
|
14580
|
+
if (booleanHere && eqAt >= 0) {
|
|
14581
|
+
errors.push({ kind: 'boolean_value', name, token });
|
|
14582
|
+
continue;
|
|
14583
|
+
}
|
|
14584
|
+
if (_VALUE_FLAGS.has(name) && !commandBooleans.has(name)) {
|
|
14585
|
+
const missingEqualsValue = eqAt >= 0 && token.slice(eqAt + 1) === '';
|
|
14586
|
+
const missingNextValue = eqAt < 0 && (raw[i + 1] == null || String(raw[i + 1]).startsWith('--'));
|
|
14587
|
+
if (missingEqualsValue || missingNextValue) {
|
|
14588
|
+
// gate 는 누락 referee 를 자체 check 배열(referee_missing_value)로 진단한다. 그 구조를
|
|
14589
|
+
// migration 전에 보존하는 early read-only 경로가 main 에 있으므로 여기서 평탄화하지 않는다.
|
|
14590
|
+
if (cmd === 'gate' && name === '--require-referee') continue;
|
|
14591
|
+
errors.push({ kind: 'missing_value', name, token });
|
|
14592
|
+
}
|
|
14593
|
+
else if (valueChoices[name]) {
|
|
14594
|
+
const value = eqAt >= 0 ? token.slice(eqAt + 1) : String(raw[i + 1]);
|
|
14595
|
+
if (!valueChoices[name].has(value.toLowerCase())) errors.push({ kind: 'invalid_value', name, token, value, choices: [...valueChoices[name]] });
|
|
14596
|
+
}
|
|
14597
|
+
}
|
|
14598
|
+
}
|
|
14599
|
+
if (!errors.length) return true;
|
|
14600
|
+
const onlyMissing = errors.every(e => e.kind === 'missing_value');
|
|
14601
|
+
const onlyInvalidValue = errors.every(e => e.kind === 'invalid_value');
|
|
14602
|
+
const en = _uiLang(process.cwd()) === 'en';
|
|
14603
|
+
const detail = errors.map(e => {
|
|
14604
|
+
if (e.kind === 'missing_value') {
|
|
14605
|
+
if (e.name === '--path') return en
|
|
14606
|
+
? '--path was given without a value (see command usage)'
|
|
14607
|
+
: '--path 에 값이 없습니다 (명령 사용법 확인)';
|
|
14608
|
+
return `${e.name} (${en ? 'value required; see command usage' : '값 필요; 명령 사용법 확인'})`;
|
|
14609
|
+
}
|
|
14610
|
+
if (e.kind === 'invalid_value') return `${e.name}=${e.value} (${en ? 'choose' : '허용값'}: ${e.choices.join('|')})`;
|
|
14611
|
+
return `${e.token} (${en ? `boolean flag; use ${e.name}` : `부울 플래그 — ${e.name} 형태로 사용`})`;
|
|
14612
|
+
}).join(', ');
|
|
14613
|
+
const code = onlyMissing ? 'missing_flag_value' : (onlyInvalidValue ? 'invalid_flag_value' : 'invalid_flag_syntax');
|
|
14614
|
+
failJson(has('--json'), code, en
|
|
14615
|
+
? `Invalid flag syntax: ${detail}`
|
|
14616
|
+
: `잘못된 플래그 형식: ${detail}`);
|
|
14617
|
+
return false;
|
|
14618
|
+
}
|
|
14619
|
+
|
|
14620
|
+
// 전역 registry는 "값을 갖는가"만 답한다. 아래 audit 완료 route는 현재 명령이 그 플래그를
|
|
14621
|
+
// 실제 소비하는지도 검사한다. 확인하지 않은 97개 표면을 추측 allowlist로 막지 않고 검증된 route부터 확장한다.
|
|
14622
|
+
// --path 는 기존 공용 호출 래퍼가 모든 명령에 붙이는 호환 플래그다. 명령이 별도 positional 파일을
|
|
14623
|
+
// 쓰더라도 이를 거부하면 정상 호출을 false-BLOCK 하므로 공통으로 보존한다.
|
|
14624
|
+
const _COMMON_COMMAND_FLAGS = ['--language', '--no-stale-check', '--path'];
|
|
14625
|
+
const _STRICT_COMMAND_FLAGS = {
|
|
14626
|
+
commands: { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'commands [--json] [--language en|ko] [--path <path>]' },
|
|
14627
|
+
lens: { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'lens [domain] [--json]' },
|
|
14628
|
+
about: { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'about [--json]' },
|
|
14629
|
+
identity: { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'identity [--json]' },
|
|
14630
|
+
gate: { allowed: ['--claims', '--require-referee'], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'gate [path] [--claims] [--require-referee <id>] [--json]' },
|
|
14631
|
+
'verify-code': { allowed: ['--build', '--bench', '--strict'], globals: [..._COMMON_COMMAND_FLAGS], usage: 'verify-code [path] [--build] [--bench] [--strict]' },
|
|
14632
|
+
'contract verify': { allowed: ['--allow-empty'], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'contract verify <spec.md> <impl.js> [--allow-empty] [--json]' },
|
|
14633
|
+
};
|
|
14634
|
+
function _validateCommandFlags(cmd, args) {
|
|
14635
|
+
const route = cmd === 'contract' && args[1] === 'verify' ? 'contract verify' : cmd;
|
|
14636
|
+
const cfg = _STRICT_COMMAND_FLAGS[route];
|
|
14637
|
+
if (!cfg) return true;
|
|
14638
|
+
return _rejectUnknownFlags(cfg.allowed, cfg.usage, { globals: cfg.globals });
|
|
14639
|
+
}
|
|
14640
|
+
|
|
14486
14641
|
function taskUpdate(root, id) {
|
|
14487
14642
|
if (!_requireInit(root, 'task update')) return; // 1.9.311 (UR-0047): init 가드
|
|
14488
14643
|
if (!_rejectUnknownFlags(['--status', '--evidence', '--next', '--note'], 'task update T-0001 --status done --evidence "..." --next "..."')) { process.exitCode = 1; return; } // 1.17.5 (UR-0048)
|
|
@@ -29672,35 +29827,13 @@ async function main() {
|
|
|
29672
29827
|
return log(VERSION);
|
|
29673
29828
|
}
|
|
29674
29829
|
if (has('--help') || has('-h')) return help();
|
|
29675
|
-
|
|
29676
|
-
if (!
|
|
29677
|
-
|
|
29678
|
-
|
|
29679
|
-
|
|
29680
|
-
//
|
|
29681
|
-
|
|
29682
|
-
// 그 값이 본문에 붙는다(실측: `decision add "use redis" --resaon faster` → 저장된 제목 "use redis faster").
|
|
29683
|
-
// 파싱 기본값을 뒤집으면 미분류 부울 리터럴 쪽에서 positional 을 삼키는 새 회귀가 나므로, 동작 대신
|
|
29684
|
-
// 가시성을 준다 — stderr 로만 알리고 stdout/--json/exit code 는 건드리지 않는다(기계 소비자 무영향).
|
|
29685
|
-
{
|
|
29686
|
-
const _unknownFlags = [];
|
|
29687
|
-
for (const _tok of process.argv.slice(2)) {
|
|
29688
|
-
if (typeof _tok !== 'string' || !_tok.startsWith('--') || _tok === '--') continue;
|
|
29689
|
-
const _name = _tok.split('=')[0];
|
|
29690
|
-
if (_VALUE_FLAGS.has(_name) || _BOOL_FLAGS.has(_name)) continue;
|
|
29691
|
-
if (!_unknownFlags.includes(_name)) _unknownFlags.push(_name);
|
|
29692
|
-
}
|
|
29693
|
-
if (_unknownFlags.length) {
|
|
29694
|
-
const _L = _uiLang(process.cwd());
|
|
29695
|
-
console.error(_L === 'en'
|
|
29696
|
-
? `⚠ Unknown flag: ${_unknownFlags.join(' ')} — no such flag in leerness. Its value can be absorbed into the text or path argument. Check: leerness --help`
|
|
29697
|
-
: `⚠ 알 수 없는 플래그: ${_unknownFlags.join(' ')} — leerness 에 없는 이름입니다. 그 값이 본문이나 경로 인자에 섞일 수 있습니다. 확인: leerness --help`);
|
|
29698
|
-
}
|
|
29699
|
-
}
|
|
29700
|
-
// 1.36.103 (codex 검수 #9): --keep 은 소비처가 둘인데 한쪽(release cleanup)은 거부하고 다른 쪽(아카이브 retention)은
|
|
29701
|
-
// 조용히 기본값 10 이 됐다 — `init . --keep abc --json` 이 exit 0 + {ok:true} 로 끝났다.
|
|
29702
|
-
// "준 값을 조용히 바꾸지 않는다" 를 소비처마다 반복하지 않고 진입점 한 곳에서 강제한다.
|
|
29703
|
-
{
|
|
29830
|
+
// 입력 오류는 자동 legacy migration·usage 기록보다 먼저 거부해야 실패 호출이 상태를 바꾸지 않는다.
|
|
29831
|
+
if (!_rejectUnregisteredFlags()) return;
|
|
29832
|
+
if (!_rejectMalformedFlagForms(cmd, args)) return;
|
|
29833
|
+
if (!_validateCommandFlags(cmd, args)) return;
|
|
29834
|
+
// 값이 존재하지만 의미가 잘못된 --keep 도 legacy migration/write 전에 거부한다.
|
|
29835
|
+
// preview serve 의 --keep 은 route별 boolean 계약이므로 숫자 검증 대상이 아니다.
|
|
29836
|
+
if (!_commandBooleanValueFlags(cmd, args[1]).has('--keep')) {
|
|
29704
29837
|
const _k = arg('--keep', null);
|
|
29705
29838
|
if (_k !== null) {
|
|
29706
29839
|
const _kn = Number(_k);
|
|
@@ -29713,6 +29846,14 @@ async function main() {
|
|
|
29713
29846
|
}
|
|
29714
29847
|
}
|
|
29715
29848
|
}
|
|
29849
|
+
if (cmd !== 'migrate-workspace-dir' && !_guardLinkedWorkspaceTargets(args)) return;
|
|
29850
|
+
// gate 의 누락 referee 는 기존의 진단 가능한 checks[] 계약을 유지하되, 잘못된 호출이
|
|
29851
|
+
// legacy workspace migration을 일으키기 전에 읽기전용으로 종료한다.
|
|
29852
|
+
if (cmd === 'gate' && _argAllStrict('--require-referee').missing) return gate(arg('--path', args[1] || process.cwd()));
|
|
29853
|
+
if (!_autoMigrateLegacyWorkspace(args, cmd)) return;
|
|
29854
|
+
// 1.36.144 (T-0117, 전수 실측): `--dry-run` 을 받았으면 쓰기를 막는다 — 구현한 명령은 애초에 안 쓰므로
|
|
29855
|
+
// 영향이 없고, 무시하던 명령은 첫 쓰기에서 멈춰 **아무것도 바꾸지 않은 채** 사유를 말한다.
|
|
29856
|
+
if (has('--dry-run')) { setDryRunGuard(true); try { require('../lib/git').setGitDryRun(true); } catch {} }
|
|
29716
29857
|
// 1.36.101 (명령 표면 감사): '없는 경로에 초록 보고' 클래스를 디스패처 한 곳에서 막는다.
|
|
29717
29858
|
// 실측 — 32개 조사 대상 중 16개가 존재하지 않는 디렉토리에 exit 0 과 정상 보고를 냈다
|
|
29718
29859
|
// (pulse/round-history/milestones/session-resume 는 "✓ 비정상 종료 신호 없음 · R0 · 정상 운영 중" 까지 찍었다 — 오타가 안심 보고가 된다).
|
|
@@ -30448,12 +30589,9 @@ async function main() {
|
|
|
30448
30589
|
// 1.36.51 (사용자 요청 UR-0061): 모호성 질문 + 미리보기 승인 워크플로
|
|
30449
30590
|
if (cmd === 'clarify') return _clar.clarifyCmd(arg('--path', process.cwd()), args.slice(1).filter(x => !x.startsWith('-')).join(' '), { has });
|
|
30450
30591
|
if (cmd === 'preview') {
|
|
30451
|
-
//
|
|
30452
|
-
|
|
30453
|
-
|
|
30454
|
-
if (_pvRaw[i].startsWith('--')) { if (!_pvRaw[i].includes('=') && _VALUE_FLAGS.has(_pvRaw[i]) && _pvRaw[i + 1]) i++; continue; } // 1.36.75 (검수 #2): --mockup 값이 제목에 흡수되던 것. 1.36.103: 인라인 목록 → _VALUE_FLAGS (--select 등이 제목에 붙던 것) + 등호형은 다음 토큰을 먹지 않음(codex 검수 #5 재현)
|
|
30455
|
-
_pvToks.push(_pvRaw[i]);
|
|
30456
|
-
}
|
|
30592
|
+
// 전역 2-pass 파서가 command별 arity까지 반영한 positional 단일출처다.
|
|
30593
|
+
// preview 전용 raw 재파싱은 --keep 을 다시 value로 오인해 뒤의 P-ID를 삼켰다(T-0144 교차검토).
|
|
30594
|
+
const _pvToks = args.slice(1);
|
|
30457
30595
|
// 1.36.80 (UR-0067): 라이브 미리보기 — serve(로컬 서버) / mode(self|project) 는 별도 모듈로 위임
|
|
30458
30596
|
const _pvRoot = arg('--path', process.cwd());
|
|
30459
30597
|
if (_pvToks[0] === 'serve') return _pvsrv.previewServeCmd(_pvRoot, _pvToks[1], { has, arg, previews: _clar._loadPreviews(_pvRoot) });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.166",
|
|
4
4
|
"description": "The AI-coding operations layer that makes \"done\" require evidence — persistent memory, evidence-gated completion checks, and clean handoffs for any AI agent (Claude Code, Codex, Cursor). State lives as plain files in your repo. CLI + MCP, 0 runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"scripts": {
|
|
51
51
|
"test": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/handoff-readonly-probe.js && npm run test:commands && npm run test:installed && node ./scripts/e2e.js",
|
|
52
52
|
"test:core": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/handoff-readonly-probe.js",
|
|
53
|
-
"test:commands": "node ./scripts/e2e-command-surface.js",
|
|
53
|
+
"test:commands": "node ./scripts/command-flags-probe.js && node ./scripts/e2e-command-surface.js",
|
|
54
54
|
"test:installed": "node ./scripts/installed-cleanroom-probe.js",
|
|
55
55
|
"test:handoff": "node ./scripts/handoff-readonly-probe.js",
|
|
56
56
|
"test:fast": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/lock-probe.js && node ./scripts/platform-smoke.js && node ./scripts/release-runtime-probe.js && node ./scripts/smoke.js",
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const CLI = path.resolve(__dirname, '..', 'bin', 'leerness.js');
|
|
11
|
+
const env = {
|
|
12
|
+
...process.env,
|
|
13
|
+
LEERNESS_OFFLINE: '1',
|
|
14
|
+
LEERNESS_NO_PROMPT: '1',
|
|
15
|
+
LEERNESS_NO_AUTOCHCP: '1',
|
|
16
|
+
LEERNESS_NO_AUTO_ROADMAP: '1',
|
|
17
|
+
LEERNESS_INTERNAL: '1',
|
|
18
|
+
LEERNESS_NO_STALE_CHECK: '1',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function run(args) {
|
|
22
|
+
return spawnSync(process.execPath, [CLI, ...args], {
|
|
23
|
+
cwd: path.resolve(__dirname, '..'),
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
timeout: 30000,
|
|
26
|
+
env,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function json(result) {
|
|
31
|
+
try { return JSON.parse(result.stdout || ''); } catch { return null; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function treeSnapshot(root) {
|
|
35
|
+
const rows = [];
|
|
36
|
+
function walk(dir, relative = '') {
|
|
37
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
38
|
+
const rel = path.join(relative, entry.name).replace(/\\/g, '/');
|
|
39
|
+
const absolute = path.join(dir, entry.name);
|
|
40
|
+
const stat = fs.lstatSync(absolute, { bigint: true });
|
|
41
|
+
const metadata = [Number(stat.mode), stat.size.toString(), stat.mtimeNs.toString()];
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
rows.push([rel, 'directory', ...metadata]);
|
|
44
|
+
walk(absolute, rel);
|
|
45
|
+
} else if (entry.isFile()) {
|
|
46
|
+
rows.push([rel, 'file', ...metadata, crypto.createHash('sha256').update(fs.readFileSync(absolute)).digest('hex')]);
|
|
47
|
+
} else if (entry.isSymbolicLink()) {
|
|
48
|
+
rows.push([rel, 'symlink', ...metadata, fs.readlinkSync(absolute)]);
|
|
49
|
+
} else {
|
|
50
|
+
rows.push([rel, 'other', ...metadata]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
walk(root);
|
|
55
|
+
return JSON.stringify(rows);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const failures = [];
|
|
59
|
+
function check(label, condition, result) {
|
|
60
|
+
if (condition) {
|
|
61
|
+
console.log(`✓ ${label}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
failures.push(label);
|
|
65
|
+
const detail = result
|
|
66
|
+
? `exit=${result.status}\nstdout=${String(result.stdout || '').slice(0, 300)}\nstderr=${String(result.stderr || '').slice(0, 300)}`
|
|
67
|
+
: '';
|
|
68
|
+
console.log(`✗ ${label}${detail ? `\n${detail}` : ''}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const catalog = run(['commands', '--json']);
|
|
72
|
+
const catalogJson = json(catalog);
|
|
73
|
+
const catalogRows = catalogJson?.categories ? Object.values(catalogJson.categories).flat() : [];
|
|
74
|
+
check('commands --json returns the machine-readable catalog',
|
|
75
|
+
catalog.status === 0 && catalogJson?.totalCommands === 99 && catalogJson?.categories && !catalog.stderr,
|
|
76
|
+
catalog);
|
|
77
|
+
check('commands totalCommands equals the category sum',
|
|
78
|
+
catalogJson?.totalCommands === catalogRows.length,
|
|
79
|
+
catalog);
|
|
80
|
+
check('commands catalog includes verify-code and contract verify',
|
|
81
|
+
catalogRows.some(row => /^verify-code\b/.test(row.cmd))
|
|
82
|
+
&& catalogRows.some(row => /^contract verify\b/.test(row.cmd)),
|
|
83
|
+
catalog);
|
|
84
|
+
|
|
85
|
+
const englishCatalog = run(['commands', '--language', 'en', '--json']);
|
|
86
|
+
check('commands accepts --language and returns an English catalog',
|
|
87
|
+
englishCatalog.status === 0 && json(englishCatalog)?.lang === 'en' && !englishCatalog.stderr,
|
|
88
|
+
englishCatalog);
|
|
89
|
+
|
|
90
|
+
const englishCatalogEquals = run(['commands', '--language=en', '--json']);
|
|
91
|
+
check('commands accepts the documented equals form for value flags',
|
|
92
|
+
englishCatalogEquals.status === 0 && json(englishCatalogEquals)?.lang === 'en' && !englishCatalogEquals.stderr,
|
|
93
|
+
englishCatalogEquals);
|
|
94
|
+
|
|
95
|
+
const missingLanguage = run(['commands', '--language', '--json']);
|
|
96
|
+
check('a value flag without a value fails instead of falling back',
|
|
97
|
+
missingLanguage.status === 1 && json(missingLanguage)?.code === 'missing_flag_value' && !missingLanguage.stderr,
|
|
98
|
+
missingLanguage);
|
|
99
|
+
|
|
100
|
+
const invalidLanguage = run(['commands', '--language', 'xx', '--json', '--no-stale-check']);
|
|
101
|
+
check('an invalid language value fails instead of silently falling back',
|
|
102
|
+
invalidLanguage.status === 1 && json(invalidLanguage)?.code === 'invalid_flag_value' && !invalidLanguage.stderr,
|
|
103
|
+
invalidLanguage);
|
|
104
|
+
|
|
105
|
+
const known = run(['commands', '--path', process.cwd(), '--json']);
|
|
106
|
+
check('commands accepts its documented global flags',
|
|
107
|
+
known.status === 0 && json(known)?.categories && !known.stderr,
|
|
108
|
+
known);
|
|
109
|
+
|
|
110
|
+
const nonexistent = run(['commands', '--definitely-unknown', '--json']);
|
|
111
|
+
const nonexistentJson = json(nonexistent);
|
|
112
|
+
check('a nonexistent flag fails closed with one JSON error',
|
|
113
|
+
nonexistent.status === 1 && nonexistentJson?.ok === false
|
|
114
|
+
&& nonexistentJson?.code === 'unknown_flag'
|
|
115
|
+
&& String(nonexistentJson?.error || '').includes('--definitely-unknown')
|
|
116
|
+
&& !nonexistent.stderr,
|
|
117
|
+
nonexistent);
|
|
118
|
+
|
|
119
|
+
const nonexistentEquals = run(['commands', '--definitely-unknown=value', '--json']);
|
|
120
|
+
check('an equals-form nonexistent flag also fails closed',
|
|
121
|
+
nonexistentEquals.status === 1 && json(nonexistentEquals)?.code === 'unknown_flag' && !nonexistentEquals.stderr,
|
|
122
|
+
nonexistentEquals);
|
|
123
|
+
|
|
124
|
+
const nonexistentHuman = run(['commands', '--definitely-unknown']);
|
|
125
|
+
check('human-mode unknown flags stay visible on stderr while failing closed',
|
|
126
|
+
nonexistentHuman.status === 1 && /알 수 없는 플래그|Unknown flag/.test(nonexistentHuman.stderr || '')
|
|
127
|
+
&& !(nonexistentHuman.stdout || '').trim(),
|
|
128
|
+
nonexistentHuman);
|
|
129
|
+
|
|
130
|
+
const booleanEquals = run(['commands', '--json=true']);
|
|
131
|
+
check('boolean flags reject equals-form values that their consumers would ignore',
|
|
132
|
+
booleanEquals.status === 1 && /--json=true/.test((booleanEquals.stdout || '') + (booleanEquals.stderr || '')),
|
|
133
|
+
booleanEquals);
|
|
134
|
+
|
|
135
|
+
const missingMeasure = run(['benchmark', '--measure']);
|
|
136
|
+
check('missing values retain a user-facing command-usage hint',
|
|
137
|
+
missingMeasure.status === 1 && /사용법|usage|task/i.test((missingMeasure.stdout || '') + (missingMeasure.stderr || '')),
|
|
138
|
+
missingMeasure);
|
|
139
|
+
|
|
140
|
+
const missingPath = run(['status', '--path']);
|
|
141
|
+
check('missing --path retains its specific no-value diagnostic',
|
|
142
|
+
missingPath.status === 1 && /--path 에 값이 없습니다|--path was given without a value/.test((missingPath.stdout || '') + (missingPath.stderr || '')),
|
|
143
|
+
missingPath);
|
|
144
|
+
|
|
145
|
+
// --cmd exists elsewhere in leerness, so the global registry alone cannot catch
|
|
146
|
+
// it. The commands command must reject a foreign, command-specific flag too.
|
|
147
|
+
const foreign = run(['commands', '--cmd', 'ignored-value', '--json']);
|
|
148
|
+
const foreignJson = json(foreign);
|
|
149
|
+
check('commands rejects a known-but-foreign command flag',
|
|
150
|
+
foreign.status === 1 && foreignJson?.ok === false
|
|
151
|
+
&& foreignJson?.code === 'unknown_flag'
|
|
152
|
+
&& String(foreignJson?.error || '').includes('--cmd')
|
|
153
|
+
&& !foreign.stderr,
|
|
154
|
+
foreign);
|
|
155
|
+
|
|
156
|
+
const foreignTestCommand = run(['commands', '--test-cmd', 'ignored-command', '--json']);
|
|
157
|
+
check('commands rejects a value flag owned by another command',
|
|
158
|
+
foreignTestCommand.status === 1 && json(foreignTestCommand)?.code === 'unknown_flag' && !foreignTestCommand.stderr,
|
|
159
|
+
foreignTestCommand);
|
|
160
|
+
|
|
161
|
+
const lensUnknown = run(['lens', '--type', 'database', '--json']);
|
|
162
|
+
check('lens rejects an unknown value flag before its value becomes a domain',
|
|
163
|
+
lensUnknown.status === 1 && json(lensUnknown)?.code === 'unknown_flag' && !lensUnknown.stderr,
|
|
164
|
+
lensUnknown);
|
|
165
|
+
|
|
166
|
+
const lensForeign = run(['lens', '--test-cmd', 'database', '--json']);
|
|
167
|
+
check('lens rejects a known-but-foreign value flag',
|
|
168
|
+
lensForeign.status === 1 && json(lensForeign)?.code === 'unknown_flag' && !lensForeign.stderr,
|
|
169
|
+
lensForeign);
|
|
170
|
+
|
|
171
|
+
const contractUnknown = run(['contract', 'verify', 'missing-spec.md', '--type', 'missing-impl.md', '--json']);
|
|
172
|
+
check('contract verify rejects an unknown flag before positional error meaning changes',
|
|
173
|
+
contractUnknown.status === 1 && json(contractUnknown)?.code === 'unknown_flag'
|
|
174
|
+
&& json(contractUnknown)?.code !== 'spec_not_found' && !contractUnknown.stderr,
|
|
175
|
+
contractUnknown);
|
|
176
|
+
|
|
177
|
+
const aboutForeign = run(['about', '--test-cmd', 'ignored-command', '--json']);
|
|
178
|
+
check('about rejects a known-but-foreign value flag',
|
|
179
|
+
aboutForeign.status === 1 && json(aboutForeign)?.code === 'unknown_flag' && !aboutForeign.stderr,
|
|
180
|
+
aboutForeign);
|
|
181
|
+
|
|
182
|
+
// The external evaluation's concrete case was verify-code --cmd/--test-cmd:
|
|
183
|
+
// both are valid names elsewhere, but this command does not consume them.
|
|
184
|
+
const emptyProject = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-command-flags-'));
|
|
185
|
+
try {
|
|
186
|
+
const initialized = run(['init', emptyProject, '--yes', '--json']);
|
|
187
|
+
check('route-arity fixture initializes successfully',
|
|
188
|
+
initialized.status === 0 && json(initialized)?.ok === true,
|
|
189
|
+
initialized);
|
|
190
|
+
|
|
191
|
+
for (const mutationFlag of ['--fix', '--baseline', '--auto-track']) {
|
|
192
|
+
const beforeInvalidGate = treeSnapshot(emptyProject);
|
|
193
|
+
const invalidGate = run(['gate', emptyProject, '--require-referee', mutationFlag, '--json']);
|
|
194
|
+
check(`gate rejects foreign ${mutationFlag} before any sub-check can write`,
|
|
195
|
+
invalidGate.status === 1 && json(invalidGate)?.code === 'unknown_flag'
|
|
196
|
+
&& String(json(invalidGate)?.error || '').includes(mutationFlag)
|
|
197
|
+
&& treeSnapshot(emptyProject) === beforeInvalidGate,
|
|
198
|
+
invalidGate);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const updateCheck = run(['update', emptyProject, '--check', '--json']);
|
|
202
|
+
check('update keeps --check as a boolean flag',
|
|
203
|
+
updateCheck.status === 0 && json(updateCheck)?.code !== 'missing_flag_value'
|
|
204
|
+
&& typeof json(updateCheck)?.verdict === 'string',
|
|
205
|
+
updateCheck);
|
|
206
|
+
|
|
207
|
+
const updateCheckEquals = run(['update', emptyProject, '--check=true', '--json']);
|
|
208
|
+
check('update rejects a value attached to its boolean --check',
|
|
209
|
+
updateCheckEquals.status === 1 && json(updateCheckEquals)?.code === 'invalid_flag_syntax',
|
|
210
|
+
updateCheckEquals);
|
|
211
|
+
|
|
212
|
+
const previewKeep = run(['preview', 'serve', 'P-NOT-FOUND', '--keep', '--path', emptyProject, '--json']);
|
|
213
|
+
check('preview serve keeps --keep as a boolean flag',
|
|
214
|
+
previewKeep.status === 1 && json(previewKeep)?.code === 'not_found',
|
|
215
|
+
previewKeep);
|
|
216
|
+
|
|
217
|
+
const previewKeepBeforeId = run(['preview', 'serve', '--keep', 'P-NOT-FOUND', '--path', emptyProject, '--json']);
|
|
218
|
+
check('preview serve does not let --keep swallow a following preview id',
|
|
219
|
+
previewKeepBeforeId.status === 1 && json(previewKeepBeforeId)?.code === 'not_found'
|
|
220
|
+
&& String(json(previewKeepBeforeId)?.error || '').includes('P-NOT-FOUND'),
|
|
221
|
+
previewKeepBeforeId);
|
|
222
|
+
|
|
223
|
+
const previewKeepBeforeSubcommand = run(['preview', '--keep', 'serve', 'P-NOT-FOUND', '--path', emptyProject, '--json']);
|
|
224
|
+
check('preview serve resolves its boolean --keep even before the subcommand',
|
|
225
|
+
previewKeepBeforeSubcommand.status === 1 && json(previewKeepBeforeSubcommand)?.code === 'not_found'
|
|
226
|
+
&& String(json(previewKeepBeforeSubcommand)?.error || '').includes('P-NOT-FOUND'),
|
|
227
|
+
previewKeepBeforeSubcommand);
|
|
228
|
+
|
|
229
|
+
const refereeMissingCheck = run(['referee', 'add', 'route-arity-missing', '--check', '--good', 'node -e "process.exit(0)"', '--bad', 'node -e "process.exit(1)"', '--path', emptyProject, '--json']);
|
|
230
|
+
check('referee add still requires a value for --check',
|
|
231
|
+
refereeMissingCheck.status === 1 && json(refereeMissingCheck)?.code === 'missing_flag_value',
|
|
232
|
+
refereeMissingCheck);
|
|
233
|
+
|
|
234
|
+
const cleanupMissingKeep = run(['release', 'cleanup', '--keep', '--path', emptyProject, '--json']);
|
|
235
|
+
check('release cleanup still requires a numeric value for --keep',
|
|
236
|
+
cleanupMissingKeep.status === 1 && json(cleanupMissingKeep)?.code === 'missing_flag_value',
|
|
237
|
+
cleanupMissingKeep);
|
|
238
|
+
|
|
239
|
+
const gateMissingReferee = run(['gate', emptyProject, '--require-referee', '--json']);
|
|
240
|
+
check('gate retains its structured missing-referee check before dispatch',
|
|
241
|
+
gateMissingReferee.status === 1 && Array.isArray(json(gateMissingReferee)?.checks)
|
|
242
|
+
&& json(gateMissingReferee).checks.some(row => row?.code === 'referee_missing_value'),
|
|
243
|
+
gateMissingReferee);
|
|
244
|
+
|
|
245
|
+
const spec = path.join(emptyProject, 'contract.md');
|
|
246
|
+
const impl = path.join(emptyProject, 'contract.js');
|
|
247
|
+
fs.writeFileSync(spec, '- charge()\n', 'utf8');
|
|
248
|
+
fs.writeFileSync(impl, 'function charge() {}\nmodule.exports = { charge };\n', 'utf8');
|
|
249
|
+
const contractWithGlobalPath = run(['contract', 'verify', spec, impl, '--path', emptyProject, '--json']);
|
|
250
|
+
check('contract verify retains the global --path compatibility flag',
|
|
251
|
+
contractWithGlobalPath.status === 0 && json(contractWithGlobalPath)?.ok === true && !contractWithGlobalPath.stderr,
|
|
252
|
+
contractWithGlobalPath);
|
|
253
|
+
|
|
254
|
+
const verifyForeign = run(['verify-code', emptyProject, '--cmd', 'ignored-command']);
|
|
255
|
+
check('verify-code rejects flags it does not implement',
|
|
256
|
+
verifyForeign.status === 1 && /--cmd/.test((verifyForeign.stdout || '') + (verifyForeign.stderr || '')),
|
|
257
|
+
verifyForeign);
|
|
258
|
+
|
|
259
|
+
const verifyTestCommand = run(['verify-code', emptyProject, '--test-cmd', 'ignored-command']);
|
|
260
|
+
check('verify-code rejects --test-cmd instead of recording unrelated auto-detected evidence',
|
|
261
|
+
verifyTestCommand.status === 1 && /--test-cmd/.test((verifyTestCommand.stdout || '') + (verifyTestCommand.stderr || '')),
|
|
262
|
+
verifyTestCommand);
|
|
263
|
+
|
|
264
|
+
const strictEquals = run(['verify-code', emptyProject, '--strict=true']);
|
|
265
|
+
check('verify-code rejects a boolean value form instead of losing strict mode',
|
|
266
|
+
strictEquals.status === 1 && /--strict=true/.test((strictEquals.stdout || '') + (strictEquals.stderr || '')),
|
|
267
|
+
strictEquals);
|
|
268
|
+
|
|
269
|
+
const claimOwnsTestCommand = run(['verify-claim', 'T-NOT-FOUND', '--run-tests', '--test-cmd', 'node -e "process.exit(0)"', '--path', emptyProject, '--json']);
|
|
270
|
+
check('verify-claim retains its supported --test-cmd contract',
|
|
271
|
+
claimOwnsTestCommand.status === 1 && json(claimOwnsTestCommand)?.code !== 'unknown_flag',
|
|
272
|
+
claimOwnsTestCommand);
|
|
273
|
+
} finally {
|
|
274
|
+
fs.rmSync(emptyProject, { recursive: true, force: true });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Invalid input must not trigger the legacy workspace auto-migration that is
|
|
278
|
+
// normally allowed for a valid command.
|
|
279
|
+
const legacyProject = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-command-flags-legacy-'));
|
|
280
|
+
try {
|
|
281
|
+
const legacyWorkspaceName = ['.', 'harness'].join('');
|
|
282
|
+
const legacyDir = path.join(legacyProject, legacyWorkspaceName);
|
|
283
|
+
fs.mkdirSync(legacyDir);
|
|
284
|
+
fs.writeFileSync(path.join(legacyDir, 'HARNESS_VERSION'), '1.36.161\n', 'utf8');
|
|
285
|
+
const before = fs.readFileSync(path.join(legacyDir, 'HARNESS_VERSION'), 'utf8');
|
|
286
|
+
for (const mutationFlag of ['--fix', '--baseline', '--auto-track']) {
|
|
287
|
+
const beforeInvalidGate = treeSnapshot(legacyProject);
|
|
288
|
+
const invalidGate = run(['gate', '--require-referee', mutationFlag, '--path', legacyProject, '--json']);
|
|
289
|
+
check(`gate ${mutationFlag} rejection cannot migrate or mutate a legacy workspace`,
|
|
290
|
+
invalidGate.status === 1 && json(invalidGate)?.code === 'unknown_flag'
|
|
291
|
+
&& fs.existsSync(legacyDir) && !fs.existsSync(path.join(legacyProject, '.leerness'))
|
|
292
|
+
&& treeSnapshot(legacyProject) === beforeInvalidGate,
|
|
293
|
+
invalidGate);
|
|
294
|
+
}
|
|
295
|
+
const invalidLegacy = run(['commands', '--unknown-value', 'database', '--path', legacyProject, '--json']);
|
|
296
|
+
check('unknown flags fail before legacy workspace migration or writes',
|
|
297
|
+
invalidLegacy.status === 1 && json(invalidLegacy)?.code === 'unknown_flag'
|
|
298
|
+
&& fs.existsSync(legacyDir) && !fs.existsSync(path.join(legacyProject, '.leerness'))
|
|
299
|
+
&& fs.readFileSync(path.join(legacyDir, 'HARNESS_VERSION'), 'utf8') === before,
|
|
300
|
+
invalidLegacy);
|
|
301
|
+
|
|
302
|
+
const invalidKeepLegacy = run(['init', '--keep', 'abc', '--path', legacyProject, '--json']);
|
|
303
|
+
check('invalid value semantics fail before legacy workspace migration or writes',
|
|
304
|
+
invalidKeepLegacy.status === 1 && json(invalidKeepLegacy)?.code === 'invalid_keep'
|
|
305
|
+
&& fs.existsSync(legacyDir) && !fs.existsSync(path.join(legacyProject, '.leerness'))
|
|
306
|
+
&& fs.readFileSync(path.join(legacyDir, 'HARNESS_VERSION'), 'utf8') === before,
|
|
307
|
+
invalidKeepLegacy);
|
|
308
|
+
|
|
309
|
+
const missingRefereeLegacy = run(['gate', '--require-referee', '--path', legacyProject, '--json']);
|
|
310
|
+
check('command-specific malformed diagnostics also run before legacy migration',
|
|
311
|
+
missingRefereeLegacy.status === 1 && Array.isArray(json(missingRefereeLegacy)?.checks)
|
|
312
|
+
&& json(missingRefereeLegacy).checks.some(row => row?.code === 'referee_missing_value')
|
|
313
|
+
&& fs.existsSync(legacyDir) && !fs.existsSync(path.join(legacyProject, '.leerness')),
|
|
314
|
+
missingRefereeLegacy);
|
|
315
|
+
} finally {
|
|
316
|
+
fs.rmSync(legacyProject, { recursive: true, force: true });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (failures.length) {
|
|
320
|
+
console.log(`UNKNOWN_FLAGS_ACCEPTED: ${failures.join('; ')}`);
|
|
321
|
+
process.exitCode = 1;
|
|
322
|
+
} else {
|
|
323
|
+
console.log('command flags probe passed');
|
|
324
|
+
}
|
|
@@ -104,9 +104,9 @@ async function race(name, config) {
|
|
|
104
104
|
}));
|
|
105
105
|
results.push(await race('memory-restore-decisions', {
|
|
106
106
|
stallTarget: 'decisions.json',
|
|
107
|
-
seed: d => { run(d, ['decision', 'add', 'archived', '--
|
|
107
|
+
seed: d => { run(d, ['decision', 'add', 'archived', '--reason', 'test']); run(d, ['decision', 'drop', 'archived']); },
|
|
108
108
|
first: ['memory', 'restore', 'decisions', 'archived'],
|
|
109
|
-
second: ['decision', 'add', 'survivor', '--
|
|
109
|
+
second: ['decision', 'add', 'survivor', '--reason', 'test'],
|
|
110
110
|
inspect: d => {
|
|
111
111
|
const titles = JSON.parse(fs.readFileSync(path.join(d, '.leerness', 'decisions.json'), 'utf8')).map(item => item.title);
|
|
112
112
|
return { titles, preserved: titles.some(title => title.includes('archived')) && titles.some(title => title.includes('survivor')) };
|
package/scripts/e2e.js
CHANGED
|
@@ -10630,7 +10630,7 @@ total++;
|
|
|
10630
10630
|
{ cwd: d, encoding: 'utf8', timeout: 300000, env: ENV0 });
|
|
10631
10631
|
const CASES = [
|
|
10632
10632
|
['task add', ['task', 'add', '락프로브']],
|
|
10633
|
-
['decision add', ['decision', 'add', '락프로브', '--
|
|
10633
|
+
['decision add', ['decision', 'add', '락프로브', '--reason', 'x']],
|
|
10634
10634
|
['lesson save', ['lesson', 'save', '락프로브']],
|
|
10635
10635
|
['rule add', ['rule', 'add', '락프로브', '--trigger', 'every-session']],
|
|
10636
10636
|
['rule pause', ['rule', 'pause', 'R-0001'], ['rule', 'add', '씨앗룰', '--trigger', 'every-session']],
|
|
@@ -10640,7 +10640,7 @@ total++;
|
|
|
10640
10640
|
['creds refresh', ['creds', 'refresh', '락프로브'], d => SEED(d, ['creds', 'register', '락프로브', '--env-var', 'LOCK_PROBE'])],
|
|
10641
10641
|
// team ID는 ASCII slug만 허용한다. 표시 라벨과 달리 seed/삭제 대상은 실제 유효 ID를 쓴다.
|
|
10642
10642
|
['team remove', ['team', 'remove', 'lock-probe'], d => SEED(d, ['team', 'add', 'lock-probe', '--name', 'lock'])],
|
|
10643
|
-
['memory restore decisions', ['memory', 'restore', 'decisions', '락프로브'], d => { SEED(d, ['decision', 'add', '락프로브', '--
|
|
10643
|
+
['memory restore decisions', ['memory', 'restore', 'decisions', '락프로브'], d => { SEED(d, ['decision', 'add', '락프로브', '--reason', 'x']); SEED(d, ['decision', 'drop', '락프로브']); }],
|
|
10644
10644
|
['memory restore lessons', ['memory', 'restore', 'lessons', '락프로브'], d => { SEED(d, ['lesson', 'save', '락프로브']); SEED(d, ['lesson', 'drop', '락프로브']); }],
|
|
10645
10645
|
['memory restore plan', ['memory', 'restore', 'plan', '락프로브'], d => { SEED(d, ['plan', 'add', '락프로브']); SEED(d, ['plan', 'remove', '락프로브']); }],
|
|
10646
10646
|
['plan drop', ['plan', 'drop', '락프로브']],
|
|
@@ -10940,7 +10940,7 @@ total++;
|
|
|
10940
10940
|
const R = (a) => cp.spawnSync(process.execPath, [CLI, ...a, '--path', d], { cwd: d, encoding: 'utf8', timeout: 300000, env: ENV });
|
|
10941
10941
|
// 상태를 조금 만든다 — 빈 프로젝트는 출력이 짧아 누출을 과소평가한다. 입력은 전부 영어.
|
|
10942
10942
|
R(['task', 'add', 'Implement the parser']);
|
|
10943
|
-
R(['decision', 'add', 'Use JSON storage', '--
|
|
10943
|
+
R(['decision', 'add', 'Use JSON storage', '--reason', 'simplest']);
|
|
10944
10944
|
R(['plan', 'add', 'Ship v1']);
|
|
10945
10945
|
const HANGUL = /[가-힣ㄱ-ㆎ]/;
|
|
10946
10946
|
// 측정 대상은 **도구가 말하는 표면**이다. 사용자 데이터(프로젝트명·과거 기록)는 한국어여도 정상이므로
|
|
@@ -11255,7 +11255,7 @@ total++;
|
|
|
11255
11255
|
const SURFACES = [
|
|
11256
11256
|
['requests', (d, t) => R(d, ['requests', 'add', t]), (d) => [R(d, ['requests', 'list']), R(d, ['requests', 'audit'])]],
|
|
11257
11257
|
['task', (d, t) => R(d, ['task', 'add', t]), (d) => [R(d, ['task', 'list'])]],
|
|
11258
|
-
['decision', (d, t) => R(d, ['decision', 'add', t, '--
|
|
11258
|
+
['decision', (d, t) => R(d, ['decision', 'add', t, '--reason', 'r']), (d) => [R(d, ['decision', 'list'])]],
|
|
11259
11259
|
['lesson', (d, t) => R(d, ['lesson', 'save', t]), (d) => [R(d, ['lesson', 'list'])]],
|
|
11260
11260
|
['rule', (d, t) => R(d, ['rule', 'add', t, '--trigger', 'every-round']), (d) => [R(d, ['rule', 'list'])]],
|
|
11261
11261
|
['plan', (d, t) => R(d, ['plan', 'add', t]), (d) => [R(d, ['plan', 'list'])]],
|
|
@@ -11464,11 +11464,11 @@ total++;
|
|
|
11464
11464
|
const SEED = [['requests', 'add', 'seed'], ['preview', 'add', 'seed'], ['toggle', 'set', 'gate', 'off'],
|
|
11465
11465
|
['creds', 'register', 'seed', '--env-var', 'K'], ['permissions', 'set', 'extended'], ['wakeup-interval', 'set', '900'],
|
|
11466
11466
|
['next-action', 'add', 'seed'], ['constraints', 'add', 'seed', '--constraint', 'k:v'],
|
|
11467
|
-
['decision', 'add', 'seed', '--
|
|
11467
|
+
['decision', 'add', 'seed', '--reason', 'r'], ['lesson', 'save', 'seed']];
|
|
11468
11468
|
const MUT = [['requests', 'add', 'x2'], ['preview', 'add', 'x2'], ['toggle', 'set', 'lens', 'off'],
|
|
11469
11469
|
['creds', 'register', 'x2', '--env-var', 'K2'], ['permissions', 'set', 'basic'], ['wakeup-interval', 'set', '1200'],
|
|
11470
11470
|
['next-action', 'add', 'x2'], ['constraints', 'add', 'x2', '--constraint', 'k:v'],
|
|
11471
|
-
['decision', 'add', 'x2', '--
|
|
11471
|
+
['decision', 'add', 'x2', '--reason', 'r'], ['lesson', 'save', 'x2'], ['task', 'add', 'x2'], ['plan', 'add', 'x2']];
|
|
11472
11472
|
const DERIVED = new Set(['manifest.json', 'skills-lock.json', 'leerness-config.json', 'environment.json']);
|
|
11473
11473
|
const seeded = () => { const d = mk(); for (const a of SEED) R(d, a); return d; };
|
|
11474
11474
|
const base = seeded();
|