@asc-agent/runtime 0.4.0 → 0.5.1
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/claude-code/guard.js +26 -1
- package/dist/adapters/claude-code/install.d.ts +27 -0
- package/dist/adapters/claude-code/install.js +3 -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 +569 -17
- package/dist/composition/runtime.d.ts +20 -0
- package/dist/composition/runtime.js +32 -1
- package/dist/core/attach/setup-plan.d.ts +107 -6
- package/dist/core/attach/setup-plan.js +104 -6
- package/dist/core/attach/setup.d.ts +3 -1
- package/dist/core/attach/setup.js +25 -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/distribution/service-runtime.d.ts +40 -0
- package/dist/core/distribution/service-runtime.js +76 -0
- 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/dist/schemas/profile.d.ts +2 -2
- package/package.json +1 -1
package/dist/cli/asc.js
CHANGED
|
@@ -14,7 +14,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
14
14
|
import { homedir, hostname, userInfo } from 'node:os';
|
|
15
15
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
16
16
|
import { MINIMUM_NODE_MAJOR, checkNodeRuntime, reexecWithCandidate, } from "../core/distribution/node-runtime.js";
|
|
17
|
-
import { RELEASE_VERSION } from "../core/distribution/release.js";
|
|
17
|
+
import { RELEASE_VERSION, RUNTIME_PACKAGE } from "../core/distribution/release.js";
|
|
18
18
|
import { GitHubClient, discoverToken } from "../adapters/github/client.js";
|
|
19
19
|
import { GitHubChangeContext, GitHubInventory, GitHubResourceContext } from "../adapters/github/context.js";
|
|
20
20
|
import { GitHubEventSource } from "../adapters/github/event-source.js";
|
|
@@ -46,6 +46,9 @@ 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 { isTransientPath, resolveServiceRuntime, serviceRuntimeLine, } from "../core/distribution/service-runtime.js";
|
|
51
|
+
import { LocalCanonicalReader } from "../adapters/local/canonical.js";
|
|
49
52
|
import { LocalRepoAdapter } from "../adapters/local/repo.js";
|
|
50
53
|
import { GitHubAdapter } from "../adapters/github/adapter.js";
|
|
51
54
|
import { GitLabAdapter } from "../adapters/gitlab/adapter.js";
|
|
@@ -54,7 +57,7 @@ import { healJam, healLine } from "../adapters/jam/setup.js";
|
|
|
54
57
|
import { statusIndicatesDone } from "../adapters/jam/ports.js";
|
|
55
58
|
import { ProgressService } from "../core/operator/progress.js";
|
|
56
59
|
import { composeBindings, defaultAdapters } from "../composition/registry.js";
|
|
57
|
-
import { buildObservationChannels, buildRuntimePorts, closeToolClients, rolesFor } from "../composition/runtime.js";
|
|
60
|
+
import { buildObservationChannels, buildRuntimePorts, closeToolClients, rolesFor, workItemRoles, } from "../composition/runtime.js";
|
|
58
61
|
import { proposeBindings } from "../composition/propose.js";
|
|
59
62
|
import { buildEventObservation } from "../composition/observe.js";
|
|
60
63
|
import { availableProfiles, planBootstrap, renderPlan } from "../core/attach/bootstrap.js";
|
|
@@ -69,12 +72,14 @@ import { AuditLedger, decisionLines, delegationLine, executionLines, reclaimLine
|
|
|
69
72
|
import { ClosureLedger } from "../core/runtime/closure.js";
|
|
70
73
|
import { Orchestrator, renderTick } from "../core/runtime/orchestrator.js";
|
|
71
74
|
import { LAST_RUN_KEY, RuntimeLease, fileScope, readBackground, renderBackground, staleAfter, } from "../core/runtime/background.js";
|
|
72
|
-
import { dueWorkspaces, renderWorkspaces,
|
|
75
|
+
import { dueWorkspaces, renderWorkspaces, summarizePass, viewWorkspaces } from "../core/runtime/workspaces.js";
|
|
73
76
|
import { persistentRuntimeLine, planPersistentRuntime, } from "../core/distribution/persistent-runtime.js";
|
|
74
77
|
import { launchdAdapter } from "../adapters/service/launchd.js";
|
|
75
78
|
import { schtasksAdapter } from "../adapters/service/schtasks.js";
|
|
76
79
|
import { serviceAdapterFor, systemdUserAdapter } from "../adapters/service/systemd-user.js";
|
|
77
80
|
import { QueryLedger } from "../core/runtime/query.js";
|
|
81
|
+
import { CoordinationLedger, coordinationLines, responsesFrom, viewCoordination, } from "../core/runtime/coordination.js";
|
|
82
|
+
import { publishLine, publishOnce, recordPublication } from "../core/runtime/publish.js";
|
|
78
83
|
import { ObservationLedger } from "../core/monitor/observation.js";
|
|
79
84
|
import { DeliveryLedger, deliver, planDigest } from "../core/presentation/digest.js";
|
|
80
85
|
import { LocalPresentation } from "../adapters/local/presentation.js";
|
|
@@ -191,6 +196,11 @@ const USAGE = `asc — Agent Session Control
|
|
|
191
196
|
[--to <authority>]
|
|
192
197
|
asc query list [--json]
|
|
193
198
|
|
|
199
|
+
asc coordination [status] [--json] # what was asked outside, and whether it reached anyone
|
|
200
|
+
asc coordination publish --query <ID> --title <text> --body-file <path>
|
|
201
|
+
[--audience <who>] [--known <objectId>] [--work <ref>] [--json]
|
|
202
|
+
asc coordination observe [--json] # did anything come back on what we published
|
|
203
|
+
|
|
194
204
|
asc progress show [<S-ID>]
|
|
195
205
|
asc progress report <S-ID> --physical <id> --phase <text>
|
|
196
206
|
[--milestone <text>...] [--next <text>] [--unresolved <text>...]
|
|
@@ -577,6 +587,11 @@ function parseArgsOrThrow(argv) {
|
|
|
577
587
|
validator: { type: 'string' },
|
|
578
588
|
result: { type: 'string' },
|
|
579
589
|
finding: { type: 'string', multiple: true },
|
|
590
|
+
query: { type: 'string' },
|
|
591
|
+
title: { type: 'string' },
|
|
592
|
+
'body-file': { type: 'string' },
|
|
593
|
+
audience: { type: 'string', multiple: true },
|
|
594
|
+
known: { type: 'string', multiple: true },
|
|
580
595
|
blocker: { type: 'string', multiple: true },
|
|
581
596
|
risk: { type: 'string', multiple: true },
|
|
582
597
|
evidence: { type: 'string', multiple: true },
|
|
@@ -718,7 +733,7 @@ async function runParsedCommand(values, positionals, entry, argv) {
|
|
|
718
733
|
// 순서가 뒤집힌다. 나머지 profile 명령은 아래 attach 경로에 그대로 남는다.
|
|
719
734
|
if (group === 'profile' && command === 'adopt')
|
|
720
735
|
return runProfileAdopt(values, entry);
|
|
721
|
-
if (!['inbox', 'grant', 'monitor', 'runtime', 'front', 'freeze', 'thaw', 'escalate', 'profile', 'session', 'controller', 'proceed', 'progress', 'preflight', 'closure', 'query'].includes(group)) {
|
|
736
|
+
if (!['inbox', 'grant', 'monitor', 'runtime', 'front', 'coordination', 'freeze', 'thaw', 'escalate', 'profile', 'session', 'controller', 'proceed', 'progress', 'preflight', 'closure', 'query'].includes(group)) {
|
|
722
737
|
console.error(`Unknown command: ${group}\n\n${USAGE}`);
|
|
723
738
|
return 2;
|
|
724
739
|
}
|
|
@@ -760,6 +775,9 @@ async function runParsedCommand(values, positionals, entry, argv) {
|
|
|
760
775
|
// 새 대화가 붙었을 때 지금 상태를 되찾는다 (C-12 §4). 읽기만 한다.
|
|
761
776
|
if (group === 'front')
|
|
762
777
|
return runFront(command, values, store, root);
|
|
778
|
+
// 밖에 물은 것이 실제로 전달됐는가. 읽기만 한다.
|
|
779
|
+
if (group === 'coordination')
|
|
780
|
+
return runCoordination(command, values, store, root, guard.runtime);
|
|
763
781
|
// 사람에게 올릴 자격이 있는가 (C-13). 자격 없으면 request가 만들어지지 않는다.
|
|
764
782
|
if (group === 'escalate')
|
|
765
783
|
return runEscalate(command, target, values, store, guard.runtime);
|
|
@@ -1314,6 +1332,207 @@ async function runSetup(command, values, entry = 'runtime') {
|
|
|
1314
1332
|
* Core의 planner는 파일도 network도 모른다 — 사실은 여기서 관측해 넘긴다. 그래야
|
|
1315
1333
|
* "이 명령이 무엇을 바꿀 것인가"를 아무것도 바꾸지 않고 물어볼 수 있다.
|
|
1316
1334
|
*/
|
|
1335
|
+
/**
|
|
1336
|
+
* 이 저장소가 스스로 증명하는 Profile 이름 (P0 F2).
|
|
1337
|
+
*
|
|
1338
|
+
* **파일을 만들지 않는다** — 이름과 "이미 있는가"만 관측한다. 다른 논리 workspace 를
|
|
1339
|
+
* 찾아 합치는 것이 아니라, 이 checkout 의 remote 하나에서 읽히는 신원이다 (C-11 유지).
|
|
1340
|
+
*/
|
|
1341
|
+
async function adoptableState(projectRoot, git) {
|
|
1342
|
+
if (!git)
|
|
1343
|
+
return {};
|
|
1344
|
+
const remotes = await gitRemotes(projectRoot);
|
|
1345
|
+
if (remotes.length === 0)
|
|
1346
|
+
return {};
|
|
1347
|
+
try {
|
|
1348
|
+
const adopted = buildAdoptedProfile({
|
|
1349
|
+
dirName: basename(projectRoot),
|
|
1350
|
+
remotes,
|
|
1351
|
+
scmForHost: (host) => (host === 'github.com' ? 'github' : 'git'),
|
|
1352
|
+
});
|
|
1353
|
+
// **공유 주소가 없으면 후보가 아니다.** remote 가 프로젝트 신원을 증명하지 못하면
|
|
1354
|
+
// 그것은 이 기계 안의 폴더일 뿐이고, 무엇으로 붙을지는 사람이 정한다.
|
|
1355
|
+
const project = adopted.profile.project;
|
|
1356
|
+
if (!project?.repository || project.repository.startsWith('local/'))
|
|
1357
|
+
return {};
|
|
1358
|
+
const exists = existsSync(join(externalProfileRoot(), adopted.id, 'profile.json'));
|
|
1359
|
+
return { adoptable: { id: adopted.id, exists } };
|
|
1360
|
+
}
|
|
1361
|
+
catch {
|
|
1362
|
+
// 이름을 만들 수 없는 저장소가 있다. 그때는 이 축이 없는 것이고, 사람이 고른다.
|
|
1363
|
+
return {};
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* 승인 권한자가 서 있는가, 그리고 지금 이 사람을 무엇으로 부를 수 있는가 (P0 F2).
|
|
1368
|
+
*
|
|
1369
|
+
* **인증된 provider 가 말한 이름을 쓴다.** ASC 가 지어내지 않고, 자격 값은 읽지 않는다.
|
|
1370
|
+
*/
|
|
1371
|
+
async function identityState(ascRoot, workspaceComing) {
|
|
1372
|
+
const wired = ascRoot ? Object.keys(await loadIdentityMap(ascRoot)).length > 0 : false;
|
|
1373
|
+
if (wired)
|
|
1374
|
+
return { identity: { wired: true } };
|
|
1375
|
+
// **세울 workspace 가 없으면 누구인지 묻지 않는다.** 물으려면 provider CLI 를 불러야
|
|
1376
|
+
// 하고, 그 도구는 자기 설정 파일을 만든다 — 멈출 계획이 남기는 자국이 되면 안 된다.
|
|
1377
|
+
if (!workspaceComing)
|
|
1378
|
+
return { identity: { wired: false } };
|
|
1379
|
+
const actor = await detectActor();
|
|
1380
|
+
return { identity: { wired: false, ...(actor ? { actor } : {}) } };
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* 지금 이 사람을 `<channel>:<actor>` 로. 인증된 통로가 먼저다 — 그것이 실제로 밖에서
|
|
1384
|
+
* 나를 부르는 이름이고, git 의 표시 이름은 아무 계정과도 이어지지 않는다.
|
|
1385
|
+
*/
|
|
1386
|
+
async function detectActor() {
|
|
1387
|
+
const gitlab = await execText('glab', ['api', 'user']);
|
|
1388
|
+
const gitlabName = readJsonField(gitlab, 'username');
|
|
1389
|
+
if (gitlabName)
|
|
1390
|
+
return `gitlab:${gitlabName}`;
|
|
1391
|
+
const github = await execText('gh', ['api', 'user']);
|
|
1392
|
+
const githubName = readJsonField(github, 'login');
|
|
1393
|
+
if (githubName)
|
|
1394
|
+
return `github:${githubName}`;
|
|
1395
|
+
const local = await detectSelf();
|
|
1396
|
+
return local ? `local:${local}` : null;
|
|
1397
|
+
}
|
|
1398
|
+
function readJsonField(text, field) {
|
|
1399
|
+
if (!text)
|
|
1400
|
+
return null;
|
|
1401
|
+
try {
|
|
1402
|
+
const value = JSON.parse(text)[field];
|
|
1403
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
1404
|
+
}
|
|
1405
|
+
catch {
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* 발견이 증명하는 결합 (P0 F2).
|
|
1411
|
+
*
|
|
1412
|
+
* **역할은 capability 로 정한다** — provider 이름으로 정하면 이 자리가 provider 목록이
|
|
1413
|
+
* 된다. 변경을 아는 통로가 code, 작업 항목을 세는 통로가 work 다. 같은 역할에 후보가
|
|
1414
|
+
* 둘이면 origin remote 가 가리키는 쪽을 쓰고, 그것으로도 갈리면 아무것도 제안하지 않는다.
|
|
1415
|
+
*/
|
|
1416
|
+
async function bindingProposalState(projectRoot, profileId, willCreate) {
|
|
1417
|
+
if (!profileId)
|
|
1418
|
+
return {};
|
|
1419
|
+
// 이미 선언이 있으면 손대지 않는다 — 사람이 적은 것이 먼저다.
|
|
1420
|
+
// 파일이 아직 없어도 **이번에 만들 것이면** 비어 있는 것과 같다: adopt 는 결합을
|
|
1421
|
+
// 적지 않으므로, 그 사실을 여기서 미리 알아야 한 번의 apply 로 끝난다.
|
|
1422
|
+
const declared = await readProfileBindings(profileId);
|
|
1423
|
+
if (declared === null && !willCreate)
|
|
1424
|
+
return {};
|
|
1425
|
+
if (declared !== null && declared.length > 0)
|
|
1426
|
+
return {};
|
|
1427
|
+
const adapters = monitorAdapters();
|
|
1428
|
+
const plan = await composeBindings({ context: { projectRoot, env: process.env }, adapters, roles: [] });
|
|
1429
|
+
const usable = plan.bindings.filter((binding) => binding.state === 'AVAILABLE' || binding.state === 'DEGRADED');
|
|
1430
|
+
const origin = (await gitRemotes(projectRoot)).find((remote) => remote.name === 'origin');
|
|
1431
|
+
const originAlias = origin ? normalizeRemote(origin.url) : null;
|
|
1432
|
+
const byRole = new Map();
|
|
1433
|
+
for (const binding of usable) {
|
|
1434
|
+
const role = binding.provides.includes('context.change')
|
|
1435
|
+
? 'code-primary'
|
|
1436
|
+
: binding.provides.includes('inventory.enumerate')
|
|
1437
|
+
? 'work'
|
|
1438
|
+
: null;
|
|
1439
|
+
if (!role)
|
|
1440
|
+
continue;
|
|
1441
|
+
byRole.set(role, [...(byRole.get(role) ?? []), { role, adapter: binding.adapterId, resource: binding.resource }]);
|
|
1442
|
+
}
|
|
1443
|
+
const proposal = [];
|
|
1444
|
+
for (const [role, candidates] of byRole) {
|
|
1445
|
+
if (candidates.length === 1) {
|
|
1446
|
+
proposal.push(candidates[0]);
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
// origin 이 가리키는 것 하나면 그것이다 — 이 저장소의 주소가 그 판정의 근거다.
|
|
1450
|
+
const matching = originAlias
|
|
1451
|
+
? candidates.filter((candidate) => originAlias.endsWith(`/${candidate.resource}`))
|
|
1452
|
+
: [];
|
|
1453
|
+
if (matching.length === 1)
|
|
1454
|
+
proposal.push(matching[0]);
|
|
1455
|
+
else
|
|
1456
|
+
return {};
|
|
1457
|
+
}
|
|
1458
|
+
return proposal.length > 0 ? { bindingProposal: proposal } : {};
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* 발견이 증명한 결합을 Profile 파일에 적는다.
|
|
1462
|
+
*
|
|
1463
|
+
* **이미 있는 선언은 건드리지 않는다** — 사람이 적은 것이 먼저다. 그 밖의 필드도 그대로
|
|
1464
|
+
* 둔다: 이 함수가 아는 것은 `bindings` 한 칸뿐이다.
|
|
1465
|
+
*/
|
|
1466
|
+
async function writeProfileBindings(profileId, bindings) {
|
|
1467
|
+
const path = join(externalProfileRoot(), profileId, 'profile.json');
|
|
1468
|
+
const profile = JSON.parse(await readFile(path, 'utf8'));
|
|
1469
|
+
if (Array.isArray(profile.bindings) && profile.bindings.length > 0)
|
|
1470
|
+
return false;
|
|
1471
|
+
profile.bindings = bindings.map((binding) => ({ ...binding }));
|
|
1472
|
+
await writeFile(path, `${JSON.stringify(profile, null, 2)}\n`, 'utf8');
|
|
1473
|
+
console.log(`bindings declared in ${profileId}: ${bindings.map((b) => `${b.role}=${b.adapter}`).join(', ')}`);
|
|
1474
|
+
return true;
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* 이 checkout 이 증명하는 정본 갈래 (P0 F5).
|
|
1478
|
+
*
|
|
1479
|
+
* **remote 에게 물어본다.** 로컬 `origin/HEAD` 는 clone 시점에 고정돼 낡는다 — 이 저장소에서
|
|
1480
|
+
* 그 값은 `main` 인데 remote 의 기본 branch 는 `develop` 이었다. 낡은 값을 정본으로 적으면
|
|
1481
|
+
* 세션이 엉뚱한 baseline 을 딛는다. 물어보지 못하면 적지 않는다.
|
|
1482
|
+
*/
|
|
1483
|
+
async function canonicalProposalState(projectRoot, git, profileId, willCreate) {
|
|
1484
|
+
if (!git || !profileId)
|
|
1485
|
+
return {};
|
|
1486
|
+
const declared = await readProfileCanonical(profileId);
|
|
1487
|
+
if (declared === null && !willCreate)
|
|
1488
|
+
return {};
|
|
1489
|
+
if (declared !== null && declared > 0)
|
|
1490
|
+
return {};
|
|
1491
|
+
const symref = await execText('git', ['-C', projectRoot, 'ls-remote', '--symref', 'origin', 'HEAD']);
|
|
1492
|
+
const match = symref ? /^ref:\s+refs\/heads\/(\S+)\s+HEAD$/m.exec(symref) : null;
|
|
1493
|
+
const branch = match?.[1];
|
|
1494
|
+
if (!branch)
|
|
1495
|
+
return {};
|
|
1496
|
+
// provider 는 `git` 이다 — 이 값은 checkout 이 이미 들고 있는 사실이고, 그것을 읽는 통로가
|
|
1497
|
+
// 선언된 provider 를 따른다 (canonical baseline 읽기).
|
|
1498
|
+
return { canonicalProposal: { id: branch, provider: 'git', remote: 'origin', ref: branch } };
|
|
1499
|
+
}
|
|
1500
|
+
/** Profile 이 선언한 정본 갈래 수. 파일이 없으면 `null`. */
|
|
1501
|
+
async function readProfileCanonical(profileId) {
|
|
1502
|
+
const path = join(externalProfileRoot(), profileId, 'profile.json');
|
|
1503
|
+
try {
|
|
1504
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
1505
|
+
return Array.isArray(parsed.canonical?.sources) ? parsed.canonical.sources.length : 0;
|
|
1506
|
+
}
|
|
1507
|
+
catch {
|
|
1508
|
+
return null;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* remote 가 말한 정본 갈래를 Profile 에 적는다. 이미 선언이 있으면 손대지 않는다.
|
|
1513
|
+
*/
|
|
1514
|
+
async function writeProfileCanonical(profileId, source) {
|
|
1515
|
+
const path = join(externalProfileRoot(), profileId, 'profile.json');
|
|
1516
|
+
const profile = JSON.parse(await readFile(path, 'utf8'));
|
|
1517
|
+
const canonical = (profile.canonical ?? {});
|
|
1518
|
+
if (Array.isArray(canonical.sources) && canonical.sources.length > 0)
|
|
1519
|
+
return false;
|
|
1520
|
+
profile.canonical = { ...canonical, sources: [source] };
|
|
1521
|
+
await writeFile(path, `${JSON.stringify(profile, null, 2)}\n`, 'utf8');
|
|
1522
|
+
console.log(`canonical source declared in ${profileId}: ${source.remote}/${source.ref}`);
|
|
1523
|
+
return true;
|
|
1524
|
+
}
|
|
1525
|
+
/** Profile 이 이미 선언한 결합. 파일이 없으면 `null` — 없는 것과 비어 있는 것은 다르다. */
|
|
1526
|
+
async function readProfileBindings(profileId) {
|
|
1527
|
+
const path = join(externalProfileRoot(), profileId, 'profile.json');
|
|
1528
|
+
try {
|
|
1529
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
1530
|
+
return Array.isArray(parsed.bindings) ? parsed.bindings : [];
|
|
1531
|
+
}
|
|
1532
|
+
catch {
|
|
1533
|
+
return null;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1317
1536
|
async function detectSetupState(values, entry) {
|
|
1318
1537
|
const { root: projectRoot, git } = await discoverProjectRoot(process.cwd());
|
|
1319
1538
|
const resolution = await resolveRoot(process.cwd(), values.root);
|
|
@@ -1323,6 +1542,10 @@ async function detectSetupState(values, entry) {
|
|
|
1323
1542
|
const attachmentBroken = ascRoot ? (await inspectSetup(ascRoot)).attachment === 'BROKEN' : false;
|
|
1324
1543
|
const scope = values.scope === 'project' ? 'project' : 'local';
|
|
1325
1544
|
const hostReport = await verifyInstall(hostPaths());
|
|
1545
|
+
const adoptable = await adoptableState(projectRoot, git);
|
|
1546
|
+
const attachedProfile = ascRoot ? await lockedProfileId(ascRoot) : undefined;
|
|
1547
|
+
// 이번 계획이 쓸 Profile 하나 — 그것의 결합 선언이 비어 있을 때만 제안을 관측한다.
|
|
1548
|
+
const targetProfile = values.profile ?? attachedProfile ?? adoptable.adoptable?.id;
|
|
1326
1549
|
return {
|
|
1327
1550
|
entry,
|
|
1328
1551
|
projectRoot,
|
|
@@ -1332,6 +1555,12 @@ async function detectSetupState(values, entry) {
|
|
|
1332
1555
|
...(values.profile ? { requestedProfile: values.profile } : {}),
|
|
1333
1556
|
profileCandidates: await availableProfiles(installRoot(), externalProfileRoot()),
|
|
1334
1557
|
scope,
|
|
1558
|
+
...(attachedProfile ? { attachedProfile } : {}),
|
|
1559
|
+
// fresh onboarding 이 사람에게 되묻지 않으려면 이 셋이 관측돼 있어야 한다 (P0 F2).
|
|
1560
|
+
...adoptable,
|
|
1561
|
+
...(await identityState(ascRoot, Boolean(ascRoot) || Boolean(targetProfile))),
|
|
1562
|
+
...(await bindingProposalState(projectRoot, targetProfile, Boolean(adoptable.adoptable && !adoptable.adoptable.exists))),
|
|
1563
|
+
...(await canonicalProposalState(projectRoot, git, targetProfile, Boolean(adoptable.adoptable && !adoptable.adoptable.exists))),
|
|
1335
1564
|
host: [{ id: 'claude', status: hostReport.status }],
|
|
1336
1565
|
// Profile 이 작업 도구를 선언했으면 그 준비 상태까지 본다 (설계 §9.3).
|
|
1337
1566
|
...(await workBindingState(ascRoot, projectRoot)),
|
|
@@ -1350,7 +1579,10 @@ async function detectSetupState(values, entry) {
|
|
|
1350
1579
|
* 스스로 할 때만 의미가 있다.
|
|
1351
1580
|
*/
|
|
1352
1581
|
function runningFromInstalledPackage() {
|
|
1353
|
-
|
|
1582
|
+
const here = fileURLToPath(import.meta.url).replace(/\\/g, '/');
|
|
1583
|
+
// **npx 캐시도 node_modules 다.** 경로 모양만 보면 임시 자리가 설치본으로 통과하고,
|
|
1584
|
+
// 실제로 그렇게 통과해 등록물이 npx 캐시를 가리켰다 — 캐시를 지우면 깨진다.
|
|
1585
|
+
return here.includes('/node_modules/@asc-agent/runtime/') && !isTransientPath(here);
|
|
1354
1586
|
}
|
|
1355
1587
|
/**
|
|
1356
1588
|
* 지금 이 실행에서 기계 등록을 다뤄도 되는가.
|
|
@@ -1377,7 +1609,13 @@ async function persistentRuntimeState(values) {
|
|
|
1377
1609
|
const adapter = serviceAdapter();
|
|
1378
1610
|
if (!adapter)
|
|
1379
1611
|
return {};
|
|
1380
|
-
|
|
1612
|
+
// 등록물이 가리킬 자리가 없으면 이 축을 그리지 않는다 — 깨진 등록을 남기느니 등록하지
|
|
1613
|
+
// 않고 그 사실을 말한다 (P0 F1).
|
|
1614
|
+
const runtime = await serviceRuntime();
|
|
1615
|
+
if (runtime.kind !== 'STABLE') {
|
|
1616
|
+
return { persistentRuntime: { action: 'unsupported', adapter: adapter.id, detail: runtime.detail } };
|
|
1617
|
+
}
|
|
1618
|
+
const plan = await planPersistentRuntime(adapter, serviceCommand(serviceInterval(values), runtime)).catch(() => null);
|
|
1381
1619
|
if (!plan)
|
|
1382
1620
|
return {};
|
|
1383
1621
|
return {
|
|
@@ -1618,6 +1856,14 @@ async function runSetupLifecycle(command, values, entry) {
|
|
|
1618
1856
|
const speak = console.log;
|
|
1619
1857
|
if (asJson)
|
|
1620
1858
|
console.log = console.error;
|
|
1859
|
+
const relock = async (profile) => {
|
|
1860
|
+
const root = await discoverRoot(process.cwd(), values.root);
|
|
1861
|
+
if (!root)
|
|
1862
|
+
return;
|
|
1863
|
+
const code = await runProfile('resolve', { ...values, profile, write: true }, root);
|
|
1864
|
+
if (code !== 0)
|
|
1865
|
+
throw new Error(`profile re-lock 실패 (exit ${code})`);
|
|
1866
|
+
};
|
|
1621
1867
|
let outcome;
|
|
1622
1868
|
try {
|
|
1623
1869
|
outcome = await applySetupPlan(plan, {
|
|
@@ -1640,8 +1886,13 @@ async function runSetupLifecycle(command, values, entry) {
|
|
|
1640
1886
|
const adapter = serviceAdapter();
|
|
1641
1887
|
if (!adapter)
|
|
1642
1888
|
return;
|
|
1889
|
+
const runtime = await serviceRuntime();
|
|
1890
|
+
if (runtime.kind !== 'STABLE') {
|
|
1891
|
+
console.error(`Persistent runtime not registered — ${runtime.detail}`);
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1643
1894
|
try {
|
|
1644
|
-
await adapter.install(serviceCommand(serviceInterval(values)));
|
|
1895
|
+
await adapter.install(serviceCommand(serviceInterval(values), runtime));
|
|
1645
1896
|
console.log(`Persistent runtime registered with ${adapter.id}.`);
|
|
1646
1897
|
}
|
|
1647
1898
|
catch (error) {
|
|
@@ -1663,8 +1914,41 @@ async function runSetupLifecycle(command, values, entry) {
|
|
|
1663
1914
|
if (code !== 0)
|
|
1664
1915
|
throw new Error(`${change.host} 설치 실패 (exit ${code})`);
|
|
1665
1916
|
},
|
|
1917
|
+
// 이 저장소를 설명하는 Profile 을 만든다. 이름은 plan 이 이미 정했다.
|
|
1918
|
+
adoptProfile: async (change) => {
|
|
1919
|
+
const code = await runProfileAdopt({ ...values, id: change.profile, json: true }, entry);
|
|
1920
|
+
if (code !== 0)
|
|
1921
|
+
throw new Error(`profile adopt 실패 (exit ${code})`);
|
|
1922
|
+
},
|
|
1923
|
+
// 승인 권한자를 세운다. 이름과 채널뿐이고 비밀은 다루지 않는다.
|
|
1924
|
+
wireIdentity: async (change) => {
|
|
1925
|
+
const code = await runSetupIdentity({ ...values, actor: change.actor, role: 'both' });
|
|
1926
|
+
if (code !== 0)
|
|
1927
|
+
throw new Error(`identity 결선 실패 (exit ${code})`);
|
|
1928
|
+
},
|
|
1929
|
+
// 발견이 증명한 결합을 Profile 에 적는다. 갈리는 것은 plan 에 들어오지 않는다.
|
|
1930
|
+
declareCanonical: async (change) => {
|
|
1931
|
+
const written = await writeProfileCanonical(change.profile, change.source);
|
|
1932
|
+
if (written)
|
|
1933
|
+
await relock(change.profile);
|
|
1934
|
+
},
|
|
1935
|
+
declareBindings: async (change) => {
|
|
1936
|
+
const written = await writeProfileBindings(change.profile, change.bindings);
|
|
1937
|
+
// Profile 을 고쳤으면 lock 이 어긋난다 — 다음 명령이 그 drift 앞에서 멈춘다.
|
|
1938
|
+
// 고친 쪽이 닫는다 (setup identity 가 하는 것과 같다).
|
|
1939
|
+
if (written)
|
|
1940
|
+
await relock(change.profile);
|
|
1941
|
+
},
|
|
1666
1942
|
});
|
|
1667
1943
|
}
|
|
1944
|
+
catch (error) {
|
|
1945
|
+
// **적용 실패는 stack trace 가 아니라 답이어야 한다.** agent 는 이 문서를 읽고 다음
|
|
1946
|
+
// 행동을 정한다 — 예외가 그대로 나가면 stdout 이 비고 아무것도 판단할 수 없다.
|
|
1947
|
+
console.log = speak;
|
|
1948
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1949
|
+
emit({ ...plan, status: 'apply_failed', changesApplied: false, detail });
|
|
1950
|
+
return 1;
|
|
1951
|
+
}
|
|
1668
1952
|
finally {
|
|
1669
1953
|
console.log = speak;
|
|
1670
1954
|
}
|
|
@@ -1702,6 +1986,7 @@ async function inspectSetup(root) {
|
|
|
1702
1986
|
...(runtime
|
|
1703
1987
|
? { profile: { id: runtime.layers.profile.id, origin: runtime.layers.profileOrigin } }
|
|
1704
1988
|
: {}),
|
|
1989
|
+
...(runtime ? { canonicalSources: runtime.layers.profile.canonical.sources.length } : {}),
|
|
1705
1990
|
hasApprovers: Object.keys(await loadIdentityMap(root)).length > 0,
|
|
1706
1991
|
hasControllerIdentities: Object.keys(runtime?.controllerIdentities ?? {}).length > 0,
|
|
1707
1992
|
hasMonitorIdentities: (runtime?.monitor.identities?.length ?? 0) > 0,
|
|
@@ -1929,11 +2214,23 @@ async function runProfile(command, values, root) {
|
|
|
1929
2214
|
async function scmFor(resolved) {
|
|
1930
2215
|
if (!resolved)
|
|
1931
2216
|
return undefined;
|
|
2217
|
+
const sources = resolved.layers.profile.canonical.sources;
|
|
2218
|
+
// Profile 은 갈래마다 provider 를 적는다. `git` 이라고 적힌 것은 이 checkout 이 이미
|
|
2219
|
+
// 들고 있는 사실이므로 원격 API 로 가지 않는다 — 갈 수 있다는 보장도 없다.
|
|
2220
|
+
if (sources.length > 0 && sources.every((source) => source.provider === 'git')) {
|
|
2221
|
+
const { root } = await discoverProjectRoot(process.cwd());
|
|
2222
|
+
const refs = {};
|
|
2223
|
+
for (const source of sources) {
|
|
2224
|
+
if (source.ref)
|
|
2225
|
+
refs[source.id] = { ref: source.ref, ...(source.remote ? { remote: source.remote } : {}) };
|
|
2226
|
+
}
|
|
2227
|
+
return new LocalCanonicalReader({ cwd: root, sourceRefs: refs });
|
|
2228
|
+
}
|
|
1932
2229
|
const token = await discoverToken();
|
|
1933
2230
|
if (!token)
|
|
1934
2231
|
return undefined;
|
|
1935
2232
|
const sourceRefs = {};
|
|
1936
|
-
for (const source of
|
|
2233
|
+
for (const source of sources) {
|
|
1937
2234
|
if (source.ref)
|
|
1938
2235
|
sourceRefs[source.id] = { ref: source.ref };
|
|
1939
2236
|
}
|
|
@@ -2172,6 +2469,7 @@ async function runProceed(values, store, root, resolved) {
|
|
|
2172
2469
|
const workRef = values.work ?? undefined;
|
|
2173
2470
|
const ingress = workRef ? await buildWorkIngress(store, root, sessions, resolved) : undefined;
|
|
2174
2471
|
if (workRef && !ingress) {
|
|
2472
|
+
// **정본이 없는 것과 다른 실패다.** 정본은 code 쪽 사실이고 이것은 작업 항목 쪽이다.
|
|
2175
2473
|
console.error(`작업 항목 '${workRef}' 을 읽을 통로가 없다 — Profile bindings 에 작업 항목 provider 를 선언하라.`);
|
|
2176
2474
|
return 2;
|
|
2177
2475
|
}
|
|
@@ -2387,7 +2685,11 @@ async function buildWorkIngress(store, root, runtime, resolved) {
|
|
|
2387
2685
|
plan,
|
|
2388
2686
|
// 제안은 **말하는 것**이지 정하는 것이 아니다. 역할을 박아 넣으면 선언과 구분되지 않고,
|
|
2389
2687
|
// capability 해석은 후보가 유일할 때 이미 스스로 풀린다.
|
|
2390
|
-
|
|
2688
|
+
//
|
|
2689
|
+
// 다만 **작업 항목을 누구에게 물을지는 여기서 정한다** — code 와 work 가 둘 다 자원
|
|
2690
|
+
// 조회를 제공하면 `rolesFor` 는 아무것도 정하지 못하고, 그러면 선언해 둔 work binding 이
|
|
2691
|
+
// 있는데도 "통로가 없다" 가 된다 (P0 F7).
|
|
2692
|
+
roles: { ...rolesFor(plan, declared), ...workItemRoles(plan, declared) },
|
|
2391
2693
|
perPage: 30,
|
|
2392
2694
|
...jamComposition(projectRoot),
|
|
2393
2695
|
endpointFor: (binding) => endpointOf(adapters, binding),
|
|
@@ -3095,6 +3397,25 @@ const CLOSURE_CHECKLIST = 'closureChecklist';
|
|
|
3095
3397
|
/** Orchestration Audit (C-10). 회수 후에도 남는 기록이라 Progress와 다른 자리에 산다. */
|
|
3096
3398
|
const auditLedger = (store) => new AuditLedger(store.scope('audit'));
|
|
3097
3399
|
const closureLedger = (store) => new ClosureLedger(store.scope('closure'));
|
|
3400
|
+
/**
|
|
3401
|
+
* 조율 증거 원장. Bounded Query 와 **다른 scope** 에 산다 — 기대와 증거는 다른 사실이고,
|
|
3402
|
+
* 한 자리에 두면 "물어봤다"와 "전달됐다"가 다시 한 레코드가 된다.
|
|
3403
|
+
*/
|
|
3404
|
+
const coordinationLedger = (store) => new CoordinationLedger(store.scope('coordination'));
|
|
3405
|
+
/**
|
|
3406
|
+
* 지금 이 workspace 의 조율 상태.
|
|
3407
|
+
*
|
|
3408
|
+
* 기대는 Bounded Query 에서 오고 증거는 원장에서 온다. **여기서 상태를 만들지 않는다** —
|
|
3409
|
+
* 둘을 맞춰 파생할 뿐이다.
|
|
3410
|
+
*/
|
|
3411
|
+
async function coordinationNow(store, resolved) {
|
|
3412
|
+
const queries = await queryLedger(store, resolved).list();
|
|
3413
|
+
return viewCoordination(coordinationLedger(store),
|
|
3414
|
+
// 답이 이미 안에서 쓰인 질의는 조율 대상이 아니다 — 밖에 물을 이유가 끝났다.
|
|
3415
|
+
queries
|
|
3416
|
+
.filter((entry) => entry.answer === null)
|
|
3417
|
+
.map((entry) => ({ id: entry.query.id, expectsResponse: entry.query.expectedResponse !== undefined })));
|
|
3418
|
+
}
|
|
3098
3419
|
/** Bounded Query (B-25). 결정권 판정에 Profile 책임 지도가 필요하다. */
|
|
3099
3420
|
const queryLedger = (store, resolved) => new QueryLedger(store.scope('query'), resolved?.ownership);
|
|
3100
3421
|
/**
|
|
@@ -3461,7 +3782,11 @@ async function serviceHealth(values) {
|
|
|
3461
3782
|
const adapter = serviceAdapter();
|
|
3462
3783
|
if (!adapter)
|
|
3463
3784
|
return null;
|
|
3464
|
-
const
|
|
3785
|
+
const runtime = await serviceRuntime();
|
|
3786
|
+
if (runtime.kind !== 'STABLE') {
|
|
3787
|
+
return { adapter: adapter.id, action: 'unsupported', line: serviceRuntimeLine(runtime) };
|
|
3788
|
+
}
|
|
3789
|
+
const plan = await planPersistentRuntime(adapter, serviceCommand(serviceInterval(values), runtime)).catch(() => null);
|
|
3465
3790
|
if (!plan)
|
|
3466
3791
|
return null;
|
|
3467
3792
|
return { adapter: adapter.id, action: plan.action, line: persistentRuntimeLine(adapter.id, plan) };
|
|
@@ -3516,13 +3841,56 @@ function serviceAdapter() {
|
|
|
3516
3841
|
* 지금 도는 실행 파일과 진입점을 그대로 쓴다 — 어느 build 를 쓸지는 그 진입점이 다시
|
|
3517
3842
|
* 정하므로(C-14), 등록물이 build 를 고르는 일은 없다.
|
|
3518
3843
|
*/
|
|
3519
|
-
function serviceCommand(intervalSeconds) {
|
|
3844
|
+
function serviceCommand(intervalSeconds, runtime) {
|
|
3520
3845
|
return {
|
|
3521
|
-
program:
|
|
3522
|
-
args: [
|
|
3846
|
+
program: runtime.node,
|
|
3847
|
+
args: [runtime.entry, 'runtime', 'tick', '--all'],
|
|
3523
3848
|
intervalSeconds,
|
|
3849
|
+
...serviceEnvironment(runtime.node),
|
|
3850
|
+
logPath: join(ascHome(), 'service.log'),
|
|
3524
3851
|
};
|
|
3525
3852
|
}
|
|
3853
|
+
/**
|
|
3854
|
+
* 등록물이 가리킬 Node 와 진입점 (P0 — fresh onboarding).
|
|
3855
|
+
*
|
|
3856
|
+
* 지금 이 프로세스가 어디서 도는지와 **다른 질문이다.** bootstrap 은 npx 캐시에서 돌 수
|
|
3857
|
+
* 있고, 그 자리는 지워진다. 전역 설치본이 있으면 그것이 답이고, 없으면 등록하지 않는다.
|
|
3858
|
+
*/
|
|
3859
|
+
async function serviceRuntime() {
|
|
3860
|
+
const stable = await globalRuntimeEntry();
|
|
3861
|
+
const check = await checkNodeRuntime(nodeRuntimeDeps());
|
|
3862
|
+
return resolveServiceRuntime({
|
|
3863
|
+
runningEntry: fileURLToPath(import.meta.url),
|
|
3864
|
+
runningNode: process.execPath,
|
|
3865
|
+
runningNodeVersion: process.version,
|
|
3866
|
+
...(stable ? { stableEntry: stable } : {}),
|
|
3867
|
+
...(check.ok ? {} : { nodeCandidates: check.candidates }),
|
|
3868
|
+
});
|
|
3869
|
+
}
|
|
3870
|
+
/** 전역 설치본의 진입점. npm 이 말하는 prefix 를 쓴다 — 경로를 지어내지 않는다. */
|
|
3871
|
+
async function globalRuntimeEntry() {
|
|
3872
|
+
const prefix = await execText('npm', ['prefix', '-g']);
|
|
3873
|
+
if (!prefix)
|
|
3874
|
+
return undefined;
|
|
3875
|
+
const entry = process.platform === 'win32'
|
|
3876
|
+
? join(prefix, 'node_modules', RUNTIME_PACKAGE, 'dist', 'cli', 'asc.js')
|
|
3877
|
+
: join(prefix, 'lib', 'node_modules', RUNTIME_PACKAGE, 'dist', 'cli', 'asc.js');
|
|
3878
|
+
return existsSync(entry) ? entry : undefined;
|
|
3879
|
+
}
|
|
3880
|
+
/**
|
|
3881
|
+
* 서비스가 외부 통로를 열 수 있게 하는 환경 (P0-R1).
|
|
3882
|
+
*
|
|
3883
|
+
* 여기가 어떤 실행 파일이 필요한지 아는 자리다 — 조립 계층은 provider 를 안다. 지금 도는
|
|
3884
|
+
* node 의 디렉터리를 맨 앞에 둬서 같은 node 의 npx 가 먼저 잡히게 한다. Windows 의 예약
|
|
3885
|
+
* 작업은 사용자 환경을 그대로 물려받으므로 환경을 싣지 않는다.
|
|
3886
|
+
*/
|
|
3887
|
+
function serviceEnvironment(node) {
|
|
3888
|
+
if (process.platform === 'win32')
|
|
3889
|
+
return {};
|
|
3890
|
+
const tools = [node, jamLauncher().command, 'glab', 'git'];
|
|
3891
|
+
const { path } = servicePath(tools);
|
|
3892
|
+
return { environment: { PATH: path } };
|
|
3893
|
+
}
|
|
3526
3894
|
const serviceInterval = (values) => values['interval-min'] === undefined ? 5 * 60 : Math.max(60, Number(values['interval-min']) * 60);
|
|
3527
3895
|
/**
|
|
3528
3896
|
* `asc runtime service` — 이 기계의 지속 등록 (설계 §4).
|
|
@@ -3541,7 +3909,17 @@ async function runRuntimeService(command, values) {
|
|
|
3541
3909
|
// 못 하는 것을 "했다"로 적지 않는다. 다만 이것이 오류는 아니다.
|
|
3542
3910
|
return 0;
|
|
3543
3911
|
}
|
|
3544
|
-
const
|
|
3912
|
+
const runtime = await serviceRuntime();
|
|
3913
|
+
if (runtime.kind !== 'STABLE') {
|
|
3914
|
+
// 등록물이 가리킬 안정된 자리가 없다. 무엇이 없어서인지 말한다 — 등록하지 않는 것이
|
|
3915
|
+
// 답이고, 깨진 등록을 남기는 것은 답이 아니다.
|
|
3916
|
+
if (values.json)
|
|
3917
|
+
console.log(JSON.stringify({ supported: true, stable: false, ...runtime }, null, 2));
|
|
3918
|
+
else
|
|
3919
|
+
console.error(serviceRuntimeLine(runtime));
|
|
3920
|
+
return command === 'status' || command === undefined ? 0 : 1;
|
|
3921
|
+
}
|
|
3922
|
+
const wanted = serviceCommand(serviceInterval(values), runtime);
|
|
3545
3923
|
if (command === undefined || command === 'status') {
|
|
3546
3924
|
const plan = await planPersistentRuntime(adapter, wanted);
|
|
3547
3925
|
if (values.json) {
|
|
@@ -3626,11 +4004,15 @@ async function tickAllWorkspaces(values, lease) {
|
|
|
3626
4004
|
// 죽은 것으로 보이고 두 번째 프로세스가 끼어든다.
|
|
3627
4005
|
await lease.renew();
|
|
3628
4006
|
}
|
|
4007
|
+
const skippedViews = skipped.map((view) => ({ workspaceId: view.workspaceId, health: view.health }));
|
|
4008
|
+
const summary = summarizePass(results, skippedViews);
|
|
3629
4009
|
if (values.json) {
|
|
3630
4010
|
console.log(JSON.stringify({
|
|
3631
4011
|
ran: results,
|
|
3632
4012
|
// 건너뛴 것을 조용히 빼지 않는다 — "아무 일도 없었다"와 다른 사실이다
|
|
3633
|
-
skipped:
|
|
4013
|
+
skipped: skippedViews,
|
|
4014
|
+
outcome: summary.outcome,
|
|
4015
|
+
failed: summary.failed,
|
|
3634
4016
|
}, null, 2));
|
|
3635
4017
|
}
|
|
3636
4018
|
else {
|
|
@@ -3638,9 +4020,11 @@ async function tickAllWorkspaces(values, lease) {
|
|
|
3638
4020
|
console.log(`${result.workspaceId}: pass exited ${result.code}`);
|
|
3639
4021
|
for (const view of skipped)
|
|
3640
4022
|
console.log(`${view.workspaceId}: ${view.health} — not observed this pass`);
|
|
4023
|
+
console.log(summary.line);
|
|
3641
4024
|
}
|
|
3642
|
-
// 한 workspace 가 실패해도
|
|
3643
|
-
|
|
4025
|
+
// 한 workspace 가 실패해도 다른 workspace 는 전부 돌았다. 그러나 실패했다는 사실은 회차의
|
|
4026
|
+
// 종료 코드에 남는다 — OS 가 보는 것은 그것 하나뿐이다 (P0-R2).
|
|
4027
|
+
return summary.code;
|
|
3644
4028
|
}
|
|
3645
4029
|
/**
|
|
3646
4030
|
* 상시 Runtime (C-12). 대화를 켜 두지 않고 **상태를 지속시키고 계산을 짧게 돌린다.**
|
|
@@ -4136,11 +4520,178 @@ async function runFrontOpen(values) {
|
|
|
4136
4520
|
escalations: await escalationLedger(store).pending(),
|
|
4137
4521
|
health: evaluateHealth(await new CoverageLedger(store.scope(scope)).health(), new Date().toISOString(), HEALTH_THRESHOLDS),
|
|
4138
4522
|
...(workspace ? { workspace } : {}),
|
|
4523
|
+
// 안에서 할 일이 없는 것과 밖에서 답이 안 온 것은 다른 사실이다
|
|
4524
|
+
coordination: await coordinationNow(store),
|
|
4139
4525
|
bindings: claudeBindings(store),
|
|
4140
4526
|
});
|
|
4141
4527
|
},
|
|
4142
4528
|
}));
|
|
4143
4529
|
}
|
|
4530
|
+
/**
|
|
4531
|
+
* 이 workspace 의 조율 표면. 없으면 없다고 말한다 — 아무 데나 대신 게시하지 않는다.
|
|
4532
|
+
*/
|
|
4533
|
+
async function coordinationSurfaceFor(resolved) {
|
|
4534
|
+
const { root: projectRoot } = await discoverProjectRoot(process.cwd());
|
|
4535
|
+
const adapters = monitorAdapters();
|
|
4536
|
+
const declared = resolved?.layers.profile.bindings ?? [];
|
|
4537
|
+
const plan = await composeBindings({
|
|
4538
|
+
context: { projectRoot, env: process.env },
|
|
4539
|
+
adapters,
|
|
4540
|
+
roles: declared.map((b) => ({ adapterId: b.adapter, resource: b.resource, role: b.role })),
|
|
4541
|
+
});
|
|
4542
|
+
const ports = await buildRuntimePorts({
|
|
4543
|
+
plan,
|
|
4544
|
+
roles: rolesFor(plan, declared),
|
|
4545
|
+
...jamComposition(projectRoot),
|
|
4546
|
+
endpointFor: (binding) => endpointOf(adapters, binding),
|
|
4547
|
+
});
|
|
4548
|
+
return { surface: ports.coordinationSurface, unavailable: ports.unavailable };
|
|
4549
|
+
}
|
|
4550
|
+
/**
|
|
4551
|
+
* `asc coordination publish` — 기대 하나를 밖에 실제로 내보낸다.
|
|
4552
|
+
*
|
|
4553
|
+
* 본문을 파일로 받는 이유는 하나다: 사람이 읽을 글이 셸을 지나며 조용히 달라지는 것을
|
|
4554
|
+
* 막는다. 그리고 **내부 메모가 섞일 자리를 주지 않는다** — 나가는 것은 제목·본문·라벨뿐이다.
|
|
4555
|
+
*/
|
|
4556
|
+
async function runCoordinationPublish(values, store, resolved) {
|
|
4557
|
+
const queryId = typeof values.query === 'string' ? values.query : undefined;
|
|
4558
|
+
const title = typeof values.title === 'string' ? values.title : undefined;
|
|
4559
|
+
const bodyFile = typeof values['body-file'] === 'string' ? values['body-file'] : undefined;
|
|
4560
|
+
if (!queryId || !title || !bodyFile) {
|
|
4561
|
+
console.error('coordination publish needs --query, --title and --body-file\n\n' + USAGE);
|
|
4562
|
+
return 2;
|
|
4563
|
+
}
|
|
4564
|
+
const { surface, unavailable } = await coordinationSurfaceFor(resolved);
|
|
4565
|
+
if (!surface) {
|
|
4566
|
+
console.error('No coordination surface is bound to this workspace — nothing was published.');
|
|
4567
|
+
for (const line of unavailable)
|
|
4568
|
+
console.error(` ${line}`);
|
|
4569
|
+
return 1;
|
|
4570
|
+
}
|
|
4571
|
+
const body = await readFile(bodyFile, 'utf8');
|
|
4572
|
+
const audience = values.audience ?? [];
|
|
4573
|
+
const known = (values.known ?? []).map((objectId) => ({
|
|
4574
|
+
objectType: 'issue',
|
|
4575
|
+
objectId,
|
|
4576
|
+
}));
|
|
4577
|
+
const workReference = typeof values.work === 'string' ? values.work : undefined;
|
|
4578
|
+
const outcome = await publishOnce({
|
|
4579
|
+
queryId,
|
|
4580
|
+
publicPayload: { title, body },
|
|
4581
|
+
...(audience.length > 0 ? { audience } : {}),
|
|
4582
|
+
...(known.length > 0 ? { known } : {}),
|
|
4583
|
+
...(workReference ? { workReference } : {}),
|
|
4584
|
+
}, { surface, bindingRole: 'coordination-surface' });
|
|
4585
|
+
if (outcome.ok) {
|
|
4586
|
+
const recorded = await recordPublication(coordinationLedger(store), outcome);
|
|
4587
|
+
if (values.json) {
|
|
4588
|
+
console.log(JSON.stringify({ publish: outcome, recorded: recorded.ok }, null, 2));
|
|
4589
|
+
return 0;
|
|
4590
|
+
}
|
|
4591
|
+
console.log(publishLine(outcome));
|
|
4592
|
+
// 이미 적힌 사실을 다시 적지 않는다. 그것은 실패가 아니라 같은 것을 두 번 안 세는 것이다.
|
|
4593
|
+
console.log(recorded.ok ? 'recorded as communication evidence' : 'already recorded');
|
|
4594
|
+
return 0;
|
|
4595
|
+
}
|
|
4596
|
+
if (values.json)
|
|
4597
|
+
console.log(JSON.stringify({ publish: outcome }, null, 2));
|
|
4598
|
+
else
|
|
4599
|
+
console.error(publishLine(outcome));
|
|
4600
|
+
return 1;
|
|
4601
|
+
}
|
|
4602
|
+
/**
|
|
4603
|
+
* `asc coordination observe` — 게시한 것에 답이 왔는가.
|
|
4604
|
+
*
|
|
4605
|
+
* **답의 의미를 정하지 않는다.** 여기서 남는 것은 "밖에서 사람이 글을 남겼다"까지이고,
|
|
4606
|
+
* 그것이 결정인지 승인인지는 다른 계약의 몫이다. 그 시스템이 스스로 남긴 자국과 우리가
|
|
4607
|
+
* 쓴 글은 세지 않는다 — 그 둘을 세면 아무도 답하지 않은 스레드가 답이 온 것으로 보인다.
|
|
4608
|
+
*/
|
|
4609
|
+
async function runCoordinationObserve(values, store, root, resolved) {
|
|
4610
|
+
const ledger = coordinationLedger(store);
|
|
4611
|
+
const communications = await ledger.communications();
|
|
4612
|
+
if (communications.length === 0) {
|
|
4613
|
+
console.log('Nothing has been published from here yet.');
|
|
4614
|
+
return 0;
|
|
4615
|
+
}
|
|
4616
|
+
const { root: projectRoot } = await discoverProjectRoot(process.cwd());
|
|
4617
|
+
const adapters = monitorAdapters();
|
|
4618
|
+
const declared = resolved?.layers.profile.bindings ?? [];
|
|
4619
|
+
const plan = await composeBindings({
|
|
4620
|
+
context: { projectRoot, env: process.env },
|
|
4621
|
+
adapters,
|
|
4622
|
+
roles: declared.map((b) => ({ adapterId: b.adapter, resource: b.resource, role: b.role })),
|
|
4623
|
+
});
|
|
4624
|
+
// **그 게시물을 만든 통로에게 묻는다.** 프로젝트 전체의 자원 조회를 하나 고르면
|
|
4625
|
+
// 어느 것을 고를지 갈리고(실제로 갈렸다), 갈리지 않더라도 다른 시스템에게 남의 게시물을
|
|
4626
|
+
// 묻게 된다. 증거에 적힌 adapter 가 곧 그 통로다.
|
|
4627
|
+
const built = await buildObservationChannels({
|
|
4628
|
+
plan,
|
|
4629
|
+
roles: rolesFor(plan, declared),
|
|
4630
|
+
...jamComposition(projectRoot),
|
|
4631
|
+
endpointFor: (binding) => endpointOf(adapters, binding),
|
|
4632
|
+
});
|
|
4633
|
+
const contextOf = (adapterId) => built.channels.find((channel) => channel.adapterId === adapterId && channel.resourceContext)?.resourceContext;
|
|
4634
|
+
// 나를 나로 알아보는 곳은 identities.json 하나다. 채널 접두사를 떼면 계정 이름이 남는다.
|
|
4635
|
+
const identity = await loadIdentityMap(root);
|
|
4636
|
+
const mine = new Set();
|
|
4637
|
+
for (const [name, accounts] of Object.entries(identity)) {
|
|
4638
|
+
mine.add(name);
|
|
4639
|
+
for (const account of accounts)
|
|
4640
|
+
mine.add(account.includes(':') ? account.slice(account.indexOf(':') + 1) : account);
|
|
4641
|
+
}
|
|
4642
|
+
let added = 0;
|
|
4643
|
+
let looked = 0;
|
|
4644
|
+
for (const communication of communications) {
|
|
4645
|
+
const context = contextOf(communication.identity.adapter);
|
|
4646
|
+
if (!context) {
|
|
4647
|
+
// 못 본 것을 "답이 없다"로 넘기지 않는다.
|
|
4648
|
+
console.error(` ${communication.identity.objectId}: ${communication.identity.adapter} 통로가 열리지 않아 보지 못했다`);
|
|
4649
|
+
continue;
|
|
4650
|
+
}
|
|
4651
|
+
looked += 1;
|
|
4652
|
+
const remarks = await context
|
|
4653
|
+
.getComments(communication.identity.objectId, { limit: 100 })
|
|
4654
|
+
.catch(() => []);
|
|
4655
|
+
for (const response of responsesFrom(communication, remarks, mine)) {
|
|
4656
|
+
const outcome = await ledger.responseRecorded(response);
|
|
4657
|
+
if (outcome.ok)
|
|
4658
|
+
added += 1;
|
|
4659
|
+
}
|
|
4660
|
+
}
|
|
4661
|
+
const views = await coordinationNow(store, resolved);
|
|
4662
|
+
if (values.json) {
|
|
4663
|
+
console.log(JSON.stringify({ published: communications.length, observed: looked, recorded: added, coordination: views }, null, 2));
|
|
4664
|
+
return 0;
|
|
4665
|
+
}
|
|
4666
|
+
console.log(`Looked at ${looked} of ${communications.length} published artefact(s) — ${added} new response(s).`);
|
|
4667
|
+
for (const line of coordinationLines(views))
|
|
4668
|
+
console.log(line);
|
|
4669
|
+
return 0;
|
|
4670
|
+
}
|
|
4671
|
+
/**
|
|
4672
|
+
* `asc coordination` — 밖에 물은 것이 실제로 나갔는가, 답이 왔는가.
|
|
4673
|
+
*
|
|
4674
|
+
* **읽기만 한다.** 이 화면이 상태를 만들지 않는다는 것이 요점이다 — 보이는 것은 전부
|
|
4675
|
+
* 기대와 증거에서 파생한 값이다.
|
|
4676
|
+
*/
|
|
4677
|
+
async function runCoordination(command, values, store, root, resolved) {
|
|
4678
|
+
if (command === 'publish')
|
|
4679
|
+
return runCoordinationPublish(values, store, resolved);
|
|
4680
|
+
if (command === 'observe')
|
|
4681
|
+
return runCoordinationObserve(values, store, root, resolved);
|
|
4682
|
+
if (command !== undefined && command !== 'status') {
|
|
4683
|
+
console.error(`Unknown coordination command: ${command}\n\n${USAGE}`);
|
|
4684
|
+
return 2;
|
|
4685
|
+
}
|
|
4686
|
+
const views = await coordinationNow(store, resolved);
|
|
4687
|
+
if (values.json) {
|
|
4688
|
+
console.log(JSON.stringify({ coordination: views }, null, 2));
|
|
4689
|
+
return 0;
|
|
4690
|
+
}
|
|
4691
|
+
for (const line of coordinationLines(views))
|
|
4692
|
+
console.log(line);
|
|
4693
|
+
return 0;
|
|
4694
|
+
}
|
|
4144
4695
|
async function runFront(command, values, store, root) {
|
|
4145
4696
|
if (command !== undefined && command !== 'status') {
|
|
4146
4697
|
console.error(`Unknown front command: ${command}
|
|
@@ -4158,6 +4709,7 @@ ${USAGE}`);
|
|
|
4158
4709
|
escalations: await escalationLedger(store).pending(),
|
|
4159
4710
|
health: evaluateHealth(await new CoverageLedger(store.scope(scope)).health(), new Date().toISOString(), HEALTH_THRESHOLDS),
|
|
4160
4711
|
...(located ? { workspace: { workspaceId: located.workspaceId, locator: located.locator } } : {}),
|
|
4712
|
+
coordination: await coordinationNow(store),
|
|
4161
4713
|
// 도는 세션을 누가 집고 있는지. --physical 을 다시 물어보게 하지 않는다 (L-4).
|
|
4162
4714
|
bindings: claudeBindings(store),
|
|
4163
4715
|
});
|