@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,231 @@
1
+ // Claude Host 설치물 관리 — skill · guard hook · manifest (C-03 §5.1).
2
+ //
3
+ // spike 결론(B-15): user-scope Skill + user-scope settings hook. Plugin을 버린 이유는
4
+ // marketplace 관리가 더 무겁고, enforcement 안정성은 설치 위치가 아니라 hook의 판별
5
+ // 로직이 결정하기 때문이다. 프로젝트 tracked 파일은 만들지 않는다 —
6
+ // `.claude/settings.local.json`은 gitignore 보장이 없어 배제했다(spike 실측).
7
+ //
8
+ // 설치는 계약이 셋이다 (C-03 §5.1):
9
+ // 반복 install → idempotent
10
+ // 같은 경로의 사용자 파일 → 무단 overwrite 금지 (digest 검증)
11
+ // uninstall → ASC가 설치했다고 manifest로 증명되는 것만 제거
12
+ import { createHash } from 'node:crypto';
13
+ import { mkdir, readFile, rm, rmdir, writeFile } from 'node:fs/promises';
14
+ import { homedir } from 'node:os';
15
+ import { dirname, join } from 'node:path';
16
+ import { hookScript } from "./guard.js";
17
+ import { skillBundle } from "./skill.js";
18
+ const sha = (text) => createHash('sha256').update(text).digest('hex').slice(0, 16);
19
+ export const defaultPaths = () => ({ claudeHome: join(homedir(), '.claude') });
20
+ const HOOK_MATCHER = 'Bash';
21
+ const HOOK_MARKER = 'asc-external-write-guard';
22
+ function locate(paths) {
23
+ return {
24
+ /** Bundle 전체. 파일이 늘어도 아래 계약(manifest·digest·멱등)은 그대로다 (C-05 §5). */
25
+ skills: skillBundle().map((skill) => ({
26
+ name: skill.name,
27
+ path: join(paths.claudeHome, 'skills', skill.name, 'SKILL.md'),
28
+ text: skill.text,
29
+ })),
30
+ /** hook은 **하나**로 둔다. guard는 안전 층이고 중복 등록은 그 자체가 위험이다. */
31
+ hook: join(paths.claudeHome, 'asc', 'guard-hook.mjs'),
32
+ settings: join(paths.claudeHome, 'settings.json'),
33
+ manifest: join(paths.claudeHome, 'asc', 'install-manifest.json'),
34
+ };
35
+ }
36
+ async function readJson(path) {
37
+ try {
38
+ return JSON.parse(await readFile(path, 'utf8'));
39
+ }
40
+ catch (error) {
41
+ if (error.code === 'ENOENT')
42
+ return null;
43
+ throw error;
44
+ }
45
+ }
46
+ export async function install(paths, now = () => new Date().toISOString(), opts = {}) {
47
+ const where = locate(paths);
48
+ const force = opts.force ?? false;
49
+ const manifest = (await readJson(where.manifest)) ?? {
50
+ files: {},
51
+ settingsHook: false,
52
+ installedAt: now(),
53
+ };
54
+ const written = [];
55
+ const skipped = [];
56
+ // 파일 배치. **세 경우를 가른다** (아래 `fileState` 와 같은 판정이다):
57
+ // 지금 source와 같다 → 그대로 둔다 (idempotent)
58
+ // 설치 당시와 같다(stale) → 지금 source로 수렴시킨다 — 이것이 업그레이드다
59
+ // 둘 다 아니다(user-modified)→ 남긴다. uninstall이 보존하는 것을 install이 지우면 안 된다
60
+ for (const [path, content] of [
61
+ ...where.skills.map((skill) => [skill.path, skill.text]),
62
+ [where.hook, hookScript()],
63
+ ]) {
64
+ const existing = await readFile(path, 'utf8').catch(() => null);
65
+ const state = fileState(existing, content, manifest.files[path]);
66
+ if (state === 'current') {
67
+ manifest.files[path] = sha(content); // 내용 동일 — manifest만 확실히 잡는다
68
+ continue;
69
+ }
70
+ if (state === 'modified' && !force) {
71
+ skipped.push({
72
+ path,
73
+ reason: manifest.files[path] === undefined
74
+ ? 'a file of yours is already there — not overwriting it'
75
+ : 'you edited this after install — pass --force to overwrite',
76
+ });
77
+ continue;
78
+ }
79
+ await mkdir(dirname(path), { recursive: true });
80
+ await writeFile(path, content, 'utf8');
81
+ manifest.files[path] = sha(content);
82
+ written.push(path);
83
+ }
84
+ // settings.json에 PreToolUse hook 등록 — ASC 항목만 다루고 나머지는 손대지 않는다
85
+ const settings = (await readJson(where.settings)) ?? {};
86
+ const hooks = (settings.hooks ?? {});
87
+ const preToolUse = (hooks.PreToolUse ?? []);
88
+ const command = hookCommand(where.hook);
89
+ const ours = preToolUse.filter((entry) => entry.hooks?.some((h) => h._asc === HOOK_MARKER));
90
+ // 등록은 돼 있는데 **다른 곳을 가리키는** 경우가 있다 — 그 상태에서 "설치됨"이라고
91
+ // 하면 guard가 없는데 있다고 믿는다. 우리 항목만 지금 경로로 고친다.
92
+ const misdirected = ours.some((entry) => entry.hooks?.some((h) => h._asc === HOOK_MARKER && h.command !== command));
93
+ if (ours.length === 0) {
94
+ preToolUse.push({ matcher: HOOK_MATCHER, hooks: [{ type: 'command', command, _asc: HOOK_MARKER }] });
95
+ }
96
+ else if (misdirected) {
97
+ for (const entry of ours) {
98
+ for (const h of entry.hooks ?? [])
99
+ if (h._asc === HOOK_MARKER)
100
+ h.command = command;
101
+ }
102
+ }
103
+ if (ours.length === 0 || misdirected) {
104
+ settings.hooks = { ...hooks, PreToolUse: preToolUse };
105
+ await mkdir(dirname(where.settings), { recursive: true });
106
+ await writeFile(where.settings, JSON.stringify(settings, null, 2) + '\n', 'utf8');
107
+ written.push(`${where.settings} (PreToolUse hook)`);
108
+ }
109
+ manifest.settingsHook = true;
110
+ await mkdir(dirname(where.manifest), { recursive: true });
111
+ await writeFile(where.manifest, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
112
+ return { written, skipped, hookRegistered: true };
113
+ }
114
+ /** 한 파일의 3-way 판정. install과 verify가 **같은 함수**를 쓴다 — 둘이 다르면 그게 결함이다. */
115
+ function fileState(existing, source, recorded) {
116
+ if (existing === null)
117
+ return 'missing';
118
+ const now = sha(existing);
119
+ if (now === sha(source))
120
+ return 'current';
121
+ if (recorded !== undefined && now === recorded)
122
+ return 'stale';
123
+ return 'modified';
124
+ }
125
+ function hookCommand(hookPath) {
126
+ return `node "${hookPath}"`;
127
+ }
128
+ export async function verifyInstall(paths) {
129
+ const where = locate(paths);
130
+ const manifest = (await readJson(where.manifest));
131
+ const expected = [
132
+ ...where.skills.map((skill) => [skill.path, skill.text]),
133
+ [where.hook, hookScript()],
134
+ ];
135
+ const files = [];
136
+ for (const [path, content] of expected) {
137
+ const existing = await readFile(path, 'utf8').catch(() => null);
138
+ files.push({ path, state: fileState(existing, content, manifest?.files[path]) });
139
+ }
140
+ const settings = await readJson(where.settings);
141
+ const preToolUse = (settings?.hooks?.PreToolUse ?? []);
142
+ const ourHooks = preToolUse.flatMap((entry) => (entry.hooks ?? []).filter((h) => h._asc === HOOK_MARKER));
143
+ const hookRegistered = ourHooks.length > 0;
144
+ // 등록은 있는데 다른 곳을 가리키면 설치본이 뒤처진 것이다 — 없는 것으로 치지 않고 stale로 본다
145
+ const hookMisdirected = hookRegistered && !ourHooks.some((h) => h.command === hookCommand(where.hook));
146
+ const status = (() => {
147
+ if (!manifest && files.every((f) => f.state === 'missing') && !hookRegistered)
148
+ return 'NOT_INSTALLED';
149
+ if (!manifest || files.some((f) => f.state === 'missing') || !hookRegistered)
150
+ return 'BROKEN';
151
+ if (files.some((f) => f.state === 'modified'))
152
+ return 'INSTALLED_MODIFIED';
153
+ if (files.some((f) => f.state === 'stale') || hookMisdirected)
154
+ return 'INSTALLED_STALE';
155
+ return 'INSTALLED_CURRENT';
156
+ })();
157
+ return { status, files, hookRegistered };
158
+ }
159
+ /**
160
+ * 설치가 지금 source 그대로인가. 기존 호출부(setup 판정·probe)는 참/거짓만 필요하다.
161
+ * **stale도 거짓이다** — 낡은 설치본을 "설치됨"이라 부르면 L-5가 그대로 남는다.
162
+ */
163
+ export async function verifyInstalled(paths) {
164
+ return (await verifyInstall(paths)).status === 'INSTALLED_CURRENT';
165
+ }
166
+ /** 사람이 읽는 줄. 상태마다 다음에 할 일이 다르므로 그것까지 말한다. */
167
+ export function installReportLines(report) {
168
+ const remedy = {
169
+ NOT_INSTALLED: 'not installed yet — run `asc host claude install`',
170
+ INSTALLED_CURRENT: 'matches the current source',
171
+ INSTALLED_STALE: 'the installation is behind the current source — `asc host claude install` converges it',
172
+ INSTALLED_MODIFIED: 'you edited some installed files — leave them, or overwrite with `install --force`',
173
+ BROKEN: 'part of the installation is missing, or the hook is not registered — `asc host claude install`',
174
+ };
175
+ const lines = [`Install state: ${report.status} — ${remedy[report.status]}`];
176
+ for (const file of report.files) {
177
+ if (file.state === 'current')
178
+ continue;
179
+ lines.push(` [${file.state}] ${file.path}`);
180
+ }
181
+ if (!report.hookRegistered)
182
+ lines.push(' [missing] PreToolUse hook registration in settings.json');
183
+ return lines;
184
+ }
185
+ /** manifest로 증명되는 것만 제거한다. 사용자가 고친 파일은 남기고 이유를 말한다. */
186
+ export async function uninstall(paths) {
187
+ const where = locate(paths);
188
+ const manifest = (await readJson(where.manifest));
189
+ const removed = [];
190
+ const kept = [];
191
+ if (!manifest)
192
+ return { removed, kept: [{ path: where.manifest, reason: 'no manifest — nothing is recorded as installed' }] };
193
+ for (const [path, digest] of Object.entries(manifest.files)) {
194
+ const existing = await readFile(path, 'utf8').catch(() => null);
195
+ if (existing === null)
196
+ continue;
197
+ if (sha(existing) !== digest) {
198
+ kept.push({ path, reason: 'edited after install — cannot prove ASC owns it, so it stays' });
199
+ continue;
200
+ }
201
+ await rm(path);
202
+ removed.push(path);
203
+ }
204
+ // settings에서 ASC hook 항목만 걷어낸다 — 무관한 설정은 그대로
205
+ const settings = await readJson(where.settings);
206
+ if (settings?.hooks) {
207
+ const hooks = settings.hooks;
208
+ const preToolUse = (hooks.PreToolUse ?? []);
209
+ const filtered = preToolUse.filter((entry) => !entry.hooks?.some((h) => h._asc === HOOK_MARKER));
210
+ if (filtered.length !== preToolUse.length) {
211
+ if (filtered.length > 0)
212
+ hooks.PreToolUse = filtered;
213
+ else
214
+ delete hooks.PreToolUse;
215
+ // 우리가 만든 hooks 컨테이너가 비면 키째 걷는다 — 빈 {}도 원래 없던 흔적이다
216
+ if (Object.keys(hooks).length === 0)
217
+ delete settings.hooks;
218
+ await writeFile(where.settings, JSON.stringify(settings, null, 2) + '\n', 'utf8');
219
+ removed.push(`${where.settings} (PreToolUse hook entry)`);
220
+ }
221
+ }
222
+ await rm(where.manifest, { force: true });
223
+ await rm(dirname(where.manifest), { recursive: true, force: true }).catch(() => { });
224
+ // 파일만 지우면 빈 skills/<name>/ 이 남는다 (P1 관찰 ⑥). 우리가 만든 디렉터리이므로
225
+ // 우리가 걷되 **비어 있을 때만** 걷는다 — rmdir은 안에 무언가 남아 있으면 실패하고,
226
+ // 그 경우엔 남기는 것이 맞다(사용자가 넣은 것일 수 있다).
227
+ for (const skill of where.skills) {
228
+ await rmdir(dirname(skill.path)).catch(() => { });
229
+ }
230
+ return { removed, kept };
231
+ }
@@ -0,0 +1,38 @@
1
+ import { z } from 'zod';
2
+ import type { ScopedStore } from '../../ports/state-store.ts';
3
+ export declare const Heartbeat: z.ZodObject<{
4
+ logicalSessionId: z.ZodString;
5
+ /** binding의 owner. subagent가 여럿이어도 heartbeat는 Logical Session당 하나다. */
6
+ physicalSessionId: z.ZodString;
7
+ /** 실제로 관찰된 세션 — owner일 수도 workerId일 수도 있다. */
8
+ observedSessionId: z.ZodString;
9
+ lastTool: z.ZodOptional<z.ZodString>;
10
+ lastActivityAt: z.ZodString;
11
+ }, "strip", z.ZodTypeAny, {
12
+ logicalSessionId: string;
13
+ physicalSessionId: string;
14
+ observedSessionId: string;
15
+ lastActivityAt: string;
16
+ lastTool?: string | undefined;
17
+ }, {
18
+ logicalSessionId: string;
19
+ physicalSessionId: string;
20
+ observedSessionId: string;
21
+ lastActivityAt: string;
22
+ lastTool?: string | undefined;
23
+ }>;
24
+ export type Heartbeat = z.infer<typeof Heartbeat>;
25
+ export declare const heartbeatKey: (logicalSessionId: string) => string;
26
+ /** 읽는 쪽. 깨진 기록은 없는 것으로 친다 — 관찰값 때문에 화면이 죽으면 안 된다. */
27
+ export declare function readHeartbeat(scope: ScopedStore, logicalSessionId: string): Promise<Heartbeat | null>;
28
+ /**
29
+ * hook에 텍스트로 삽입되는 observer 본문. hook은 의존성 없는 단일 파일이어야 하므로
30
+ * (guard.ts 주석) 저장 계약을 여기서 하드코딩한다 — 형식이 바뀌면 이 파일과
31
+ * Markdown ScopedStore가 함께 움직여야 한다.
32
+ *
33
+ * 규약 셋:
34
+ * - 절대 throw하지 않는다. 호출자는 try/catch로 한 번 더 감싸지만 여기서도 삼킨다.
35
+ * - 아무것도 반환하지 않는다 — 호출자의 분기에 영향을 줄 값이 없어야 한다.
36
+ * - tmp+rename으로 쓴다. 반쯤 쓰인 파일을 다른 프로세스가 읽지 않도록.
37
+ */
38
+ export declare function observerSnippet(): string;
@@ -0,0 +1,77 @@
1
+ // Runtime Observer — hook에서 관찰되는 활동 신호 (B-18).
2
+ //
3
+ // Safety Guard(guard.ts)와 코드를 나눈 이유: 둘은 실패의 의미가 다르다. 차단이 실패하면
4
+ // 막아야 할 것이 나가고, 관찰이 실패하면 화면 한 줄이 비는 것뿐이다. 같은 hook 안에서
5
+ // 돌더라도 telemetry가 safety 판정을 흔들 수 없어야 하고, 그 경계가 파일로 보여야 한다.
6
+ //
7
+ // 무엇을 기록하지 않는가가 더 중요하다:
8
+ //
9
+ // heartbeat는 "worker가 살아 있다"가 아니라 **"Bash가 관찰됐다"** 까지다.
10
+ // 파일만 고치는 구간, 모델이 생각하는 구간, subagent를 기다리는 구간에는 신호가 없다 —
11
+ // 신호가 없다는 것이 멈췄다는 뜻이 아니다. 그래서 이 값으로 inactive·stale을
12
+ // 판정하지 않는다 (Renderer 규칙: render.ts).
13
+ //
14
+ // 왜 RuntimeBinding에 쓰지 않는가: binding 파일은 guard가 "이 세션이 ASC-managed인가"를
15
+ // 판별하는 근거다. 매 Bash마다 그 파일을 다시 쓰면, 쓰기 중 읽히거나 한 번 깨졌을 때
16
+ // 세션이 unmanaged로 보이고 외부 write 차단이 통째로 풀린다. 관찰값이 안전 판정을
17
+ // 망가뜨리는 구조는 만들지 않는다 — 그래서 별도 키다.
18
+ import { z } from 'zod';
19
+ export const Heartbeat = z.object({
20
+ logicalSessionId: z.string().min(1),
21
+ /** binding의 owner. subagent가 여럿이어도 heartbeat는 Logical Session당 하나다. */
22
+ physicalSessionId: z.string().min(1),
23
+ /** 실제로 관찰된 세션 — owner일 수도 workerId일 수도 있다. */
24
+ observedSessionId: z.string().min(1),
25
+ lastTool: z.string().optional(),
26
+ lastActivityAt: z.string().min(1),
27
+ });
28
+ export const heartbeatKey = (logicalSessionId) => `heartbeat:${logicalSessionId}`;
29
+ /** 읽는 쪽. 깨진 기록은 없는 것으로 친다 — 관찰값 때문에 화면이 죽으면 안 된다. */
30
+ export async function readHeartbeat(scope, logicalSessionId) {
31
+ try {
32
+ const raw = await scope.get(heartbeatKey(logicalSessionId));
33
+ if (!raw)
34
+ return null;
35
+ const parsed = Heartbeat.safeParse(JSON.parse(raw));
36
+ return parsed.success ? parsed.data : null;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ /**
43
+ * hook에 텍스트로 삽입되는 observer 본문. hook은 의존성 없는 단일 파일이어야 하므로
44
+ * (guard.ts 주석) 저장 계약을 여기서 하드코딩한다 — 형식이 바뀌면 이 파일과
45
+ * Markdown ScopedStore가 함께 움직여야 한다.
46
+ *
47
+ * 규약 셋:
48
+ * - 절대 throw하지 않는다. 호출자는 try/catch로 한 번 더 감싸지만 여기서도 삼킨다.
49
+ * - 아무것도 반환하지 않는다 — 호출자의 분기에 영향을 줄 값이 없어야 한다.
50
+ * - tmp+rename으로 쓴다. 반쯤 쓰인 파일을 다른 프로세스가 읽지 않도록.
51
+ */
52
+ export function observerSnippet() {
53
+ return `
54
+ // ── 여기부터 telemetry다 ──────────────────────────────────────────────
55
+ // 실패해도 차단 판정에 영향이 없어야 한다. 반환값 없음, 예외 없음.
56
+ function recordActivity(ascRoot, entry, observedSessionId, toolName) {
57
+ try {
58
+ const dir = join(ascRoot, 'adapters', 'claude-code')
59
+ const key = 'heartbeat:' + entry.logicalSessionId
60
+ const file = join(dir, key.replace(/[^A-Za-z0-9._-]/g, '-') + '.json')
61
+ const value = JSON.stringify({
62
+ logicalSessionId: entry.logicalSessionId,
63
+ // owner를 적는다 — workerId로 매치됐어도 소유권 기록과 어긋나면 안 된다
64
+ physicalSessionId: entry.physicalSessionId,
65
+ observedSessionId,
66
+ lastTool: toolName,
67
+ lastActivityAt: new Date().toISOString(),
68
+ })
69
+ const tmp = file + '.tmp-' + process.pid
70
+ writeFileSync(tmp, JSON.stringify({ key, value }, null, 2), 'utf8')
71
+ renameSync(tmp, file)
72
+ } catch {
73
+ // 관찰 실패는 관찰 실패일 뿐이다
74
+ }
75
+ }
76
+ `;
77
+ }
@@ -0,0 +1,53 @@
1
+ export declare const CAPABILITIES: readonly ["cross_session_message", "list_live_sessions", "notify_when_idle", "fork_subagent", "background_agent", "agent_view_json", "worktree_isolation", "goal_loop", "hooks", "remote_control", "agent_teams", "dynamic_workflows", "external_write_guard"];
2
+ export type CapabilityName = (typeof CAPABILITIES)[number];
3
+ export type CapabilityVerdict = {
4
+ available: boolean | 'unknown';
5
+ source: 'cli-probe' | 'host-report' | 'not-probed';
6
+ detail?: string;
7
+ };
8
+ export type ProbeResult = {
9
+ claudeVersion: string | null;
10
+ probedAt: string;
11
+ capabilities: Record<CapabilityName, CapabilityVerdict>;
12
+ };
13
+ /** 안전에 필수인 capability — 없으면 degrade가 아니라 STOP이다 (C-03 §5.2). */
14
+ export declare const SAFETY_CRITICAL: readonly CapabilityName[];
15
+ /**
16
+ * 외부 명령 한 번. 실패 이유는 구분하지 않는다 — 미설치든 비정상 종료든 "실측하지 못했다"는
17
+ * 같은 결론이기 때문이다.
18
+ */
19
+ export type CommandRunner = (command: string, args: string[]) => Promise<{
20
+ ok: boolean;
21
+ stdout: string;
22
+ }>;
23
+ /**
24
+ * CLI에서 실측 가능한 범위의 probe. runtime tool 여부는 unknown으로 남긴다.
25
+ * @param guardInstalled install manifest 검증 결과 — hook이 실제로 배치·등록됐는가.
26
+ */
27
+ export declare function probe(input: {
28
+ /** user-scope: hook 스크립트·settings 등록이 온전한가 (3층). */
29
+ guardInstalled: boolean;
30
+ /** project-scope: worker-settings.json(permissions.deny)이 준비됐는가 (2층). 프로젝트 밖이면 undefined. */
31
+ workerSettingsReady?: boolean;
32
+ now?: () => string;
33
+ /**
34
+ * 외부 명령 실행 통로. 생략하면 실제 CLI를 부른다 — 테스트가 호스트에 무엇이 깔렸는지에
35
+ * 좌우되지 않도록 주입만 열어 두고, 기본값은 바꾸지 않는다.
36
+ */
37
+ run?: CommandRunner;
38
+ }): Promise<ProbeResult>;
39
+ /**
40
+ * 호스트 세션(Claude 자신)이 자기 도구 목록을 보고 채우는 경로.
41
+ * skill이 안내하는 self-report가 여기로 들어온다.
42
+ */
43
+ export declare function applyHostReport(result: ProbeResult, report: Partial<Record<CapabilityName, boolean>>, at: string): ProbeResult;
44
+ export type Readiness = {
45
+ ok: true;
46
+ degraded: CapabilityName[];
47
+ } | {
48
+ ok: false;
49
+ reason: 'STOP';
50
+ missing: CapabilityName[];
51
+ };
52
+ /** optional 부재는 degrade, 안전 필수 부재·unknown은 STOP (C-03 §5.2). */
53
+ export declare function assessReadiness(result: ProbeResult): Readiness;
@@ -0,0 +1,129 @@
1
+ // Capability Probe — 버전표가 아니라 실측 (C-03 §5.2).
2
+ //
3
+ // 공식 Docs와 CHANGELOG가 일시적으로 다를 수 있으므로 실제 확인이 우선한다. 다만 정직해야
4
+ // 한다: CLI에서 실행해 알 수 있는 것과, Claude 세션 안에서만 보이는 것(runtime tool)은
5
+ // 다르다. 후자를 CLI가 아는 척하면 probe가 버전표보다 나을 게 없다. 그래서 판정마다
6
+ // 어떻게 알았는지(source)를 함께 기록하고, 모르는 것은 unknown으로 남긴다 —
7
+ // 호스트 세션이 자기 도구 목록을 보고 채우는 것은 별도 경로(host-report)다.
8
+ import { execFile } from 'node:child_process';
9
+ import { promisify } from 'node:util';
10
+ const run = promisify(execFile);
11
+ export const CAPABILITIES = [
12
+ 'cross_session_message',
13
+ 'list_live_sessions',
14
+ 'notify_when_idle',
15
+ 'fork_subagent',
16
+ 'background_agent',
17
+ 'agent_view_json',
18
+ 'worktree_isolation',
19
+ 'goal_loop',
20
+ 'hooks',
21
+ 'remote_control',
22
+ 'agent_teams',
23
+ 'dynamic_workflows',
24
+ 'external_write_guard',
25
+ ];
26
+ /** 안전에 필수인 capability — 없으면 degrade가 아니라 STOP이다 (C-03 §5.2). */
27
+ export const SAFETY_CRITICAL = ['external_write_guard'];
28
+ const tryRun = async (command, args) => {
29
+ try {
30
+ const { stdout } = await run(command, args, { timeout: 15_000 });
31
+ return { ok: true, stdout };
32
+ }
33
+ catch {
34
+ return { ok: false, stdout: '' };
35
+ }
36
+ };
37
+ /**
38
+ * CLI에서 실측 가능한 범위의 probe. runtime tool 여부는 unknown으로 남긴다.
39
+ * @param guardInstalled install manifest 검증 결과 — hook이 실제로 배치·등록됐는가.
40
+ */
41
+ export async function probe(input) {
42
+ const at = (input.now ?? (() => new Date().toISOString()))();
43
+ const exec = input.run ?? tryRun;
44
+ const version = await exec('claude', ['--version']);
45
+ const claudeVersion = version.ok ? version.stdout.trim().split(/\s/)[0] ?? null : null;
46
+ const unknown = (detail) => ({ available: 'unknown', source: 'not-probed', detail });
47
+ const capabilities = {};
48
+ if (!claudeVersion) {
49
+ for (const name of CAPABILITIES)
50
+ capabilities[name] = unknown('claude CLI를 찾지 못했다');
51
+ capabilities.external_write_guard = {
52
+ available: false,
53
+ source: 'cli-probe',
54
+ detail: 'claude CLI가 없으면 guard 등록을 확인할 수 없다',
55
+ };
56
+ return { claudeVersion, probedAt: at, capabilities };
57
+ }
58
+ // Agent View — 실제로 실행해 본다 (read-only)
59
+ const agents = await exec('claude', ['agents', '--json']);
60
+ let agentViewDetail = 'claude agents --json 실행 실패';
61
+ let agentViewOk = false;
62
+ if (agents.ok) {
63
+ try {
64
+ JSON.parse(agents.stdout);
65
+ agentViewOk = true;
66
+ agentViewDetail = 'claude agents --json 정상 파싱';
67
+ }
68
+ catch {
69
+ agentViewDetail = '실행은 됐으나 JSON이 아니다';
70
+ }
71
+ }
72
+ capabilities.agent_view_json = { available: agentViewOk, source: 'cli-probe', detail: agentViewDetail };
73
+ // Agent View가 돌면 background 세션 표면도 있는 것이다
74
+ capabilities.background_agent = { available: agentViewOk, source: 'cli-probe', detail: 'agent_view_json에서 추론' };
75
+ capabilities.list_live_sessions = { available: agentViewOk, source: 'cli-probe', detail: 'agent_view_json에서 추론' };
76
+ // hooks — 설정 표면이 있는지는 CLI로 단정할 수 없으나, guard 설치 검증이 실질 판정이다
77
+ capabilities.hooks = input.guardInstalled
78
+ ? { available: true, source: 'cli-probe', detail: 'ASC guard hook이 등록·검증됨' }
79
+ : unknown('guard 미설치 — asc host claude install 후 재probe');
80
+ // 안전 필수: 3층 중 enforcement 두 층(hook + worker permission deny)이 실제로 서 있는가.
81
+ // 프로젝트 밖에서는 worker-settings를 판정할 수 없으므로 false로 둔다 — worker는
82
+ // 프로젝트에서 돌고, 모르는 것을 있다고 치지 않는다.
83
+ const workerReady = input.workerSettingsReady === true;
84
+ capabilities.external_write_guard = {
85
+ available: input.guardInstalled && workerReady,
86
+ source: 'cli-probe',
87
+ detail: !input.guardInstalled
88
+ ? 'guard hook 미설치 — asc host claude install'
89
+ : !workerReady
90
+ ? 'worker-settings 미준비 — attach된 프로젝트에서 asc host claude guard'
91
+ : 'hook(3층) + worker permission deny(2층) 검증 통과',
92
+ };
93
+ // runtime tool — Claude 세션 안에서만 보인다. 아는 척하지 않는다
94
+ for (const name of [
95
+ 'cross_session_message',
96
+ 'notify_when_idle',
97
+ 'fork_subagent',
98
+ 'worktree_isolation',
99
+ 'goal_loop',
100
+ 'remote_control',
101
+ 'agent_teams',
102
+ 'dynamic_workflows',
103
+ ]) {
104
+ capabilities[name] = unknown('runtime tool — 호스트 세션의 self-report(host-report)로 채운다');
105
+ }
106
+ return { claudeVersion, probedAt: at, capabilities };
107
+ }
108
+ /**
109
+ * 호스트 세션(Claude 자신)이 자기 도구 목록을 보고 채우는 경로.
110
+ * skill이 안내하는 self-report가 여기로 들어온다.
111
+ */
112
+ export function applyHostReport(result, report, at) {
113
+ const merged = { ...result, probedAt: at, capabilities: { ...result.capabilities } };
114
+ for (const [name, available] of Object.entries(report)) {
115
+ // cli-probe가 이미 확정한 것은 self-report가 덮지 못한다 — 실측이 자기 보고보다 세다
116
+ if (merged.capabilities[name]?.source === 'cli-probe')
117
+ continue;
118
+ merged.capabilities[name] = { available, source: 'host-report' };
119
+ }
120
+ return merged;
121
+ }
122
+ /** optional 부재는 degrade, 안전 필수 부재·unknown은 STOP (C-03 §5.2). */
123
+ export function assessReadiness(result) {
124
+ const missing = SAFETY_CRITICAL.filter((name) => result.capabilities[name]?.available !== true);
125
+ if (missing.length > 0)
126
+ return { ok: false, reason: 'STOP', missing };
127
+ const degraded = CAPABILITIES.filter((name) => !SAFETY_CRITICAL.includes(name) && result.capabilities[name]?.available !== true);
128
+ return { ok: true, degraded };
129
+ }
@@ -0,0 +1,8 @@
1
+ /** 설치 단위. 파일이 늘어도 manifest 기반 설치·제거 계약은 그대로다 (C-05 §5). */
2
+ export declare function skillBundle(): {
3
+ name: string;
4
+ text: string;
5
+ }[];
6
+ export declare function skillText(): string;
7
+ export declare function inboxSkillText(): string;
8
+ export declare function reviewSkillText(): string;