leerness 1.36.174 → 1.36.176
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 +10 -0
- package/README.md +4 -4
- package/bin/leerness.js +478 -152
- package/lib/agents.js +5 -1
- package/lib/clarify.js +245 -46
- package/lib/dashboard.js +25 -3
- package/lib/health.js +20 -18
- package/lib/mcp-tools.js +3 -3
- package/lib/preview-serve.js +41 -15
- package/lib/pure-utils.js +93 -13
- package/lib/session-close.js +43 -23
- package/package.json +4 -3
- package/scripts/e2e.js +69 -30
- package/scripts/false-claim-probe.js +1507 -0
- package/scripts/release-runtime-probe.js +5 -2
- package/scripts/workspace-dir-migration-probe.js +3 -1
package/bin/leerness.js
CHANGED
|
@@ -27,7 +27,7 @@ const { _isSecretKey, _isPlaceholderSecret, _looksSecretLike, _mergeLines, _merg
|
|
|
27
27
|
_withBuiltinSource, _esc, _roadmapTokenStyles, _parseSkillMd, _lintSkillMeta,
|
|
28
28
|
_migrationGuideText, _parseContractSpec, _gitignoreMatch,
|
|
29
29
|
_featureGraphTemplate, _parseFeatureGraph, _nextFeatureId, _featureBlock, _featureImpactBfs,
|
|
30
|
-
_parseChangelogBetween, _cellSafe, _cellUnescape, _lineSafe, _parseLimit, _parseAddTitle, _parseImplExports, _taskPositionalPath, _completionClaimAllowed, _minorKey, _shouldPublishNpm,
|
|
30
|
+
_parseChangelogBetween, _cellSafe, _cellUnescape, _lineSafe, _milestoneSurface, _recentChangesAggregate, _parseLimit, _parseAddTitle, _parseImplExports, _taskPositionalPath, _completionClaimAllowed, _minorKey, _shouldPublishNpm,
|
|
31
31
|
_matchTool, _parsePackageJsonDeps, _parseRequirementsTxt, _buildGlossary, _renderGlossaryMd, _briefUnfilled, _planGoalUnfilled, _draftAnchors, _replaceMdSection, _mdSectionBody, _blocksWithOffset, _decisionBlocksWithOffset } = require('../lib/pure-utils'); // 1.9.318~1.11.4 (UR-0025/.../0007 glossary): 순수 유틸 모듈 분리 · 1.36.36 anchors
|
|
32
32
|
// 1.9.304 (UR-0025): 순수 분석/검증 함수 모듈 분리.
|
|
33
33
|
const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInGit, _epistemicHonestyCheck } = require('../lib/analyzers');
|
|
@@ -48,7 +48,7 @@ const {
|
|
|
48
48
|
migrateLegacyWorkspace,
|
|
49
49
|
} = require('../lib/workspace-dir');
|
|
50
50
|
|
|
51
|
-
const VERSION = '1.36.
|
|
51
|
+
const VERSION = '1.36.176';
|
|
52
52
|
|
|
53
53
|
// MCP lifecycle 주소 표식은 현재 CLI 호출 한 번에만 유효하다. CLI bootstrap에서 즉시 env에서
|
|
54
54
|
// 떼어 두어 `--no-record`/hook처럼 presence 기록 함수에 도달하지 않는 경로도 후속 child에 유출하지 않는다.
|
|
@@ -3173,59 +3173,169 @@ function _detectDeliveredRequests(root) {
|
|
|
3173
3173
|
|
|
3174
3174
|
// 1.9.324 (UR-0025): _compareSemver 제거 — pure-utils compareVer 와 동일 기능(중복) → compareVer 단일화.
|
|
3175
3175
|
|
|
3176
|
+
// Git refs may contain `|`, while shortened refs can change when another ref is
|
|
3177
|
+
// ambiguous. Use the full tag ref plus NUL (forbidden in ref names), then accept
|
|
3178
|
+
// only the exact refs/tags/vX.Y.Z namespace and a real calendar/offset value.
|
|
3179
|
+
const _ROUND_TAG_FORMAT = '%(refname)%00%(creatordate:iso8601)';
|
|
3180
|
+
function _validRoundTagDate(value) {
|
|
3181
|
+
const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2}) ([+-])(\d{2})(\d{2})$/.exec(value);
|
|
3182
|
+
if (!m) return false;
|
|
3183
|
+
const year = Number(m[1]), month = Number(m[2]), day = Number(m[3]);
|
|
3184
|
+
const hour = Number(m[4]), minute = Number(m[5]), second = Number(m[6]);
|
|
3185
|
+
const offsetHour = Number(m[8]), offsetMinute = Number(m[9]);
|
|
3186
|
+
if (year < 1 || month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59
|
|
3187
|
+
|| offsetHour > 14 || offsetMinute > 59 || (offsetHour === 14 && offsetMinute !== 0)) return false;
|
|
3188
|
+
const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
3189
|
+
const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
3190
|
+
return day >= 1 && day <= days[month - 1];
|
|
3191
|
+
}
|
|
3192
|
+
function _parseRoundTagOutput(stdout) {
|
|
3193
|
+
const tags = [];
|
|
3194
|
+
for (const record of String(stdout || '').split(/\r?\n/)) {
|
|
3195
|
+
if (!record) continue;
|
|
3196
|
+
const sep = record.indexOf('\0');
|
|
3197
|
+
if (sep <= 0) continue;
|
|
3198
|
+
const ref = record.slice(0, sep);
|
|
3199
|
+
const date = record.slice(sep + 1).trim();
|
|
3200
|
+
const match = /^refs\/tags\/(v\d+\.\d+\.\d+)$/.exec(ref);
|
|
3201
|
+
if (!match || !_validRoundTagDate(date)) continue;
|
|
3202
|
+
const name = match[1];
|
|
3203
|
+
tags.push({ name, version: name.slice(1), date });
|
|
3204
|
+
}
|
|
3205
|
+
return tags;
|
|
3206
|
+
}
|
|
3207
|
+
function _gitHistoryFailure(reason, detail) {
|
|
3208
|
+
const clean = _lineSafe(detail || 'Git history could not be measured');
|
|
3209
|
+
return { code: 'git_history_unavailable', reason, error: clean };
|
|
3210
|
+
}
|
|
3211
|
+
function _gitHistoryFailureFromResult(r) {
|
|
3212
|
+
let reason = 'git_failed';
|
|
3213
|
+
let detail = '';
|
|
3214
|
+
if (r && r.error) {
|
|
3215
|
+
reason = String(r.error.code || '') === 'ETIMEDOUT' ? 'timeout' : String(r.error.code || 'spawn_error').toLowerCase();
|
|
3216
|
+
detail = r.error.message || String(r.error);
|
|
3217
|
+
} else if (r && r.signal) {
|
|
3218
|
+
reason = 'signal';
|
|
3219
|
+
detail = `git terminated by signal ${r.signal}`;
|
|
3220
|
+
} else if (r && r.status != null) {
|
|
3221
|
+
reason = `exit_${r.status}`;
|
|
3222
|
+
detail = String(r.stderr || `git exited with status ${r.status}`).trim();
|
|
3223
|
+
}
|
|
3224
|
+
return _gitHistoryFailure(reason, detail);
|
|
3225
|
+
}
|
|
3226
|
+
function _readRoundTags(root) {
|
|
3227
|
+
let r;
|
|
3228
|
+
try {
|
|
3229
|
+
r = _gitSpawn(['tag', '--list', 'v*', '--sort=creatordate', `--format=${_ROUND_TAG_FORMAT}`], {
|
|
3230
|
+
cwd: root, encoding: 'utf8', timeout: 5000
|
|
3231
|
+
});
|
|
3232
|
+
} catch (err) {
|
|
3233
|
+
return { state: 'unavailable', tags: [], error: _gitHistoryFailure('spawn_exception', err && err.message) };
|
|
3234
|
+
}
|
|
3235
|
+
if (r && r.status === 0) {
|
|
3236
|
+
return { state: 'available', tags: r.stdout ? _parseRoundTagOutput(r.stdout) : [], error: null };
|
|
3237
|
+
}
|
|
3238
|
+
return { state: 'unavailable', tags: [], error: _gitHistoryFailureFromResult(r) };
|
|
3239
|
+
}
|
|
3240
|
+
function _throwGitHistoryUnavailable(error) {
|
|
3241
|
+
const detail = error && error.error ? error.error : 'Git history could not be measured';
|
|
3242
|
+
throw Object.assign(new Error(detail), { code: 'E_GIT_HISTORY_UNAVAILABLE', historyError: error || null });
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3176
3245
|
// 1.9.226: round-history — git tag v1.9.X 기반 자율 라운드 통계 + 다음 마일스톤
|
|
3177
|
-
//
|
|
3178
|
-
//
|
|
3179
|
-
|
|
3246
|
+
// 태그 0건은 실제 0으로, Git 실행 실패는 unavailable/null 로 구분한다.
|
|
3247
|
+
// currentVersion 은 호환 별칭일 뿐이며 반드시 currentVersionScope 와 함께 해석한다.
|
|
3248
|
+
// 응답: { cliVersion, harnessVersion, currentVersion(deprecated alias), currentVersionScope, roundCount, ... }
|
|
3249
|
+
function _computeRoundHistory(root, opts = {}) {
|
|
3180
3250
|
const result = {
|
|
3251
|
+
cliVersion: VERSION,
|
|
3252
|
+
harnessVersion: null,
|
|
3253
|
+
harnessVersionState: 'missing',
|
|
3254
|
+
// Backward compatibility for 1.9.226+ consumers. This never meant an app/project version.
|
|
3181
3255
|
currentVersion: VERSION,
|
|
3256
|
+
currentVersionScope: 'leerness_cli',
|
|
3182
3257
|
roundCount: 0,
|
|
3183
3258
|
baselineVersion: null,
|
|
3259
|
+
latestTagVersion: null,
|
|
3184
3260
|
latestTags: [],
|
|
3185
3261
|
nextMilestone: null,
|
|
3186
3262
|
roundsToNextMilestone: null,
|
|
3187
3263
|
firstTagAt: null,
|
|
3188
3264
|
latestTagAt: null,
|
|
3189
3265
|
daysActive: 0,
|
|
3190
|
-
avgRoundsPerDay: 0
|
|
3266
|
+
avgRoundsPerDay: 0,
|
|
3267
|
+
gitHistoryState: 'available',
|
|
3268
|
+
gitHistoryError: null
|
|
3191
3269
|
};
|
|
3270
|
+
{
|
|
3271
|
+
const history = _readRoundTags(root);
|
|
3272
|
+
result.gitHistoryState = history.state;
|
|
3273
|
+
result.gitHistoryError = history.error;
|
|
3274
|
+
if (history.state !== 'available') {
|
|
3275
|
+
result.roundCount = null;
|
|
3276
|
+
result.latestTags = null;
|
|
3277
|
+
result.daysActive = null;
|
|
3278
|
+
result.avgRoundsPerDay = null;
|
|
3279
|
+
}
|
|
3280
|
+
const tags = history.tags;
|
|
3281
|
+
if (tags.length > 0) {
|
|
3282
|
+
result.roundCount = tags.length;
|
|
3283
|
+
result.baselineVersion = tags[0].version;
|
|
3284
|
+
result.firstTagAt = tags[0].date;
|
|
3285
|
+
result.latestTagAt = tags[tags.length - 1].date;
|
|
3286
|
+
result.latestTagVersion = tags[tags.length - 1].version;
|
|
3287
|
+
// `date` is a public field from 1.9.226. Git's iso8601 formatter uses a
|
|
3288
|
+
// space separator, so splitting it silently removed its time/offset in
|
|
3289
|
+
// 1.36.175. Preserve that established value and add a date-only view.
|
|
3290
|
+
result.latestTags = tags.slice(-10).reverse().map(t => ({
|
|
3291
|
+
version: t.version,
|
|
3292
|
+
date: t.date,
|
|
3293
|
+
dateOnly: t.date.split(' ')[0],
|
|
3294
|
+
}));
|
|
3295
|
+
const milestones = [50, 75, 100, 125, 150, 175, 200, 250, 300, 400, 500];
|
|
3296
|
+
const next = milestones.find(m => m > result.roundCount);
|
|
3297
|
+
if (next != null) {
|
|
3298
|
+
result.nextMilestone = next;
|
|
3299
|
+
result.roundsToNextMilestone = next - result.roundCount;
|
|
3300
|
+
}
|
|
3301
|
+
const first = new Date(result.firstTagAt).getTime();
|
|
3302
|
+
const last = new Date(result.latestTagAt).getTime();
|
|
3303
|
+
const spanDays = (last - first) / 86400000;
|
|
3304
|
+
if (tags.length >= 2 && spanDays > 0) {
|
|
3305
|
+
result.daysActive = Math.max(1, Math.floor(spanDays));
|
|
3306
|
+
result.avgRoundsPerDay = Math.round((result.roundCount / spanDays) * 100) / 100;
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
// Git history and workspace provenance are independent evidence sources.
|
|
3312
|
+
// Aggregate callers may retain the measured tag count while exposing a
|
|
3313
|
+
// workspace-policy error; direct round-history/cadence calls still fail.
|
|
3314
|
+
let hv;
|
|
3192
3315
|
try {
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
return { name: name.trim(), version: name.replace(/^v/, '').trim(), date: date.trim() };
|
|
3203
|
-
});
|
|
3204
|
-
result.roundCount = tags.length;
|
|
3205
|
-
result.baselineVersion = tags[0].version;
|
|
3206
|
-
result.firstTagAt = tags[0].date;
|
|
3207
|
-
result.latestTagAt = tags[tags.length - 1].date;
|
|
3208
|
-
// 최근 10개
|
|
3209
|
-
result.latestTags = tags.slice(-10).reverse().map(t => ({ version: t.version, date: t.date.split('T')[0] }));
|
|
3210
|
-
// 다음 마일스톤 (50/100/150/200/300/...)
|
|
3211
|
-
const milestones = [50, 75, 100, 125, 150, 175, 200, 250, 300, 400, 500];
|
|
3212
|
-
const next = milestones.find(m => m > result.roundCount);
|
|
3213
|
-
if (next != null) {
|
|
3214
|
-
result.nextMilestone = next;
|
|
3215
|
-
result.roundsToNextMilestone = next - result.roundCount;
|
|
3216
|
-
}
|
|
3217
|
-
// 일일 평균 — 1.36.78 (F8): 태그 2+ & 양의 구간일 때만(1개면 속도 관측 불가 → 0 유지)
|
|
3218
|
-
const first = new Date(result.firstTagAt).getTime();
|
|
3219
|
-
const last = new Date(result.latestTagAt).getTime();
|
|
3220
|
-
const spanDays = (last - first) / 86400000;
|
|
3221
|
-
if (tags.length >= 2 && spanDays > 0) {
|
|
3222
|
-
result.daysActive = Math.max(1, Math.floor(spanDays));
|
|
3223
|
-
result.avgRoundsPerDay = Math.round((result.roundCount / spanDays) * 100) / 100;
|
|
3224
|
-
} else {
|
|
3225
|
-
result.daysActive = 0;
|
|
3226
|
-
result.avgRoundsPerDay = 0;
|
|
3316
|
+
hv = path.join(_workspaceDirAbs(root), 'HARNESS_VERSION');
|
|
3317
|
+
} catch (err) {
|
|
3318
|
+
if (opts.allowWorkspaceError && err && /^E_WORKSPACE_DIR_[A-Z_]+$/.test(String(err.code || ''))) {
|
|
3319
|
+
result.harnessVersionState = 'workspace-error';
|
|
3320
|
+
result.workspaceError = {
|
|
3321
|
+
code: String(err.code).slice(2).toLowerCase(),
|
|
3322
|
+
error: _lineSafe(err.message || String(err)),
|
|
3323
|
+
};
|
|
3324
|
+
return result;
|
|
3227
3325
|
}
|
|
3228
|
-
|
|
3326
|
+
throw err;
|
|
3327
|
+
}
|
|
3328
|
+
try {
|
|
3329
|
+
// Positional read-only calls select the legacy workspace without renaming
|
|
3330
|
+
// it, while still reporting the version actually installed there.
|
|
3331
|
+
if (exists(hv)) {
|
|
3332
|
+
const parsed = parseHarnessVersion(read(hv));
|
|
3333
|
+
// `leerness@X+plus@Y`의 설치 Leerness 버전은 X다. plus는 옛
|
|
3334
|
+
// 호환 레이어 버전이며 프로젝트 하네스 버전을 대신할 수 없다.
|
|
3335
|
+
result.harnessVersion = parsed.base || null;
|
|
3336
|
+
result.harnessVersionState = result.harnessVersion ? 'valid' : 'invalid';
|
|
3337
|
+
}
|
|
3338
|
+
} catch { result.harnessVersionState = 'unreadable'; }
|
|
3229
3339
|
return result;
|
|
3230
3340
|
}
|
|
3231
3341
|
|
|
@@ -3238,19 +3348,22 @@ function _computeMilestones(root) {
|
|
|
3238
3348
|
reached: [],
|
|
3239
3349
|
next: null,
|
|
3240
3350
|
avgRoundsPerDay: 0,
|
|
3241
|
-
baselineAt: null
|
|
3351
|
+
baselineAt: null,
|
|
3352
|
+
gitHistoryState: 'available',
|
|
3353
|
+
gitHistoryError: null
|
|
3242
3354
|
};
|
|
3243
|
-
|
|
3244
|
-
const
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
if (
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3355
|
+
{
|
|
3356
|
+
const history = _readRoundTags(root);
|
|
3357
|
+
result.gitHistoryState = history.state;
|
|
3358
|
+
result.gitHistoryError = history.error;
|
|
3359
|
+
if (history.state !== 'available') {
|
|
3360
|
+
result.totalRounds = null;
|
|
3361
|
+
result.reached = null;
|
|
3362
|
+
result.avgRoundsPerDay = null;
|
|
3363
|
+
return result;
|
|
3364
|
+
}
|
|
3365
|
+
const tags = history.tags;
|
|
3366
|
+
if (tags.length === 0) return result;
|
|
3254
3367
|
result.totalRounds = tags.length;
|
|
3255
3368
|
result.baselineAt = tags[0].date;
|
|
3256
3369
|
const milestones = [25, 50, 75, 100, 125, 150, 175, 200, 250, 300, 400, 500];
|
|
@@ -3288,7 +3401,7 @@ function _computeMilestones(root) {
|
|
|
3288
3401
|
// 속도 관측 불가 — 다음 마일스톤은 표시하되 ETA 는 null(추정 없음)
|
|
3289
3402
|
result.next = { milestone: nextM, roundsRemaining: nextM - tags.length, etaDays: null, etaDate: null };
|
|
3290
3403
|
}
|
|
3291
|
-
}
|
|
3404
|
+
}
|
|
3292
3405
|
return result;
|
|
3293
3406
|
}
|
|
3294
3407
|
|
|
@@ -3300,6 +3413,7 @@ function milestonesCmd(root) {
|
|
|
3300
3413
|
const yl = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
|
|
3301
3414
|
const dm = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
3302
3415
|
const data = _computeMilestones(root);
|
|
3416
|
+
if (data.gitHistoryState !== 'available') _throwGitHistoryUnavailable(data.gitHistoryError);
|
|
3303
3417
|
if (has('--json')) { log(JSON.stringify(data, null, 2)); return; }
|
|
3304
3418
|
log(cy(`# leerness milestones (1.9.229) — 도달 마일스톤 + 다음 ETA`));
|
|
3305
3419
|
log('');
|
|
@@ -3503,34 +3617,39 @@ function agentModeCmd(root, sub) {
|
|
|
3503
3617
|
process.exitCode = 1;
|
|
3504
3618
|
}
|
|
3505
3619
|
|
|
3506
|
-
// 1.9.234: recentChanges — 최근 N 라운드의 핵심 변경
|
|
3507
|
-
//
|
|
3508
|
-
|
|
3620
|
+
// 1.9.234/1.36.175: recentChanges — 최근 N 라운드의 핵심 변경 요약.
|
|
3621
|
+
// shortened decoration과 `|` 구분자는 모호하다. full ref와 NUL 필드를
|
|
3622
|
+
// 사용하고, Git 실패는 빈 배열이 아니라 unavailable/items:null 이다.
|
|
3623
|
+
const _RECENT_CHANGE_FORMAT = '%(refname)%00%(creatordate:iso8601)%00%(*subject)%00%(subject)';
|
|
3624
|
+
function _parseRecentChangeOutput(stdout, limit = 5) {
|
|
3509
3625
|
const items = [];
|
|
3626
|
+
for (const record of String(stdout || '').split(/\r?\n/)) {
|
|
3627
|
+
if (!record) continue;
|
|
3628
|
+
const fields = record.split('\0');
|
|
3629
|
+
if (fields.length !== 4) continue;
|
|
3630
|
+
const match = /^refs\/tags\/v(\d+\.\d+\.\d+)$/.exec(fields[0]);
|
|
3631
|
+
const date = String(fields[1] || '').trim();
|
|
3632
|
+
if (!match || !_validRoundTagDate(date)) continue;
|
|
3633
|
+
const subject = _lineSafe(fields[2] || fields[3] || '')
|
|
3634
|
+
.replace(/^\d+\.\d+\.\d+\s*[—-]\s*/, '').trim().slice(0, 100);
|
|
3635
|
+
items.push({ version: match[1], date: date.split(' ')[0], subject });
|
|
3636
|
+
if (items.length >= limit) break;
|
|
3637
|
+
}
|
|
3638
|
+
return items;
|
|
3639
|
+
}
|
|
3640
|
+
function _computeRecentChanges(root, limit = 5) {
|
|
3641
|
+
let r;
|
|
3510
3642
|
try {
|
|
3511
|
-
|
|
3643
|
+
r = _gitSpawn(['for-each-ref', '--sort=-creatordate', `--format=${_RECENT_CHANGE_FORMAT}`, 'refs/tags/v*'], {
|
|
3512
3644
|
cwd: root, encoding: 'utf8', timeout: 5000
|
|
3513
3645
|
});
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
if (idx2 < 0 || idx3 < 0) continue;
|
|
3522
|
-
const refs = line.slice(idx1 + 1, idx2);
|
|
3523
|
-
const date = line.slice(idx2 + 1, idx3);
|
|
3524
|
-
const subject = line.slice(idx3 + 1);
|
|
3525
|
-
const m = refs.match(/tag: v(\d+\.\d+\.\d+)/);
|
|
3526
|
-
if (!m) continue;
|
|
3527
|
-
const version = m[1];
|
|
3528
|
-
const cleanSubject = subject.replace(/^\d+\.\d+\.\d+\s*[—-]\s*/, '').trim();
|
|
3529
|
-
items.push({ version, date, subject: cleanSubject.slice(0, 100) });
|
|
3530
|
-
if (items.length >= limit) break;
|
|
3531
|
-
}
|
|
3532
|
-
} catch {}
|
|
3533
|
-
return items;
|
|
3646
|
+
} catch (err) {
|
|
3647
|
+
return { state: 'unavailable', error: _gitHistoryFailure('spawn_exception', err && err.message), items: null };
|
|
3648
|
+
}
|
|
3649
|
+
if (!r || r.status !== 0) {
|
|
3650
|
+
return { state: 'unavailable', error: _gitHistoryFailureFromResult(r), items: null };
|
|
3651
|
+
}
|
|
3652
|
+
return { state: 'available', error: null, items: _parseRecentChangeOutput(r.stdout, limit) };
|
|
3534
3653
|
}
|
|
3535
3654
|
|
|
3536
3655
|
// 1.9.241: 환경 정보 종합 수집 + 셸 스크립트 인코딩 위험 감지 (사용자 명시 UR-0014)
|
|
@@ -4051,7 +4170,7 @@ function _selfTestCases() {
|
|
|
4051
4170
|
{ name: '_isSecretKey: 비시크릿 LEERNESS_* 통과', run: () => _isSecretKey('LEERNESS_ENABLE_AGY') === false && _isSecretKey('LANG') === false },
|
|
4052
4171
|
{ name: 'compareVer: 대소 비교', run: () => compareVer('1.9.258', '1.9.257') === 1 && compareVer('1.9.0', '1.10.0') === -1 && compareVer('2.0.0', '2.0.0') === 0 },
|
|
4053
4172
|
{ name: 'compareVer: 누락 파트/null 안전', run: () => compareVer('1.9', '1.9.0') === 0 && compareVer(null, '0.0.0') === 0 },
|
|
4054
|
-
{ name: 'parseHarnessVersion: canonical
|
|
4173
|
+
{ name: 'parseHarnessVersion: exact canonical/legacy formats only', run: () => parseHarnessVersion('1.9.0').base === '1.9.0' && parseHarnessVersion('leerness@1.8.0+plus@1.0.1').plus === '1.0.1' && parseHarnessVersion('leerness@1.8.0').base === '1.8.0' && parseHarnessVersion('garbage leerness@1.8.0 trailing').base === null && parseHarnessVersion('leerness@1.8.0.1').base === null && parseHarnessVersion('plus@1.0.1 junk').plus === null },
|
|
4055
4174
|
{ name: '_classifyCJK: 한국어 감지', run: () => { const b = Buffer.from('안녕', 'utf8'); return _classifyCJK(b, b.length).korean > 0; } },
|
|
4056
4175
|
{ name: '_classifyCJK: 중국어/일본어 구분', run: () => { const z = Buffer.from('中', 'utf8'), j = Buffer.from('ひ', 'utf8'); return _classifyCJK(z, z.length).chinese > 0 && _classifyCJK(j, j.length).japanese > 0; } },
|
|
4057
4176
|
{ name: '_riskLabel: korean→CP949 / chinese→CP936', run: () => /CP949/.test(_riskLabel({ korean: 3, japanese: 0, chinese: 0, other: 0 }).risk) && /CP936/.test(_riskLabel({ korean: 0, japanese: 0, chinese: 3, other: 0 }).risk) },
|
|
@@ -5003,12 +5122,12 @@ function _selfTestCases() {
|
|
|
5003
5122
|
{ name: 'UR-0073 Phase A: team 정의 레지스트리 (_renderTeamsMd + canonical load/save) 행위 (1.9.371)', run: () => { const m = require('../lib/pure-utils'); if (typeof teamCmd !== 'function' || typeof _renderTeamsMd !== 'function' || m._renderTeamsMd !== _renderTeamsMd) return false; const md = _renderTeamsMd([{ id: 't1', name: 'N', personas: ['security'], members: ['claude'], schedule: 'daily', status: 'active' }]); const mdOk = md.includes('## t1') && md.includes('security') && md.includes('daily') && md.includes('정의 전용'); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_team_')); let rtOk = false; try { _saveTeams(tmp, [{ id: 'x', name: 'X', personas: [], members: [], schedule: 'manual', status: 'active' }]); const loaded = _loadTeams(tmp); rtOk = loaded.length === 1 && loaded[0].id === 'x' && fs.existsSync(path.join(tmp, '.leerness', 'teams.json')) && fs.existsSync(path.join(tmp, '.leerness', 'teams.md')); } finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } return mdOk && rtOk; } },
|
|
5004
5123
|
{ name: 'UR-0073 Phase B: _composeTeamPlan dry-run 실행 계획 (멤버별 dispatch, 실행 없음) 행위 (1.9.372)', run: () => { const m = require('../lib/pure-utils'); if (typeof _composeTeamPlan !== 'function' || m._composeTeamPlan !== _composeTeamPlan) return false; const team = { id: 'rev', name: 'R', purpose: 'PR 리뷰', personas: ['security', 'perf'], members: ['claude', 'codex'], schedule: 'manual' }; const p1 = _composeTeamPlan(team, '점검'); const ok1 = p1.steps.length === 2 && p1.task === '점검' && p1.steps[0].member === 'claude' && p1.steps[0].suggestedCommand.includes('agents dispatch') && p1.steps[0].suggestedCommand.includes('--to claude') && p1.steps[0].dispatchPrompt.includes('security'); const p2 = _composeTeamPlan(team, null); const ok2 = p2.task === 'PR 리뷰'; const p3 = _composeTeamPlan({ id: 'e', personas: [], members: [] }, 'x'); const ok3 = p3.steps.length === 0 && p3.memberCount === 0; return ok1 && ok2 && ok3; } },
|
|
5005
5124
|
{ name: 'UR-0073 Phase C: _teamHandoffReminders 스케줄 알림 (비-manual·active 만, 실행 트리거 아님) 행위 (1.9.373) + i18n en(1.31.3)', run: () => { const m = require('../lib/pure-utils'); if (typeof _teamHandoffReminders !== 'function' || m._teamHandoffReminders !== _teamHandoffReminders) return false; const r = _teamHandoffReminders([{ id: 'rev', schedule: 'every-session', status: 'active', members: ['a', 'b'] }, { id: 'man', schedule: 'manual', status: 'active' }, { id: 'paused', schedule: 'daily', status: 'paused' }]); const behaviorOk = r.length === 1 && r[0].includes('rev') && r[0].includes('every-session') && r[0].includes('team preview rev') && !r.join('|').includes('man') && !r.join('|').includes('paused'); const _H = /[가-힣]/; const en = _teamHandoffReminders([{ id: 'rev', schedule: 'every-session', status: 'active', members: ['a', 'b'], review: true }], 'en')[0] || ''; const ko = _teamHandoffReminders([{ id: 'rev', schedule: 'every-session', status: 'active', members: ['a', 'b'], review: true }])[0] || ''; const i18nOk = !_H.test(en) && /2 members/.test(en) && /review needed/.test(en) && /preview:/.test(en) && _H.test(ko) && /2명/.test(ko) && /검수필요/.test(ko); return behaviorOk && i18nOk; } },
|
|
5006
|
-
{ name: 'UR-0074: _cadenceAssessment 릴리스 빈도 평가 (임계값) 행위 (1.9.374)', run: () => { const m = require('../lib/pure-utils'); if (typeof _cadenceAssessment !== 'function' || m._cadenceAssessment !== _cadenceAssessment || typeof releaseCadenceCmd !== 'function') return false; return _cadenceAssessment(7,
|
|
5125
|
+
{ name: 'UR-0074: _cadenceAssessment 릴리스 빈도 평가 (임계값) 행위 (1.9.374)', run: () => { const m = require('../lib/pure-utils'); if (typeof _cadenceAssessment !== 'function' || m._cadenceAssessment !== _cadenceAssessment || typeof releaseCadenceCmd !== 'function') return false; return _cadenceAssessment(7, 10, 2).level === 'very-high' && _cadenceAssessment(3, 10, 2).level === 'high' && _cadenceAssessment(1, 10, 2).level === 'moderate' && _cadenceAssessment(0.2, 10, 2).level === 'healthy' && _cadenceAssessment(7, 1, 0).level === 'insufficient-data' && _cadenceAssessment(7, 10, 2).recommendation.length > 0; } },
|
|
5007
5126
|
{ name: 'UR-0084 + 1.36.160: _withLock 35s 대기 + 토큰 소유권 + owner fail-closed 행위', run: () => _p0160LockOwnershipOk() },
|
|
5008
5127
|
{ name: 'UR-0073 Phase D: _teamDeployGate 이중 게이트 (dry-run 기본/env 게이트/실행) 행위 (1.9.376)', run: () => { const m = require('../lib/pure-utils'); if (typeof _teamDeployGate !== 'function' || m._teamDeployGate !== _teamDeployGate) return false; const team = { id: 'd', deployCommand: 'echo hi' }; const noCmd = _teamDeployGate({ id: 'x' }, { yes: true, envOn: true }).mode === 'no-command'; const dry = _teamDeployGate(team, { yes: false, envOn: true }).mode === 'dry-run'; const gated = _teamDeployGate(team, { yes: true, envOn: false }).mode === 'gated'; const exec = _teamDeployGate(team, { yes: true, envOn: true }).mode === 'execute'; return noCmd && dry && gated && exec; } },
|
|
5009
5128
|
{ name: 'UR-0025: _renderWorkspaceReferenceGuide 모듈 분리 + canonical migration 안내 (1.36.162)', run: () => { const m = require('../lib/pure-utils'); if (typeof _renderWorkspaceReferenceGuide !== 'function' || m._renderWorkspaceReferenceGuide !== _renderWorkspaceReferenceGuide) return false; const g = _renderWorkspaceReferenceGuide(CANONICAL_WORKSPACE_DIR, '9.9.9', '2026-01-01T00:00:00.000Z', LEGACY_WORKSPACE_DIR); const wrapperThin = read(__filename).includes('return _renderWorkspaceReferenceGuide(dirName, VERSION, new Date().toISOString(), LEGACY_WORKSPACE_DIR)'); const guideSafe = g.includes('canonical workspace') && g.includes('workspace-dir-conflict') && g.includes('.leerness/cache/agent-runs/') && g.includes(`${LEGACY_WORKSPACE_DIR}/`); return g.includes('.leerness/progress-tracker.md') && g.includes('9.9.9') && g.includes('자주 묻는 위치') && guideSafe && wrapperThin; } },
|
|
5010
5129
|
{ name: 'UR-0073: team MCP 도구 2종(read-only) 정의 + dispatch 와이어 (1.9.378)', run: () => { const tools = require('../lib/mcp-tools'); const src = read(__filename); const tl = tools.find(t => t.name === 'leerness_team_list'); const tp = tools.find(t => t.name === 'leerness_team_preview'); const defsOk = tl && tl.requiredTier === 'read-only' && tp && tp.requiredTier === 'read-only' && tp.inputSchema.required && tp.inputSchema.required.includes('id'); const wired = src.includes("case " + "'leerness_team_list':") && src.includes("case " + "'leerness_team_preview':") && /cliArgs = \['team', 'list'/.test(src) && /cliArgs = \['team', 'preview'/.test(src); return !!defsOk && wired; } },
|
|
5011
|
-
{ name: 'UR-0025
|
|
5130
|
+
{ name: 'UR-0025/T-0095: pulse 렌더 코어 + Git 이력 unavailable 정직 표기', run: () => { const m = require('../lib/pure-utils'); if (typeof _memorySurface !== 'function' || typeof _renderPulseLine !== 'function' || m._memorySurface !== _memorySurface || m._renderPulseLine !== _renderPulseLine) return false; const ms = _memorySurface({ tasks: 1, decisions: 2, rules: 3, milestones: 4, lessons: 5 }) === 'T1/D2/R3/P4/L5' && _memorySurface({}) === 'T0/D0/R0/P0/L0'; const base = _renderPulseLine({ version: '1.0.0', roundCount: 7, mcpTools: 85, memorySurface: 'T0/D1/R0/P2/L0' }); const ln = base.includes('v1.0.0') && base.includes('R7') && base.includes('MCP 85') && base.includes('T0/D1/R0/P2/L0') && !base.includes('🎯') && !base.includes('abnormal'); const full = _renderPulseLine({ version: '1.0.0', roundCount: 7, mcpTools: 85, memorySurface: 'x', nextMilestone: 400, etaDays: 6, abnormalShutdown: 'high' }); const ln2 = full.includes('🎯 R400 (6d)') && full.includes('abnormal:high'); const unavailable = _renderPulseLine({ version: '1.0.0', roundCount: null, mcpTools: 85, memorySurface: 'x' }); const unavailableOk = unavailable.includes('unavailable') && !unavailable.includes('Rnull'); const wired = read(__filename).includes('const line = _renderPulseLine(data)') && read(__filename).includes('data.memorySurface = _memorySurface('); return ms && ln && ln2 && unavailableOk && wired; } },
|
|
5012
5131
|
{ name: 'UR-0025: REQUIRED_WORKSPACE_FILES 단일출처 — verify/migrate audit·apply 3중 중복 제거 (1.9.380)', run: () => { const c = require('../lib/catalogs'); if (REQUIRED_WORKSPACE_FILES !== c.REQUIRED_WORKSPACE_FILES) return false; const listOk = Array.isArray(c.REQUIRED_WORKSPACE_FILES) && c.REQUIRED_WORKSPACE_FILES.length === 9 && c.REQUIRED_WORKSPACE_FILES.includes('AGENTS.md') && c.REQUIRED_WORKSPACE_FILES.includes('.leerness/plan.md'); const harnessUses = (read(__filename).match(/const required = REQUIRED_WORKSPACE_FILES;/g) || []).length >= 1; const migUses = (read(path.join(path.dirname(__filename), '..', 'lib', 'migrate.js')).match(/const required = REQUIRED_WORKSPACE_FILES;/g) || []).length >= 2; return listOk && harnessUses && migUses; } },
|
|
5013
5132
|
{ name: 'UR-0025: KEYWORD_STOPWORDS 단일출처 — handoff/lessons 키워드 stopwords 2중 중복 제거 (1.9.381)', run: () => { const c = require('../lib/catalogs'); if (KEYWORD_STOPWORDS !== c.KEYWORD_STOPWORDS) return false; const setOk = c.KEYWORD_STOPWORDS instanceof Set && c.KEYWORD_STOPWORDS.has('작업') && c.KEYWORD_STOPWORDS.has('task') && !c.KEYWORD_STOPWORDS.has('고유단어') && c.KEYWORD_STOPWORDS.size >= 25; const usesConst = (read(__filename).match(/const stopwords = KEYWORD_STOPWORDS;/g) || []).length >= 2 && !/const stopwords = new Set\(\[/.test(read(__filename)); return setOk && usesConst; } },
|
|
5014
5133
|
{ name: 'UR-0025 큰핸들러토대: lib/io.js 프리미티브(log/ok/warn/fail/today/now) 모듈 분리 + 동작 (1.9.382)', run: () => { const io = require('../lib/io'); const exportsOk = ['log', 'ok', 'warn', 'fail', 'today', 'now'].every(k => typeof io[k] === 'function') && io.log === log && io.fail === fail && io.now === now; const todayOk = /^\d{4}-\d{2}-\d{2}$/.test(io.today()) && /^\d{4}-\d{2}-\d{2}T/.test(io.now()); const src = read(__filename); const moved = src.includes("require('../lib/io')") && !/^function fail\(s\) \{ log/m.test(src) && !/^function now\(\) \{ return new Date/m.test(src); let exitOk = false; const saved = process.exitCode; const _w = process.stdout.write, _e = process.stderr.write; try { process.stdout.write = () => true; process.stderr.write = () => true; process.exitCode = 0; io.fail('probe'); exitOk = process.exitCode === 1; } finally { process.stdout.write = _w; process.stderr.write = _e; process.exitCode = saved; io.setQuiet(false); } return exportsOk && todayOk && moved && exitOk; } },
|
|
@@ -5041,10 +5160,10 @@ function _selfTestCases() {
|
|
|
5041
5160
|
{ name: 'T-0077 후속 graph auto-gen: handoff opt-in 배선 + quiet 무로그 (1.34.4)', run: () => { const m = require('../lib/graph'); const src = read(__filename); const wired = src.includes('_maybeAuto' + 'Graph(_hp)') && src.includes('LEERNESS_AUTO_' + 'GRAPH'); let quietOk = false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_autograph_')); const _w = process.stdout.write; let so = ''; try { process.stdout.write = s => { so += s; return true; }; fs.mkdirSync(path.join(tmp, '.leerness'), { recursive: true }); fs.writeFileSync(path.join(tmp, '.leerness', 'progress-tracker.md'), '| ID | Status | Request | Evidence | Next | Updated |\n|---|---|---|---|---|---|\n| T-0001 | done | x | - | - | 2026-06-26 |\n'); const out = path.join(tmp, 'leerness.html'); const r = m.graphHtmlCmd(tmp, { _roadmapData, _loadDecisions, _loadLessons, quiet: true }, out); quietOk = !!r && r.ok === true && fs.existsSync(out) && so === ''; } catch (e) { quietOk = false; } finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } return wired && quietOk; } },
|
|
5042
5161
|
{ name: 'graph --html: 빈/미초기화 하네스 무크래시 + 유효 빈 HTML (1.35.0 방어가드)', run: () => { const m = require('../lib/graph'); let ok = false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_emptyg_')); const _w = process.stdout.write; let so = ''; try { process.stdout.write = s => { so += s; return true; }; fs.mkdirSync(path.join(tmp, '.leerness'), { recursive: true }); const data = m.buildGraphData(tmp, {}); const out = path.join(tmp, 'leerness.html'); const r = m.graphHtmlCmd(tmp, { quiet: true }, out); const html = fs.readFileSync(out, 'utf8'); const closers = html.split('</' + 'script>').length - 1; const dm = html.match(/var DATA = (\{[\s\S]*?\});/); let parsed = null; try { parsed = JSON.parse(dm[1]); } catch (e) {} ok = !!data && Array.isArray(data.nodes) && data.nodes.length === 0 && !!r && r.ok === true && r.nodes === 0 && fs.existsSync(out) && closers === 1 && !!parsed && parsed.nodes.length === 0 && so === ''; } catch (e) { ok = false; } finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } return ok; } },
|
|
5043
5162
|
{ name: 'graph --html: 임베드 script JS 신택스 유효성(U+2028/정규식 리터럴 회귀 영구 차단, 1.35.2)', run: () => { const m = require('../lib/graph'); let ok = false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_gjs_')); const _w = process.stdout.write; let so = ''; try { process.stdout.write = s => { so += s; return true; }; fs.mkdirSync(path.join(tmp, '.leerness'), { recursive: true }); fs.writeFileSync(path.join(tmp, '.leerness', 'progress-tracker.md'), '| ID | Status | Request | Evidence | Next | Updated |\n|---|---|---|---|---|---|\n| T-0001 | done | x </scr' + 'ipt> & <b> ' + String.fromCharCode(0x24) + '{y} | M-0002 | - | 2026-06-26 |\n'); const out = path.join(tmp, 'leerness.html'); m.graphHtmlCmd(tmp, { _roadmapData, _loadDecisions, _loadLessons, quiet: true }, out); const html = fs.readFileSync(out, 'utf8'); const o = '<scr' + 'ipt>', c = '</scr' + 'ipt>'; const js = html.slice(html.indexOf(o) + o.length, html.lastIndexOf(c)); let synOk = false; try { new Function(js); synOk = true; } catch (e) { synOk = false; } ok = js.length > 200 && synOk; } catch (e) { ok = false; } finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } return ok; } },
|
|
5044
|
-
{ name: '7번째 버그헌트 P1-A (UR-0104): 테이블셀 안전화 _cellSafe/_cellUnescape (파이프/개행 injection 차단) (1.9.399)', run: () => { const m = require('../lib/pure-utils'); if (m._cellSafe !== _cellSafe || m._cellUnescape !== _cellUnescape) return false; const safe = _cellSafe('fix | bug\nrow2'); const noRaw = !/(?<!\\)\|/.test(safe) && !/[\r\n]/.test(safe); const pipeRt = _cellUnescape(_cellSafe('a | b | c')) === 'a | b | c'; const nlGone = _cellSafe('a\nb') === 'a b'; const src = read(__filename); const wired = src.includes('_cellSafe(r.request)') && src.includes('_cellSafe(r.rule)'); return noRaw && pipeRt && nlGone && wired; } },
|
|
5163
|
+
{ name: '7번째 버그헌트 P1-A (UR-0104): 테이블셀 안전화 _cellSafe/_cellUnescape (파이프/개행 injection 차단) (1.9.399/1.36.175)', run: () => { const m = require('../lib/pure-utils'); if (m._cellSafe !== _cellSafe || m._cellUnescape !== _cellUnescape) return false; const safe = _cellSafe('fix | bug\nrow2\u2028row3\u2029row4'); const noRaw = !/(?<!\\)\|/.test(safe) && !/[\r\n\u2028\u2029]/.test(safe); const pipeRt = _cellUnescape(_cellSafe('a | b | c')) === 'a | b | c'; const nlGone = _cellSafe('a\nb\u2028c\u2029d') === 'a b c d'; const src = read(__filename); const wired = src.includes('_cellSafe(r.request)') && src.includes('_cellSafe(r.rule)'); return noRaw && pipeRt && nlGone && wired; } },
|
|
5045
5164
|
{ name: '7번째 버그헌트 P1-B (UR-0105): verify-claim/optimism-check/honesty-check --json 에러 구조화 (1.9.400)', run: () => { const src = read(__filename); const vc = /function verifyClaimCmd[\s\S]{0,1200}?failJson\(_j, 'not_found'/.test(src); const oc = /function optimismCheckCmd[\s\S]{0,700}?failJson\(_j, 'not_found'/.test(src); const hc = /function honestyCheckCmd[\s\S]{0,900}?failJson\(has\('--json'\), 'not_found'/.test(src); return vc && oc && hc; } }, // 1.30.5: {0,400}→{0,700} (F4 가 missing_args 라인을 en/ko 로 늘려 not_found 가 창 밖) · 1.33.2: vc {0,700}→{0,1200} (opts.collect 가드 라인이 not_found 를 더 밀어냄)
|
|
5046
5165
|
{ name: '7번째 버그헌트 P1-C (UR-0106): 시크릿 FN — gitignore 부정(!) + placeholder substring 정밀화 (1.9.401)', run: () => { const m = require('../lib/pure-utils'); const gm = m._gitignoreMatch; const negOk = gm('*.example\n!.env.example', '.env.example') === false && gm('*.log', 'a.log') === true && gm('a.log\n!a.log', 'a.log') === false && gm('.env', '.env') === true; const ph = m._isPlaceholderSecret; const phOk = ph('sk-EXAMPLEab12cd34ef56gh78ij90kl') === false && ph('sk-proj-realKEYexample9988776655') === false && ph('your-key-here') === true && ph('changeme') === true && ph('example') === true && ph('xxxxxxxxxxxxxxxxxxxxxxxxxxxx') === true; return negOk && phOk; } },
|
|
5047
|
-
{ name: '7번째 버그헌트 P1-A 잔여 (UR-0108):
|
|
5166
|
+
{ name: '7번째 버그헌트 P1-A 잔여 (UR-0108/T-0095): MD/터미널 제어문자 주입 차단 _lineSafe (1.9.402/1.36.175)', run: () => { const m = require('../lib/pure-utils'); if (m._lineSafe !== _lineSafe) return false; const lsOk = _lineSafe('a\nb\r\nc\u2028d\u2029e') === 'a b c d e'; const controls = _lineSafe('a\u000b\u000c\u001b[2J\u007f\u0085b'); const controlsOk = !/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/.test(controls); const md = m._renderDecisionsMd([{ date: '2026-06-07', title: 'real\n### 2099-01-01 — FAKE\n- Decision: forged', decision: 'd', reason: 'r' }]); const re = m._decisionsFromMd(md); const noInject = re.length === 1 && !/^### 2099-01-01 — FAKE/m.test(md); const lmd = m._renderLessonsMd([{ date: '2026-06-07', text: 'l1\n### FAKE\n- Lesson: x', tag: 't' }]); const lre = m._parseLessonEntries(lmd); const lNoInject = lre.length === 1; return lsOk && controlsOk && noInject && lNoInject; } },
|
|
5048
5167
|
{ name: '7번째 버그헌트 P2 (UR-0107): api-skill show/drop 에러 exit code 1 (1.9.403)', run: () => {
|
|
5049
5168
|
// 낡은 소스-리터럴(인라인 `process.exitCode = 1`) 대신 **행위 검사**: 1.36.74 에서 에러 경로가 failJson 으로
|
|
5050
5169
|
// 리팩터되며 리터럴이 가드 자기 줄에만 남아 공허참이 됐다. 실제로 호출해 exit code 를 확인한다.
|
|
@@ -7240,9 +7359,9 @@ function _selfTestCases() {
|
|
|
7240
7359
|
fs.mkdirSync(path.join(tmp, '.leerness'), { recursive: true });
|
|
7241
7360
|
fs.writeFileSync(c._previewsPath(tmp), '{"legacy":{"id":"P-0042"}}');
|
|
7242
7361
|
const chk1 = c._loadPreviewsChecked(tmp);
|
|
7243
|
-
fs.writeFileSync(c._previewsPath(tmp), '[{"id":"P-0001","title":"ok"},null]');
|
|
7362
|
+
fs.writeFileSync(c._previewsPath(tmp), '[{"id":"P-0001","title":"ok","status":"proposed"},null]');
|
|
7244
7363
|
const chk2 = c._loadPreviewsChecked(tmp);
|
|
7245
|
-
fs.writeFileSync(c._previewsPath(tmp), '[{"id":"P-0001","title":"ok"}]');
|
|
7364
|
+
fs.writeFileSync(c._previewsPath(tmp), '[{"id":"P-0001","title":"ok","status":"proposed"}]');
|
|
7246
7365
|
const chk3 = c._loadPreviewsChecked(tmp);
|
|
7247
7366
|
shape = chk1.invalid && chk2.invalid && !chk3.invalid && chk3.list.length === 1;
|
|
7248
7367
|
} finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
@@ -9121,7 +9240,7 @@ function pulseCmd(root) {
|
|
|
9121
9240
|
const dm = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
9122
9241
|
const data = {
|
|
9123
9242
|
version: VERSION,
|
|
9124
|
-
roundCount:
|
|
9243
|
+
roundCount: null,
|
|
9125
9244
|
mcpTools: 0,
|
|
9126
9245
|
memorySurface: 'T0/D0/R0/P0/L0',
|
|
9127
9246
|
// 1.36.39 (codex 4차 #8): pulse 는 이 3종을 계산하지 않는다 — 상태값처럼 보이던 'unknown' placeholder 가
|
|
@@ -9132,10 +9251,26 @@ function pulseCmd(root) {
|
|
|
9132
9251
|
notComputed: ['security', 'health', 'driftScore'],
|
|
9133
9252
|
nextMilestone: null,
|
|
9134
9253
|
etaDays: null,
|
|
9135
|
-
abnormalShutdown: 'none'
|
|
9254
|
+
abnormalShutdown: 'none',
|
|
9255
|
+
roundHistoryError: null,
|
|
9256
|
+
gitHistoryState: null,
|
|
9257
|
+
gitHistoryError: null,
|
|
9258
|
+
workspaceError: null
|
|
9136
9259
|
};
|
|
9137
9260
|
// 라운드 카운트
|
|
9138
|
-
try {
|
|
9261
|
+
try {
|
|
9262
|
+
const rh = _computeRoundHistory(root, { allowWorkspaceError: true });
|
|
9263
|
+
data.roundCount = rh.roundCount;
|
|
9264
|
+
data.gitHistoryState = rh.gitHistoryState;
|
|
9265
|
+
data.gitHistoryError = rh.gitHistoryError;
|
|
9266
|
+
data.workspaceError = rh.workspaceError || null;
|
|
9267
|
+
data.roundHistoryError = rh.gitHistoryError || rh.workspaceError || null;
|
|
9268
|
+
} catch (err) {
|
|
9269
|
+
const failure = _gitHistoryFailure('compute_exception', err && err.message);
|
|
9270
|
+
data.gitHistoryState = 'unavailable';
|
|
9271
|
+
data.gitHistoryError = failure;
|
|
9272
|
+
data.roundHistoryError = failure;
|
|
9273
|
+
}
|
|
9139
9274
|
// MCP 도구 수 (1.9.297: lib/mcp-tools.js 단일 출처)
|
|
9140
9275
|
try { data.mcpTools = _mcpToolCount(); } catch {}
|
|
9141
9276
|
// Memory Surface (T/D/R/P/L) — 1.9.232 BUG fix: memoryStatusCmd 와 동일한 패턴 사용
|
|
@@ -9172,6 +9307,7 @@ function pulseCmd(root) {
|
|
|
9172
9307
|
log(cy(`# leerness pulse (1.9.231/1.9.232) — 한 줄 종합 요약`));
|
|
9173
9308
|
log('');
|
|
9174
9309
|
log(` ${line}`);
|
|
9310
|
+
if (data.roundHistoryError) warn(` round-history provenance unavailable: ${data.roundHistoryError.code}`);
|
|
9175
9311
|
log('');
|
|
9176
9312
|
log(dm(` → 상세: leerness handoff . | health . | round-history | milestones`));
|
|
9177
9313
|
}
|
|
@@ -9591,7 +9727,7 @@ const _GROUP_USAGE_KO = {
|
|
|
9591
9727
|
rule: 'rule add "<텍스트>" --trigger <트리거> | rule list | rule pause/resume/remove <ID> | rule verify',
|
|
9592
9728
|
skill: 'skill list | skill add <id> | skill use <id> | skill search "<키>" | skill match "<텍스트>"',
|
|
9593
9729
|
feature: 'feature add "<이름>" | feature list | feature show <ID> | feature link <A> <B> | feature impact <ID>',
|
|
9594
|
-
memory: 'memory search "<키>" [--json] | memory status | memory archive | memory restore',
|
|
9730
|
+
memory: 'memory search "<키>" [--json] | memory status | memory archive list | memory restore <surface> <target>',
|
|
9595
9731
|
readme: 'readme sync [path]',
|
|
9596
9732
|
contract: 'contract verify <spec.md> <impl.js> [--json] [--allow-empty]',
|
|
9597
9733
|
self: 'self check [path]',
|
|
@@ -9608,7 +9744,7 @@ const _GROUP_USAGE_EN = {
|
|
|
9608
9744
|
rule: 'rule add "<text>" --trigger <trigger> | rule list | rule pause/resume/remove <ID> | rule verify',
|
|
9609
9745
|
skill: 'skill list | skill add <id> | skill use <id> | skill search "<key>" | skill match "<text>"',
|
|
9610
9746
|
feature: 'feature add "<name>" | feature list | feature show <ID> | feature link <A> <B> | feature impact <ID>',
|
|
9611
|
-
memory: 'memory search "<key>" [--json] | memory status | memory archive | memory restore',
|
|
9747
|
+
memory: 'memory search "<key>" [--json] | memory status | memory archive list | memory restore <surface> <target>',
|
|
9612
9748
|
readme: 'readme sync [path]',
|
|
9613
9749
|
contract: 'contract verify <spec.md> <impl.js> [--json] [--allow-empty]',
|
|
9614
9750
|
self: 'self check [path]',
|
|
@@ -10610,26 +10746,33 @@ function _p0104UsageOk() {
|
|
|
10610
10746
|
fs.writeFileSync(path.join(d, 'package.json'), '{"name":"x","version":"0.1.0"}');
|
|
10611
10747
|
cp2.spawnSync(process.execPath, [__filename, 'init', d, '--yes'], { cwd: d, encoding: 'utf8', timeout: 180000, env: env2 });
|
|
10612
10748
|
}
|
|
10613
|
-
const mcp = (name) => cp2.spawnSync(process.execPath, [__filename, 'mcp', 'serve'], {
|
|
10749
|
+
const mcp = (name, toolArgs = {}) => cp2.spawnSync(process.execPath, [__filename, 'mcp', 'serve'], {
|
|
10614
10750
|
cwd: cwdP, encoding: 'utf8', timeout: 180000, env: env2, maxBuffer: 32 * 1024 * 1024,
|
|
10615
10751
|
input: [
|
|
10616
10752
|
{ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'g', version: '1' } } },
|
|
10617
10753
|
{ jsonrpc: '2.0', method: 'notifications/initialized' },
|
|
10618
|
-
{ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name, arguments: { path: tgt } } },
|
|
10754
|
+
{ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name, arguments: { ...toolArgs, path: tgt } } },
|
|
10619
10755
|
].map(r => JSON.stringify(r)).join('\n') + '\n'
|
|
10620
10756
|
});
|
|
10757
|
+
// Observation-only MCP calls must not mutate their target merely to count
|
|
10758
|
+
// themselves. Writable calls still provide attributable usage evidence.
|
|
10621
10759
|
mcp('leerness_about'); mcp('leerness_pulse');
|
|
10622
10760
|
const U = (d) => ((cp2.spawnSync(process.execPath, [__filename, 'usage', '--path', d], { cwd: cwdP, encoding: 'utf8', timeout: 120000, env: env2 }).stdout) || '');
|
|
10761
|
+
const uReadOnly = U(tgt);
|
|
10762
|
+
const readOnlyClean = !/leerness_about|leerness_pulse/.test(uReadOnly);
|
|
10763
|
+
mcp('leerness_task_add', { text: 'usage attribution fixture' });
|
|
10764
|
+
mcp('leerness_decision_add', { title: 'usage attribution fixture' });
|
|
10623
10765
|
const uT = U(tgt), uC = U(cwdP);
|
|
10624
|
-
// ① 실제로
|
|
10625
|
-
const shows = /
|
|
10766
|
+
// ① 실제로 쓴 프로젝트가 그 사실을 말하고, 관찰 호출은 기록하지 않는다
|
|
10767
|
+
const shows = /leerness_task_add/.test(uT) && /leerness_decision_add/.test(uT)
|
|
10768
|
+
&& !/leerness_about|leerness_pulse/.test(uT) && !/\(사용 기록 없음\)/.test(uT);
|
|
10626
10769
|
// ② 서버가 우연히 서 있던 프로젝트에 남의 활동이 기록되지 않는다
|
|
10627
10770
|
const cwdClean = !/\|\s*mcp\s*\|/.test(uC);
|
|
10628
10771
|
// ③ 대조군 — 평범한 CLI 경로는 그대로 명령 표에 남는다(표시 조건을 바꾸며 죽이지 않았는가)
|
|
10629
10772
|
cp2.spawnSync(process.execPath, [__filename, 'audit', '--path', tgt], { cwd: cwdP, encoding: 'utf8', timeout: 180000, env: env2 });
|
|
10630
10773
|
const uT2 = U(tgt);
|
|
10631
|
-
const cliStillCounted = /\|\s*audit\s*\|/.test(uT2) && /
|
|
10632
|
-
return shows && cwdClean && cliStillCounted;
|
|
10774
|
+
const cliStillCounted = /\|\s*audit\s*\|/.test(uT2) && /leerness_task_add/.test(uT2);
|
|
10775
|
+
return readOnlyClean && shows && cwdClean && cliStillCounted;
|
|
10633
10776
|
} catch { return false; } finally { try { fs.rmSync(arena, { recursive: true, force: true }); } catch { /* 정리 실패는 판정과 무관 */ } }
|
|
10634
10777
|
}
|
|
10635
10778
|
// 1.36.103: --json 계약 회귀 가드 — "실패해도 stdout 은 항상 파싱 가능한 JSON 하나".
|
|
@@ -11331,10 +11474,12 @@ function commandsCmd(root) {
|
|
|
11331
11474
|
function releaseCadenceCmd(root) {
|
|
11332
11475
|
root = absRoot(root);
|
|
11333
11476
|
const rh = _computeRoundHistory(root);
|
|
11477
|
+
if (rh.gitHistoryState !== 'available') _throwGitHistoryUnavailable(rh.gitHistoryError);
|
|
11334
11478
|
const a = _cadenceAssessment(rh.avgRoundsPerDay, rh.roundCount, rh.daysActive);
|
|
11335
|
-
if (has('--json')) { log(JSON.stringify({ version: VERSION, currentVersion: rh.currentVersion, baselineVersion: rh.baselineVersion, ...a }, null, 2)); return; }
|
|
11479
|
+
if (has('--json')) { log(JSON.stringify({ version: VERSION, cliVersion: rh.cliVersion, harnessVersion: rh.harnessVersion, harnessVersionState: rh.harnessVersionState, currentVersion: rh.currentVersion, currentVersionScope: rh.currentVersionScope, baselineVersion: rh.baselineVersion, latestTagVersion: rh.latestTagVersion, gitHistoryState: rh.gitHistoryState, gitHistoryError: rh.gitHistoryError, ...a }, null, 2)); return; }
|
|
11336
11480
|
log(`# leerness release cadence (1.9.374, UR-0074) — 릴리스 빈도 진단 (읽기 전용)`);
|
|
11337
|
-
|
|
11481
|
+
const observedRate = a.dataSufficient ? `${a.releasesPerDay}/day` : '관측 불가 (서로 다른 시각의 태그 2개 이상 필요)';
|
|
11482
|
+
log(` 누적 릴리스: ${a.total} · 활동일: ${a.daysActive}일 · 빈도: ${observedRate}`);
|
|
11338
11483
|
log(` 수준: ${a.level}`);
|
|
11339
11484
|
log(` 권장: ${a.recommendation}`);
|
|
11340
11485
|
log(`\n 관련: leerness release channel (stable/next 정책) · leerness install-safety (공급망 신뢰)`);
|
|
@@ -11347,10 +11492,15 @@ function roundHistoryCmd(root) {
|
|
|
11347
11492
|
const yl = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
|
|
11348
11493
|
const dm = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
11349
11494
|
const data = _computeRoundHistory(root);
|
|
11495
|
+
if (data.gitHistoryState !== 'available') _throwGitHistoryUnavailable(data.gitHistoryError);
|
|
11350
11496
|
if (has('--json')) { log(JSON.stringify(data, null, 2)); return; }
|
|
11351
11497
|
log(cy(`# leerness round-history (1.9.226) — 자율 라운드 통계`));
|
|
11352
11498
|
log('');
|
|
11353
|
-
log(` 📦
|
|
11499
|
+
log(` 📦 leerness CLI: ${gr(data.cliVersion)}`);
|
|
11500
|
+
if (data.harnessVersion) log(` 🧩 프로젝트 하네스: ${gr(data.harnessVersion)}`);
|
|
11501
|
+
else if (data.harnessVersionState === 'invalid' || data.harnessVersionState === 'unreadable') log(yl(` 🧩 프로젝트 하네스: 판독 불가 (${data.harnessVersionState})`));
|
|
11502
|
+
else if (data.harnessVersionState === 'missing') log(yl(` 🧩 프로젝트 하네스: 없음 (missing)`));
|
|
11503
|
+
if (data.latestTagVersion) log(` 🏷️ 저장소 최근 태그: v${data.latestTagVersion}`);
|
|
11354
11504
|
log(` 🔄 누적 라운드: ${gr(String(data.roundCount))}`);
|
|
11355
11505
|
if (data.baselineVersion) log(` 📍 baseline: v${data.baselineVersion} (${(data.firstTagAt || '').split('T')[0]})`);
|
|
11356
11506
|
if (data.latestTagAt) log(` ⏰ 최근 tag: ${(data.latestTagAt || '').split('T')[0]}`);
|
|
@@ -11876,23 +12026,49 @@ function _isKnownTopLevelCommand(cmd) {
|
|
|
11876
12026
|
return _knownTopLevelCommands.has(String(cmd || ''));
|
|
11877
12027
|
}
|
|
11878
12028
|
|
|
12029
|
+
// Single authority for CLI routes that explicitly promise observation only.
|
|
12030
|
+
// The default is stateful: a newly added command cannot accidentally gain a
|
|
12031
|
+
// read-only exemption merely because a heuristic failed to recognize it.
|
|
12032
|
+
const _OBSERVATION_ONLY_COMMANDS = new Set([
|
|
12033
|
+
'about', 'identity', 'status', 'commands', 'install-safety', 'capabilities',
|
|
12034
|
+
'security-surface', 'dashboard', 'round-history', 'milestones', 'pulse',
|
|
12035
|
+
]);
|
|
12036
|
+
const _OBSERVATION_ONLY_SUBCOMMANDS = new Map([
|
|
12037
|
+
['release', new Set(['cadence'])],
|
|
12038
|
+
['auto-update', new Set(['status'])],
|
|
12039
|
+
['preview', new Set(['list', 'show'])],
|
|
12040
|
+
]);
|
|
12041
|
+
function _cliMutationClass(args, cmd) {
|
|
12042
|
+
if (cmd === 'mcp') return 'observation-only'; // server startup must not mutate its incidental cwd
|
|
12043
|
+
if (_OBSERVATION_ONLY_COMMANDS.has(cmd)) return 'observation-only';
|
|
12044
|
+
// `preview` with no explicit child is an exact alias for `preview list`.
|
|
12045
|
+
// Classify the alias before auto-migration/telemetry so adding --json/--path
|
|
12046
|
+
// cannot turn a read into a write merely because no child token exists.
|
|
12047
|
+
if (cmd === 'preview' && (!args || !args[1] || String(args[1]).startsWith('-'))) return 'observation-only';
|
|
12048
|
+
const children = _OBSERVATION_ONLY_SUBCOMMANDS.get(cmd);
|
|
12049
|
+
return children && args && children.has(args[1]) ? 'observation-only' : 'stateful';
|
|
12050
|
+
}
|
|
12051
|
+
function _mustRemainReadOnly(args, cmd) { return _cliMutationClass(args, cmd) === 'observation-only'; }
|
|
12052
|
+
|
|
11879
12053
|
function _autoMigrateLegacyWorkspace(args, cmd) {
|
|
11880
12054
|
if (process.env.LEERNESS_NO_AUTO_WORKSPACE_MIGRATION === '1') return true;
|
|
11881
12055
|
if (has('--dry-run') || cmd === 'migrate-workspace-dir' || cmd === 'workspace-dir') return true;
|
|
12056
|
+
if (_mustRemainReadOnly(args, cmd)) return true;
|
|
11882
12057
|
// Invalid commands must be observationally pure. In particular, a typo in
|
|
11883
12058
|
// a legacy project may not rename project memory before reporting the typo.
|
|
11884
12059
|
if (!_isKnownTopLevelCommand(cmd)) return true;
|
|
11885
12060
|
const candidates = [];
|
|
11886
12061
|
const explicit = arg('--path', null);
|
|
11887
12062
|
if (typeof explicit === 'string' && explicit.trim()) candidates.push(explicit);
|
|
11888
|
-
candidates.push(process.cwd());
|
|
12063
|
+
else candidates.push(process.cwd());
|
|
11889
12064
|
// Never interpret arbitrary command content as a project to mutate. Topics,
|
|
11890
12065
|
// titles and output paths may all be path-shaped (for example
|
|
11891
12066
|
// `brainstorm ./legacy-design`); scanning every positional token used to
|
|
11892
12067
|
// migrate those unrelated directories before command dispatch. Positional
|
|
11893
12068
|
// project targets can be migrated explicitly with `migrate-workspace-dir`,
|
|
11894
12069
|
// while normal automatic migration is limited to cwd and the unambiguous
|
|
11895
|
-
// global `--path` option.
|
|
12070
|
+
// global `--path` option. When it is present, the unrelated process cwd is
|
|
12071
|
+
// not another migration target.
|
|
11896
12072
|
const seen = new Set();
|
|
11897
12073
|
for (const candidate of candidates) {
|
|
11898
12074
|
let root;
|
|
@@ -13893,8 +14069,10 @@ function _handoffVersionSkew(root) {
|
|
|
13893
14069
|
const versionFile = path.join(absRoot(root), '.leerness', 'HARNESS_VERSION');
|
|
13894
14070
|
if (!exists(versionFile)) return null;
|
|
13895
14071
|
const parsed = parseHarnessVersion(read(versionFile));
|
|
13896
|
-
|
|
13897
|
-
|
|
14072
|
+
// `leerness@X+plus@Y`에서 X가 설치된 Leerness 버전이고 Y는 옛 compatibility suffix다.
|
|
14073
|
+
// round-history/status와 같은 출처를 사용해야 skew 방향이 뒤집히지 않는다.
|
|
14074
|
+
const harnessVersion = parsed.base;
|
|
14075
|
+
if (!harnessVersion) return { detected: true, kind: 'invalid-harness-version', cliVersion: VERSION, harnessVersion: _lineSafe(parsed.raw) };
|
|
13898
14076
|
const order = compareVer(VERSION, harnessVersion);
|
|
13899
14077
|
if (order === 0) return null;
|
|
13900
14078
|
return {
|
|
@@ -13909,11 +14087,11 @@ function _handoffVersionSkew(root) {
|
|
|
13909
14087
|
function _emitHandoffVersionSkew(skew) {
|
|
13910
14088
|
if (!skew) return;
|
|
13911
14089
|
if (skew.kind === 'cli-older') {
|
|
13912
|
-
warn(`버전 불일치: 실행 CLI v${skew.cliVersion} < 프로젝트 하네스 v${skew.harnessVersion} — 쓰기 명령 전 CLI를 업데이트하세요`);
|
|
14090
|
+
warn(`버전 불일치: 실행 CLI v${_lineSafe(skew.cliVersion)} < 프로젝트 하네스 v${_lineSafe(skew.harnessVersion)} — 쓰기 명령 전 CLI를 업데이트하세요`);
|
|
13913
14091
|
} else if (skew.kind === 'harness-older') {
|
|
13914
|
-
warn(`버전 불일치: 실행 CLI v${skew.cliVersion} > 프로젝트 하네스 v${skew.harnessVersion} — leerness update --yes 권장`);
|
|
14092
|
+
warn(`버전 불일치: 실행 CLI v${_lineSafe(skew.cliVersion)} > 프로젝트 하네스 v${_lineSafe(skew.harnessVersion)} — leerness update --yes 권장`);
|
|
13915
14093
|
} else {
|
|
13916
|
-
warn(`프로젝트 HARNESS_VERSION 을 해석할 수 없습니다: ${skew.harnessVersion}`);
|
|
14094
|
+
warn(`프로젝트 HARNESS_VERSION 을 해석할 수 없습니다: ${_lineSafe(skew.harnessVersion)}`);
|
|
13917
14095
|
}
|
|
13918
14096
|
}
|
|
13919
14097
|
// 1.36.7 (메타-테스트 도그푸드): 세션에서 작업 중(task/decision add)인데 handoff 가 오래됨/미실행이면 넛지.
|
|
@@ -15785,21 +15963,46 @@ function route(name) {
|
|
|
15785
15963
|
|
|
15786
15964
|
function status(root) {
|
|
15787
15965
|
root = absRoot(root);
|
|
15788
|
-
const _L = _uiLang(root); const t = (ko, en) => (_L === 'en' ? en : ko); // 1.23.1 (UR-0010 Phase 6)
|
|
15789
15966
|
// 1.9.434 (11th 외부평가 Opus P2, UR-0136): 미존재 경로는 healthy 위조 금지 — failJson + exit 1.
|
|
15790
|
-
if (!exists(root)) {
|
|
15791
|
-
|
|
15792
|
-
|
|
15793
|
-
|
|
15967
|
+
if (!exists(root)) {
|
|
15968
|
+
const missingLang = _uiLang(root);
|
|
15969
|
+
failJson(has('--json'), 'path_not_found', missingLang === 'en' ? `path not found: ${root}` : `경로 없음: ${root}`);
|
|
15970
|
+
return;
|
|
15971
|
+
}
|
|
15972
|
+
// Resolve the active workspace exactly once. A live legacy installation is
|
|
15973
|
+
// observable in place; status must neither migrate it nor report canonical
|
|
15974
|
+
// paths as missing. Conflict/override errors remain fail-closed in main().
|
|
15975
|
+
const workspaceDir = _workspaceDirName(root);
|
|
15976
|
+
const workspaceAbs = path.join(root, workspaceDir);
|
|
15977
|
+
const verF = path.join(workspaceAbs, 'HARNESS_VERSION');
|
|
15978
|
+
let ver = null;
|
|
15979
|
+
let versionState = 'missing';
|
|
15980
|
+
if (exists(verF)) {
|
|
15981
|
+
try {
|
|
15982
|
+
const parsed = parseHarnessVersion(read(verF));
|
|
15983
|
+
ver = parsed.base || null;
|
|
15984
|
+
versionState = ver ? 'valid' : 'invalid';
|
|
15985
|
+
} catch { versionState = 'unreadable'; }
|
|
15986
|
+
}
|
|
15987
|
+
const langF = path.join(workspaceAbs, 'LANGUAGE');
|
|
15988
|
+
const lang = exists(langF) ? read(langF).trim() : 'ko';
|
|
15989
|
+
const requestedLang = String(arg('--language', '') || process.env.LEERNESS_LANG || '').toLowerCase();
|
|
15990
|
+
const _L = requestedLang === 'en' || requestedLang === 'ko' ? requestedLang : (lang === 'en' ? 'en' : 'ko');
|
|
15991
|
+
const t = (ko, en) => (_L === 'en' ? en : ko); // 1.23.1 (UR-0010 Phase 6)
|
|
15794
15992
|
// 1.9.287 (Codex 리뷰 수렴): --minimal 설치는 manifest.minimal=true → 의도적으로 생략된 파일을 missing 으로 경고하지 않음.
|
|
15795
15993
|
let isMinimal = false;
|
|
15796
|
-
try { const mf = path.join(
|
|
15797
|
-
const
|
|
15798
|
-
const
|
|
15994
|
+
try { const mf = path.join(workspaceAbs, 'manifest.json'); if (exists(mf)) isMinimal = !!JSON.parse(read(mf)).minimal; } catch {}
|
|
15995
|
+
const canonicalFiles = Object.keys(coreFiles(root, lang, [], { minimal: isMinimal }));
|
|
15996
|
+
const files = canonicalFiles.map(f => f === CANONICAL_WORKSPACE_DIR
|
|
15997
|
+
? workspaceDir
|
|
15998
|
+
: (f.startsWith(CANONICAL_WORKSPACE_DIR + '/')
|
|
15999
|
+
? workspaceDir + f.slice(CANONICAL_WORKSPACE_DIR.length)
|
|
16000
|
+
: f));
|
|
16001
|
+
const missing = files.filter(f => !exists(path.join(root, f)));
|
|
15799
16002
|
// 1.9.384 (5번째 외부평가/UR-0085): --json 일관성 — AI 에이전트용 구조화 출력.
|
|
15800
16003
|
// 1.9.418 (9th 외부평가 Codex P2): healthy 의 의미를 명시(설치 파일 존재 ≠ 프로젝트 안전). 프로젝트 안전은 gate/scan secrets 사용.
|
|
15801
|
-
if (has('--json')) { log(JSON.stringify({ version: ver, language: lang, minimal: isMinimal, scope: 'install', total: files.length, present: files.length - missing.length, missing, healthy: missing.length === 0, healthyMeaning: t('설치 파일 존재 여부(프로젝트 안전 아님 — 보안/품질은 leerness gate / scan secrets 사용)', 'install-file presence only (not project safety — use leerness gate / scan secrets for security/quality)') }, null, 2)); return; }
|
|
15802
|
-
log(`Leerness: ${ver}${isMinimal ? ' (minimal)' : ''}`);
|
|
16004
|
+
if (has('--json')) { log(JSON.stringify({ version: ver, versionState, workspaceDir, language: lang, minimal: isMinimal, scope: 'install', total: files.length, present: files.length - missing.length, missing, healthy: missing.length === 0, healthyMeaning: t('설치 파일 존재 여부(프로젝트 안전 아님 — 보안/품질은 leerness gate / scan secrets 사용)', 'install-file presence only (not project safety — use leerness gate / scan secrets for security/quality)') }, null, 2)); return; }
|
|
16005
|
+
log(`Leerness: ${ver || versionState}${isMinimal ? ' (minimal)' : ''}`);
|
|
15803
16006
|
log(`Files: ${files.length - missing.length}/${files.length}`);
|
|
15804
16007
|
if (missing.length) missing.forEach(x => warn('missing: ' + x));
|
|
15805
16008
|
else ok(`required files present${isMinimal ? ' (minimal set)' : ''}`);
|
|
@@ -16620,30 +16823,33 @@ function handoff(root) {
|
|
|
16620
16823
|
} catch {}
|
|
16621
16824
|
// 1.9.227: roundHistory 통합 (handoff JSON 6번째 통합 필드) — 자율 진행도 외부 회수
|
|
16622
16825
|
try {
|
|
16623
|
-
const rh = _computeRoundHistory(root);
|
|
16826
|
+
const rh = _computeRoundHistory(root, { allowWorkspaceError: true });
|
|
16624
16827
|
result.roundHistory = {
|
|
16625
16828
|
roundCount: rh.roundCount,
|
|
16626
16829
|
baselineVersion: rh.baselineVersion,
|
|
16627
16830
|
nextMilestone: rh.nextMilestone,
|
|
16628
16831
|
roundsToNextMilestone: rh.roundsToNextMilestone,
|
|
16629
16832
|
daysActive: rh.daysActive,
|
|
16630
|
-
avgRoundsPerDay: rh.avgRoundsPerDay
|
|
16833
|
+
avgRoundsPerDay: rh.avgRoundsPerDay,
|
|
16834
|
+
harnessVersionState: rh.harnessVersionState,
|
|
16835
|
+
workspaceError: rh.workspaceError || null,
|
|
16836
|
+
gitHistoryState: rh.gitHistoryState,
|
|
16837
|
+
gitHistoryError: rh.gitHistoryError
|
|
16631
16838
|
};
|
|
16632
16839
|
} catch {}
|
|
16633
16840
|
// 1.9.230: milestones 통합 (handoff JSON 7번째 통합 필드) — 도달 마일스톤 + ETA
|
|
16634
16841
|
try {
|
|
16635
16842
|
const ms = _computeMilestones(root);
|
|
16636
|
-
result.milestones =
|
|
16637
|
-
|
|
16638
|
-
|
|
16639
|
-
|
|
16640
|
-
avgRoundsPerDay: ms.avgRoundsPerDay
|
|
16641
|
-
};
|
|
16642
|
-
} catch {}
|
|
16843
|
+
result.milestones = _milestoneSurface(ms);
|
|
16844
|
+
} catch (err) {
|
|
16845
|
+
result.milestones = _milestoneSurface({ gitHistoryState: 'unavailable', gitHistoryError: _gitHistoryFailure('aggregate_exception', err && err.message) });
|
|
16846
|
+
}
|
|
16643
16847
|
// 1.9.234: recentChanges 통합 (handoff JSON 8번째 통합 필드) — 최근 5 라운드 변경 요약
|
|
16644
16848
|
try {
|
|
16645
|
-
result
|
|
16646
|
-
} catch {
|
|
16849
|
+
Object.assign(result, _recentChangesAggregate(_computeRecentChanges(root, 5)));
|
|
16850
|
+
} catch (err) {
|
|
16851
|
+
Object.assign(result, _recentChangesAggregate({ state: 'unavailable', error: _gitHistoryFailure('aggregate_exception', err && err.message) }));
|
|
16852
|
+
}
|
|
16647
16853
|
// 1.9.240: pyFiles 통합 (handoff JSON 9번째 통합 필드) — Python 다중 언어 가시화 (UR-0013 2단계)
|
|
16648
16854
|
try {
|
|
16649
16855
|
const pyFiles = _collectPyFiles(root, 200);
|
|
@@ -16751,6 +16957,10 @@ function handoff(root) {
|
|
|
16751
16957
|
// 채널(메일박스)을 새로 만들기 전에, 이미 있는 신호가 도달하게 하는 것이 먼저다.
|
|
16752
16958
|
try { result.sessions = _sessionSignal(root); } catch { /* 레지스트리를 못 읽어도 handoff 는 진행 */ }
|
|
16753
16959
|
try { const _rem = _agentReminderSignal(root); if (_rem) result.agentReminders = _rem; } catch {}
|
|
16960
|
+
// A corrupt preview workflow is not equivalent to "0 pending". Machine
|
|
16961
|
+
// consumers must keep the approval gate unknown until previews.json is
|
|
16962
|
+
// repaired, rather than silently treating malformed/missing statuses as done.
|
|
16963
|
+
result.previewApproval = _previewApprovalSurface(root);
|
|
16754
16964
|
log(JSON.stringify(result, null, 2));
|
|
16755
16965
|
return;
|
|
16756
16966
|
}
|
|
@@ -16780,6 +16990,11 @@ function handoff(root) {
|
|
|
16780
16990
|
// 이 경로가 MCP handoff·REPL·agent-mode 가 실제로 쓰는 경로이고, 종전엔 여기서 헤드라인이 통째로 사라졌다(200줄→3줄).
|
|
16781
16991
|
try { const _ss = _sessionSignal(root); flags.push(..._sessionSignalFlags(_ss)); } catch {}
|
|
16782
16992
|
try { const _rm = _agentReminderSignal(root); if (_rm) flags.push(`🔔 리마인더 ${_rm.lines}줄`); } catch {}
|
|
16993
|
+
// 1.36.175 (T-0095): MCP handoff 가 바로 이 compact 경로를 사용한다. 승인 저장소가
|
|
16994
|
+
// 손상됐는데도 아무 표시가 없으면 에이전트가 `미승인 0`으로 오독하므로 known/count/code를
|
|
16995
|
+
// compact 텍스트에도 명시한다. 0건도 known=true로 써서 "침묵=확인됨" 추론을 없앤다.
|
|
16996
|
+
const _pa = _previewApprovalSurface(root, false);
|
|
16997
|
+
flags.push(`${_pa.known ? '🎨' : '⚠'} ${_previewApprovalText(_pa)}`);
|
|
16783
16998
|
log(_lineSafe(`leerness compact: ${detectProjectName(root)} · ${done}/${rows.length}(${pct}%) done · WIP ${wip} · T${wip}/D${decN}/R${rulesActive}/P${mileN}/L${lessN}${flags.length ? ' · ' + flags.join(' · ') : ''}`)); // 1.36.18 (codex F4): projectName 개행 주입 차단
|
|
16784
16999
|
if (nx) log(_lineSafe(`다음: ${nx.id} [${nx.status}] ${nx.nextAction || nx.request || ''}`).slice(0, 200));
|
|
16785
17000
|
log('핵심 규칙: 의존성0 · 한국어주석 · UTF-8noBOM · reuse-map등록 · anti-lazy-work · verify-claim자동검수');
|
|
@@ -17030,8 +17245,12 @@ function handoff(root) {
|
|
|
17030
17245
|
// 17) 1.9.226: round-history 헤드라인 — 자율 라운드 카운터 + 다음 마일스톤 (5 라운드 이상 시만 표시)
|
|
17031
17246
|
// 1.9.230: 다음 마일스톤 ETA (10R 이내 + ETA 7일 이내일 때 강조)
|
|
17032
17247
|
try {
|
|
17033
|
-
const rh = _computeRoundHistory(root);
|
|
17034
|
-
if (rh.
|
|
17248
|
+
const rh = _computeRoundHistory(root, { allowWorkspaceError: true });
|
|
17249
|
+
if (rh.gitHistoryError) {
|
|
17250
|
+
parts.push(`⚠ Git history unavailable:${rh.gitHistoryError.reason}`);
|
|
17251
|
+
} else if (rh.workspaceError) {
|
|
17252
|
+
parts.push(`⚠ workspace provenance unavailable:${rh.workspaceError.code}`);
|
|
17253
|
+
} else if (rh.roundCount >= 5) {
|
|
17035
17254
|
let label = `🔄 R${rh.roundCount}`;
|
|
17036
17255
|
if (rh.nextMilestone != null && rh.roundsToNextMilestone <= 20) {
|
|
17037
17256
|
label += ` → R${rh.nextMilestone} (${rh.roundsToNextMilestone}R ${t('남음', 'left')})`;
|
|
@@ -17088,9 +17307,15 @@ function handoff(root) {
|
|
|
17088
17307
|
} catch {}
|
|
17089
17308
|
// 1.36.51 (UR-0061): 미승인 미리보기 — 사용자 답을 기다리는 기능이 있으면 헤드라인 노출 (코드 작성 보류 계약)
|
|
17090
17309
|
try {
|
|
17091
|
-
const
|
|
17092
|
-
if (
|
|
17093
|
-
|
|
17310
|
+
const _pa = _previewApprovalSurface(root, false);
|
|
17311
|
+
if (!_pa.known) {
|
|
17312
|
+
parts.push(t(`⚠ 미리보기 승인 상태 불명 (${_pa.code}) — 복구 전 승인 완료로 간주 금지`, `⚠ preview approval unknown (${_pa.code}) — do not treat as approved until repaired`));
|
|
17313
|
+
} else if (_pa.pendingCount) {
|
|
17314
|
+
parts.push(t(`🎨 미승인 미리보기 ${_pa.pendingCount}건 (approve 전 코드 금지)`, `🎨 ${_pa.pendingCount} preview(s) awaiting approval (no code before approve)`));
|
|
17315
|
+
}
|
|
17316
|
+
} catch (e) {
|
|
17317
|
+
parts.push(t('⚠ 미리보기 승인 상태 확인 실패 — 복구 전 승인 완료로 간주 금지', '⚠ preview approval check failed — do not treat as approved until repaired'));
|
|
17318
|
+
}
|
|
17094
17319
|
// 14) 1.9.209: pre-wake-audit 최근 보고서 (사용자 명시) — 깨어남 직후 자동 노출
|
|
17095
17320
|
try {
|
|
17096
17321
|
const pwState = _loadPreWakeReport(root);
|
|
@@ -18166,6 +18391,48 @@ function _parseSince(s) {
|
|
|
18166
18391
|
return cutoff.toISOString().slice(0, 10); // YYYY-MM-DD
|
|
18167
18392
|
}
|
|
18168
18393
|
|
|
18394
|
+
// 승인 상태는 단일/멀티 handoff와 MCP가 같은 필드 계약을 공유한다. `known=true`일 때도
|
|
18395
|
+
// code=null을 생략하지 않아 compact 텍스트를 파싱하는 에이전트가 침묵을 추론하지 않게 한다.
|
|
18396
|
+
function _previewApprovalSurface(root, includeItems = true) {
|
|
18397
|
+
try {
|
|
18398
|
+
const state = _clar.previewApprovalState(root);
|
|
18399
|
+
return {
|
|
18400
|
+
known: !!state.known,
|
|
18401
|
+
pendingCount: state.known ? state.count : null,
|
|
18402
|
+
code: state.known ? null : _lineSafe(String(state.code || 'approval_state_unavailable')),
|
|
18403
|
+
items: includeItems && state.known
|
|
18404
|
+
? state.items.slice(0, 10).map(p => ({ id: p.id, title: p.title, status: p.status }))
|
|
18405
|
+
: [],
|
|
18406
|
+
};
|
|
18407
|
+
} catch (e) {
|
|
18408
|
+
return {
|
|
18409
|
+
known: false,
|
|
18410
|
+
pendingCount: null,
|
|
18411
|
+
code: _lineSafe(String((e && e.code) || 'approval_state_unavailable')),
|
|
18412
|
+
items: [],
|
|
18413
|
+
};
|
|
18414
|
+
}
|
|
18415
|
+
}
|
|
18416
|
+
|
|
18417
|
+
function _previewApprovalText(state) {
|
|
18418
|
+
return state.known
|
|
18419
|
+
? `previewApproval known=true count=${state.pendingCount} code=null`
|
|
18420
|
+
: `previewApproval known=false count=null code=${state.code || 'approval_state_unavailable'}`;
|
|
18421
|
+
}
|
|
18422
|
+
|
|
18423
|
+
function _workspacePreviewApproval(projects) {
|
|
18424
|
+
const unknown = projects.filter(p => !p.previewApproval.known);
|
|
18425
|
+
const codes = Array.from(new Set(unknown.map(p => p.previewApproval.code || 'approval_state_unavailable')));
|
|
18426
|
+
return {
|
|
18427
|
+
known: unknown.length === 0,
|
|
18428
|
+
pendingCount: unknown.length === 0
|
|
18429
|
+
? projects.reduce((sum, p) => sum + (p.previewApproval.pendingCount || 0), 0)
|
|
18430
|
+
: null,
|
|
18431
|
+
code: unknown.length === 0 ? null : (codes.length === 1 ? codes[0] : 'multiple_approval_state_errors'),
|
|
18432
|
+
unknownProjects: unknown.map(p => ({ project: p.project, path: p.path, code: p.previewApproval.code })),
|
|
18433
|
+
};
|
|
18434
|
+
}
|
|
18435
|
+
|
|
18169
18436
|
// 1.9.17→1.9.18: 워크스페이스 통합 handoff — 4개 agent 동시 작업 시 메인 agent용 한 줄 요약
|
|
18170
18437
|
// 1.9.18: --since <duration> 추가, 최근 수정된 T-row 강조 (🆕 마크 + 별도 섹션)
|
|
18171
18438
|
function _handoffWorkspace(rootBase) {
|
|
@@ -18197,10 +18464,11 @@ function _handoffWorkspace(rootBase) {
|
|
|
18197
18464
|
blocked: (buckets['blocked'] || []).length,
|
|
18198
18465
|
activeRules,
|
|
18199
18466
|
nextAction: (buckets['in-progress']?.[0]?.nextAction) || (buckets['planned']?.[0]?.nextAction) || (buckets['requested']?.[0]?.nextAction) || null,
|
|
18467
|
+
previewApproval: _previewApprovalSurface(p),
|
|
18200
18468
|
recent: recent.map(r => ({ id: r.id, status: r.status, request: r.request, updated: r.updated }))
|
|
18201
18469
|
};
|
|
18202
18470
|
});
|
|
18203
|
-
log(JSON.stringify({ workspace: path.basename(rootBase), since: sinceCutoff, projects, totals: {
|
|
18471
|
+
log(JSON.stringify({ workspace: path.basename(rootBase), since: sinceCutoff, previewApproval: _workspacePreviewApproval(projects), projects, totals: {
|
|
18204
18472
|
tasks: projects.reduce((a, b) => a + b.total, 0),
|
|
18205
18473
|
done: projects.reduce((a, b) => a + b.done, 0),
|
|
18206
18474
|
inProgress: projects.reduce((a, b) => a + b.inProgress, 0),
|
|
@@ -18213,6 +18481,7 @@ function _handoffWorkspace(rootBase) {
|
|
|
18213
18481
|
if (has('--compact')) {
|
|
18214
18482
|
let totalDone = 0, totalTasks = 0, totalWIP = 0, totalRecent = 0;
|
|
18215
18483
|
const projSummaries = [];
|
|
18484
|
+
const projectApprovals = [];
|
|
18216
18485
|
for (const p of paths) {
|
|
18217
18486
|
const rows = readProgressRows(p);
|
|
18218
18487
|
const buckets = {};
|
|
@@ -18222,9 +18491,12 @@ function _handoffWorkspace(rootBase) {
|
|
|
18222
18491
|
const recent = sinceCutoff ? rows.filter(isRecent).length : 0;
|
|
18223
18492
|
totalDone += done; totalTasks += rows.length; totalWIP += wip; totalRecent += recent;
|
|
18224
18493
|
const pct = rows.length ? Math.round(done / rows.length * 100) : 0;
|
|
18225
|
-
|
|
18494
|
+
const previewApproval = _previewApprovalSurface(p, false);
|
|
18495
|
+
projectApprovals.push({ project: path.basename(p), path: p, previewApproval });
|
|
18496
|
+
projSummaries.push(`${path.basename(p)} ${done}/${rows.length}(${pct}%) · ${_previewApprovalText(previewApproval)}`);
|
|
18226
18497
|
}
|
|
18227
|
-
|
|
18498
|
+
const workspaceApproval = _workspacePreviewApproval(projectApprovals);
|
|
18499
|
+
log(`leerness compact (1.9.22): ${paths.length}프로젝트 · ${totalDone}/${totalTasks}(${totalTasks?Math.round(totalDone/totalTasks*100):0}%) done · WIP ${totalWIP}${sinceCutoff?` · 🆕${totalRecent}`:''} · ${_previewApprovalText(workspaceApproval)}`);
|
|
18228
18500
|
log(`projects: ${projSummaries.join(' | ')}`);
|
|
18229
18501
|
log(`핵심 규칙: 의존성0 · 한국어주석 · UTF-8noBOM · reuse-map등록 · anti-lazy-work · verify-claim자동검수`);
|
|
18230
18502
|
return;
|
|
@@ -18236,6 +18508,7 @@ function _handoffWorkspace(rootBase) {
|
|
|
18236
18508
|
log('## 프로젝트별 진행 상태');
|
|
18237
18509
|
let totalDone = 0, totalTasks = 0, totalWIP = 0, totalBlocked = 0, totalRecent = 0;
|
|
18238
18510
|
const allRecent = [];
|
|
18511
|
+
const projectApprovals = [];
|
|
18239
18512
|
for (const p of paths) {
|
|
18240
18513
|
const rows = readProgressRows(p);
|
|
18241
18514
|
const buckets = {};
|
|
@@ -18250,7 +18523,9 @@ function _handoffWorkspace(rootBase) {
|
|
|
18250
18523
|
const nx = (buckets['in-progress']?.[0]) || (buckets['planned']?.[0]) || null;
|
|
18251
18524
|
const pct = rows.length ? Math.round(done / rows.length * 100) : 0;
|
|
18252
18525
|
const recentBadge = recent.length ? ` · 🆕 ${recent.length}` : '';
|
|
18253
|
-
|
|
18526
|
+
const previewApproval = _previewApprovalSurface(p, false);
|
|
18527
|
+
projectApprovals.push({ project: path.basename(p), path: p, previewApproval });
|
|
18528
|
+
log(` ${path.basename(p)}: ${done}/${rows.length} (${pct}%) · WIP ${wip} · planned ${planned}${blocked ? ` · 🚫 blocked ${blocked}` : ''}${recentBadge} · ${_previewApprovalText(previewApproval)}`);
|
|
18254
18529
|
if (nx) log(` └ 다음: ${nx.id} [${nx.status}] ${nx.nextAction || nx.request}`);
|
|
18255
18530
|
}
|
|
18256
18531
|
// 1.9.18: --since 모드일 때 최근 추가/수정 섹션
|
|
@@ -18266,6 +18541,7 @@ function _handoffWorkspace(rootBase) {
|
|
|
18266
18541
|
log(`## 📊 워크스페이스 총합`);
|
|
18267
18542
|
log(` - 누적 task: ${totalTasks} (done ${totalDone}, ${totalTasks ? Math.round(totalDone / totalTasks * 100) : 0}%)`);
|
|
18268
18543
|
log(` - 진행중 (WIP): ${totalWIP} · 차단: ${totalBlocked}${sinceCutoff ? ` · 🆕 최근 ${totalRecent}` : ''}`);
|
|
18544
|
+
log(` - 미리보기 승인: ${_previewApprovalText(_workspacePreviewApproval(projectApprovals))}`);
|
|
18269
18545
|
if (totalBlocked > 0) log(` - ⚠ ${totalBlocked}건이 blocked — 우선 처리 검토`);
|
|
18270
18546
|
log('');
|
|
18271
18547
|
log(`## 💡 멀티에이전트 오케스트레이션 권장`);
|
|
@@ -21539,7 +21815,7 @@ function sessionClose(root, opts = {}) { // 1.36.132 (검수): handoff 는 --no-
|
|
|
21539
21815
|
// (실측: handoff --no-record → 0건, session close --no-record → 1건). 같은 스토어의 같은 계약은 같은 문장으로.
|
|
21540
21816
|
_captureDirectMcpPresenceMarker(); // hook/--no-record여도 nested skill/drift child에는 일회성 MCP marker를 물려주지 않는다
|
|
21541
21817
|
if (!has('--no-record') && process.env.LEERNESS_HOOK !== '1') { try { _sessionPresenceRecord(root, 'close'); } catch {} } // 1.36.129 (P-0016 P1): close 는 '기록됨' 일 뿐 '프로세스가 죽었다' 가 아니다
|
|
21542
|
-
return _sessionClose.sessionClose(root, opts, { VERSION, STATUSES, MARK, has, arg, uiLang: _uiLang(root), harnessPath: __filename, readProgressRows, evidencePath, handoffPath, currentStatePath, taskLogPath, verifyRules, _autoRoadmap, _readUsageStats, readSessionCounter, writeSessionCounter, _retroAggregate, _retroOneLine, retroCmd, _loadDecisions, readRules, planPath, _loadLessons, _readFeatureGraph, _auditUserRequests, _detectDeliveredRequests, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _scanShellScriptsEncoding, _listAPISkills, _matchAPISkills, _loadShellFailures, _shellEnvDrift, _runPreWakeAudit, _saveAndAppendPreWakeReport, _runIdempotencyAudit, _detectAbnormalShutdown, _updateUserRequest, _detectOptimism, _scanCodeForPatterns, _collectSecretFindings, _withLock }); } // 1.17.6 (UR-0049): 마감 정합 — done 낙관 재확인 + 시크릿 재확인 · 1.36.108: 락 주입(T-0097)
|
|
21818
|
+
return _sessionClose.sessionClose(root, opts, { VERSION, STATUSES, MARK, has, arg, uiLang: _uiLang(root), harnessPath: __filename, readProgressRows, evidencePath, handoffPath, currentStatePath, taskLogPath, verifyRules, _autoRoadmap, _readUsageStats, readSessionCounter, writeSessionCounter, _retroAggregate, _retroOneLine, retroCmd, _loadDecisions, readRules, planPath, _loadLessons, _readFeatureGraph, _auditUserRequests, _detectDeliveredRequests, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _scanShellScriptsEncoding, _listAPISkills, _matchAPISkills, _loadShellFailures, _shellEnvDrift, _runPreWakeAudit, _saveAndAppendPreWakeReport, _runIdempotencyAudit, _detectAbnormalShutdown, _updateUserRequest, _detectOptimism, _scanCodeForPatterns, _collectSecretFindings, previewApprovalState: _clar.previewApprovalState, _withLock }); } // 1.17.6 (UR-0049): 마감 정합 — done 낙관 재확인 + 시크릿 재확인 · 1.36.108: 락 주입(T-0097)
|
|
21543
21819
|
|
|
21544
21820
|
function readmeCmd(root) { syncReadme(absRoot(root)); }
|
|
21545
21821
|
function consistencyCheck(root) {
|
|
@@ -26170,7 +26446,10 @@ function mcpServeCmd(root) {
|
|
|
26170
26446
|
try {
|
|
26171
26447
|
cliArgs = _mcpToCliArgs(name, args, targetPath);
|
|
26172
26448
|
if (cliArgs === null) return send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } });
|
|
26173
|
-
|
|
26449
|
+
const _mcpReadOnly = !!(_toolDef && _toolDef.requiredTier === 'read-only');
|
|
26450
|
+
// A read-only tool may not mutate the target merely to count itself.
|
|
26451
|
+
// Writable tools retain existing per-project MCP telemetry.
|
|
26452
|
+
if (!_mcpReadOnly) { try { _bumpMcpUsage(targetPath, name); } catch {} }
|
|
26174
26453
|
// 1.9.288 (Codex gpt-5.5 리뷰 #1 수렴): MCP 도구도 policy enforce 적용 — read-only enforce 시 write 도구 차단.
|
|
26175
26454
|
// 이전: _policyEnforce 는 agents multi --execute 한 곳뿐 → MCP state_start 등이 정책 우회하고 .leerness 기록.
|
|
26176
26455
|
// cliArgs(실제 실행 명령) 로 required tier 판정 → enforce ON 이고 초과 시 JSON-RPC error 반환(실행 안 함).
|
|
@@ -26186,8 +26465,10 @@ function mcpServeCmd(root) {
|
|
|
26186
26465
|
// 실제로 받은 **직접 handoff/close**에만 붙인다. state/agent-mode/일반 내부 스폰은
|
|
26187
26466
|
// presence 억제 예외가 아니며, sessionKey 자체는 상태 귀속용 env로만 전달한다.
|
|
26188
26467
|
const _lifecycle = name === 'leerness_handoff' || name === 'leerness_session_close';
|
|
26189
|
-
const _mcpEnv =
|
|
26190
|
-
if (
|
|
26468
|
+
const _mcpEnv = {};
|
|
26469
|
+
if (_sk) _mcpEnv.LEERNESS_SESSION_ID = _sk;
|
|
26470
|
+
if (_sk && _lifecycle) _mcpEnv.LEERNESS_MCP_ADDRESS_EXPLICIT = '1';
|
|
26471
|
+
if (_mcpReadOnly) _mcpEnv.LEERNESS_NO_AUTO_WORKSPACE_MIGRATION = '1';
|
|
26191
26472
|
const r = callLeerness(cliArgs, _mcpEnv);
|
|
26192
26473
|
// 1.9.61: cursor 기반 페이지네이션 — 긴 출력은 cursor offset로 다음 청크
|
|
26193
26474
|
const fullText = r.stdout || r.stderr || '(no output)';
|
|
@@ -30620,7 +30901,7 @@ async function main() {
|
|
|
30620
30901
|
// (scan secrets 처럼 '파일 경로' 가 정상 입력인 명령도 있어, 디렉토리 강제는 그 명령들엔 맞지 않는다.)
|
|
30621
30902
|
const _strict = new Set(['pulse', 'round-history', 'milestones', 'session-resume',
|
|
30622
30903
|
'tech', 'library', 'retro', 'insights', 'reuse-map', 'py-check', 'pre-wake-audit']);
|
|
30623
|
-
const _strictPair = { memory: 'status', consistency: 'check', env: 'check', idempotency: 'audit', 'api-skill': 'list' };
|
|
30904
|
+
const _strictPair = { memory: 'status', consistency: 'check', env: 'check', idempotency: 'audit', 'api-skill': 'list', release: 'cadence' };
|
|
30624
30905
|
const _isStrict = _strict.has(cmd) || (_strictPair[cmd] && args[1] === _strictPair[cmd]);
|
|
30625
30906
|
if (_isStrict) {
|
|
30626
30907
|
// 사용자가 '경로를 명시했을 때만' 검사한다 — 미지정(cwd)은 항상 존재하므로 검사할 것이 없다.
|
|
@@ -30634,7 +30915,9 @@ async function main() {
|
|
|
30634
30915
|
// (좁히다가 false-BLOCK 을 false-PASS 로 바꾸면 1.36.101 이 잡던 바로 그 증상이 되돌아온다).
|
|
30635
30916
|
const _SUBS = { library: new Set(['page']) };
|
|
30636
30917
|
const _looksPath = (a) => /^([A-Za-z]:[\\/]|\/|\.\.?[\\/])/.test(a);
|
|
30637
|
-
|
|
30918
|
+
// Paired commands own args[1] as their subcommand. Keep args[2] as a
|
|
30919
|
+
// positional path even when its literal text happens to equal that verb.
|
|
30920
|
+
let _cands = (_strictPair[cmd] && args[1] === _strictPair[cmd]) ? [args[2]] : [args[1], args[2]];
|
|
30638
30921
|
if (_SUBS[cmd] && args[1] && !args[1].startsWith('-')) {
|
|
30639
30922
|
if (_SUBS[cmd].has(args[1])) _cands = [null, args[2]]; // 하위명령 — 경로 아님
|
|
30640
30923
|
// codex 검수 #4 는 "존재하는 디렉토리면 맨이름도 경로로 받아 달라" 였다 — 받지 않는다.
|
|
@@ -30649,13 +30932,13 @@ async function main() {
|
|
|
30649
30932
|
return;
|
|
30650
30933
|
}
|
|
30651
30934
|
}
|
|
30652
|
-
const _explicit = arg('--path', null) || _cands.find(a => a && !a.startsWith('-')
|
|
30935
|
+
const _explicit = arg('--path', null) || _cands.find(a => a && !a.startsWith('-'));
|
|
30653
30936
|
if (_explicit) {
|
|
30654
30937
|
const _abs = absRoot(_explicit);
|
|
30655
30938
|
if (!exists(_abs) || !fs.statSync(_abs).isDirectory()) {
|
|
30656
30939
|
// 로케일을 붙인다 — en 프로젝트에서 이 가드만 한국어를 내면 그 자체가 i18n 누수다.
|
|
30657
30940
|
const _en = _uiLang(_abs) === 'en' || _uiLang(process.cwd()) === 'en';
|
|
30658
|
-
failJson(has('--json'), 'path_not_found', _en ? `path not found or not a directory: ${_abs}` : `경로 없음 또는 디렉토리 아님: ${_abs}`);
|
|
30941
|
+
failJson(has('--json'), 'path_not_found', _lineSafe(_en ? `path not found or not a directory: ${_abs}` : `경로 없음 또는 디렉토리 아님: ${_abs}`));
|
|
30659
30942
|
return;
|
|
30660
30943
|
}
|
|
30661
30944
|
}
|
|
@@ -30685,9 +30968,9 @@ async function main() {
|
|
|
30685
30968
|
const _abs = absRoot(_p);
|
|
30686
30969
|
if (!exists(_abs)) {
|
|
30687
30970
|
const _en = _uiLang(process.cwd()) === 'en';
|
|
30688
|
-
failJson(has('--json'), 'path_not_found', _en
|
|
30971
|
+
failJson(has('--json'), 'path_not_found', _lineSafe(_en
|
|
30689
30972
|
? `path not found: ${_abs} — no directory was created`
|
|
30690
|
-
: `경로 없음: ${_abs} — 디렉토리를 새로 만들지 않았습니다`);
|
|
30973
|
+
: `경로 없음: ${_abs} — 디렉토리를 새로 만들지 않았습니다`));
|
|
30691
30974
|
return;
|
|
30692
30975
|
}
|
|
30693
30976
|
// 1.36.148 (재검수 P2): 존재성만 봐서 **일반 파일**이 프로젝트 root 로 통과했고,
|
|
@@ -30696,9 +30979,9 @@ async function main() {
|
|
|
30696
30979
|
try {
|
|
30697
30980
|
if (!fs.statSync(_abs).isDirectory()) {
|
|
30698
30981
|
const _en2 = _uiLang(process.cwd()) === 'en';
|
|
30699
|
-
failJson(has('--json'), 'path_not_a_directory', _en2
|
|
30982
|
+
failJson(has('--json'), 'path_not_a_directory', _lineSafe(_en2
|
|
30700
30983
|
? `--path is not a directory: ${_abs}`
|
|
30701
|
-
: `--path 가 디렉토리가 아닙니다: ${_abs}`);
|
|
30984
|
+
: `--path 가 디렉토리가 아닙니다: ${_abs}`));
|
|
30702
30985
|
return;
|
|
30703
30986
|
}
|
|
30704
30987
|
} catch { /* stat 실패는 위 존재 검사에서 이미 걸러졌다 */ }
|
|
@@ -30711,7 +30994,7 @@ async function main() {
|
|
|
30711
30994
|
// 1.36.104: 'mcp' 를 제외한다. MCP 서버는 프로젝트 명령이 아니라 상주 프로세스라, 서버의 cwd 가 우연히
|
|
30712
30995
|
// leerness 프로젝트면 거기에 `mcp: N` 이 쌓였다 — 사용자가 그 프로젝트에서 쓴 적 없는 '명령' 이 통계에 뜬다
|
|
30713
30996
|
// (실측: 대상 프로젝트는 "사용 기록 없음", 무관한 cwd 는 "총 3회 호출"). 도구별 귀속은 _bumpMcpUsage 가 이미 한다.
|
|
30714
|
-
if (process.env.LEERNESS_INTERNAL !== '1' && cmd !== 'usage' && cmd !== 'init' && cmd !== 'migrate' && cmd !== 'mcp' && cmd !== '--version' && cmd !== '--help') {
|
|
30997
|
+
if (process.env.LEERNESS_INTERNAL !== '1' && !_mustRemainReadOnly(args, cmd) && cmd !== 'usage' && cmd !== 'init' && cmd !== 'migrate' && cmd !== 'mcp' && cmd !== '--version' && cmd !== '--help') {
|
|
30715
30998
|
try {
|
|
30716
30999
|
// 1.9.352 (UR-0069 외부리뷰): usage 루트 — --path 우선, 없으면 args[1] 이 .leerness 보유 디렉토리일 때만 path(positional 보존), 아니면 cwd. (이전: args[1] 무조건 path 가정 → subcommand[decision add 등] root=cwd/add → .leerness 못 찾아 미집계)
|
|
30717
31000
|
const _pathArg = arg('--path', null);
|
|
@@ -31360,7 +31643,11 @@ async function main() {
|
|
|
31360
31643
|
const _pvToks = args.slice(1);
|
|
31361
31644
|
// 1.36.80 (UR-0067): 라이브 미리보기 — serve(로컬 서버) / mode(self|project) 는 별도 모듈로 위임
|
|
31362
31645
|
const _pvRoot = arg('--path', process.cwd());
|
|
31363
|
-
if (_pvToks[0] === 'serve')
|
|
31646
|
+
if (_pvToks[0] === 'serve') {
|
|
31647
|
+
const _pvChecked = _clar._loadPreviewsChecked(_pvRoot);
|
|
31648
|
+
if (_clar._failInvalidPreviewStore(_pvRoot, _pvChecked, has('--json'))) return;
|
|
31649
|
+
return _pvsrv.previewServeCmd(_pvRoot, _pvToks[1], { has, arg, previews: _pvChecked.list });
|
|
31650
|
+
}
|
|
31364
31651
|
if (_pvToks[0] === 'mode') return _pvsrv.previewModeCmd(_pvRoot, _pvToks[1], { has, arg });
|
|
31365
31652
|
return _clar.previewCmd(_pvRoot, _pvToks[0], _pvToks.slice(1), { has, arg, _withLock, _onResolved: (id) => _pvsrv.cleanupServeDir(_pvRoot, id) }); // 1.36.54 (#2): 변경은 락 직렬화 · 1.36.80: 승인/수정 시 임시 워크스페이스 정리
|
|
31366
31653
|
}
|
|
@@ -31543,7 +31830,7 @@ async function main() {
|
|
|
31543
31830
|
if (cmd === 'release' && args[1] === 'cleanup') return releaseCleanupCmd((args[2] && !args[2].startsWith('-')) ? args[2] : arg('--path', process.cwd()));
|
|
31544
31831
|
// 1.9.275 (UR-0026): leerness release channel — 안정/실험 채널 정책 (npm dist-tag)
|
|
31545
31832
|
if (cmd === 'release' && args[1] === 'channel') return releaseChannelCmd((args[2] && !args[2].startsWith('-')) ? args[2] : arg('--path', process.cwd()));
|
|
31546
|
-
if (cmd === 'release' && args[1] === 'cadence') return releaseCadenceCmd((args[2]
|
|
31833
|
+
if (cmd === 'release' && args[1] === 'cadence') return releaseCadenceCmd(_resolveRoot(args[2])); // 1.9.374/T-0095: --path > positional > cwd 단일 해석
|
|
31547
31834
|
// 1.9.141: feature causality graph
|
|
31548
31835
|
// 1.36.2 (clean-room, UR-0184): add/show/link/impact 도 trailing positional path 인식 — list(1.9.412)만 지원하던 것을 일관 확대.
|
|
31549
31836
|
// 기존엔 add 가 모든 non-flag positional 을 NAME 으로 join → 경로가 이름에 흡수 + cwd 에 stray .leerness scaffold(조용한 오독).
|
|
@@ -31618,11 +31905,35 @@ async function main() {
|
|
|
31618
31905
|
const root = absRoot(arg('--path', args[3] && !args[3].startsWith('-') ? args[3] : process.cwd()));
|
|
31619
31906
|
return memoryArchiveListCmd(root, { json: has('--json') });
|
|
31620
31907
|
}
|
|
31908
|
+
if (cmd === 'memory' && args[1] === 'archive') {
|
|
31909
|
+
const child = args[2] || null;
|
|
31910
|
+
const _memLang = _uiLang(arg('--path', process.cwd()));
|
|
31911
|
+
if (!child) {
|
|
31912
|
+
failJson(has('--json'), 'subcommand_required', _tx(_memLang,
|
|
31913
|
+
'memory archive 하위명령 필요 — 사용법: leerness memory archive list',
|
|
31914
|
+
'memory archive subcommand required — usage: leerness memory archive list'));
|
|
31915
|
+
} else {
|
|
31916
|
+
const safeChild = _lineSafe(child);
|
|
31917
|
+
failJson(has('--json'), 'unknown_subcommand', _tx(_memLang,
|
|
31918
|
+
`알 수 없는 memory archive 하위명령: ${safeChild} — 가능: list`,
|
|
31919
|
+
`Unknown memory archive subcommand: ${safeChild} — valid: list`));
|
|
31920
|
+
}
|
|
31921
|
+
return process.exit(process.exitCode || 1);
|
|
31922
|
+
}
|
|
31621
31923
|
// 1.9.128: memory restore — archive 블록을 active 파일로 복귀 (DELETE→RESTORE cycle)
|
|
31622
31924
|
if (cmd === 'memory' && args[1] === 'restore') {
|
|
31623
31925
|
const root = absRoot(arg('--path', process.cwd()));
|
|
31624
31926
|
return memoryRestoreCmd(root, args[2], args[3]);
|
|
31625
31927
|
}
|
|
31928
|
+
if (cmd === 'memory' && args[1]) {
|
|
31929
|
+
const sub = args[1];
|
|
31930
|
+
const safeSub = _lineSafe(sub);
|
|
31931
|
+
const _memLang = _uiLang(arg('--path', process.cwd()));
|
|
31932
|
+
failJson(has('--json'), 'unknown_subcommand', _tx(_memLang,
|
|
31933
|
+
`알 수 없는 memory 하위명령: ${safeSub} — leerness memory search <query> | status | archive list | restore <surface> <target>`,
|
|
31934
|
+
`Unknown memory subcommand: ${safeSub} — leerness memory search <query> | status | archive list | restore <surface> <target>`));
|
|
31935
|
+
return process.exit(process.exitCode || 1);
|
|
31936
|
+
}
|
|
31626
31937
|
// 1.9.112: lesson save — lessons.md에 새 lesson 추가
|
|
31627
31938
|
// 1.9.117: lesson list — lessons.md 조회 + --tag 필터 + --json
|
|
31628
31939
|
if (cmd === 'lesson') {
|
|
@@ -31702,6 +32013,19 @@ if (require.main === module) {
|
|
|
31702
32013
|
`이 명령은 --dry-run 을 구현하지 않았습니다 — ${err.file} 을(를) 쓰려다 멈췄습니다. 변경된 것은 없습니다(--dry-run 을 빼고 실행하세요).`);
|
|
31703
32014
|
process.exit(1);
|
|
31704
32015
|
}
|
|
32016
|
+
if (err && /^E_WORKSPACE_DIR_[A-Z_]+$/.test(String(err.code || ''))) {
|
|
32017
|
+
// Resolver policy failures are part of the public read/status result.
|
|
32018
|
+
// Do not downgrade split-brain/invalid overrides to a generic error or
|
|
32019
|
+
// to harnessVersionState:"unreadable".
|
|
32020
|
+
failJson(process.argv.includes('--json'), String(err.code).slice(2).toLowerCase(),
|
|
32021
|
+
_lineSafe(err && err.message ? err.message : String(err)));
|
|
32022
|
+
process.exit(1);
|
|
32023
|
+
}
|
|
32024
|
+
if (err && err.code === 'E_GIT_HISTORY_UNAVAILABLE') {
|
|
32025
|
+
failJson(process.argv.includes('--json'), 'git_history_unavailable',
|
|
32026
|
+
_lineSafe(err && err.message ? err.message : 'Git history could not be measured'));
|
|
32027
|
+
process.exit(1);
|
|
32028
|
+
}
|
|
31705
32029
|
fail(err && err.message ? err.message : String(err));
|
|
31706
32030
|
// 1.36.88: 완료 게이트는 upsertProgress 에서 던진다 — taskUpdate/taskSync 외의 경로로 올라와도
|
|
31707
32031
|
// 스택 트레이스 대신 탈출구를 보여준다(막힌 사용자가 빠져나갈 방법을 알아야 한다).
|
|
@@ -31720,6 +32044,7 @@ if (require.main === module) {
|
|
|
31720
32044
|
// _isSecretKey (시크릿 차단, 보안), compareVer (버전 비교, stale-check 정확성), parseHarnessVersion (버전 파싱).
|
|
31721
32045
|
module.exports = {
|
|
31722
32046
|
VERSION,
|
|
32047
|
+
_cliMutationClass, _mustRemainReadOnly,
|
|
31723
32048
|
_npmGlobalBin, _dirInPath, _leernessResolvable, _pathDiagnose, _registerPath,
|
|
31724
32049
|
_winPathPsScript, _unixPathBlock, pathSetupCmd,
|
|
31725
32050
|
_isSecretKey, compareVer, parseHarnessVersion,
|
|
@@ -31776,5 +32101,6 @@ module.exports = {
|
|
|
31776
32101
|
_capLensDomains, _scanLensSignals,
|
|
31777
32102
|
_handoffNudgeState, _getLastHandoffGap, _recordLastHandoff, _recordHandoffFreshness,
|
|
31778
32103
|
_releaseFileSnapshot, _runReleaseTransaction,
|
|
31779
|
-
_detectOptimism, _scanCodeForPatterns
|
|
32104
|
+
_detectOptimism, _scanCodeForPatterns,
|
|
32105
|
+
_parseRoundTagOutput, _parseRecentChangeOutput, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _gitHistoryFailureFromResult
|
|
31780
32106
|
};
|