From 0ceddf02d1e1bea2ed51aaca49feeee0c97ef83c Mon Sep 17 00:00:00 2001 From: quolu <226230081+quolu@users.noreply.github.com> Date: Sat, 3 Oct 2026 03:06:13 +0000 Subject: [PATCH] =?UTF-8?q?=E6=B6=88=E3=81=88=E3=81=9Fcwd=E3=81=8B?= =?UTF-8?q?=E3=82=89=E3=81=AE=E5=AE=9F=E8=A1=8C=E3=82=92=E5=86=85=E9=83=A8?= =?UTF-8?q?=E6=95=85=E9=9A=9C=E3=81=A7=E3=81=AA=E3=81=8FCWD=5FUNAVAILABLE?= =?UTF-8?q?=E3=81=A7=E8=BF=94=E3=81=99=EF=BC=880.71.1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mac-kiteのBugHub d3c4989e(LATTICE.CLI_INTERNAL_FAILED)の調査で、削除済みcwdから run系commandを打つとprocess.cwd()のENOENTがtyped契約の外へ漏れ、内部故障として 記録されることを実機で再現した。todo・status等は生のstackで落ちていた。 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 10 +++++ bin/lattice.mjs | 21 ++++++++++ package-lock.json | 4 +- package.json | 2 +- test/cli-cwd-unavailable.test.mjs | 65 +++++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 test/cli-cwd-unavailable.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index c7911534..1f3b2c22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.71.1 — 2026-10-03 + +### 修正 + +- 作業ディレクトリが消えた場所(閉じたworktreeに残ったshell等)から実行すると、run系などのcommandが + `INTERNAL_FAILURE` を返し、内部故障(`LATTICE.CLI_INTERNAL_FAILED`)として記録されていた。 + todo・status などは Node の生のstackで落ちていた。どれも `lattice.cli_error.v2` の + `CWD_UNAVAILABLE`(exit 1)で返し、内部故障としては記録しないようにした。 + cwdを読まない `runtime-errors`・`bridge`・`hooks`・`setup`・`factory-diagnostics` はそのまま動く。 + ## 0.71.0 — 2026-09-30 ### sensor: 上流 CodeGraph の追従(49c11fc..7639c78、368コミット) diff --git a/bin/lattice.mjs b/bin/lattice.mjs index bab7eafc..2fb835d0 100755 --- a/bin/lattice.mjs +++ b/bin/lattice.mjs @@ -8,6 +8,8 @@ import packageJson from '../package.json' with { type: 'json' }; installPipeCloseGuard(); const args = process.argv.slice(2); +// 作業ディレクトリを読まないsurface。cwdが消えた席(閉じたworktree)からでも動かす。 +const CWD_FREE_COMMANDS = new Set(['runtime-errors', 'bridge', 'hooks', 'setup', 'factory-diagnostics']); installSqliteExperimentalWarningFilter(); const help = renderCliHelp(args); @@ -15,6 +17,16 @@ if (help !== null) { process.stdout.write(help); } else if (args.length === 1 && args[0] === '--version') { process.stdout.write(`${packageJson.version}\n`); +} else if (!CWD_FREE_COMMANDS.has(args[0]) && workingDirectory() === null) { + // 呼び出し元のcwdが削除済み(閉じたworktreeに残ったshell等)。`process.cwd()`のENOENTが + // 各分岐で生のstackや内部故障(LATTICE.CLI_INTERNAL_FAILED)として漏れていた。製品の故障ではなく + // 呼び出し環境の不備なので、観測せずtyped契約で原因と直し方を返す。 + process.stderr.write(`${JSON.stringify({ + schema: 'lattice.cli_error.v2', code: 'CWD_UNAVAILABLE', + message: 'current working directory no longer exists', + detail: { next_action: 'cd_to_existing_project_directory_then_retry' }, + })}\n`); + process.exitCode = 1; } else if (args.length === 2 && args[0] === 'session-context' && args[1] === '--json') { const { runSessionContext } = await import('../src/project-cli.mjs'); process.exitCode = await runSessionContext({ @@ -160,6 +172,15 @@ if (help !== null) { } } +function workingDirectory() { + try { + return process.cwd(); + } catch (error) { + if (error?.code === 'ENOENT') return null; + throw error; + } +} + async function runRuntimeErrorsCli(rest) { const runtimeErrors = await import('../src/runtime-errors.mjs'); const usage = () => { diff --git a/package-lock.json b/package-lock.json index 5eedc418..894b7bd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@quolu/lattice", - "version": "0.71.0", + "version": "0.71.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@quolu/lattice", - "version": "0.71.0", + "version": "0.71.1", "license": "PolyForm-Noncommercial-1.0.0", "dependencies": { "@clack/prompts": "^1.3.0", diff --git a/package.json b/package.json index 32ed8bd2..9f865b3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@quolu/lattice", - "version": "0.71.0", + "version": "0.71.1", "description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution", "author": { "name": "Quo / クオ at kitepon.dev", diff --git a/test/cli-cwd-unavailable.test.mjs b/test/cli-cwd-unavailable.test.mjs new file mode 100644 index 00000000..f1c0e8f7 --- /dev/null +++ b/test/cli-cwd-unavailable.test.mjs @@ -0,0 +1,65 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import nodeTest from 'node:test'; +import { fileURLToPath } from 'node:url'; + +// 削除済みcwdはPOSIXでだけ作れる(Windowsは使用中のディレクトリを消せない)。 +const test = process.platform === 'win32' ? nodeTest.skip : nodeTest; + +const cliPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'lattice.mjs'); + +async function makeCollectingEnv() { + const root = await mkdtemp(path.join(os.tmpdir(), 'lattice-cwd-gone-')); + const configDir = path.join(root, 'xdg-config', 'dotagents'); + await mkdir(configDir, { recursive: true, mode: 0o700 }); + await writeFile(path.join(configDir, 'factory-reporter.json'), JSON.stringify({ + schema_version: '1.0', + host: { id: 'test-host', profile: 'mac' }, + collection: { enabled: true }, + reporting: { enabled: false }, + }), { mode: 0o600 }); + const env = { + ...process.env, + HOME: root, + XDG_CONFIG_HOME: path.join(root, 'xdg-config'), + XDG_STATE_HOME: path.join(root, 'xdg-state'), + }; + return { root, env, storePath: path.join(root, 'xdg-state', 'lattice', 'runtime-errors.json') }; +} + +// cwdを作ってから消し、そこでCLIを起動する(閉じたworktreeに残ったshellと同じ状態)。 +function runInDeletedCwd(env, args) { + return spawnSync('sh', ['-c', 'dir=$(mktemp -d) && cd "$dir" && rmdir "$dir" && exec "$@"', 'sh', + process.execPath, cliPath, ...args], { encoding: 'utf8', env }); +} + +test('削除済みcwdからのcommandは内部故障にせずCWD_UNAVAILABLEのtyped errorで返し、観測しない', async () => { + const { root, env, storePath } = await makeCollectingEnv(); + try { + // run系(以前はLATTICE.CLI_INTERNAL_FAILEDとして観測された)、todo・status(以前は生のstackで落ちた)。 + for (const args of [['run', 'list', '--json'], ['todo', 'status', '--json'], ['status', '--json']]) { + const result = runInDeletedCwd(env, args); + assert.equal(result.status, 1, `${args.join(' ')}: ${result.stderr}`); + assert.equal(result.stdout, ''); + const lines = result.stderr.trim().split('\n'); + assert.equal(lines.length, 1, result.stderr); + const error = JSON.parse(lines[0]); + assert.equal(error.schema, 'lattice.cli_error.v2'); + assert.equal(error.code, 'CWD_UNAVAILABLE'); + } + assert.equal(existsSync(storePath), false, '呼び出し環境の不備を内部故障として記録しない'); + + // cwdを読まないsurfaceは、消えたcwdからでもそのまま動く。 + const snapshot = runInDeletedCwd(env, ['runtime-errors', 'diagnostics', '--json']); + assert.equal(snapshot.status, 0, snapshot.stderr); + assert.equal(JSON.parse(snapshot.stdout.trim()).schema, 'lattice.runtime_error_diagnostics.v1'); + const version = runInDeletedCwd(env, ['--version']); + assert.equal(version.status, 0, version.stderr); + } finally { + await rm(root, { recursive: true, force: true }); + } +});