Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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コミット)
Expand Down
21 changes: 21 additions & 0 deletions bin/lattice.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,25 @@ 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);

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({
Expand Down Expand Up @@ -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 = () => {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
65 changes: 65 additions & 0 deletions test/cli-cwd-unavailable.test.mjs
Original file line number Diff line number Diff line change
@@ -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 });
}
});
Loading