@asc-agent/runtime 0.2.1 → 0.3.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/README.md +25 -25
- package/dist/adapters/claude-code/skill.js +245 -239
- package/dist/adapters/gitlab/adapter.d.ts +3 -0
- package/dist/adapters/gitlab/adapter.js +22 -3
- package/dist/adapters/gitlab/client.d.ts +27 -1
- package/dist/adapters/gitlab/client.js +30 -0
- package/dist/adapters/gitlab/ports.d.ts +3 -3
- package/dist/adapters/jam/adapter.d.ts +14 -0
- package/dist/adapters/jam/adapter.js +84 -6
- package/dist/adapters/jam/ports.d.ts +9 -0
- package/dist/adapters/jam/ports.js +49 -2
- package/dist/adapters/local/repo.d.ts +15 -0
- package/dist/adapters/local/repo.js +179 -0
- package/dist/adapters/markdown/state-store.js +13 -1
- package/dist/adapters/memory/state-store.js +4 -0
- package/dist/cli/asc.js +452 -4
- package/dist/composition/propose.d.ts +19 -0
- package/dist/composition/propose.js +35 -0
- package/dist/composition/runtime.d.ts +2 -0
- package/dist/composition/runtime.js +41 -5
- package/dist/core/attach/init.d.ts +17 -0
- package/dist/core/attach/init.js +28 -0
- package/dist/core/distribution/release.d.ts +3 -3
- package/dist/core/distribution/release.js +1 -1
- package/dist/core/monitor/investigation.d.ts +16 -0
- package/dist/core/monitor/investigation.js +18 -9
- package/dist/core/operator/contract-draft.d.ts +124 -0
- package/dist/core/operator/contract-draft.js +234 -0
- package/dist/core/operator/derive-draft.d.ts +37 -0
- package/dist/core/operator/derive-draft.js +216 -0
- package/dist/core/operator/proceed.d.ts +79 -0
- package/dist/core/operator/proceed.js +123 -1
- package/dist/core/operator/work-state.d.ts +54 -0
- package/dist/core/operator/work-state.js +145 -0
- package/dist/core/runtime/closure.d.ts +2 -2
- package/dist/ports/local-repo.d.ts +61 -0
- package/dist/ports/local-repo.js +9 -0
- package/dist/ports/resource-context.d.ts +7 -0
- package/dist/schemas/profile.d.ts +2 -2
- package/package.json +2 -2
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
// 변경으로 끝난다 — 그것이 "provider 교체는 Binding 교체" 의 실제 모습이다.
|
|
6
6
|
//
|
|
7
7
|
// **Core는 이 파일을 import하지 않는다.** 방향은 언제나 Composition → Core다.
|
|
8
|
+
import { execFile } from 'node:child_process';
|
|
9
|
+
import { promisify } from 'node:util';
|
|
8
10
|
import { resolveCapability } from "../core/binding/types.js";
|
|
9
11
|
import { GitHubClient, discoverToken } from "../adapters/github/client.js";
|
|
10
12
|
import { GitHubChangeContext, GitHubInventory, GitHubResourceContext } from "../adapters/github/context.js";
|
|
11
13
|
import { GitHubEventSource } from "../adapters/github/event-source.js";
|
|
12
14
|
import { GitHubScm } from "../adapters/github/scm.js";
|
|
13
|
-
import { GitLabClient, discoverToken as discoverGitLabToken } from "../adapters/gitlab/client.js";
|
|
15
|
+
import { GitLabClient, GlabApiClient, discoverToken as discoverGitLabToken, glabAvailable, } from "../adapters/gitlab/client.js";
|
|
14
16
|
import { GitLabChangeContext, GitLabEventSource, GitLabInventory, GitLabResourceContext, } from "../adapters/gitlab/ports.js";
|
|
15
17
|
import { JamMcpClient } from "../adapters/jam/mcp-client.js";
|
|
16
18
|
import { JamEventSource } from "../adapters/jam/event-source.js";
|
|
@@ -19,7 +21,9 @@ const FACTORIES = {
|
|
|
19
21
|
gitlab(binding, input, token) {
|
|
20
22
|
// 자체 호스팅이 흔하다. 어디를 가리키는지는 발견 단계가 이미 알아냈으므로 같은 값을 쓴다.
|
|
21
23
|
const baseUrl = input.endpointFor?.(binding);
|
|
22
|
-
|
|
24
|
+
// 토큰이 빈 문자열이면 자격이 도구 안에 있다는 뜻이다 (P1-H). 값을 꺼내 오지 않고
|
|
25
|
+
// 그 도구에게 요청을 대신 보내 달라고 한다 — 읽기 전용이다.
|
|
26
|
+
const client = token === '' ? new GlabApiClient(defaultGlabRun) : new GitLabClient({ token, ...(baseUrl ? { baseUrl } : {}) });
|
|
23
27
|
const project = binding.resource;
|
|
24
28
|
return {
|
|
25
29
|
eventSource: new GitLabEventSource({ client, project, perPage: input.perPage ?? 30 }),
|
|
@@ -44,11 +48,11 @@ const FACTORIES = {
|
|
|
44
48
|
// JAM은 토큰을 받지 않는다 — 자격은 도구가 자기 안에서 관리하고 ASC는 상태만 읽는다.
|
|
45
49
|
if (!input.jam)
|
|
46
50
|
return {};
|
|
47
|
-
const client = new JamMcpClient({
|
|
51
|
+
const client = registerToolClient(new JamMcpClient({
|
|
48
52
|
command: input.jam.command,
|
|
49
53
|
...(input.jam.args ? { args: input.jam.args } : {}),
|
|
50
54
|
...(input.jam.cwd ? { cwd: input.jam.cwd } : {}),
|
|
51
|
-
});
|
|
55
|
+
}));
|
|
52
56
|
const projectKey = binding.resource;
|
|
53
57
|
const timezone = input.jam.timezone;
|
|
54
58
|
const inventory = new JamInventory({ client, projectKey, ...(timezone ? { timezone } : {}) });
|
|
@@ -64,8 +68,40 @@ const FACTORIES = {
|
|
|
64
68
|
};
|
|
65
69
|
},
|
|
66
70
|
};
|
|
71
|
+
const execFileAsync = promisify(execFile);
|
|
72
|
+
/** glab 실행 통로. 실패는 예외로 올라가고, 호출측이 "없다"로 접는다. */
|
|
73
|
+
const defaultGlabRun = async (command, args) => {
|
|
74
|
+
const { stdout } = await execFileAsync(command, [...args]);
|
|
75
|
+
return stdout;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* gitlab 자격 찾기. env 토큰이 먼저고, 없으면 로그인된 `glab` 을 통로로 인정해 빈 문자열을
|
|
79
|
+
* 돌려준다 — **값이 아니라 "통로가 있다"는 사실이다.** 둘 다 없으면 null 이고, 그때만
|
|
80
|
+
* 조립하지 않는다.
|
|
81
|
+
*/
|
|
82
|
+
const discoverGitLabAccess = async () => {
|
|
83
|
+
const token = discoverGitLabToken();
|
|
84
|
+
if (token)
|
|
85
|
+
return token;
|
|
86
|
+
return (await glabAvailable(defaultGlabRun)) ? '' : null;
|
|
87
|
+
};
|
|
67
88
|
/** 이 adapter는 토큰 없이 조립된다. 자격은 도구가 자기 안에서 진다. */
|
|
68
89
|
const TOKENLESS = new Set(['jam']);
|
|
90
|
+
/**
|
|
91
|
+
* 자식 프로세스를 띄우는 도구 클라이언트들. 명령이 끝나면 닫아야 한다 — 안 닫으면
|
|
92
|
+
* CLI 가 할 일을 다 하고도 종료하지 못하고 서버 프로세스가 남는다(실제로 그렇게 됐다).
|
|
93
|
+
*/
|
|
94
|
+
const toolClients = new Set();
|
|
95
|
+
function registerToolClient(client) {
|
|
96
|
+
toolClients.add(client);
|
|
97
|
+
return client;
|
|
98
|
+
}
|
|
99
|
+
/** 이 프로세스가 띄운 도구 자식들을 정리한다. 여러 번 불러도 안전하다. */
|
|
100
|
+
export async function closeToolClients() {
|
|
101
|
+
const clients = [...toolClients];
|
|
102
|
+
toolClients.clear();
|
|
103
|
+
await Promise.all(clients.map((client) => client.stop().catch(() => undefined)));
|
|
104
|
+
}
|
|
69
105
|
/**
|
|
70
106
|
* capability와 Port의 대응. **이 표가 없으면 조립이 덮어쓰기가 된다** —
|
|
71
107
|
* 두 binding이 각각 다른 capability를 맡았는데 나중 것이 앞 것의 Port까지 밀어낸다.
|
|
@@ -108,7 +144,7 @@ export async function buildRuntimePorts(input) {
|
|
|
108
144
|
const ports = { unavailable: [] };
|
|
109
145
|
// 자격은 adapter마다 다른 곳에 있다. Core는 이 사실을 모르고, 여기서만 안다.
|
|
110
146
|
const findToken = input.findToken ??
|
|
111
|
-
(async (adapterId) => adapterId === 'gitlab' ?
|
|
147
|
+
(async (adapterId) => adapterId === 'gitlab' ? await discoverGitLabAccess() : await discoverToken());
|
|
112
148
|
// capability마다 따로 푼다. 한 binding이 여럿을 제공해도, 서로 다른 binding이 나눠
|
|
113
149
|
// 맡아도 같은 경로로 조립된다 — 어느 갈래가 어디서 왔는지가 Port마다 정확해야 한다.
|
|
114
150
|
const wanted = Object.keys(PORT_OF);
|
|
@@ -27,3 +27,20 @@ export declare function overrideTemplate(): string;
|
|
|
27
27
|
export declare function identitiesTemplate(): string;
|
|
28
28
|
/** 이미 있으면 건드리지 않는다. 사람이 채운 것을 덮지 않기 위해서다. */
|
|
29
29
|
export declare function writeIfAbsent(path: string, content: string): Promise<boolean>;
|
|
30
|
+
/**
|
|
31
|
+
* 지금 이 사람을 승인 권한자·감시 대상으로 세운 두 파일의 내용 (P1-F).
|
|
32
|
+
*
|
|
33
|
+
* 순수 함수인 이유: 이 결정은 두 파일에 서로 다른 형식으로 적히고, 한쪽만 채워 두면
|
|
34
|
+
* 게이트가 왜 안 열리는지 알 수 없는 상태가 된다. 그 짝을 코드 한 곳에서 만든다.
|
|
35
|
+
*
|
|
36
|
+
* **비밀은 다루지 않는다** — 이름과 채널만 적힌다.
|
|
37
|
+
*/
|
|
38
|
+
export declare function withIdentity(identities: Record<string, unknown>, override: Record<string, unknown>, input: {
|
|
39
|
+
name: string;
|
|
40
|
+
actor: string;
|
|
41
|
+
controller: boolean;
|
|
42
|
+
monitor: boolean;
|
|
43
|
+
}): {
|
|
44
|
+
identities: Record<string, unknown>;
|
|
45
|
+
override: Record<string, unknown>;
|
|
46
|
+
};
|
package/dist/core/attach/init.js
CHANGED
|
@@ -98,3 +98,31 @@ export async function writeIfAbsent(path, content) {
|
|
|
98
98
|
throw error;
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* 지금 이 사람을 승인 권한자·감시 대상으로 세운 두 파일의 내용 (P1-F).
|
|
103
|
+
*
|
|
104
|
+
* 순수 함수인 이유: 이 결정은 두 파일에 서로 다른 형식으로 적히고, 한쪽만 채워 두면
|
|
105
|
+
* 게이트가 왜 안 열리는지 알 수 없는 상태가 된다. 그 짝을 코드 한 곳에서 만든다.
|
|
106
|
+
*
|
|
107
|
+
* **비밀은 다루지 않는다** — 이름과 채널만 적힌다.
|
|
108
|
+
*/
|
|
109
|
+
export function withIdentity(identities, override, input) {
|
|
110
|
+
const nextIdentities = { ...identities };
|
|
111
|
+
const nextOverride = { ...override };
|
|
112
|
+
if (input.controller) {
|
|
113
|
+
nextIdentities[input.name] = [input.actor];
|
|
114
|
+
const controller = { ...(nextOverride.controller ?? {}) };
|
|
115
|
+
controller.identities = {
|
|
116
|
+
...(controller.identities ?? {}),
|
|
117
|
+
[input.name]: [input.actor],
|
|
118
|
+
};
|
|
119
|
+
nextOverride.controller = controller;
|
|
120
|
+
}
|
|
121
|
+
if (input.monitor) {
|
|
122
|
+
const existing = Array.isArray(nextOverride.monitorIdentities)
|
|
123
|
+
? nextOverride.monitorIdentities.filter((v) => typeof v === 'string')
|
|
124
|
+
: [];
|
|
125
|
+
nextOverride.monitorIdentities = [...new Set([...existing, input.name])];
|
|
126
|
+
}
|
|
127
|
+
return { identities: nextIdentities, override: nextOverride };
|
|
128
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export declare const RUNTIME_PACKAGE = "@asc-agent/runtime";
|
|
2
2
|
export declare const BOOTSTRAP_PACKAGE = "@asc-agent/bootstrap";
|
|
3
3
|
/** runtime과 bootstrap은 초기 release에서 lockstep이다. */
|
|
4
|
-
export declare const RELEASE_VERSION = "0.
|
|
5
|
-
export declare const RUNTIME_SPEC = "@asc-agent/runtime@0.
|
|
6
|
-
export declare const BOOTSTRAP_SPEC = "@asc-agent/bootstrap@0.
|
|
4
|
+
export declare const RELEASE_VERSION = "0.3.0";
|
|
5
|
+
export declare const RUNTIME_SPEC = "@asc-agent/runtime@0.3.0";
|
|
6
|
+
export declare const BOOTSTRAP_SPEC = "@asc-agent/bootstrap@0.3.0";
|
|
7
7
|
/**
|
|
8
8
|
* 아직 설치되지 않은 machine에서 그대로 실행되는 형태 (C-14 §3.4).
|
|
9
9
|
*
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
export const RUNTIME_PACKAGE = '@asc-agent/runtime';
|
|
10
10
|
export const BOOTSTRAP_PACKAGE = '@asc-agent/bootstrap';
|
|
11
11
|
/** runtime과 bootstrap은 초기 release에서 lockstep이다. */
|
|
12
|
-
export const RELEASE_VERSION = '0.
|
|
12
|
+
export const RELEASE_VERSION = '0.3.0';
|
|
13
13
|
export const RUNTIME_SPEC = `${RUNTIME_PACKAGE}@${RELEASE_VERSION}`;
|
|
14
14
|
export const BOOTSTRAP_SPEC = `${BOOTSTRAP_PACKAGE}@${RELEASE_VERSION}`;
|
|
15
15
|
/**
|
|
@@ -5,6 +5,18 @@ import { type OwnershipMap } from '../policy/ownership.ts';
|
|
|
5
5
|
import type { Relevance } from './relevance.ts';
|
|
6
6
|
export declare const STEPS: readonly ["resource", "delta", "responsibility", "work", "thread", "change", "work-context", "canonical", "relevance", "recommendation", "draft"];
|
|
7
7
|
export type StepId = (typeof STEPS)[number];
|
|
8
|
+
/**
|
|
9
|
+
* 확인하지 못한 이유. "안 봤다"와 "볼 수 없다"가 같은 결과로 뭉개지면, 조사 누락이
|
|
10
|
+
* 접근 불가처럼 보이고 그 상태로 추천이 나간다.
|
|
11
|
+
*
|
|
12
|
+
* - `MISSING` — 통로 자체가 없어 **보지 않았다** (Port 미배선)
|
|
13
|
+
* - `UNAVAILABLE` — 보려 했으나 실패했다 (조회 오류·접근 거부·사라짐)
|
|
14
|
+
* - `NOT_APPLICABLE` — 이 사건에 해당하지 않는다
|
|
15
|
+
*
|
|
16
|
+
* 이전 판의 결과가 `done` 으로 되돌아오면 이 필드가 없다. 그때는 MISSING 으로 읽는다 —
|
|
17
|
+
* 모르는 쪽을 "확인했다"로 읽는 것보다 "안 봤다"로 읽는 편이 안전하다.
|
|
18
|
+
*/
|
|
19
|
+
export type StepReason = 'MISSING' | 'UNAVAILABLE' | 'NOT_APPLICABLE';
|
|
8
20
|
export type StepResult = {
|
|
9
21
|
id: StepId;
|
|
10
22
|
kind: 'DONE';
|
|
@@ -15,13 +27,17 @@ export type StepResult = {
|
|
|
15
27
|
id: StepId;
|
|
16
28
|
kind: 'UNDECIDABLE';
|
|
17
29
|
detail: string;
|
|
30
|
+
reason?: Extract<StepReason, 'MISSING' | 'UNAVAILABLE'>;
|
|
18
31
|
}
|
|
19
32
|
/** 이 사건에는 해당하지 않는다 (변경이 없는 사건의 change 단계 등). */
|
|
20
33
|
| {
|
|
21
34
|
id: StepId;
|
|
22
35
|
kind: 'SKIPPED';
|
|
23
36
|
detail: string;
|
|
37
|
+
reason?: Extract<StepReason, 'NOT_APPLICABLE'>;
|
|
24
38
|
};
|
|
39
|
+
/** 단계 결과를 세 상태로 읽는다. reason 이 없는 옛 결과는 보수적으로 읽는다. */
|
|
40
|
+
export declare function stepReason(step: StepResult): 'DONE' | StepReason;
|
|
25
41
|
export type Investigation = {
|
|
26
42
|
steps: StepResult[];
|
|
27
43
|
/** 사람이 읽는 상황. 단계 산출을 이어 붙인 것이며 요약하지 않는다. */
|
|
@@ -21,10 +21,19 @@ export const STEPS = [
|
|
|
21
21
|
'recommendation', // ⑩ 무엇을 하면 되는가
|
|
22
22
|
'draft', // ⑪ 초안 (조건을 만족할 때만)
|
|
23
23
|
];
|
|
24
|
+
/** 단계 결과를 세 상태로 읽는다. reason 이 없는 옛 결과는 보수적으로 읽는다. */
|
|
25
|
+
export function stepReason(step) {
|
|
26
|
+
if (step.kind === 'DONE')
|
|
27
|
+
return 'DONE';
|
|
28
|
+
if (step.kind === 'SKIPPED')
|
|
29
|
+
return step.reason ?? 'NOT_APPLICABLE';
|
|
30
|
+
return step.reason ?? 'MISSING';
|
|
31
|
+
}
|
|
24
32
|
const missingPort = (id, what) => ({
|
|
25
33
|
id,
|
|
26
34
|
kind: 'UNDECIDABLE',
|
|
27
35
|
detail: `${what} 를 제공하는 binding이 없다 — 이 단계는 확인하지 못했다`,
|
|
36
|
+
reason: 'MISSING',
|
|
28
37
|
});
|
|
29
38
|
/**
|
|
30
39
|
* 단계를 순서대로 밟는다. **각 단계는 자기가 필요한 Port만 요청한다** — 어떤 외부 시스템이
|
|
@@ -45,7 +54,7 @@ export async function investigate(input, ports, done = []) {
|
|
|
45
54
|
if (!ports.resource)
|
|
46
55
|
put(missingPort('resource', '리소스 조회'));
|
|
47
56
|
else if (!resource || resource.missing) {
|
|
48
|
-
put({ id: 'resource', kind: 'UNDECIDABLE', detail: '리소스를 읽지 못했다 (사라졌거나 접근 불가)' });
|
|
57
|
+
put({ id: 'resource', kind: 'UNDECIDABLE', detail: '리소스를 읽지 못했다 (사라졌거나 접근 불가)', reason: 'UNAVAILABLE' });
|
|
49
58
|
}
|
|
50
59
|
else {
|
|
51
60
|
put({
|
|
@@ -60,7 +69,7 @@ export async function investigate(input, ports, done = []) {
|
|
|
60
69
|
}
|
|
61
70
|
// ② Delta — 현재 모습만 보면 무엇이 새로운지 알 수 없다.
|
|
62
71
|
if (!input.previous) {
|
|
63
|
-
put({ id: 'delta', kind: 'SKIPPED', detail: '지난 관측이 없다 — 처음 보는 사건이다' });
|
|
72
|
+
put({ id: 'delta', kind: 'SKIPPED', detail: '지난 관측이 없다 — 처음 보는 사건이다', reason: 'NOT_APPLICABLE' });
|
|
64
73
|
}
|
|
65
74
|
else if (!resource) {
|
|
66
75
|
put(missingPort('delta', '리소스 조회'));
|
|
@@ -88,11 +97,11 @@ export async function investigate(input, ports, done = []) {
|
|
|
88
97
|
}
|
|
89
98
|
put(responsibility.length > 0
|
|
90
99
|
? { id: 'responsibility', kind: 'DONE', findings: responsibility }
|
|
91
|
-
: { id: 'responsibility', kind: 'SKIPPED', detail: 'owner·결정 영역이 선언되지 않았다' });
|
|
100
|
+
: { id: 'responsibility', kind: 'SKIPPED', detail: 'owner·결정 영역이 선언되지 않았다', reason: 'NOT_APPLICABLE' });
|
|
92
101
|
// ④ 지금 돌고 있는 것과의 관계.
|
|
93
102
|
put(input.activeSessions?.length
|
|
94
103
|
? { id: 'work', kind: 'DONE', findings: [`활성 세션: ${input.activeSessions.join(', ')}`] }
|
|
95
|
-
: { id: 'work', kind: 'SKIPPED', detail: '지금 돌고 있는 세션이 없다' });
|
|
104
|
+
: { id: 'work', kind: 'SKIPPED', detail: '지금 돌고 있는 세션이 없다', reason: 'NOT_APPLICABLE' });
|
|
96
105
|
// ⑤ Thread — 전부 읽지 않는다. 판단에 필요한 만큼만 (C-05 §3).
|
|
97
106
|
if (!ports.resource)
|
|
98
107
|
put(missingPort('thread', '스레드 조회'));
|
|
@@ -101,7 +110,7 @@ export async function investigate(input, ports, done = []) {
|
|
|
101
110
|
.getComments(input.reference, { limit: input.commentLimit ?? 5 })
|
|
102
111
|
.catch(() => null);
|
|
103
112
|
put(comments === null
|
|
104
|
-
? { id: 'thread', kind: 'UNDECIDABLE', detail: '논의를 읽지 못했다' }
|
|
113
|
+
? { id: 'thread', kind: 'UNDECIDABLE', detail: '논의를 읽지 못했다', reason: 'UNAVAILABLE' }
|
|
105
114
|
: {
|
|
106
115
|
id: 'thread',
|
|
107
116
|
kind: 'DONE',
|
|
@@ -128,7 +137,7 @@ export async function investigate(input, ports, done = []) {
|
|
|
128
137
|
}
|
|
129
138
|
// ⑦ Work Context — 작업 항목의 상태·연결. 코드 쪽과 다른 Binding일 수 있다.
|
|
130
139
|
if (!input.workReference) {
|
|
131
|
-
put({ id: 'work-context', kind: 'SKIPPED', detail: '연결된 작업 항목이 선언되지 않았다' });
|
|
140
|
+
put({ id: 'work-context', kind: 'SKIPPED', detail: '연결된 작업 항목이 선언되지 않았다', reason: 'NOT_APPLICABLE' });
|
|
132
141
|
}
|
|
133
142
|
else if (!ports.work) {
|
|
134
143
|
put(missingPort('work-context', '작업 항목 조회'));
|
|
@@ -136,7 +145,7 @@ export async function investigate(input, ports, done = []) {
|
|
|
136
145
|
else {
|
|
137
146
|
const item = await ports.work.getResource(input.workReference).catch(() => null);
|
|
138
147
|
if (!item || item.missing) {
|
|
139
|
-
put({ id: 'work-context', kind: 'UNDECIDABLE', detail: '작업 항목을 읽지 못했다' });
|
|
148
|
+
put({ id: 'work-context', kind: 'UNDECIDABLE', detail: '작업 항목을 읽지 못했다', reason: 'UNAVAILABLE' });
|
|
140
149
|
}
|
|
141
150
|
else {
|
|
142
151
|
// 경위는 있으면 더한다. 없다고 이 단계 전체가 무너지지는 않는다 —
|
|
@@ -163,7 +172,7 @@ export async function investigate(input, ports, done = []) {
|
|
|
163
172
|
else {
|
|
164
173
|
const snapshots = await ports.baselines().catch(() => null);
|
|
165
174
|
if (!snapshots)
|
|
166
|
-
put({ id: 'canonical', kind: 'UNDECIDABLE', detail: '정본을 읽지 못했다' });
|
|
175
|
+
put({ id: 'canonical', kind: 'UNDECIDABLE', detail: '정본을 읽지 못했다', reason: 'UNAVAILABLE' });
|
|
167
176
|
else {
|
|
168
177
|
const touched = (change?.changedPaths ?? []).filter((path) => input.canonicalPaths?.length && isWithinScopes(path, input.canonicalPaths));
|
|
169
178
|
canonicalConflict = touched.length > 0;
|
|
@@ -184,7 +193,7 @@ export async function investigate(input, ports, done = []) {
|
|
|
184
193
|
kind: 'DONE',
|
|
185
194
|
findings: input.relevance.evidence.map((e) => `${e.supports ? '+' : '-'} ${e.detail}`),
|
|
186
195
|
}
|
|
187
|
-
: { id: 'relevance', kind: 'SKIPPED', detail: '관련성 판정을 받지 못했다' });
|
|
196
|
+
: { id: 'relevance', kind: 'SKIPPED', detail: '관련성 판정을 받지 못했다', reason: 'NOT_APPLICABLE' });
|
|
188
197
|
const undecidable = steps
|
|
189
198
|
.filter((s) => s.kind === 'UNDECIDABLE')
|
|
190
199
|
.map((s) => `${s.id}: ${s.detail}`);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { EscalationPredicate } from '../runtime/escalation.ts';
|
|
3
|
+
import { type OwnershipMap } from '../policy/ownership.ts';
|
|
4
|
+
import { type PreflightResult } from './preflight.ts';
|
|
5
|
+
import type { ResolvedPolicy } from '../policy/policy.ts';
|
|
6
|
+
/**
|
|
7
|
+
* 이 값이 어디서 왔는가. **claims.ts의 CONFIRMED/INFERRED/PENDING과 같은 축이다** —
|
|
8
|
+
* 이름만 계약 초안의 어휘로 부른다 (agent가 읽는 문서에서 "추론"보다 "PROPOSAL"이
|
|
9
|
+
* 무엇을 해야 하는지 더 곧게 말한다).
|
|
10
|
+
*/
|
|
11
|
+
export type DraftStatus =
|
|
12
|
+
/** 사용자·정본·work item·Profile에서 직접 확인된 값. 뒤집으려면 그 출처를 봐야 한다. */
|
|
13
|
+
'FACT'
|
|
14
|
+
/** agent가 근거를 갖고 제안한 값. 근거가 충분하고 경계 안이면 그대로 진행해도 된다. */
|
|
15
|
+
| 'PROPOSAL'
|
|
16
|
+
/** agent가 확정해서는 안 되는 값. 사람의 경계다. */
|
|
17
|
+
| 'DECISION_REQUIRED';
|
|
18
|
+
/** 값이 어디서 왔는지. 지어낸 것과 읽어 온 것을 문자열 하나로 구분한다. */
|
|
19
|
+
export declare const DRAFT_SOURCES: readonly ["user", "work_item", "profile", "repository", "canonical", "agent_proposal"];
|
|
20
|
+
export type DraftSource = (typeof DRAFT_SOURCES)[number];
|
|
21
|
+
/** 초안의 출처 한 줄. CLI가 받는 문자열도 이 스키마로 통과시켜야 들어온다. */
|
|
22
|
+
export declare const DraftProvenance: z.ZodObject<{
|
|
23
|
+
field: z.ZodEnum<["id", "role", "goal", "boundary", "criteria", "owner"]>;
|
|
24
|
+
status: z.ZodEnum<["FACT", "PROPOSAL", "DECISION_REQUIRED"]>;
|
|
25
|
+
source: z.ZodEnum<["user", "work_item", "profile", "repository", "canonical", "agent_proposal"]>;
|
|
26
|
+
/** 왜 이 값인가. PROPOSAL이면 반드시 있어야 한다 — 근거 없는 제안은 추측이다. */
|
|
27
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
28
|
+
}, "strip", z.ZodTypeAny, {
|
|
29
|
+
status: "FACT" | "PROPOSAL" | "DECISION_REQUIRED";
|
|
30
|
+
source: "canonical" | "profile" | "user" | "work_item" | "repository" | "agent_proposal";
|
|
31
|
+
field: "id" | "role" | "goal" | "owner" | "boundary" | "criteria";
|
|
32
|
+
reason?: string | undefined;
|
|
33
|
+
}, {
|
|
34
|
+
status: "FACT" | "PROPOSAL" | "DECISION_REQUIRED";
|
|
35
|
+
source: "canonical" | "profile" | "user" | "work_item" | "repository" | "agent_proposal";
|
|
36
|
+
field: "id" | "role" | "goal" | "owner" | "boundary" | "criteria";
|
|
37
|
+
reason?: string | undefined;
|
|
38
|
+
}>;
|
|
39
|
+
export type DraftField = z.infer<typeof DraftProvenance>;
|
|
40
|
+
/** agent가 만들어 오는 것. 완성된 계약이 아니라 **초안**이다. */
|
|
41
|
+
export type SessionContractDraft = {
|
|
42
|
+
id?: string;
|
|
43
|
+
role?: string;
|
|
44
|
+
goal?: string;
|
|
45
|
+
boundary?: readonly string[];
|
|
46
|
+
criteria?: readonly string[];
|
|
47
|
+
owner?: string;
|
|
48
|
+
decisionDomains?: readonly string[];
|
|
49
|
+
decisionAuthority?: Readonly<Record<string, string>>;
|
|
50
|
+
/** 각 값의 출처. 없는 필드는 `agent_proposal` 로 간주하지 않고 **출처 미상**으로 본다. */
|
|
51
|
+
provenance?: readonly DraftField[];
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* 사람이 답해야 하는 것 하나. **빠진 정보 전부가 아니라 결정 지점 하나씩** 든다 —
|
|
55
|
+
* 목록이 길면 사람은 그것을 질문이 아니라 서식으로 읽는다.
|
|
56
|
+
*/
|
|
57
|
+
export type UnresolvedDecision = {
|
|
58
|
+
field: DraftField['field'];
|
|
59
|
+
/** 왜 사람인가. escalation predicate와 같은 어휘를 쓴다 (C-13). */
|
|
60
|
+
reason: (typeof EscalationPredicate)['_type'] | 'missing_input' | 'multiple_options';
|
|
61
|
+
detail: string;
|
|
62
|
+
/** 고를 수 있는 것들. 하나뿐이면 질문이 아니라 제안이다. */
|
|
63
|
+
options?: string[];
|
|
64
|
+
/** 추천 인덱스. 추천 없이 선택지만 주는 것은 판단을 떠넘기는 것이다. */
|
|
65
|
+
recommended?: number;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* 발급 권한은 **사람의 것이다** (OM §450). 다만 그 문장이 "매번 사람이 직접 쳐야 한다"는
|
|
69
|
+
* 뜻은 아니다 — Controller가 **범위를 정해 위임**할 수 있고, 그 범위 안에서만 agent가
|
|
70
|
+
* 스스로 발급한다. 위임이 없으면 계약이 완성돼도 발급하지 않고 사람에게 넘긴다.
|
|
71
|
+
*
|
|
72
|
+
* 위임은 Profile/Override의 `policy.unionLists.issuanceDelegation` 에 **역할 이름**으로
|
|
73
|
+
* 적는다 (closureChecklist와 같은 관례 키 자리). 경로 범위는 이미 `roleScopes` 가 좁히고
|
|
74
|
+
* 있으므로 여기서 다시 정의하지 않는다 — 권한을 두 곳에 적으면 둘이 갈라진다.
|
|
75
|
+
*/
|
|
76
|
+
export declare const ISSUANCE_DELEGATION_KEY = "issuanceDelegation";
|
|
77
|
+
export type IssuanceAuthority = {
|
|
78
|
+
/** `controller` = 사람이 발급한다. `delegated` = 이 역할에 한해 agent가 발급해도 된다. */
|
|
79
|
+
authority: 'controller' | 'delegated';
|
|
80
|
+
/** Controller가 위임한 역할들. 비어 있으면 위임이 없다는 뜻이다. */
|
|
81
|
+
delegatedRoles: string[];
|
|
82
|
+
detail: string;
|
|
83
|
+
};
|
|
84
|
+
export type ContractPlanStatus =
|
|
85
|
+
/** 지금 발급해도 된다. 사람에게 물을 것이 없다. */
|
|
86
|
+
'READY_TO_ISSUE'
|
|
87
|
+
/** 구조는 맞는데 사람만 정할 수 있는 것이 남았다. */
|
|
88
|
+
| 'NEEDS_DECISION'
|
|
89
|
+
/** 이 초안으로는 계약이 성립하지 않는다 — 문법·범위가 틀렸다. */
|
|
90
|
+
| 'INVALID';
|
|
91
|
+
export type SessionContractPlan = {
|
|
92
|
+
status: ContractPlanStatus;
|
|
93
|
+
draft: SessionContractDraft;
|
|
94
|
+
/** 출처가 확인된 값들. */
|
|
95
|
+
facts: DraftField[];
|
|
96
|
+
/** 근거를 갖고 제안된 값들. */
|
|
97
|
+
proposals: DraftField[];
|
|
98
|
+
unresolved: UnresolvedDecision[];
|
|
99
|
+
/** 계약이 완성돼도 **발급해도 되는가**는 별개다 (OM §450). */
|
|
100
|
+
issuance: IssuanceAuthority;
|
|
101
|
+
/** 구조가 깨진 지점. 있으면 status는 INVALID다. */
|
|
102
|
+
invalid: {
|
|
103
|
+
field: string;
|
|
104
|
+
detail: string;
|
|
105
|
+
}[];
|
|
106
|
+
/** 경로·책임 축 판정 원본. 있는 그대로 실어 agent가 다시 계산하지 않게 한다. */
|
|
107
|
+
preflight?: PreflightResult;
|
|
108
|
+
};
|
|
109
|
+
export type ContractPlanInput = {
|
|
110
|
+
draft: SessionContractDraft;
|
|
111
|
+
policy?: ResolvedPolicy;
|
|
112
|
+
ownership?: OwnershipMap;
|
|
113
|
+
/** 이미 있는 세션 id들. 겹치면 발급이 실패하므로 미리 말한다. */
|
|
114
|
+
existingIds?: readonly string[];
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* 초안을 잰다. **아무것도 쓰지 않는다** — 순수 함수이고, 같은 입력이면 같은 답이다.
|
|
118
|
+
*
|
|
119
|
+
* 판정 순서에 뜻이 있다: 먼저 구조(문법·필수값)를 보고, 그 다음 경계(범위·책임)를 본다.
|
|
120
|
+
* 구조가 깨진 초안의 경계를 논하는 것은 의미가 없고, 사람에게 두 번 묻게 만든다.
|
|
121
|
+
*/
|
|
122
|
+
export declare function planSessionContract(input: ContractPlanInput): SessionContractPlan;
|
|
123
|
+
/** 발급 명령의 인자. plan이 통과했을 때만 부른다 — 통과하지 않은 초안은 명령이 되지 않는다. */
|
|
124
|
+
export declare function issueArgs(draft: SessionContractDraft): string[];
|