@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.
Files changed (40) hide show
  1. package/README.md +25 -25
  2. package/dist/adapters/claude-code/skill.js +245 -239
  3. package/dist/adapters/gitlab/adapter.d.ts +3 -0
  4. package/dist/adapters/gitlab/adapter.js +22 -3
  5. package/dist/adapters/gitlab/client.d.ts +27 -1
  6. package/dist/adapters/gitlab/client.js +30 -0
  7. package/dist/adapters/gitlab/ports.d.ts +3 -3
  8. package/dist/adapters/jam/adapter.d.ts +14 -0
  9. package/dist/adapters/jam/adapter.js +84 -6
  10. package/dist/adapters/jam/ports.d.ts +9 -0
  11. package/dist/adapters/jam/ports.js +49 -2
  12. package/dist/adapters/local/repo.d.ts +15 -0
  13. package/dist/adapters/local/repo.js +179 -0
  14. package/dist/adapters/markdown/state-store.js +13 -1
  15. package/dist/adapters/memory/state-store.js +4 -0
  16. package/dist/cli/asc.js +452 -4
  17. package/dist/composition/propose.d.ts +19 -0
  18. package/dist/composition/propose.js +35 -0
  19. package/dist/composition/runtime.d.ts +2 -0
  20. package/dist/composition/runtime.js +41 -5
  21. package/dist/core/attach/init.d.ts +17 -0
  22. package/dist/core/attach/init.js +28 -0
  23. package/dist/core/distribution/release.d.ts +3 -3
  24. package/dist/core/distribution/release.js +1 -1
  25. package/dist/core/monitor/investigation.d.ts +16 -0
  26. package/dist/core/monitor/investigation.js +18 -9
  27. package/dist/core/operator/contract-draft.d.ts +124 -0
  28. package/dist/core/operator/contract-draft.js +234 -0
  29. package/dist/core/operator/derive-draft.d.ts +37 -0
  30. package/dist/core/operator/derive-draft.js +216 -0
  31. package/dist/core/operator/proceed.d.ts +79 -0
  32. package/dist/core/operator/proceed.js +123 -1
  33. package/dist/core/operator/work-state.d.ts +54 -0
  34. package/dist/core/operator/work-state.js +145 -0
  35. package/dist/core/runtime/closure.d.ts +2 -2
  36. package/dist/ports/local-repo.d.ts +61 -0
  37. package/dist/ports/local-repo.js +9 -0
  38. package/dist/ports/resource-context.d.ts +7 -0
  39. package/dist/schemas/profile.d.ts +2 -2
  40. package/package.json +2 -2
package/dist/cli/asc.js CHANGED
@@ -9,9 +9,9 @@
9
9
  import { execFile, spawnSync } from 'node:child_process';
10
10
  import { parseArgs, promisify } from 'node:util';
11
11
  import { existsSync, readdirSync, realpathSync } from 'node:fs';
12
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
12
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
13
13
  import { fileURLToPath } from 'node:url';
14
- import { homedir } from 'node:os';
14
+ import { homedir, userInfo } from 'node:os';
15
15
  import { basename, dirname, join, resolve } from 'node:path';
16
16
  import { MINIMUM_NODE_MAJOR, checkNodeRuntime, } from "../core/distribution/node-runtime.js";
17
17
  import { GitHubClient, discoverToken } from "../adapters/github/client.js";
@@ -20,6 +20,7 @@ import { GitHubEventSource } from "../adapters/github/event-source.js";
20
20
  import { GitHubScm } from "../adapters/github/scm.js";
21
21
  import { MarkdownStateStore } from "../adapters/markdown/state-store.js";
22
22
  import { LocalIdentityBinding } from "../adapters/local/identity.js";
23
+ import { IDENTITY_FILE } from "./identity-config.js";
23
24
  import { TextRenderer } from "../adapters/text/renderer.js";
24
25
  import { ApprovalService } from "../core/approval/service.js";
25
26
  import { Executor } from "../core/execution/executor.js";
@@ -32,6 +33,7 @@ import { adoptionLine, judgeAdoption, migrate } from "../core/workspace/migrate.
32
33
  import { newWorkspaceId, normalizeRemote, recoverCandidates, recoverLines } from "../core/workspace/identity.js";
33
34
  import { resolveWorkspace, resolutionLine } from "../core/workspace/resolve.js";
34
35
  import { assessSetup, renderSetup } from "../core/attach/setup.js";
36
+ import { withIdentity } from "../core/attach/init.js";
35
37
  import { applySetupPlan, computeSetupPlan, renderSetupPlan, } from "../core/attach/setup-plan.js";
36
38
  import { CLAUDE_PROVIDER, CLAUDE_SCOPE, claudeBindings } from "../adapters/claude-code/binding.js";
37
39
  import { readHeartbeat } from "../adapters/claude-code/observer.js";
@@ -42,15 +44,23 @@ import { MonitorEngine } from "../core/monitor/engine.js";
42
44
  import { CoverageLedger, renderHealth } from "../core/monitor/coverage.js";
43
45
  import { evaluateHealth, healthAlertLines } from "../core/monitor/health-alerts.js";
44
46
  import { Operator } from "../core/operator/proceed.js";
47
+ import { deriveSessionContractDraft } from "../core/operator/derive-draft.js";
48
+ import { LocalRepoAdapter } from "../adapters/local/repo.js";
49
+ import { GitHubAdapter } from "../adapters/github/adapter.js";
50
+ import { GitLabAdapter } from "../adapters/gitlab/adapter.js";
51
+ import { JamAdapter } from "../adapters/jam/adapter.js";
52
+ import { statusIndicatesDone } from "../adapters/jam/ports.js";
45
53
  import { ProgressService } from "../core/operator/progress.js";
46
54
  import { composeBindings, defaultAdapters } from "../composition/registry.js";
47
- import { buildRuntimePorts, rolesFor } from "../composition/runtime.js";
55
+ import { buildRuntimePorts, closeToolClients, rolesFor } from "../composition/runtime.js";
56
+ import { proposeBindings } from "../composition/propose.js";
48
57
  import { buildEventObservation } from "../composition/observe.js";
49
58
  import { availableProfiles, planBootstrap, renderPlan } from "../core/attach/bootstrap.js";
50
59
  import { detectStableInstall, installStableRuntime, verifyStableInstall, } from "../core/distribution/runtime-install.js";
51
60
  import { readRuntimeSelection, remediationAction, remediationLines, resolveRuntimeTarget, runtimeSelectionLine, selectionPath, writeRuntimeSelection, } from "../core/distribution/runtime-select.js";
52
61
  import { portableCommand, shorthandCommand } from "../core/distribution/release.js";
53
62
  import { preflight } from "../core/operator/preflight.js";
63
+ import { DraftProvenance, issueArgs, planSessionContract, } from "../core/operator/contract-draft.js";
54
64
  import { lookupAuthority } from "../core/policy/ownership.js";
55
65
  import { renderProgress } from "../core/operator/render.js";
56
66
  import { AuditLedger, decisionLines, delegationLine, executionLines, reclaimLine, validationLines, } from "../core/runtime/audit.js";
@@ -76,7 +86,7 @@ import { LocalOperator } from "../core/operator/local-operator.js";
76
86
  import { loadIdentityMap } from "./identity-config.js";
77
87
  const USAGE = `asc — Agent Session Control
78
88
 
79
- asc proceed [--session <id>] [--goal <text>] [--json]
89
+ asc proceed [--session <id>] [--work <WORK-ID>] [--goal <text>] [--json]
80
90
 
81
91
  asc inbox list [--all] [--priority P0|P1|P2] [--json]
82
92
  asc inbox show <REQUEST_ID> [--json]
@@ -121,11 +131,16 @@ const USAGE = `asc — Agent Session Control
121
131
  # without --profile: report what was detected, then stop
122
132
 
123
133
  asc setup status [--json]
134
+ asc setup identity [--role controller|monitor|both] [--actor <channel:actor>]
135
+ # 지금 이 사람을 승인 권한자로 세우고 재고정까지 한다
124
136
  asc setup plan [--profile <id>] [--scope local|project] [--json]
125
137
  # says what it would change — changes nothing
126
138
  asc setup apply [--profile <id>] [--scope local|project] [--json]
127
139
  asc setup apply --json # non-interactive apply. stdout is a single JSON document
128
140
 
141
+ asc session plan [--id <S-ID>] [--role <role>] [--goal <text>] [--boundary <glob>...]
142
+ [--criteria <text>...] [--owner <role>] [--provenance <f>=<STATUS>[:<src>]...]
143
+ [--json] # is this draft issuable? changes nothing
129
144
  asc session issue <ID> --role <role> --goal <text> [--block <id>]
130
145
  [--parent <S-ID>] [--issued-by <principal>]
131
146
  asc session pause <S-ID> --position <t> --next <t> [--physical <id>]
@@ -189,6 +204,8 @@ Options
189
204
  --write actually write the artefacts (default: preview)
190
205
  --role planner|researcher|implementer|verifier
191
206
  --goal the single goal of this session
207
+ --work work item to investigate before proposing a contract (asc proceed)
208
+ --actor who you are, as <channel>:<actor> (asc setup identity)
192
209
  --boundary write scope (must be narrower than the Profile's)
193
210
  --exception SOFT DENY item allowed for this session only
194
211
  --criteria a verifiable done-criterion (repeatable)
@@ -410,6 +427,8 @@ export async function runAscCommand(argv, entry = 'runtime') {
410
427
  ownership: { type: 'string', multiple: true },
411
428
  verification: { type: 'string', multiple: true },
412
429
  why: { type: 'string', multiple: true },
430
+ /** 초안의 출처 — `<field>=<FACT|PROPOSAL|DECISION_REQUIRED>[:<source>]` (session plan). */
431
+ provenance: { type: 'string', multiple: true },
413
432
  offline: { type: 'boolean', default: false },
414
433
  id: { type: 'string' },
415
434
  intent: { type: 'string' },
@@ -458,6 +477,8 @@ export async function runAscCommand(argv, entry = 'runtime') {
458
477
  body: { type: 'string' },
459
478
  to: { type: 'string' },
460
479
  session: { type: 'string' },
480
+ work: { type: 'string' },
481
+ actor: { type: 'string' },
461
482
  position: { type: 'string' },
462
483
  next: { type: 'string' },
463
484
  done: { type: 'string', multiple: true },
@@ -1075,6 +1096,8 @@ async function runSetup(command, values, entry = 'runtime') {
1075
1096
  // plan/apply는 같은 판단을 나눠 쓴다 (C-14 §6). `--agent` 는 apply의 비대화 형태다.
1076
1097
  if (command === 'plan' || command === 'apply')
1077
1098
  return runSetupLifecycle(command, values, entry);
1099
+ if (command === 'identity')
1100
+ return runSetupIdentity(values);
1078
1101
  if (values.agent)
1079
1102
  return runSetupLifecycle('apply', values, entry);
1080
1103
  if (command !== undefined && command !== 'status') {
@@ -1173,6 +1196,104 @@ const nodeProcessRunner = async (command, args) => {
1173
1196
  * 사람이 보는 줄과 agent가 파싱하는 JSON은 **같은 plan**에서 나온다 (C-14 불변식 ①).
1174
1197
  * agent 경로의 stdout은 JSON 문서 하나뿐이고, 그 밖의 말은 stderr로 간다 (§7).
1175
1198
  */
1199
+ /**
1200
+ * `asc setup identity` — 지금 이 사람을 이 workspace 의 승인 권한자로 세운다 (P1-F).
1201
+ *
1202
+ * 결정은 사람이 하고(이 명령을 실행하는 것이 그 결정이다), **파일 편집은 ASC 가 한다.**
1203
+ * 그 전까지는 identities.json 과 override.json 을 손으로 고치는 것이 유일한 길이었고,
1204
+ * 그 둘은 서로 다른 형식이라 한쪽만 채워 놓고 왜 안 열리는지 모르는 상태가 흔했다.
1205
+ *
1206
+ * 비밀은 읽지도 쓰지도 않는다. 여기서 다루는 것은 이름과 채널뿐이다.
1207
+ */
1208
+ async function runSetupIdentity(values) {
1209
+ const resolution = await resolveRoot(process.cwd(), values.root);
1210
+ if (resolution.kind === 'UNRESOLVED') {
1211
+ console.error('Not attached yet — run `asc init --profile <id>` first.');
1212
+ return 2;
1213
+ }
1214
+ const root = resolution.root;
1215
+ const explicit = values.actor;
1216
+ const candidate = explicit ?? (await detectSelf());
1217
+ if (!candidate) {
1218
+ console.error('Could not tell who you are here. Pass one: --actor local:<name>');
1219
+ return 2;
1220
+ }
1221
+ const actor = candidate.includes(':') ? candidate : `local:${candidate}`;
1222
+ const name = actor.slice(actor.indexOf(':') + 1);
1223
+ const roles = values.role ?? 'both';
1224
+ if (!['controller', 'monitor', 'both'].includes(roles)) {
1225
+ console.error("--role is controller|monitor|both");
1226
+ return 2;
1227
+ }
1228
+ const asController = roles !== 'monitor';
1229
+ const asMonitor = roles !== 'controller';
1230
+ // 어느 Profile 로 재고정할지는 lock 파일에서 읽는다. attachment 판정을 쓰면 안 되는 이유는
1231
+ // 이 명령 자신이 drift 를 만들기 때문이다 — 한 번 실패하면 그 다음부터는 자기가 닫아야 할
1232
+ // drift 때문에 profile 을 못 읽어 영영 못 닫는다.
1233
+ const attachedProfile = values.profile ?? (await lockedProfileId(root));
1234
+ if (!attachedProfile) {
1235
+ console.error('붙어 있는 Profile 을 알 수 없다 — `asc setup status` 를 보고, 필요하면 --profile 로 지목하라.');
1236
+ return 1;
1237
+ }
1238
+ const identitiesPath = join(root, IDENTITY_FILE);
1239
+ const overridePath = join(root, 'override.json');
1240
+ const merged = withIdentity(await readJson(identitiesPath), await readJson(overridePath), {
1241
+ name,
1242
+ actor,
1243
+ controller: asController,
1244
+ monitor: asMonitor,
1245
+ });
1246
+ await writeJson(identitiesPath, merged.identities);
1247
+ await writeJson(overridePath, merged.override);
1248
+ console.log(`${name} — ${actor}`);
1249
+ console.log(` identities.json ${asController ? 'approver 등록' : '건드리지 않음'}`);
1250
+ console.log(` override.json ${asController ? 'controller.identities' : ''}${asController && asMonitor ? ' · ' : ''}${asMonitor ? 'monitorIdentities' : ''}`);
1251
+ // override 는 lock digest 에 들어간다 — 고친 뒤 재고정하지 않으면 다음 명령이 멈춘다.
1252
+ // 어느 Profile 로 재고정할지는 지금 붙어 있는 것이 답이다 — 사람에게 다시 묻지 않는다.
1253
+ const relocked = await runProfile('resolve', { ...values, profile: attachedProfile, write: true }, root);
1254
+ if (relocked !== 0)
1255
+ return relocked;
1256
+ console.log('');
1257
+ console.log(renderSetup(await inspectSetup(root)));
1258
+ return 0;
1259
+ }
1260
+ /** 지금 붙어 있는 Profile id. lock 이 어긋나 있어도 읽힌다 — 파일에 그대로 남아 있다. */
1261
+ async function lockedProfileId(root) {
1262
+ try {
1263
+ const lock = JSON.parse(await readFile(join(root, 'profile.lock'), 'utf8'));
1264
+ return lock.profile?.id;
1265
+ }
1266
+ catch {
1267
+ return undefined;
1268
+ }
1269
+ }
1270
+ /** git·계정에서 "지금 이 사람"의 이름만 읽는다. 자격 값은 읽지 않는다. */
1271
+ async function detectSelf() {
1272
+ const fromGit = await execText('git', ['config', 'user.name']);
1273
+ if (fromGit)
1274
+ return fromGit;
1275
+ const user = userInfo().username;
1276
+ return user || null;
1277
+ }
1278
+ async function execText(command, args) {
1279
+ try {
1280
+ const { stdout } = await execFileAsync(command, args);
1281
+ return stdout.trim() || null;
1282
+ }
1283
+ catch {
1284
+ return null;
1285
+ }
1286
+ }
1287
+ async function readJson(path) {
1288
+ try {
1289
+ const parsed = JSON.parse(await readFile(path, 'utf8'));
1290
+ return parsed && typeof parsed === 'object' ? parsed : {};
1291
+ }
1292
+ catch {
1293
+ return {};
1294
+ }
1295
+ }
1296
+ const writeJson = (path, value) => writeFile(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
1176
1297
  async function runSetupLifecycle(command, values, entry) {
1177
1298
  const asJson = Boolean(values.json) || Boolean(values.agent);
1178
1299
  const plan = computeSetupPlan(await detectSetupState(values, entry));
@@ -1713,11 +1834,18 @@ async function runProceed(values, store, root, resolved) {
1713
1834
  canonicalSources: (resolved?.canonicalSources ?? []).map((sourceId) => ({ sourceId })),
1714
1835
  ...(resolved?.ownership ? { ownership: resolved.ownership } : {}),
1715
1836
  });
1837
+ const workRef = values.work ?? undefined;
1838
+ const ingress = workRef ? await buildWorkIngress(store, root, sessions, resolved) : undefined;
1839
+ if (workRef && !ingress) {
1840
+ console.error(`작업 항목 '${workRef}' 을 읽을 통로가 없다 — Profile bindings 에 작업 항목 provider 를 선언하라.`);
1841
+ return 2;
1842
+ }
1716
1843
  const operator = new Operator({
1717
1844
  store,
1718
1845
  sessions,
1719
1846
  // 막힌 node만 보고 판단한다 — checkpoint를 발행했다는 이유로 멈추지 않는다 (C-13 §3.1)
1720
1847
  escalations: escalationLedger(store),
1848
+ ...(ingress ? { ingress } : {}),
1721
1849
  // main()의 checkBootstrap과 같은 원천(bootstrapGuard)이다 — 중복 판단이 아니라 같은 문
1722
1850
  guard: async () => {
1723
1851
  const outcome = await checkBootstrap(root);
@@ -1727,7 +1855,10 @@ async function runProceed(values, store, root, resolved) {
1727
1855
  const outcome = await operator.proceed({
1728
1856
  ...(values.session ? { sessionId: values.session } : {}),
1729
1857
  ...(values.goal ? { goal: values.goal } : {}),
1858
+ ...(workRef ? { workRef } : {}),
1730
1859
  });
1860
+ // 도구 자식(JAM MCP 서버 등)을 여기서 닫는다 — 안 닫으면 출력까지 끝내고도 종료하지 못한다.
1861
+ await closeToolClients();
1731
1862
  if (values.json) {
1732
1863
  console.log(JSON.stringify(outcome, null, 2));
1733
1864
  return outcome.kind.startsWith('BLOCKED') || outcome.kind === 'FAILED' ? 1 : 0;
@@ -1781,7 +1912,39 @@ async function runProceed(values, store, root, resolved) {
1781
1912
  console.log(` ${c.id} ${c.status.padEnd(7)} ${c.wouldDo.padEnd(8)} ${c.goal}`);
1782
1913
  }
1783
1914
  return 1;
1915
+ case 'WORK_STATE': {
1916
+ const shown = outcome.result.leaning
1917
+ ? `${outcome.result.state} (${outcome.result.leaning})`
1918
+ : outcome.result.state;
1919
+ console.log(`${outcome.workRef}: ${shown}`);
1920
+ for (const line of outcome.result.evidence)
1921
+ console.log(` 근거 ${line}`);
1922
+ for (const line of outcome.result.limitations)
1923
+ console.log(` 한계 ${line}`);
1924
+ for (const line of outcome.result.missing)
1925
+ console.log(` 미확인 ${line}`);
1926
+ console.log(`\n다음 행동: ${outcome.nextAction}`);
1927
+ return outcome.result.state === 'UNDECIDABLE' ? 1 : 0;
1928
+ }
1784
1929
  case 'PROPOSE_CONTRACT':
1930
+ if (outcome.plan) {
1931
+ console.log(`${outcome.plan.status}${outcome.full?.id ? ` — ${outcome.full.id}` : ''}`);
1932
+ for (const fact of outcome.plan.facts)
1933
+ console.log(` fact ${fact.field} (${fact.source})`);
1934
+ for (const proposal of outcome.plan.proposals) {
1935
+ console.log(` proposal ${proposal.field} — ${proposal.reason ?? proposal.source}`);
1936
+ }
1937
+ for (const item of outcome.plan.invalid)
1938
+ console.log(` invalid ${item.field}: ${item.detail}`);
1939
+ for (const item of outcome.plan.unresolved) {
1940
+ console.log(` decide ${item.field} [${item.reason}]: ${item.detail}`);
1941
+ }
1942
+ if (outcome.forController) {
1943
+ console.log(`\n계약은 성립한다. 발급은 Controller 의 것이다 — ${outcome.plan.issuance.detail}:`);
1944
+ console.log(` ${shorthandCommand(outcome.forController.slice(1))}`);
1945
+ }
1946
+ return outcome.plan.status === 'READY_TO_ISSUE' ? 0 : 1;
1947
+ }
1785
1948
  console.log('No runnable session. If a new contract is needed, the Controller issues it:');
1786
1949
  console.log(` asc session issue S-<date>-<n> --role ${outcome.draft.role} --goal "${outcome.draft.goal || '<goal>'}"`);
1787
1950
  return 1;
@@ -1806,6 +1969,199 @@ async function runProceed(values, store, root, resolved) {
1806
1969
  return 1;
1807
1970
  }
1808
1971
  }
1972
+ /**
1973
+ * 이미 쓴 세션 id. **회수돼 보관된 것까지 센다** — 그 번호를 다시 쓰면 앞의 계약 기록
1974
+ * 위에 다른 계약이 앉는다. 실제로 그렇게 기록 하나를 잃었다.
1975
+ */
1976
+ async function usedSessionIds(store, root) {
1977
+ const active = (await store.list('session')).map((session) => session.id);
1978
+ let archived = [];
1979
+ try {
1980
+ archived = (await readdir(join(root, 'sessions', 'archive')))
1981
+ .filter((name) => name.endsWith('.md'))
1982
+ .map((name) => name.slice(0, -3));
1983
+ }
1984
+ catch {
1985
+ archived = [];
1986
+ }
1987
+ return [...new Set([...active, ...archived])];
1988
+ }
1989
+ /**
1990
+ * 저장소의 최상위 자리와 그 아래 `src`. 분류 이름이 어느 모듈에 맞는지 재는 재료다 —
1991
+ * 목록일 뿐이고, 이것이 쓰기 범위가 되지는 않는다 (derive 가 최상위는 범위로 쓰지 않는다).
1992
+ */
1993
+ async function topLevelModules(projectRoot) {
1994
+ try {
1995
+ const entries = await readdir(projectRoot, { withFileTypes: true });
1996
+ return entries
1997
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules')
1998
+ .flatMap((entry) => [entry.name, `${entry.name}/src`]);
1999
+ }
2000
+ catch {
2001
+ return [];
2002
+ }
2003
+ }
2004
+ /** 선행·의존으로 볼 후보. 링크가 먼저고, 본문이 말한 키가 그다음이다. 상한은 5. */
2005
+ const DEPENDENCY_CAP = 5;
2006
+ function dependencyCandidates(workItem) {
2007
+ if (!workItem)
2008
+ return [];
2009
+ // 본문이 "BLOCKED BY: KEY-1" 처럼 말한 것은 링크가 없어도 의존이다 — 링크가 없다는
2010
+ // 사실이 의존이 없다는 뜻은 아니다.
2011
+ const fromBody = new Set();
2012
+ const project = workItem.reference.split('-')[0];
2013
+ if (project) {
2014
+ const pattern = new RegExp(`(?:blocked\\s*by|선행|의존)[^\\n]*?(${project}-\\d+)`, 'gi');
2015
+ for (const [, key] of `${workItem.body ?? ''}`.matchAll(pattern))
2016
+ if (key)
2017
+ fromBody.add(key);
2018
+ }
2019
+ // 부모·하위 작업은 포함 관계이지 선행이 아니다 — 그것까지 세면 거의 모든 작업이 막힌다.
2020
+ const ordered = [...(workItem.blockedBy ?? []), ...fromBody].filter((key) => key !== workItem.reference);
2021
+ return [...new Set(ordered)].slice(0, DEPENDENCY_CAP);
2022
+ }
2023
+ /**
2024
+ * 작업 항목 하나를 조사해 계약까지 잇는 통로 (P0-D).
2025
+ *
2026
+ * 여기서 판정하지 않는다. 모으고(gather·observeRepo), 도출하고(derive), **기존 판정기에
2027
+ * 넘긴다**(plan = planSessionContract, issue = SessionRuntime). 범위·책임·발급 권한은
2028
+ * 그것들이 답하는 것을 그대로 쓴다.
2029
+ *
2030
+ * buildMonitorEngine 을 거치지 않는 이유: 그 함수는 GitHub 토큰이 없으면 멈춘다. 감시에는
2031
+ * 맞는 문이지만, GitLab·JAM 프로젝트에서 저장소 조사까지 막아 버린다 — 그것이 "원격이
2032
+ * 막혔으니 저장소도 못 본다"는 잘못된 결론을 만든 구조다.
2033
+ */
2034
+ async function buildWorkIngress(store, root, runtime, resolved) {
2035
+ const { root: projectRoot } = await discoverProjectRoot(process.cwd());
2036
+ // JAM 은 아직 `jam` 바이너리를 깔지 않는 설치가 기본이다 — 그 경우 launcher 를 통해 부른다.
2037
+ const jamCommand = process.env.ASC_JAM_PATH ? undefined : { command: 'npx', args: ['--yes', '@jam-mcp/launcher'] };
2038
+ const adapters = [
2039
+ new GitHubAdapter(),
2040
+ new GitLabAdapter(),
2041
+ new JamAdapter(jamCommand ?? {}),
2042
+ ];
2043
+ const declared = resolved?.layers.profile.bindings ?? [];
2044
+ const plan = await composeBindings({
2045
+ context: { projectRoot, env: process.env },
2046
+ adapters,
2047
+ roles: declared.map((b) => ({ adapterId: b.adapter, resource: b.resource, role: b.role })),
2048
+ });
2049
+ // 선언이 없으면 발견된 사실로 제안한다 (P1-G). 저장하지 않고, 갈리면 고르지 않는다.
2050
+ const proposed = declared.length === 0 ? proposeBindings(plan) : undefined;
2051
+ if (proposed) {
2052
+ for (const reason of proposed.reasons)
2053
+ console.error(` 제안 ${reason}`);
2054
+ for (const conflict of proposed.conflicts)
2055
+ console.error(` 보류 ${conflict}`);
2056
+ }
2057
+ const ports = await buildRuntimePorts({
2058
+ plan,
2059
+ // 제안은 **말하는 것**이지 정하는 것이 아니다. 역할을 박아 넣으면 선언과 구분되지 않고,
2060
+ // capability 해석은 후보가 유일할 때 이미 스스로 풀린다.
2061
+ roles: rolesFor(plan, declared),
2062
+ perPage: 30,
2063
+ jam: { command: jamCommand?.command ?? 'jam', args: [...(jamCommand?.args ?? []), 'serve'], cwd: projectRoot },
2064
+ endpointFor: (binding) => endpointOf(adapters, binding),
2065
+ });
2066
+ const work = ports.resourceContext;
2067
+ if (!work)
2068
+ return undefined;
2069
+ // 저장소는 원격 provider 와 무관하게 본다. 이 한 줄이 P0-E 의 요점이다.
2070
+ const repo = new LocalRepoAdapter({ cwd: projectRoot });
2071
+ const canonicalRef = resolved?.layers.profile.canonical.sources[0]?.ref;
2072
+ const canonicalPaths = resolved?.layers.profile.canonical.sources.flatMap((source) => source.paths) ?? [];
2073
+ const changeContext = ports.changeContext;
2074
+ return {
2075
+ gather: async (workRef) => {
2076
+ const workItem = await work.getResource(workRef).catch(() => undefined);
2077
+ const comments = await work
2078
+ .getComments(workRef, { limit: 20 })
2079
+ .then((list) => list)
2080
+ .catch(() => 'UNAVAILABLE');
2081
+ const change = changeContext
2082
+ ? await changeContext
2083
+ .getChange(workRef)
2084
+ .then((summary) => summary)
2085
+ .catch(() => 'UNAVAILABLE')
2086
+ : 'UNAVAILABLE';
2087
+ // 선행 작업이 열려 있는지는 **조회해야** 안다. 키만 넘기면 판정이 늘 "모른다"가 되고,
2088
+ // 그러면 막힌 작업이 착수 가능으로 보인다. adapter 가 막는 것을 앞에 실어 주므로
2089
+ // 상한에 걸려도 blocker 가 먼저 확인된다.
2090
+ const candidates = dependencyCandidates(workItem);
2091
+ const dependencies = await Promise.all(candidates.map(async (reference) => {
2092
+ const item = await work.getResource(reference).catch(() => undefined);
2093
+ const done = item && !item.missing ? statusIndicatesDone(item.state) : undefined;
2094
+ return {
2095
+ reference,
2096
+ ...(item?.state ? { state: item.state } : {}),
2097
+ ...(done === undefined ? {} : { open: !done }),
2098
+ };
2099
+ }));
2100
+ return {
2101
+ ...(workItem ? { workItem } : {}),
2102
+ ...(workItem ? { trackerDone: statusIndicatesDone(workItem.state) } : {}),
2103
+ comments,
2104
+ change,
2105
+ dependencies,
2106
+ };
2107
+ },
2108
+ observeRepo: async (query) => {
2109
+ // 조회할 경로는 **작업 항목이 지목한 것**이 먼저다. 그것을 확인해야 좁은 범위를
2110
+ // 만들 수 있고, 확인하지 않으면 넓은 범위밖에 남지 않는다. 저장소의 최상위 자리도
2111
+ // 함께 확인한다 — 분류 이름이 어느 모듈 하나에만 맞는지 재려면 그 목록이 있어야 한다.
2112
+ const modules = await topLevelModules(projectRoot);
2113
+ const paths = [...new Set([...(query.paths ?? []), ...canonicalPaths])];
2114
+ return repo.observe({
2115
+ refHint: query.refHint,
2116
+ ...(canonicalRef ? { canonicalRef } : {}),
2117
+ ...(paths.length > 0 ? { paths } : {}),
2118
+ ...(modules.length > 0 ? { modulePaths: modules } : {}),
2119
+ });
2120
+ },
2121
+ usedIds: () => usedSessionIds(store, root),
2122
+ derive: (input) => deriveSessionContractDraft({
2123
+ existingIds: input.existingIds,
2124
+ intent: { workRef: input.workRef, ...(input.goal ? { goal: input.goal } : {}) },
2125
+ workItem: input.workItem,
2126
+ workState: input.workState,
2127
+ repo: input.repo,
2128
+ // 상한이지 출처가 아니다 — 도출한 후보가 이 밖으로 나가지 않는지 재는 데만 쓴다.
2129
+ maxScopes: resolved?.resolved.policy.roleScopes.implementer ?? [],
2130
+ ...(resolved?.ownership ? { ownership: resolved.ownership } : {}),
2131
+ today: new Date().toISOString().slice(0, 10).replace(/-/g, ''),
2132
+ }),
2133
+ plan: async (draft) => planSessionContract({
2134
+ draft,
2135
+ ...(resolved?.resolved.policy ? { policy: resolved.resolved.policy } : {}),
2136
+ ...(resolved?.ownership ? { ownership: resolved.ownership } : {}),
2137
+ existingIds: await usedSessionIds(store, root),
2138
+ }),
2139
+ issue: async (draft) => {
2140
+ // 발급 경로는 하나뿐이다 — `asc session issue` 와 같은 SessionRuntime.issue 를 부른다.
2141
+ const issued = await runtime.issue({
2142
+ id: draft.id,
2143
+ role: SessionRole.parse(draft.role ?? 'implementer'),
2144
+ goal: draft.goal ?? '',
2145
+ ...(draft.criteria ? { doneCriteria: [...draft.criteria] } : {}),
2146
+ ...(draft.boundary ? { writeBoundary: [...draft.boundary] } : {}),
2147
+ ...(draft.owner ? { owner: draft.owner } : {}),
2148
+ });
2149
+ if (!issued.ok) {
2150
+ return { ok: false, detail: issued.failures.map((f) => `${f.kind}: ${f.detail}`).join('; ') };
2151
+ }
2152
+ await auditLedger(store).delegate({
2153
+ childSessionId: issued.session.id,
2154
+ role: issued.session.role,
2155
+ goal: issued.session.goal,
2156
+ scope: issued.session.writeBoundary,
2157
+ doneCriteria: issued.session.doneCriteria,
2158
+ issuedBy: issued.session.owner ?? '(위임 범위 내 자동 발급)',
2159
+ issuedAt: new Date().toISOString(),
2160
+ });
2161
+ return { ok: true, sessionId: issued.session.id };
2162
+ },
2163
+ };
2164
+ }
1809
2165
  /** 세션 lifecycle. 계약을 발급하고, 중단·재개하고, Handoff까지 남긴다 (OM §6.2). */
1810
2166
  /**
1811
2167
  * 이 세션이 걸린 결정마다 실제 결정권자. 세션 계약이 먼저고, 없으면 Profile 지도에서 푼다.
@@ -1840,6 +2196,94 @@ function parseAuthority(pairs) {
1840
2196
  }
1841
2197
  return { ok: true, map };
1842
2198
  }
2199
+ /**
2200
+ * 초안을 재 본다. **아무것도 발급하지 않는다** (C-14 §6의 plan/apply 분리와 같은 자세).
2201
+ *
2202
+ * agent가 사용자 요청·work item·Profile·저장소를 읽어 계약 초안을 만들고, 여기서 그것이
2203
+ * 구조·경계로 성립하는지 확인한다. 판정은 셋뿐이다 — 발급해도 된다 / 사람이 정할 것이
2204
+ * 남았다 / 이 초안으로는 계약이 안 된다.
2205
+ */
2206
+ async function runSessionPlan(values, store, resolved) {
2207
+ const authority = parseAuthority(values.authority);
2208
+ if (!authority.ok) {
2209
+ console.error(authority.detail);
2210
+ return 2;
2211
+ }
2212
+ // 출처는 `<field>=<status>[:<source>]` 로 받는다 — 초안을 만든 쪽이 무엇을 확인했고
2213
+ // 무엇을 제안했는지 스스로 적게 한다. 적지 않으면 제안으로 셈한다(사실로 올리지 않는다).
2214
+ const provenance = [];
2215
+ for (const raw of values.provenance ?? []) {
2216
+ const [field, rest] = raw.split('=', 2);
2217
+ const [status, source] = (rest ?? '').split(':', 2);
2218
+ const parsed = DraftProvenance.safeParse({
2219
+ field,
2220
+ status,
2221
+ source: source ?? 'agent_proposal',
2222
+ ...(values.why ? { reason: values.why[0] } : {}),
2223
+ });
2224
+ if (!parsed.success) {
2225
+ console.error(`--provenance 는 <field>=<FACT|PROPOSAL|DECISION_REQUIRED>[:<source>] 형식이다: '${raw}'`);
2226
+ return 2;
2227
+ }
2228
+ provenance.push(parsed.data);
2229
+ }
2230
+ const draft = {
2231
+ ...(values.id ? { id: values.id } : {}),
2232
+ ...(values.role ? { role: values.role } : {}),
2233
+ ...(values.goal ? { goal: values.goal } : {}),
2234
+ ...(values.boundary ? { boundary: values.boundary } : {}),
2235
+ ...(values.criteria ? { criteria: values.criteria } : {}),
2236
+ ...(values.owner ? { owner: values.owner } : {}),
2237
+ ...(values.domain ? { decisionDomains: values.domain } : {}),
2238
+ ...(Object.keys(authority.map).length > 0 ? { decisionAuthority: authority.map } : {}),
2239
+ ...(provenance.length > 0 ? { provenance } : {}),
2240
+ };
2241
+ const plan = planSessionContract({
2242
+ draft,
2243
+ ...(resolved?.resolved.policy ? { policy: resolved.resolved.policy } : {}),
2244
+ ...(resolved?.ownership ? { ownership: resolved.ownership } : {}),
2245
+ existingIds: (await store.list('session')).map((session) => session.id),
2246
+ });
2247
+ // 통과했을 때만 실행 가능한 명령을 준다. 통과하지 않은 초안의 명령을 함께 주면
2248
+ // agent는 그것을 "고치고 나서 쓸 것"이 아니라 "지금 쓸 것"으로 읽는다.
2249
+ //
2250
+ // 그리고 **완성된 계약이라고 해서 발급해도 되는 것은 아니다** (OM §450). 위임이 없으면
2251
+ // 명령은 `forController` 로 간다 — actions에 넣으면 "portable을 실행하라"는 지시를 따르는
2252
+ // agent가 사람의 권한을 대신 쓰게 된다.
2253
+ const issuable = plan.status === 'READY_TO_ISSUE';
2254
+ const command = {
2255
+ display: shorthandCommand(issueArgs(draft)),
2256
+ portable: shorthandCommand([...issueArgs(draft), '--json']),
2257
+ };
2258
+ const actions = issuable && plan.issuance.authority === 'delegated' ? [{ type: 'issue_session', ...command }] : [];
2259
+ const forController = issuable && plan.issuance.authority === 'controller' ? command : undefined;
2260
+ if (values.json || values.agent) {
2261
+ console.log(JSON.stringify({ ...plan, nextActions: actions.map((action) => action.portable), actions, ...(forController ? { forController } : {}) }, null, 2));
2262
+ }
2263
+ else {
2264
+ console.log(`${plan.status}${plan.draft.id ? ` — ${plan.draft.id}` : ''}`);
2265
+ for (const fact of plan.facts)
2266
+ console.log(` fact ${fact.field} (${fact.source})`);
2267
+ for (const proposal of plan.proposals)
2268
+ console.log(` proposal ${proposal.field} — ${proposal.reason ?? proposal.source}`);
2269
+ for (const item of plan.invalid)
2270
+ console.log(` invalid ${item.field}: ${item.detail}`);
2271
+ for (const item of plan.unresolved) {
2272
+ console.log(` decide ${item.field} [${item.reason}]: ${item.detail}`);
2273
+ for (const [index, option] of (item.options ?? []).entries()) {
2274
+ console.log(` ${index + 1}. ${option}${item.recommended === index ? ' ← recommended' : ''}`);
2275
+ }
2276
+ }
2277
+ for (const action of actions)
2278
+ console.log(`\nIssue it: ${action.display}`);
2279
+ if (forController) {
2280
+ console.log(`\nThe contract holds. Issuing it is the Controller's — ${plan.issuance.detail}:`);
2281
+ console.log(` ${forController.display}`);
2282
+ }
2283
+ }
2284
+ // 사람이 정할 것이 남았거나 초안이 성립하지 않으면 1이다 — setup plan과 같은 규칙.
2285
+ return plan.status === 'READY_TO_ISSUE' ? 0 : 1;
2286
+ }
1843
2287
  async function runSession(command, target, values, store, resolved) {
1844
2288
  // Profile이 정한 Role 범위와 금지가 여기까지 와야 실제로 강제된다.
1845
2289
  // 정책 없이 만들면 붙어 있는 프로젝트에서도 계약 검사가 통째로 비어버린다.
@@ -1863,6 +2307,10 @@ async function runSession(command, target, values, store, resolved) {
1863
2307
  }
1864
2308
  return 0;
1865
2309
  }
2310
+ // `plan` 은 **아직 세션이 없을 때** 부르는 것이므로 id를 요구하지 않는다. 초안에 id가
2311
+ // 없다는 사실 자체가 판정 대상이다 (없으면 그것이 unresolved로 나온다).
2312
+ if (command === 'plan')
2313
+ return runSessionPlan(values, store, resolved);
1866
2314
  if (!target) {
1867
2315
  console.error(`Usage: asc session ${command ?? '<command>'} <SESSION_ID>`);
1868
2316
  return 2;
@@ -0,0 +1,19 @@
1
+ import type { BindingPlan, Capability } from '../core/binding/types.ts';
2
+ export type BindingProposal = {
3
+ /**
4
+ * capability → 이 통로를 맡을 adapter.
5
+ *
6
+ * **조립에 밀어 넣는 값이 아니다.** 후보가 유일하면 capability 해석은 스스로 풀리므로,
7
+ * 여기서 역할을 박으면 선언과 제안이 같은 자리에 섞인다. 이 표는 "무엇이 무엇을 맡게
8
+ * 됐는지"를 사람에게 말하기 위한 것이다.
9
+ */
10
+ roles: Partial<Record<Capability, string>>;
11
+ /** 사람이 읽는 근거. 제안이라는 사실을 문장 안에 남긴다. */
12
+ reasons: string[];
13
+ /** 후보가 갈려 제안하지 않은 것들. */
14
+ conflicts: string[];
15
+ };
16
+ /**
17
+ * 선언이 하나도 없을 때만 부른다. 선언이 있으면 그것이 답이고, 여기서 다시 정하지 않는다.
18
+ */
19
+ export declare function proposeBindings(plan: BindingPlan): BindingProposal;
@@ -0,0 +1,35 @@
1
+ // Binding 자동 제안 (P1-G).
2
+ //
3
+ // Profile 은 **팀이 정한 것**을 담는다. 그런데 지금까지는 발견하면 알 수 있는 사실까지
4
+ // 거기 적어야 했다 — 원격이 하나뿐이고 그 원격을 다룰 adapter 도 하나뿐인데도, 누가
5
+ // bindings 에 적어 주기 전에는 아무 통로가 서지 않았다.
6
+ //
7
+ // 여기서 하는 일은 그 한 가지뿐이다: **후보가 유일할 때만** 역할을 제안한다. 저장하지
8
+ // 않고, Profile 을 고치지 않으며, 둘 이상이면 고르지 않는다 — 고르는 것은 사람의 일이고,
9
+ // 틀린 연결은 없는 연결보다 나쁘다.
10
+ const USABLE = new Set(['AVAILABLE', 'DEGRADED']);
11
+ /**
12
+ * 선언이 하나도 없을 때만 부른다. 선언이 있으면 그것이 답이고, 여기서 다시 정하지 않는다.
13
+ */
14
+ export function proposeBindings(plan) {
15
+ const proposal = { roles: {}, reasons: [], conflicts: [] };
16
+ const usable = plan.bindings.filter((binding) => USABLE.has(binding.state));
17
+ const byCapability = new Map();
18
+ for (const binding of usable) {
19
+ for (const capability of binding.provides) {
20
+ byCapability.set(capability, [...(byCapability.get(capability) ?? []), binding]);
21
+ }
22
+ }
23
+ for (const [capability, candidates] of byCapability) {
24
+ const distinct = new Map(candidates.map((c) => [`${c.adapterId}:${c.resource}`, c]));
25
+ if (distinct.size !== 1) {
26
+ proposal.conflicts.push(`${capability}: 후보가 ${distinct.size}개다 (${[...distinct.keys()].join(', ')}) — 어느 쪽인지는 사람이 정한다`);
27
+ continue;
28
+ }
29
+ const only = [...distinct.values()][0];
30
+ const role = only.role ?? only.adapterId;
31
+ proposal.roles[capability] = role;
32
+ proposal.reasons.push(`${capability} ← ${only.adapterId}:${only.resource} (유일한 후보라 제안한다 — Profile 에 저장하지 않는다)`);
33
+ }
34
+ return proposal;
35
+ }
@@ -42,6 +42,8 @@ export type BuildInput = {
42
42
  /** 이 binding이 어느 주소를 가리키는지. 발견 단계가 알아낸 값을 그대로 잇는다. */
43
43
  endpointFor?: (binding: ResolvedBinding) => string | undefined;
44
44
  };
45
+ /** 이 프로세스가 띄운 도구 자식들을 정리한다. 여러 번 불러도 안전하다. */
46
+ export declare function closeToolClients(): Promise<void>;
45
47
  /**
46
48
  * capability가 필요한 자리마다 어느 binding이 맡을지 정해 Port를 만든다.
47
49
  *