leerness 1.36.169 → 1.36.170
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 +6 -0
- package/README.md +4 -4
- package/bin/leerness.js +15 -8
- package/package.json +1 -1
- package/scripts/next-action-suggestion-probe.js +48 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.170 — 2026-08-27
|
|
4
|
+
|
|
5
|
+
- next-action의 정규화 계약을 모든 읽기 표면으로 확장했습니다. release-time auto-resume plan 생성과 외부 에이전트용 `context`가 legacy `--filter`, 자동 완료 명령, 시간 기반 중복, 존재하지 않는 stress 실행을 다시 노출하지 않습니다.
|
|
6
|
+
- 1.36.169 이전에 저장된 `auto-resume-plan.json`도 `resume`/`handoff`에서 지연 정규화하며, pre-wake audit가 폐기된 `pending` 필드 대신 canonical queue를 세어 대기 작업을 정확히 보고합니다.
|
|
7
|
+
- 회귀 probe가 `next-action list/take`뿐 아니라 release plan, 기존 plan 재개, context JSON, pre-wake pending count까지 실제 CLI로 검증합니다. 외부 Codex 교차 리뷰 결과 P0/P1/P2는 없었습니다.
|
|
8
|
+
|
|
3
9
|
## 1.36.169 — 2026-08-27
|
|
4
10
|
|
|
5
11
|
- 멀티세션 lost-update 방지: plan/anchors canonical lock, decisions·lessons migration transaction, rule baseline verification serialization, 11-pair forced-interleaving regressions
|
package/README.md
CHANGED
|
@@ -138,7 +138,7 @@ MIT
|
|
|
138
138
|
<!-- leerness:project-readme:start -->
|
|
139
139
|
## Leerness Project Harness
|
|
140
140
|
|
|
141
|
-
이 프로젝트는 Leerness v1.36.
|
|
141
|
+
이 프로젝트는 Leerness v1.36.170 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
142
142
|
|
|
143
143
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
144
144
|
|
|
@@ -192,7 +192,7 @@ leerness memory restore decision <date|title>
|
|
|
192
192
|
|
|
193
193
|
### MCP server (외부 AI 통합)
|
|
194
194
|
|
|
195
|
-
Leerness v1.36.
|
|
195
|
+
Leerness v1.36.170는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **89개 도구**를 노출:
|
|
196
196
|
|
|
197
197
|
```jsonc
|
|
198
198
|
// 카테고리별
|
|
@@ -213,7 +213,7 @@ Leerness v1.36.169는 stdio JSON-RPC MCP server를 내장합니다 — Claude Co
|
|
|
213
213
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
214
214
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
215
215
|
|
|
216
|
-
현재 누적: **v1.9.x → 1.36.
|
|
216
|
+
현재 누적: **v1.9.x → 1.36.170 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
217
217
|
|
|
218
218
|
### 성능 가이드
|
|
219
219
|
|
|
@@ -251,5 +251,5 @@ leerness release pack --close --auto-main-push
|
|
|
251
251
|
- `.leerness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
252
252
|
- `.leerness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
253
253
|
|
|
254
|
-
Last synced by Leerness v1.36.
|
|
254
|
+
Last synced by Leerness v1.36.170: 2026-08-27
|
|
255
255
|
<!-- leerness:project-readme:end -->
|
package/bin/leerness.js
CHANGED
|
@@ -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.170';
|
|
52
52
|
|
|
53
53
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
54
54
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -11400,12 +11400,14 @@ function _runPreWakeAudit(root) {
|
|
|
11400
11400
|
|
|
11401
11401
|
// 6) next-action queue 잔여 (1.9.201)
|
|
11402
11402
|
try {
|
|
11403
|
-
|
|
11404
|
-
|
|
11403
|
+
// loader 계약은 { queue, at }이다. 과거 `pending`을 읽어 이 검사가 항상 무언
|
|
11404
|
+
// no-op이었고, raw 큐 수를 쓰면 legacy 중복까지 과대 계수된다.
|
|
11405
|
+
const q = _normalizedNextActionState(root);
|
|
11406
|
+
if (q.queue.length > 0) {
|
|
11405
11407
|
audit.findings.info.push({
|
|
11406
11408
|
kind: 'next-action-pending',
|
|
11407
|
-
count: q.
|
|
11408
|
-
detail: `${q.
|
|
11409
|
+
count: q.queue.length,
|
|
11410
|
+
detail: `${q.queue.length}건 next-action 대기 — leerness next-action take 로 즉시 처리`
|
|
11409
11411
|
});
|
|
11410
11412
|
}
|
|
11411
11413
|
} catch {}
|
|
@@ -11908,7 +11910,11 @@ function _loadAutoResumePlan(root) {
|
|
|
11908
11910
|
if (!j.savedAt) return null;
|
|
11909
11911
|
const ageMs = Date.now() - new Date(j.savedAt).getTime();
|
|
11910
11912
|
const expiredMs = j.expectedFireAt ? Date.now() - new Date(j.expectedFireAt).getTime() : 0;
|
|
11911
|
-
|
|
11913
|
+
// 1.36.170: 1.36.169 이전에 저장한 plan도 resume/handoff에서 곧바로 현재 명령 계약으로
|
|
11914
|
+
// 읽는다. 큐 list/take만 정규화하면 release 직후 저장된 plan이 legacy --filter,
|
|
11915
|
+
// 자동 done 명령, 존재하지 않는 stress 파일 실행을 다시 노출할 수 있다.
|
|
11916
|
+
const nextActions = _compactNextActionQueue(Array.isArray(j.nextActions) ? j.nextActions : []);
|
|
11917
|
+
return { ...j, nextActions, ageMs, ageMin: Math.floor(ageMs / 60000), elapsedFromExpected: Math.floor(expiredMs / 60000) };
|
|
11912
11918
|
} catch { return null; }
|
|
11913
11919
|
}
|
|
11914
11920
|
function _writeAutoResumePlan(root, plan) {
|
|
@@ -11931,7 +11937,7 @@ function _buildAutoResumePlan(root, opts) {
|
|
|
11931
11937
|
// 현재 VERSION
|
|
11932
11938
|
const currentVersion = VERSION;
|
|
11933
11939
|
// next-action queue snapshot
|
|
11934
|
-
const queueState =
|
|
11940
|
+
const queueState = _normalizedNextActionState(root);
|
|
11935
11941
|
const queueTop = queueState.queue.length > 0 ? queueState.queue[queueState.queue.length - 1] : null;
|
|
11936
11942
|
// memory surface counts
|
|
11937
11943
|
const memorySurface = {
|
|
@@ -27306,7 +27312,8 @@ function contextCmd(root, opts = {}) {
|
|
|
27306
27312
|
const _decs = _loadDecisions(root);
|
|
27307
27313
|
const decisionCount = _decs.length;
|
|
27308
27314
|
const recentDecisions = _decs.slice(-3).reverse().map(d => ({ date: d.date || null, title: (d.title || '').slice(0, 100) }));
|
|
27309
|
-
|
|
27315
|
+
// 외부 에이전트 context도 next-action list/take와 같은 정규화 계약을 공유한다.
|
|
27316
|
+
const queueState = _normalizedNextActionState(root);
|
|
27310
27317
|
const nextActions = (queueState.queue || []).slice(-3).reverse().map(a => ({ title: a.title, command: a.command || null }));
|
|
27311
27318
|
const memory = {
|
|
27312
27319
|
tasksInProgress: rows.filter(r => r.status === 'in-progress').length,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.170",
|
|
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",
|
|
@@ -16,6 +16,11 @@ if (start < 0 || end <= start) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const generator = source.slice(start, end);
|
|
19
|
+
const resumeBuildStart = source.indexOf('function _buildAutoResumePlan(');
|
|
20
|
+
const resumeBuildEnd = source.indexOf('function resumeCmd(', resumeBuildStart);
|
|
21
|
+
const resumeBuilder = resumeBuildStart >= 0 && resumeBuildEnd > resumeBuildStart
|
|
22
|
+
? source.slice(resumeBuildStart, resumeBuildEnd)
|
|
23
|
+
: '';
|
|
19
24
|
const offenders = [];
|
|
20
25
|
const run = (args, cwd) => cp.spawnSync(process.execPath, [cliPath, ...args], {
|
|
21
26
|
cwd,
|
|
@@ -30,6 +35,13 @@ const writeQueue = (root, queue) => {
|
|
|
30
35
|
};
|
|
31
36
|
const commandArgs = (command) => (String(command).match(/"[^"]*"|\S+/g) || []).map(x => x.startsWith('"') ? x.slice(1, -1) : x);
|
|
32
37
|
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
38
|
+
const hasLegacySuggestion = actions => (Array.isArray(actions) ? actions : []).some(a =>
|
|
39
|
+
/\s--filter(?:\s|$)/.test(String(a.command || ''))
|
|
40
|
+
|| String(a.command || '').includes('./scripts/e2e.js')
|
|
41
|
+
|| /task update .* --status completed/.test(String(a.command || ''))
|
|
42
|
+
|| (/^progress-tracker \d+h 정체/.test(String(a.title || '')))
|
|
43
|
+
|| (/^node _apps\/leerness-stress/.test(String(a.command || '')))
|
|
44
|
+
);
|
|
33
45
|
|
|
34
46
|
async function reproduceTakeMigrationRace(root) {
|
|
35
47
|
const queuePath = path.join(root, '.leerness', 'next-action-queue.json');
|
|
@@ -74,6 +86,7 @@ async function main() {
|
|
|
74
86
|
if (generator.includes('leerness plan list --filter')) offenders.push('plan-list-filter');
|
|
75
87
|
if (/stress-v\$\{latest\+1\}/.test(generator)) offenders.push('missing-stress-execution');
|
|
76
88
|
if (generator.includes('node ./scripts/e2e.js')) offenders.push('consumer-missing-e2e');
|
|
89
|
+
if (!resumeBuilder.includes('_normalizedNextActionState(root)')) offenders.push('auto-resume-build-raw-queue');
|
|
77
90
|
|
|
78
91
|
const arena = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-next-action-probe-'));
|
|
79
92
|
try {
|
|
@@ -105,6 +118,41 @@ async function main() {
|
|
|
105
118
|
if (queue.some(a => a.icon === '🔄' && /\d+h/.test(String(a.title)))) offenders.push('dynamic-progress-title');
|
|
106
119
|
if (queue.filter(a => ['🎯', '💭', '🧪', '🧬', '🔄'].includes(a.icon)).some(a => !a.actionKey)) offenders.push('stable-action-key');
|
|
107
120
|
|
|
121
|
+
// Every read surface must expose the same normalized contract as next-action list/take.
|
|
122
|
+
// A published release writes auto-resume-plan from this snapshot, while context is consumed
|
|
123
|
+
// by external agents; leaking legacy commands through either surface reintroduces the bug.
|
|
124
|
+
const planRoot = path.join(arena, 'resume-project');
|
|
125
|
+
fs.mkdirSync(path.join(planRoot, '.leerness'), { recursive: true });
|
|
126
|
+
fs.writeFileSync(path.join(planRoot, '.leerness', 'auto-resume-plan.json'), JSON.stringify({
|
|
127
|
+
savedAt: new Date().toISOString(),
|
|
128
|
+
expectedFireAt: new Date(Date.now() + 60000).toISOString(),
|
|
129
|
+
nextRoundVersion: 'next after 1.36.169',
|
|
130
|
+
nextActions: legacy
|
|
131
|
+
}, null, 2), 'utf8');
|
|
132
|
+
const resumed = run(['resume', '--path', planRoot, '--json'], planRoot);
|
|
133
|
+
let resumedActions = [];
|
|
134
|
+
try { resumedActions = JSON.parse(resumed.stdout).nextActions; } catch { offenders.push('resume-json-contract'); }
|
|
135
|
+
if (resumed.status !== 0) offenders.push('resume-exit');
|
|
136
|
+
if (hasLegacySuggestion(resumedActions)) offenders.push('legacy-auto-resume-plan');
|
|
137
|
+
if ((resumedActions || []).filter(a => a.icon === '🧬').length !== 1
|
|
138
|
+
|| (resumedActions || []).filter(a => a.icon === '🔄').length !== 1) offenders.push('auto-resume-plan-dedup');
|
|
139
|
+
|
|
140
|
+
const contextual = run(['context', '--path', listRoot, '--json'], listRoot);
|
|
141
|
+
let contextActions = [];
|
|
142
|
+
try { contextActions = JSON.parse(contextual.stdout).nextActions; } catch { offenders.push('context-json-contract'); }
|
|
143
|
+
if (contextual.status !== 0) offenders.push('context-exit');
|
|
144
|
+
if (hasLegacySuggestion(contextActions)) offenders.push('context-raw-queue');
|
|
145
|
+
|
|
146
|
+
const preWake = run(['pre-wake-audit', '--path', listRoot, '--json'], listRoot);
|
|
147
|
+
let pendingFinding = null;
|
|
148
|
+
try {
|
|
149
|
+
const report = JSON.parse(preWake.stdout);
|
|
150
|
+
pendingFinding = (report.findings.info || []).find(f => f.kind === 'next-action-pending');
|
|
151
|
+
} catch { offenders.push('pre-wake-json-contract'); }
|
|
152
|
+
if (preWake.status !== 0) offenders.push('pre-wake-exit');
|
|
153
|
+
if (!pendingFinding) offenders.push('pre-wake-missing-next-action');
|
|
154
|
+
else if (pendingFinding.count !== queue.length) offenders.push('pre-wake-raw-next-action-count');
|
|
155
|
+
|
|
108
156
|
// Execute corrected generated commands against an ordinary project. This guards command
|
|
109
157
|
// existence and flags, rather than merely asserting that selected source strings changed.
|
|
110
158
|
for (const action of queue.filter(a => ['🎯', '💭', '🧪'].includes(a.icon))) {
|