@asc-agent/runtime 0.1.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 (210) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +22 -0
  3. package/dist/adapters/claude-code/binding.d.ts +5 -0
  4. package/dist/adapters/claude-code/binding.js +14 -0
  5. package/dist/adapters/claude-code/guard.d.ts +54 -0
  6. package/dist/adapters/claude-code/guard.js +295 -0
  7. package/dist/adapters/claude-code/install.d.ts +67 -0
  8. package/dist/adapters/claude-code/install.js +231 -0
  9. package/dist/adapters/claude-code/observer.d.ts +38 -0
  10. package/dist/adapters/claude-code/observer.js +77 -0
  11. package/dist/adapters/claude-code/probe.d.ts +53 -0
  12. package/dist/adapters/claude-code/probe.js +129 -0
  13. package/dist/adapters/claude-code/skill.d.ts +8 -0
  14. package/dist/adapters/claude-code/skill.js +267 -0
  15. package/dist/adapters/fixture-work/index.d.ts +34 -0
  16. package/dist/adapters/fixture-work/index.js +78 -0
  17. package/dist/adapters/github/adapter.d.ts +32 -0
  18. package/dist/adapters/github/adapter.js +95 -0
  19. package/dist/adapters/github/client.d.ts +30 -0
  20. package/dist/adapters/github/client.js +90 -0
  21. package/dist/adapters/github/context.d.ts +44 -0
  22. package/dist/adapters/github/context.js +170 -0
  23. package/dist/adapters/github/event-source.d.ts +55 -0
  24. package/dist/adapters/github/event-source.js +183 -0
  25. package/dist/adapters/github/scm.d.ts +50 -0
  26. package/dist/adapters/github/scm.js +104 -0
  27. package/dist/adapters/gitlab/adapter.d.ts +36 -0
  28. package/dist/adapters/gitlab/adapter.js +117 -0
  29. package/dist/adapters/gitlab/client.d.ts +30 -0
  30. package/dist/adapters/gitlab/client.js +47 -0
  31. package/dist/adapters/gitlab/ports.d.ts +40 -0
  32. package/dist/adapters/gitlab/ports.js +186 -0
  33. package/dist/adapters/jam/adapter.d.ts +53 -0
  34. package/dist/adapters/jam/adapter.js +144 -0
  35. package/dist/adapters/jam/event-source.d.ts +28 -0
  36. package/dist/adapters/jam/event-source.js +65 -0
  37. package/dist/adapters/jam/mcp-client.d.ts +50 -0
  38. package/dist/adapters/jam/mcp-client.js +218 -0
  39. package/dist/adapters/jam/ports.d.ts +51 -0
  40. package/dist/adapters/jam/ports.js +167 -0
  41. package/dist/adapters/local/identity.d.ts +27 -0
  42. package/dist/adapters/local/identity.js +40 -0
  43. package/dist/adapters/local/presentation.d.ts +14 -0
  44. package/dist/adapters/local/presentation.js +42 -0
  45. package/dist/adapters/markdown/layout.d.ts +15 -0
  46. package/dist/adapters/markdown/layout.js +49 -0
  47. package/dist/adapters/markdown/serialize.d.ts +9 -0
  48. package/dist/adapters/markdown/serialize.js +120 -0
  49. package/dist/adapters/markdown/state-store.d.ts +23 -0
  50. package/dist/adapters/markdown/state-store.js +316 -0
  51. package/dist/adapters/mattermost/client.d.ts +23 -0
  52. package/dist/adapters/mattermost/client.js +54 -0
  53. package/dist/adapters/mattermost/presentation.d.ts +43 -0
  54. package/dist/adapters/mattermost/presentation.js +88 -0
  55. package/dist/adapters/memory/mocks.d.ts +87 -0
  56. package/dist/adapters/memory/mocks.js +163 -0
  57. package/dist/adapters/memory/runtime-binding.d.ts +23 -0
  58. package/dist/adapters/memory/runtime-binding.js +118 -0
  59. package/dist/adapters/memory/state-store.d.ts +17 -0
  60. package/dist/adapters/memory/state-store.js +125 -0
  61. package/dist/adapters/text/renderer.d.ts +7 -0
  62. package/dist/adapters/text/renderer.js +81 -0
  63. package/dist/adapters/webhook/ingress.d.ts +103 -0
  64. package/dist/adapters/webhook/ingress.js +150 -0
  65. package/dist/cli/asc.d.ts +16 -0
  66. package/dist/cli/asc.js +2914 -0
  67. package/dist/cli/identity-config.d.ts +7 -0
  68. package/dist/cli/identity-config.js +31 -0
  69. package/dist/composition/observe.d.ts +21 -0
  70. package/dist/composition/observe.js +72 -0
  71. package/dist/composition/registry.d.ts +33 -0
  72. package/dist/composition/registry.js +62 -0
  73. package/dist/composition/runtime.d.ts +63 -0
  74. package/dist/composition/runtime.js +155 -0
  75. package/dist/core/approval/service.d.ts +17 -0
  76. package/dist/core/approval/service.js +129 -0
  77. package/dist/core/attach/bootstrap.d.ts +102 -0
  78. package/dist/core/attach/bootstrap.js +178 -0
  79. package/dist/core/attach/init.d.ts +29 -0
  80. package/dist/core/attach/init.js +100 -0
  81. package/dist/core/attach/setup-plan.d.ts +125 -0
  82. package/dist/core/attach/setup-plan.js +177 -0
  83. package/dist/core/attach/setup.d.ts +40 -0
  84. package/dist/core/attach/setup.js +140 -0
  85. package/dist/core/binding/types.d.ts +93 -0
  86. package/dist/core/binding/types.js +74 -0
  87. package/dist/core/distribution/release.d.ts +15 -0
  88. package/dist/core/distribution/release.js +27 -0
  89. package/dist/core/distribution/runtime-install.d.ts +50 -0
  90. package/dist/core/distribution/runtime-install.js +90 -0
  91. package/dist/core/distribution/runtime-select.d.ts +74 -0
  92. package/dist/core/distribution/runtime-select.js +149 -0
  93. package/dist/core/execution/executor.d.ts +51 -0
  94. package/dist/core/execution/executor.js +106 -0
  95. package/dist/core/execution/grant.d.ts +42 -0
  96. package/dist/core/execution/grant.js +78 -0
  97. package/dist/core/model/entities.d.ts +872 -0
  98. package/dist/core/model/entities.js +285 -0
  99. package/dist/core/model/ids.d.ts +41 -0
  100. package/dist/core/model/ids.js +50 -0
  101. package/dist/core/model/transitions.d.ts +26 -0
  102. package/dist/core/model/transitions.js +124 -0
  103. package/dist/core/monitor/coverage.d.ts +111 -0
  104. package/dist/core/monitor/coverage.js +129 -0
  105. package/dist/core/monitor/engine.d.ts +134 -0
  106. package/dist/core/monitor/engine.js +574 -0
  107. package/dist/core/monitor/health-alerts.d.ts +35 -0
  108. package/dist/core/monitor/health-alerts.js +88 -0
  109. package/dist/core/monitor/investigation.d.ts +82 -0
  110. package/dist/core/monitor/investigation.js +232 -0
  111. package/dist/core/monitor/observation.d.ts +79 -0
  112. package/dist/core/monitor/observation.js +110 -0
  113. package/dist/core/monitor/relevance.d.ts +47 -0
  114. package/dist/core/monitor/relevance.js +100 -0
  115. package/dist/core/monitor/signals.d.ts +59 -0
  116. package/dist/core/monitor/signals.js +105 -0
  117. package/dist/core/operator/local-operator.d.ts +60 -0
  118. package/dist/core/operator/local-operator.js +82 -0
  119. package/dist/core/operator/preflight.d.ts +60 -0
  120. package/dist/core/operator/preflight.js +139 -0
  121. package/dist/core/operator/proceed.d.ts +98 -0
  122. package/dist/core/operator/proceed.js +167 -0
  123. package/dist/core/operator/progress.d.ts +124 -0
  124. package/dist/core/operator/progress.js +135 -0
  125. package/dist/core/operator/render.d.ts +34 -0
  126. package/dist/core/operator/render.js +170 -0
  127. package/dist/core/operator/runtime-binding.d.ts +61 -0
  128. package/dist/core/operator/runtime-binding.js +22 -0
  129. package/dist/core/policy/ownership.d.ts +44 -0
  130. package/dist/core/policy/ownership.js +46 -0
  131. package/dist/core/policy/policy.d.ts +99 -0
  132. package/dist/core/policy/policy.js +147 -0
  133. package/dist/core/policy/remote-freeze.d.ts +101 -0
  134. package/dist/core/policy/remote-freeze.js +151 -0
  135. package/dist/core/policy/scope.d.ts +23 -0
  136. package/dist/core/policy/scope.js +93 -0
  137. package/dist/core/presentation/digest.d.ts +98 -0
  138. package/dist/core/presentation/digest.js +160 -0
  139. package/dist/core/resolver/load.d.ts +121 -0
  140. package/dist/core/resolver/load.js +246 -0
  141. package/dist/core/resolver/render.d.ts +4 -0
  142. package/dist/core/resolver/render.js +62 -0
  143. package/dist/core/resolver/resolve.d.ts +42 -0
  144. package/dist/core/resolver/resolve.js +51 -0
  145. package/dist/core/resolver/version.d.ts +22 -0
  146. package/dist/core/resolver/version.js +49 -0
  147. package/dist/core/runtime/audit.d.ts +374 -0
  148. package/dist/core/runtime/audit.js +454 -0
  149. package/dist/core/runtime/claims.d.ts +115 -0
  150. package/dist/core/runtime/claims.js +153 -0
  151. package/dist/core/runtime/closure.d.ts +73 -0
  152. package/dist/core/runtime/closure.js +162 -0
  153. package/dist/core/runtime/controller.d.ts +40 -0
  154. package/dist/core/runtime/controller.js +121 -0
  155. package/dist/core/runtime/escalation.d.ts +188 -0
  156. package/dist/core/runtime/escalation.js +322 -0
  157. package/dist/core/runtime/execution-state.d.ts +43 -0
  158. package/dist/core/runtime/execution-state.js +81 -0
  159. package/dist/core/runtime/front.d.ts +95 -0
  160. package/dist/core/runtime/front.js +144 -0
  161. package/dist/core/runtime/orchestrator.d.ts +54 -0
  162. package/dist/core/runtime/orchestrator.js +98 -0
  163. package/dist/core/runtime/query.d.ts +184 -0
  164. package/dist/core/runtime/query.js +213 -0
  165. package/dist/core/runtime/report.d.ts +33 -0
  166. package/dist/core/runtime/report.js +108 -0
  167. package/dist/core/runtime/session.d.ts +156 -0
  168. package/dist/core/runtime/session.js +281 -0
  169. package/dist/core/runtime/store-ops.d.ts +22 -0
  170. package/dist/core/runtime/store-ops.js +26 -0
  171. package/dist/core/view/build-view.d.ts +36 -0
  172. package/dist/core/view/build-view.js +131 -0
  173. package/dist/core/view/decision-view.d.ts +564 -0
  174. package/dist/core/view/decision-view.js +103 -0
  175. package/dist/core/workspace/identity.d.ts +82 -0
  176. package/dist/core/workspace/identity.js +133 -0
  177. package/dist/core/workspace/index-store.d.ts +140 -0
  178. package/dist/core/workspace/index-store.js +125 -0
  179. package/dist/core/workspace/migrate.d.ts +73 -0
  180. package/dist/core/workspace/migrate.js +123 -0
  181. package/dist/core/workspace/resolve.d.ts +34 -0
  182. package/dist/core/workspace/resolve.js +89 -0
  183. package/dist/ports/adapter.d.ts +35 -0
  184. package/dist/ports/adapter.js +12 -0
  185. package/dist/ports/approval.d.ts +98 -0
  186. package/dist/ports/approval.js +7 -0
  187. package/dist/ports/change-context.d.ts +22 -0
  188. package/dist/ports/change-context.js +8 -0
  189. package/dist/ports/event-source.d.ts +42 -0
  190. package/dist/ports/event-source.js +6 -0
  191. package/dist/ports/inventory.d.ts +53 -0
  192. package/dist/ports/inventory.js +12 -0
  193. package/dist/ports/presentation.d.ts +62 -0
  194. package/dist/ports/presentation.js +13 -0
  195. package/dist/ports/renderer.d.ts +23 -0
  196. package/dist/ports/renderer.js +7 -0
  197. package/dist/ports/resource-context.d.ts +51 -0
  198. package/dist/ports/resource-context.js +9 -0
  199. package/dist/ports/scm.d.ts +40 -0
  200. package/dist/ports/scm.js +6 -0
  201. package/dist/ports/state-store.d.ts +97 -0
  202. package/dist/ports/state-store.js +12 -0
  203. package/dist/presets/balanced.json +8 -0
  204. package/dist/presets/conservative.json +9 -0
  205. package/dist/presets/lightweight.json +8 -0
  206. package/dist/profiles/example-team/profile.json +101 -0
  207. package/dist/profiles/pilot-local/profile.json +56 -0
  208. package/dist/schemas/profile.d.ts +631 -0
  209. package/dist/schemas/profile.js +287 -0
  210. package/package.json +55 -0
@@ -0,0 +1,117 @@
1
+ // GitLab Adapter 계약 — describe / discover / probe (C-09 §5).
2
+ //
3
+ // GitHub adapter와 같은 세 함수를 갖되 내용은 다르다. 그 "같은 모양 다른 내용"이
4
+ // Adapter 계약이 실제로 계약이라는 증거다.
5
+ import { execFile } from 'node:child_process';
6
+ import { promisify } from 'node:util';
7
+ import { GitLabClient, discoverToken, encodeProject } from "./client.js";
8
+ const run = promisify(execFile);
9
+ const PROVIDES = [
10
+ 'observe.delta',
11
+ 'inventory.enumerate',
12
+ 'context.resource',
13
+ 'context.thread',
14
+ 'context.change',
15
+ ];
16
+ /**
17
+ * 이 host는 다른 adapter가 맡는다. 여기서 후보로 잡으면 같은 remote를 둘이 주장한다.
18
+ *
19
+ * 목록이 아니라 **판정**이므로 짧게 유지한다 — 여기가 길어지기 시작하면 그건 adapter가
20
+ * provider 목록을 아는 물건이 됐다는 뜻이다.
21
+ */
22
+ const CLAIMED_ELSEWHERE = new Set(['github.com']);
23
+ export class GitLabAdapter {
24
+ #listRemotes;
25
+ #findToken;
26
+ #reach;
27
+ #host;
28
+ /**
29
+ * 후보를 찾을 때 본 host. probe가 어디로 물어봐야 하는지 알아야 하는데 후보 자체는
30
+ * 경로만 들고 있다 — Core 타입에 자리를 만드는 대신 여기 둔다.
31
+ *
32
+ * discover와 probe를 같은 인스턴스가 이어서 부르는 것이 조립 순서다(composition).
33
+ * 그렇지 않은 호출자는 `ASC_GITLAB_URL` 로 명시하면 된다.
34
+ */
35
+ #endpoints = new Map();
36
+ constructor(deps = {}) {
37
+ this.#listRemotes = deps.listRemotes ?? defaultListRemotes;
38
+ this.#findToken = deps.findToken ?? discoverToken;
39
+ this.#reach = deps.reach;
40
+ this.#host = deps.host;
41
+ }
42
+ describe() {
43
+ return {
44
+ id: 'gitlab',
45
+ version: '1',
46
+ provides: PROVIDES,
47
+ requiresCredential: ['ASC_GITLAB_TOKEN | GITLAB_TOKEN'],
48
+ prerequisites: ['git remote 가 이 host를 가리켜야 한다'],
49
+ };
50
+ }
51
+ /**
52
+ * remote **전부**를 본다. `origin` 하나만 보면 origin이 코드 정본이 아닌 프로젝트에서
53
+ * 아무것도 못 찾는다 (지시 §13).
54
+ *
55
+ * host를 미리 알고 있지 않다 — 자체 호스팅이 흔하고, 주소를 코드에 박으면 그 프로젝트
56
+ * 하나를 위한 adapter가 된다. 대신 **다른 adapter가 맡는 host만 비켜** 나머지를 후보로
57
+ * 올린다. 후보는 추측이 아니라 후보이며, 실제로 되는지는 probe가 정한다.
58
+ */
59
+ async discover(context) {
60
+ const remotes = await this.#listRemotes(context.projectRoot).catch(() => []);
61
+ const override = this.#host ?? context.env?.ASC_GITLAB_HOST;
62
+ const found = new Map();
63
+ for (const url of remotes) {
64
+ const parsed = parseRemote(url);
65
+ if (!parsed)
66
+ continue;
67
+ if (override ? parsed.host !== override : CLAIMED_ELSEWHERE.has(parsed.host))
68
+ continue;
69
+ // 하위 그룹이 흔하다 — `group/sub/project` 를 통째로 자원 이름으로 쓴다.
70
+ this.#endpoints.set(parsed.project, `https://${parsed.host}/api/v4`);
71
+ found.set(parsed.project, {
72
+ adapterId: 'gitlab',
73
+ resource: parsed.project,
74
+ provides: PROVIDES,
75
+ discoveredBy: `git remote (${parsed.host})`,
76
+ });
77
+ }
78
+ return [...found.values()];
79
+ }
80
+ async probe(candidate, context) {
81
+ const token = this.#findToken(context.env);
82
+ if (!token) {
83
+ return { state: 'UNCONFIGURED', detail: '토큰이 없다 — ASC_GITLAB_TOKEN 또는 GITLAB_TOKEN 을 두어라' };
84
+ }
85
+ const baseUrl = context.env?.ASC_GITLAB_URL ?? this.#endpoints.get(candidate.resource);
86
+ const reach = this.#reach ?? ((project, secret) => defaultReach(project, secret, baseUrl));
87
+ const result = await reach(candidate.resource, token);
88
+ return result.ok
89
+ ? { state: 'AVAILABLE', provides: candidate.provides }
90
+ : { state: 'UNAVAILABLE', detail: result.detail ?? '프로젝트에 닿지 못했다' };
91
+ }
92
+ /** 이 후보가 어느 주소를 가리키는지. composition이 Port를 만들 때 같은 값을 써야 한다. */
93
+ endpointFor(resource) {
94
+ return this.#endpoints.get(resource);
95
+ }
96
+ }
97
+ export function parseRemote(url) {
98
+ const ssh = /^[^@]+@([^:]+):(.+?)(?:\.git)?$/.exec(url.trim());
99
+ if (ssh)
100
+ return { host: ssh[1], project: ssh[2] };
101
+ const https = /^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/.exec(url.trim());
102
+ if (https)
103
+ return { host: https[1], project: https[2] };
104
+ return null;
105
+ }
106
+ async function defaultListRemotes(projectRoot) {
107
+ const { stdout } = await run('git', ['-C', projectRoot, 'remote', '-v']);
108
+ return stdout
109
+ .split('\n')
110
+ .map((line) => line.split(/\s+/)[1])
111
+ .filter((url) => Boolean(url));
112
+ }
113
+ async function defaultReach(project, token, baseUrl) {
114
+ const client = new GitLabClient({ token, ...(baseUrl ? { baseUrl } : {}) });
115
+ const response = await client.get(`/projects/${encodeProject(project)}`);
116
+ return response.ok ? { ok: true } : { ok: false, detail: `HTTP ${response.status}` };
117
+ }
@@ -0,0 +1,30 @@
1
+ export type Fetch = typeof globalThis.fetch;
2
+ export type GitLabClientDeps = {
3
+ token: string;
4
+ fetch?: Fetch;
5
+ /** self-hosted가 흔하다. 기본값은 공개 인스턴스. */
6
+ baseUrl?: string;
7
+ };
8
+ export type GitLabResponse<T> = {
9
+ ok: boolean;
10
+ status: number;
11
+ data: T | null;
12
+ /** 다음 페이지 번호. 없으면 끝이다. */
13
+ nextPage?: string;
14
+ error?: string;
15
+ };
16
+ /** 환경변수에서만 찾는다. 값을 파일이나 Profile에 남기지 않는다. */
17
+ export declare function discoverToken(env?: NodeJS.ProcessEnv): string | null;
18
+ export declare class GitLabClient {
19
+ #private;
20
+ constructor(deps: GitLabClientDeps);
21
+ get<T>(path: string): Promise<GitLabResponse<T>>;
22
+ }
23
+ /** `group/sub/project!19` 또는 `group/project#7` 을 쪼갠다. */
24
+ export declare function parseRef(reference: string): {
25
+ project: string;
26
+ kind: 'change' | 'issue';
27
+ iid: number;
28
+ } | null;
29
+ /** 경로를 URL 조각으로. GitLab은 프로젝트 경로를 통째로 인코딩해 받는다. */
30
+ export declare const encodeProject: (project: string) => string;
@@ -0,0 +1,47 @@
1
+ // GitLab REST 최소 클라이언트.
2
+ //
3
+ // GitHub 쪽과 같은 모양을 일부러 유지한다 — 두 adapter가 비슷하게 생겼다는 사실이
4
+ // "Core가 바뀌지 않는다"를 보여주는 근거의 일부다. 토큰은 여기서 만들지도 저장하지도
5
+ // 않는다 (OM §4.5).
6
+ //
7
+ // fetch를 주입받는 이유는 테스트 때문이다. 실 네트워크에 기대는 테스트는 남의 사정으로
8
+ // 깨지고, 그러면 아무도 안 보게 된다.
9
+ /** 환경변수에서만 찾는다. 값을 파일이나 Profile에 남기지 않는다. */
10
+ export function discoverToken(env = process.env) {
11
+ return env.ASC_GITLAB_TOKEN ?? env.GITLAB_TOKEN ?? null;
12
+ }
13
+ export class GitLabClient {
14
+ #token;
15
+ #fetch;
16
+ #baseUrl;
17
+ constructor(deps) {
18
+ this.#token = deps.token;
19
+ this.#fetch = deps.fetch ?? globalThis.fetch;
20
+ this.#baseUrl = deps.baseUrl ?? 'https://gitlab.com/api/v4';
21
+ }
22
+ async get(path) {
23
+ const response = await this.#fetch(`${this.#baseUrl}${path}`, {
24
+ headers: { accept: 'application/json', 'private-token': this.#token },
25
+ });
26
+ const nextPage = response.headers.get('x-next-page') ?? undefined;
27
+ if (!response.ok) {
28
+ return { ok: false, status: response.status, data: null, error: `HTTP ${response.status}` };
29
+ }
30
+ return {
31
+ ok: true,
32
+ status: response.status,
33
+ data: (await response.json()),
34
+ // 빈 문자열은 "다음 없음"이다 — 그대로 실어 보내면 0페이지를 영원히 돈다.
35
+ ...(nextPage ? { nextPage } : {}),
36
+ };
37
+ }
38
+ }
39
+ /** `group/sub/project!19` 또는 `group/project#7` 을 쪼갠다. */
40
+ export function parseRef(reference) {
41
+ const match = /^(.+?)([!#])(\d+)$/.exec(reference.trim());
42
+ if (!match)
43
+ return null;
44
+ return { project: match[1], kind: match[2] === '!' ? 'change' : 'issue', iid: Number(match[3]) };
45
+ }
46
+ /** 경로를 URL 조각으로. GitLab은 프로젝트 경로를 통째로 인코딩해 받는다. */
47
+ export const encodeProject = (project) => encodeURIComponent(project);
@@ -0,0 +1,40 @@
1
+ import type { ChangeContextPort, ChangeSummary } from '../../ports/change-context.ts';
2
+ import type { Cursor, EventBatch, EventSource } from '../../ports/event-source.ts';
3
+ import type { InventoryPage, InventoryPort, InventoryQuery } from '../../ports/inventory.ts';
4
+ import type { CommentQuery, ContextComment, ResourceContextPort, ResourceSnapshot } from '../../ports/resource-context.ts';
5
+ import { type GitLabClient } from './client.ts';
6
+ export type GitLabDeps = {
7
+ client: GitLabClient;
8
+ /** `group/sub/project`. */
9
+ project: string;
10
+ perPage?: number;
11
+ };
12
+ declare abstract class GitLabBase {
13
+ readonly id: string;
14
+ protected client: GitLabClient;
15
+ protected project: string;
16
+ protected perPage: number;
17
+ constructor(deps: GitLabDeps);
18
+ protected path(reference: string): string | null;
19
+ }
20
+ /**
21
+ * 할 일 목록(todo)으로 증분을 받는다. GitHub의 알림과 같은 자리이고, 같은 한계를 갖는다 —
22
+ * 지정이 빠지면 오지 않는다. 그래서 Inventory가 따로 있다.
23
+ */
24
+ export declare class GitLabEventSource extends GitLabBase implements EventSource {
25
+ readonly id = "gitlab-todo";
26
+ cursorFrom(since: string): Cursor;
27
+ drain(cursor: Cursor): Promise<EventBatch>;
28
+ }
29
+ /** 상태 무관 열거. 이슈와 변경요청을 각각 받아 합친다 — GitLab은 두 목록이 따로다. */
30
+ export declare class GitLabInventory extends GitLabBase implements InventoryPort {
31
+ enumerate(query: InventoryQuery, cursor?: string): Promise<InventoryPage>;
32
+ }
33
+ export declare class GitLabResourceContext extends GitLabBase implements ResourceContextPort {
34
+ getResource(reference: string): Promise<ResourceSnapshot>;
35
+ getComments(reference: string, query?: CommentQuery): Promise<ContextComment[]>;
36
+ }
37
+ export declare class GitLabChangeContext extends GitLabBase implements ChangeContextPort {
38
+ getChange(reference: string): Promise<ChangeSummary>;
39
+ }
40
+ export {};
@@ -0,0 +1,186 @@
1
+ // GitLab Adapter — EventSource · Inventory · ResourceContext · ChangeContext.
2
+ //
3
+ // 이 adapter의 목적은 GitLab 지원 자체가 아니라 **같은 Core가 다른 adapter를 Binding
4
+ // 교체만으로 소비하는지** 보이는 것이다 (C-09 §9). 그래서 일부러 다르게 생긴 것을 고른다:
5
+ //
6
+ // 참조 문법 `group/sub/proj!19` (변경요청) · `group/proj#7` (이슈)
7
+ // event kind `todo:` · `note:` — GitHub의 `notification:` 과 다르다
8
+ // 상태 어휘 opened / closed / merged
9
+ //
10
+ // Core가 이 셋 중 무엇도 해석하지 않는다는 것이 Swap Gate의 내용이다.
11
+ import { encodeProject, parseRef } from "./client.js";
12
+ const marker = (parts) => parts.map((p) => (p === undefined ? '' : String(p))).join('|');
13
+ class GitLabBase {
14
+ // 하위 클래스가 좁혀 쓴다 — EventSource만 `gitlab-todo` 로 자기 통로를 밝힌다.
15
+ id = 'gitlab';
16
+ client;
17
+ project;
18
+ perPage;
19
+ constructor(deps) {
20
+ this.client = deps.client;
21
+ this.project = deps.project;
22
+ this.perPage = deps.perPage ?? 50;
23
+ }
24
+ path(reference) {
25
+ const ref = parseRef(reference);
26
+ if (!ref)
27
+ return null;
28
+ const kind = ref.kind === 'change' ? 'merge_requests' : 'issues';
29
+ return `/projects/${encodeProject(ref.project)}/${kind}/${ref.iid}`;
30
+ }
31
+ }
32
+ /**
33
+ * 할 일 목록(todo)으로 증분을 받는다. GitHub의 알림과 같은 자리이고, 같은 한계를 갖는다 —
34
+ * 지정이 빠지면 오지 않는다. 그래서 Inventory가 따로 있다.
35
+ */
36
+ export class GitLabEventSource extends GitLabBase {
37
+ id = 'gitlab-todo';
38
+ cursorFrom(since) {
39
+ return JSON.stringify({ since });
40
+ }
41
+ async drain(cursor) {
42
+ const parsed = cursor ? JSON.parse(cursor) : {};
43
+ const params = new URLSearchParams({ per_page: String(this.perPage) });
44
+ if (parsed.page)
45
+ params.set('page', parsed.page);
46
+ const response = await this.client.get(`/todos?${params}`);
47
+ if (!response.ok || !response.data)
48
+ return { events: [], cursor };
49
+ const events = [];
50
+ for (const todo of response.data) {
51
+ // 기준선 이전 것은 버린다. provider가 since를 지원하지 않아 여기서 거른다 —
52
+ // 겹쳐 읽고 key로 중복을 거르는 편이 놓치는 것보다 싸다 (OM §10.5).
53
+ if (parsed.since && todo.updated_at < parsed.since)
54
+ continue;
55
+ const project = todo.project?.path_with_namespace ?? this.project;
56
+ const separator = todo.target_type === 'MergeRequest' ? '!' : '#';
57
+ events.push({
58
+ eventKey: `todo:${todo.id}:${todo.updated_at}`,
59
+ detectedAt: todo.updated_at,
60
+ reference: `${project}${separator}${todo.target?.iid ?? 0}`,
61
+ ...(todo.author?.username ? { hints: { actors: [todo.author.username] } } : {}),
62
+ raw: {
63
+ kind: 'todo',
64
+ // provider 어휘 그대로 둔다 — Generic Signal로 옮기는 것은 Profile의 몫이다 (OM §10.6).
65
+ reason: todo.action_name,
66
+ title: todo.target?.title,
67
+ body: todo.body,
68
+ },
69
+ });
70
+ }
71
+ const next = response.nextPage
72
+ ? JSON.stringify({ ...(parsed.since ? { since: parsed.since } : {}), page: response.nextPage })
73
+ : JSON.stringify({ since: events.at(-1)?.detectedAt ?? parsed.since });
74
+ return { events, cursor: next, ...(response.nextPage ? { hasMore: true } : {}) };
75
+ }
76
+ }
77
+ /** 상태 무관 열거. 이슈와 변경요청을 각각 받아 합친다 — GitLab은 두 목록이 따로다. */
78
+ export class GitLabInventory extends GitLabBase {
79
+ async enumerate(query, cursor) {
80
+ const page = cursor ?? '1';
81
+ const base = new URLSearchParams({ per_page: String(this.perPage), page, scope: 'all' });
82
+ if (query.updatedSince)
83
+ base.set('updated_after', query.updatedSince);
84
+ const wanted = new Set(query.kinds ?? []);
85
+ const items = [];
86
+ let next;
87
+ for (const [kind, path, separator] of [
88
+ ['issue', 'issues', '#'],
89
+ ['change', 'merge_requests', '!'],
90
+ ]) {
91
+ if (wanted.size > 0 && !wanted.has(kind))
92
+ continue;
93
+ const response = await this.client.get(`/projects/${encodeProject(this.project)}/${path}?${base}`);
94
+ // 한 갈래라도 못 읽었으면 완주했다고 말할 수 없다.
95
+ if (!response.ok || !response.data)
96
+ return { items, complete: false };
97
+ if (response.nextPage)
98
+ next = response.nextPage;
99
+ for (const row of response.data) {
100
+ items.push({
101
+ reference: `${this.project}${separator}${row.iid}`,
102
+ state: row.state,
103
+ updatedAt: row.updated_at,
104
+ revisionMarker: marker([row.updated_at, row.sha, row.state, row.user_notes_count]),
105
+ title: row.title,
106
+ assignees: (row.assignees ?? []).map((a) => a.username),
107
+ labels: row.labels ?? [],
108
+ });
109
+ }
110
+ }
111
+ return { items, ...(next ? { next } : {}), complete: next === undefined };
112
+ }
113
+ }
114
+ export class GitLabResourceContext extends GitLabBase {
115
+ async getResource(reference) {
116
+ const path = this.path(reference);
117
+ if (!path)
118
+ return missing(reference);
119
+ const response = await this.client.get(path);
120
+ if (!response.ok || !response.data)
121
+ return missing(reference);
122
+ const row = response.data;
123
+ return {
124
+ reference,
125
+ state: row.state,
126
+ title: row.title,
127
+ ...(row.description ? { body: row.description } : {}),
128
+ ...(row.author?.username ? { author: row.author.username } : {}),
129
+ assignees: (row.assignees ?? []).map((a) => a.username),
130
+ labels: row.labels ?? [],
131
+ updatedAt: row.updated_at,
132
+ revisionMarker: marker([row.updated_at, row.sha, row.state, row.user_notes_count]),
133
+ };
134
+ }
135
+ async getComments(reference, query = {}) {
136
+ const path = this.path(reference);
137
+ if (!path)
138
+ return [];
139
+ const params = new URLSearchParams({ per_page: String(query.limit ?? 20), sort: 'desc' });
140
+ const response = await this.client.get(`${path}/notes?${params}`);
141
+ if (!response.ok || !response.data)
142
+ return [];
143
+ return response.data
144
+ .filter((note) => !query.since || note.updated_at >= query.since)
145
+ .map((note) => ({
146
+ id: String(note.id),
147
+ author: note.author?.username ?? '(unknown)',
148
+ at: note.updated_at ?? note.created_at,
149
+ body: note.body ?? '',
150
+ // 여기는 resolvable 여부를 알려준다 — 아는 것만 적는다.
151
+ ...(note.resolvable ? { unresolved: note.resolved !== true } : {}),
152
+ }));
153
+ }
154
+ }
155
+ export class GitLabChangeContext extends GitLabBase {
156
+ async getChange(reference) {
157
+ const ref = parseRef(reference);
158
+ if (!ref || ref.kind !== 'change') {
159
+ return { reference, changedPaths: [], revisionMarker: '', missing: true };
160
+ }
161
+ const base = `/projects/${encodeProject(ref.project)}/merge_requests/${ref.iid}`;
162
+ const detail = await this.client.get(base);
163
+ if (!detail.ok || !detail.data)
164
+ return { reference, changedPaths: [], revisionMarker: '', missing: true };
165
+ const changes = await this.client.get(`${base}/changes`);
166
+ const paths = (changes.data?.changes ?? []).map((c) => c.new_path ?? c.old_path ?? '').filter(Boolean);
167
+ return {
168
+ reference,
169
+ changedPaths: paths,
170
+ // provider가 "다 못 준다"고 말해 준다. 그 사실을 숨기면 "내 영역은 안 바뀌었다"는
171
+ // 틀린 판정이 나온다.
172
+ ...(changes.data?.overflow || !changes.ok ? { truncated: true } : {}),
173
+ ...(detail.data.sha ? { revisions: [detail.data.sha] } : {}),
174
+ revisionMarker: marker([detail.data.updated_at, detail.data.sha, detail.data.state]),
175
+ ...(detail.data.detailed_merge_status ? { reviewState: detail.data.detailed_merge_status } : {}),
176
+ };
177
+ }
178
+ }
179
+ const missing = (reference) => ({
180
+ reference,
181
+ state: 'unknown',
182
+ title: '',
183
+ updatedAt: '',
184
+ revisionMarker: '',
185
+ missing: true,
186
+ });
@@ -0,0 +1,53 @@
1
+ import type { AdapterDescriptor, BindingCandidate } from '../../core/binding/types.ts';
2
+ import type { Adapter, DiscoveryContext, ProbeResult, RuntimeStatus } from '../../ports/adapter.ts';
3
+ export type JamAdapterDeps = {
4
+ /**
5
+ * JAM 실행 명령. 아직 패키지로 배포되지 않아 경로가 환경마다 다르다 —
6
+ * 그래서 환경에서 받고, 값을 문서·Profile·저장소에 남기지 않는다.
7
+ */
8
+ command?: string;
9
+ args?: readonly string[];
10
+ /** 상태 조회 통로. 테스트가 실제 프로세스를 띄우지 않기 위한 주입점. */
11
+ authStatus?: (context: DiscoveryContext) => Promise<AuthStatus>;
12
+ /** 선언 파일 읽기 통로. */
13
+ readDeclaration?: (projectRoot: string) => Promise<string | null>;
14
+ };
15
+ /** `jam auth status --json` 의 응답. **토큰 값은 여기 오지 않는다.** */
16
+ export type AuthStatus = {
17
+ status?: 'configured' | 'not_configured';
18
+ code?: string;
19
+ source?: string;
20
+ /** 어느 인스턴스인지. 사람이 "맞는 곳인가"를 확인하는 데 쓴다. */
21
+ baseUrl?: string;
22
+ /** 실행 자체가 안 됐을 때의 이유. */
23
+ error?: string;
24
+ };
25
+ /**
26
+ * 어떻게 실행할 것인가.
27
+ *
28
+ * 아직 패키지로 배포되지 않아 환경마다 형태가 다르다 — 실행 파일일 수도, 스크립트 경로일
29
+ * 수도 있다. 스크립트면 지금 도는 것과 같은 런타임으로 부른다.
30
+ *
31
+ * composition도 같은 규칙을 써야 probe와 실제 호출이 같은 곳을 가리킨다.
32
+ */
33
+ export declare function resolveJamCommand(env: NodeJS.ProcessEnv | undefined, fallbackCommand?: string, fallbackArgs?: readonly string[]): {
34
+ command: string;
35
+ args: string[];
36
+ };
37
+ /** `project.yaml` 에서 key만 꺼낸다. YAML 파서를 들이지 않는다 — 읽을 것이 한 줄이다. */
38
+ export declare function parseProjectKey(text: string): string | null;
39
+ export declare class JamAdapter implements Adapter {
40
+ #private;
41
+ constructor(deps?: JamAdapterDeps);
42
+ describe(): AdapterDescriptor;
43
+ /**
44
+ * 선언 파일이 있을 때만 후보를 만든다.
45
+ *
46
+ * 다른 저장소의 선언이나 코드 저장소 이름에서 키를 **추측하지 않는다.** 이름이 비슷하다는
47
+ * 것은 연결됐다는 뜻이 아니고, 틀린 연결은 없는 연결보다 나쁘다.
48
+ */
49
+ discover(context: DiscoveryContext): Promise<BindingCandidate[]>;
50
+ probe(candidate: BindingCandidate, context: DiscoveryContext): Promise<ProbeResult>;
51
+ /** 프로젝트와 무관하게 "도구가 지금 쓸 수 있는가". binding 유무와 별개 사실이다. */
52
+ runtime(context: DiscoveryContext): Promise<RuntimeStatus>;
53
+ }
@@ -0,0 +1,144 @@
1
+ // JAM Adapter 계약 — describe / discover / probe / runtime.
2
+ //
3
+ // 이 adapter가 지키는 선 하나: **자격을 대신 다루지 않는다.** JAM은 사람이 직접 로그인하게
4
+ // 설계돼 있고, 에이전트에게는 "됐는지 아닌지"만 알려 준다. ASC가 토큰을 받아 넘기거나
5
+ // 로그인을 대신 실행하면 그 설계를 우회하는 것이다 — 하지 않는다.
6
+ import { execFile } from 'node:child_process';
7
+ import { readFile } from 'node:fs/promises';
8
+ import { join } from 'node:path';
9
+ import { promisify } from 'node:util';
10
+ const run = promisify(execFile);
11
+ /**
12
+ * 실제로 되는 것만 적는다 (C-09 §1.1).
13
+ *
14
+ * 빠진 둘에 이유가 있다:
15
+ * context.history provider가 변경 이력을 주지 않는다 — 요청 경로 자체가 없다
16
+ *
17
+ * `observe.delta` 는 연다. **푸시가 있다는 뜻이 아니다** — C-07 §1.1이 Delta를
18
+ * "webhook / incremental polling / provider notification / updated-since" 로 정의하고
19
+ * 그 안에 시각 기준 증분 조회를 명시적으로 포함한다. JAM에는 webhook이 없고, 여기서
20
+ * 여는 것은 `updated >= watermark` 조회다 (adapters/jam/event-source.ts).
21
+ * push와 같은 말로 쓰면 "실시간이다"라는 잘못된 기대가 생기므로 그 구분을 여기 남긴다.
22
+ */
23
+ const PROVIDES = [
24
+ 'observe.delta',
25
+ 'inventory.enumerate',
26
+ 'context.resource',
27
+ 'context.thread',
28
+ ];
29
+ /** 프로젝트가 어느 작업 항목 묶음에 붙어 있는지 선언하는 파일. 없으면 붙어 있지 않은 것이다. */
30
+ const DECLARATION = join('.jira-agent', 'project.yaml');
31
+ /**
32
+ * 어떻게 실행할 것인가.
33
+ *
34
+ * 아직 패키지로 배포되지 않아 환경마다 형태가 다르다 — 실행 파일일 수도, 스크립트 경로일
35
+ * 수도 있다. 스크립트면 지금 도는 것과 같은 런타임으로 부른다.
36
+ *
37
+ * composition도 같은 규칙을 써야 probe와 실제 호출이 같은 곳을 가리킨다.
38
+ */
39
+ export function resolveJamCommand(env, fallbackCommand = 'jam', fallbackArgs = []) {
40
+ const path = env?.ASC_JAM_PATH;
41
+ if (!path)
42
+ return { command: fallbackCommand, args: [...fallbackArgs] };
43
+ return path.endsWith('.js') || path.endsWith('.mjs')
44
+ ? { command: process.execPath, args: [path] }
45
+ : { command: path, args: [] };
46
+ }
47
+ /** `project.yaml` 에서 key만 꺼낸다. YAML 파서를 들이지 않는다 — 읽을 것이 한 줄이다. */
48
+ export function parseProjectKey(text) {
49
+ const match = /^\s*key:\s*["']?([A-Za-z][A-Za-z0-9_]*)["']?\s*$/m.exec(text);
50
+ return match ? match[1] : null;
51
+ }
52
+ export class JamAdapter {
53
+ #command;
54
+ #args;
55
+ #authStatus;
56
+ #readDeclaration;
57
+ constructor(deps = {}) {
58
+ this.#command = deps.command ?? 'jam';
59
+ this.#args = deps.args ?? [];
60
+ this.#authStatus = deps.authStatus;
61
+ this.#readDeclaration = deps.readDeclaration ?? defaultRead;
62
+ }
63
+ describe() {
64
+ return {
65
+ id: 'jam',
66
+ version: '1',
67
+ provides: PROVIDES,
68
+ // 자격이 필요하다는 사실과 **사람이 해야 한다는 사실**까지. 값은 오지 않는다.
69
+ requiresCredential: ['사람이 직접 `jam auth login` — ASC가 대신 로그인하지 않는다'],
70
+ prerequisites: [
71
+ '프로젝트 루트에 .jira-agent/project.yaml 이 있어야 한다',
72
+ 'JAM 실행 경로를 ASC_JAM_PATH 로 알려 줘야 한다 (아직 패키지로 배포되지 않았다)',
73
+ ],
74
+ };
75
+ }
76
+ /**
77
+ * 선언 파일이 있을 때만 후보를 만든다.
78
+ *
79
+ * 다른 저장소의 선언이나 코드 저장소 이름에서 키를 **추측하지 않는다.** 이름이 비슷하다는
80
+ * 것은 연결됐다는 뜻이 아니고, 틀린 연결은 없는 연결보다 나쁘다.
81
+ */
82
+ async discover(context) {
83
+ const text = await this.#readDeclaration(context.projectRoot);
84
+ if (!text)
85
+ return [];
86
+ const key = parseProjectKey(text);
87
+ if (!key)
88
+ return [];
89
+ return [{ adapterId: 'jam', resource: key, provides: PROVIDES, discoveredBy: DECLARATION }];
90
+ }
91
+ async probe(candidate, context) {
92
+ const status = await this.#status(context);
93
+ if (status.state !== 'AVAILABLE')
94
+ return { state: status.state, ...(status.detail ? { detail: status.detail } : {}) };
95
+ return { state: 'AVAILABLE', provides: candidate.provides };
96
+ }
97
+ /** 프로젝트와 무관하게 "도구가 지금 쓸 수 있는가". binding 유무와 별개 사실이다. */
98
+ async runtime(context) {
99
+ return this.#status(context);
100
+ }
101
+ async #status(context) {
102
+ const read = this.#authStatus ?? ((ctx) => this.#defaultAuthStatus(ctx));
103
+ const status = await read(context).catch((error) => ({ error: String(error) }));
104
+ if (status.error) {
105
+ return { state: 'UNAVAILABLE', detail: `JAM을 실행하지 못했다 — ${status.error}` };
106
+ }
107
+ if (status.status === 'configured') {
108
+ return { state: 'AVAILABLE', ...(status.baseUrl ? { detail: `연결 대상 ${status.baseUrl}` } : {}) };
109
+ }
110
+ // 설정이 안 된 것은 고장이 아니다. 사람이 할 일이 남았다는 뜻이고, 그 일을 알려 준다.
111
+ return {
112
+ state: 'UNCONFIGURED',
113
+ detail: '자격이 없다 — 사람이 직접 `jam auth login` 을 실행해야 한다 (ASC가 대신하지 않는다)',
114
+ };
115
+ }
116
+ async #defaultAuthStatus(context) {
117
+ const { command, args } = resolveJamCommand(context.env, this.#command, this.#args);
118
+ try {
119
+ const { stdout } = await run(command, [...args, 'auth', 'status', '--json']);
120
+ return JSON.parse(stdout);
121
+ }
122
+ catch (error) {
123
+ // 실행 실패와 "자격 없음"을 합치지 않는다. 전자는 설치·경로 문제다.
124
+ const stdout = error.stdout;
125
+ if (stdout) {
126
+ try {
127
+ return JSON.parse(stdout);
128
+ }
129
+ catch {
130
+ // 아래로 떨어뜨린다
131
+ }
132
+ }
133
+ return { error: String(error.message ?? error).slice(0, 200) };
134
+ }
135
+ }
136
+ }
137
+ async function defaultRead(projectRoot) {
138
+ try {
139
+ return await readFile(join(projectRoot, DECLARATION), 'utf8');
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ }
@@ -0,0 +1,28 @@
1
+ import type { InventoryPort } from '../../ports/inventory.ts';
2
+ import type { Cursor, EventBatch, EventSource } from '../../ports/event-source.ts';
3
+ /**
4
+ * 분 단위 정밀도를 덮는 겹침. JamInventory의 JQL 변환이 쓰는 값과 같은 이유로 60초다 —
5
+ * 한쪽만 겹치면 겹치지 않은 쪽이 조용히 놓친다.
6
+ */
7
+ export declare const OVERLAP_MS = 60000;
8
+ export type JamEventSourceDeps = {
9
+ inventory: InventoryPort;
10
+ /** 이 통로의 이름. 관측 기록이 사는 scope가 이 값으로 정해진다. */
11
+ id?: string;
12
+ now?: () => string;
13
+ };
14
+ export declare class JamEventSource implements EventSource {
15
+ #private;
16
+ readonly id: string;
17
+ constructor(deps: JamEventSourceDeps);
18
+ /** "지금부터 보겠다". 처음 붙인 프로젝트의 과거를 통째로 긁으면 그 자체가 잡음이다. */
19
+ cursorFrom(since: string): Cursor;
20
+ /**
21
+ * cursor 이후로 바뀐 것을 사건으로 흘린다.
22
+ *
23
+ * **cursor는 성공한 회차에만 전진한다.** 목록을 끝까지 못 봤으면(`complete=false`)
24
+ * 그 회차의 최대 시각으로 올리지 않는다 — 못 본 구간을 본 것으로 표시하는 셈이 된다
25
+ * (C-07 §1.5).
26
+ */
27
+ drain(cursor: Cursor): Promise<EventBatch>;
28
+ }