@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,98 @@
1
+ import type { Session } from '../model/entities.ts';
2
+ import type { StateStore } from '../../ports/state-store.ts';
3
+ import type { CanonicalDrift, SessionRuntime } from '../runtime/session.ts';
4
+ import { type EscalationLedger } from '../runtime/escalation.ts';
5
+ import { type ExecutionVerdict } from '../runtime/execution-state.ts';
6
+ /** proceed 진입 전 설정 검증 결과. 실 구현은 bootstrapGuard를 감싼다 — 새 판단 금지. */
7
+ export type ConfigCheck = {
8
+ ok: true;
9
+ } | {
10
+ ok: false;
11
+ detail: string;
12
+ };
13
+ export type ProceedIntent = {
14
+ /** 사용자가 특정 세션을 지목했다. 자동 탐색과 semantics가 다르다 (C-03 §1.5). */
15
+ sessionId?: string;
16
+ /** 후보가 없을 때 초안에 실어 줄 목표 힌트. 확정이 아니다. */
17
+ goal?: string;
18
+ };
19
+ /** 후보 나열용 요약 — 사람이 고를 근거까지 함께 준다. */
20
+ export type SessionCandidate = {
21
+ id: string;
22
+ status: Session['status'];
23
+ role: Session['role'];
24
+ goal: string;
25
+ /** 이 세션을 고르면 무슨 일이 일어나는지. */
26
+ wouldDo: 'start' | 'resume' | 'continue';
27
+ };
28
+ /** 실행 가능 outcome이 Host에 넘겨주는 것 — 계약과 이어받을 지점. */
29
+ type Handout = {
30
+ contract: Session;
31
+ doneCriteria: string[];
32
+ checkpoint?: Session['checkpoint'];
33
+ /** 지금 무엇이 막혔고 무엇이 가는가 (C-13 §6). 상신 원장이 없으면 없다. */
34
+ gate?: ExecutionVerdict;
35
+ /** 열려 있는 상신 id. 진행 화면이 "판단 필요 없음"이라고 말하지 않게 하는 근거다. */
36
+ awaiting?: string[];
37
+ };
38
+ export type ProceedOutcome = ({
39
+ kind: 'STARTED';
40
+ } & Handout) | ({
41
+ kind: 'RESUMED';
42
+ } & Handout) | ({
43
+ kind: 'CONTINUE_ACTIVE';
44
+ } & Handout) | {
45
+ kind: 'NEEDS_SELECTION';
46
+ candidates: SessionCandidate[];
47
+ } | {
48
+ kind: 'PROPOSE_CONTRACT';
49
+ /** 초안일 뿐이다 — issue는 Controller 승인 후 별도 행위 (C-03 §1.3). */
50
+ draft: {
51
+ role: Session['role'];
52
+ goal: string;
53
+ doneCriteria: string[];
54
+ };
55
+ }
56
+ /**
57
+ * 미해소 상신이 실행 가능한 node를 전부 덮었다 (C-13 §6).
58
+ *
59
+ * BLOCKED와 다르다: 세션은 PAUSED 그대로이고, 경계가 풀리면 그대로 이어진다.
60
+ * **전이를 일으키지 않는다** — 사람이 결정할 때까지 기다리는 것이지 실패가 아니다.
61
+ */
62
+ | {
63
+ kind: 'HELD';
64
+ detail: string;
65
+ verdict: ExecutionVerdict;
66
+ escalations: string[];
67
+ } | {
68
+ kind: 'BLOCKED_CONFIG';
69
+ detail: string;
70
+ } | {
71
+ kind: 'BLOCKED_CANONICAL';
72
+ detail: string;
73
+ drifts?: CanonicalDrift[];
74
+ } | {
75
+ kind: 'FAILED';
76
+ reason: 'NOT_FOUND' | 'SESSION_BLOCKED' | 'NOT_RUNNABLE' | 'TRANSITION';
77
+ detail: string;
78
+ };
79
+ export type OperatorDeps = {
80
+ store: StateStore;
81
+ sessions: SessionRuntime;
82
+ /**
83
+ * 상신 원장 (C-13). 주면 proceed가 **막힌 node만** 보고 판단한다 —
84
+ * 주지 않으면 예전처럼 상태만 보고 간다(기존 호출자 무손상).
85
+ */
86
+ escalations?: EscalationLedger;
87
+ /**
88
+ * 필수다. 모든 진입이 bootstrap/profile.lock 검증을 지난다 — Surface가 어디든.
89
+ * 실 조립은 factory(cli의 createOperator)가 bootstrapGuard로 고정한다.
90
+ */
91
+ guard: () => Promise<ConfigCheck>;
92
+ };
93
+ export declare class Operator {
94
+ #private;
95
+ constructor(deps: OperatorDeps);
96
+ proceed(intent?: ProceedIntent): Promise<ProceedOutcome>;
97
+ }
98
+ export {};
@@ -0,0 +1,167 @@
1
+ // Generic Operator — "ASC로 진행해"의 provider-neutral 구현 (C-03 §1).
2
+ //
3
+ // 사람이 CLI 명령을 조립하던 절차를 하나의 진입으로 수렴한다. 판단은 전부 기존 것을
4
+ // 쓴다: 상태 전이는 SessionRuntime, 설정 검증은 bootstrap guard, 정본 판정은
5
+ // checkCanonical. 이 파일에 새 판단 로직이 생기면 그건 Core를 우회하는 두 번째 길이다.
6
+ //
7
+ // guard는 optional이 아니다. CLI가 아닌 Host Adapter가 직접 불러도 profile.lock 검증을
8
+ // 건너뛸 수 없어야 하며(C-03 §1.2), 정본 조립은 Surface가 아니라 factory가 진다.
9
+ import { proceedGateFacts } from "../runtime/escalation.js";
10
+ import { deriveExecutionState } from "../runtime/execution-state.js";
11
+ const RUNNABLE = new Set(['READY', 'PAUSED', 'ACTIVE']);
12
+ export class Operator {
13
+ #store;
14
+ #sessions;
15
+ #escalations;
16
+ #guard;
17
+ constructor(deps) {
18
+ this.#store = deps.store;
19
+ this.#sessions = deps.sessions;
20
+ this.#escalations = deps.escalations;
21
+ this.#guard = deps.guard;
22
+ }
23
+ async proceed(intent = {}) {
24
+ const config = await this.#guard();
25
+ if (!config.ok)
26
+ return { kind: 'BLOCKED_CONFIG', detail: config.detail };
27
+ // 명시 지정 — 사용자가 가리킨 그 세션만 본다. 다른 것을 권하지 않는다 (C-03 §1.5).
28
+ if (intent.sessionId) {
29
+ const session = await this.#store.get('session', intent.sessionId);
30
+ if (!session) {
31
+ return { kind: 'FAILED', reason: 'NOT_FOUND', detail: `세션 '${intent.sessionId}' 을 찾지 못했다` };
32
+ }
33
+ return this.#advance(session);
34
+ }
35
+ // 자동 탐색 — 실행 가능한 것만 후보다. archive는 collect가 이미 치웠다.
36
+ const candidates = (await this.#store.list('session')).filter((s) => RUNNABLE.has(s.status));
37
+ if (candidates.length === 0) {
38
+ // 자동 issue 금지. 초안을 제안할 수는 있으나 발급은 Controller 승인 후 별도 행위다.
39
+ return {
40
+ kind: 'PROPOSE_CONTRACT',
41
+ draft: { role: 'implementer', goal: intent.goal ?? '', doneCriteria: [] },
42
+ };
43
+ }
44
+ if (candidates.length > 1) {
45
+ // 임의 선택 금지 — 잘못 고른 세션 위에서 작업이 시작되는 것보다 한 번 묻는 게 싸다.
46
+ return {
47
+ kind: 'NEEDS_SELECTION',
48
+ candidates: candidates.map((s) => ({
49
+ id: s.id,
50
+ status: s.status,
51
+ role: s.role,
52
+ goal: s.goal,
53
+ wouldDo: s.status === 'READY' ? 'start' : s.status === 'PAUSED' ? 'resume' : 'continue',
54
+ })),
55
+ };
56
+ }
57
+ return this.#advance(candidates[0]);
58
+ }
59
+ /** 상태별로 한 걸음. 전이는 전부 SessionRuntime을 지난다 — 우회 경로 없음. */
60
+ async #advance(session) {
61
+ switch (session.status) {
62
+ case 'READY':
63
+ return this.#mapStart(await this.#sessions.start(session.id), 'STARTED', session.id);
64
+ case 'PAUSED': {
65
+ // **checkpoint를 발행했다는 이유로 멈추지 않는다** (C-13 불변식 ④).
66
+ // 멈추는 근거는 미해소 상신뿐이고, 그것도 막힌 node에 한한다.
67
+ const gate = await this.#gate(session);
68
+ if (gate && gate.runnable.length === 0 && gate.escalations.length > 0) {
69
+ return {
70
+ kind: 'HELD',
71
+ detail: `실행 가능한 항목이 없다 — 상신 ${gate.escalations.length}건이 전부를 덮었다`,
72
+ verdict: gate.verdict,
73
+ escalations: gate.escalations,
74
+ };
75
+ }
76
+ const resumed = await this.#mapStart(await this.#sessions.resume(session.id), 'RESUMED', session.id);
77
+ return gate && 'contract' in resumed
78
+ ? { ...resumed, gate: gate.verdict, ...(gate.escalations.length > 0 ? { awaiting: gate.escalations } : {}) }
79
+ : resumed;
80
+ }
81
+ case 'ACTIVE': {
82
+ // 이미 검증됐다고 가정하지 않는다 — 이어가기 전에도 정본을 다시 본다 (C-03 §1.6).
83
+ const canonical = await this.#sessions.checkCanonical(session.id);
84
+ if (canonical.status === 'DRIFT') {
85
+ return { kind: 'BLOCKED_CANONICAL', detail: '정본이 발급 시점과 다르다', drifts: canonical.drifts };
86
+ }
87
+ if (canonical.status === 'UNAVAILABLE') {
88
+ return { kind: 'BLOCKED_CANONICAL', detail: canonical.detail };
89
+ }
90
+ if (canonical.status === 'NOT_FOUND') {
91
+ return { kind: 'FAILED', reason: 'NOT_FOUND', detail: `세션 '${session.id}' 을 찾지 못했다` };
92
+ }
93
+ // 돌고 있는 세션도 같은 판정을 받는다. PAUSED에만 걸면 "한 번 재개된 뒤에는
94
+ // 전부 막혀도 계속 간다고 말하는" 구멍이 생긴다 (실제로 그랬다).
95
+ const gate = await this.#gate(session);
96
+ if (gate && gate.runnable.length === 0 && gate.escalations.length > 0) {
97
+ return {
98
+ kind: 'HELD',
99
+ detail: `실행 가능한 항목이 없다 — 상신 ${gate.escalations.length}건이 전부를 덮었다`,
100
+ verdict: gate.verdict,
101
+ escalations: gate.escalations,
102
+ };
103
+ }
104
+ return {
105
+ kind: 'CONTINUE_ACTIVE',
106
+ ...handout(session),
107
+ ...(gate ? { gate: gate.verdict } : {}),
108
+ ...(gate && gate.escalations.length > 0 ? { awaiting: gate.escalations } : {}),
109
+ };
110
+ }
111
+ case 'BLOCKED':
112
+ return {
113
+ kind: 'FAILED',
114
+ reason: 'SESSION_BLOCKED',
115
+ detail: `${session.id} 는 BLOCKED — Controller가 해소해야 진행할 수 있다`,
116
+ };
117
+ case 'DONE':
118
+ case 'FAILED':
119
+ return {
120
+ kind: 'FAILED',
121
+ reason: 'NOT_RUNNABLE',
122
+ detail: `${session.id} 는 ${session.status} — 이어갈 수 없는 상태다`,
123
+ };
124
+ }
125
+ }
126
+ /**
127
+ * 이 세션이 지금 어디까지 갈 수 있는가. 원장이 없으면 판단하지 않는다 —
128
+ * 없는 근거로 세우지 않는다.
129
+ */
130
+ async #gate(session) {
131
+ if (!this.#escalations)
132
+ return null;
133
+ const pending = (await this.#escalations.pending()).filter((record) => record.sessionId === session.id);
134
+ const facts = proceedGateFacts(pending, session.doneCriteria);
135
+ return {
136
+ verdict: deriveExecutionState({
137
+ doneCriteria: session.doneCriteria,
138
+ ...(facts.waitingOn.length > 0 ? { waitingOn: facts.waitingOn } : {}),
139
+ ...(facts.conditions.length > 0 ? { conditions: facts.conditions } : {}),
140
+ }),
141
+ runnable: facts.runnable,
142
+ escalations: pending.map((record) => record.escalationId),
143
+ };
144
+ }
145
+ async #mapStart(outcome, kind, id) {
146
+ if (outcome.ok)
147
+ return { kind, ...handout(outcome.entity) };
148
+ if (outcome.reason === 'CANONICAL_DRIFT') {
149
+ return { kind: 'BLOCKED_CANONICAL', detail: '정본이 발급 시점과 다르다', drifts: outcome.drifts };
150
+ }
151
+ if (outcome.reason === 'CANONICAL_UNAVAILABLE') {
152
+ return { kind: 'BLOCKED_CANONICAL', detail: outcome.detail };
153
+ }
154
+ if (outcome.reason === 'NOT_FOUND') {
155
+ return { kind: 'FAILED', reason: 'NOT_FOUND', detail: `세션 '${id}' 을 찾지 못했다` };
156
+ }
157
+ const detail = outcome.reason === 'REJECTED' ? outcome.failure.message : `전이 실패: ${outcome.reason}`;
158
+ return { kind: 'FAILED', reason: 'TRANSITION', detail };
159
+ }
160
+ }
161
+ function handout(session) {
162
+ return {
163
+ contract: session,
164
+ doneCriteria: session.doneCriteria,
165
+ ...(session.checkpoint ? { checkpoint: session.checkpoint } : {}),
166
+ };
167
+ }
@@ -0,0 +1,124 @@
1
+ import { z } from 'zod';
2
+ import type { ScopedStore } from '../../ports/state-store.ts';
3
+ import type { RuntimeBindings } from './runtime-binding.ts';
4
+ /**
5
+ * 사용자 판단이 지금 필요한가. 단순 BLOCKED 여부보다 이게 사람에게 더 중요하다 —
6
+ * "내가 지금 뭘 해야 하지"에 바로 답한다.
7
+ */
8
+ export declare const DecisionNeed: z.ZodEnum<["NONE", "LATER", "NOW"]>;
9
+ export type DecisionNeed = z.infer<typeof DecisionNeed>;
10
+ /** 독립 검증의 관측 상태. 자기 보고(Handoff.verified)와 다른 축이다. */
11
+ export declare const VerifierState: z.ZodEnum<["NONE", "RUNNING", "PASS", "FAIL"]>;
12
+ export type VerifierState = z.infer<typeof VerifierState>;
13
+ export declare const ProgressReport: z.ZodObject<{
14
+ logicalSessionId: z.ZodString;
15
+ /** 지금 무엇을 하는 중인가 — 한 줄. */
16
+ phase: z.ZodString;
17
+ /** 무엇이 끝났는가. 파일 하나·테스트 하나가 아니라 의미 있는 묶음만. */
18
+ milestones: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
19
+ /** 다음에는 무엇을 하는가. */
20
+ nextStep: z.ZodOptional<z.ZodString>;
21
+ /** 확인이 필요한 항목 — 작업을 막지는 않는 것들. */
22
+ unresolved: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
23
+ needsUserDecision: z.ZodDefault<z.ZodEnum<["NONE", "LATER", "NOW"]>>;
24
+ /**
25
+ * NOW일 때 사람이 어디서 결정하는지. 판단 요청의 정본은 계속 ApprovalRequest/Inbox다 —
26
+ * Progress가 두 번째 결정 창구가 되면 control plane이 갈라진다.
27
+ */
28
+ decisionRef: z.ZodOptional<z.ZodString>;
29
+ verifier: z.ZodDefault<z.ZodEnum<["NONE", "RUNNING", "PASS", "FAIL"]>>;
30
+ verifierDetail: z.ZodOptional<z.ZodString>;
31
+ /**
32
+ * 종결 후 보존되는 최종 보고. DONE 즉시 삭제하면 "무엇을 마쳤나"를 볼 수단이 사라진다 —
33
+ * live projection 정리는 collect가 하고, terminal view는 남긴다.
34
+ */
35
+ terminal: z.ZodDefault<z.ZodBoolean>;
36
+ /** 기록한 Physical Session. 승계 후 옛 Host가 덮어쓰는 것을 막는 근거다. */
37
+ recordedBy: z.ZodString;
38
+ lastUpdatedAt: z.ZodString;
39
+ }, "strip", z.ZodTypeAny, {
40
+ verifier: "NONE" | "RUNNING" | "PASS" | "FAIL";
41
+ unresolved: string[];
42
+ logicalSessionId: string;
43
+ phase: string;
44
+ milestones: string[];
45
+ needsUserDecision: "NONE" | "LATER" | "NOW";
46
+ terminal: boolean;
47
+ recordedBy: string;
48
+ lastUpdatedAt: string;
49
+ nextStep?: string | undefined;
50
+ decisionRef?: string | undefined;
51
+ verifierDetail?: string | undefined;
52
+ }, {
53
+ logicalSessionId: string;
54
+ phase: string;
55
+ recordedBy: string;
56
+ lastUpdatedAt: string;
57
+ verifier?: "NONE" | "RUNNING" | "PASS" | "FAIL" | undefined;
58
+ unresolved?: string[] | undefined;
59
+ milestones?: string[] | undefined;
60
+ nextStep?: string | undefined;
61
+ needsUserDecision?: "NONE" | "LATER" | "NOW" | undefined;
62
+ decisionRef?: string | undefined;
63
+ verifierDetail?: string | undefined;
64
+ terminal?: boolean | undefined;
65
+ }>;
66
+ export type ProgressReport = z.infer<typeof ProgressReport>;
67
+ /** 기록 입력 — 저장 책임 필드(recordedBy·lastUpdatedAt)는 서비스가 채운다. */
68
+ export type ProgressInput = {
69
+ phase: string;
70
+ milestones?: string[];
71
+ nextStep?: string;
72
+ unresolved?: string[];
73
+ needsUserDecision?: DecisionNeed;
74
+ decisionRef?: string;
75
+ verifier?: VerifierState;
76
+ verifierDetail?: string;
77
+ terminal?: boolean;
78
+ };
79
+ export type ReportOutcome = {
80
+ ok: true;
81
+ report: ProgressReport;
82
+ } | {
83
+ ok: false;
84
+ reason: 'NOT_OWNER';
85
+ detail: string;
86
+ } | {
87
+ ok: false;
88
+ reason: 'INVALID';
89
+ detail: string;
90
+ };
91
+ export type ProgressDeps = {
92
+ /** Adapter가 넘기는 격리 저장소. Core entity를 건드리지 않는 유일한 이유다. */
93
+ scope: ScopedStore;
94
+ /**
95
+ * 기록 권한 판정용. owner인 Physical Session만 쓴다 — RuntimeBinding이 이미
96
+ * 단일 소유권을 정해 뒀으므로 여기서 두 번째 권한 모델을 만들지 않는다.
97
+ */
98
+ bindings: RuntimeBindings;
99
+ now?: () => string;
100
+ /** History 기록. 없으면 남기지 않는다(테스트·비-store 환경). */
101
+ appendHistory?: (entry: {
102
+ at: string;
103
+ actor: string;
104
+ kind: string;
105
+ ref: string;
106
+ detail?: string;
107
+ }) => Promise<void>;
108
+ };
109
+ export declare class ProgressService {
110
+ #private;
111
+ constructor(deps: ProgressDeps);
112
+ /**
113
+ * 진행 보고를 기록한다. Session entity는 읽지도 쓰지도 않는다 —
114
+ * canonical state와 무관함이 호출 경로에서부터 보장돼야 한다.
115
+ */
116
+ report(logicalSessionId: string, physicalSessionId: string, input: ProgressInput): Promise<ReportOutcome>;
117
+ get(logicalSessionId: string): Promise<ProgressReport | null>;
118
+ /**
119
+ * collect 시점의 live projection 정리. terminal 보고는 남긴다 — 완료 화면이
120
+ * 사라지면 "무엇을 마쳤나"를 볼 수단이 없어진다.
121
+ * @returns 실제로 지운 세션 id
122
+ */
123
+ collect(closedSessionIds: readonly string[]): Promise<string[]>;
124
+ }
@@ -0,0 +1,135 @@
1
+ // Semantic Progress — 작업 "중" 가시성의 정본 (B-17, post-b16-candidates P1).
2
+ //
3
+ // 왜 별도 구조인가: 사람이 알고 싶은 것은 "내 작업이 지금 어떻게 되고 있는가"인데,
4
+ // canonical Session state는 READY/ACTIVE/PAUSED/DONE 넷뿐이라 23분 동안 ACTIVE 하나로
5
+ // 침묵한다(B-16 friction ④ — 사용자가 중간보고를 직접 요청했다).
6
+ //
7
+ // 불변조건 (이 파일의 존재 이유):
8
+ // Runtime Progress ≠ Canonical Session State.
9
+ // Agent가 "80% 완료"라고 보고해도 Session.status는 변하지 않는다. 그래서 이 모듈은
10
+ // SessionRuntime을 import하지 않는다 — 전이 경로가 아예 없어야 실수로도 못 바꾼다.
11
+ // 저장도 EntityMap 밖(ScopedStore)이라 Session 파일에 손이 닿지 않는다.
12
+ //
13
+ // Checkpoint와의 관계: Checkpoint는 Physical Run 승계 정보(PAUSED의 필수 요건)이고
14
+ // Progress는 표시용 projection이다. 병합하지 않으며, Checkpoint lifecycle도 건드리지
15
+ // 않는다 — ACTIVE 세션에 남아 있는 옛 checkpoint를 현재 진행으로 읽지 않는 것은
16
+ // Renderer의 규칙이다 (render.ts).
17
+ import { z } from 'zod';
18
+ /**
19
+ * 사용자 판단이 지금 필요한가. 단순 BLOCKED 여부보다 이게 사람에게 더 중요하다 —
20
+ * "내가 지금 뭘 해야 하지"에 바로 답한다.
21
+ */
22
+ export const DecisionNeed = z.enum([
23
+ 'NONE', // 계속 진행 가능
24
+ 'LATER', // 미확정 항목 있음 — 완료 시 함께 판단
25
+ 'NOW', // 여기서 멈췄다 — 판단 없이 다음 단계 불가
26
+ ]);
27
+ /** 독립 검증의 관측 상태. 자기 보고(Handoff.verified)와 다른 축이다. */
28
+ export const VerifierState = z.enum(['NONE', 'RUNNING', 'PASS', 'FAIL']);
29
+ export const ProgressReport = z.object({
30
+ logicalSessionId: z.string().min(1),
31
+ /** 지금 무엇을 하는 중인가 — 한 줄. */
32
+ phase: z.string().min(1),
33
+ /** 무엇이 끝났는가. 파일 하나·테스트 하나가 아니라 의미 있는 묶음만. */
34
+ milestones: z.array(z.string()).default([]),
35
+ /** 다음에는 무엇을 하는가. */
36
+ nextStep: z.string().optional(),
37
+ /** 확인이 필요한 항목 — 작업을 막지는 않는 것들. */
38
+ unresolved: z.array(z.string()).default([]),
39
+ needsUserDecision: DecisionNeed.default('NONE'),
40
+ /**
41
+ * NOW일 때 사람이 어디서 결정하는지. 판단 요청의 정본은 계속 ApprovalRequest/Inbox다 —
42
+ * Progress가 두 번째 결정 창구가 되면 control plane이 갈라진다.
43
+ */
44
+ decisionRef: z.string().optional(),
45
+ verifier: VerifierState.default('NONE'),
46
+ verifierDetail: z.string().optional(),
47
+ /**
48
+ * 종결 후 보존되는 최종 보고. DONE 즉시 삭제하면 "무엇을 마쳤나"를 볼 수단이 사라진다 —
49
+ * live projection 정리는 collect가 하고, terminal view는 남긴다.
50
+ */
51
+ terminal: z.boolean().default(false),
52
+ /** 기록한 Physical Session. 승계 후 옛 Host가 덮어쓰는 것을 막는 근거다. */
53
+ recordedBy: z.string().min(1),
54
+ lastUpdatedAt: z.string().min(1),
55
+ });
56
+ const keyOf = (logicalSessionId) => `progress:${logicalSessionId}`;
57
+ export class ProgressService {
58
+ #scope;
59
+ #bindings;
60
+ #now;
61
+ #appendHistory;
62
+ constructor(deps) {
63
+ this.#scope = deps.scope;
64
+ this.#bindings = deps.bindings;
65
+ this.#now = deps.now ?? (() => new Date().toISOString());
66
+ if (deps.appendHistory)
67
+ this.#appendHistory = deps.appendHistory;
68
+ }
69
+ /**
70
+ * 진행 보고를 기록한다. Session entity는 읽지도 쓰지도 않는다 —
71
+ * canonical state와 무관함이 호출 경로에서부터 보장돼야 한다.
72
+ */
73
+ async report(logicalSessionId, physicalSessionId, input) {
74
+ const binding = await this.#bindings.get(logicalSessionId);
75
+ if (!binding) {
76
+ return {
77
+ ok: false,
78
+ reason: 'NOT_OWNER',
79
+ detail: `${logicalSessionId} 에 Runtime이 붙어 있지 않다 — 먼저 소유권을 주장하라`,
80
+ };
81
+ }
82
+ if (binding.physicalSessionId !== physicalSessionId) {
83
+ // 승계 후 죽지 않은 옛 Host가 계속 쓰면 표시가 오염된다. owner만 쓴다.
84
+ return {
85
+ ok: false,
86
+ reason: 'NOT_OWNER',
87
+ detail: `${logicalSessionId} 의 owner가 아니다 (현재 owner: ${binding.physicalSessionId})`,
88
+ };
89
+ }
90
+ const at = this.#now();
91
+ const parsed = ProgressReport.safeParse({
92
+ logicalSessionId,
93
+ ...input,
94
+ recordedBy: physicalSessionId,
95
+ lastUpdatedAt: at,
96
+ });
97
+ if (!parsed.success) {
98
+ return { ok: false, reason: 'INVALID', detail: parsed.error.issues.map((i) => i.message).join('; ') };
99
+ }
100
+ // 갱신은 last-write-wins다. 표시 전용이고 owner가 하나뿐이라 수용한다 —
101
+ // 원자성이 필요한 것(소유권·전이)은 각자 다른 곳에서 이미 지키고 있다.
102
+ await this.#scope.set(keyOf(logicalSessionId), JSON.stringify(parsed.data));
103
+ await this.#appendHistory?.({
104
+ at,
105
+ actor: physicalSessionId,
106
+ kind: 'session_progress',
107
+ ref: logicalSessionId,
108
+ detail: parsed.data.phase,
109
+ });
110
+ return { ok: true, report: parsed.data };
111
+ }
112
+ async get(logicalSessionId) {
113
+ const raw = await this.#scope.get(keyOf(logicalSessionId));
114
+ if (!raw)
115
+ return null;
116
+ const parsed = ProgressReport.safeParse(JSON.parse(raw));
117
+ return parsed.success ? parsed.data : null;
118
+ }
119
+ /**
120
+ * collect 시점의 live projection 정리. terminal 보고는 남긴다 — 완료 화면이
121
+ * 사라지면 "무엇을 마쳤나"를 볼 수단이 없어진다.
122
+ * @returns 실제로 지운 세션 id
123
+ */
124
+ async collect(closedSessionIds) {
125
+ const removed = [];
126
+ for (const id of closedSessionIds) {
127
+ const current = await this.get(id);
128
+ if (!current || current.terminal)
129
+ continue;
130
+ await this.#scope.delete(keyOf(id));
131
+ removed.push(id);
132
+ }
133
+ return removed;
134
+ }
135
+ }
@@ -0,0 +1,34 @@
1
+ import type { Session } from '../model/entities.ts';
2
+ import type { ProgressReport } from './progress.ts';
3
+ export type RenderInput = {
4
+ /**
5
+ * 종결·archive 후에는 세션 entity가 활성 목록에 없다. 그때도 "무엇을 마쳤나"는
6
+ * 보여야 하므로 없어도 렌더한다 — 최종 화면이 사라지면 완료를 확인할 수단이 없다.
7
+ */
8
+ session: Session | null;
9
+ progress: ProgressReport | null;
10
+ /**
11
+ * 관찰된 활동 신호. **진척이 아니다** — 도구가 한 번 돌았다는 사실까지다.
12
+ * 없거나 오래됐다고 "멈췄다"고 말하지 않는다: 파일만 고치는 구간, 생각하는 구간,
13
+ * 하위 작업을 기다리는 구간에는 애초에 신호가 없다 (B-18).
14
+ */
15
+ liveness?: {
16
+ lastActivityAt: string;
17
+ lastTool?: string;
18
+ };
19
+ /**
20
+ * 지금 사람의 결정을 기다리는 것들 (상신·요청 참조).
21
+ *
22
+ * `progress.needsUserDecision` 은 **일하는 쪽의 신고**다. 신고가 없거나 NONE이어도
23
+ * 열린 상신이 있으면 판단은 실제로 필요하다 — 그때 "판단 필요 없음"이라고 말하는
24
+ * 화면은 거짓말이다 (B-65 dogfood에서 잡힌 것). 여기 값이 있으면 그것이 이긴다.
25
+ */
26
+ awaiting?: readonly string[];
27
+ now?: Date;
28
+ };
29
+ /** 사람에게 보여줄 본문과, 그 아래 붙는 작은 상세 한 줄. */
30
+ export type RenderedProgress = {
31
+ body: string[];
32
+ detail: string;
33
+ };
34
+ export declare function renderProgress(input: RenderInput): RenderedProgress;