@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,95 @@
1
+ import type { StateStore } from '../../ports/state-store.ts';
2
+ import type { HealthAlert } from '../monitor/health-alerts.ts';
3
+ import type { EscalationRecord } from './escalation.ts';
4
+ import type { DecisionSummary } from '../view/decision-view.ts';
5
+ export type FrontState = {
6
+ /** 지금 돌고 있는 것. */
7
+ active: {
8
+ id: string;
9
+ role: string;
10
+ status: string;
11
+ goal: string;
12
+ position?: string;
13
+ /**
14
+ * 지금 이 세션을 집고 있는 physical 실행 id (기존 한계 L-4).
15
+ *
16
+ * `--physical` 을 요구하는 명령(progress report·pause·done·release)이 여럿인데
17
+ * 그 값을 되찾을 곳은 `asc session audit <S-ID>` 뿐이었다 — 세션 id를 이미 알아야
18
+ * 부를 수 있는 명령이다. 지금 무엇이 도는지 보는 화면이 그것을 같이 말한다.
19
+ */
20
+ physical?: string;
21
+ }[];
22
+ /**
23
+ * 발급됐는데 아무도 집지 않은 것 (C-10 §1.1). 회수 대기와 다른 상태다 —
24
+ * 이건 시작조차 안 됐다는 뜻이고, 그 사실이 안 보이면 위임이 조용히 증발한다.
25
+ */
26
+ unclaimed: {
27
+ id: string;
28
+ role: string;
29
+ goal: string;
30
+ }[];
31
+ /** 회수를 기다리는 handoff. 끝났는데 아무도 안 거둔 것들이다. */
32
+ awaitingCollect: {
33
+ id: string;
34
+ next: string;
35
+ }[];
36
+ /** 사람이 결정해야 하는 것. */
37
+ pendingDecisions: DecisionSummary[];
38
+ /** Controller 상태가 든 미결. closure·query가 여기 합류한다. */
39
+ awaitingController: readonly string[];
40
+ /** 감시 자체의 상태. 비어 있지 않으면 위 목록을 그대로 믿으면 안 된다. */
41
+ health: readonly HealthAlert[];
42
+ /**
43
+ * 사람이 결정해야 풀리는 것 (C-13). **무엇이 계속 가는지 함께 든다** —
44
+ * 외부 대기 하나가 전체를 세운 것처럼 보이면 사람이 그 세션을 포기한다.
45
+ */
46
+ escalations: readonly {
47
+ id: string;
48
+ predicates: string[];
49
+ question: string;
50
+ blocked: string[];
51
+ /** 막힌 경계(쓰기 범위). 막힌 항목과 다른 사실이라 따로 든다. */
52
+ blockedScope: string[];
53
+ runnable: string[];
54
+ }[];
55
+ /** 이 workspace가 무엇인지. 붙은 자리를 사람이 확인할 수 있어야 한다. */
56
+ workspace?: {
57
+ workspaceId: string;
58
+ locator: string;
59
+ };
60
+ };
61
+ export type RestoreInput = {
62
+ store: StateStore;
63
+ pending: readonly DecisionSummary[];
64
+ health?: readonly HealthAlert[];
65
+ /** 미해소 상신. 없으면 이 축은 그리지 않는다 — 없는 것을 0으로 보이게 하지 않는다. */
66
+ escalations?: readonly EscalationRecord[];
67
+ workspace?: {
68
+ workspaceId: string;
69
+ locator: string;
70
+ };
71
+ /**
72
+ * 세션별 소유권 조회. **구조로만 받는다** — Core가 Host adapter를 알면 안 된다.
73
+ * 없으면 이 축은 그리지 않는다(모르는 것을 "없음"으로 그리지 않는다).
74
+ */
75
+ bindings?: {
76
+ get(sessionId: string): Promise<{
77
+ physicalSessionId: string;
78
+ } | null>;
79
+ };
80
+ };
81
+ /**
82
+ * 지금 상태를 모은다. **쓰지 않는다.**
83
+ *
84
+ * 세션 목록은 entity에서, 미결은 Control State에서 온다 — 둘 다 이미 있는 것이고
85
+ * 새로 계산하지 않는다. 여기서 파생을 만들면 그 파생이 곧 두 번째 정본이 된다.
86
+ */
87
+ export declare function restoreFront(input: RestoreInput): Promise<FrontState>;
88
+ /**
89
+ * 사람이 읽는 복원 화면.
90
+ *
91
+ * 순서가 곧 우선순위다: **내가 지금 결정해야 하는 것**이 먼저고, 돌고 있는 것이 그다음,
92
+ * 감시 상태가 마지막이다. 다만 감시가 고장 났으면 그 사실을 목록보다 먼저 말한다 —
93
+ * 믿을 수 없는 목록을 조용히 보여주는 것이 가장 나쁘다.
94
+ */
95
+ export declare function renderFront(state: FrontState): string[];
@@ -0,0 +1,144 @@
1
+ // Front Session Restore — 새 대화를 열었을 때 지금 상태를 되찾는다 (C-12 §4).
2
+ //
3
+ // 사람이 매번 `asc status`·`asc resume`·`asc inbox list` 를 차례로 쳐서 자기 상황을
4
+ // 재구성하는 것은 최종형이 아니다. **상태는 지속되고 대화는 갈아입는다** — 새 Front
5
+ // Session이 붙으면 그 자리에서 지금 무엇이 걸려 있는지 보여야 한다.
6
+ //
7
+ // 여기는 **읽기만 한다** (C-12 불변식 ⑮):
8
+ //
9
+ // 전이하지 않는다 · 승인 대기를 소비하지 않는다 · 소유권을 뺏지 않는다
10
+ //
11
+ // 복원이 상태를 건드리면 "확인했더니 바뀌었다"가 되고, 그러면 사람이 화면을 여는 것조차
12
+ // 조심스러워진다.
13
+ /**
14
+ * 지금 상태를 모은다. **쓰지 않는다.**
15
+ *
16
+ * 세션 목록은 entity에서, 미결은 Control State에서 온다 — 둘 다 이미 있는 것이고
17
+ * 새로 계산하지 않는다. 여기서 파생을 만들면 그 파생이 곧 두 번째 정본이 된다.
18
+ */
19
+ export async function restoreFront(input) {
20
+ const sessions = await input.store.list('session');
21
+ const control = await input.store.getControlState();
22
+ const running = sessions.filter((s) => s.status === 'ACTIVE' || s.status === 'PAUSED');
23
+ // 도는 세션만 소유권을 묻는다 — 전수 조회가 아니라 화면에 나오는 것만.
24
+ const owners = new Map();
25
+ if (input.bindings) {
26
+ for (const session of running) {
27
+ const owner = await input.bindings.get(session.id).catch(() => null);
28
+ if (owner)
29
+ owners.set(session.id, owner.physicalSessionId);
30
+ }
31
+ }
32
+ const active = running
33
+ .map((s) => ({
34
+ id: s.id,
35
+ role: s.role,
36
+ status: s.status,
37
+ goal: s.goal,
38
+ ...(owners.has(s.id) ? { physical: owners.get(s.id) } : {}),
39
+ // 이어받을 지점이 있으면 그것까지. 없으면 없는 대로 둔다.
40
+ ...(s.checkpoint ? { position: s.checkpoint.position } : {}),
41
+ }));
42
+ const unclaimed = sessions
43
+ .filter((s) => s.status === 'READY')
44
+ .map((s) => ({ id: s.id, role: s.role, goal: s.goal }));
45
+ const awaitingCollect = sessions
46
+ .filter((s) => s.status === 'DONE' && s.handoff)
47
+ .map((s) => ({ id: s.id, next: s.handoff.next }));
48
+ return {
49
+ active,
50
+ unclaimed,
51
+ awaitingCollect,
52
+ pendingDecisions: [...input.pending],
53
+ awaitingController: control.awaitingController ?? [],
54
+ health: input.health ?? [],
55
+ escalations: (input.escalations ?? []).map((record) => ({
56
+ id: record.escalationId,
57
+ predicates: [...record.predicates],
58
+ question: record.question,
59
+ blocked: [...record.blockedNodes],
60
+ blockedScope: [...record.blockedScope],
61
+ runnable: [...record.stillRunnableNodes],
62
+ })),
63
+ ...(input.workspace ? { workspace: input.workspace } : {}),
64
+ };
65
+ }
66
+ /**
67
+ * 사람이 읽는 복원 화면.
68
+ *
69
+ * 순서가 곧 우선순위다: **내가 지금 결정해야 하는 것**이 먼저고, 돌고 있는 것이 그다음,
70
+ * 감시 상태가 마지막이다. 다만 감시가 고장 났으면 그 사실을 목록보다 먼저 말한다 —
71
+ * 믿을 수 없는 목록을 조용히 보여주는 것이 가장 나쁘다.
72
+ */
73
+ export function renderFront(state) {
74
+ const lines = [];
75
+ if (state.workspace)
76
+ lines.push(`workspace ${state.workspace.workspaceId} · ${state.workspace.locator}`);
77
+ if (state.health.length > 0) {
78
+ lines.push('Read this first — monitoring state:');
79
+ for (const alert of state.health)
80
+ lines.push(` [${alert.kind}] ${alert.detail}`);
81
+ }
82
+ lines.push(state.pendingDecisions.length > 0
83
+ ? `Awaiting your decision (${state.pendingDecisions.length}):`
84
+ : 'Nothing awaiting your decision');
85
+ for (const item of state.pendingDecisions.slice(0, 5)) {
86
+ lines.push(` ${item.priority} ${item.requestId} ${item.reference} ${item.title}`);
87
+ }
88
+ if (state.pendingDecisions.length > 5)
89
+ lines.push(` … and ${state.pendingDecisions.length - 5} more`);
90
+ if (state.active.length > 0) {
91
+ lines.push(`Running (${state.active.length}):`);
92
+ for (const session of state.active) {
93
+ lines.push(` ${session.id} [${session.role}] ${session.status} — ${session.goal}`);
94
+ // --physical 을 요구하는 명령이 여럿이다. 그 값을 여기서 집어 갈 수 있어야 한다 (L-4).
95
+ if (session.physical)
96
+ lines.push(` held by: ${session.physical}`);
97
+ if (session.position)
98
+ lines.push(` resume at: ${session.position}`);
99
+ }
100
+ }
101
+ if (state.escalations.length > 0) {
102
+ lines.push(`Awaiting an outside decision (${state.escalations.length}):`);
103
+ for (const item of state.escalations) {
104
+ lines.push(` ${item.id} [${item.predicates.join(', ')}] ${item.question}`);
105
+ lines.push(` blocked: ${item.blocked.join(', ')}`);
106
+ // 막힌 항목과 막힌 경계는 다른 사실이다 — 경계가 안 보이면 "어디까지 남의 것인가"를 다시 물어야 한다
107
+ if (item.blockedScope.length > 0)
108
+ lines.push(` boundary: ${item.blockedScope.join(', ')}`);
109
+ // 계속 가는 것을 같이 보여야 "전부 멈췄다"로 읽히지 않는다 (C-13 §6)
110
+ if (item.runnable.length > 0)
111
+ lines.push(` still running: ${item.runnable.join(', ')}`);
112
+ }
113
+ }
114
+ if (state.unclaimed.length > 0) {
115
+ lines.push(`Issued but nobody has picked them up (${state.unclaimed.length}):`);
116
+ for (const session of state.unclaimed)
117
+ lines.push(` ${session.id} [${session.role}] ${session.goal}`);
118
+ }
119
+ if (state.awaitingCollect.length > 0) {
120
+ lines.push(`Awaiting collection (${state.awaitingCollect.length}) — \`asc controller collect --as <actor>\``);
121
+ for (const done of state.awaitingCollect)
122
+ lines.push(` ${done.id} → ${done.next}`);
123
+ }
124
+ if (state.awaitingController.length > 0) {
125
+ lines.push('Still open:');
126
+ for (const line of state.awaitingController.slice(0, 5))
127
+ lines.push(` - ${line}`);
128
+ if (state.awaitingController.length > 5) {
129
+ lines.push(` … and ${state.awaitingController.length - 5} more`);
130
+ }
131
+ }
132
+ if (state.pendingDecisions.length === 0 &&
133
+ state.active.length === 0 &&
134
+ state.escalations.length === 0 &&
135
+ state.unclaimed.length === 0 &&
136
+ state.awaitingCollect.length === 0 &&
137
+ state.awaitingController.length === 0) {
138
+ // 비어 있는 것과 못 보는 것을 구분한다 (C-12 불변식 ⑫과 같은 태도)
139
+ lines.push(state.health.length > 0
140
+ ? 'Nothing is pending — but read the monitoring state above first.'
141
+ : 'Nothing is pending.');
142
+ }
143
+ return lines;
144
+ }
@@ -0,0 +1,54 @@
1
+ export type TickKind = 'delta' | 'reconcile' | 'census' | 'digest';
2
+ /** 한 회차에 무엇을 할지. 실패는 다음 회차로 넘어간다 — 한 번 실패가 루프를 죽이지 않는다. */
3
+ export type TickOutcome = {
4
+ at: string;
5
+ ran: TickKind[];
6
+ skipped: TickKind[];
7
+ failures: {
8
+ kind: TickKind;
9
+ detail: string;
10
+ }[];
11
+ };
12
+ export type Schedule = {
13
+ /** 각 작업의 최소 간격(ms). 0이면 매 회차마다. */
14
+ deltaMs: number;
15
+ reconcileMs: number;
16
+ censusMs: number;
17
+ digestMs: number;
18
+ };
19
+ export type OrchestratorDeps = {
20
+ schedule: Schedule;
21
+ /** 실제 일. 없는 갈래는 아예 돌리지 않는다 — 없는 것을 있는 척하지 않는다. */
22
+ actions: Partial<Record<TickKind, () => Promise<void>>>;
23
+ /**
24
+ * 마지막 실행 시각을 어디에 남길지. 재기동 후 복원의 근거가 이것뿐이다 (C-12 §1.1).
25
+ * 저장에 실패하면 다음 회차가 더 자주 도는 것으로 끝난다 — 감지가 멈추는 것보다 낫다.
26
+ */
27
+ lastRunAt: {
28
+ read: () => Promise<Partial<Record<TickKind, string>>>;
29
+ write: (kind: TickKind, at: string) => Promise<void>;
30
+ };
31
+ now?: () => string;
32
+ /** 진행 상황을 사람이 볼 수 있게. 조용히 도는 것은 상시성이 아니라 불투명이다. */
33
+ log?: (line: string) => void;
34
+ };
35
+ export declare class Orchestrator {
36
+ #private;
37
+ constructor(deps: OrchestratorDeps);
38
+ /**
39
+ * 한 회차. **호출 자체가 계기다** — 이 함수를 cron이 불러도, 아래 run()이 불러도 같다.
40
+ *
41
+ * 간격이 안 됐으면 건너뛴다. 건너뛴 것도 결과에 남긴다 — "돌았는데 아무 일도 없었다"와
42
+ * "아예 안 돌았다"는 다른 사실이다.
43
+ */
44
+ tick(): Promise<TickOutcome>;
45
+ /**
46
+ * 멈출 때까지 돈다.
47
+ *
48
+ * 회차 하나가 터져도 루프는 계속한다 — 외부가 잠깐 죽었다고 감시가 영영 서면
49
+ * 그 사이의 변화는 아무도 보지 못한다. 대신 실패를 조용히 삼키지 않고 남긴다.
50
+ */
51
+ run(intervalMs: number, sleep: (ms: number) => Promise<void>): Promise<void>;
52
+ stop(): void;
53
+ }
54
+ export declare function renderTick(outcome: TickOutcome): string;
@@ -0,0 +1,98 @@
1
+ // Background Orchestrator — 실행 계기를 갖는 자리 (C-12 §1).
2
+ //
3
+ // 지금까지 감시는 사람이 `asc monitor scan` 을 칠 때만 돌았다. lease·retry·cursor 같은
4
+ // 재진입 안전장치는 완비인데 **재진입시키는 주체가 없었다.**
5
+ //
6
+ // 이 모듈이 하는 일은 계기뿐이다:
7
+ //
8
+ // 하는 일 언제 무엇을 부를지 정한다
9
+ // 안 하는 일 판정한다 / 상태를 만든다 / 승인한다
10
+ //
11
+ // 같은 함수를 사람이 부르든 여기가 부르든 결과가 같아야 한다 (C-12 불변식 ②).
12
+ // 그래서 이 파일에는 Monitor·Approval의 로직이 한 줄도 없다 — 호출만 있다.
13
+ //
14
+ // 주기는 Core 상수가 아니다 (C-12 불변식 ③). 호출자가 정해 넣는다.
15
+ // scheduler 제품도 모른다 (불변식 ④) — cron이 이걸 부르든, 이게 스스로 돌든 같다.
16
+ const ORDER = ['delta', 'reconcile', 'census', 'digest'];
17
+ export class Orchestrator {
18
+ #deps;
19
+ #now;
20
+ #stopped = false;
21
+ constructor(deps) {
22
+ this.#deps = deps;
23
+ this.#now = deps.now ?? (() => new Date().toISOString());
24
+ }
25
+ /**
26
+ * 한 회차. **호출 자체가 계기다** — 이 함수를 cron이 불러도, 아래 run()이 불러도 같다.
27
+ *
28
+ * 간격이 안 됐으면 건너뛴다. 건너뛴 것도 결과에 남긴다 — "돌았는데 아무 일도 없었다"와
29
+ * "아예 안 돌았다"는 다른 사실이다.
30
+ */
31
+ async tick() {
32
+ const at = this.#now();
33
+ const last = await this.#deps.lastRunAt.read();
34
+ const outcome = { at, ran: [], skipped: [], failures: [] };
35
+ for (const kind of ORDER) {
36
+ const action = this.#deps.actions[kind];
37
+ if (!action)
38
+ continue;
39
+ if (!due(last[kind], at, intervalOf(this.#deps.schedule, kind))) {
40
+ outcome.skipped.push(kind);
41
+ continue;
42
+ }
43
+ try {
44
+ await action();
45
+ outcome.ran.push(kind);
46
+ // 성공한 것만 시각을 옮긴다. 실패한 회차를 "했다"로 적으면 다음 회차가 건너뛴다.
47
+ await this.#deps.lastRunAt.write(kind, at);
48
+ }
49
+ catch (error) {
50
+ outcome.failures.push({ kind, detail: error instanceof Error ? error.message : String(error) });
51
+ }
52
+ }
53
+ return outcome;
54
+ }
55
+ /**
56
+ * 멈출 때까지 돈다.
57
+ *
58
+ * 회차 하나가 터져도 루프는 계속한다 — 외부가 잠깐 죽었다고 감시가 영영 서면
59
+ * 그 사이의 변화는 아무도 보지 못한다. 대신 실패를 조용히 삼키지 않고 남긴다.
60
+ */
61
+ async run(intervalMs, sleep) {
62
+ this.#stopped = false;
63
+ while (!this.#stopped) {
64
+ const outcome = await this.tick();
65
+ this.#deps.log?.(renderTick(outcome));
66
+ if (this.#stopped)
67
+ break;
68
+ await sleep(intervalMs);
69
+ }
70
+ }
71
+ stop() {
72
+ this.#stopped = true;
73
+ }
74
+ }
75
+ const intervalOf = (schedule, kind) => kind === 'delta'
76
+ ? schedule.deltaMs
77
+ : kind === 'reconcile'
78
+ ? schedule.reconcileMs
79
+ : kind === 'census'
80
+ ? schedule.censusMs
81
+ : schedule.digestMs;
82
+ /** 간격이 찼는가. 기록이 없으면 처음이므로 돈다. */
83
+ function due(lastAt, at, intervalMs) {
84
+ if (!lastAt)
85
+ return true;
86
+ const elapsed = new Date(at).getTime() - new Date(lastAt).getTime();
87
+ // 시계가 뒤로 갔거나 기록이 깨졌으면 도는 쪽을 고른다 — 멈춰 있는 것이 더 나쁘다
88
+ return Number.isNaN(elapsed) || elapsed >= intervalMs;
89
+ }
90
+ export function renderTick(outcome) {
91
+ const parts = [`${outcome.at}`];
92
+ parts.push(outcome.ran.length > 0 ? `실행 ${outcome.ran.join(', ')}` : '실행 없음');
93
+ if (outcome.skipped.length > 0)
94
+ parts.push(`간격 대기 ${outcome.skipped.join(', ')}`);
95
+ for (const failure of outcome.failures)
96
+ parts.push(`실패 ${failure.kind}: ${failure.detail}`);
97
+ return parts.join(' · ');
98
+ }
@@ -0,0 +1,184 @@
1
+ import { z } from 'zod';
2
+ import { type OwnershipMap } from '../policy/ownership.ts';
3
+ import type { ScopedStore } from '../../ports/state-store.ts';
4
+ export declare const BoundedQuery: z.ZodObject<{
5
+ id: z.ZodString;
6
+ /** 물은 쪽. 답을 받은 뒤 흐름이 돌아갈 자리다. */
7
+ ownerSessionId: z.ZodString;
8
+ /** 물은 쪽의 파트. 되던지기 판정의 기준이라 발행 시점 스냅샷으로 박아 둔다. */
9
+ ownerRole: z.ZodOptional<z.ZodString>;
10
+ /** 어느 결정을 묻는가 (decision domain). 누구에게가 아니라 무엇에 대해서다. */
11
+ requestedAuthority: z.ZodString;
12
+ question: z.ZodString;
13
+ context: z.ZodOptional<z.ZodString>;
14
+ /**
15
+ * 답이 없을 때 물은 쪽이 취할 기본값. 이것이 있으면 질문은 차단이 아니라 확인이 되고,
16
+ * 없으면 blockingScope가 실제로 막힌다.
17
+ */
18
+ proposedDefault: z.ZodOptional<z.ZodString>;
19
+ /** 이 답이 없으면 막히는 범위. 비어 있으면 "막히지 않는다"는 뜻이다. */
20
+ blockingScope: z.ZodOptional<z.ZodString>;
21
+ expectedResponse: z.ZodDefault<z.ZodEnum<["DECIDE", "ANSWER"]>>;
22
+ /** 이 질의가 어느 질의에서 파생했는가. 되던지기는 여기로 드러난다. */
23
+ inReplyTo: z.ZodOptional<z.ZodString>;
24
+ openedAt: z.ZodString;
25
+ }, "strip", z.ZodTypeAny, {
26
+ id: string;
27
+ question: string;
28
+ openedAt: string;
29
+ ownerSessionId: string;
30
+ requestedAuthority: string;
31
+ expectedResponse: "DECIDE" | "ANSWER";
32
+ context?: string | undefined;
33
+ ownerRole?: string | undefined;
34
+ proposedDefault?: string | undefined;
35
+ blockingScope?: string | undefined;
36
+ inReplyTo?: string | undefined;
37
+ }, {
38
+ id: string;
39
+ question: string;
40
+ openedAt: string;
41
+ ownerSessionId: string;
42
+ requestedAuthority: string;
43
+ context?: string | undefined;
44
+ ownerRole?: string | undefined;
45
+ proposedDefault?: string | undefined;
46
+ blockingScope?: string | undefined;
47
+ expectedResponse?: "DECIDE" | "ANSWER" | undefined;
48
+ inReplyTo?: string | undefined;
49
+ }>;
50
+ export type BoundedQuery = z.infer<typeof BoundedQuery>;
51
+ export declare const QueryAnswer: z.ZodObject<{
52
+ kind: z.ZodEnum<["DECIDE", "ANSWER", "ESCALATE"]>;
53
+ /** 답한 파트. DECIDE는 이 값이 그 결정의 주인일 때만 성립한다. */
54
+ byRole: z.ZodString;
55
+ body: z.ZodString;
56
+ /** ESCALATE 대상. Agent가 아니라 사람·권한자여야 한다 (C-04 §4.1). */
57
+ escalateTo: z.ZodOptional<z.ZodString>;
58
+ at: z.ZodString;
59
+ }, "strip", z.ZodTypeAny, {
60
+ at: string;
61
+ kind: "DECIDE" | "ANSWER" | "ESCALATE";
62
+ byRole: string;
63
+ body: string;
64
+ escalateTo?: string | undefined;
65
+ }, {
66
+ at: string;
67
+ kind: "DECIDE" | "ANSWER" | "ESCALATE";
68
+ byRole: string;
69
+ body: string;
70
+ escalateTo?: string | undefined;
71
+ }>;
72
+ export type QueryAnswer = z.infer<typeof QueryAnswer>;
73
+ export type OpenOutcome = {
74
+ ok: true;
75
+ query: BoundedQuery;
76
+ } | {
77
+ ok: false;
78
+ reason: 'INVALID_ID';
79
+ detail: string;
80
+ } | {
81
+ ok: false;
82
+ reason: 'ALREADY_EXISTS';
83
+ detail: string;
84
+ } | {
85
+ ok: false;
86
+ reason: 'ORIGIN_NOT_FOUND';
87
+ detail: string;
88
+ }
89
+ /** 받은 질의를 제3자에게 다시 넘겼다. 종결 수단은 DECIDE/ANSWER/ESCALATE뿐이다. */
90
+ | {
91
+ ok: false;
92
+ reason: 'ONE_HOP_VIOLATION';
93
+ detail: string;
94
+ origin: BoundedQuery;
95
+ }
96
+ /** 같은 결정이 물은 쪽으로 되돌아왔다. */
97
+ | {
98
+ ok: false;
99
+ reason: 'CIRCULAR_DELEGATION';
100
+ detail: string;
101
+ origin: BoundedQuery;
102
+ };
103
+ export type AnswerOutcome = {
104
+ ok: true;
105
+ query: BoundedQuery;
106
+ answer: QueryAnswer;
107
+ } | {
108
+ ok: false;
109
+ reason: 'NOT_FOUND';
110
+ detail: string;
111
+ } | {
112
+ ok: false;
113
+ reason: 'ALREADY_ANSWERED';
114
+ detail: string;
115
+ answer: QueryAnswer;
116
+ }
117
+ /** 그 결정의 주인이 아니다. 할 수 있는 것은 ANSWER 또는 ESCALATE다. */
118
+ | {
119
+ ok: false;
120
+ reason: 'FORBIDDEN_AUTHORITY';
121
+ detail: string;
122
+ };
123
+ export type OpenSpec = {
124
+ id: string;
125
+ ownerSessionId: string;
126
+ ownerRole?: string;
127
+ requestedAuthority: string;
128
+ question: string;
129
+ context?: string;
130
+ proposedDefault?: string;
131
+ blockingScope?: string;
132
+ expectedResponse?: 'DECIDE' | 'ANSWER';
133
+ inReplyTo?: string;
134
+ };
135
+ export type Violation = {
136
+ attemptedId: string;
137
+ kind: 'ONE_HOP_VIOLATION' | 'CIRCULAR_DELEGATION';
138
+ originId: string;
139
+ detail: string;
140
+ at: string;
141
+ };
142
+ export declare class QueryLedger {
143
+ #private;
144
+ constructor(scope: ScopedStore, ownership?: OwnershipMap, now?: () => string);
145
+ /**
146
+ * 질의를 연다. 되던지기는 **여기서** 막는다 — 실행 중인 세션을 끊는 대신 발행을 막는
147
+ * 것이 OM §16(Interrupt 부재)을 지키면서 순환을 끊는 유일한 자리다.
148
+ */
149
+ open(spec: OpenSpec): Promise<OpenOutcome>;
150
+ /**
151
+ * 답은 한 번만 쓰인다. 두 사람이 동시에 답해도 먼저 쓴 것이 남고 나중 것은 거절된다 —
152
+ * 조용히 덮으면 누구 말을 따랐는지 알 수 없게 된다.
153
+ *
154
+ * **이 호출은 어떤 Core 전이도 일으키지 않는다.** DECIDE도 마찬가지다 (C-04 §3.4).
155
+ */
156
+ answer(id: string, input: Omit<QueryAnswer, 'at'>): Promise<AnswerOutcome>;
157
+ get(id: string): Promise<BoundedQuery | null>;
158
+ getAnswer(id: string): Promise<QueryAnswer | null>;
159
+ list(): Promise<{
160
+ query: BoundedQuery;
161
+ answer: QueryAnswer | null;
162
+ }[]>;
163
+ /**
164
+ * 사람에게 넘겨진 질의. 답은 쓰였지만 **끝난 것이 아니다** — 그 답이 "사람이 정하라"였다.
165
+ * 이것이 없으면 ESCALATE는 어느 화면에도 뜨지 않는 write-only 로그가 된다.
166
+ */
167
+ escalated(): Promise<{
168
+ query: BoundedQuery;
169
+ answer: QueryAnswer;
170
+ }[]>;
171
+ /** 아직 답이 없는 질의. 누군가 기다리고 있다는 뜻이다. */
172
+ pending(): Promise<BoundedQuery[]>;
173
+ /** 막힌 발행 시도. 막았다고 사라지지 않는다 — 같은 결정이 돌고 있었다는 사실이다. */
174
+ violations(): Promise<Violation[]>;
175
+ }
176
+ /**
177
+ * 사람에게 넘겨진 질의를 줄로. 상신했는데 어느 화면에도 안 뜨는 상태를 막는 것이 목적이다.
178
+ */
179
+ export declare function escalatedLines(answered: readonly {
180
+ query: BoundedQuery;
181
+ answer: QueryAnswer;
182
+ }[]): string[];
183
+ /** 사람이 읽을 줄로. collect의 "판단이 필요한 것"에 그대로 들어간다. */
184
+ export declare function queryLines(open: readonly BoundedQuery[], violations: readonly Violation[]): string[];