@quolu/lattice 0.46.0 → 0.46.2
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/README.ja.md +9 -1
- package/README.md +3 -1
- package/bin/lattice.mjs +9 -1
- package/package.json +1 -1
- package/src/hooks-cli.mjs +31 -8
- package/src/sensor-node-runtime.mjs +53 -0
- package/src/sensor-runtime.mjs +5 -1
package/README.ja.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src=".github/og.png" alt="Lattice —
|
|
2
|
+
<img src=".github/og.png" alt="Lattice — 閉ざされて見えた山岳地形から複数の進行経路が現れる" width="100%">
|
|
3
|
+
<br>
|
|
4
|
+
<sub><em>この画像は、閉ざされているように見えた場所から複数の進行経路が現れ、自律した実行者たちが協調して動き出す瞬間を表しています。</em></sub>
|
|
3
5
|
</p>
|
|
4
6
|
|
|
5
7
|
# Lattice
|
|
@@ -295,6 +297,12 @@ dashboard daemonは起動時に読み込んだ版数をhealthで名乗り、inst
|
|
|
295
297
|
その間伸びます(実測: 8 project登録で約50秒台)。待ち時間は固定秒数ではなく、spawnした子が生きている
|
|
296
298
|
間だけ待ち、子が死ねば即座に`DASHBOARD_DAEMON_UNAVAILABLE`を返します。既定120秒の上限は、応答を
|
|
297
299
|
返さない子に対するbackstopであって正常な起動時間の見積りではありません。
|
|
300
|
+
daemonの生死はdescriptor 1枚では持ちません。daemonは自分のpidの記録を書き、起動のたびに死んだ記録を
|
|
301
|
+
掃除して、descriptorから外れた生存daemonを再認証のうえ停止します。入れ替えの途中でCLI側が落ちても、
|
|
302
|
+
旧daemonが観測されないまま生き残ることはありません。descriptorだけを失った場合は2本目を建てず、
|
|
303
|
+
配信を続けているdaemonを引き取ります。signalを送るのはその場で再認証を通った相手だけで、応答しない
|
|
304
|
+
pidへは送りません(pid再利用で無関係のprocessを止めうるためです)。規約は
|
|
305
|
+
[ADR 0157](docs/adr/0157-dashboard-daemons-are-discoverable-by-record.md)が正です。
|
|
298
306
|
状態を書き込む`start / block / unblock / done / evidence promote / reopen / revise / revise-phase / revise-set`
|
|
299
307
|
では、監査actorとして次の3環境変数をすべて設定してください。
|
|
300
308
|
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src=".github/og.png" alt="Lattice —
|
|
2
|
+
<img src=".github/og.png" alt="Lattice — several viable routes emerging through an apparently blocked mountain valley" width="100%">
|
|
3
|
+
<br>
|
|
4
|
+
<sub><em>This image represents several viable paths emerging from terrain that first appeared blocked, as autonomous executors begin moving in coordination.</em></sub>
|
|
3
5
|
</p>
|
|
4
6
|
|
|
5
7
|
# Lattice
|
package/bin/lattice.mjs
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import { installPipeCloseGuard } from '../src/cli-stdio.mjs';
|
|
4
5
|
import { runRuntimeCli } from '../src/runtime-cli.mjs';
|
|
5
6
|
import { renderCliHelp } from '../src/cli-help.mjs';
|
|
7
|
+
import { relaunchSensorForNode22IfNeeded } from '../src/sensor-node-runtime.mjs';
|
|
6
8
|
import packageJson from '../package.json' with { type: 'json' };
|
|
7
9
|
|
|
8
10
|
installPipeCloseGuard();
|
|
9
11
|
|
|
10
12
|
const args = process.argv.slice(2);
|
|
13
|
+
const sensorRelaunchStatus = relaunchSensorForNode22IfNeeded({
|
|
14
|
+
args,
|
|
15
|
+
scriptPath: fileURLToPath(import.meta.url),
|
|
16
|
+
});
|
|
11
17
|
const help = renderCliHelp(args);
|
|
12
18
|
|
|
13
|
-
if (
|
|
19
|
+
if (sensorRelaunchStatus !== null) {
|
|
20
|
+
process.exitCode = sensorRelaunchStatus;
|
|
21
|
+
} else if (help !== null) {
|
|
14
22
|
process.stdout.write(help);
|
|
15
23
|
} else if (args.length === 1 && args[0] === '--version') {
|
|
16
24
|
process.stdout.write(`${packageJson.version}\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.46.
|
|
3
|
+
"version": "0.46.2",
|
|
4
4
|
"description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Quo / クオ at kitepon.dev",
|
package/src/hooks-cli.mjs
CHANGED
|
@@ -427,18 +427,41 @@ function hostHandler(host, command) {
|
|
|
427
427
|
: { type: 'command', command, timeout: 5, async: false, statusMessage: null };
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
-
async function
|
|
430
|
+
export async function resolveStableNodePath(execPath, {
|
|
431
|
+
platform = process.platform,
|
|
432
|
+
accessImpl = access,
|
|
433
|
+
realpathImpl = realpath,
|
|
434
|
+
} = {}) {
|
|
435
|
+
await accessImpl(execPath, fsConstants.X_OK);
|
|
436
|
+
if (platform !== 'darwin') return execPath;
|
|
437
|
+
const match = execPath.match(/^\/(opt\/homebrew|usr\/local)\/Cellar\/([^/]+)\/[^/]+\/bin\/node$/u);
|
|
438
|
+
if (match === null) return execPath;
|
|
439
|
+
const prefix = `/${match[1]}`;
|
|
440
|
+
const candidates = [path.join(prefix, 'bin/node'), path.join(prefix, 'opt', match[2], 'bin/node')];
|
|
441
|
+
const resolvedExec = await realpathImpl(execPath);
|
|
442
|
+
for (const candidate of candidates) {
|
|
443
|
+
try {
|
|
444
|
+
await accessImpl(candidate, fsConstants.X_OK);
|
|
445
|
+
if (await realpathImpl(candidate) === resolvedExec) return candidate;
|
|
446
|
+
} catch {
|
|
447
|
+
// A candidate is usable only when it exists and resolves to this running Node binary.
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return execPath;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function resolveCanonical(host, source, platform) {
|
|
431
454
|
const sourcePaths = [source.execPath, source.binPath];
|
|
432
455
|
if (sourcePaths.some((entry) => typeof entry !== 'string' || !path.isAbsolute(entry)
|
|
433
456
|
|| /[\0\r\n]/u.test(entry))) {
|
|
434
457
|
throw Object.assign(new Error('install source is not absolute'), { code: 'INSTALL_SOURCE_UNRESOLVED' });
|
|
435
458
|
}
|
|
436
459
|
try {
|
|
437
|
-
await
|
|
460
|
+
const executable = await resolveStableNodePath(source.execPath, { platform });
|
|
438
461
|
const script = await realpath(source.binPath);
|
|
439
462
|
if (/[\0\r\n]/u.test(script)) throw new Error('resolved install source has unsafe characters');
|
|
440
463
|
await access(script, fsConstants.R_OK | fsConstants.X_OK);
|
|
441
|
-
return [
|
|
464
|
+
return [executable, script, 'hooks', 'emit', '--host', host];
|
|
442
465
|
} catch (error) {
|
|
443
466
|
throw Object.assign(error, { code: 'INSTALL_SOURCE_UNRESOLVED' });
|
|
444
467
|
}
|
|
@@ -613,13 +636,13 @@ async function hostDirectory(home, host) {
|
|
|
613
636
|
}
|
|
614
637
|
}
|
|
615
638
|
|
|
616
|
-
async function mutate(host, env, stdout, uninstall, source, testHooks) {
|
|
639
|
+
async function mutate(host, env, stdout, uninstall, source, platform, testHooks) {
|
|
617
640
|
const home = env.HOME ?? os.homedir();
|
|
618
641
|
if (await hostDirectory(home, host) === null) {
|
|
619
642
|
return failure(stdout, 'HOST_NOT_PRESENT', 'host home directory is not present');
|
|
620
643
|
}
|
|
621
644
|
let current;
|
|
622
|
-
try { current = await resolveCanonical(host, source); } catch {
|
|
645
|
+
try { current = await resolveCanonical(host, source, platform); } catch {
|
|
623
646
|
return failure(stdout, 'INSTALL_SOURCE_UNRESOLVED', 'install source cannot be resolved');
|
|
624
647
|
}
|
|
625
648
|
const target = configPath(home, host);
|
|
@@ -708,7 +731,7 @@ async function status(host, env, stdout, source, platform, testHooks) {
|
|
|
708
731
|
const home = env.HOME ?? os.homedir();
|
|
709
732
|
const target = configPath(home, host);
|
|
710
733
|
let argv;
|
|
711
|
-
try { argv = await resolveCanonical(host, source); } catch {
|
|
734
|
+
try { argv = await resolveCanonical(host, source, platform); } catch {
|
|
712
735
|
writeJson(stdout, statusResult(host, target, null, 'unreadable', 0, false, 0));
|
|
713
736
|
return 1;
|
|
714
737
|
}
|
|
@@ -1023,8 +1046,8 @@ export async function runHooksCli({
|
|
|
1023
1046
|
if (command === 'status') return status(host, env, stdout, source, platform, testHooks);
|
|
1024
1047
|
return failure(stdout, 'HOST_PLATFORM_UNSUPPORTED', 'native Windows hooks are unsupported');
|
|
1025
1048
|
}
|
|
1026
|
-
if (command === 'install') return mutate(host, env, stdout, false, source, testHooks);
|
|
1027
|
-
if (command === 'uninstall') return mutate(host, env, stdout, true, source, testHooks);
|
|
1049
|
+
if (command === 'install') return mutate(host, env, stdout, false, source, platform, testHooks);
|
|
1050
|
+
if (command === 'uninstall') return mutate(host, env, stdout, true, source, platform, testHooks);
|
|
1028
1051
|
if (command === 'status') return status(host, env, stdout, source, platform, testHooks);
|
|
1029
1052
|
return emit(host, env, stdin, stdout, spawnImpl, gitTimeoutMs, testHooks);
|
|
1030
1053
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export const SQLITE_EXPERIMENTAL_WARNING_FLAG = '--disable-warning=ExperimentalWarning';
|
|
4
|
+
const RELAUNCH_GUARD_ENV = 'LATTICE_SENSOR_SQLITE_WARNING_RELAUNCHED';
|
|
5
|
+
|
|
6
|
+
export function sensorNodeRuntimeFlags(nodeVersion = process.versions.node) {
|
|
7
|
+
const major = Number(nodeVersion.split('.')[0]);
|
|
8
|
+
return major === 22 ? [SQLITE_EXPERIMENTAL_WARNING_FLAG] : [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function sensorRelaunchArgv(
|
|
12
|
+
scriptPath,
|
|
13
|
+
args,
|
|
14
|
+
execArgv = process.execArgv,
|
|
15
|
+
nodeVersion = process.versions.node,
|
|
16
|
+
) {
|
|
17
|
+
const flags = sensorNodeRuntimeFlags(nodeVersion)
|
|
18
|
+
.filter((flag) => !execArgv.includes(flag));
|
|
19
|
+
return [...flags, ...execArgv, scriptPath, ...args];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Node 22 prints node:sqlite's ExperimentalWarning to stderr. Sensor commands
|
|
24
|
+
* have a strict JSON-only stderr contract, so relaunch only that command
|
|
25
|
+
* surface with Node's warning-category flag before node:sqlite is imported.
|
|
26
|
+
*/
|
|
27
|
+
export function relaunchSensorForNode22IfNeeded({ args, scriptPath, env = process.env }) {
|
|
28
|
+
const flags = sensorNodeRuntimeFlags();
|
|
29
|
+
if (args[0] !== 'sensor'
|
|
30
|
+
|| flags.length === 0
|
|
31
|
+
|| flags.every((flag) => process.execArgv.includes(flag))
|
|
32
|
+
|| env[RELAUNCH_GUARD_ENV] === '1') return null;
|
|
33
|
+
|
|
34
|
+
const result = spawnSync(
|
|
35
|
+
process.execPath,
|
|
36
|
+
sensorRelaunchArgv(scriptPath, args),
|
|
37
|
+
{
|
|
38
|
+
cwd: process.cwd(),
|
|
39
|
+
env: { ...env, [RELAUNCH_GUARD_ENV]: '1' },
|
|
40
|
+
stdio: 'inherit',
|
|
41
|
+
windowsHide: true,
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
if (result.error) {
|
|
45
|
+
process.stderr.write(`${JSON.stringify({
|
|
46
|
+
schema: 'lattice.cli_error.v2',
|
|
47
|
+
code: 'LATTICE_SENSOR_RELAUNCH_FAILED',
|
|
48
|
+
message: result.error.message,
|
|
49
|
+
})}\n`);
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
return result.status ?? 1;
|
|
53
|
+
}
|
package/src/sensor-runtime.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
2
|
import { lstatSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { sensorNodeRuntimeFlags } from './sensor-node-runtime.mjs';
|
|
4
5
|
|
|
5
6
|
export const LATTICE_SENSOR_CLI = fileURLToPath(
|
|
6
7
|
new URL('../sensor/dist/bin/lattice-sensor.js', import.meta.url),
|
|
@@ -28,7 +29,10 @@ export function sensorCliInvocation(args) {
|
|
|
28
29
|
throw new TypeError('sensor args must be an array of strings');
|
|
29
30
|
}
|
|
30
31
|
assertBundledSensor();
|
|
31
|
-
return Object.freeze({
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
command: process.execPath,
|
|
34
|
+
args: Object.freeze([...sensorNodeRuntimeFlags(), LATTICE_SENSOR_CLI, ...args]),
|
|
35
|
+
});
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
export function invokeSensorCli(run, args, options) {
|