@asc-agent/runtime 0.4.0 → 0.5.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 +1 -1
- package/dist/adapters/fixture-surface/index.d.ts +33 -0
- package/dist/adapters/fixture-surface/index.js +97 -0
- package/dist/adapters/gitlab/adapter.js +1 -0
- package/dist/adapters/gitlab/client.d.ts +16 -2
- package/dist/adapters/gitlab/client.js +27 -0
- package/dist/adapters/gitlab/coordination.d.ts +19 -0
- package/dist/adapters/gitlab/coordination.js +111 -0
- package/dist/adapters/gitlab/ports.d.ts +6 -0
- package/dist/adapters/gitlab/ports.js +29 -2
- package/dist/adapters/local/canonical.d.ts +20 -0
- package/dist/adapters/local/canonical.js +58 -0
- package/dist/adapters/markdown/state-store.js +2 -1
- package/dist/adapters/service/launchd.js +18 -1
- package/dist/adapters/service/systemd-user.js +4 -1
- package/dist/adapters/webhook/ingress.d.ts +2 -2
- package/dist/cli/asc.js +244 -6
- package/dist/composition/runtime.d.ts +3 -0
- package/dist/composition/runtime.js +6 -1
- package/dist/core/binding/types.d.ts +1 -1
- package/dist/core/binding/types.js +1 -0
- package/dist/core/distribution/external-command.d.ts +26 -0
- package/dist/core/distribution/external-command.js +58 -1
- package/dist/core/distribution/persistent-runtime.d.ts +9 -0
- package/dist/core/distribution/release.d.ts +3 -3
- package/dist/core/distribution/release.js +1 -1
- package/dist/core/runtime/coordination.d.ts +312 -0
- package/dist/core/runtime/coordination.js +247 -0
- package/dist/core/runtime/front.d.ts +10 -0
- package/dist/core/runtime/front.js +16 -1
- package/dist/core/runtime/publish.d.ts +107 -0
- package/dist/core/runtime/publish.js +153 -0
- package/dist/core/runtime/workspaces.d.ts +22 -0
- package/dist/core/runtime/workspaces.js +18 -0
- package/dist/ports/coordination-surface.d.ts +59 -0
- package/dist/ports/coordination-surface.js +16 -0
- package/dist/ports/resource-context.d.ts +5 -0
- package/package.json +1 -1
package/dist/cli/asc.js
CHANGED
|
@@ -46,6 +46,8 @@ import { CoverageLedger, renderHealth } from "../core/monitor/coverage.js";
|
|
|
46
46
|
import { evaluateHealth, healthAlertLines } from "../core/monitor/health-alerts.js";
|
|
47
47
|
import { Operator } from "../core/operator/proceed.js";
|
|
48
48
|
import { deriveSessionContractDraft } from "../core/operator/derive-draft.js";
|
|
49
|
+
import { servicePath } from "../core/distribution/external-command.js";
|
|
50
|
+
import { LocalCanonicalReader } from "../adapters/local/canonical.js";
|
|
49
51
|
import { LocalRepoAdapter } from "../adapters/local/repo.js";
|
|
50
52
|
import { GitHubAdapter } from "../adapters/github/adapter.js";
|
|
51
53
|
import { GitLabAdapter } from "../adapters/gitlab/adapter.js";
|
|
@@ -69,12 +71,14 @@ import { AuditLedger, decisionLines, delegationLine, executionLines, reclaimLine
|
|
|
69
71
|
import { ClosureLedger } from "../core/runtime/closure.js";
|
|
70
72
|
import { Orchestrator, renderTick } from "../core/runtime/orchestrator.js";
|
|
71
73
|
import { LAST_RUN_KEY, RuntimeLease, fileScope, readBackground, renderBackground, staleAfter, } from "../core/runtime/background.js";
|
|
72
|
-
import { dueWorkspaces, renderWorkspaces,
|
|
74
|
+
import { dueWorkspaces, renderWorkspaces, summarizePass, viewWorkspaces } from "../core/runtime/workspaces.js";
|
|
73
75
|
import { persistentRuntimeLine, planPersistentRuntime, } from "../core/distribution/persistent-runtime.js";
|
|
74
76
|
import { launchdAdapter } from "../adapters/service/launchd.js";
|
|
75
77
|
import { schtasksAdapter } from "../adapters/service/schtasks.js";
|
|
76
78
|
import { serviceAdapterFor, systemdUserAdapter } from "../adapters/service/systemd-user.js";
|
|
77
79
|
import { QueryLedger } from "../core/runtime/query.js";
|
|
80
|
+
import { CoordinationLedger, coordinationLines, responsesFrom, viewCoordination, } from "../core/runtime/coordination.js";
|
|
81
|
+
import { publishLine, publishOnce, recordPublication } from "../core/runtime/publish.js";
|
|
78
82
|
import { ObservationLedger } from "../core/monitor/observation.js";
|
|
79
83
|
import { DeliveryLedger, deliver, planDigest } from "../core/presentation/digest.js";
|
|
80
84
|
import { LocalPresentation } from "../adapters/local/presentation.js";
|
|
@@ -191,6 +195,11 @@ const USAGE = `asc — Agent Session Control
|
|
|
191
195
|
[--to <authority>]
|
|
192
196
|
asc query list [--json]
|
|
193
197
|
|
|
198
|
+
asc coordination [status] [--json] # what was asked outside, and whether it reached anyone
|
|
199
|
+
asc coordination publish --query <ID> --title <text> --body-file <path>
|
|
200
|
+
[--audience <who>] [--known <objectId>] [--work <ref>] [--json]
|
|
201
|
+
asc coordination observe [--json] # did anything come back on what we published
|
|
202
|
+
|
|
194
203
|
asc progress show [<S-ID>]
|
|
195
204
|
asc progress report <S-ID> --physical <id> --phase <text>
|
|
196
205
|
[--milestone <text>...] [--next <text>] [--unresolved <text>...]
|
|
@@ -577,6 +586,11 @@ function parseArgsOrThrow(argv) {
|
|
|
577
586
|
validator: { type: 'string' },
|
|
578
587
|
result: { type: 'string' },
|
|
579
588
|
finding: { type: 'string', multiple: true },
|
|
589
|
+
query: { type: 'string' },
|
|
590
|
+
title: { type: 'string' },
|
|
591
|
+
'body-file': { type: 'string' },
|
|
592
|
+
audience: { type: 'string', multiple: true },
|
|
593
|
+
known: { type: 'string', multiple: true },
|
|
580
594
|
blocker: { type: 'string', multiple: true },
|
|
581
595
|
risk: { type: 'string', multiple: true },
|
|
582
596
|
evidence: { type: 'string', multiple: true },
|
|
@@ -718,7 +732,7 @@ async function runParsedCommand(values, positionals, entry, argv) {
|
|
|
718
732
|
// 순서가 뒤집힌다. 나머지 profile 명령은 아래 attach 경로에 그대로 남는다.
|
|
719
733
|
if (group === 'profile' && command === 'adopt')
|
|
720
734
|
return runProfileAdopt(values, entry);
|
|
721
|
-
if (!['inbox', 'grant', 'monitor', 'runtime', 'front', 'freeze', 'thaw', 'escalate', 'profile', 'session', 'controller', 'proceed', 'progress', 'preflight', 'closure', 'query'].includes(group)) {
|
|
735
|
+
if (!['inbox', 'grant', 'monitor', 'runtime', 'front', 'coordination', 'freeze', 'thaw', 'escalate', 'profile', 'session', 'controller', 'proceed', 'progress', 'preflight', 'closure', 'query'].includes(group)) {
|
|
722
736
|
console.error(`Unknown command: ${group}\n\n${USAGE}`);
|
|
723
737
|
return 2;
|
|
724
738
|
}
|
|
@@ -760,6 +774,9 @@ async function runParsedCommand(values, positionals, entry, argv) {
|
|
|
760
774
|
// 새 대화가 붙었을 때 지금 상태를 되찾는다 (C-12 §4). 읽기만 한다.
|
|
761
775
|
if (group === 'front')
|
|
762
776
|
return runFront(command, values, store, root);
|
|
777
|
+
// 밖에 물은 것이 실제로 전달됐는가. 읽기만 한다.
|
|
778
|
+
if (group === 'coordination')
|
|
779
|
+
return runCoordination(command, values, store, root, guard.runtime);
|
|
763
780
|
// 사람에게 올릴 자격이 있는가 (C-13). 자격 없으면 request가 만들어지지 않는다.
|
|
764
781
|
if (group === 'escalate')
|
|
765
782
|
return runEscalate(command, target, values, store, guard.runtime);
|
|
@@ -1929,11 +1946,23 @@ async function runProfile(command, values, root) {
|
|
|
1929
1946
|
async function scmFor(resolved) {
|
|
1930
1947
|
if (!resolved)
|
|
1931
1948
|
return undefined;
|
|
1949
|
+
const sources = resolved.layers.profile.canonical.sources;
|
|
1950
|
+
// Profile 은 갈래마다 provider 를 적는다. `git` 이라고 적힌 것은 이 checkout 이 이미
|
|
1951
|
+
// 들고 있는 사실이므로 원격 API 로 가지 않는다 — 갈 수 있다는 보장도 없다.
|
|
1952
|
+
if (sources.length > 0 && sources.every((source) => source.provider === 'git')) {
|
|
1953
|
+
const { root } = await discoverProjectRoot(process.cwd());
|
|
1954
|
+
const refs = {};
|
|
1955
|
+
for (const source of sources) {
|
|
1956
|
+
if (source.ref)
|
|
1957
|
+
refs[source.id] = { ref: source.ref, ...(source.remote ? { remote: source.remote } : {}) };
|
|
1958
|
+
}
|
|
1959
|
+
return new LocalCanonicalReader({ cwd: root, sourceRefs: refs });
|
|
1960
|
+
}
|
|
1932
1961
|
const token = await discoverToken();
|
|
1933
1962
|
if (!token)
|
|
1934
1963
|
return undefined;
|
|
1935
1964
|
const sourceRefs = {};
|
|
1936
|
-
for (const source of
|
|
1965
|
+
for (const source of sources) {
|
|
1937
1966
|
if (source.ref)
|
|
1938
1967
|
sourceRefs[source.id] = { ref: source.ref };
|
|
1939
1968
|
}
|
|
@@ -3095,6 +3124,25 @@ const CLOSURE_CHECKLIST = 'closureChecklist';
|
|
|
3095
3124
|
/** Orchestration Audit (C-10). 회수 후에도 남는 기록이라 Progress와 다른 자리에 산다. */
|
|
3096
3125
|
const auditLedger = (store) => new AuditLedger(store.scope('audit'));
|
|
3097
3126
|
const closureLedger = (store) => new ClosureLedger(store.scope('closure'));
|
|
3127
|
+
/**
|
|
3128
|
+
* 조율 증거 원장. Bounded Query 와 **다른 scope** 에 산다 — 기대와 증거는 다른 사실이고,
|
|
3129
|
+
* 한 자리에 두면 "물어봤다"와 "전달됐다"가 다시 한 레코드가 된다.
|
|
3130
|
+
*/
|
|
3131
|
+
const coordinationLedger = (store) => new CoordinationLedger(store.scope('coordination'));
|
|
3132
|
+
/**
|
|
3133
|
+
* 지금 이 workspace 의 조율 상태.
|
|
3134
|
+
*
|
|
3135
|
+
* 기대는 Bounded Query 에서 오고 증거는 원장에서 온다. **여기서 상태를 만들지 않는다** —
|
|
3136
|
+
* 둘을 맞춰 파생할 뿐이다.
|
|
3137
|
+
*/
|
|
3138
|
+
async function coordinationNow(store, resolved) {
|
|
3139
|
+
const queries = await queryLedger(store, resolved).list();
|
|
3140
|
+
return viewCoordination(coordinationLedger(store),
|
|
3141
|
+
// 답이 이미 안에서 쓰인 질의는 조율 대상이 아니다 — 밖에 물을 이유가 끝났다.
|
|
3142
|
+
queries
|
|
3143
|
+
.filter((entry) => entry.answer === null)
|
|
3144
|
+
.map((entry) => ({ id: entry.query.id, expectsResponse: entry.query.expectedResponse !== undefined })));
|
|
3145
|
+
}
|
|
3098
3146
|
/** Bounded Query (B-25). 결정권 판정에 Profile 책임 지도가 필요하다. */
|
|
3099
3147
|
const queryLedger = (store, resolved) => new QueryLedger(store.scope('query'), resolved?.ownership);
|
|
3100
3148
|
/**
|
|
@@ -3521,8 +3569,24 @@ function serviceCommand(intervalSeconds) {
|
|
|
3521
3569
|
program: process.execPath,
|
|
3522
3570
|
args: [fileURLToPath(import.meta.url), 'runtime', 'tick', '--all'],
|
|
3523
3571
|
intervalSeconds,
|
|
3572
|
+
...serviceEnvironment(),
|
|
3573
|
+
logPath: join(ascHome(), 'service.log'),
|
|
3524
3574
|
};
|
|
3525
3575
|
}
|
|
3576
|
+
/**
|
|
3577
|
+
* 서비스가 외부 통로를 열 수 있게 하는 환경 (P0-R1).
|
|
3578
|
+
*
|
|
3579
|
+
* 여기가 어떤 실행 파일이 필요한지 아는 자리다 — 조립 계층은 provider 를 안다. 지금 도는
|
|
3580
|
+
* node 의 디렉터리를 맨 앞에 둬서 같은 node 의 npx 가 먼저 잡히게 한다. Windows 의 예약
|
|
3581
|
+
* 작업은 사용자 환경을 그대로 물려받으므로 환경을 싣지 않는다.
|
|
3582
|
+
*/
|
|
3583
|
+
function serviceEnvironment() {
|
|
3584
|
+
if (process.platform === 'win32')
|
|
3585
|
+
return {};
|
|
3586
|
+
const tools = [process.execPath, jamLauncher().command, 'glab', 'git'];
|
|
3587
|
+
const { path } = servicePath(tools);
|
|
3588
|
+
return { environment: { PATH: path } };
|
|
3589
|
+
}
|
|
3526
3590
|
const serviceInterval = (values) => values['interval-min'] === undefined ? 5 * 60 : Math.max(60, Number(values['interval-min']) * 60);
|
|
3527
3591
|
/**
|
|
3528
3592
|
* `asc runtime service` — 이 기계의 지속 등록 (설계 §4).
|
|
@@ -3626,11 +3690,15 @@ async function tickAllWorkspaces(values, lease) {
|
|
|
3626
3690
|
// 죽은 것으로 보이고 두 번째 프로세스가 끼어든다.
|
|
3627
3691
|
await lease.renew();
|
|
3628
3692
|
}
|
|
3693
|
+
const skippedViews = skipped.map((view) => ({ workspaceId: view.workspaceId, health: view.health }));
|
|
3694
|
+
const summary = summarizePass(results, skippedViews);
|
|
3629
3695
|
if (values.json) {
|
|
3630
3696
|
console.log(JSON.stringify({
|
|
3631
3697
|
ran: results,
|
|
3632
3698
|
// 건너뛴 것을 조용히 빼지 않는다 — "아무 일도 없었다"와 다른 사실이다
|
|
3633
|
-
skipped:
|
|
3699
|
+
skipped: skippedViews,
|
|
3700
|
+
outcome: summary.outcome,
|
|
3701
|
+
failed: summary.failed,
|
|
3634
3702
|
}, null, 2));
|
|
3635
3703
|
}
|
|
3636
3704
|
else {
|
|
@@ -3638,9 +3706,11 @@ async function tickAllWorkspaces(values, lease) {
|
|
|
3638
3706
|
console.log(`${result.workspaceId}: pass exited ${result.code}`);
|
|
3639
3707
|
for (const view of skipped)
|
|
3640
3708
|
console.log(`${view.workspaceId}: ${view.health} — not observed this pass`);
|
|
3709
|
+
console.log(summary.line);
|
|
3641
3710
|
}
|
|
3642
|
-
// 한 workspace 가 실패해도
|
|
3643
|
-
|
|
3711
|
+
// 한 workspace 가 실패해도 다른 workspace 는 전부 돌았다. 그러나 실패했다는 사실은 회차의
|
|
3712
|
+
// 종료 코드에 남는다 — OS 가 보는 것은 그것 하나뿐이다 (P0-R2).
|
|
3713
|
+
return summary.code;
|
|
3644
3714
|
}
|
|
3645
3715
|
/**
|
|
3646
3716
|
* 상시 Runtime (C-12). 대화를 켜 두지 않고 **상태를 지속시키고 계산을 짧게 돌린다.**
|
|
@@ -4136,11 +4206,178 @@ async function runFrontOpen(values) {
|
|
|
4136
4206
|
escalations: await escalationLedger(store).pending(),
|
|
4137
4207
|
health: evaluateHealth(await new CoverageLedger(store.scope(scope)).health(), new Date().toISOString(), HEALTH_THRESHOLDS),
|
|
4138
4208
|
...(workspace ? { workspace } : {}),
|
|
4209
|
+
// 안에서 할 일이 없는 것과 밖에서 답이 안 온 것은 다른 사실이다
|
|
4210
|
+
coordination: await coordinationNow(store),
|
|
4139
4211
|
bindings: claudeBindings(store),
|
|
4140
4212
|
});
|
|
4141
4213
|
},
|
|
4142
4214
|
}));
|
|
4143
4215
|
}
|
|
4216
|
+
/**
|
|
4217
|
+
* 이 workspace 의 조율 표면. 없으면 없다고 말한다 — 아무 데나 대신 게시하지 않는다.
|
|
4218
|
+
*/
|
|
4219
|
+
async function coordinationSurfaceFor(resolved) {
|
|
4220
|
+
const { root: projectRoot } = await discoverProjectRoot(process.cwd());
|
|
4221
|
+
const adapters = monitorAdapters();
|
|
4222
|
+
const declared = resolved?.layers.profile.bindings ?? [];
|
|
4223
|
+
const plan = await composeBindings({
|
|
4224
|
+
context: { projectRoot, env: process.env },
|
|
4225
|
+
adapters,
|
|
4226
|
+
roles: declared.map((b) => ({ adapterId: b.adapter, resource: b.resource, role: b.role })),
|
|
4227
|
+
});
|
|
4228
|
+
const ports = await buildRuntimePorts({
|
|
4229
|
+
plan,
|
|
4230
|
+
roles: rolesFor(plan, declared),
|
|
4231
|
+
...jamComposition(projectRoot),
|
|
4232
|
+
endpointFor: (binding) => endpointOf(adapters, binding),
|
|
4233
|
+
});
|
|
4234
|
+
return { surface: ports.coordinationSurface, unavailable: ports.unavailable };
|
|
4235
|
+
}
|
|
4236
|
+
/**
|
|
4237
|
+
* `asc coordination publish` — 기대 하나를 밖에 실제로 내보낸다.
|
|
4238
|
+
*
|
|
4239
|
+
* 본문을 파일로 받는 이유는 하나다: 사람이 읽을 글이 셸을 지나며 조용히 달라지는 것을
|
|
4240
|
+
* 막는다. 그리고 **내부 메모가 섞일 자리를 주지 않는다** — 나가는 것은 제목·본문·라벨뿐이다.
|
|
4241
|
+
*/
|
|
4242
|
+
async function runCoordinationPublish(values, store, resolved) {
|
|
4243
|
+
const queryId = typeof values.query === 'string' ? values.query : undefined;
|
|
4244
|
+
const title = typeof values.title === 'string' ? values.title : undefined;
|
|
4245
|
+
const bodyFile = typeof values['body-file'] === 'string' ? values['body-file'] : undefined;
|
|
4246
|
+
if (!queryId || !title || !bodyFile) {
|
|
4247
|
+
console.error('coordination publish needs --query, --title and --body-file\n\n' + USAGE);
|
|
4248
|
+
return 2;
|
|
4249
|
+
}
|
|
4250
|
+
const { surface, unavailable } = await coordinationSurfaceFor(resolved);
|
|
4251
|
+
if (!surface) {
|
|
4252
|
+
console.error('No coordination surface is bound to this workspace — nothing was published.');
|
|
4253
|
+
for (const line of unavailable)
|
|
4254
|
+
console.error(` ${line}`);
|
|
4255
|
+
return 1;
|
|
4256
|
+
}
|
|
4257
|
+
const body = await readFile(bodyFile, 'utf8');
|
|
4258
|
+
const audience = values.audience ?? [];
|
|
4259
|
+
const known = (values.known ?? []).map((objectId) => ({
|
|
4260
|
+
objectType: 'issue',
|
|
4261
|
+
objectId,
|
|
4262
|
+
}));
|
|
4263
|
+
const workReference = typeof values.work === 'string' ? values.work : undefined;
|
|
4264
|
+
const outcome = await publishOnce({
|
|
4265
|
+
queryId,
|
|
4266
|
+
publicPayload: { title, body },
|
|
4267
|
+
...(audience.length > 0 ? { audience } : {}),
|
|
4268
|
+
...(known.length > 0 ? { known } : {}),
|
|
4269
|
+
...(workReference ? { workReference } : {}),
|
|
4270
|
+
}, { surface, bindingRole: 'coordination-surface' });
|
|
4271
|
+
if (outcome.ok) {
|
|
4272
|
+
const recorded = await recordPublication(coordinationLedger(store), outcome);
|
|
4273
|
+
if (values.json) {
|
|
4274
|
+
console.log(JSON.stringify({ publish: outcome, recorded: recorded.ok }, null, 2));
|
|
4275
|
+
return 0;
|
|
4276
|
+
}
|
|
4277
|
+
console.log(publishLine(outcome));
|
|
4278
|
+
// 이미 적힌 사실을 다시 적지 않는다. 그것은 실패가 아니라 같은 것을 두 번 안 세는 것이다.
|
|
4279
|
+
console.log(recorded.ok ? 'recorded as communication evidence' : 'already recorded');
|
|
4280
|
+
return 0;
|
|
4281
|
+
}
|
|
4282
|
+
if (values.json)
|
|
4283
|
+
console.log(JSON.stringify({ publish: outcome }, null, 2));
|
|
4284
|
+
else
|
|
4285
|
+
console.error(publishLine(outcome));
|
|
4286
|
+
return 1;
|
|
4287
|
+
}
|
|
4288
|
+
/**
|
|
4289
|
+
* `asc coordination observe` — 게시한 것에 답이 왔는가.
|
|
4290
|
+
*
|
|
4291
|
+
* **답의 의미를 정하지 않는다.** 여기서 남는 것은 "밖에서 사람이 글을 남겼다"까지이고,
|
|
4292
|
+
* 그것이 결정인지 승인인지는 다른 계약의 몫이다. 그 시스템이 스스로 남긴 자국과 우리가
|
|
4293
|
+
* 쓴 글은 세지 않는다 — 그 둘을 세면 아무도 답하지 않은 스레드가 답이 온 것으로 보인다.
|
|
4294
|
+
*/
|
|
4295
|
+
async function runCoordinationObserve(values, store, root, resolved) {
|
|
4296
|
+
const ledger = coordinationLedger(store);
|
|
4297
|
+
const communications = await ledger.communications();
|
|
4298
|
+
if (communications.length === 0) {
|
|
4299
|
+
console.log('Nothing has been published from here yet.');
|
|
4300
|
+
return 0;
|
|
4301
|
+
}
|
|
4302
|
+
const { root: projectRoot } = await discoverProjectRoot(process.cwd());
|
|
4303
|
+
const adapters = monitorAdapters();
|
|
4304
|
+
const declared = resolved?.layers.profile.bindings ?? [];
|
|
4305
|
+
const plan = await composeBindings({
|
|
4306
|
+
context: { projectRoot, env: process.env },
|
|
4307
|
+
adapters,
|
|
4308
|
+
roles: declared.map((b) => ({ adapterId: b.adapter, resource: b.resource, role: b.role })),
|
|
4309
|
+
});
|
|
4310
|
+
// **그 게시물을 만든 통로에게 묻는다.** 프로젝트 전체의 자원 조회를 하나 고르면
|
|
4311
|
+
// 어느 것을 고를지 갈리고(실제로 갈렸다), 갈리지 않더라도 다른 시스템에게 남의 게시물을
|
|
4312
|
+
// 묻게 된다. 증거에 적힌 adapter 가 곧 그 통로다.
|
|
4313
|
+
const built = await buildObservationChannels({
|
|
4314
|
+
plan,
|
|
4315
|
+
roles: rolesFor(plan, declared),
|
|
4316
|
+
...jamComposition(projectRoot),
|
|
4317
|
+
endpointFor: (binding) => endpointOf(adapters, binding),
|
|
4318
|
+
});
|
|
4319
|
+
const contextOf = (adapterId) => built.channels.find((channel) => channel.adapterId === adapterId && channel.resourceContext)?.resourceContext;
|
|
4320
|
+
// 나를 나로 알아보는 곳은 identities.json 하나다. 채널 접두사를 떼면 계정 이름이 남는다.
|
|
4321
|
+
const identity = await loadIdentityMap(root);
|
|
4322
|
+
const mine = new Set();
|
|
4323
|
+
for (const [name, accounts] of Object.entries(identity)) {
|
|
4324
|
+
mine.add(name);
|
|
4325
|
+
for (const account of accounts)
|
|
4326
|
+
mine.add(account.includes(':') ? account.slice(account.indexOf(':') + 1) : account);
|
|
4327
|
+
}
|
|
4328
|
+
let added = 0;
|
|
4329
|
+
let looked = 0;
|
|
4330
|
+
for (const communication of communications) {
|
|
4331
|
+
const context = contextOf(communication.identity.adapter);
|
|
4332
|
+
if (!context) {
|
|
4333
|
+
// 못 본 것을 "답이 없다"로 넘기지 않는다.
|
|
4334
|
+
console.error(` ${communication.identity.objectId}: ${communication.identity.adapter} 통로가 열리지 않아 보지 못했다`);
|
|
4335
|
+
continue;
|
|
4336
|
+
}
|
|
4337
|
+
looked += 1;
|
|
4338
|
+
const remarks = await context
|
|
4339
|
+
.getComments(communication.identity.objectId, { limit: 100 })
|
|
4340
|
+
.catch(() => []);
|
|
4341
|
+
for (const response of responsesFrom(communication, remarks, mine)) {
|
|
4342
|
+
const outcome = await ledger.responseRecorded(response);
|
|
4343
|
+
if (outcome.ok)
|
|
4344
|
+
added += 1;
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
const views = await coordinationNow(store, resolved);
|
|
4348
|
+
if (values.json) {
|
|
4349
|
+
console.log(JSON.stringify({ published: communications.length, observed: looked, recorded: added, coordination: views }, null, 2));
|
|
4350
|
+
return 0;
|
|
4351
|
+
}
|
|
4352
|
+
console.log(`Looked at ${looked} of ${communications.length} published artefact(s) — ${added} new response(s).`);
|
|
4353
|
+
for (const line of coordinationLines(views))
|
|
4354
|
+
console.log(line);
|
|
4355
|
+
return 0;
|
|
4356
|
+
}
|
|
4357
|
+
/**
|
|
4358
|
+
* `asc coordination` — 밖에 물은 것이 실제로 나갔는가, 답이 왔는가.
|
|
4359
|
+
*
|
|
4360
|
+
* **읽기만 한다.** 이 화면이 상태를 만들지 않는다는 것이 요점이다 — 보이는 것은 전부
|
|
4361
|
+
* 기대와 증거에서 파생한 값이다.
|
|
4362
|
+
*/
|
|
4363
|
+
async function runCoordination(command, values, store, root, resolved) {
|
|
4364
|
+
if (command === 'publish')
|
|
4365
|
+
return runCoordinationPublish(values, store, resolved);
|
|
4366
|
+
if (command === 'observe')
|
|
4367
|
+
return runCoordinationObserve(values, store, root, resolved);
|
|
4368
|
+
if (command !== undefined && command !== 'status') {
|
|
4369
|
+
console.error(`Unknown coordination command: ${command}\n\n${USAGE}`);
|
|
4370
|
+
return 2;
|
|
4371
|
+
}
|
|
4372
|
+
const views = await coordinationNow(store, resolved);
|
|
4373
|
+
if (values.json) {
|
|
4374
|
+
console.log(JSON.stringify({ coordination: views }, null, 2));
|
|
4375
|
+
return 0;
|
|
4376
|
+
}
|
|
4377
|
+
for (const line of coordinationLines(views))
|
|
4378
|
+
console.log(line);
|
|
4379
|
+
return 0;
|
|
4380
|
+
}
|
|
4144
4381
|
async function runFront(command, values, store, root) {
|
|
4145
4382
|
if (command !== undefined && command !== 'status') {
|
|
4146
4383
|
console.error(`Unknown front command: ${command}
|
|
@@ -4158,6 +4395,7 @@ ${USAGE}`);
|
|
|
4158
4395
|
escalations: await escalationLedger(store).pending(),
|
|
4159
4396
|
health: evaluateHealth(await new CoverageLedger(store.scope(scope)).health(), new Date().toISOString(), HEALTH_THRESHOLDS),
|
|
4160
4397
|
...(located ? { workspace: { workspaceId: located.workspaceId, locator: located.locator } } : {}),
|
|
4398
|
+
coordination: await coordinationNow(store),
|
|
4161
4399
|
// 도는 세션을 누가 집고 있는지. --physical 을 다시 물어보게 하지 않는다 (L-4).
|
|
4162
4400
|
bindings: claudeBindings(store),
|
|
4163
4401
|
});
|
|
@@ -4,12 +4,15 @@ import type { EventSource } from '../ports/event-source.ts';
|
|
|
4
4
|
import type { InventoryPort } from '../ports/inventory.ts';
|
|
5
5
|
import type { ResourceContextPort } from '../ports/resource-context.ts';
|
|
6
6
|
import type { ScmPort } from '../ports/scm.ts';
|
|
7
|
+
import type { CoordinationSurfacePort } from '../ports/coordination-surface.ts';
|
|
7
8
|
export type RuntimePorts = {
|
|
8
9
|
eventSource?: EventSource;
|
|
9
10
|
scm?: ScmPort;
|
|
10
11
|
inventory?: InventoryPort;
|
|
11
12
|
resourceContext?: ResourceContextPort;
|
|
12
13
|
changeContext?: ChangeContextPort;
|
|
14
|
+
/** 밖에 물은 것이 실제로 있게 하는 통로 (C-04·C-10). 없으면 게시는 일어나지 않는다. */
|
|
15
|
+
coordinationSurface?: CoordinationSurfacePort;
|
|
13
16
|
/** 무엇을 왜 못 만들었는지. 조용히 빠지면 사람이 이유를 알 수 없다. */
|
|
14
17
|
unavailable: string[];
|
|
15
18
|
};
|
|
@@ -13,6 +13,7 @@ import { GitHubChangeContext, GitHubInventory, GitHubResourceContext } from "../
|
|
|
13
13
|
import { GitHubEventSource } from "../adapters/github/event-source.js";
|
|
14
14
|
import { GitHubScm } from "../adapters/github/scm.js";
|
|
15
15
|
import { GitLabClient, GlabApiClient, discoverToken as discoverGitLabToken, glabAvailable, } from "../adapters/gitlab/client.js";
|
|
16
|
+
import { GitLabCoordinationSurface } from "../adapters/gitlab/coordination.js";
|
|
16
17
|
import { GitLabChangeContext, GitLabEventSource, GitLabInventory, GitLabResourceContext, } from "../adapters/gitlab/ports.js";
|
|
17
18
|
import { JamMcpClient } from "../adapters/jam/mcp-client.js";
|
|
18
19
|
import { JamEventSource } from "../adapters/jam/event-source.js";
|
|
@@ -30,7 +31,10 @@ const FACTORIES = {
|
|
|
30
31
|
inventory: new GitLabInventory({ client, project }),
|
|
31
32
|
resourceContext: new GitLabResourceContext({ client, project }),
|
|
32
33
|
changeContext: new GitLabChangeContext({ client, project }),
|
|
33
|
-
//
|
|
34
|
+
// 조율 표면. 토큰 통로일 때만 쓰기가 가능하다 — 도구를 통로로 쓰는 경우도 POST 를
|
|
35
|
+
// 대신 보내 준다. 둘 다 아니면 create 가 그 사실을 그대로 말한다.
|
|
36
|
+
coordinationSurface: new GitLabCoordinationSurface({ reader: client, writer: client, project }),
|
|
37
|
+
// canonical 통로는 아직 없다. 없는 것을 있는 척하지 않는다.
|
|
34
38
|
};
|
|
35
39
|
},
|
|
36
40
|
github(binding, input, token) {
|
|
@@ -112,6 +116,7 @@ const PORT_OF = {
|
|
|
112
116
|
'context.change': 'changeContext',
|
|
113
117
|
'context.resource': 'resourceContext',
|
|
114
118
|
'canonical.read': 'scm',
|
|
119
|
+
'coordination.surface': 'coordinationSurface',
|
|
115
120
|
};
|
|
116
121
|
/**
|
|
117
122
|
* capability가 필요한 자리마다 어느 binding이 맡을지 정해 Port를 만든다.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 여기에 provider 이름을 넣지 않는다 — `scm.<제품>` 같은 값은 capability가 아니라
|
|
5
5
|
* adapter identity이며, 그것으로 갈라지기 시작하면 Core가 provider를 아는 셈이 된다.
|
|
6
6
|
*/
|
|
7
|
-
export declare const CAPABILITIES: readonly ["observe.delta", "inventory.enumerate", "context.resource", "context.thread", "context.change", "context.history", "canonical.read", "action.comment", "action.update", "presentation.digest", "presentation.priority", "approval.interactive", "identity.resolve"];
|
|
7
|
+
export declare const CAPABILITIES: readonly ["observe.delta", "inventory.enumerate", "context.resource", "context.thread", "context.change", "context.history", "canonical.read", "action.comment", "coordination.surface", "action.update", "presentation.digest", "presentation.priority", "approval.interactive", "identity.resolve"];
|
|
8
8
|
export type Capability = (typeof CAPABILITIES)[number];
|
|
9
9
|
/** describe() 산출물 — 호출도 네트워크도 없는 정적 선언 (C-09 §5). */
|
|
10
10
|
export type AdapterDescriptor = {
|
|
@@ -20,3 +20,29 @@ export type ResolveDeps = {
|
|
|
20
20
|
*/
|
|
21
21
|
export declare function shimTarget(shimText: string): string | null;
|
|
22
22
|
export declare function resolveExternalCommand(command: string, args: readonly string[], deps?: ResolveDeps): ResolvedInvocation;
|
|
23
|
+
/**
|
|
24
|
+
* PATH 에서 실행 파일 하나를 찾는다 (POSIX). 없으면 null — 있는 척하지 않는다.
|
|
25
|
+
*
|
|
26
|
+
* Windows 는 위 `resolveExternalCommand` 가 shim 까지 풀어 주므로 여기서는 다루지 않는다.
|
|
27
|
+
*/
|
|
28
|
+
export declare function findOnPath(command: string, deps?: {
|
|
29
|
+
env?: NodeJS.ProcessEnv;
|
|
30
|
+
exists?: (path: string) => boolean;
|
|
31
|
+
platform?: NodeJS.Platform;
|
|
32
|
+
}): string | null;
|
|
33
|
+
/** 시스템 기본 자리. 서비스 관리자가 주는 PATH 가 대개 이것이다. */
|
|
34
|
+
export declare const SYSTEM_PATH: readonly ["/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"];
|
|
35
|
+
/**
|
|
36
|
+
* 서비스에 실어 보낼 PATH — **지금 이 셸의 PATH 를 통째로 옮기지 않는다.** 세션마다 붙는
|
|
37
|
+
* 임시 디렉터리가 그대로 들어가면 등록물이 매번 STALE 이 되고, 사라진 경로가 남는다.
|
|
38
|
+
* 대신 필요한 실행 파일이 **실제로 있는 디렉터리만** 고른다. 못 찾은 도구는 조용히 빠지지
|
|
39
|
+
* 않고 `missing` 에 남는다 — 그 도구를 쓰는 통로는 서비스에서도 열리지 않을 것이다.
|
|
40
|
+
*/
|
|
41
|
+
export declare function servicePath(tools: readonly string[], deps?: {
|
|
42
|
+
env?: NodeJS.ProcessEnv;
|
|
43
|
+
exists?: (path: string) => boolean;
|
|
44
|
+
platform?: NodeJS.Platform;
|
|
45
|
+
}): {
|
|
46
|
+
path: string;
|
|
47
|
+
missing: string[];
|
|
48
|
+
};
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// ③ shim을 못 읽으면 cmd.exe /d /c 로 그 .cmd 를 부른다 — cmd.exe는 진짜 실행 파일이라
|
|
14
14
|
// shell 옵션이 필요 없다.
|
|
15
15
|
// 셋 다 실패하면 이름 그대로 돌려준다 — PATH에 진짜 실행 파일이 있는 환경이 그 경우다.
|
|
16
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
16
|
+
import { accessSync, constants, existsSync, readFileSync, statSync } from 'node:fs';
|
|
17
17
|
import { delimiter as winDelimiter, dirname, extname, isAbsolute, join } from 'node:path/win32';
|
|
18
18
|
const defaultRead = (path) => {
|
|
19
19
|
try {
|
|
@@ -77,3 +77,60 @@ export function resolveExternalCommand(command, args, deps = {}) {
|
|
|
77
77
|
}
|
|
78
78
|
return { command, args: [...args] };
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* PATH 에서 실행 파일 하나를 찾는다 (POSIX). 없으면 null — 있는 척하지 않는다.
|
|
82
|
+
*
|
|
83
|
+
* Windows 는 위 `resolveExternalCommand` 가 shim 까지 풀어 주므로 여기서는 다루지 않는다.
|
|
84
|
+
*/
|
|
85
|
+
export function findOnPath(command, deps = {}) {
|
|
86
|
+
const platform = deps.platform ?? process.platform;
|
|
87
|
+
if (platform === 'win32')
|
|
88
|
+
return null;
|
|
89
|
+
if (command.includes('/'))
|
|
90
|
+
return (deps.exists ?? isExecutable)(command) ? command : null;
|
|
91
|
+
const exists = deps.exists ?? isExecutable;
|
|
92
|
+
const pathValue = (deps.env ?? process.env).PATH ?? '';
|
|
93
|
+
for (const dir of pathValue.split(':')) {
|
|
94
|
+
if (!dir)
|
|
95
|
+
continue;
|
|
96
|
+
const candidate = `${dir}/${command}`;
|
|
97
|
+
if (exists(candidate))
|
|
98
|
+
return candidate;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const isExecutable = (path) => {
|
|
103
|
+
try {
|
|
104
|
+
accessSync(path, constants.X_OK);
|
|
105
|
+
return statSync(path).isFile();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
/** 시스템 기본 자리. 서비스 관리자가 주는 PATH 가 대개 이것이다. */
|
|
112
|
+
export const SYSTEM_PATH = ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin'];
|
|
113
|
+
/**
|
|
114
|
+
* 서비스에 실어 보낼 PATH — **지금 이 셸의 PATH 를 통째로 옮기지 않는다.** 세션마다 붙는
|
|
115
|
+
* 임시 디렉터리가 그대로 들어가면 등록물이 매번 STALE 이 되고, 사라진 경로가 남는다.
|
|
116
|
+
* 대신 필요한 실행 파일이 **실제로 있는 디렉터리만** 고른다. 못 찾은 도구는 조용히 빠지지
|
|
117
|
+
* 않고 `missing` 에 남는다 — 그 도구를 쓰는 통로는 서비스에서도 열리지 않을 것이다.
|
|
118
|
+
*/
|
|
119
|
+
export function servicePath(tools, deps = {}) {
|
|
120
|
+
const dirs = [];
|
|
121
|
+
const missing = [];
|
|
122
|
+
const push = (dir) => {
|
|
123
|
+
if (!dirs.includes(dir))
|
|
124
|
+
dirs.push(dir);
|
|
125
|
+
};
|
|
126
|
+
for (const tool of tools) {
|
|
127
|
+
const found = findOnPath(tool, deps);
|
|
128
|
+
if (found)
|
|
129
|
+
push(found.slice(0, found.lastIndexOf('/')) || '/');
|
|
130
|
+
else
|
|
131
|
+
missing.push(tool);
|
|
132
|
+
}
|
|
133
|
+
for (const dir of SYSTEM_PATH)
|
|
134
|
+
push(dir);
|
|
135
|
+
return { path: dirs.join(':'), missing };
|
|
136
|
+
}
|
|
@@ -14,6 +14,15 @@ export type ServiceCommand = {
|
|
|
14
14
|
args: readonly string[];
|
|
15
15
|
/** 회차 간격(초). Core 상수가 아니다 — 호출자가 정한다 (C-12 불변식 ③). */
|
|
16
16
|
intervalSeconds: number;
|
|
17
|
+
/**
|
|
18
|
+
* 등록물이 돌 때의 환경. **서비스는 로그인 셸의 PATH 를 물려받지 않는다** — 실기계에서
|
|
19
|
+
* launchd 가 준 PATH 는 `/usr/bin:/bin:/usr/sbin:/sbin` 뿐이라 외부 통로 실행 파일을
|
|
20
|
+
* 하나도 못 찾았다. 등록하는 쪽이 필요한 것을 결정해 여기 실어 보내고, 내용이 바뀌면
|
|
21
|
+
* 등록물이 STALE 로 드러난다 — 셸 설정을 추측하는 경로는 없다.
|
|
22
|
+
*/
|
|
23
|
+
environment?: Readonly<Record<string, string>>;
|
|
24
|
+
/** 회차의 표준 출력·오류를 남길 파일. 없으면 OS 기본(대개 버려진다). */
|
|
25
|
+
logPath?: string;
|
|
17
26
|
};
|
|
18
27
|
/** 지금 이 기계의 등록 상태. */
|
|
19
28
|
export type ServiceState =
|
|
@@ -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.5.0";
|
|
5
|
+
export declare const RUNTIME_SPEC = "@asc-agent/runtime@0.5.0";
|
|
6
|
+
export declare const BOOTSTRAP_SPEC = "@asc-agent/bootstrap@0.5.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.5.0';
|
|
13
13
|
export const RUNTIME_SPEC = `${RUNTIME_PACKAGE}@${RELEASE_VERSION}`;
|
|
14
14
|
export const BOOTSTRAP_SPEC = `${BOOTSTRAP_PACKAGE}@${RELEASE_VERSION}`;
|
|
15
15
|
/**
|