@asc-agent/runtime 0.3.2 → 0.5.0

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.
Files changed (60) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/claude-code/install.d.ts +7 -0
  3. package/dist/adapters/claude-code/install.js +114 -38
  4. package/dist/adapters/claude-code/session-start.d.ts +20 -0
  5. package/dist/adapters/claude-code/session-start.js +111 -0
  6. package/dist/adapters/fixture-surface/index.d.ts +33 -0
  7. package/dist/adapters/fixture-surface/index.js +97 -0
  8. package/dist/adapters/gitlab/adapter.js +1 -0
  9. package/dist/adapters/gitlab/client.d.ts +16 -2
  10. package/dist/adapters/gitlab/client.js +27 -0
  11. package/dist/adapters/gitlab/coordination.d.ts +19 -0
  12. package/dist/adapters/gitlab/coordination.js +111 -0
  13. package/dist/adapters/gitlab/ports.d.ts +6 -0
  14. package/dist/adapters/gitlab/ports.js +29 -2
  15. package/dist/adapters/jam/adapter.d.ts +46 -9
  16. package/dist/adapters/jam/adapter.js +88 -22
  17. package/dist/adapters/jam/setup.d.ts +62 -0
  18. package/dist/adapters/jam/setup.js +85 -0
  19. package/dist/adapters/local/canonical.d.ts +20 -0
  20. package/dist/adapters/local/canonical.js +58 -0
  21. package/dist/adapters/markdown/state-store.js +2 -1
  22. package/dist/adapters/service/launchd.d.ts +12 -0
  23. package/dist/adapters/service/launchd.js +97 -0
  24. package/dist/adapters/service/schtasks.d.ts +16 -0
  25. package/dist/adapters/service/schtasks.js +67 -0
  26. package/dist/adapters/service/systemd-user.d.ts +15 -0
  27. package/dist/adapters/service/systemd-user.js +98 -0
  28. package/dist/adapters/webhook/ingress.d.ts +2 -2
  29. package/dist/cli/asc.js +1080 -145
  30. package/dist/composition/registry.js +11 -3
  31. package/dist/composition/runtime.d.ts +36 -0
  32. package/dist/composition/runtime.js +80 -1
  33. package/dist/core/attach/setup-plan.d.ts +66 -2
  34. package/dist/core/attach/setup-plan.js +50 -0
  35. package/dist/core/binding/types.d.ts +1 -1
  36. package/dist/core/binding/types.js +1 -0
  37. package/dist/core/distribution/external-command.d.ts +26 -0
  38. package/dist/core/distribution/external-command.js +59 -2
  39. package/dist/core/distribution/persistent-runtime.d.ts +82 -0
  40. package/dist/core/distribution/persistent-runtime.js +49 -0
  41. package/dist/core/distribution/release.d.ts +3 -3
  42. package/dist/core/distribution/release.js +1 -1
  43. package/dist/core/runtime/background.d.ts +104 -0
  44. package/dist/core/runtime/background.js +225 -0
  45. package/dist/core/runtime/coordination.d.ts +312 -0
  46. package/dist/core/runtime/coordination.js +247 -0
  47. package/dist/core/runtime/front.d.ts +58 -0
  48. package/dist/core/runtime/front.js +50 -1
  49. package/dist/core/runtime/publish.d.ts +107 -0
  50. package/dist/core/runtime/publish.js +153 -0
  51. package/dist/core/runtime/workspaces.d.ts +61 -0
  52. package/dist/core/runtime/workspaces.js +82 -0
  53. package/dist/core/workspace/resolve.d.ts +36 -0
  54. package/dist/core/workspace/resolve.js +110 -3
  55. package/dist/ports/adapter.d.ts +12 -0
  56. package/dist/ports/coordination-surface.d.ts +59 -0
  57. package/dist/ports/coordination-surface.js +16 -0
  58. package/dist/ports/resource-context.d.ts +5 -0
  59. package/dist/schemas/profile.d.ts +6 -6
  60. package/package.json +1 -1
@@ -314,7 +314,8 @@ function renderControlState(state) {
314
314
  '# Execution',
315
315
  `활성 Block: ${state.activeBlock ?? '없음'}`,
316
316
  `활성 세션: ${state.activeSessions.join(', ') || '없음'}`,
317
- `Write Boundary 점유: ${state.writeBoundaryOccupancy.join(', ') || '없음'}`,
317
+ // 객체를 그대로 문자열에 넣으면 "[object Object]" 가 남는다 — 실제 state.md 에서 그렇게 보였다.
318
+ `Write Boundary 점유: ${state.writeBoundaryOccupancy.map((o) => `${o.sessionId} → ${o.paths.join(' ')}`).join(', ') || '없음'}`,
318
319
  `승인 대기: ${state.awaitingController.join(', ') || '없음'}`,
319
320
  '',
320
321
  '# Monitoring',
@@ -0,0 +1,12 @@
1
+ import { type PersistentRuntimeAdapter, type ServiceCommand } from '../../core/distribution/persistent-runtime.ts';
2
+ export declare const plistPath: (home?: string) => string;
3
+ /**
4
+ * plist 본문. **내용이 곧 비교 기준이다** — 같으면 CURRENT, 다르면 STALE 이다.
5
+ * digest 를 따로 두지 않는 이유: 파일이 우리가 만들 내용과 같은지 보는 것이 더 정확하다.
6
+ */
7
+ export declare function launchAgentPlist(command: ServiceCommand): string;
8
+ export type LaunchdDeps = {
9
+ home?: string;
10
+ exec?: (command: string, args: readonly string[]) => Promise<void>;
11
+ };
12
+ export declare function launchdAdapter(deps?: LaunchdDeps): PersistentRuntimeAdapter;
@@ -0,0 +1,97 @@
1
+ // macOS — user LaunchAgent (설계 §4.3).
2
+ //
3
+ // root daemon 이 아니라 **사용자 것**이다. `~/Library/LaunchAgents/` 에 살고 로그인 이후에
4
+ // 돈다. 그것이 이 계약의 경계이고, 그 위로 올리지 않는다 — 사람의 세션과 무관하게 도는
5
+ // 것은 사용자가 켜지 않은 상시 프로세스이며 다른 종류의 결정이다.
6
+ //
7
+ // `StartInterval` 로 짧은 회차를 반복시킨다. 계속 도는 프로세스를 등록하지 않는 이유는
8
+ // port 주석에 있다: 죽었을 때 되살리는 일을 OS 가 더 잘한다.
9
+ import { execFile } from 'node:child_process';
10
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
11
+ import { homedir } from 'node:os';
12
+ import { dirname, join } from 'node:path';
13
+ import { promisify } from 'node:util';
14
+ import { SERVICE_LABEL, } from "../../core/distribution/persistent-runtime.js";
15
+ const run = promisify(execFile);
16
+ export const plistPath = (home = homedir()) => join(home, 'Library', 'LaunchAgents', `${SERVICE_LABEL}.plist`);
17
+ /** XML 이스케이프. 경로에 `&` 가 있는 기계가 실제로 있다. */
18
+ const xml = (value) => value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
19
+ /**
20
+ * plist 본문. **내용이 곧 비교 기준이다** — 같으면 CURRENT, 다르면 STALE 이다.
21
+ * digest 를 따로 두지 않는 이유: 파일이 우리가 만들 내용과 같은지 보는 것이 더 정확하다.
22
+ */
23
+ export function launchAgentPlist(command) {
24
+ const args = [command.program, ...command.args].map((arg) => ` <string>${xml(arg)}</string>`).join('\n');
25
+ // 키를 정렬해 적는다 — 같은 환경이면 같은 문서여야 CURRENT 판정이 선다.
26
+ const env = Object.entries(command.environment ?? {}).sort(([a], [b]) => a.localeCompare(b));
27
+ const environment = env.length === 0
28
+ ? ''
29
+ : ` <key>EnvironmentVariables</key>
30
+ <dict>
31
+ ${env.map(([key, value]) => ` <key>${xml(key)}</key>\n <string>${xml(value)}</string>`).join('\n')}
32
+ </dict>
33
+ `;
34
+ // 회차가 무엇을 했는지 볼 자리. 없으면 launchd 는 출력을 버리고, 실패는 종료 코드 하나로만 남는다.
35
+ const logs = command.logPath
36
+ ? ` <key>StandardOutPath</key>
37
+ <string>${xml(command.logPath)}</string>
38
+ <key>StandardErrorPath</key>
39
+ <string>${xml(command.logPath)}</string>
40
+ `
41
+ : '';
42
+ return `<?xml version="1.0" encoding="UTF-8"?>
43
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
44
+ <plist version="1.0">
45
+ <dict>
46
+ <key>Label</key>
47
+ <string>${SERVICE_LABEL}</string>
48
+ <key>ProgramArguments</key>
49
+ <array>
50
+ ${args}
51
+ </array>
52
+ ${environment}${logs} <key>StartInterval</key>
53
+ <integer>${command.intervalSeconds}</integer>
54
+ <key>RunAtLoad</key>
55
+ <true/>
56
+ <key>ProcessType</key>
57
+ <string>Background</string>
58
+ </dict>
59
+ </plist>
60
+ `;
61
+ }
62
+ export function launchdAdapter(deps = {}) {
63
+ const home = deps.home ?? homedir();
64
+ const path = plistPath(home);
65
+ const exec = deps.exec ??
66
+ (async (command, args) => {
67
+ await run(command, [...args]);
68
+ });
69
+ return {
70
+ id: 'launchd',
71
+ async supported() {
72
+ return process.platform === 'darwin';
73
+ },
74
+ async status(command) {
75
+ const wanted = launchAgentPlist(command);
76
+ const existing = await readFile(path, 'utf8').catch(() => null);
77
+ if (existing === null)
78
+ return { kind: 'ABSENT' };
79
+ if (existing === wanted)
80
+ return { kind: 'CURRENT', detail: path };
81
+ // 우리 파일이 우리 내용과 다르다 — 경로나 간격이 바뀌었다는 뜻이다.
82
+ return { kind: 'STALE', detail: `${path} does not match the current runtime` };
83
+ },
84
+ async install(command) {
85
+ await mkdir(dirname(path), { recursive: true });
86
+ await writeFile(path, launchAgentPlist(command), 'utf8');
87
+ // 다시 읽히게 한다. 이미 없는 것을 unload 하는 것은 오류가 아니므로 삼킨다.
88
+ await exec('launchctl', ['unload', path]).catch(() => undefined);
89
+ await exec('launchctl', ['load', path]);
90
+ },
91
+ async uninstall() {
92
+ await exec('launchctl', ['unload', path]).catch(() => undefined);
93
+ // 우리가 만든 것만 지운다. 없으면 지울 것이 없다.
94
+ await rm(path, { force: true });
95
+ },
96
+ };
97
+ }
@@ -0,0 +1,16 @@
1
+ import { type PersistentRuntimeAdapter, type ServiceCommand } from '../../core/distribution/persistent-runtime.ts';
2
+ /** 작업 이름. `\` 없이 두면 루트 폴더에 만들어진다. */
3
+ export declare const TASK_NAME = "com.asc-agent.runtime";
4
+ /**
5
+ * `/TR` 에 들어갈 한 줄.
6
+ *
7
+ * schtasks 는 명령을 문자열 하나로 받는다 — 경로에 공백이 있으면 통째로 깨지므로
8
+ * 각 조각을 따옴표로 감싼다. 따옴표가 든 인자는 등록 자체를 깨뜨리므로 거른다.
9
+ */
10
+ export declare function taskRunLine(command: ServiceCommand): string;
11
+ /** 분 단위 반복. 1분 아래로는 Task Scheduler 가 받지 않는다. */
12
+ export declare const taskMinutes: (intervalSeconds: number) => number;
13
+ export type SchtasksDeps = {
14
+ exec?: (command: string, args: readonly string[]) => Promise<string>;
15
+ };
16
+ export declare function schtasksAdapter(deps?: SchtasksDeps): PersistentRuntimeAdapter;
@@ -0,0 +1,67 @@
1
+ // Windows — 사용자 Scheduled Task (설계 §4.3).
2
+ //
3
+ // 시스템 서비스가 아니라 **로그인한 사용자의 작업**이다. `/RU` 없이 만들면 현재 사용자로
4
+ // 등록되고, 그것이 이 계약의 경계다.
5
+ //
6
+ // 반복은 Task Scheduler 가 한다(`/SC MINUTE /MO n`). ASC 는 한 회차만 도는 명령을 준다.
7
+ import { execFile } from 'node:child_process';
8
+ import { promisify } from 'node:util';
9
+ import { SERVICE_LABEL, } from "../../core/distribution/persistent-runtime.js";
10
+ const run = promisify(execFile);
11
+ /** 작업 이름. `\` 없이 두면 루트 폴더에 만들어진다. */
12
+ export const TASK_NAME = SERVICE_LABEL;
13
+ /**
14
+ * `/TR` 에 들어갈 한 줄.
15
+ *
16
+ * schtasks 는 명령을 문자열 하나로 받는다 — 경로에 공백이 있으면 통째로 깨지므로
17
+ * 각 조각을 따옴표로 감싼다. 따옴표가 든 인자는 등록 자체를 깨뜨리므로 거른다.
18
+ */
19
+ export function taskRunLine(command) {
20
+ const quote = (value) => `\\"${value}\\"`;
21
+ return [command.program, ...command.args].map(quote).join(' ');
22
+ }
23
+ /** 분 단위 반복. 1분 아래로는 Task Scheduler 가 받지 않는다. */
24
+ export const taskMinutes = (intervalSeconds) => Math.max(1, Math.round(intervalSeconds / 60));
25
+ export function schtasksAdapter(deps = {}) {
26
+ const exec = deps.exec ??
27
+ (async (command, args) => {
28
+ const { stdout } = await run(command, [...args]);
29
+ return stdout;
30
+ });
31
+ return {
32
+ id: 'schtasks',
33
+ async supported() {
34
+ return process.platform === 'win32';
35
+ },
36
+ async status(command) {
37
+ const query = await exec('schtasks', ['/Query', '/TN', TASK_NAME, '/FO', 'LIST', '/V']).catch(() => null);
38
+ // 조회가 실패하는 것은 대개 "없다"이다. 없는 것과 못 읽은 것을 구분할 방법이
39
+ // schtasks 에는 없으므로, 없는 쪽으로 읽고 install 이 다시 판정하게 둔다.
40
+ if (query === null)
41
+ return { kind: 'ABSENT' };
42
+ const wanted = taskRunLine(command).replace(/\\"/g, '"');
43
+ // 조회 출력에 우리 명령이 그대로 들어 있는가. 없으면 낡은 등록이다.
44
+ return query.includes(wanted)
45
+ ? { kind: 'CURRENT', detail: TASK_NAME }
46
+ : { kind: 'STALE', detail: `${TASK_NAME} runs a different command` };
47
+ },
48
+ async install(command) {
49
+ // `/F` 로 덮어쓴다 — 같은 이름의 우리 등록을 지금 형태로 수렴시키는 것이다.
50
+ await exec('schtasks', [
51
+ '/Create',
52
+ '/F',
53
+ '/TN',
54
+ TASK_NAME,
55
+ '/TR',
56
+ taskRunLine(command),
57
+ '/SC',
58
+ 'MINUTE',
59
+ '/MO',
60
+ String(taskMinutes(command.intervalSeconds)),
61
+ ]);
62
+ },
63
+ async uninstall() {
64
+ await exec('schtasks', ['/Delete', '/TN', TASK_NAME, '/F']).catch(() => undefined);
65
+ },
66
+ };
67
+ }
@@ -0,0 +1,15 @@
1
+ import { type PersistentRuntimeAdapter, type ServiceCommand } from '../../core/distribution/persistent-runtime.ts';
2
+ export declare const unitDir: (home?: string) => string;
3
+ export declare const SERVICE_UNIT = "com.asc-agent.runtime.service";
4
+ export declare const TIMER_UNIT = "com.asc-agent.runtime.timer";
5
+ export declare function serviceUnit(command: ServiceCommand): string;
6
+ export declare function timerUnit(command: ServiceCommand): string;
7
+ export type SystemdDeps = {
8
+ home?: string;
9
+ exec?: (command: string, args: readonly string[]) => Promise<void>;
10
+ /** systemd --user 를 쓸 수 있는가. 테스트 주입점. */
11
+ available?: () => Promise<boolean>;
12
+ };
13
+ export declare function systemdUserAdapter(deps?: SystemdDeps): PersistentRuntimeAdapter;
14
+ /** 이 기계에 맞는 adapter. 모르면 `null` — 없는 것을 있는 척하지 않는다. */
15
+ export declare function serviceAdapterFor(platform: NodeJS.Platform): 'launchd' | 'schtasks' | 'systemd-user' | null;
@@ -0,0 +1,98 @@
1
+ // Linux — systemd --user (설계 §4.3).
2
+ //
3
+ // 사용자 세션에 묶인다. **lingering 을 조용히 켜지 않는다** — 그것은 "로그아웃해도 돈다"는
4
+ // 뜻이고, 사용자가 하지 않은 결정이다. 필요해지면 사람이 명시적으로 켠다.
5
+ //
6
+ // timer 가 주기를 갖고 service 가 한 회차를 돈다. 계속 도는 프로세스를 두지 않는 이유는
7
+ // port 주석과 같다.
8
+ import { execFile } from 'node:child_process';
9
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
10
+ import { homedir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ import { promisify } from 'node:util';
13
+ import { SERVICE_LABEL, } from "../../core/distribution/persistent-runtime.js";
14
+ const run = promisify(execFile);
15
+ export const unitDir = (home = homedir()) => join(home, '.config', 'systemd', 'user');
16
+ export const SERVICE_UNIT = `${SERVICE_LABEL}.service`;
17
+ export const TIMER_UNIT = `${SERVICE_LABEL}.timer`;
18
+ /** systemd 인자 인용. 공백이 든 경로가 통째로 깨지지 않게 한다. */
19
+ const quote = (value) => `"${value.replace(/(["\\])/g, '\\$1')}"`;
20
+ export function serviceUnit(command) {
21
+ return `[Unit]
22
+ Description=ASC persistent runtime
23
+
24
+ [Service]
25
+ Type=oneshot
26
+ ExecStart=${[command.program, ...command.args].map(quote).join(' ')}
27
+ ${Object.entries(command.environment ?? {})
28
+ .sort(([a], [b]) => a.localeCompare(b))
29
+ .map(([key, value]) => `Environment=${quote(`${key}=${value}`)}\n`)
30
+ .join('')}`;
31
+ }
32
+ export function timerUnit(command) {
33
+ return `[Unit]
34
+ Description=ASC persistent runtime schedule
35
+
36
+ [Timer]
37
+ OnBootSec=${command.intervalSeconds}
38
+ OnUnitActiveSec=${command.intervalSeconds}
39
+ Unit=${SERVICE_UNIT}
40
+
41
+ [Install]
42
+ WantedBy=timers.target
43
+ `;
44
+ }
45
+ export function systemdUserAdapter(deps = {}) {
46
+ const home = deps.home ?? homedir();
47
+ const dir = unitDir(home);
48
+ const exec = deps.exec ??
49
+ (async (command, args) => {
50
+ await run(command, [...args]);
51
+ });
52
+ const available = deps.available ??
53
+ (async () => {
54
+ if (process.platform !== 'linux')
55
+ return false;
56
+ // 컨테이너처럼 user bus 가 없는 자리가 흔하다 — 있는 척하지 않는다.
57
+ return run('systemctl', ['--user', 'show-environment'])
58
+ .then(() => true)
59
+ .catch(() => false);
60
+ });
61
+ return {
62
+ id: 'systemd-user',
63
+ supported: available,
64
+ async status(command) {
65
+ const service = await readFile(join(dir, SERVICE_UNIT), 'utf8').catch(() => null);
66
+ const timer = await readFile(join(dir, TIMER_UNIT), 'utf8').catch(() => null);
67
+ if (service === null || timer === null)
68
+ return { kind: 'ABSENT' };
69
+ return service === serviceUnit(command) && timer === timerUnit(command)
70
+ ? { kind: 'CURRENT', detail: dir }
71
+ : { kind: 'STALE', detail: `${dir} does not match the current runtime` };
72
+ },
73
+ async install(command) {
74
+ await mkdir(dir, { recursive: true });
75
+ await writeFile(join(dir, SERVICE_UNIT), serviceUnit(command), 'utf8');
76
+ await writeFile(join(dir, TIMER_UNIT), timerUnit(command), 'utf8');
77
+ await exec('systemctl', ['--user', 'daemon-reload']);
78
+ await exec('systemctl', ['--user', 'enable', '--now', TIMER_UNIT]);
79
+ },
80
+ async uninstall() {
81
+ await exec('systemctl', ['--user', 'disable', '--now', TIMER_UNIT]).catch(() => undefined);
82
+ // 우리가 만든 것만 지운다.
83
+ await rm(join(dir, TIMER_UNIT), { force: true });
84
+ await rm(join(dir, SERVICE_UNIT), { force: true });
85
+ await exec('systemctl', ['--user', 'daemon-reload']).catch(() => undefined);
86
+ },
87
+ };
88
+ }
89
+ /** 이 기계에 맞는 adapter. 모르면 `null` — 없는 것을 있는 척하지 않는다. */
90
+ export function serviceAdapterFor(platform) {
91
+ if (platform === 'darwin')
92
+ return 'launchd';
93
+ if (platform === 'win32')
94
+ return 'schtasks';
95
+ if (platform === 'linux')
96
+ return 'systemd-user';
97
+ return null;
98
+ }
@@ -32,8 +32,8 @@ export declare const IngressRecord: z.ZodObject<{
32
32
  raw?: unknown;
33
33
  hints?: Record<string, unknown> | undefined;
34
34
  };
35
- seq: number;
36
35
  receivedAt: string;
36
+ seq: number;
37
37
  }, {
38
38
  event: {
39
39
  detectedAt: string;
@@ -42,8 +42,8 @@ export declare const IngressRecord: z.ZodObject<{
42
42
  raw?: unknown;
43
43
  hints?: Record<string, unknown> | undefined;
44
44
  };
45
- seq: number;
46
45
  receivedAt: string;
46
+ seq: number;
47
47
  }>;
48
48
  export type IngressRecord = z.infer<typeof IngressRecord>;
49
49
  export type VerifyOutcome = {