@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.
- package/LICENSE +15 -0
- package/README.md +22 -0
- package/dist/adapters/claude-code/binding.d.ts +5 -0
- package/dist/adapters/claude-code/binding.js +14 -0
- package/dist/adapters/claude-code/guard.d.ts +54 -0
- package/dist/adapters/claude-code/guard.js +295 -0
- package/dist/adapters/claude-code/install.d.ts +67 -0
- package/dist/adapters/claude-code/install.js +231 -0
- package/dist/adapters/claude-code/observer.d.ts +38 -0
- package/dist/adapters/claude-code/observer.js +77 -0
- package/dist/adapters/claude-code/probe.d.ts +53 -0
- package/dist/adapters/claude-code/probe.js +129 -0
- package/dist/adapters/claude-code/skill.d.ts +8 -0
- package/dist/adapters/claude-code/skill.js +267 -0
- package/dist/adapters/fixture-work/index.d.ts +34 -0
- package/dist/adapters/fixture-work/index.js +78 -0
- package/dist/adapters/github/adapter.d.ts +32 -0
- package/dist/adapters/github/adapter.js +95 -0
- package/dist/adapters/github/client.d.ts +30 -0
- package/dist/adapters/github/client.js +90 -0
- package/dist/adapters/github/context.d.ts +44 -0
- package/dist/adapters/github/context.js +170 -0
- package/dist/adapters/github/event-source.d.ts +55 -0
- package/dist/adapters/github/event-source.js +183 -0
- package/dist/adapters/github/scm.d.ts +50 -0
- package/dist/adapters/github/scm.js +104 -0
- package/dist/adapters/gitlab/adapter.d.ts +36 -0
- package/dist/adapters/gitlab/adapter.js +117 -0
- package/dist/adapters/gitlab/client.d.ts +30 -0
- package/dist/adapters/gitlab/client.js +47 -0
- package/dist/adapters/gitlab/ports.d.ts +40 -0
- package/dist/adapters/gitlab/ports.js +186 -0
- package/dist/adapters/jam/adapter.d.ts +53 -0
- package/dist/adapters/jam/adapter.js +144 -0
- package/dist/adapters/jam/event-source.d.ts +28 -0
- package/dist/adapters/jam/event-source.js +65 -0
- package/dist/adapters/jam/mcp-client.d.ts +50 -0
- package/dist/adapters/jam/mcp-client.js +218 -0
- package/dist/adapters/jam/ports.d.ts +51 -0
- package/dist/adapters/jam/ports.js +167 -0
- package/dist/adapters/local/identity.d.ts +27 -0
- package/dist/adapters/local/identity.js +40 -0
- package/dist/adapters/local/presentation.d.ts +14 -0
- package/dist/adapters/local/presentation.js +42 -0
- package/dist/adapters/markdown/layout.d.ts +15 -0
- package/dist/adapters/markdown/layout.js +49 -0
- package/dist/adapters/markdown/serialize.d.ts +9 -0
- package/dist/adapters/markdown/serialize.js +120 -0
- package/dist/adapters/markdown/state-store.d.ts +23 -0
- package/dist/adapters/markdown/state-store.js +316 -0
- package/dist/adapters/mattermost/client.d.ts +23 -0
- package/dist/adapters/mattermost/client.js +54 -0
- package/dist/adapters/mattermost/presentation.d.ts +43 -0
- package/dist/adapters/mattermost/presentation.js +88 -0
- package/dist/adapters/memory/mocks.d.ts +87 -0
- package/dist/adapters/memory/mocks.js +163 -0
- package/dist/adapters/memory/runtime-binding.d.ts +23 -0
- package/dist/adapters/memory/runtime-binding.js +118 -0
- package/dist/adapters/memory/state-store.d.ts +17 -0
- package/dist/adapters/memory/state-store.js +125 -0
- package/dist/adapters/text/renderer.d.ts +7 -0
- package/dist/adapters/text/renderer.js +81 -0
- package/dist/adapters/webhook/ingress.d.ts +103 -0
- package/dist/adapters/webhook/ingress.js +150 -0
- package/dist/cli/asc.d.ts +16 -0
- package/dist/cli/asc.js +2914 -0
- package/dist/cli/identity-config.d.ts +7 -0
- package/dist/cli/identity-config.js +31 -0
- package/dist/composition/observe.d.ts +21 -0
- package/dist/composition/observe.js +72 -0
- package/dist/composition/registry.d.ts +33 -0
- package/dist/composition/registry.js +62 -0
- package/dist/composition/runtime.d.ts +63 -0
- package/dist/composition/runtime.js +155 -0
- package/dist/core/approval/service.d.ts +17 -0
- package/dist/core/approval/service.js +129 -0
- package/dist/core/attach/bootstrap.d.ts +102 -0
- package/dist/core/attach/bootstrap.js +178 -0
- package/dist/core/attach/init.d.ts +29 -0
- package/dist/core/attach/init.js +100 -0
- package/dist/core/attach/setup-plan.d.ts +125 -0
- package/dist/core/attach/setup-plan.js +177 -0
- package/dist/core/attach/setup.d.ts +40 -0
- package/dist/core/attach/setup.js +140 -0
- package/dist/core/binding/types.d.ts +93 -0
- package/dist/core/binding/types.js +74 -0
- package/dist/core/distribution/release.d.ts +15 -0
- package/dist/core/distribution/release.js +27 -0
- package/dist/core/distribution/runtime-install.d.ts +50 -0
- package/dist/core/distribution/runtime-install.js +90 -0
- package/dist/core/distribution/runtime-select.d.ts +74 -0
- package/dist/core/distribution/runtime-select.js +149 -0
- package/dist/core/execution/executor.d.ts +51 -0
- package/dist/core/execution/executor.js +106 -0
- package/dist/core/execution/grant.d.ts +42 -0
- package/dist/core/execution/grant.js +78 -0
- package/dist/core/model/entities.d.ts +872 -0
- package/dist/core/model/entities.js +285 -0
- package/dist/core/model/ids.d.ts +41 -0
- package/dist/core/model/ids.js +50 -0
- package/dist/core/model/transitions.d.ts +26 -0
- package/dist/core/model/transitions.js +124 -0
- package/dist/core/monitor/coverage.d.ts +111 -0
- package/dist/core/monitor/coverage.js +129 -0
- package/dist/core/monitor/engine.d.ts +134 -0
- package/dist/core/monitor/engine.js +574 -0
- package/dist/core/monitor/health-alerts.d.ts +35 -0
- package/dist/core/monitor/health-alerts.js +88 -0
- package/dist/core/monitor/investigation.d.ts +82 -0
- package/dist/core/monitor/investigation.js +232 -0
- package/dist/core/monitor/observation.d.ts +79 -0
- package/dist/core/monitor/observation.js +110 -0
- package/dist/core/monitor/relevance.d.ts +47 -0
- package/dist/core/monitor/relevance.js +100 -0
- package/dist/core/monitor/signals.d.ts +59 -0
- package/dist/core/monitor/signals.js +105 -0
- package/dist/core/operator/local-operator.d.ts +60 -0
- package/dist/core/operator/local-operator.js +82 -0
- package/dist/core/operator/preflight.d.ts +60 -0
- package/dist/core/operator/preflight.js +139 -0
- package/dist/core/operator/proceed.d.ts +98 -0
- package/dist/core/operator/proceed.js +167 -0
- package/dist/core/operator/progress.d.ts +124 -0
- package/dist/core/operator/progress.js +135 -0
- package/dist/core/operator/render.d.ts +34 -0
- package/dist/core/operator/render.js +170 -0
- package/dist/core/operator/runtime-binding.d.ts +61 -0
- package/dist/core/operator/runtime-binding.js +22 -0
- package/dist/core/policy/ownership.d.ts +44 -0
- package/dist/core/policy/ownership.js +46 -0
- package/dist/core/policy/policy.d.ts +99 -0
- package/dist/core/policy/policy.js +147 -0
- package/dist/core/policy/remote-freeze.d.ts +101 -0
- package/dist/core/policy/remote-freeze.js +151 -0
- package/dist/core/policy/scope.d.ts +23 -0
- package/dist/core/policy/scope.js +93 -0
- package/dist/core/presentation/digest.d.ts +98 -0
- package/dist/core/presentation/digest.js +160 -0
- package/dist/core/resolver/load.d.ts +121 -0
- package/dist/core/resolver/load.js +246 -0
- package/dist/core/resolver/render.d.ts +4 -0
- package/dist/core/resolver/render.js +62 -0
- package/dist/core/resolver/resolve.d.ts +42 -0
- package/dist/core/resolver/resolve.js +51 -0
- package/dist/core/resolver/version.d.ts +22 -0
- package/dist/core/resolver/version.js +49 -0
- package/dist/core/runtime/audit.d.ts +374 -0
- package/dist/core/runtime/audit.js +454 -0
- package/dist/core/runtime/claims.d.ts +115 -0
- package/dist/core/runtime/claims.js +153 -0
- package/dist/core/runtime/closure.d.ts +73 -0
- package/dist/core/runtime/closure.js +162 -0
- package/dist/core/runtime/controller.d.ts +40 -0
- package/dist/core/runtime/controller.js +121 -0
- package/dist/core/runtime/escalation.d.ts +188 -0
- package/dist/core/runtime/escalation.js +322 -0
- package/dist/core/runtime/execution-state.d.ts +43 -0
- package/dist/core/runtime/execution-state.js +81 -0
- package/dist/core/runtime/front.d.ts +95 -0
- package/dist/core/runtime/front.js +144 -0
- package/dist/core/runtime/orchestrator.d.ts +54 -0
- package/dist/core/runtime/orchestrator.js +98 -0
- package/dist/core/runtime/query.d.ts +184 -0
- package/dist/core/runtime/query.js +213 -0
- package/dist/core/runtime/report.d.ts +33 -0
- package/dist/core/runtime/report.js +108 -0
- package/dist/core/runtime/session.d.ts +156 -0
- package/dist/core/runtime/session.js +281 -0
- package/dist/core/runtime/store-ops.d.ts +22 -0
- package/dist/core/runtime/store-ops.js +26 -0
- package/dist/core/view/build-view.d.ts +36 -0
- package/dist/core/view/build-view.js +131 -0
- package/dist/core/view/decision-view.d.ts +564 -0
- package/dist/core/view/decision-view.js +103 -0
- package/dist/core/workspace/identity.d.ts +82 -0
- package/dist/core/workspace/identity.js +133 -0
- package/dist/core/workspace/index-store.d.ts +140 -0
- package/dist/core/workspace/index-store.js +125 -0
- package/dist/core/workspace/migrate.d.ts +73 -0
- package/dist/core/workspace/migrate.js +123 -0
- package/dist/core/workspace/resolve.d.ts +34 -0
- package/dist/core/workspace/resolve.js +89 -0
- package/dist/ports/adapter.d.ts +35 -0
- package/dist/ports/adapter.js +12 -0
- package/dist/ports/approval.d.ts +98 -0
- package/dist/ports/approval.js +7 -0
- package/dist/ports/change-context.d.ts +22 -0
- package/dist/ports/change-context.js +8 -0
- package/dist/ports/event-source.d.ts +42 -0
- package/dist/ports/event-source.js +6 -0
- package/dist/ports/inventory.d.ts +53 -0
- package/dist/ports/inventory.js +12 -0
- package/dist/ports/presentation.d.ts +62 -0
- package/dist/ports/presentation.js +13 -0
- package/dist/ports/renderer.d.ts +23 -0
- package/dist/ports/renderer.js +7 -0
- package/dist/ports/resource-context.d.ts +51 -0
- package/dist/ports/resource-context.js +9 -0
- package/dist/ports/scm.d.ts +40 -0
- package/dist/ports/scm.js +6 -0
- package/dist/ports/state-store.d.ts +97 -0
- package/dist/ports/state-store.js +12 -0
- package/dist/presets/balanced.json +8 -0
- package/dist/presets/conservative.json +9 -0
- package/dist/presets/lightweight.json +8 -0
- package/dist/profiles/example-team/profile.json +101 -0
- package/dist/profiles/pilot-local/profile.json +56 -0
- package/dist/schemas/profile.d.ts +631 -0
- package/dist/schemas/profile.js +287 -0
- package/package.json +55 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Bounded Query — 다른 파트에 묻되, 답할 수 있는 형태로만 묻는다 (C-04 §3~§5).
|
|
2
|
+
//
|
|
3
|
+
// 왜 자유형 질문을 막는가: "이거 어떻게 할까요?" 는 무엇에 답해야 하는지도, 답이 무엇을
|
|
4
|
+
// 풀어 주는지도 말하지 않는다. 받은 쪽은 판단할 근거가 없으니 다시 미루고, 그렇게 결정이
|
|
5
|
+
// Agent 사이를 돈다. 실제로 도는 동안 세 세션의 context가 소모되고 사람은 마지막에야 안다.
|
|
6
|
+
//
|
|
7
|
+
// 이 모듈이 하는 일과 하지 않는 일:
|
|
8
|
+
// 한다 — 질의를 구조로 받고, 답을 한 번만 받고, 되던지기를 발행 시점에 막고, 표면화
|
|
9
|
+
// 안 한다 — 승인·Grant·예외·범위 확대. 답 하나로 권한이 생기는 경로는 없다 (C-03 §4)
|
|
10
|
+
//
|
|
11
|
+
// DECIDE는 "그 결정의 주인이 결론을 냈다"까지다. Human Approval이 아니고, 그 자체로
|
|
12
|
+
// 어떤 Core 전이도 일으키지 않는다. 사람 결정이 필요하면 이 답을 근거로 올릴 뿐이다.
|
|
13
|
+
//
|
|
14
|
+
// 저장은 closure.ts와 같은 이유로 전부 setIfAbsent 위에 선다 — 잃는 것이 표시값이 아니라
|
|
15
|
+
// 누가 무엇을 물었고 누가 답했는가이기 때문이다. 답은 한 번만 쓰이고 덮이지 않는다.
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { QUERY_ID } from "../model/ids.js";
|
|
18
|
+
import { lookupAuthority } from "../policy/ownership.js";
|
|
19
|
+
export const BoundedQuery = z.object({
|
|
20
|
+
id: z.string().regex(QUERY_ID),
|
|
21
|
+
/** 물은 쪽. 답을 받은 뒤 흐름이 돌아갈 자리다. */
|
|
22
|
+
ownerSessionId: z.string().min(1),
|
|
23
|
+
/** 물은 쪽의 파트. 되던지기 판정의 기준이라 발행 시점 스냅샷으로 박아 둔다. */
|
|
24
|
+
ownerRole: z.string().min(1).optional(),
|
|
25
|
+
/** 어느 결정을 묻는가 (decision domain). 누구에게가 아니라 무엇에 대해서다. */
|
|
26
|
+
requestedAuthority: z.string().min(1),
|
|
27
|
+
question: z.string().min(1),
|
|
28
|
+
context: z.string().optional(),
|
|
29
|
+
/**
|
|
30
|
+
* 답이 없을 때 물은 쪽이 취할 기본값. 이것이 있으면 질문은 차단이 아니라 확인이 되고,
|
|
31
|
+
* 없으면 blockingScope가 실제로 막힌다.
|
|
32
|
+
*/
|
|
33
|
+
proposedDefault: z.string().optional(),
|
|
34
|
+
/** 이 답이 없으면 막히는 범위. 비어 있으면 "막히지 않는다"는 뜻이다. */
|
|
35
|
+
blockingScope: z.string().optional(),
|
|
36
|
+
expectedResponse: z.enum(['DECIDE', 'ANSWER']).default('DECIDE'),
|
|
37
|
+
/** 이 질의가 어느 질의에서 파생했는가. 되던지기는 여기로 드러난다. */
|
|
38
|
+
inReplyTo: z.string().regex(QUERY_ID).optional(),
|
|
39
|
+
openedAt: z.string().min(1),
|
|
40
|
+
});
|
|
41
|
+
export const QueryAnswer = z.object({
|
|
42
|
+
kind: z.enum(['DECIDE', 'ANSWER', 'ESCALATE']),
|
|
43
|
+
/** 답한 파트. DECIDE는 이 값이 그 결정의 주인일 때만 성립한다. */
|
|
44
|
+
byRole: z.string().min(1),
|
|
45
|
+
body: z.string().min(1),
|
|
46
|
+
/** ESCALATE 대상. Agent가 아니라 사람·권한자여야 한다 (C-04 §4.1). */
|
|
47
|
+
escalateTo: z.string().optional(),
|
|
48
|
+
at: z.string().min(1),
|
|
49
|
+
});
|
|
50
|
+
const queryKey = (id) => `query:req:${id}`;
|
|
51
|
+
const answerKey = (id) => `query:ans:${id}`;
|
|
52
|
+
/** 막힌 발행 시도. 막았다고 없던 일이 아니다 — 사람이 봐야 할 사실이다. */
|
|
53
|
+
const violationKey = (id) => `query:vio:${id}`;
|
|
54
|
+
export class QueryLedger {
|
|
55
|
+
#scope;
|
|
56
|
+
#ownership;
|
|
57
|
+
#now;
|
|
58
|
+
constructor(scope, ownership, now = () => new Date().toISOString()) {
|
|
59
|
+
this.#scope = scope;
|
|
60
|
+
this.#ownership = ownership;
|
|
61
|
+
this.#now = now;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 질의를 연다. 되던지기는 **여기서** 막는다 — 실행 중인 세션을 끊는 대신 발행을 막는
|
|
65
|
+
* 것이 OM §16(Interrupt 부재)을 지키면서 순환을 끊는 유일한 자리다.
|
|
66
|
+
*/
|
|
67
|
+
async open(spec) {
|
|
68
|
+
if (!QUERY_ID.test(spec.id)) {
|
|
69
|
+
return { ok: false, reason: 'INVALID_ID', detail: `질의 id 형식이 아니다: '${spec.id}' (X-YYYYMMDD-NN)` };
|
|
70
|
+
}
|
|
71
|
+
if (spec.inReplyTo) {
|
|
72
|
+
const origin = await this.get(spec.inReplyTo);
|
|
73
|
+
if (!origin) {
|
|
74
|
+
return { ok: false, reason: 'ORIGIN_NOT_FOUND', detail: `${spec.inReplyTo} 를 찾지 못했다` };
|
|
75
|
+
}
|
|
76
|
+
const refusal = this.#judgeRelay(spec, origin);
|
|
77
|
+
if (refusal) {
|
|
78
|
+
await this.#recordViolation(spec.id, refusal.reason, origin.id, refusal.detail);
|
|
79
|
+
return { ok: false, reason: refusal.reason, detail: refusal.detail, origin };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const query = BoundedQuery.parse({ ...spec, openedAt: this.#now() });
|
|
83
|
+
const written = await this.#scope.setIfAbsent(queryKey(query.id), JSON.stringify(query));
|
|
84
|
+
if (!written)
|
|
85
|
+
return { ok: false, reason: 'ALREADY_EXISTS', detail: `${query.id} 는 이미 있다` };
|
|
86
|
+
return { ok: true, query };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 파생 질의가 허용되는가.
|
|
90
|
+
*
|
|
91
|
+
* 물은 쪽이 답을 받고 다시 묻는 것은 여전히 1 hop이다 — 막지 않는다.
|
|
92
|
+
* 막는 것은 **받은 쪽이 같은 결정을 다시 넘기는** 경우이고, 그 넘김이 물은 쪽으로
|
|
93
|
+
* 돌아가면 순환, 제3자로 가면 one-hop 위반이다.
|
|
94
|
+
*/
|
|
95
|
+
#judgeRelay(spec, origin) {
|
|
96
|
+
const sameAsker = spec.ownerSessionId === origin.ownerSessionId ||
|
|
97
|
+
(!!spec.ownerRole && !!origin.ownerRole && spec.ownerRole === origin.ownerRole);
|
|
98
|
+
if (sameAsker)
|
|
99
|
+
return null;
|
|
100
|
+
const asked = lookupAuthority(this.#ownership, spec.requestedAuthority);
|
|
101
|
+
const backToOrigin = origin.ownerRole !== undefined && asked.kind === 'RESOLVED' && asked.role === origin.ownerRole;
|
|
102
|
+
return backToOrigin
|
|
103
|
+
? {
|
|
104
|
+
reason: 'CIRCULAR_DELEGATION',
|
|
105
|
+
detail: `${origin.id} 를 물은 쪽은 ${origin.ownerRole} 인데, 그 답 대신 ` +
|
|
106
|
+
`'${spec.requestedAuthority}' 를 다시 ${origin.ownerRole} 에게 묻고 있다. ` +
|
|
107
|
+
`원 요청 '${origin.requestedAuthority}' 을 DECIDE / ANSWER / ESCALATE 중 하나로 종결하라.`,
|
|
108
|
+
}
|
|
109
|
+
: {
|
|
110
|
+
reason: 'ONE_HOP_VIOLATION',
|
|
111
|
+
detail: `${origin.id} 로 받은 결정을 제3자에게 다시 넘기고 있다. ` +
|
|
112
|
+
'받은 쪽이 할 수 있는 것은 DECIDE / ANSWER / ESCALATE 뿐이며, ESCALATE는 Agent가 아니라 ' +
|
|
113
|
+
'명시된 사람·권한자에게 올린다.',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 답은 한 번만 쓰인다. 두 사람이 동시에 답해도 먼저 쓴 것이 남고 나중 것은 거절된다 —
|
|
118
|
+
* 조용히 덮으면 누구 말을 따랐는지 알 수 없게 된다.
|
|
119
|
+
*
|
|
120
|
+
* **이 호출은 어떤 Core 전이도 일으키지 않는다.** DECIDE도 마찬가지다 (C-04 §3.4).
|
|
121
|
+
*/
|
|
122
|
+
async answer(id, input) {
|
|
123
|
+
const query = await this.get(id);
|
|
124
|
+
if (!query)
|
|
125
|
+
return { ok: false, reason: 'NOT_FOUND', detail: `${id} 를 찾지 못했다` };
|
|
126
|
+
if (input.kind === 'DECIDE') {
|
|
127
|
+
const found = lookupAuthority(this.#ownership, query.requestedAuthority);
|
|
128
|
+
if (found.kind !== 'RESOLVED' || found.role !== input.byRole) {
|
|
129
|
+
const who = found.kind === 'RESOLVED'
|
|
130
|
+
? `그 결정의 주인은 '${found.role}' 이다`
|
|
131
|
+
: found.kind === 'AMBIGUOUS'
|
|
132
|
+
? `그 결정의 주인이 갈려 있다 (${found.candidates.join(', ')})`
|
|
133
|
+
: '그 결정의 주인이 선언되지 않았다';
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
reason: 'FORBIDDEN_AUTHORITY',
|
|
137
|
+
detail: `'${input.byRole}' 은 '${query.requestedAuthority}' 를 결정할 수 없다 — ${who}. ` +
|
|
138
|
+
'ANSWER(사실 반환) 또는 ESCALATE(권한자에게 상신)로 종결하라.',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const answer = QueryAnswer.parse({ ...input, at: this.#now() });
|
|
143
|
+
const written = await this.#scope.setIfAbsent(answerKey(id), JSON.stringify(answer));
|
|
144
|
+
if (!written) {
|
|
145
|
+
const existing = (await this.getAnswer(id));
|
|
146
|
+
return { ok: false, reason: 'ALREADY_ANSWERED', detail: `${id} 는 이미 답이 있다`, answer: existing };
|
|
147
|
+
}
|
|
148
|
+
return { ok: true, query, answer };
|
|
149
|
+
}
|
|
150
|
+
async get(id) {
|
|
151
|
+
const raw = await this.#scope.get(queryKey(id));
|
|
152
|
+
return raw ? BoundedQuery.parse(JSON.parse(raw)) : null;
|
|
153
|
+
}
|
|
154
|
+
async getAnswer(id) {
|
|
155
|
+
const raw = await this.#scope.get(answerKey(id));
|
|
156
|
+
return raw ? QueryAnswer.parse(JSON.parse(raw)) : null;
|
|
157
|
+
}
|
|
158
|
+
async list() {
|
|
159
|
+
const out = [];
|
|
160
|
+
for (const key of await this.#scope.keys('query:req:')) {
|
|
161
|
+
const raw = await this.#scope.get(key);
|
|
162
|
+
if (!raw)
|
|
163
|
+
continue;
|
|
164
|
+
const query = BoundedQuery.parse(JSON.parse(raw));
|
|
165
|
+
out.push({ query, answer: await this.getAnswer(query.id) });
|
|
166
|
+
}
|
|
167
|
+
return out.sort((a, b) => a.query.id.localeCompare(b.query.id));
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* 사람에게 넘겨진 질의. 답은 쓰였지만 **끝난 것이 아니다** — 그 답이 "사람이 정하라"였다.
|
|
171
|
+
* 이것이 없으면 ESCALATE는 어느 화면에도 뜨지 않는 write-only 로그가 된다.
|
|
172
|
+
*/
|
|
173
|
+
async escalated() {
|
|
174
|
+
return (await this.list()).filter((entry) => entry.answer?.kind === 'ESCALATE');
|
|
175
|
+
}
|
|
176
|
+
/** 아직 답이 없는 질의. 누군가 기다리고 있다는 뜻이다. */
|
|
177
|
+
async pending() {
|
|
178
|
+
return (await this.list()).filter((e) => e.answer === null).map((e) => e.query);
|
|
179
|
+
}
|
|
180
|
+
/** 막힌 발행 시도. 막았다고 사라지지 않는다 — 같은 결정이 돌고 있었다는 사실이다. */
|
|
181
|
+
async violations() {
|
|
182
|
+
const out = [];
|
|
183
|
+
for (const key of await this.#scope.keys('query:vio:')) {
|
|
184
|
+
const raw = await this.#scope.get(key);
|
|
185
|
+
if (raw)
|
|
186
|
+
out.push(JSON.parse(raw));
|
|
187
|
+
}
|
|
188
|
+
return out.sort((a, b) => a.attemptedId.localeCompare(b.attemptedId));
|
|
189
|
+
}
|
|
190
|
+
async #recordViolation(attemptedId, kind, originId, detail) {
|
|
191
|
+
const violation = { attemptedId, kind, originId, detail, at: this.#now() };
|
|
192
|
+
await this.#scope.setIfAbsent(violationKey(attemptedId), JSON.stringify(violation));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* 사람에게 넘겨진 질의를 줄로. 상신했는데 어느 화면에도 안 뜨는 상태를 막는 것이 목적이다.
|
|
197
|
+
*/
|
|
198
|
+
export function escalatedLines(answered) {
|
|
199
|
+
return answered.map((entry) => `${entry.query.id}: 사람에게 넘김 — ${entry.query.question}` +
|
|
200
|
+
(entry.answer.escalateTo ? ` (→ ${entry.answer.escalateTo})` : ''));
|
|
201
|
+
}
|
|
202
|
+
/** 사람이 읽을 줄로. collect의 "판단이 필요한 것"에 그대로 들어간다. */
|
|
203
|
+
export function queryLines(open, violations) {
|
|
204
|
+
const lines = [];
|
|
205
|
+
for (const q of open) {
|
|
206
|
+
const blocked = q.blockingScope ? ` (막힘: ${q.blockingScope})` : '';
|
|
207
|
+
lines.push(`${q.ownerSessionId}: 답을 기다리는 질의 ${q.id} — '${q.requestedAuthority}'${blocked}`);
|
|
208
|
+
}
|
|
209
|
+
for (const v of violations) {
|
|
210
|
+
lines.push(`${v.kind}: ${v.attemptedId} (원 질의 ${v.originId}) — ${v.detail}`);
|
|
211
|
+
}
|
|
212
|
+
return lines;
|
|
213
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { DecisionEvidence, ExecutionEvidence, ReclaimEvidence, ValidationRecord } from './audit.ts';
|
|
2
|
+
import type { EscalationRecord } from './escalation.ts';
|
|
3
|
+
import type { Claim } from './claims.ts';
|
|
4
|
+
import type { ExecutionVerdict } from './execution-state.ts';
|
|
5
|
+
import type { Handoff, Session } from '../model/entities.ts';
|
|
6
|
+
export type ReportInput = {
|
|
7
|
+
session: Pick<Session, 'id' | 'role' | 'goal' | 'status' | 'doneCriteria'> & {
|
|
8
|
+
handoff?: Handoff;
|
|
9
|
+
};
|
|
10
|
+
executions?: readonly ExecutionEvidence[];
|
|
11
|
+
validations?: readonly ValidationRecord[];
|
|
12
|
+
reclaim?: ReclaimEvidence | null;
|
|
13
|
+
claims?: readonly Claim[];
|
|
14
|
+
/** 승인 없이 간 결정들 (C-13 §4). 몇 건을 스스로 정했는지가 판정 근거다. */
|
|
15
|
+
decisions?: readonly DecisionEvidence[];
|
|
16
|
+
/** 미해소 상신 (C-13). 무엇이 막혔고 무엇이 갔는지 미결에 든다. */
|
|
17
|
+
escalations?: readonly EscalationRecord[];
|
|
18
|
+
/** 파생 실행 상태. 있으면 결과 줄이 이것을 따른다 — 두 개의 결론을 만들지 않는다. */
|
|
19
|
+
derived?: ExecutionVerdict;
|
|
20
|
+
};
|
|
21
|
+
export type FinalReport = {
|
|
22
|
+
result: string;
|
|
23
|
+
/** 왜 그렇게 판정했는가. 근거가 **어디에 있는지**를 가리키고 본문을 복제하지 않는다. */
|
|
24
|
+
basis: string[];
|
|
25
|
+
/** 미결과 위험. 없으면 없다고 적는다 — 빈 칸은 "없다"가 아니라 "안 봤다"로 읽힌다. */
|
|
26
|
+
open: string[];
|
|
27
|
+
next: string[];
|
|
28
|
+
/** 상세를 어디서 읽는가. 보고가 증거 저장소가 되지 않게 하는 장치다. */
|
|
29
|
+
evidence: string[];
|
|
30
|
+
};
|
|
31
|
+
export declare function buildFinalReport(input: ReportInput): FinalReport;
|
|
32
|
+
/** 사람이 읽는 최종 보고. 네 블록을 넘지 않는다. */
|
|
33
|
+
export declare function renderFinalReport(report: FinalReport): string[];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Compact Final Report — 보고는 증거의 projection이다 (C-10 §5 계열 · 지시 §28).
|
|
2
|
+
//
|
|
3
|
+
// 최종 보고가 길어지는 이유는 보고 자체가 저장소 역할을 하기 때문이다. 과정을 전부 옮겨
|
|
4
|
+
// 적으면 다음 사람이 그 글을 읽어야만 사실을 알 수 있고, 그러면 audit trail은 있으나
|
|
5
|
+
// 아무도 안 본다.
|
|
6
|
+
//
|
|
7
|
+
// 그래서 네 가지만 든다:
|
|
8
|
+
//
|
|
9
|
+
// 결과 · 판정 근거 · 미결/위험 · 다음 행동
|
|
10
|
+
//
|
|
11
|
+
// 나머지는 이미 남아 있는 곳에서 읽는다 — Checkpoint · Handoff · Validation · Claim History.
|
|
12
|
+
// **보고를 줄이려고 증거를 버리지 않는다.** 줄이는 것은 옮겨 적기이지 기록이 아니다.
|
|
13
|
+
export function buildFinalReport(input) {
|
|
14
|
+
const { session } = input;
|
|
15
|
+
const executions = input.executions ?? [];
|
|
16
|
+
const validations = input.validations ?? [];
|
|
17
|
+
const claims = input.claims ?? [];
|
|
18
|
+
const result = input.derived
|
|
19
|
+
? `${session.id} ${input.derived.state} — ${session.goal}`
|
|
20
|
+
: `${session.id} ${session.status} — ${session.goal}`;
|
|
21
|
+
const basis = [];
|
|
22
|
+
if (input.derived)
|
|
23
|
+
basis.push(...input.derived.reasons);
|
|
24
|
+
// 실행 증거: 몇 번이 아니라 **누가**가 중요하다. 같은 주체면 그 사실이 판정을 바꾼다.
|
|
25
|
+
if (executions.length === 0) {
|
|
26
|
+
basis.push('실행 증거 없음 — 발급됐으나 아무도 집지 않았다');
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
const principals = [...new Set(executions.map((e) => e.principal))];
|
|
30
|
+
basis.push(`실행 ${executions.length}건 · 주체 ${principals.join(', ')}`);
|
|
31
|
+
if (executions.some((e) => e.principalSource === 'derived')) {
|
|
32
|
+
basis.push('주체 일부가 선언되지 않았다 — 독립성 주장은 UNVERIFIED를 넘지 못한다');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// 검증: 등급을 빼고 결과만 적으면 자기 확인이 독립 검증처럼 보인다.
|
|
36
|
+
if (validations.length === 0) {
|
|
37
|
+
basis.push('독립 검증 없음');
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
for (const validation of validations) {
|
|
41
|
+
basis.push(`검증 ${validation.result} (${validation.independence}) — ${validation.independenceDetail}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// 자율 판단과 상신의 비율이 곧 "Approval은 예외인가"의 증거다 (C-13 §0)
|
|
45
|
+
const decisions = input.decisions ?? [];
|
|
46
|
+
const escalations = input.escalations ?? [];
|
|
47
|
+
basis.push(decisions.length > 0
|
|
48
|
+
? `승인 없이 정한 것 ${decisions.length}건 (${decisions.map((d) => d.class).join(', ')})`
|
|
49
|
+
: '승인 없이 정한 것 없음');
|
|
50
|
+
basis.push(escalations.length > 0 ? `사람에게 올린 것 ${escalations.length}건` : '사람에게 올린 것 없음');
|
|
51
|
+
if (session.handoff)
|
|
52
|
+
basis.push(`자기 확인: ${session.handoff.verified} (독립 검증 아님)`);
|
|
53
|
+
basis.push(input.reclaim ? `회수: ${input.reclaim.reclaimedBy}` : '회수 기록 없음');
|
|
54
|
+
const open = [];
|
|
55
|
+
if (session.handoff?.unresolved?.length)
|
|
56
|
+
open.push(...session.handoff.unresolved);
|
|
57
|
+
// 추론과 미확인은 결과가 아니라 미결이다 — 결과 줄에 섞으면 확정처럼 읽힌다.
|
|
58
|
+
for (const claim of claims) {
|
|
59
|
+
if (claim.status === 'INFERRED')
|
|
60
|
+
open.push(`추론(확정 아님): ${claim.statement}`);
|
|
61
|
+
if (claim.status === 'PENDING')
|
|
62
|
+
open.push(`미확인: ${claim.statement}`);
|
|
63
|
+
}
|
|
64
|
+
for (const record of escalations) {
|
|
65
|
+
open.push(`외부 결정 대기 ${record.escalationId} [${record.predicates.join(', ')}] — 막힘 ${record.blockedNodes.join(', ')}`);
|
|
66
|
+
}
|
|
67
|
+
const unmet = session.doneCriteria.filter((item) => !(session.handoff?.done ?? []).includes(item));
|
|
68
|
+
if (unmet.length > 0)
|
|
69
|
+
open.push(`${unmet.length} done-criteria remaining: ${unmet.join(', ')}`);
|
|
70
|
+
if (open.length === 0)
|
|
71
|
+
open.push('없음');
|
|
72
|
+
const next = [];
|
|
73
|
+
if (session.handoff?.next)
|
|
74
|
+
next.push(session.handoff.next);
|
|
75
|
+
if (!input.reclaim && session.status === 'DONE')
|
|
76
|
+
next.push('Controller 회수 필요 — asc controller collect --as <주체>');
|
|
77
|
+
if (validations.length === 0)
|
|
78
|
+
next.push('독립 검증이 필요하면 별도 주체로 asc session validate');
|
|
79
|
+
if (next.length === 0)
|
|
80
|
+
next.push('없음');
|
|
81
|
+
return {
|
|
82
|
+
result,
|
|
83
|
+
basis,
|
|
84
|
+
open,
|
|
85
|
+
next,
|
|
86
|
+
// 본문을 옮기지 않고 어디를 볼지만 가리킨다
|
|
87
|
+
evidence: [
|
|
88
|
+
`asc session audit ${session.id}`,
|
|
89
|
+
...(escalations.length > 0 ? [`asc escalate list`] : []),
|
|
90
|
+
...(claims.length > 0 ? ['claim history (STALE 포함)'] : []),
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** 사람이 읽는 최종 보고. 네 블록을 넘지 않는다. */
|
|
95
|
+
export function renderFinalReport(report) {
|
|
96
|
+
return [
|
|
97
|
+
`## 결과`,
|
|
98
|
+
` ${report.result}`,
|
|
99
|
+
`## 판정 근거`,
|
|
100
|
+
...report.basis.map((line) => ` ${line}`),
|
|
101
|
+
`## 미결 / 위험`,
|
|
102
|
+
...report.open.map((line) => ` ${line}`),
|
|
103
|
+
`## 다음 행동`,
|
|
104
|
+
...report.next.map((line) => ` ${line}`),
|
|
105
|
+
`## 상세는 여기서 읽는다`,
|
|
106
|
+
...report.evidence.map((line) => ` ${line}`),
|
|
107
|
+
];
|
|
108
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Session, type Checkpoint, type Handoff, type SessionRole } from '../model/entities.ts';
|
|
2
|
+
import { type OwnershipMap } from '../policy/ownership.ts';
|
|
3
|
+
import type { ResolvedPolicy } from '../policy/policy.ts';
|
|
4
|
+
import type { BaselineQuery, ScmPort } from '../../ports/scm.ts';
|
|
5
|
+
import type { StateStore } from '../../ports/state-store.ts';
|
|
6
|
+
import type { RuntimeBindings } from '../operator/runtime-binding.ts';
|
|
7
|
+
import { type ApplyOutcome } from './store-ops.ts';
|
|
8
|
+
/** 소유하지 않은 세션에 기록하려 했다 (C-10 §2.3). */
|
|
9
|
+
export type NotOwner = {
|
|
10
|
+
ok: false;
|
|
11
|
+
reason: 'NOT_OWNER';
|
|
12
|
+
detail: string;
|
|
13
|
+
};
|
|
14
|
+
/** 전이 결과에 소유권 거부가 더해진 형태. 전이 자체의 실패 갈래는 그대로다. */
|
|
15
|
+
export type WriteOutcome<T> = ApplyOutcome<T> | NotOwner;
|
|
16
|
+
export type CanonicalDrift = {
|
|
17
|
+
sourceId: string;
|
|
18
|
+
recorded: string;
|
|
19
|
+
current: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* 정본이 움직였거나, 정본을 읽을 수 없다. 둘 다 그냥 이어갈 수 없는 이유다 (OM §9).
|
|
23
|
+
*
|
|
24
|
+
* 읽지 못한 것을 "달라진 게 없다"로 넘기면 검증이 있는 척만 하게 된다 — 토큰이 없거나
|
|
25
|
+
* 조회가 실패한 채로 도는 세션이 가장 위험하다. 무엇을 딛고 선지 모르면 멈춘다.
|
|
26
|
+
*/
|
|
27
|
+
export type StartOutcome = ApplyOutcome<Session> | {
|
|
28
|
+
ok: false;
|
|
29
|
+
reason: 'CANONICAL_DRIFT';
|
|
30
|
+
drifts: CanonicalDrift[];
|
|
31
|
+
} | {
|
|
32
|
+
ok: false;
|
|
33
|
+
reason: 'CANONICAL_UNAVAILABLE';
|
|
34
|
+
detail: string;
|
|
35
|
+
};
|
|
36
|
+
export type SessionDeps = {
|
|
37
|
+
/** 정본을 실제로 다시 읽는 통로. 없으면 baseline 기록·대조를 건너뛴다. */
|
|
38
|
+
scm?: ScmPort;
|
|
39
|
+
/** Profile이 정의한 정본 갈래들 (OM §8). */
|
|
40
|
+
canonicalSources?: readonly BaselineQuery[];
|
|
41
|
+
/** Profile이 선언한 책임 지도 (C-04 §6). 없으면 결정권 판정이 성립하지 않는다. */
|
|
42
|
+
ownership?: OwnershipMap;
|
|
43
|
+
/**
|
|
44
|
+
* 현재 소유권 통로 (C-10 §2.3). 주면 checkpoint·handoff 쓰기에 owner 검사가 붙는다 —
|
|
45
|
+
* Progress가 이미 하던 검사를 나머지 둘에도 대칭으로 건다.
|
|
46
|
+
*
|
|
47
|
+
* 주지 않으면 검사하지 않는다. 소유권 개념이 없는 경로(단독 사용·기존 스크립트)를
|
|
48
|
+
* 이 계약이 갑자기 잠그지 않기 위해서다. **binding이 있는데 owner가 아닌 경우만 막는다.**
|
|
49
|
+
*/
|
|
50
|
+
bindings?: RuntimeBindings;
|
|
51
|
+
now?: () => string;
|
|
52
|
+
};
|
|
53
|
+
export type SessionSpec = {
|
|
54
|
+
id: string;
|
|
55
|
+
role: SessionRole;
|
|
56
|
+
goal: string;
|
|
57
|
+
doneCriteria?: string[];
|
|
58
|
+
blockId?: string;
|
|
59
|
+
taskPointer?: string;
|
|
60
|
+
canonicalSources?: {
|
|
61
|
+
sourceId: string;
|
|
62
|
+
baseline: string;
|
|
63
|
+
}[];
|
|
64
|
+
readScope?: string[];
|
|
65
|
+
/** Profile이 준 Role 최대 범위보다 좁아야 한다 — 넓히려 하면 발급이 실패한다. */
|
|
66
|
+
writeBoundary?: string[];
|
|
67
|
+
outOfScope?: string[];
|
|
68
|
+
/** Controller가 이 세션에 한해 허용한 SOFT DENY 항목 (OM §5.1). */
|
|
69
|
+
policyExceptions?: string[];
|
|
70
|
+
/** 이 일을 끝까지 끌고 갈 주체 (C-04 §1.1). */
|
|
71
|
+
owner?: string;
|
|
72
|
+
/** 이 일에 걸린 결정 영역. 선언하면 결정권자가 있는지 발급 전에 확인한다. */
|
|
73
|
+
decisionDomains?: string[];
|
|
74
|
+
/** 이번 세션에 한해 정한 결정권자 (domain → role). */
|
|
75
|
+
decisionAuthority?: Record<string, string>;
|
|
76
|
+
/** 외부에서 받아야 할 입력. owner를 바꾸지 않는다. */
|
|
77
|
+
dependencies?: string[];
|
|
78
|
+
};
|
|
79
|
+
export type IssueFailure = {
|
|
80
|
+
kind: 'INVALID_CONTRACT';
|
|
81
|
+
detail: string;
|
|
82
|
+
} | {
|
|
83
|
+
kind: 'CANONICAL_UNAVAILABLE';
|
|
84
|
+
detail: string;
|
|
85
|
+
} | {
|
|
86
|
+
kind: 'SCOPE_ESCALATION';
|
|
87
|
+
detail: string;
|
|
88
|
+
} | {
|
|
89
|
+
kind: 'INVALID_SCOPE';
|
|
90
|
+
detail: string;
|
|
91
|
+
} | {
|
|
92
|
+
kind: 'HARD_DENY_ESCAPE';
|
|
93
|
+
detail: string;
|
|
94
|
+
}
|
|
95
|
+
/** 이 일에 필요한 결정의 주인이 정해지지 않았다 (C-04 §1.3). */
|
|
96
|
+
| {
|
|
97
|
+
kind: 'RESPONSIBILITY_AMBIGUOUS';
|
|
98
|
+
detail: string;
|
|
99
|
+
} | {
|
|
100
|
+
kind: 'ALREADY_EXISTS';
|
|
101
|
+
detail: string;
|
|
102
|
+
};
|
|
103
|
+
export type IssueResult = {
|
|
104
|
+
ok: true;
|
|
105
|
+
session: Session;
|
|
106
|
+
} | {
|
|
107
|
+
ok: false;
|
|
108
|
+
failures: IssueFailure[];
|
|
109
|
+
};
|
|
110
|
+
export declare class SessionRuntime {
|
|
111
|
+
#private;
|
|
112
|
+
constructor(store: StateStore, policy?: ResolvedPolicy | null, deps?: SessionDeps);
|
|
113
|
+
/**
|
|
114
|
+
* Controller가 세션 계약을 발급한다. Session Contract는 Policy hierarchy의 최하위
|
|
115
|
+
* 계층이므로 상위보다 넓은 권한을 요청할 수 없다 (OM §5.2).
|
|
116
|
+
* 범위를 벗어난 요청은 조용히 좁히지 않고 거절한다 — 계약서와 실권한이 어긋난 채로
|
|
117
|
+
* 굴러가는 것이 더 위험하다.
|
|
118
|
+
*/
|
|
119
|
+
issue(spec: SessionSpec): Promise<IssueResult>;
|
|
120
|
+
get(id: string): Promise<Session | null>;
|
|
121
|
+
/**
|
|
122
|
+
* Physical Run이 계약을 집어 든다. 집기 전에 정본이 그대로인지 본다 —
|
|
123
|
+
* 바닥이 달라진 채로 이어가면 낡은 전제 위에서 작업하게 된다.
|
|
124
|
+
*/
|
|
125
|
+
start(id: string): Promise<StartOutcome>;
|
|
126
|
+
/** Run이 끊긴다. Checkpoint 없이는 멈출 수 없다 — 다음 Run이 이어받을 수 없기 때문이다. */
|
|
127
|
+
pause(id: string, checkpoint: Checkpoint, physicalSessionId?: string): Promise<WriteOutcome<Session>>;
|
|
128
|
+
/** 다른 Physical Run이 같은 Logical Session을 이어받는다. 여기서도 정본부터 확인한다. */
|
|
129
|
+
resume(id: string): Promise<StartOutcome>;
|
|
130
|
+
/**
|
|
131
|
+
* 지금 정본 상태를 읽기 전용으로 판정한다 (C-03 §1.6). ACTIVE 세션을 이어갈 때도
|
|
132
|
+
* 검증이 필요하지만 상태 전이는 없어야 하므로, 판정 로직을 Operator에 복제하지 않고
|
|
133
|
+
* 여기서 공개한다.
|
|
134
|
+
*/
|
|
135
|
+
checkCanonical(id: string): Promise<{
|
|
136
|
+
status: 'CURRENT';
|
|
137
|
+
} | {
|
|
138
|
+
status: 'DRIFT';
|
|
139
|
+
drifts: CanonicalDrift[];
|
|
140
|
+
} | {
|
|
141
|
+
status: 'UNAVAILABLE';
|
|
142
|
+
detail: string;
|
|
143
|
+
} | {
|
|
144
|
+
status: 'NOT_FOUND';
|
|
145
|
+
}>;
|
|
146
|
+
/** HARD DENY가 Goal을 막았다 — Controller 판단이 필요하다 (OM §5.3). */
|
|
147
|
+
block(id: string): Promise<ApplyOutcome<Session>>;
|
|
148
|
+
/** 막힌 사유를 Controller가 해소했다. */
|
|
149
|
+
unblock(id: string): Promise<ApplyOutcome<Session>>;
|
|
150
|
+
/**
|
|
151
|
+
* 세션 종료. Handoff까지가 에이전트 몫이고, state·block·queue 갱신은 Controller가
|
|
152
|
+
* 회수한 뒤에 한다 (OM §7.2·§9).
|
|
153
|
+
*/
|
|
154
|
+
complete(id: string, handoff: Handoff, physicalSessionId?: string): Promise<WriteOutcome<Session>>;
|
|
155
|
+
fail(id: string, actor?: 'session' | 'controller'): Promise<ApplyOutcome<Session>>;
|
|
156
|
+
}
|