@asc-agent/runtime 0.6.1 → 0.7.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 (38) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/claude-code/guard.d.ts +38 -0
  3. package/dist/adapters/claude-code/guard.js +162 -30
  4. package/dist/adapters/claude-code/skill.js +29 -0
  5. package/dist/adapters/github/scm.d.ts +2 -0
  6. package/dist/adapters/github/scm.js +5 -1
  7. package/dist/adapters/gitlab/scm.d.ts +43 -0
  8. package/dist/adapters/gitlab/scm.js +201 -0
  9. package/dist/adapters/markdown/layout.js +0 -2
  10. package/dist/adapters/markdown/serialize.js +0 -7
  11. package/dist/adapters/memory/runtime-binding.d.ts +27 -2
  12. package/dist/adapters/memory/runtime-binding.js +72 -1
  13. package/dist/cli/asc.js +189 -20
  14. package/dist/composition/runtime.d.ts +2 -0
  15. package/dist/composition/runtime.js +10 -0
  16. package/dist/core/attach/init.js +13 -5
  17. package/dist/core/distribution/release.d.ts +3 -3
  18. package/dist/core/distribution/release.js +1 -1
  19. package/dist/core/execution/executor.js +6 -2
  20. package/dist/core/execution/grant.d.ts +44 -1
  21. package/dist/core/execution/grant.js +59 -0
  22. package/dist/core/model/entities.d.ts +53 -27
  23. package/dist/core/model/entities.js +21 -14
  24. package/dist/core/model/transitions.d.ts +1 -3
  25. package/dist/core/model/transitions.js +0 -13
  26. package/dist/core/operator/local-operator.js +9 -1
  27. package/dist/core/operator/proceed.js +13 -3
  28. package/dist/core/operator/work-state.js +52 -14
  29. package/dist/core/runtime/background.d.ts +33 -0
  30. package/dist/core/runtime/background.js +68 -2
  31. package/dist/core/runtime/controller.d.ts +9 -1
  32. package/dist/core/runtime/controller.js +8 -7
  33. package/dist/core/runtime/workspaces.d.ts +1 -0
  34. package/dist/core/runtime/workspaces.js +1 -1
  35. package/dist/ports/scm.d.ts +8 -0
  36. package/dist/ports/state-store.d.ts +1 -2
  37. package/dist/ports/state-store.js +0 -1
  38. package/package.json +1 -1
@@ -16,6 +16,18 @@ import { dirname } from 'node:path';
16
16
  export const RUNTIME_LEASE_KEY = 'runtime-lease';
17
17
  /** 마지막 회차 시각이 사는 열쇠. Orchestrator가 쓰고 status가 읽는다. */
18
18
  export const LAST_RUN_KEY = 'last-run';
19
+ /**
20
+ * 마지막 기계 회차가 이 workspace 에서 **어떻게 끝났는가** (0.7.0 / Phase J).
21
+ *
22
+ * 실측 하나가 이 열쇠의 출처다. 이 기계의 workspace 세 개 중 둘이 여러 릴리스 동안
23
+ * 회차를 한 번도 통과하지 못했는데, 화면 어디에도 그 말이 없었다 — 붙어 있고, 등록물도
24
+ * 서 있고, 그래서 건강해 보였다. 실패는 `service.log` 에만 흘러갔고 아무도 그것을 읽지
25
+ * 않는다.
26
+ *
27
+ * 새 상태 저장소가 아니다. 이미 회차가 쓰고 status 가 읽는 같은 scope 의 기록 하나이며,
28
+ * 판정도 하지 않는다 — 무슨 일이 있었는지만 적는다.
29
+ */
30
+ export const LAST_PASS_KEY = 'last-pass';
19
31
  /**
20
32
  * lease가 죽은 것으로 보이기까지의 최소 시간.
21
33
  *
@@ -125,6 +137,40 @@ export class RuntimeLease {
125
137
  await this.#scope.delete(RUNTIME_LEASE_KEY);
126
138
  }
127
139
  }
140
+ /**
141
+ * 회차 결말을 적는다. **판정하지 않는다** — 코드와 연속 실패 수를 세어 둘 뿐이다.
142
+ *
143
+ * 이전 기록을 읽어 이어 세므로, 성공 한 번이 실패의 역사를 지운다. 그것이 맞다:
144
+ * 지금 돌고 있다면 사람이 볼 것은 지금이다.
145
+ */
146
+ export async function recordPass(scope, code, at = new Date().toISOString()) {
147
+ let previous;
148
+ const raw = await scope.get(LAST_PASS_KEY);
149
+ if (raw) {
150
+ try {
151
+ previous = JSON.parse(raw);
152
+ }
153
+ catch {
154
+ // 못 읽는 기록은 없는 것으로 본다 — 이 값 때문에 회차가 멈추면 안 된다
155
+ }
156
+ }
157
+ const record = {
158
+ at,
159
+ code,
160
+ consecutiveFailures: code === 0 ? 0 : (previous?.consecutiveFailures ?? 0) + 1,
161
+ ...(code === 0 ? { lastOkAt: at } : previous?.lastOkAt ? { lastOkAt: previous.lastOkAt } : {}),
162
+ };
163
+ await scope.set(LAST_PASS_KEY, JSON.stringify(record));
164
+ return record;
165
+ }
166
+ /** 붙어 있다는 것과 돌고 있다는 것은 다른 사실이다. 무엇이 어긋났는지 한 줄로 말한다. */
167
+ export function passLine(record) {
168
+ if (!record || record.code === 0)
169
+ return null;
170
+ const since = record.lastOkAt ? `last good pass ${record.lastOkAt}` : 'no pass has ever succeeded here';
171
+ return (`Background runtime: DEGRADED — the last machine pass exited ${record.code} ` +
172
+ `(${record.consecutiveFailures} in a row, ${since}). Attached is not the same as observed.`);
173
+ }
128
174
  /** 상태를 모은다. **읽기만 한다** — 보는 것이 상태를 바꾸면 아무도 못 본다. */
129
175
  export async function readBackground(scope, staleMs, now = () => new Date().toISOString()) {
130
176
  const lease = await new RuntimeLease({ scope, owner: '(reader)', staleMs, now }).read();
@@ -138,7 +184,17 @@ export async function readBackground(scope, staleMs, now = () => new Date().toIS
138
184
  // 깨진 기록은 없는 것으로 본다 — 아래 렌더가 "한 번도 돌지 않았다"로 말한다
139
185
  }
140
186
  }
141
- return { lease, lastRun, staleMs };
187
+ let lastPass;
188
+ const passRaw = await scope.get(LAST_PASS_KEY);
189
+ if (passRaw) {
190
+ try {
191
+ lastPass = JSON.parse(passRaw);
192
+ }
193
+ catch {
194
+ // 깨진 기록은 없는 것으로 본다
195
+ }
196
+ }
197
+ return { lease, lastRun, ...(lastPass ? { lastPass } : {}), staleMs };
142
198
  }
143
199
  const ORDER = ['delta', 'reconcile', 'census', 'digest'];
144
200
  /**
@@ -149,9 +205,19 @@ const ORDER = ['delta', 'reconcile', 'census', 'digest'];
149
205
  */
150
206
  export function renderBackground(status) {
151
207
  const lines = [];
208
+ // 회차가 서 있는 것과 그 회차가 이 자리를 실제로 돌았는지는 다른 사실이다.
209
+ // 어긋나 있으면 lease 상태보다 먼저 말한다 — 아래 줄들은 "돌고 있다" 로 읽힌다.
210
+ const degraded = passLine(status.lastPass);
211
+ if (degraded)
212
+ lines.push(degraded);
152
213
  switch (status.lease.kind) {
153
214
  case 'FREE':
154
- lines.push('Background runtime: not running `asc runtime start --detach` keeps it observing');
215
+ // **정상 경로는 기계 등록물이다** (0.4.0 그렇게 정했다: "those surfaces exist for
216
+ // development and recovery, not for the normal path"). 그런데 이 줄은 사람에게
217
+ // 개발용 명령을 정상 경로로 안내하고 있었다 — 화면이 결정과 어긋나 있으면 사람은
218
+ // 화면을 따른다.
219
+ lines.push('Background runtime: not running — this machine observes through its registration ' +
220
+ '(`asc runtime service status` shows it)');
155
221
  break;
156
222
  case 'HELD':
157
223
  lines.push(`Background runtime: running (pid ${status.lease.record.pid}, since ${status.lease.record.startedAt}, last heartbeat ${status.lease.record.at})`);
@@ -8,6 +8,14 @@ export type CollectOutcome = {
8
8
  active: string[];
9
9
  /** 이번에 거둔 세션들. Handoff를 읽었다는 뜻이다. */
10
10
  collected: string[];
11
+ /**
12
+ * 거둔 세션 그 자체 (0.7.0).
13
+ *
14
+ * 회수는 세션을 보관소로 옮기고 끝난다. 그 뒤에 다시 목록을 물으면 방금 거둔 것은
15
+ * 거기 없다 — 화면이 `(다음 작업 없음)` 을 적던 자리가 정확히 여기였다. 인계를 읽은
16
+ * 것은 회수이므로, 읽은 것을 그대로 넘긴다.
17
+ */
18
+ handoffs: Session[];
11
19
  /** 사람이 판단할 것 — 미결과 막힌 세션. */
12
20
  awaiting: string[];
13
21
  occupancy: {
@@ -37,4 +45,4 @@ export type CollectOptions = {
37
45
  */
38
46
  export declare function collectSessions(store: StateStore, at: string, options?: CollectOptions): Promise<CollectOutcome>;
39
47
  /** 사람이 읽는 회수 결과. 다음에 무엇을 할지가 맨 아래 오도록 짠다. */
40
- export declare function renderCollect(outcome: CollectOutcome, sessions: readonly Session[]): string;
48
+ export declare function renderCollect(outcome: CollectOutcome): string;
@@ -90,10 +90,10 @@ export async function collectSessions(store, at, options = {}) {
90
90
  // 같은 세션을 다시 거두지 않는다 — 멱등을 따로 구현할 필요가 없다 (OM §7.4).
91
91
  await store.archive('session', session.id);
92
92
  }
93
- return { active, collected: finished.map((s) => s.id), awaiting, occupancy };
93
+ return { active, collected: finished.map((s) => s.id), handoffs: finished, awaiting, occupancy };
94
94
  }
95
95
  /** 사람이 읽는 회수 결과. 다음에 무엇을 할지가 맨 아래 오도록 짠다. */
96
- export function renderCollect(outcome, sessions) {
96
+ export function renderCollect(outcome) {
97
97
  const lines = [];
98
98
  lines.push(`활성 세션: ${outcome.active.join(', ') || '없음'}`);
99
99
  if (outcome.occupancy.length > 0) {
@@ -103,13 +103,14 @@ export function renderCollect(outcome, sessions) {
103
103
  }
104
104
  if (outcome.collected.length > 0) {
105
105
  lines.push('', '거둔 세션:');
106
- for (const id of outcome.collected) {
107
- const session = sessions.find((s) => s.id === id);
108
- lines.push(` ${id} ${session?.handoff?.next ?? '(다음 작업 없음)'}`);
109
- for (const done of session?.handoff?.done ?? [])
106
+ for (const session of outcome.handoffs) {
107
+ lines.push(` ${session.id} ${session.handoff?.next ?? '(다음 작업 없음)'}`);
108
+ for (const done of session.handoff?.done ?? [])
110
109
  lines.push(` 완료: ${done}`);
111
- if (session?.handoff?.verified)
110
+ if (session.handoff?.verified)
112
111
  lines.push(` 검증: ${session.handoff.verified}`);
112
+ for (const changed of session.handoff?.changed ?? [])
113
+ lines.push(` 바꾼 것: ${changed}`);
113
114
  }
114
115
  }
115
116
  if (outcome.awaiting.length > 0) {
@@ -34,6 +34,7 @@ export declare function viewWorkspaces(workspaces: readonly WorkspaceInput[], ex
34
34
  export declare function dueWorkspaces(views: readonly WorkspaceView[]): {
35
35
  workspaceId: string;
36
36
  cwd: string;
37
+ root: string;
37
38
  }[];
38
39
  /** 사람이 읽는 기계 전체 화면. `cd` 없이 지금 무엇이 도는지 보여야 한다 (설계 §13.2). */
39
40
  export declare function renderWorkspaces(views: readonly WorkspaceView[]): string[];
@@ -44,7 +44,7 @@ export function dueWorkspaces(views) {
44
44
  .filter((view) => view.health === 'ACTIVE')
45
45
  // 여러 checkout 이 있으면 아무 곳에서나 한 번이면 된다 — 관측 대상은 workspace 이지
46
46
  // checkout 이 아니다. 첫 번째를 쓰는 것은 안정적인 선택이다(목록 순서가 index 순서다).
47
- .map((view) => ({ workspaceId: view.workspaceId, cwd: view.liveLocators[0] }));
47
+ .map((view) => ({ workspaceId: view.workspaceId, cwd: view.liveLocators[0], root: view.root }));
48
48
  }
49
49
  /** 사람이 읽는 기계 전체 화면. `cd` 없이 지금 무엇이 도는지 보여야 한다 (설계 §13.2). */
50
50
  export function renderWorkspaces(views) {
@@ -30,6 +30,14 @@ export type ExternalActionResult = {
30
30
  };
31
31
  export interface ScmPort {
32
32
  readonly id: string;
33
+ /**
34
+ * 이 통로가 그 행위를 수행할 수 있는가.
35
+ *
36
+ * 발급 시점에 묻기 위한 것이다. 예전에는 아무 action 으로나 Grant 가 발급됐고, 사람이
37
+ * 승인한 **뒤에** 실행에서 "unsupported" 가 나왔다 — 승인의 의미가 그 자리에서 무너진다.
38
+ * 두지 않아도 되며, 그때는 호출자가 확인하지 못했다는 사실을 그대로 다룬다.
39
+ */
40
+ supports?(action: string): boolean;
33
41
  getThread(reference: string): Promise<ThreadSnapshot>;
34
42
  getBaselines(queries: readonly BaselineQuery[]): Promise<CanonicalSnapshot[]>;
35
43
  /**
@@ -1,4 +1,4 @@
1
- import type { ApprovalRequest, ControlState, ExecutionGrant, MonitorEvent, QueueItem, Session } from '../core/model/entities.ts';
1
+ import type { ApprovalRequest, ControlState, ExecutionGrant, MonitorEvent, Session } from '../core/model/entities.ts';
2
2
  /**
3
3
  * 저장 가능한 entity 종류. 모든 entity는 `version`을 갖고, 그 값이 CAS 토큰이다 —
4
4
  * 별도 etag를 두지 않는다 (C-01 §8).
@@ -7,7 +7,6 @@ export type EntityMap = {
7
7
  session: Session;
8
8
  request: ApprovalRequest;
9
9
  grant: ExecutionGrant;
10
- queueItem: QueueItem;
11
10
  event: MonitorEvent;
12
11
  };
13
12
  export type EntityKind = keyof EntityMap;
@@ -7,6 +7,5 @@ export const ENTITY_KEY = {
7
7
  session: 'id',
8
8
  request: 'id',
9
9
  grant: 'id',
10
- queueItem: 'id',
11
10
  event: 'eventKey',
12
11
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asc-agent/runtime",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "ASC (Agent Session Control) \u2014 local-first human-in-the-loop agent control plane: core, CLI, and adapters",
5
5
  "keywords": [
6
6
  "agent",