@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,170 @@
1
+ // Human-first Progress Renderer (B-17).
2
+ //
3
+ // 목표는 ASC 내부 상태를 많이 보여주는 것이 아니다 — 사용자가 별도 질의 없이
4
+ // "내 작업이 지금 어떻게 되고 있는가"를 이해하게 하는 것이다. 그래서 본문은 항상
5
+ // 다섯 질문에 답한다: 지금 무엇을 / 무엇이 끝났나 / 문제가 있나 / 다음은 / 내 판단이
6
+ // 지금 필요한가.
7
+ //
8
+ // 규칙 셋:
9
+ // 1. 본문에 내부 용어(Logical Session·ACTIVE·writeBoundary·Verifier·RuntimeBinding)를
10
+ // 쓰지 않는다. 사람이 ASC를 배워야 이해되는 화면은 실패다. 내부 용어는 상세 한 줄에만.
11
+ // 2. Renderer는 progress schema 덤프가 아니다. 필드를 나열하는 대신 문장을 만든다.
12
+ // 3. ACTIVE 세션의 checkpoint를 현재 진행으로 읽지 않는다 — checkpoint는 중단 시점의
13
+ // 승계 정보이고, 재개 후에도 남아 있어 지금 상황과 다를 수 있다.
14
+ //
15
+ // heartbeat(관찰된 활동)만으로 "순조롭게 진행 중" 같은 의미론적 진척을 만들지 않는다.
16
+ // 활동 흔적은 진척이 아니다 — 보고가 없으면 없다고 말한다.
17
+ /** 내부 상태 → 사람 말. 상세 줄에서는 원래 용어를 써도 된다. */
18
+ const ROLE_WORDS = {
19
+ planner: '계획',
20
+ researcher: '조사',
21
+ implementer: '구현',
22
+ verifier: '검증',
23
+ };
24
+ const STATUS_WORDS = {
25
+ READY: '시작 대기',
26
+ ACTIVE: '작업 중',
27
+ PAUSED: '멈춤',
28
+ BLOCKED: '막힘',
29
+ DONE: '완료',
30
+ FAILED: '실패',
31
+ };
32
+ /** 보고가 이만큼 오래되면 지금 상황이라고 단정하지 않는다. */
33
+ const STALE_AFTER_MS = 20 * 60 * 1000;
34
+ export function renderProgress(input) {
35
+ const { session, progress, liveness } = input;
36
+ const awaiting = input.awaiting ?? [];
37
+ const now = input.now ?? new Date();
38
+ const work = session ? shortGoal(session.goal) : '이';
39
+ if (!progress) {
40
+ const state = session ? STATUS_WORDS[session.status] : '확인 불가';
41
+ // 활동 신호가 있으면 "언제 뭔가 돌긴 했다"까지만 더한다 — 그 이상은 지어내는 것이다
42
+ const noReport = liveness
43
+ ? `${minutesSince(liveness.lastActivityAt, now)}분 전에 활동이 관찰됐지만, 진행 내용 보고는 아직 없습니다.`
44
+ : '아직 진행 내용 보고가 없어 어디까지 됐는지는 알 수 없습니다.';
45
+ return {
46
+ body: [
47
+ `${work} 작업이 ${state === '작업 중' ? '진행 중입니다' : `${state} 상태입니다`}.`,
48
+ noReport,
49
+ awaiting.length > 0 ? awaitingLine(awaiting) : decisionLine('NONE'),
50
+ ],
51
+ detail: detailLine(session, null, now, liveness),
52
+ };
53
+ }
54
+ const body = [headline(work, progress)];
55
+ // 무엇이 끝났고 지금 무엇을 하는가 — 한 문단으로 붙인다.
56
+ // 끝난 작업에는 "지금 무엇을"이 없다 — 무엇을 마쳤는지만 말한다.
57
+ const done = progress.milestones.length > 0 ? joinKorean(progress.milestones) : '';
58
+ if (progress.terminal) {
59
+ if (done)
60
+ body.push(`${done}까지 완료했습니다.`);
61
+ }
62
+ else {
63
+ body.push(`${done ? `${done}까지 마쳤고, ` : ''}지금은 ${progress.phase}.`);
64
+ }
65
+ // 문제가 있는가 + 다음은 무엇인가
66
+ const trouble = troubleLine(progress);
67
+ const next = progress.terminal ? '' : progress.nextStep ? ` 다음은 ${progress.nextStep}.` : '';
68
+ if (trouble || next)
69
+ body.push(`${trouble}${next}`.trim());
70
+ body.push(awaiting.length > 0
71
+ ? awaitingLine(awaiting)
72
+ : decisionLine(progress.needsUserDecision, progress.decisionRef, progress.terminal));
73
+ if (isStale(progress, now)) {
74
+ body.push(`(마지막 보고가 ${minutesAgo(progress, now)}분 전입니다 — 그 사이 상황이 달라졌을 수 있습니다.)`);
75
+ }
76
+ return { body, detail: detailLine(session, progress, now, liveness) };
77
+ }
78
+ function headline(work, p) {
79
+ if (p.terminal)
80
+ return `${work} 작업을 마쳤습니다.`;
81
+ if (p.needsUserDecision === 'NOW')
82
+ return `${work} 작업이 멈춰 있으며, 사용자 판단이 필요합니다.`;
83
+ if (p.verifier === 'FAIL')
84
+ return `${work} 구현은 끝났지만 검증에서 문제를 발견했습니다.`;
85
+ if (p.verifier === 'RUNNING')
86
+ return `${work} 작업은 끝났고, 지금은 별도 검증을 진행하고 있습니다.`;
87
+ if (p.unresolved.length > 0) {
88
+ return `${work} 작업은 계속 진행 중이며, 확인이 필요한 항목 ${p.unresolved.length}건을 발견했습니다.`;
89
+ }
90
+ return `${work} 작업이 진행 중입니다.`;
91
+ }
92
+ function troubleLine(p) {
93
+ if (p.verifier === 'FAIL') {
94
+ return p.verifierDetail
95
+ ? `검증에서 확인된 문제: ${p.verifierDetail}. 아직 완료로 처리하지 않았습니다.`
96
+ : '검증에서 실패한 항목이 있어 아직 완료로 처리하지 않았습니다.';
97
+ }
98
+ if (p.unresolved.length > 0) {
99
+ return `확인이 필요한 항목: ${joinKorean(p.unresolved)}.`;
100
+ }
101
+ if (p.terminal)
102
+ return '';
103
+ // 판단을 기다리며 멈춰 있는데 "문제 없다"고 하면 앞뒤가 맞지 않는다 — 멈춤 자체가 문제다
104
+ if (p.needsUserDecision === 'NOW')
105
+ return '';
106
+ return '현재 작업을 막는 문제는 없습니다.';
107
+ }
108
+ /** 열린 상신이 있으면 무엇을 기다리는지 이름으로 말한다 — "받은함을 보라"보다 구체적이다. */
109
+ function awaitingLine(awaiting) {
110
+ return `지금 판단이 필요합니다 — ${awaiting.join(', ')} (asc inbox) 를 확인해 주세요.`;
111
+ }
112
+ function decisionLine(need, ref, terminal = false) {
113
+ switch (need) {
114
+ case 'NOW':
115
+ return ref
116
+ ? `지금 판단이 필요합니다 — ${ref} 를 확인해 주세요.`
117
+ : '지금 판단이 필요합니다 — 받은함(asc inbox)을 확인해 주세요.';
118
+ case 'LATER':
119
+ // 끝난 작업에 "계속 진행할 수 있습니다"라고 하면 아직 도는 줄 안다
120
+ return terminal
121
+ ? '자동화 작업은 끝났으며, 남은 항목은 따로 판단할 수 있습니다.'
122
+ : '작업은 계속 진행할 수 있습니다. 미확정 항목은 완료 시 함께 판단을 요청하겠습니다.';
123
+ case 'NONE':
124
+ return terminal ? '남은 판단 항목은 없습니다.' : '지금 사용자 판단이 필요한 항목은 없습니다.';
125
+ }
126
+ }
127
+ /** 2단계 정보. 여기서는 내부 용어를 그대로 써도 된다 — 본문 이해에 필수가 아니어야 한다. */
128
+ function detailLine(session, progress, now, liveness) {
129
+ const parts = session
130
+ ? [session.id, ROLE_WORDS[session.role], STATUS_WORDS[session.status]]
131
+ : [progress?.logicalSessionId ?? '(세션 미상)', '거둔 세션'];
132
+ if (progress) {
133
+ if (progress.verifier !== 'NONE')
134
+ parts.push(`검증 ${verifierWord(progress.verifier)}`);
135
+ if (progress.unresolved.length > 0)
136
+ parts.push(`미결 ${progress.unresolved.length}건`);
137
+ parts.push(`${minutesAgo(progress, now)}분 전 기준`);
138
+ }
139
+ else {
140
+ parts.push('진행 보고 없음');
141
+ }
142
+ // 있을 때만 덧붙인다 — 없다고 "활동 없음"을 적으면 그것도 판정이다
143
+ if (liveness) {
144
+ const tool = liveness.lastTool ? `(${liveness.lastTool})` : '';
145
+ parts.push(`최근 활동 ${minutesSince(liveness.lastActivityAt, now)}분 전${tool}`);
146
+ }
147
+ return parts.join(' · ');
148
+ }
149
+ const verifierWord = (v) => v === 'RUNNING' ? '진행 중' : v === 'PASS' ? 'PASS' : v === 'FAIL' ? 'FAIL' : '없음';
150
+ function minutesAgo(p, now) {
151
+ return minutesSince(p.lastUpdatedAt, now);
152
+ }
153
+ function minutesSince(at, now) {
154
+ return Math.max(0, Math.round((now.getTime() - new Date(at).getTime()) / 60_000));
155
+ }
156
+ const isStale = (p, now) => !p.terminal && now.getTime() - new Date(p.lastUpdatedAt).getTime() > STALE_AFTER_MS;
157
+ /**
158
+ * 목표 문장이 길면 첫 구절만 — 헤드라인이 목표 전문을 되뇌면 읽히지 않는다.
159
+ * ASCII 하이픈은 자르지 않는다: 식별자(G-2, spec-001)와 합성어에 흔해서
160
+ * 분리자로 쓰면 "G-2 …"가 "G"로 잘린다 (실측에서 걸렸다).
161
+ */
162
+ function shortGoal(goal) {
163
+ const head = goal.split(/\s[—–]\s|\.\s/)[0]?.trim() ?? goal;
164
+ return head.length > 40 ? `${head.slice(0, 40)}…` : head;
165
+ }
166
+ function joinKorean(items) {
167
+ if (items.length <= 1)
168
+ return items[0] ?? '';
169
+ return `${items.slice(0, -1).join(', ')}, ${items[items.length - 1]}`;
170
+ }
@@ -0,0 +1,61 @@
1
+ import { z } from 'zod';
2
+ export declare const RuntimeBinding: z.ZodObject<{
3
+ logicalSessionId: z.ZodString;
4
+ provider: z.ZodString;
5
+ physicalSessionId: z.ZodString;
6
+ workerId: z.ZodOptional<z.ZodString>;
7
+ runtimeKind: z.ZodOptional<z.ZodString>;
8
+ lastObservedState: z.ZodOptional<z.ZodString>;
9
+ capabilitySnapshot: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
10
+ updatedAt: z.ZodString;
11
+ }, "strip", z.ZodTypeAny, {
12
+ updatedAt: string;
13
+ logicalSessionId: string;
14
+ provider: string;
15
+ physicalSessionId: string;
16
+ workerId?: string | undefined;
17
+ runtimeKind?: string | undefined;
18
+ lastObservedState?: string | undefined;
19
+ capabilitySnapshot?: Record<string, boolean> | undefined;
20
+ }, {
21
+ updatedAt: string;
22
+ logicalSessionId: string;
23
+ provider: string;
24
+ physicalSessionId: string;
25
+ workerId?: string | undefined;
26
+ runtimeKind?: string | undefined;
27
+ lastObservedState?: string | undefined;
28
+ capabilitySnapshot?: Record<string, boolean> | undefined;
29
+ }>;
30
+ export type RuntimeBinding = z.infer<typeof RuntimeBinding>;
31
+ export type ClaimOutcome = {
32
+ ok: true;
33
+ binding: RuntimeBinding;
34
+ }
35
+ /** 이미 다른 Physical owner가 있다. 자동 탈취는 없다 — 명시적 rebind가 필요하다. */
36
+ | {
37
+ ok: false;
38
+ reason: 'RUNTIME_CONFLICT';
39
+ current: RuntimeBinding;
40
+ };
41
+ /**
42
+ * Adapter가 구현해야 하는 소유권 계약 (C-03 §3.2).
43
+ *
44
+ * 하나의 Logical Session에 동시 active Physical owner는 최대 1개다.
45
+ * claim은 원자적이어야 한다(`ScopedStore.setIfAbsent` 기반) — 두 Physical Session이
46
+ * 동시에 집으면 정확히 하나만 성공한다.
47
+ */
48
+ export interface RuntimeBindings {
49
+ /** owner가 없으면 잡는다. 있으면 RUNTIME_CONFLICT — 기다리지도 뺏지도 않는다. */
50
+ claim(binding: Omit<RuntimeBinding, 'updatedAt'>, at: string): Promise<ClaimOutcome>;
51
+ /** 관찰값 갱신. owner가 아닌 Physical Session의 갱신은 거부한다. */
52
+ observe(logicalSessionId: string, physicalSessionId: string, patch: Partial<Pick<RuntimeBinding, 'workerId' | 'runtimeKind' | 'lastObservedState' | 'capabilitySnapshot'>>, at: string): Promise<ClaimOutcome>;
53
+ /** owner 스스로 내려놓는다. 남의 것은 못 놓는다. */
54
+ release(logicalSessionId: string, physicalSessionId: string): Promise<boolean>;
55
+ /**
56
+ * 죽은 owner를 사람이 확인하고 갈아끼운다. 자동 탈취가 아니라 명시적 복구다 —
57
+ * stale 자동 회수는 MVP에 넣지 않는다 (C-03 §3.2).
58
+ */
59
+ rebind(binding: Omit<RuntimeBinding, 'updatedAt'>, at: string): Promise<RuntimeBinding>;
60
+ get(logicalSessionId: string): Promise<RuntimeBinding | null>;
61
+ }
@@ -0,0 +1,22 @@
1
+ // RuntimeBinding — Logical Session과 Physical Host 실행의 연결 (C-03 §3).
2
+ //
3
+ // 여기에는 provider-neutral한 형태와 계약만 있다. 실제 저장(StateStore.scope)은 Adapter
4
+ // 몫이다 — PresentationRecord와 같은 이유다(C-02 §3): provider가 늘 때 Core 스키마가
5
+ // 흔들리면 안 되고, runtime 관찰값은 정본이 아니다. Core EntityMap에 physical id가
6
+ // 들어가는 순간 이 경계는 무너진다.
7
+ //
8
+ // Binding은 관찰 metadata이자 **ownership claim**이다. Logical ≠ Physical은 승계를
9
+ // 허용하는 것이지 동시 실행을 허용하는 것이 아니다 — 서로 다른 Physical Session 둘이
10
+ // 같은 ACTIVE 세션을 동시에 이어가면 같은 계약 위에서 두 손이 움직인다.
11
+ import { z } from 'zod';
12
+ import { SessionId, Timestamp } from "../model/ids.js";
13
+ export const RuntimeBinding = z.object({
14
+ logicalSessionId: SessionId,
15
+ provider: z.string().min(1), // Adapter가 자기 이름을 댄다 — Core는 값을 해석하지 않는다
16
+ physicalSessionId: z.string().min(1),
17
+ workerId: z.string().optional(),
18
+ runtimeKind: z.string().optional(), // 'interactive' | 'background' | 'subagent' 등 provider 어휘
19
+ lastObservedState: z.string().optional(), // provider가 주는 상태 그대로 — Core는 해석하지 않는다
20
+ capabilitySnapshot: z.record(z.boolean()).optional(),
21
+ updatedAt: Timestamp,
22
+ });
@@ -0,0 +1,44 @@
1
+ /** decision domain 이름 문법. 소문자 kebab-case — 문서·CLI·JSON에서 같은 모양으로 읽힌다. */
2
+ export declare const DECISION_DOMAIN: RegExp;
3
+ export type OwnershipSpec = {
4
+ /** 이 역할의 쓰기 영역. 결정만 하는 역할(product 등)은 비어 있을 수 있다. */
5
+ paths: string[];
6
+ /** 이 역할이 최종 결정권을 갖는 decision domain 목록. */
7
+ authorities: string[];
8
+ };
9
+ export type OwnershipMap = Record<string, OwnershipSpec>;
10
+ export type AuthorityLookup = {
11
+ kind: 'RESOLVED';
12
+ role: string;
13
+ }
14
+ /** 아무도 자기 것이라 적지 않았다. */
15
+ | {
16
+ kind: 'UNDECLARED';
17
+ }
18
+ /** 둘 이상이 자기 것이라 적었다. 고르지 않는다 — 후보를 그대로 돌려준다. */
19
+ | {
20
+ kind: 'AMBIGUOUS';
21
+ candidates: string[];
22
+ };
23
+ /** 이 decision domain의 결정권자는 누구인가. */
24
+ export declare function lookupAuthority(map: OwnershipMap | undefined, domain: string): AuthorityLookup;
25
+ /** 선언된 decision domain 전체. 정렬해 돌려주므로 렌더 순서가 흔들리지 않는다. */
26
+ export declare function decisionDomains(map: OwnershipMap | undefined): string[];
27
+ export type OwnerLookup = {
28
+ kind: 'RESOLVED';
29
+ role: string;
30
+ }
31
+ /** 쓰기 범위를 통째로 품는 역할이 없다. 두 파트에 걸쳐 있거나 지도 밖이다. */
32
+ | {
33
+ kind: 'UNDECLARED';
34
+ } | {
35
+ kind: 'AMBIGUOUS';
36
+ candidates: string[];
37
+ };
38
+ /**
39
+ * 이 쓰기 범위는 누구 영역인가. **전부**를 품는 역할만 후보다 — 일부만 품는 역할을
40
+ * owner로 세우면 나머지 경로가 남의 영역인 채로 세션이 굴러간다.
41
+ *
42
+ * 범위가 비어 있으면(쓰지 않는 세션) 판정하지 않는다. 아무 역할이나 답이 되기 때문이다.
43
+ */
44
+ export declare function lookupOwnerByPaths(map: OwnershipMap | undefined, writeBoundary: readonly string[]): OwnerLookup;
@@ -0,0 +1,46 @@
1
+ // Ownership Map — 누가 어디를 쓰고 무엇을 결정하는가의 선언 (C-04 §6).
2
+ //
3
+ // 여기는 **선언을 읽는 곳**이지 판정하는 곳이 아니다. "이 세션을 막을 것인가"는 발급과
4
+ // preflight가 정하고, 이 모듈은 Profile이 뭐라고 적었는지만 돌려준다.
5
+ //
6
+ // 결정권자가 갈리는 경우를 조용히 하나로 좁히지 않는다. 두 역할이 같은 결정을 자기
7
+ // 것이라 적었다면 그건 프로젝트가 아직 정하지 않았다는 뜻이고, 그 사실이 답이다 —
8
+ // 이름이 비슷하다는 이유로 하나를 고르면 사람은 그 선택을 영영 보지 못한다.
9
+ import { isWithinScopes } from "./scope.js";
10
+ /** decision domain 이름 문법. 소문자 kebab-case — 문서·CLI·JSON에서 같은 모양으로 읽힌다. */
11
+ export const DECISION_DOMAIN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
12
+ /** 이 decision domain의 결정권자는 누구인가. */
13
+ export function lookupAuthority(map, domain) {
14
+ const candidates = Object.entries(map ?? {})
15
+ .filter(([, spec]) => spec.authorities.includes(domain))
16
+ .map(([role]) => role)
17
+ .sort();
18
+ if (candidates.length === 0)
19
+ return { kind: 'UNDECLARED' };
20
+ if (candidates.length > 1)
21
+ return { kind: 'AMBIGUOUS', candidates };
22
+ return { kind: 'RESOLVED', role: candidates[0] };
23
+ }
24
+ /** 선언된 decision domain 전체. 정렬해 돌려주므로 렌더 순서가 흔들리지 않는다. */
25
+ export function decisionDomains(map) {
26
+ return [...new Set(Object.values(map ?? {}).flatMap((spec) => spec.authorities))].sort();
27
+ }
28
+ /**
29
+ * 이 쓰기 범위는 누구 영역인가. **전부**를 품는 역할만 후보다 — 일부만 품는 역할을
30
+ * owner로 세우면 나머지 경로가 남의 영역인 채로 세션이 굴러간다.
31
+ *
32
+ * 범위가 비어 있으면(쓰지 않는 세션) 판정하지 않는다. 아무 역할이나 답이 되기 때문이다.
33
+ */
34
+ export function lookupOwnerByPaths(map, writeBoundary) {
35
+ if (writeBoundary.length === 0)
36
+ return { kind: 'UNDECLARED' };
37
+ const candidates = Object.entries(map ?? {})
38
+ .filter(([, spec]) => spec.paths.length > 0 && writeBoundary.every((s) => isWithinScopes(s, spec.paths)))
39
+ .map(([role]) => role)
40
+ .sort();
41
+ if (candidates.length === 0)
42
+ return { kind: 'UNDECLARED' };
43
+ if (candidates.length > 1)
44
+ return { kind: 'AMBIGUOUS', candidates };
45
+ return { kind: 'RESOLVED', role: candidates[0] };
46
+ }
@@ -0,0 +1,99 @@
1
+ import type { ActorRole } from '../model/entities.ts';
2
+ /** OM §5.1. Green/Yellow/Red를 정책 계층으로 승격한 것. */
3
+ export type Verdict = 'ALLOW' | 'SOFT_DENY' | 'HARD_DENY';
4
+ export type RoleName = Exclude<ActorRole, 'controller'> | 'planner' | 'researcher' | 'implementer' | 'verifier';
5
+ /**
6
+ * 한 계층이 기여하는 정책 조각. 계층 순서는 Resolver가 정한다
7
+ * (Vanilla Defaults → Project Profile → Operational Preset → User Local Override).
8
+ */
9
+ export type PolicyLayer = {
10
+ id: string;
11
+ /** 해제 불가 금지 항목. 하위 계층은 추가만 할 수 있다 (union·immutable). */
12
+ hardDeny?: readonly string[];
13
+ /** 기본 금지. Controller의 Policy Exception으로만 개별 허용된다 (union). */
14
+ softDeny?: readonly string[];
15
+ /**
16
+ * 명시 허용. HARD DENY 항목을 여기 적으면 해제 시도로 보고 resolve를 실패시킨다 —
17
+ * 조용히 무시하면 계약과 실권한이 어긋난 채로 굴러간다.
18
+ */
19
+ allow?: readonly string[];
20
+ /** Role별 최대 쓰기 범위. 하위 계층은 이보다 좁혀야 한다 (intersection). */
21
+ roleScopes?: Partial<Record<RoleName, readonly string[]>>;
22
+ /** scalar 설정. `lockedSettings`에 걸린 키는 하위 값이 무시된다. */
23
+ settings?: Readonly<Record<string, string | number | boolean>>;
24
+ lockedSettings?: readonly string[];
25
+ /** 하위 값으로 통째 교체되는 목록. */
26
+ replaceLists?: Readonly<Record<string, readonly string[]>>;
27
+ /** 상위+하위가 합쳐지는 목록. */
28
+ unionLists?: Readonly<Record<string, readonly string[]>>;
29
+ requiredCapabilities?: readonly string[];
30
+ optionalCapabilities?: readonly string[];
31
+ };
32
+ export type ResolvedPolicy = {
33
+ hardDeny: string[];
34
+ softDeny: string[];
35
+ roleScopes: Partial<Record<RoleName, string[]>>;
36
+ settings: Record<string, string | number | boolean>;
37
+ lockedSettings: string[];
38
+ lists: Record<string, string[]>;
39
+ requiredCapabilities: string[];
40
+ optionalCapabilities: string[];
41
+ /** 어떤 계층이 어떤 순서로 합쳐졌는지 — 재현성 기록(B-10 profile.lock)의 입력이다. */
42
+ layers: string[];
43
+ };
44
+ export type PolicyViolation = {
45
+ layer: string;
46
+ kind: 'HARD_DENY_ESCAPE' | 'SOFT_DENY_ESCAPE' | 'SCOPE_ESCALATION' | 'INVALID_SCOPE' | 'LOCKED_SETTING';
47
+ detail: string;
48
+ };
49
+ /** scalar — 잠기지 않은 키만 하위 값이 이긴다. */
50
+ export declare function mergeScalar<T>(upper: T | undefined, lower: T | undefined, locked: boolean): T | undefined;
51
+ /** replace-list — 하위가 있으면 통째로 교체. */
52
+ export declare function mergeReplaceList(upper?: readonly string[], lower?: readonly string[]): string[];
53
+ /** union-list — 합치고 중복 제거. 순서는 상위 먼저. */
54
+ export declare function mergeUnionList(upper?: readonly string[], lower?: readonly string[]): string[];
55
+ /**
56
+ * permission scope — lower-wins가 아니라 교집합이다.
57
+ * 하위가 상위 범위를 벗어나면 조용히 좁히지 않고 위반으로 보고한다. 조용한 clamp는
58
+ * 계약서에 적힌 범위와 실제 권한이 다른 상태를 만든다 (OM §4.7).
59
+ * 문법 밖 패턴은 판정할 수 없으므로 통과시키지 않고 따로 보고한다.
60
+ */
61
+ export declare function intersectScopes(upper: readonly string[] | undefined, lower: readonly string[] | undefined): {
62
+ scopes: string[];
63
+ escalations: string[];
64
+ invalid: string[];
65
+ };
66
+ /**
67
+ * 상위 → 하위 순으로 받은 계층들을 하나의 정책으로 합친다.
68
+ * 위반(HARD DENY 해제 시도·범위 확장·잠긴 설정 덮어쓰기)은 예외가 아니라 목록으로
69
+ * 돌려준다 — Resolver가 전부 모아 한 번에 보고할 수 있어야 하기 때문이다.
70
+ */
71
+ export declare function mergePolicyLayers(layers: readonly PolicyLayer[]): {
72
+ policy: ResolvedPolicy;
73
+ violations: PolicyViolation[];
74
+ };
75
+ export type PolicyQuery = {
76
+ /** 'external.write' · 'dependency.add' 같은 행위 키. */
77
+ action: string;
78
+ /** 파일을 쓰는 행위면 대상 경로. */
79
+ path?: string;
80
+ role?: RoleName;
81
+ /** Session Contract가 실제로 허용받은 쓰기 범위 (Profile 범위보다 좁아야 한다). */
82
+ writeBoundary?: readonly string[];
83
+ /**
84
+ * Controller가 이 Session에 한해 허용한 SOFT DENY 항목 (OM §5.1).
85
+ * Execution Grant와는 다른 것이다 — 이건 기존 Session 안의 제한적 허용일 뿐이다.
86
+ */
87
+ policyExceptions?: readonly string[];
88
+ };
89
+ export type PolicyDecision = {
90
+ verdict: Verdict;
91
+ reason: string;
92
+ };
93
+ export declare function evaluate(policy: ResolvedPolicy, query: PolicyQuery): PolicyDecision;
94
+ /**
95
+ * DENY에 닿았을 때 세션이 무엇을 해야 하는지 (OM §5.3).
96
+ * 세션 전체를 즉시 중단하는 것이 기본이 아니다 — Goal 달성 가능 여부로 갈린다.
97
+ */
98
+ export type DenyResponse = 'CONTINUE' | 'DEFER_AND_CONTINUE' | 'RECORD_UNRESOLVED_AND_CONTINUE' | 'CHECKPOINT_AND_RETURN';
99
+ export declare function denyResponse(verdict: Verdict, blocksGoal: boolean): DenyResponse;
@@ -0,0 +1,147 @@
1
+ // Policy Layer — 무엇이 자율이고 무엇이 금지인지, 그리고 계층을 합칠 때의 규칙.
2
+ // 정본: OM §5(3등급·상속·DENY 접촉 시 동작), §4.7(merge semantics).
3
+ //
4
+ // 핵심 불변식 둘:
5
+ // - 하위 계층은 상위보다 넓은 권한을 가질 수 없다. 권한 범위는 lower-wins가 아니라 교집합이다.
6
+ // - HARD DENY는 하위가 해제할 수 없다. Policy Exception으로도, Session Prompt로도 안 된다.
7
+ import { isWithinScopes, parseScope, pathInScope } from "./scope.js";
8
+ // ── merge semantics (OM §4.7) ───────────────────────────────────────────────
9
+ /** scalar — 잠기지 않은 키만 하위 값이 이긴다. */
10
+ export function mergeScalar(upper, lower, locked) {
11
+ if (locked)
12
+ return upper ?? lower;
13
+ return lower ?? upper;
14
+ }
15
+ /** replace-list — 하위가 있으면 통째로 교체. */
16
+ export function mergeReplaceList(upper = [], lower) {
17
+ return [...(lower ?? upper)];
18
+ }
19
+ /** union-list — 합치고 중복 제거. 순서는 상위 먼저. */
20
+ export function mergeUnionList(upper = [], lower = []) {
21
+ return [...new Set([...upper, ...lower])];
22
+ }
23
+ /**
24
+ * permission scope — lower-wins가 아니라 교집합이다.
25
+ * 하위가 상위 범위를 벗어나면 조용히 좁히지 않고 위반으로 보고한다. 조용한 clamp는
26
+ * 계약서에 적힌 범위와 실제 권한이 다른 상태를 만든다 (OM §4.7).
27
+ * 문법 밖 패턴은 판정할 수 없으므로 통과시키지 않고 따로 보고한다.
28
+ */
29
+ export function intersectScopes(upper, lower) {
30
+ const invalid = [...(lower ?? []), ...(upper ?? [])].filter((pattern) => parseScope(pattern) === null);
31
+ if (!lower)
32
+ return { scopes: [...(upper ?? [])], escalations: [], invalid };
33
+ if (!upper)
34
+ return { scopes: [...lower], escalations: [], invalid };
35
+ const valid = lower.filter((pattern) => parseScope(pattern) !== null);
36
+ const escalations = valid.filter((pattern) => !isWithinScopes(pattern, upper));
37
+ return { scopes: valid.filter((pattern) => isWithinScopes(pattern, upper)), escalations, invalid };
38
+ }
39
+ // ── 계층 병합 ───────────────────────────────────────────────────────────────
40
+ /**
41
+ * 상위 → 하위 순으로 받은 계층들을 하나의 정책으로 합친다.
42
+ * 위반(HARD DENY 해제 시도·범위 확장·잠긴 설정 덮어쓰기)은 예외가 아니라 목록으로
43
+ * 돌려준다 — Resolver가 전부 모아 한 번에 보고할 수 있어야 하기 때문이다.
44
+ */
45
+ export function mergePolicyLayers(layers) {
46
+ const violations = [];
47
+ const policy = {
48
+ hardDeny: [],
49
+ softDeny: [],
50
+ roleScopes: {},
51
+ settings: {},
52
+ lockedSettings: [],
53
+ lists: {},
54
+ requiredCapabilities: [],
55
+ optionalCapabilities: [],
56
+ layers: [],
57
+ };
58
+ for (const layer of layers) {
59
+ policy.layers.push(layer.id);
60
+ // 금지는 union이며 제거 연산이 없다. 상속된 금지를 allow로 뚫으려는 시도는 위반이다.
61
+ // SOFT DENY도 마찬가지다 — 그 해제는 Controller가 특정 Session에 주는 Policy
62
+ // Exception으로만 일어나고, 구성 계층이 미리 풀어둘 수 있는 것이 아니다 (OM §5.1).
63
+ for (const item of layer.allow ?? []) {
64
+ if (policy.hardDeny.includes(item)) {
65
+ violations.push({
66
+ layer: layer.id,
67
+ kind: 'HARD_DENY_ESCAPE',
68
+ detail: `'${item}' is HARD DENY and cannot be re-allowed by a lower layer`,
69
+ });
70
+ }
71
+ else if (policy.softDeny.includes(item)) {
72
+ violations.push({
73
+ layer: layer.id,
74
+ kind: 'SOFT_DENY_ESCAPE',
75
+ detail: `'${item}' is SOFT DENY — only a controller policy exception can permit it`,
76
+ });
77
+ }
78
+ }
79
+ policy.hardDeny = mergeUnionList(policy.hardDeny, layer.hardDeny);
80
+ // 상위가 금지하지 않은 항목이라도 하위가 조일 수 있다 — 강화 방향은 언제나 열려 있다.
81
+ policy.softDeny = mergeUnionList(policy.softDeny, layer.softDeny).filter((i) => !policy.hardDeny.includes(i));
82
+ for (const [role, requested] of Object.entries(layer.roleScopes ?? {})) {
83
+ const { scopes, escalations, invalid } = intersectScopes(policy.roleScopes[role], requested);
84
+ if (escalations.length > 0) {
85
+ violations.push({
86
+ layer: layer.id,
87
+ kind: 'SCOPE_ESCALATION',
88
+ detail: `${role} requested ${escalations.join(', ')} outside the inherited scope`,
89
+ });
90
+ }
91
+ if (invalid.length > 0) {
92
+ violations.push({
93
+ layer: layer.id,
94
+ kind: 'INVALID_SCOPE',
95
+ detail: `${role} scope ${invalid.join(', ')} is not valid ASC scope grammar`,
96
+ });
97
+ }
98
+ policy.roleScopes[role] = scopes;
99
+ }
100
+ for (const [key, value] of Object.entries(layer.settings ?? {})) {
101
+ const locked = policy.lockedSettings.includes(key);
102
+ if (locked && policy.settings[key] !== value) {
103
+ violations.push({ layer: layer.id, kind: 'LOCKED_SETTING', detail: `'${key}' is locked by an upper layer` });
104
+ }
105
+ const merged = mergeScalar(policy.settings[key], value, locked);
106
+ if (merged !== undefined)
107
+ policy.settings[key] = merged;
108
+ }
109
+ policy.lockedSettings = mergeUnionList(policy.lockedSettings, layer.lockedSettings);
110
+ for (const [key, values] of Object.entries(layer.replaceLists ?? {})) {
111
+ policy.lists[key] = mergeReplaceList(policy.lists[key], values);
112
+ }
113
+ for (const [key, values] of Object.entries(layer.unionLists ?? {})) {
114
+ policy.lists[key] = mergeUnionList(policy.lists[key], values);
115
+ }
116
+ policy.requiredCapabilities = mergeUnionList(policy.requiredCapabilities, layer.requiredCapabilities);
117
+ policy.optionalCapabilities = mergeUnionList(policy.optionalCapabilities, layer.optionalCapabilities);
118
+ }
119
+ return { policy, violations };
120
+ }
121
+ export function evaluate(policy, query) {
122
+ if (policy.hardDeny.includes(query.action)) {
123
+ // Policy Exception이 있어도 뚫리지 않는다. 외부 write 같은 항목은 별도 Execution
124
+ // Grant라는 다른 계약으로만 수행된다 (OM §5.2).
125
+ return { verdict: 'HARD_DENY', reason: `'${query.action}' is HARD DENY` };
126
+ }
127
+ if (query.path !== undefined) {
128
+ const boundary = query.writeBoundary ?? policy.roleScopes[query.role ?? 'implementer'] ?? [];
129
+ if (!pathInScope(query.path, boundary)) {
130
+ return { verdict: 'HARD_DENY', reason: `'${query.path}' is outside the write boundary` };
131
+ }
132
+ }
133
+ if (policy.softDeny.includes(query.action)) {
134
+ if (query.policyExceptions?.includes(query.action)) {
135
+ return { verdict: 'ALLOW', reason: `'${query.action}' allowed by policy exception` };
136
+ }
137
+ return { verdict: 'SOFT_DENY', reason: `'${query.action}' requires a policy exception` };
138
+ }
139
+ return { verdict: 'ALLOW', reason: 'within contract' };
140
+ }
141
+ export function denyResponse(verdict, blocksGoal) {
142
+ if (verdict === 'ALLOW')
143
+ return 'CONTINUE';
144
+ if (verdict === 'HARD_DENY')
145
+ return blocksGoal ? 'CHECKPOINT_AND_RETURN' : 'RECORD_UNRESOLVED_AND_CONTINUE';
146
+ return blocksGoal ? 'CHECKPOINT_AND_RETURN' : 'DEFER_AND_CONTINUE';
147
+ }