@asc-agent/runtime 0.3.1 → 0.4.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 (42) 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/probe.js +6 -1
  5. package/dist/adapters/claude-code/session-start.d.ts +20 -0
  6. package/dist/adapters/claude-code/session-start.js +111 -0
  7. package/dist/adapters/jam/adapter.d.ts +46 -9
  8. package/dist/adapters/jam/adapter.js +88 -22
  9. package/dist/adapters/jam/mcp-client.js +5 -1
  10. package/dist/adapters/jam/setup.d.ts +62 -0
  11. package/dist/adapters/jam/setup.js +85 -0
  12. package/dist/adapters/service/launchd.d.ts +12 -0
  13. package/dist/adapters/service/launchd.js +80 -0
  14. package/dist/adapters/service/schtasks.d.ts +16 -0
  15. package/dist/adapters/service/schtasks.js +67 -0
  16. package/dist/adapters/service/systemd-user.d.ts +15 -0
  17. package/dist/adapters/service/systemd-user.js +95 -0
  18. package/dist/cli/asc.js +886 -147
  19. package/dist/composition/registry.js +11 -3
  20. package/dist/composition/runtime.d.ts +33 -0
  21. package/dist/composition/runtime.js +74 -0
  22. package/dist/core/attach/setup-plan.d.ts +73 -1
  23. package/dist/core/attach/setup-plan.js +56 -3
  24. package/dist/core/distribution/external-command.d.ts +22 -0
  25. package/dist/core/distribution/external-command.js +79 -0
  26. package/dist/core/distribution/persistent-runtime.d.ts +73 -0
  27. package/dist/core/distribution/persistent-runtime.js +49 -0
  28. package/dist/core/distribution/release.d.ts +3 -3
  29. package/dist/core/distribution/release.js +1 -1
  30. package/dist/core/operator/progress.js +3 -1
  31. package/dist/core/runtime/background.d.ts +104 -0
  32. package/dist/core/runtime/background.js +225 -0
  33. package/dist/core/runtime/front.d.ts +48 -0
  34. package/dist/core/runtime/front.js +34 -0
  35. package/dist/core/runtime/session.js +6 -1
  36. package/dist/core/runtime/workspaces.d.ts +39 -0
  37. package/dist/core/runtime/workspaces.js +64 -0
  38. package/dist/core/workspace/resolve.d.ts +36 -0
  39. package/dist/core/workspace/resolve.js +124 -3
  40. package/dist/ports/adapter.d.ts +12 -0
  41. package/dist/schemas/profile.d.ts +6 -6
  42. package/package.json +1 -1
@@ -9,8 +9,12 @@
9
9
  //
10
10
  // 1. explicit root 사람이 말한 것이 이긴다
11
11
  // 2. workspace index 이 기계에 등록된 locator (경로가 바뀌어도 따라온다)
12
- // 3. project-adopted 저장소 안의 `.asc/` 팀이 채택한 경우 (legacy 개인 사용 포함)
13
- // 4. UNRESOLVED 모르면 모른다고 한다
12
+ // 3. linked worktree 같은 Git repository의 다른 checkout이 등록돼 있다 (C-11 §1.3)
13
+ // 4. project-adopted 저장소 안의 `.asc/` — 팀이 채택한 경우 (legacy 개인 사용 포함)
14
+ // 5. UNRESOLVED 모르면 모른다고 한다
15
+ //
16
+ // **3번은 index가 빗나갔을 때만 돈다.** 등록된 locator로 풀리는 정상 경로는 예전 그대로
17
+ // 파일 접근 하나이며, Git을 부르지 않는다 — guard hook이 매 Bash 호출마다 지나는 길이다.
14
18
  //
15
19
  // **3번의 탐색에는 경계가 있다.** 예전에는 파일시스템 루트까지 올라갔는데, 사용자 홈에
16
20
  // `~/.asc` 가 생기는 순간 홈 아래 아무 저장소나 그 뿌리로 오인 매칭된다. 그래서 홈을
@@ -47,11 +51,112 @@ export async function resolveWorkspace(input) {
47
51
  };
48
52
  }
49
53
  }
54
+ // index가 빗나갔다. 여기서 처음으로 Git에게 **구조**를 묻는다 — remote가 같다는 이유로
55
+ // 이어붙이지 않는다 (C-11 불변식 ③). 같은 linked repository라는 것은 로컬에서 증명된다.
56
+ if (input.index && input.worktrees) {
57
+ const linked = await resolveLinkedWorktree(input.cwd, input.index, input.worktrees, exists);
58
+ if (linked)
59
+ return linked;
60
+ }
50
61
  const local = await findProjectLocal(input.cwd, input.stopAt, exists);
51
62
  if (local)
52
63
  return { kind: 'PROJECT_LOCAL', root: join(local, ASC_DIR), projectRoot: local };
53
64
  return { kind: 'UNRESOLVED', detail: '이 경로에 붙은 ASC workspace가 없다' };
54
65
  }
66
+ /**
67
+ * 이 checkout이 **이미 등록된 checkout과 같은 Git repository**인가.
68
+ *
69
+ * `git worktree list` 는 그 저장소의 checkout만 든다. 그래서 remote가 같은 **독립 clone**은
70
+ * 서로의 경로를 담지 않고, 여기서 절대 이어붙지 않는다 — 그것이 이 판정이 alias 대조가
71
+ * 아니라 worktree 목록을 쓰는 이유다 (C-11 불변식 ③: alias 일치는 recover candidate일 뿐).
72
+ *
73
+ * 후보 workspace가 둘 이상이면 **고르지 않는다.** 고르면 사람이 그 선택을 보지 못한다.
74
+ */
75
+ async function resolveLinkedWorktree(cwd, index, probe, exists) {
76
+ const paths = await probe(cwd);
77
+ if (!paths || paths.length === 0)
78
+ return null;
79
+ const normalized = paths.map(normalizeLocator);
80
+ const here = currentWorktree(normalized, normalizeLocator(cwd));
81
+ if (!here)
82
+ return null;
83
+ const hits = new Map();
84
+ for (const path of normalized) {
85
+ if (path === here)
86
+ continue;
87
+ const found = lookupLocator(index, path);
88
+ if (found)
89
+ hits.set(found.workspaceId, { root: found.root, via: found.locator });
90
+ }
91
+ if (hits.size === 0)
92
+ return null;
93
+ if (hits.size > 1) {
94
+ return {
95
+ kind: 'UNRESOLVED',
96
+ detail: `이 저장소의 다른 checkout들이 서로 다른 workspace에 등록돼 있다 (${[...hits.keys()].join(', ')}) — 고르지 않는다`,
97
+ };
98
+ }
99
+ const [[workspaceId, hit]] = [...hits.entries()];
100
+ // 등록은 돼 있는데 뿌리가 없으면 넘어가지 않는다 — REGISTERED와 같은 태도다.
101
+ if (!(await exists(hit.root))) {
102
+ return {
103
+ kind: 'UNRESOLVED',
104
+ detail: `${workspaceId} 가 등록돼 있으나 runtime(${hit.root})이 없다 — 옮겼거나 지워졌다`,
105
+ };
106
+ }
107
+ return {
108
+ kind: 'LINKED_WORKTREE',
109
+ root: hit.root,
110
+ workspaceId,
111
+ locator: here,
112
+ via: hit.via,
113
+ // 목록의 첫 항목이 main worktree다 (git worktree list 의 계약).
114
+ kindOfLocator: here === normalized[0] ? 'checkout' : 'worktree',
115
+ };
116
+ }
117
+ /**
118
+ * 목록 중 지금 서 있는 checkout. cwd는 그 아래 하위 디렉터리일 수 있으므로 **가장 긴
119
+ * 접두어**를 고른다. 경계는 구분자에서만 인정한다 — `/a/repo` 가 `/a/repo-2` 를 삼키면 안 된다.
120
+ */
121
+ function currentWorktree(paths, cwd) {
122
+ let best = null;
123
+ for (const path of paths) {
124
+ if (cwd !== path && !cwd.startsWith(`${path}/`))
125
+ continue;
126
+ if (best === null || path.length > best.length)
127
+ best = path;
128
+ }
129
+ return best;
130
+ }
131
+ /**
132
+ * 기본 통로 — `git worktree list --porcelain`. 첫 `worktree` 항목이 main checkout이다.
133
+ *
134
+ * git이 없거나 저장소가 아니면 `null`을 돌려준다. 빈 배열이 아니다 — "checkout이 하나도
135
+ * 없다"와 "물어보지 못했다"는 다른 사실이고, 후자를 전자로 적으면 조용한 오판이 된다.
136
+ */
137
+ export const gitWorktrees = async (cwd) => {
138
+ const { execFile } = await import('node:child_process');
139
+ const { promisify } = await import('node:util');
140
+ try {
141
+ const { stdout } = await promisify(execFile)('git', ['worktree', 'list', '--porcelain'], {
142
+ cwd,
143
+ // 목록은 짧다. 오래 걸리면 그것 자체가 "물어보지 못했다"이다 — 명령을 세우지 않는다.
144
+ timeout: 5_000,
145
+ });
146
+ return parseWorktreeList(stdout);
147
+ }
148
+ catch {
149
+ return null;
150
+ }
151
+ };
152
+ /** porcelain 출력에서 checkout 경로만. 순서를 보존한다 — 첫 줄이 main worktree다. */
153
+ export function parseWorktreeList(stdout) {
154
+ return stdout
155
+ .split(/\r?\n/)
156
+ .filter((line) => line.startsWith('worktree '))
157
+ .map((line) => line.slice('worktree '.length).trim())
158
+ .filter(Boolean);
159
+ }
55
160
  /**
56
161
  * 저장소 안의 `.asc/` 를 위로 올라가며 찾되 **경계를 지킨다**.
57
162
  *
@@ -66,7 +171,13 @@ async function findProjectLocal(start, stopAt, exists) {
66
171
  // 정지선 자체는 보지 않는다 — 홈의 `.asc` 는 user runtime이지 프로젝트 상태가 아니다
67
172
  if (boundary && normalized === boundary)
68
173
  return null;
69
- if (await exists(join(dir, ASC_DIR)))
174
+ const candidate = join(dir, ASC_DIR);
175
+ // 정지선이 홈이어도 **다른 홈**의 `.asc` 는 걸러지지 않는다. Windows는 temp
176
+ // 디렉터리가 사용자 프로필 아래라, temp의 프로젝트에서 위로 걷다 실사용자
177
+ // `~/.asc` 를 프로젝트 상태로 오인했다 (실 프로젝트 실측 — setup status가
178
+ // UNATTACHED 대신 BROKEN을 답한 원인). user runtime은 내용으로 알아본다:
179
+ // workspaces/·profiles/·runtime.json 은 홈에만 생긴다.
180
+ if ((await exists(candidate)) && !(await looksLikeUserRuntime(candidate, exists)))
70
181
  return dir;
71
182
  const parent = dirname(dir);
72
183
  if (parent === dir)
@@ -74,6 +185,14 @@ async function findProjectLocal(start, stopAt, exists) {
74
185
  dir = parent;
75
186
  }
76
187
  }
188
+ /** `~/.asc` 꼴인가 — 프로젝트 부착이 아니라 이 기계의 user runtime 홈인가. */
189
+ async function looksLikeUserRuntime(ascDir, exists) {
190
+ for (const marker of ['workspaces', 'profiles', 'runtime.json']) {
191
+ if (await exists(join(ascDir, marker)))
192
+ return true;
193
+ }
194
+ return false;
195
+ }
77
196
  /** 사람이 읽는 한 줄. 왜 그 뿌리인지가 함께 와야 사람이 틀린 결합을 알아챈다. */
78
197
  export function resolutionLine(resolution) {
79
198
  switch (resolution.kind) {
@@ -81,6 +200,8 @@ export function resolutionLine(resolution) {
81
200
  return `runtime: ${resolution.root} (given with --root)`;
82
201
  case 'REGISTERED':
83
202
  return `runtime: ${resolution.root} (workspace ${resolution.workspaceId} · ${resolution.locator})`;
203
+ case 'LINKED_WORKTREE':
204
+ return `runtime: ${resolution.root} (workspace ${resolution.workspaceId} · same git repository as ${resolution.via})`;
84
205
  case 'PROJECT_LOCAL':
85
206
  return `runtime: ${resolution.root} (.asc inside the repository — team-adopted, or personal state not yet migrated)`;
86
207
  case 'UNRESOLVED':
@@ -5,6 +5,18 @@ export type DiscoveryContext = {
5
5
  projectRoot: string;
6
6
  /** 환경변수. 자격 **존재 여부** 판단에만 쓰고 값을 실어 나르지 않는다. */
7
7
  env?: NodeJS.ProcessEnv;
8
+ /**
9
+ * Profile 이 이 adapter 에 대해 선언한 자원들 (C-09 §3.1).
10
+ *
11
+ * **추측의 반대다.** adapter 가 지역 흔적에서 아무것도 못 찾아도, 사람이 Profile 에
12
+ * 적어 둔 것은 이미 내려진 결정이다 — 그것을 못 본 척하면 "선언은 있는데 아무 일도
13
+ * 일어나지 않는" 상태가 된다. adapter 는 이 값을 후보로 올릴 수 있고, 실제로 되는지는
14
+ * 여전히 probe 가 정한다.
15
+ */
16
+ declared?: readonly {
17
+ adapterId: string;
18
+ resource: string;
19
+ }[];
8
20
  };
9
21
  export type ProbeResult = {
10
22
  state: ProbeState;
@@ -212,20 +212,20 @@ export declare const ProjectProfile: z.ZodPipeline<z.ZodEffects<z.ZodUnknown, un
212
212
  resource: z.ZodString;
213
213
  }, "strip", z.ZodTypeAny, {
214
214
  role: string;
215
- resource: string;
216
215
  adapter: string;
216
+ resource: string;
217
217
  }, {
218
218
  role: string;
219
- resource: string;
220
219
  adapter: string;
220
+ resource: string;
221
221
  }>, "many">>, {
222
222
  role: string;
223
- resource: string;
224
223
  adapter: string;
224
+ resource: string;
225
225
  }[] | undefined, {
226
226
  role: string;
227
- resource: string;
228
227
  adapter: string;
228
+ resource: string;
229
229
  }[] | undefined>;
230
230
  /** 프로젝트 용어를 Core 개념에 붙인다. 사람이 읽는 문서에만 쓰인다. */
231
231
  terminology: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
@@ -280,8 +280,8 @@ export declare const ProjectProfile: z.ZodPipeline<z.ZodEffects<z.ZodUnknown, un
280
280
  }> | undefined;
281
281
  bindings?: {
282
282
  role: string;
283
- resource: string;
284
283
  adapter: string;
284
+ resource: string;
285
285
  }[] | undefined;
286
286
  }, {
287
287
  id: string;
@@ -318,8 +318,8 @@ export declare const ProjectProfile: z.ZodPipeline<z.ZodEffects<z.ZodUnknown, un
318
318
  }> | undefined;
319
319
  bindings?: {
320
320
  role: string;
321
- resource: string;
322
321
  adapter: string;
322
+ resource: string;
323
323
  }[] | undefined;
324
324
  policy?: {
325
325
  hardDeny?: string[] | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asc-agent/runtime",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "ASC (Agent Session Control) \u2014 local-first human-in-the-loop agent control plane: core, CLI, and adapters",
5
5
  "keywords": [
6
6
  "agent",