@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
@@ -0,0 +1,61 @@
1
+ /** 운영 상태 — 별도 state machine 이 아니라 읽기 모델이다. */
2
+ export type WorkspaceHealth = 'ACTIVE' | 'DEGRADED' | 'DORMANT';
3
+ export type WorkspaceView = {
4
+ workspaceId: string;
5
+ /** 이 workspace 의 runtime 뿌리. */
6
+ root: string;
7
+ /** 지금 살아 있는 checkout 들. 비어 있으면 DORMANT 다. */
8
+ liveLocators: string[];
9
+ /** 등록돼 있지만 지금 없는 checkout 들. 지우지 않는다 — 돌아올 수 있다. */
10
+ missingLocators: string[];
11
+ health: WorkspaceHealth;
12
+ aliases: readonly string[];
13
+ };
14
+ export type WorkspaceInput = {
15
+ workspaceId: string;
16
+ root: string;
17
+ aliases: readonly string[];
18
+ locators: readonly string[];
19
+ };
20
+ /**
21
+ * 지금 이 기계의 상태를 계산한다. **판정도 삭제도 하지 않는다.**
22
+ *
23
+ * `rootExists` 가 거짓이면 runtime 자체가 사라진 것이다 — locator 가 살아 있어도
24
+ * 관측할 수 없으므로 DEGRADED 로 든다. 사라진 것을 조용히 지우지 않는 이유는 위와 같다.
25
+ */
26
+ export declare function viewWorkspaces(workspaces: readonly WorkspaceInput[], exists: (path: string) => boolean): WorkspaceView[];
27
+ /**
28
+ * 이번 회차에 실제로 돌 곳.
29
+ *
30
+ * DORMANT 는 **밖을 치지 않는다** (설계 §19). 살아 있는 checkout 이 없으면 그 저장소를
31
+ * 대신해 무엇을 물어볼 자리도 없고, 없는 자리를 대신해 외부에 질문하면 그것은 관측이
32
+ * 아니라 잡음이다.
33
+ */
34
+ export declare function dueWorkspaces(views: readonly WorkspaceView[]): {
35
+ workspaceId: string;
36
+ cwd: string;
37
+ }[];
38
+ /** 사람이 읽는 기계 전체 화면. `cd` 없이 지금 무엇이 도는지 보여야 한다 (설계 §13.2). */
39
+ export declare function renderWorkspaces(views: readonly WorkspaceView[]): string[];
40
+ /** 회차 하나에서 workspace 하나가 어떻게 끝났는가. */
41
+ export type PassResult = {
42
+ workspaceId: string;
43
+ code: number;
44
+ };
45
+ /**
46
+ * 기계 회차 전체의 결과 (P0-R2).
47
+ *
48
+ * 원칙 둘이 같이 선다: **한 workspace 의 실패가 다른 workspace 를 세우지 않는다** — 그래서
49
+ * 전부 돌린 뒤에 센다. 그리고 **기계 수준 결과는 실패를 삼키지 않는다** — 하나라도 0 이 아니면
50
+ * 회차도 0 이 아니다. 예전에는 무조건 0 을 돌려줘서 OS 기록(LastExitStatus)에는 두 workspace 가
51
+ * 실패한 회차가 정상으로 남았다. 실패를 지운 것이 아니라 볼 수 없게 만든 것이고, 그것이 더 나쁘다.
52
+ */
53
+ export declare function summarizePass(ran: readonly PassResult[], skipped: readonly {
54
+ workspaceId: string;
55
+ health: WorkspaceHealth;
56
+ }[]): {
57
+ code: 0 | 1;
58
+ outcome: 'ok' | 'partial' | 'failed';
59
+ failed: PassResult[];
60
+ line: string;
61
+ };
@@ -0,0 +1,82 @@
1
+ // 이 기계가 돌보는 workspace 들 (설계 §6·§7).
2
+ //
3
+ // **두 번째 등록부를 만들지 않는다.** `monitorEnabled=true` 같은 플래그를 새로 두면 그것이
4
+ // 곧 두 번째 정본이 되고, 붙어 있는데 감시되지 않는(또는 그 반대의) 상태가 생긴다.
5
+ // 운영 상태는 **이미 있는 사실에서 계산한다**:
6
+ //
7
+ // workspace 가 index 에 있다 → 이 기계가 아는 workspace 다
8
+ // 살아 있는 locator 가 하나라도 있다 → 관측할 수 있다
9
+ // 하나도 없다 → DORMANT — 상태는 남기고 밖을 치지 않는다
10
+ //
11
+ // DORMANT 는 삭제 대상이 아니다 (설계 §3.9). checkout 을 잠시 지웠다고 몇 달치 기록을
12
+ // 버리면, 그 삭제를 되돌릴 방법이 없다. 새 locator 가 나타나면 다시 ACTIVE 가 된다.
13
+ /**
14
+ * 지금 이 기계의 상태를 계산한다. **판정도 삭제도 하지 않는다.**
15
+ *
16
+ * `rootExists` 가 거짓이면 runtime 자체가 사라진 것이다 — locator 가 살아 있어도
17
+ * 관측할 수 없으므로 DEGRADED 로 든다. 사라진 것을 조용히 지우지 않는 이유는 위와 같다.
18
+ */
19
+ export function viewWorkspaces(workspaces, exists) {
20
+ return workspaces.map((workspace) => {
21
+ const liveLocators = workspace.locators.filter((locator) => exists(locator));
22
+ const missingLocators = workspace.locators.filter((locator) => !exists(locator));
23
+ const rootExists = exists(workspace.root);
24
+ const health = liveLocators.length === 0 ? 'DORMANT' : rootExists ? 'ACTIVE' : 'DEGRADED';
25
+ return {
26
+ workspaceId: workspace.workspaceId,
27
+ root: workspace.root,
28
+ liveLocators,
29
+ missingLocators,
30
+ health,
31
+ aliases: workspace.aliases,
32
+ };
33
+ });
34
+ }
35
+ /**
36
+ * 이번 회차에 실제로 돌 곳.
37
+ *
38
+ * DORMANT 는 **밖을 치지 않는다** (설계 §19). 살아 있는 checkout 이 없으면 그 저장소를
39
+ * 대신해 무엇을 물어볼 자리도 없고, 없는 자리를 대신해 외부에 질문하면 그것은 관측이
40
+ * 아니라 잡음이다.
41
+ */
42
+ export function dueWorkspaces(views) {
43
+ return views
44
+ .filter((view) => view.health === 'ACTIVE')
45
+ // 여러 checkout 이 있으면 아무 곳에서나 한 번이면 된다 — 관측 대상은 workspace 이지
46
+ // checkout 이 아니다. 첫 번째를 쓰는 것은 안정적인 선택이다(목록 순서가 index 순서다).
47
+ .map((view) => ({ workspaceId: view.workspaceId, cwd: view.liveLocators[0] }));
48
+ }
49
+ /** 사람이 읽는 기계 전체 화면. `cd` 없이 지금 무엇이 도는지 보여야 한다 (설계 §13.2). */
50
+ export function renderWorkspaces(views) {
51
+ if (views.length === 0)
52
+ return ['No workspaces are registered on this machine.'];
53
+ const lines = [];
54
+ for (const view of views) {
55
+ const alias = view.aliases[0] ?? '(no alias)';
56
+ lines.push(`${view.workspaceId} ${view.health} ${alias}`);
57
+ for (const locator of view.liveLocators)
58
+ lines.push(` ${locator}`);
59
+ // 없어진 checkout 도 든다 — 지우지 않았다는 사실이 보여야 사람이 놀라지 않는다
60
+ for (const locator of view.missingLocators)
61
+ lines.push(` ${locator} (gone)`);
62
+ }
63
+ return lines;
64
+ }
65
+ /**
66
+ * 기계 회차 전체의 결과 (P0-R2).
67
+ *
68
+ * 원칙 둘이 같이 선다: **한 workspace 의 실패가 다른 workspace 를 세우지 않는다** — 그래서
69
+ * 전부 돌린 뒤에 센다. 그리고 **기계 수준 결과는 실패를 삼키지 않는다** — 하나라도 0 이 아니면
70
+ * 회차도 0 이 아니다. 예전에는 무조건 0 을 돌려줘서 OS 기록(LastExitStatus)에는 두 workspace 가
71
+ * 실패한 회차가 정상으로 남았다. 실패를 지운 것이 아니라 볼 수 없게 만든 것이고, 그것이 더 나쁘다.
72
+ */
73
+ export function summarizePass(ran, skipped) {
74
+ const failed = ran.filter((result) => result.code !== 0);
75
+ const outcome = failed.length === 0 ? 'ok' : failed.length === ran.length ? 'failed' : 'partial';
76
+ const parts = [`${ran.length - failed.length} passed`];
77
+ if (failed.length > 0)
78
+ parts.push(`${failed.length} failed (${failed.map((r) => `${r.workspaceId}: ${r.code}`).join(', ')})`);
79
+ if (skipped.length > 0)
80
+ parts.push(`${skipped.length} skipped (${skipped.map((s) => s.health).join(', ')})`);
81
+ return { code: failed.length === 0 ? 0 : 1, outcome, failed, line: `Machine pass ${outcome} — ${parts.join(' · ')}` };
82
+ }
@@ -11,6 +11,21 @@ export type Resolution = {
11
11
  workspaceId: string;
12
12
  locator: string;
13
13
  }
14
+ /**
15
+ * 등록된 적은 없지만 **같은 Git repository의 다른 checkout이 등록돼 있다.**
16
+ * 같은 논리 workspace의 다른 execution instance다 (C-11 §1.3) — 쪼개지도 합치지도 않는다.
17
+ */
18
+ | {
19
+ kind: 'LINKED_WORKTREE';
20
+ root: string;
21
+ workspaceId: string;
22
+ /** 지금 이 checkout의 최상위. 호출자가 이 값을 index에 등록해 다음 번을 빠르게 만든다. */
23
+ locator: string;
24
+ /** 근거가 된, 이미 등록돼 있던 형제 checkout. */
25
+ via: string;
26
+ /** main checkout인가 linked worktree인가. */
27
+ kindOfLocator: 'checkout' | 'worktree';
28
+ }
14
29
  /** 저장소 안의 `.asc/` — 팀이 채택했거나 아직 이전하지 않은 개인 legacy다. */
15
30
  | {
16
31
  kind: 'PROJECT_LOCAL';
@@ -28,7 +43,28 @@ export type ResolveInput = {
28
43
  /** 여기를 넘어서는 위로 올라가지 않는다. 보통 사용자 홈. */
29
44
  stopAt?: string;
30
45
  exists?: (path: string) => Promise<boolean>;
46
+ /**
47
+ * 이 경로가 속한 Git repository의 checkout 전부(main + linked). 없으면 이 갈래를 건너뛴다.
48
+ *
49
+ * **주입이 기본값이다** — guard hook처럼 매 호출마다 도는 소비자는 이것을 넘기지 않고,
50
+ * 그러면 예전과 똑같이 index만 본다.
51
+ */
52
+ worktrees?: WorktreeProbe;
31
53
  };
54
+ /**
55
+ * 같은 Git repository에 속한 checkout 경로들. 첫 번째가 main worktree다.
56
+ * git이 없거나 저장소가 아니면 `null` — "없다"와 "못 봤다"를 구분한다.
57
+ */
58
+ export type WorktreeProbe = (cwd: string) => Promise<readonly string[] | null>;
32
59
  export declare function resolveWorkspace(input: ResolveInput): Promise<Resolution>;
60
+ /**
61
+ * 기본 통로 — `git worktree list --porcelain`. 첫 `worktree` 항목이 main checkout이다.
62
+ *
63
+ * git이 없거나 저장소가 아니면 `null`을 돌려준다. 빈 배열이 아니다 — "checkout이 하나도
64
+ * 없다"와 "물어보지 못했다"는 다른 사실이고, 후자를 전자로 적으면 조용한 오판이 된다.
65
+ */
66
+ export declare const gitWorktrees: WorktreeProbe;
67
+ /** porcelain 출력에서 checkout 경로만. 순서를 보존한다 — 첫 줄이 main worktree다. */
68
+ export declare function parseWorktreeList(stdout: string): string[];
33
69
  /** 사람이 읽는 한 줄. 왜 그 뿌리인지가 함께 와야 사람이 틀린 결합을 알아챈다. */
34
70
  export declare function resolutionLine(resolution: Resolution): string;
@@ -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
  *
@@ -69,7 +174,7 @@ async function findProjectLocal(start, stopAt, exists) {
69
174
  const candidate = join(dir, ASC_DIR);
70
175
  // 정지선이 홈이어도 **다른 홈**의 `.asc` 는 걸러지지 않는다. Windows는 temp
71
176
  // 디렉터리가 사용자 프로필 아래라, temp의 프로젝트에서 위로 걷다 실사용자
72
- // `~/.asc` 를 프로젝트 상태로 오인했다 (SSAFESTA 실측 — setup status가
177
+ // `~/.asc` 를 프로젝트 상태로 오인했다 ( 프로젝트 실측 — setup status가
73
178
  // UNATTACHED 대신 BROKEN을 답한 원인). user runtime은 내용으로 알아본다:
74
179
  // workspaces/·profiles/·runtime.json 은 홈에만 생긴다.
75
180
  if ((await exists(candidate)) && !(await looksLikeUserRuntime(candidate, exists)))
@@ -95,6 +200,8 @@ export function resolutionLine(resolution) {
95
200
  return `runtime: ${resolution.root} (given with --root)`;
96
201
  case 'REGISTERED':
97
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})`;
98
205
  case 'PROJECT_LOCAL':
99
206
  return `runtime: ${resolution.root} (.asc inside the repository — team-adopted, or personal state not yet migrated)`;
100
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;
@@ -0,0 +1,59 @@
1
+ import type { RemoteIdentity } from '../core/runtime/coordination.ts';
2
+ /**
3
+ * 밖으로 나가는 내용 **전부**.
4
+ *
5
+ * 상관 관계·근거 출처·라우팅·사적 메모는 여기 없다. 그것들은 호출자 쪽 내부 값이며
6
+ * 이 타입으로 들어올 자리가 없다 — 그것이 이 분리의 요점이다.
7
+ */
8
+ export type PublicPayload = {
9
+ title: string;
10
+ body: string;
11
+ labels?: readonly string[];
12
+ };
13
+ /**
14
+ * 이미 있는 것을 찾기 위한 질의.
15
+ *
16
+ * **제목 유사도로 찾지 않는다.** 제목은 사람이 바꾸고, 비슷한 제목은 다른 것일 수 있다.
17
+ * 여기서 주는 것은 안정적인 관계 근거다 — 무엇에 대한 조율인지, 어떤 작업 항목에
18
+ * 걸려 있는지, 우리가 이미 아는 게시물이 있는지.
19
+ */
20
+ export type SurfaceQuery = {
21
+ /** 이 조율이 어느 기대에 대한 것인가. adapter 는 이것을 자기 방식으로 심을 수 있다. */
22
+ correlation: string;
23
+ /** 관련된 작업 항목의 신원. adapter 어휘 그대로이며 Core 는 해석하지 않는다. */
24
+ workReference?: string;
25
+ /** 이미 아는 게시물. 있으면 그것부터 확인한다. */
26
+ known?: readonly Pick<RemoteIdentity, 'objectType' | 'objectId'>[];
27
+ };
28
+ /** 찾아진 후보 하나. */
29
+ export type SurfaceCandidate = {
30
+ identity: RemoteIdentity;
31
+ title: string;
32
+ /**
33
+ * 왜 후보인가. **판정 근거를 값으로 든다** — 산문으로 적으면 호출자가 그것으로
34
+ * 판단할 수 없다.
35
+ */
36
+ matchedBy: 'known-identity' | 'correlation' | 'work-reference' | 'weak';
37
+ /** 닫힌 것인가. 닫힌 곳에 이어 붙일지는 호출자가 정한다. */
38
+ closed?: boolean;
39
+ };
40
+ /** 되읽기 결과. 없으면 `null` — "만들었다고 했는데 없다"는 별개의 사실이다. */
41
+ export type SurfaceSnapshot = {
42
+ identity: RemoteIdentity;
43
+ title: string;
44
+ closed?: boolean;
45
+ };
46
+ /**
47
+ * 조율 표면 하나. adapter 가 구현한다.
48
+ *
49
+ * Core 는 이 인터페이스만 알고 provider 를 모른다 (C-09 §6).
50
+ */
51
+ export interface CoordinationSurfacePort {
52
+ readonly id: string;
53
+ /** 이미 있는가. 없으면 빈 배열 — 못 찾은 것과 없는 것을 호출자가 구분하려면 throw 한다. */
54
+ find(query: SurfaceQuery): Promise<SurfaceCandidate[]>;
55
+ /** 만든다. 돌려주는 것은 주소가 아니라 **안정 신원**이다. */
56
+ create(payload: PublicPayload, query: SurfaceQuery): Promise<RemoteIdentity>;
57
+ /** 지금도 있는가. 만든 직후 이것으로 확인한다 — 만들었다는 주장만으로 끝내지 않는다. */
58
+ read(identity: Pick<RemoteIdentity, 'objectType' | 'objectId'>): Promise<SurfaceSnapshot | null>;
59
+ }
@@ -0,0 +1,16 @@
1
+ // Coordination Surface — 밖에 무언가를 게시하고, 그것을 다시 찾고, 다시 읽는 통로.
2
+ //
3
+ // 기존 `ScmPort.execute` 로는 이 일을 할 수 없다. 그쪽은 성공을 문자열 하나로 돌려주고
4
+ // (`resultRef`), 만들기 전에 이미 있는지 찾는 경로도, 만든 뒤 정말 있는지 되읽는 경로도
5
+ // 없다. 실 프로젝트에서 그 구조가 같은 것을 두 번 만들게 했다:
6
+ //
7
+ // 생성 성공 → 돌아온 주소 모양이 예상과 다름 → 실패로 판정 → 다시 생성
8
+ //
9
+ // 대조할 값이 계약에 없으면 그 판정을 막을 방법이 없다. 그래서 이 Port 는 세 가지를
10
+ // 요구한다 — **찾기 · 만들기 · 되읽기**. 그리고 성공의 정본은 주소가 아니라 그 시스템이
11
+ // 준 안정적인 id 다.
12
+ //
13
+ // **공개될 것만 지나간다.** `PublicPayload` 밖의 값은 이 경계에 올 수 없다 — 내부 메모와
14
+ // 공개 본문을 한 문자열에 담았다가 나중에 가르는 구조가 유출을 만들었고, 관례로 가르면
15
+ // 언젠가 갈리지 않는다 (F4).
16
+ export {};
@@ -6,6 +6,11 @@ export type ContextComment = {
6
6
  body: string;
7
7
  /** 아직 닫히지 않은 논의인가. provider가 알려주지 않으면 생략한다 — 추측하지 않는다. */
8
8
  unresolved?: boolean;
9
+ /**
10
+ * 사람이 쓴 것이 아니라 그 시스템이 남긴 자국인가 (커밋 언급·배정 변경 등).
11
+ * 사람의 답과 섞이면 "답이 왔다"가 자동으로 성립해 버린다.
12
+ */
13
+ system?: boolean;
9
14
  };
10
15
  export type ResourceSnapshot = {
11
16
  reference: string;
@@ -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.2",
3
+ "version": "0.5.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",