@asc-agent/runtime 0.4.0 → 0.5.1

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 (48) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/claude-code/guard.js +26 -1
  3. package/dist/adapters/claude-code/install.d.ts +27 -0
  4. package/dist/adapters/claude-code/install.js +3 -1
  5. package/dist/adapters/fixture-surface/index.d.ts +33 -0
  6. package/dist/adapters/fixture-surface/index.js +97 -0
  7. package/dist/adapters/gitlab/adapter.js +1 -0
  8. package/dist/adapters/gitlab/client.d.ts +16 -2
  9. package/dist/adapters/gitlab/client.js +27 -0
  10. package/dist/adapters/gitlab/coordination.d.ts +19 -0
  11. package/dist/adapters/gitlab/coordination.js +111 -0
  12. package/dist/adapters/gitlab/ports.d.ts +6 -0
  13. package/dist/adapters/gitlab/ports.js +29 -2
  14. package/dist/adapters/local/canonical.d.ts +20 -0
  15. package/dist/adapters/local/canonical.js +58 -0
  16. package/dist/adapters/markdown/state-store.js +2 -1
  17. package/dist/adapters/service/launchd.js +18 -1
  18. package/dist/adapters/service/systemd-user.js +4 -1
  19. package/dist/adapters/webhook/ingress.d.ts +2 -2
  20. package/dist/cli/asc.js +569 -17
  21. package/dist/composition/runtime.d.ts +20 -0
  22. package/dist/composition/runtime.js +32 -1
  23. package/dist/core/attach/setup-plan.d.ts +107 -6
  24. package/dist/core/attach/setup-plan.js +104 -6
  25. package/dist/core/attach/setup.d.ts +3 -1
  26. package/dist/core/attach/setup.js +25 -1
  27. package/dist/core/binding/types.d.ts +1 -1
  28. package/dist/core/binding/types.js +1 -0
  29. package/dist/core/distribution/external-command.d.ts +26 -0
  30. package/dist/core/distribution/external-command.js +58 -1
  31. package/dist/core/distribution/persistent-runtime.d.ts +9 -0
  32. package/dist/core/distribution/release.d.ts +3 -3
  33. package/dist/core/distribution/release.js +1 -1
  34. package/dist/core/distribution/service-runtime.d.ts +40 -0
  35. package/dist/core/distribution/service-runtime.js +76 -0
  36. package/dist/core/runtime/coordination.d.ts +312 -0
  37. package/dist/core/runtime/coordination.js +247 -0
  38. package/dist/core/runtime/front.d.ts +10 -0
  39. package/dist/core/runtime/front.js +16 -1
  40. package/dist/core/runtime/publish.d.ts +107 -0
  41. package/dist/core/runtime/publish.js +153 -0
  42. package/dist/core/runtime/workspaces.d.ts +22 -0
  43. package/dist/core/runtime/workspaces.js +18 -0
  44. package/dist/ports/coordination-surface.d.ts +59 -0
  45. package/dist/ports/coordination-surface.js +16 -0
  46. package/dist/ports/resource-context.d.ts +5 -0
  47. package/dist/schemas/profile.d.ts +2 -2
  48. package/package.json +1 -1
@@ -4,12 +4,15 @@ import type { EventSource } from '../ports/event-source.ts';
4
4
  import type { InventoryPort } from '../ports/inventory.ts';
5
5
  import type { ResourceContextPort } from '../ports/resource-context.ts';
6
6
  import type { ScmPort } from '../ports/scm.ts';
7
+ import type { CoordinationSurfacePort } from '../ports/coordination-surface.ts';
7
8
  export type RuntimePorts = {
8
9
  eventSource?: EventSource;
9
10
  scm?: ScmPort;
10
11
  inventory?: InventoryPort;
11
12
  resourceContext?: ResourceContextPort;
12
13
  changeContext?: ChangeContextPort;
14
+ /** 밖에 물은 것이 실제로 있게 하는 통로 (C-04·C-10). 없으면 게시는 일어나지 않는다. */
15
+ coordinationSurface?: CoordinationSurfacePort;
13
16
  /** 무엇을 왜 못 만들었는지. 조용히 빠지면 사람이 이유를 알 수 없다. */
14
17
  unavailable: string[];
15
18
  };
@@ -50,6 +53,23 @@ export declare function closeToolClients(): Promise<void>;
50
53
  * 후보가 갈리면 만들지 않는다 — `AMBIGUOUS_BINDING`은 사람이 정할 문제이고, 여기서 하나를
51
54
  * 고르면 그 선택을 아무도 보지 못한다 (C-09 §4.2).
52
55
  */
56
+ /**
57
+ * 작업 항목을 읽는 통로의 역할 이름 (P0 F7).
58
+ *
59
+ * `rolesFor` 는 **선언된 binding 이 하나일 때만** 역할을 정한다. 그래서 code binding 과
60
+ * work binding 이 둘 다 `context.resource` 를 제공하면 아무 역할도 서지 않고, 작업 항목
61
+ * 조회는 "통로가 없다" 로 끝난다 — 실기계에서 감시는 되는데 `proceed --work` 만 실패한
62
+ * 자리가 여기다. 없는 것이 아니라 **누구에게 물을지 정하지 않은 것**이었다.
63
+ *
64
+ * 가르는 기준은 provider 이름이 아니라 capability 모양이다: 작업 항목 통로는 목록과 자원을
65
+ * 알고 **변경을 모른다**. 코드 통로는 변경을 안다. 그 차이가 곧 두 축의 정의다
66
+ * (C-09 §2.1 — Port 를 좁게 나눈 이유가 그것이다).
67
+ */
68
+ export declare function workItemRoles(plan: BindingPlan, declared: readonly {
69
+ role: string;
70
+ adapter: string;
71
+ resource: string;
72
+ }[]): Partial<Record<Capability, string>>;
53
73
  /**
54
74
  * Profile이 선언한 역할 배정을 capability별 역할로 옮긴다 (C-09 §3.1·§4).
55
75
  *
@@ -13,6 +13,7 @@ import { GitHubChangeContext, GitHubInventory, GitHubResourceContext } from "../
13
13
  import { GitHubEventSource } from "../adapters/github/event-source.js";
14
14
  import { GitHubScm } from "../adapters/github/scm.js";
15
15
  import { GitLabClient, GlabApiClient, discoverToken as discoverGitLabToken, glabAvailable, } from "../adapters/gitlab/client.js";
16
+ import { GitLabCoordinationSurface } from "../adapters/gitlab/coordination.js";
16
17
  import { GitLabChangeContext, GitLabEventSource, GitLabInventory, GitLabResourceContext, } from "../adapters/gitlab/ports.js";
17
18
  import { JamMcpClient } from "../adapters/jam/mcp-client.js";
18
19
  import { JamEventSource } from "../adapters/jam/event-source.js";
@@ -30,7 +31,10 @@ const FACTORIES = {
30
31
  inventory: new GitLabInventory({ client, project }),
31
32
  resourceContext: new GitLabResourceContext({ client, project }),
32
33
  changeContext: new GitLabChangeContext({ client, project }),
33
- // canonical·외부 write 통로는 아직 없다. 없는 것을 있는 척하지 않는다.
34
+ // 조율 표면. 토큰 통로일 때만 쓰기가 가능하다 도구를 통로로 쓰는 경우도 POST 를
35
+ // 대신 보내 준다. 둘 다 아니면 create 가 그 사실을 그대로 말한다.
36
+ coordinationSurface: new GitLabCoordinationSurface({ reader: client, writer: client, project }),
37
+ // canonical 통로는 아직 없다. 없는 것을 있는 척하지 않는다.
34
38
  };
35
39
  },
36
40
  github(binding, input, token) {
@@ -112,6 +116,7 @@ const PORT_OF = {
112
116
  'context.change': 'changeContext',
113
117
  'context.resource': 'resourceContext',
114
118
  'canonical.read': 'scm',
119
+ 'coordination.surface': 'coordinationSurface',
115
120
  };
116
121
  /**
117
122
  * capability가 필요한 자리마다 어느 binding이 맡을지 정해 Port를 만든다.
@@ -119,6 +124,32 @@ const PORT_OF = {
119
124
  * 후보가 갈리면 만들지 않는다 — `AMBIGUOUS_BINDING`은 사람이 정할 문제이고, 여기서 하나를
120
125
  * 고르면 그 선택을 아무도 보지 못한다 (C-09 §4.2).
121
126
  */
127
+ /**
128
+ * 작업 항목을 읽는 통로의 역할 이름 (P0 F7).
129
+ *
130
+ * `rolesFor` 는 **선언된 binding 이 하나일 때만** 역할을 정한다. 그래서 code binding 과
131
+ * work binding 이 둘 다 `context.resource` 를 제공하면 아무 역할도 서지 않고, 작업 항목
132
+ * 조회는 "통로가 없다" 로 끝난다 — 실기계에서 감시는 되는데 `proceed --work` 만 실패한
133
+ * 자리가 여기다. 없는 것이 아니라 **누구에게 물을지 정하지 않은 것**이었다.
134
+ *
135
+ * 가르는 기준은 provider 이름이 아니라 capability 모양이다: 작업 항목 통로는 목록과 자원을
136
+ * 알고 **변경을 모른다**. 코드 통로는 변경을 안다. 그 차이가 곧 두 축의 정의다
137
+ * (C-09 §2.1 — Port 를 좁게 나눈 이유가 그것이다).
138
+ */
139
+ export function workItemRoles(plan, declared) {
140
+ const candidates = plan.bindings
141
+ .filter((binding) => binding.role !== undefined)
142
+ .filter((binding) => declared.some((d) => d.adapter === binding.adapterId && d.resource === binding.resource))
143
+ .filter((binding) => binding.provides.includes('context.resource') &&
144
+ binding.provides.includes('inventory.enumerate') &&
145
+ !binding.provides.includes('context.change'));
146
+ const roles = new Set(candidates.map((binding) => binding.role));
147
+ // 갈리면 고르지 않는다 — 그것은 사람이 정할 문제다 (C-09 §4.2).
148
+ if (roles.size !== 1)
149
+ return {};
150
+ const role = [...roles][0];
151
+ return { 'context.resource': role, 'inventory.enumerate': role, 'context.thread': role, 'context.history': role };
152
+ }
122
153
  /**
123
154
  * Profile이 선언한 역할 배정을 capability별 역할로 옮긴다 (C-09 §3.1·§4).
124
155
  *
@@ -61,6 +61,46 @@ export type SetupState = {
61
61
  * 사람에게 "그 도구를 설정할까요?"라고 되물으면, 사람은 자기가 이미 적어 둔 것을 다시
62
62
  * 답하게 된다. 고칠 수 있는 것은 고치고, 사람만 할 수 있는 것에서만 멈춘다.
63
63
  */
64
+ /**
65
+ * 이 저장소 자신의 remote 에서 읽히는 Profile id (P0 fresh onboarding).
66
+ *
67
+ * **다른 논리 workspace 를 추측해 합치는 것이 아니다** (C-11 금지사항). 이 저장소가
68
+ * 스스로 증명하는 신원 하나이고, 그 이름의 Profile 이 이미 있으면 그것을 쓰고 없으면
69
+ * 만든다. 이것이 없어서 fresh 설치가 `ASC_PROFILE_SELECTION_REQUIRED` 앞에 멈췄고,
70
+ * 사람이 `profile adopt` 를 따로 쳐야 했다.
71
+ */
72
+ adoptable?: {
73
+ id: string;
74
+ exists: boolean;
75
+ };
76
+ /**
77
+ * 이 workspace 에 승인 권한자가 서 있는가, 그리고 지금 이 사람을 무엇으로 알아볼 수
78
+ * 있는가. `actor` 는 인증된 provider 가 말한 값이며 ASC 가 지어내지 않는다.
79
+ */
80
+ identity?: {
81
+ wired: boolean;
82
+ actor?: string;
83
+ };
84
+ /**
85
+ * 발견이 **갈리지 않고** 제안하는 결합. Profile 에 결합 선언이 없을 때만 본다.
86
+ * 갈리면 비어 있다 — 고르는 것은 사람이다 (C-09 §4.2).
87
+ */
88
+ bindingProposal?: readonly {
89
+ role: string;
90
+ adapter: string;
91
+ resource: string;
92
+ }[];
93
+ /**
94
+ * 이 checkout 이 증명하는 정본 갈래. remote 가 스스로 말한 기본 branch 하나이며,
95
+ * Profile 에 정본 선언이 비어 있을 때만 관측한다. 알 수 없으면 없다 — 지어내면 세션이
96
+ * 읽지 못하는 정본을 딛고 서게 된다.
97
+ */
98
+ canonicalProposal?: {
99
+ id: string;
100
+ provider: string;
101
+ remote: string;
102
+ ref: string;
103
+ };
64
104
  workBinding?: {
65
105
  adapter: string;
66
106
  resource: string;
@@ -86,12 +126,63 @@ export type SetupChange =
86
126
  strategy: 'npm-global';
87
127
  from: string;
88
128
  }
129
+ /**
130
+ * 이 저장소를 설명하는 Profile 을 만든다 (P0 fresh onboarding).
131
+ *
132
+ * 근거는 이 저장소의 remote 하나뿐이다. 다른 workspace 를 추측하지 않는다.
133
+ */
134
+ | {
135
+ target: 'adopt-profile';
136
+ profile: string;
137
+ }
89
138
  /** 이 checkout에 runtime을 붙인다. local scope면 저장소에는 아무것도 만들지 않는다. */
90
139
  | {
91
140
  target: 'attach-workspace';
92
141
  scope: 'local' | 'project';
93
142
  profile: string;
94
143
  }
144
+ /**
145
+ * 지금 이 사람을 이 workspace 의 승인 권한자로 세운다.
146
+ *
147
+ * **비밀은 다루지 않는다** — 이름과 채널뿐이다. 이것이 계획에 없으면 fresh 설치가
148
+ * 승인자 0 인 채로 READY 가 되고, 사람이 `setup identity` 를 따로 쳐야 했다.
149
+ */
150
+ | {
151
+ target: 'identity';
152
+ actor: string;
153
+ }
154
+ /**
155
+ * 발견이 증명한 결합을 Profile 에 적는다.
156
+ *
157
+ * 갈리는 것은 담지 않는다. 여기 없으면 사람이 Profile 을 손으로 고쳐야 했고, 그것이
158
+ * fresh onboarding 에서 실제로 필요했던 단계다.
159
+ */
160
+ | {
161
+ target: 'profile-bindings';
162
+ profile: string;
163
+ bindings: readonly {
164
+ role: string;
165
+ adapter: string;
166
+ resource: string;
167
+ }[];
168
+ }
169
+ /**
170
+ * Profile 에 정본 갈래를 적는다.
171
+ *
172
+ * 이것이 비어 있으면 판정이 전부 "확인 못 함" 으로 나온다 — relevance 도
173
+ * responsibility 도 정본을 딛고 서기 때문이다. 근거는 remote 가 스스로 말한 기본
174
+ * branch 이고, 그 값을 얻지 못하면 이 변경은 계획에 들지 않는다.
175
+ */
176
+ | {
177
+ target: 'profile-canonical';
178
+ profile: string;
179
+ source: {
180
+ id: string;
181
+ provider: string;
182
+ remote: string;
183
+ ref: string;
184
+ };
185
+ }
95
186
  /** Host 설치물을 지금 source에 맞춘다. 왜 필요한지까지 든다. */
96
187
  | {
97
188
  target: 'host-install';
@@ -190,17 +281,27 @@ export type SetupEffects = {
190
281
  registerPersistentRuntime?(change: Extract<SetupChange, {
191
282
  target: 'persistent-runtime';
192
283
  }>): Promise<void>;
284
+ /** 이 저장소를 설명하는 Profile 을 만든다. */
285
+ adoptProfile?(change: Extract<SetupChange, {
286
+ target: 'adopt-profile';
287
+ }>): Promise<void>;
288
+ /** 승인 권한자를 세운다. 비밀은 다루지 않는다. */
289
+ wireIdentity?(change: Extract<SetupChange, {
290
+ target: 'identity';
291
+ }>): Promise<void>;
292
+ /** 발견이 증명한 결합을 Profile 에 적는다. */
293
+ declareBindings?(change: Extract<SetupChange, {
294
+ target: 'profile-bindings';
295
+ }>): Promise<void>;
296
+ /** remote 가 말한 정본 갈래를 Profile 에 적는다. */
297
+ declareCanonical?(change: Extract<SetupChange, {
298
+ target: 'profile-canonical';
299
+ }>): Promise<void>;
193
300
  };
194
301
  export type ApplyResult = {
195
302
  applied: SetupChange[];
196
303
  changesApplied: boolean;
197
304
  };
198
- /**
199
- * plan에 적힌 것만 실행한다. **다시 판단하지 않는다** (C-14 불변식 ⑩).
200
- *
201
- * 여기서 상태를 다시 보고 마음을 바꾸면, 사람이 승인한 plan과 실제로 일어난 일이
202
- * 달라진다. 그 순간 plan은 아무것도 보장하지 않는 문서가 된다.
203
- */
204
305
  export declare function applySetupPlan(plan: SetupPlan, effects: SetupEffects): Promise<ApplyResult>;
205
306
  /** 사람이 읽는 줄. 같은 plan에서 나온다 — agent가 보는 JSON과 다른 판단이 아니다. */
206
307
  export declare function renderSetupPlan(plan: SetupPlan): string[];
@@ -66,7 +66,7 @@ export function computeSetupPlan(state) {
66
66
  code: 'ASC_HOST_INSTALL_MODIFIED',
67
67
  // 사람이 고친 것을 덮는 것은 사람이 정한다 — plan에 담아 몰래 적용하지 않는다.
68
68
  // 다만 runtime 설치처럼 이 결정과 무관한 준비는 계획에 남는다.
69
- changes,
69
+ changes: orderChanges(changes, false),
70
70
  requiresUserAction: true,
71
71
  ...actions(mode, evidence, [
72
72
  { type: 'force_host_install', ...command(['host', host.id, 'install', '--force']) },
@@ -96,7 +96,7 @@ export function computeSetupPlan(state) {
96
96
  return {
97
97
  status: 'user_action_required',
98
98
  code: 'ASC_WORK_BINDING_NEEDS_USER',
99
- changes,
99
+ changes: orderChanges(changes, false),
100
100
  requiresUserAction: true,
101
101
  ...actions(mode, evidence, [{ type: 'proceed', ...command(['setup', 'status']) }]),
102
102
  };
@@ -112,6 +112,7 @@ export function computeSetupPlan(state) {
112
112
  }
113
113
  }
114
114
  if (state.ascRoot && !state.attachmentBroken) {
115
+ pushWorkspaceChanges(changes, state, evidence);
115
116
  // 붙어 있어도 **무엇을 고를 수 있었는지**는 사실이다. 사용자 소유 Profile을 새로 놓고
116
117
  // 계획을 물었을 때 그것이 어디에도 안 보이면, 놓은 사람은 경로를 의심하게 된다.
117
118
  if (state.profileCandidates.length > 0) {
@@ -121,13 +122,23 @@ export function computeSetupPlan(state) {
121
122
  }
122
123
  // 아직 안 붙었거나, 붙이다 말았다(BROKEN). 무엇으로 붙을지는 사람이 정한다 —
123
124
  // BROKEN이면 같은 선택으로 다시 붙이는 것이 repair다.
124
- const profile = state.requestedProfile ?? soleCandidate(state.profileCandidates);
125
+ // 저장소가 스스로 증명하는 Profile 이 있으면 그것이 답이다 — 이름이 이미 있으면
126
+ // 그것을 쓰고, 없으면 만든다. 둘 다 "다른 workspace 를 추측"하는 것이 아니다.
127
+ const adoptable = state.adoptable;
128
+ const profile = state.requestedProfile ??
129
+ (adoptable && state.profileCandidates.includes(adoptable.id) ? adoptable.id : undefined) ??
130
+ (adoptable ? adoptable.id : undefined) ??
131
+ soleCandidate(state.profileCandidates);
132
+ if (adoptable && !state.requestedProfile && !adoptable.exists) {
133
+ evidence.push(`profile=${adoptable.id} (adopted from this repository)`);
134
+ changes.push({ target: 'adopt-profile', profile: adoptable.id });
135
+ }
125
136
  if (!profile) {
126
137
  evidence.push(`profile candidates=${state.profileCandidates.join(', ') || '(none)'}`);
127
138
  return {
128
139
  status: 'user_action_required',
129
140
  code: 'ASC_PROFILE_SELECTION_REQUIRED',
130
- changes,
141
+ changes: orderChanges(changes, false),
131
142
  requiresUserAction: true,
132
143
  profiles: state.profileCandidates,
133
144
  // 고를 것이 **없을** 수도 있다 — 배포본이 들고 있는 것은 예시뿐이고, 이 프로젝트를
@@ -153,9 +164,36 @@ export function computeSetupPlan(state) {
153
164
  evidence.push('adoption=project (explicit)');
154
165
  }
155
166
  changes.push({ target: 'attach-workspace', scope: state.scope, profile });
156
- evidence.push(`profile=${profile}${state.requestedProfile ? ' (given)' : ' (sole candidate)'}`);
167
+ if (!changes.some((c) => c.target === 'adopt-profile')) {
168
+ evidence.push(`profile=${profile}${state.requestedProfile ? ' (given)' : ' (sole candidate)'}`);
169
+ }
170
+ pushWorkspaceChanges(changes, state, evidence, profile);
157
171
  return finish(changes, evidence, state, mode, command);
158
172
  }
173
+ /**
174
+ * workspace 가 서고 나서 필요한 것들 — 승인자와 결합 선언.
175
+ *
176
+ * 둘 다 **관측된 사실이 있을 때만** 계획에 든다. 승인자는 인증된 provider 가 말한 이름이고,
177
+ * 결합은 발견이 갈리지 않고 제안한 것이다. 없으면 담지 않는다 — 지어내면 그 값이 곧
178
+ * 사람이 겪는 오작동이 된다.
179
+ */
180
+ function pushWorkspaceChanges(changes, state, evidence, attachingProfile) {
181
+ if (state.identity && !state.identity.wired && state.identity.actor) {
182
+ evidence.push(`identity=${state.identity.actor} (not yet recorded)`);
183
+ changes.push({ target: 'identity', actor: state.identity.actor });
184
+ }
185
+ const profile = attachingProfile ?? state.attachedProfile;
186
+ if (!profile)
187
+ return;
188
+ if (state.bindingProposal && state.bindingProposal.length > 0) {
189
+ evidence.push(`bindings proposed=${state.bindingProposal.map((b) => `${b.role}:${b.adapter}`).join(', ')}`);
190
+ changes.push({ target: 'profile-bindings', profile, bindings: state.bindingProposal });
191
+ }
192
+ if (state.canonicalProposal) {
193
+ evidence.push(`canonical proposed=${state.canonicalProposal.remote}/${state.canonicalProposal.ref}`);
194
+ changes.push({ target: 'profile-canonical', profile, source: state.canonicalProposal });
195
+ }
196
+ }
159
197
  /** 후보가 하나뿐이어도 대신 고르지 않는다 — 여기서 돌려주는 것은 "고를 것이 없다"뿐이다. */
160
198
  function soleCandidate(candidates) {
161
199
  return candidates.length === 1 ? candidates[0] : undefined;
@@ -172,7 +210,9 @@ function actions(mode, evidence, list) {
172
210
  evidence,
173
211
  };
174
212
  }
175
- function finish(changes, evidence, state, mode, command) {
213
+ function finish(raw, evidence, state, mode, command) {
214
+ const settled = (Boolean(state.ascRoot) && !state.attachmentBroken) || raw.some((c) => c.target === 'attach-workspace');
215
+ const changes = orderChanges(raw, settled);
176
216
  if (changes.length === 0) {
177
217
  return {
178
218
  status: 'already_configured',
@@ -194,6 +234,34 @@ function finish(changes, evidence, state, mode, command) {
194
234
  * 여기서 상태를 다시 보고 마음을 바꾸면, 사람이 승인한 plan과 실제로 일어난 일이
195
235
  * 달라진다. 그 순간 plan은 아무것도 보장하지 않는 문서가 된다.
196
236
  */
237
+ /**
238
+ * 실행 순서 (설계 §9 — fresh onboarding).
239
+ *
240
+ * **등록이 맨 마지막이다.** 예전에는 등록이 붙이기보다 먼저 계획에 들어가, OS 가 아직
241
+ * identity·binding 이 서지 않은 workspace 를 회차로 돌렸다. 그 회차는 실패했고, 사람은
242
+ * 자기가 setup 을 끝내기도 전에 실패 기록을 봤다.
243
+ */
244
+ const CHANGE_ORDER = {
245
+ 'runtime-install': 0,
246
+ 'adopt-profile': 1,
247
+ 'attach-workspace': 2,
248
+ identity: 3,
249
+ 'profile-bindings': 4,
250
+ 'profile-canonical': 5,
251
+ 'work-binding-setup': 6,
252
+ 'host-install': 7,
253
+ 'persistent-runtime': 8,
254
+ };
255
+ /**
256
+ * 순서를 고정하고, **아직 설 자리가 아닌 등록은 뺀다.**
257
+ *
258
+ * 붙는 것이 이번 계획으로 끝나지 않으면 이 기계에 등록할 이유가 없다 — 등록물은
259
+ * workspace 를 돌보는 것이고, 돌볼 workspace 가 아직 없기 때문이다.
260
+ */
261
+ function orderChanges(changes, attachmentSettled) {
262
+ const kept = attachmentSettled ? [...changes] : changes.filter((change) => change.target !== 'persistent-runtime');
263
+ return kept.sort((a, b) => CHANGE_ORDER[a.target] - CHANGE_ORDER[b.target]);
264
+ }
197
265
  export async function applySetupPlan(plan, effects) {
198
266
  const applied = [];
199
267
  for (const change of plan.changes) {
@@ -220,6 +288,26 @@ export async function applySetupPlan(plan, effects) {
220
288
  continue;
221
289
  await effects.setupWorkBinding(change);
222
290
  break;
291
+ case 'adopt-profile':
292
+ if (!effects.adoptProfile)
293
+ continue;
294
+ await effects.adoptProfile(change);
295
+ break;
296
+ case 'identity':
297
+ if (!effects.wireIdentity)
298
+ continue;
299
+ await effects.wireIdentity(change);
300
+ break;
301
+ case 'profile-bindings':
302
+ if (!effects.declareBindings)
303
+ continue;
304
+ await effects.declareBindings(change);
305
+ break;
306
+ case 'profile-canonical':
307
+ if (!effects.declareCanonical)
308
+ continue;
309
+ await effects.declareCanonical(change);
310
+ break;
223
311
  }
224
312
  applied.push(change);
225
313
  }
@@ -254,5 +342,15 @@ function changeLine(change) {
254
342
  return ` repair ${change.adapter} for ${change.resource} through its own setup (${change.version})`;
255
343
  case 'persistent-runtime':
256
344
  return ` register this machine's ASC runtime with ${change.adapter}`;
345
+ case 'adopt-profile':
346
+ return ` create profile ${change.profile} from this repository's remote`;
347
+ case 'identity':
348
+ return ` record ${change.actor} as this workspace's approver`;
349
+ case 'profile-bindings':
350
+ return ` declare bindings in ${change.profile}: ${change.bindings
351
+ .map((b) => `${b.role}=${b.adapter}:${b.resource}`)
352
+ .join(', ')}`;
353
+ case 'profile-canonical':
354
+ return ` declare canonical source in ${change.profile}: ${change.source.remote}/${change.source.ref}`;
257
355
  }
258
356
  }
@@ -8,7 +8,7 @@ export type AttachmentState = 'READY' | 'UNATTACHED' | 'BROKEN' | 'LOCK_DRIFT';
8
8
  */
9
9
  export type GateState = 'OPEN' | 'BLOCKED' | 'DEGRADED';
10
10
  export type SetupGate = {
11
- id: 'approval' | 'monitor' | 'external-write';
11
+ id: 'approval' | 'monitor' | 'external-write' | 'canonical';
12
12
  label: string;
13
13
  state: GateState;
14
14
  /** 무엇이 없어서 막혔는지. */
@@ -30,6 +30,8 @@ export type SetupStatus = {
30
30
  gates: SetupGate[];
31
31
  };
32
32
  export type SetupInput = {
33
+ /** Profile 이 선언한 정본 갈래 수. 붙지 않았으면 알 수 없으므로 `undefined` 다. */
34
+ canonicalSources?: number;
33
35
  attachment: AttachmentState;
34
36
  /** 붙어 있는 Profile의 id와 출처. **Surface가 읽어 넘긴다** — Core는 경로를 모른다. */
35
37
  profile?: {
@@ -28,7 +28,7 @@ export function assessSetup(input) {
28
28
  attachment: input.attachment,
29
29
  ...(input.profile ? { profile: input.profile } : {}),
30
30
  ready: [...ALWAYS_READY],
31
- gates: [approvalGate(input), monitorGate(input), externalWriteGate(input)],
31
+ gates: [approvalGate(input), canonicalGate(input), monitorGate(input), externalWriteGate(input)],
32
32
  };
33
33
  }
34
34
  /** 승인 결정. identities.json은 lock digest에 없어 재고정이 필요 없다. */
@@ -46,6 +46,30 @@ function approvalGate(input) {
46
46
  howTo: ['open identities.json and add an approver in the $example form (no re-lock needed)'],
47
47
  };
48
48
  }
49
+ /**
50
+ * 정본 조회 (C-08 · C-11).
51
+ *
52
+ * 코드 결합은 선언돼 있는데 정본 갈래가 비어 있으면, 판정은 "확인 못 함" 으로만 나온다 —
53
+ * relevance 도 responsibility 도 정본을 딛고 서기 때문이다. 실기계에서 그 상태가 READY 로
54
+ * 보였고, 사람은 무엇이 빠졌는지 알 방법이 없었다. 없는 것을 없다고 말한다.
55
+ */
56
+ function canonicalGate(input) {
57
+ // 붙지 않았으면 이 축을 판정하지 않는다 — 아직 Profile 이 없다.
58
+ if (input.attachment !== 'READY' || input.canonicalSources === undefined) {
59
+ return { id: 'canonical', label: 'canonical evidence', state: 'OPEN', missing: [], warnings: [], howTo: [] };
60
+ }
61
+ if (input.canonicalSources > 0) {
62
+ return { id: 'canonical', label: 'canonical evidence', state: 'OPEN', missing: [], warnings: [], howTo: [] };
63
+ }
64
+ return {
65
+ id: 'canonical',
66
+ label: 'canonical evidence',
67
+ state: 'BLOCKED',
68
+ missing: ['the profile declares no canonical source, so nothing can be judged against a baseline'],
69
+ warnings: [],
70
+ howTo: [`declare one in the profile's canonical.sources — ${RESOLVE_AGAIN}`],
71
+ };
72
+ }
49
73
  /**
50
74
  * 외부 감시. 두 설정이 서로 다른 것을 막는다 —
51
75
  * controller.identities가 없으면 실행 자체가 안 되고(단 --as로 우회 가능),
@@ -4,7 +4,7 @@
4
4
  * 여기에 provider 이름을 넣지 않는다 — `scm.<제품>` 같은 값은 capability가 아니라
5
5
  * adapter identity이며, 그것으로 갈라지기 시작하면 Core가 provider를 아는 셈이 된다.
6
6
  */
7
- export declare const CAPABILITIES: readonly ["observe.delta", "inventory.enumerate", "context.resource", "context.thread", "context.change", "context.history", "canonical.read", "action.comment", "action.update", "presentation.digest", "presentation.priority", "approval.interactive", "identity.resolve"];
7
+ export declare const CAPABILITIES: readonly ["observe.delta", "inventory.enumerate", "context.resource", "context.thread", "context.change", "context.history", "canonical.read", "action.comment", "coordination.surface", "action.update", "presentation.digest", "presentation.priority", "approval.interactive", "identity.resolve"];
8
8
  export type Capability = (typeof CAPABILITIES)[number];
9
9
  /** describe() 산출물 — 호출도 네트워크도 없는 정적 선언 (C-09 §5). */
10
10
  export type AdapterDescriptor = {
@@ -21,6 +21,7 @@ export const CAPABILITIES = [
21
21
  'context.history',
22
22
  'canonical.read',
23
23
  'action.comment',
24
+ 'coordination.surface',
24
25
  'action.update',
25
26
  'presentation.digest',
26
27
  'presentation.priority',
@@ -20,3 +20,29 @@ export type ResolveDeps = {
20
20
  */
21
21
  export declare function shimTarget(shimText: string): string | null;
22
22
  export declare function resolveExternalCommand(command: string, args: readonly string[], deps?: ResolveDeps): ResolvedInvocation;
23
+ /**
24
+ * PATH 에서 실행 파일 하나를 찾는다 (POSIX). 없으면 null — 있는 척하지 않는다.
25
+ *
26
+ * Windows 는 위 `resolveExternalCommand` 가 shim 까지 풀어 주므로 여기서는 다루지 않는다.
27
+ */
28
+ export declare function findOnPath(command: string, deps?: {
29
+ env?: NodeJS.ProcessEnv;
30
+ exists?: (path: string) => boolean;
31
+ platform?: NodeJS.Platform;
32
+ }): string | null;
33
+ /** 시스템 기본 자리. 서비스 관리자가 주는 PATH 가 대개 이것이다. */
34
+ export declare const SYSTEM_PATH: readonly ["/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"];
35
+ /**
36
+ * 서비스에 실어 보낼 PATH — **지금 이 셸의 PATH 를 통째로 옮기지 않는다.** 세션마다 붙는
37
+ * 임시 디렉터리가 그대로 들어가면 등록물이 매번 STALE 이 되고, 사라진 경로가 남는다.
38
+ * 대신 필요한 실행 파일이 **실제로 있는 디렉터리만** 고른다. 못 찾은 도구는 조용히 빠지지
39
+ * 않고 `missing` 에 남는다 — 그 도구를 쓰는 통로는 서비스에서도 열리지 않을 것이다.
40
+ */
41
+ export declare function servicePath(tools: readonly string[], deps?: {
42
+ env?: NodeJS.ProcessEnv;
43
+ exists?: (path: string) => boolean;
44
+ platform?: NodeJS.Platform;
45
+ }): {
46
+ path: string;
47
+ missing: string[];
48
+ };
@@ -13,7 +13,7 @@
13
13
  // ③ shim을 못 읽으면 cmd.exe /d /c 로 그 .cmd 를 부른다 — cmd.exe는 진짜 실행 파일이라
14
14
  // shell 옵션이 필요 없다.
15
15
  // 셋 다 실패하면 이름 그대로 돌려준다 — PATH에 진짜 실행 파일이 있는 환경이 그 경우다.
16
- import { existsSync, readFileSync } from 'node:fs';
16
+ import { accessSync, constants, existsSync, readFileSync, statSync } from 'node:fs';
17
17
  import { delimiter as winDelimiter, dirname, extname, isAbsolute, join } from 'node:path/win32';
18
18
  const defaultRead = (path) => {
19
19
  try {
@@ -77,3 +77,60 @@ export function resolveExternalCommand(command, args, deps = {}) {
77
77
  }
78
78
  return { command, args: [...args] };
79
79
  }
80
+ /**
81
+ * PATH 에서 실행 파일 하나를 찾는다 (POSIX). 없으면 null — 있는 척하지 않는다.
82
+ *
83
+ * Windows 는 위 `resolveExternalCommand` 가 shim 까지 풀어 주므로 여기서는 다루지 않는다.
84
+ */
85
+ export function findOnPath(command, deps = {}) {
86
+ const platform = deps.platform ?? process.platform;
87
+ if (platform === 'win32')
88
+ return null;
89
+ if (command.includes('/'))
90
+ return (deps.exists ?? isExecutable)(command) ? command : null;
91
+ const exists = deps.exists ?? isExecutable;
92
+ const pathValue = (deps.env ?? process.env).PATH ?? '';
93
+ for (const dir of pathValue.split(':')) {
94
+ if (!dir)
95
+ continue;
96
+ const candidate = `${dir}/${command}`;
97
+ if (exists(candidate))
98
+ return candidate;
99
+ }
100
+ return null;
101
+ }
102
+ const isExecutable = (path) => {
103
+ try {
104
+ accessSync(path, constants.X_OK);
105
+ return statSync(path).isFile();
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ };
111
+ /** 시스템 기본 자리. 서비스 관리자가 주는 PATH 가 대개 이것이다. */
112
+ export const SYSTEM_PATH = ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin'];
113
+ /**
114
+ * 서비스에 실어 보낼 PATH — **지금 이 셸의 PATH 를 통째로 옮기지 않는다.** 세션마다 붙는
115
+ * 임시 디렉터리가 그대로 들어가면 등록물이 매번 STALE 이 되고, 사라진 경로가 남는다.
116
+ * 대신 필요한 실행 파일이 **실제로 있는 디렉터리만** 고른다. 못 찾은 도구는 조용히 빠지지
117
+ * 않고 `missing` 에 남는다 — 그 도구를 쓰는 통로는 서비스에서도 열리지 않을 것이다.
118
+ */
119
+ export function servicePath(tools, deps = {}) {
120
+ const dirs = [];
121
+ const missing = [];
122
+ const push = (dir) => {
123
+ if (!dirs.includes(dir))
124
+ dirs.push(dir);
125
+ };
126
+ for (const tool of tools) {
127
+ const found = findOnPath(tool, deps);
128
+ if (found)
129
+ push(found.slice(0, found.lastIndexOf('/')) || '/');
130
+ else
131
+ missing.push(tool);
132
+ }
133
+ for (const dir of SYSTEM_PATH)
134
+ push(dir);
135
+ return { path: dirs.join(':'), missing };
136
+ }
@@ -14,6 +14,15 @@ export type ServiceCommand = {
14
14
  args: readonly string[];
15
15
  /** 회차 간격(초). Core 상수가 아니다 — 호출자가 정한다 (C-12 불변식 ③). */
16
16
  intervalSeconds: number;
17
+ /**
18
+ * 등록물이 돌 때의 환경. **서비스는 로그인 셸의 PATH 를 물려받지 않는다** — 실기계에서
19
+ * launchd 가 준 PATH 는 `/usr/bin:/bin:/usr/sbin:/sbin` 뿐이라 외부 통로 실행 파일을
20
+ * 하나도 못 찾았다. 등록하는 쪽이 필요한 것을 결정해 여기 실어 보내고, 내용이 바뀌면
21
+ * 등록물이 STALE 로 드러난다 — 셸 설정을 추측하는 경로는 없다.
22
+ */
23
+ environment?: Readonly<Record<string, string>>;
24
+ /** 회차의 표준 출력·오류를 남길 파일. 없으면 OS 기본(대개 버려진다). */
25
+ logPath?: string;
17
26
  };
18
27
  /** 지금 이 기계의 등록 상태. */
19
28
  export type ServiceState =
@@ -1,9 +1,9 @@
1
1
  export declare const RUNTIME_PACKAGE = "@asc-agent/runtime";
2
2
  export declare const BOOTSTRAP_PACKAGE = "@asc-agent/bootstrap";
3
3
  /** runtime과 bootstrap은 초기 release에서 lockstep이다. */
4
- export declare const RELEASE_VERSION = "0.4.0";
5
- export declare const RUNTIME_SPEC = "@asc-agent/runtime@0.4.0";
6
- export declare const BOOTSTRAP_SPEC = "@asc-agent/bootstrap@0.4.0";
4
+ export declare const RELEASE_VERSION = "0.5.1";
5
+ export declare const RUNTIME_SPEC = "@asc-agent/runtime@0.5.1";
6
+ export declare const BOOTSTRAP_SPEC = "@asc-agent/bootstrap@0.5.1";
7
7
  /**
8
8
  * 아직 설치되지 않은 machine에서 그대로 실행되는 형태 (C-14 §3.4).
9
9
  *
@@ -9,7 +9,7 @@
9
9
  export const RUNTIME_PACKAGE = '@asc-agent/runtime';
10
10
  export const BOOTSTRAP_PACKAGE = '@asc-agent/bootstrap';
11
11
  /** runtime과 bootstrap은 초기 release에서 lockstep이다. */
12
- export const RELEASE_VERSION = '0.4.0';
12
+ export const RELEASE_VERSION = '0.5.1';
13
13
  export const RUNTIME_SPEC = `${RUNTIME_PACKAGE}@${RELEASE_VERSION}`;
14
14
  export const BOOTSTRAP_SPEC = `${BOOTSTRAP_PACKAGE}@${RELEASE_VERSION}`;
15
15
  /**