@asc-agent/runtime 0.3.2 → 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/claude-code/install.d.ts +7 -0
- package/dist/adapters/claude-code/install.js +114 -38
- package/dist/adapters/claude-code/session-start.d.ts +20 -0
- package/dist/adapters/claude-code/session-start.js +111 -0
- 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/jam/adapter.d.ts +46 -9
- package/dist/adapters/jam/adapter.js +88 -22
- package/dist/adapters/jam/setup.d.ts +62 -0
- package/dist/adapters/jam/setup.js +85 -0
- 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.d.ts +12 -0
- package/dist/adapters/service/launchd.js +97 -0
- package/dist/adapters/service/schtasks.d.ts +16 -0
- package/dist/adapters/service/schtasks.js +67 -0
- package/dist/adapters/service/systemd-user.d.ts +15 -0
- package/dist/adapters/service/systemd-user.js +98 -0
- package/dist/adapters/webhook/ingress.d.ts +2 -2
- package/dist/cli/asc.js +1080 -145
- package/dist/composition/registry.js +11 -3
- package/dist/composition/runtime.d.ts +36 -0
- package/dist/composition/runtime.js +80 -1
- package/dist/core/attach/setup-plan.d.ts +66 -2
- package/dist/core/attach/setup-plan.js +50 -0
- 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 +59 -2
- package/dist/core/distribution/persistent-runtime.d.ts +82 -0
- package/dist/core/distribution/persistent-runtime.js +49 -0
- package/dist/core/distribution/release.d.ts +3 -3
- package/dist/core/distribution/release.js +1 -1
- package/dist/core/runtime/background.d.ts +104 -0
- package/dist/core/runtime/background.js +225 -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 +58 -0
- package/dist/core/runtime/front.js +50 -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 +61 -0
- package/dist/core/runtime/workspaces.js +82 -0
- package/dist/core/workspace/resolve.d.ts +36 -0
- package/dist/core/workspace/resolve.js +110 -3
- package/dist/ports/adapter.d.ts +12 -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 +6 -6
- package/package.json +1 -1
package/dist/cli/asc.js
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
// 출력만 있고 판단은 한 줄도 없다.
|
|
7
7
|
//
|
|
8
8
|
// 읽기 전용이다. 결정 제출은 사람의 명시적 의사표현을 받는 별도 경로로 나간다 (B-06).
|
|
9
|
-
import { execFile, spawnSync } from 'node:child_process';
|
|
9
|
+
import { execFile, spawn, spawnSync } from 'node:child_process';
|
|
10
10
|
import { parseArgs, promisify } from 'node:util';
|
|
11
11
|
import { existsSync, readdirSync, realpathSync } from 'node:fs';
|
|
12
12
|
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
|
-
import { homedir, userInfo } from 'node:os';
|
|
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
17
|
import { RELEASE_VERSION } from "../core/distribution/release.js";
|
|
@@ -32,7 +32,7 @@ import { AdoptError, buildAdoptedProfile } from "../core/attach/adopt.js";
|
|
|
32
32
|
import { locatorsOf, lookupLocator, readIndex, register, writeIndex } from "../core/workspace/index-store.js";
|
|
33
33
|
import { adoptionLine, judgeAdoption, migrate } from "../core/workspace/migrate.js";
|
|
34
34
|
import { newWorkspaceId, normalizeRemote, recoverCandidates, recoverLines } from "../core/workspace/identity.js";
|
|
35
|
-
import { resolveWorkspace, resolutionLine } from "../core/workspace/resolve.js";
|
|
35
|
+
import { gitWorktrees, resolveWorkspace, resolutionLine } from "../core/workspace/resolve.js";
|
|
36
36
|
import { assessSetup, renderSetup } from "../core/attach/setup.js";
|
|
37
37
|
import { withIdentity } from "../core/attach/init.js";
|
|
38
38
|
import { applySetupPlan, computeSetupPlan, renderSetupPlan, } from "../core/attach/setup-plan.js";
|
|
@@ -46,14 +46,17 @@ 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";
|
|
52
|
-
import { JamAdapter } from "../adapters/jam/adapter.js";
|
|
54
|
+
import { JamAdapter, resolveJamCommand } from "../adapters/jam/adapter.js";
|
|
55
|
+
import { healJam, healLine } from "../adapters/jam/setup.js";
|
|
53
56
|
import { statusIndicatesDone } from "../adapters/jam/ports.js";
|
|
54
57
|
import { ProgressService } from "../core/operator/progress.js";
|
|
55
58
|
import { composeBindings, defaultAdapters } from "../composition/registry.js";
|
|
56
|
-
import { buildRuntimePorts, closeToolClients, rolesFor } from "../composition/runtime.js";
|
|
59
|
+
import { buildObservationChannels, buildRuntimePorts, closeToolClients, rolesFor } from "../composition/runtime.js";
|
|
57
60
|
import { proposeBindings } from "../composition/propose.js";
|
|
58
61
|
import { buildEventObservation } from "../composition/observe.js";
|
|
59
62
|
import { availableProfiles, planBootstrap, renderPlan } from "../core/attach/bootstrap.js";
|
|
@@ -67,12 +70,21 @@ import { renderProgress } from "../core/operator/render.js";
|
|
|
67
70
|
import { AuditLedger, decisionLines, delegationLine, executionLines, reclaimLine, validationLines, DecisionClass, } from "../core/runtime/audit.js";
|
|
68
71
|
import { ClosureLedger } from "../core/runtime/closure.js";
|
|
69
72
|
import { Orchestrator, renderTick } from "../core/runtime/orchestrator.js";
|
|
73
|
+
import { LAST_RUN_KEY, RuntimeLease, fileScope, readBackground, renderBackground, staleAfter, } from "../core/runtime/background.js";
|
|
74
|
+
import { dueWorkspaces, renderWorkspaces, summarizePass, viewWorkspaces } from "../core/runtime/workspaces.js";
|
|
75
|
+
import { persistentRuntimeLine, planPersistentRuntime, } from "../core/distribution/persistent-runtime.js";
|
|
76
|
+
import { launchdAdapter } from "../adapters/service/launchd.js";
|
|
77
|
+
import { schtasksAdapter } from "../adapters/service/schtasks.js";
|
|
78
|
+
import { serviceAdapterFor, systemdUserAdapter } from "../adapters/service/systemd-user.js";
|
|
70
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";
|
|
71
82
|
import { ObservationLedger } from "../core/monitor/observation.js";
|
|
72
83
|
import { DeliveryLedger, deliver, planDigest } from "../core/presentation/digest.js";
|
|
73
84
|
import { LocalPresentation } from "../adapters/local/presentation.js";
|
|
74
85
|
import { collectSessions, renderCollect } from "../core/runtime/controller.js";
|
|
75
|
-
import { renderFront, restoreFront } from "../core/runtime/front.js";
|
|
86
|
+
import { frontOpeningLines, openFront, renderFront, restoreFront } from "../core/runtime/front.js";
|
|
87
|
+
import { sessionStartPayload } from "../adapters/claude-code/session-start.js";
|
|
76
88
|
import { EscalationLedger, escalationLines } from "../core/runtime/escalation.js";
|
|
77
89
|
import { deriveExecutionState, executionLine } from "../core/runtime/execution-state.js";
|
|
78
90
|
import { buildFinalReport, renderFinalReport } from "../core/runtime/report.js";
|
|
@@ -109,13 +121,19 @@ const USAGE = `asc — Agent Session Control
|
|
|
109
121
|
asc profile adopt [--id <name>] [--json] # make a profile for this repository
|
|
110
122
|
asc profile resolve --profile <id> [--preset <id>] [--install <path>] [--write]
|
|
111
123
|
|
|
112
|
-
asc runtime start [--
|
|
113
|
-
[--census-min <n>] [--digest-min <n>]
|
|
114
|
-
|
|
115
|
-
asc runtime
|
|
124
|
+
asc runtime start [--detach] [--interval-min <n>] [--delta-min <n>]
|
|
125
|
+
[--reconcile-min <n>] [--census-min <n>] [--digest-min <n>]
|
|
126
|
+
# --detach: keep observing after this terminal closes
|
|
127
|
+
asc runtime tick [--all] # --all: every workspace this machine knows
|
|
128
|
+
asc runtime list [--json] # every workspace, without visiting each one
|
|
129
|
+
asc runtime service [status] [--json] # the machine's persistent registration
|
|
130
|
+
asc runtime service install|uninstall [--interval-min <n>]
|
|
131
|
+
asc runtime stop # ask the background runtime to finish its pass
|
|
132
|
+
asc runtime status [--json] # which build is in use, and whether it observes
|
|
116
133
|
asc runtime use package
|
|
117
134
|
asc runtime use development <checkout> # run a built checkout instead
|
|
118
135
|
asc front [status] [--json]
|
|
136
|
+
asc front open [--json] # a host session opened here — what is waiting
|
|
119
137
|
asc escalate open <S-ID> --predicate <p>... --question <t> --blocked <node>...
|
|
120
138
|
--evidence <ref>... [--blocked-scope <path>...] [--previous <ESC-ID> --why <t>]
|
|
121
139
|
asc escalate list
|
|
@@ -177,6 +195,11 @@ const USAGE = `asc — Agent Session Control
|
|
|
177
195
|
[--to <authority>]
|
|
178
196
|
asc query list [--json]
|
|
179
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
|
+
|
|
180
203
|
asc progress show [<S-ID>]
|
|
181
204
|
asc progress report <S-ID> --physical <id> --phase <text>
|
|
182
205
|
[--milestone <text>...] [--next <text>] [--unresolved <text>...]
|
|
@@ -232,15 +255,58 @@ async function discoverRoot(start, explicitRoot) {
|
|
|
232
255
|
return resolution.kind === 'UNRESOLVED' ? null : resolution.root;
|
|
233
256
|
}
|
|
234
257
|
async function resolveRoot(start, explicitRoot) {
|
|
235
|
-
|
|
258
|
+
const home = ascHome();
|
|
259
|
+
const index = await readIndex(home);
|
|
260
|
+
const resolution = await resolveWorkspace({
|
|
236
261
|
cwd: start,
|
|
237
262
|
...(explicitRoot ? { explicitRoot } : {}),
|
|
238
|
-
index
|
|
263
|
+
index,
|
|
239
264
|
// 홈의 `~/.asc` 는 user runtime이지 프로젝트 상태가 아니다 — 그 위로 올라가지 않는다
|
|
240
265
|
stopAt: homedir(),
|
|
266
|
+
// 등록이 빗나갔을 때만 Git에게 구조를 묻는다 (C-11 §1.3).
|
|
267
|
+
worktrees: gitWorktrees,
|
|
241
268
|
});
|
|
269
|
+
// 같은 저장소의 다른 checkout으로 풀렸으면 **이 경로를 등록해 둔다.** 그래야 다음부터는
|
|
270
|
+
// 예전과 같은 index 조회 하나로 끝나고, guard hook도 이 checkout을 관리 대상으로 본다.
|
|
271
|
+
if (resolution.kind === 'LINKED_WORKTREE')
|
|
272
|
+
await rememberWorktree(home, index, resolution);
|
|
273
|
+
return resolution;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* 이번에 구조로 풀린 checkout을 index에 적는다. **workspace를 새로 만들지 않는다** —
|
|
277
|
+
* 같은 논리 workspace의 execution instance가 하나 늘었을 뿐이다 (C-11 §1.3).
|
|
278
|
+
*
|
|
279
|
+
* 실패해도 이번 판정을 무르지 않는다. 다음 호출이 Git을 한 번 더 부르는 것으로 끝난다 —
|
|
280
|
+
* 쓰기 하나가 안 됐다고 붙어 있는 workspace를 못 쓰게 만드는 쪽이 나쁘다.
|
|
281
|
+
*/
|
|
282
|
+
async function rememberWorktree(home, index, resolution) {
|
|
283
|
+
const now = new Date().toISOString();
|
|
284
|
+
try {
|
|
285
|
+
await writeIndex(home, register(index, {
|
|
286
|
+
workspaceId: resolution.workspaceId,
|
|
287
|
+
root: resolution.root,
|
|
288
|
+
locator: {
|
|
289
|
+
path: resolution.locator,
|
|
290
|
+
kind: resolution.kindOfLocator,
|
|
291
|
+
platform: process.platform,
|
|
292
|
+
observedAt: now,
|
|
293
|
+
},
|
|
294
|
+
now,
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// 등록만 못 했을 뿐이다. 판정은 그대로 선다.
|
|
299
|
+
}
|
|
242
300
|
}
|
|
243
301
|
const execFileAsync = promisify(execFile);
|
|
302
|
+
/**
|
|
303
|
+
* Claude Host 설치 경로 + **이 CLI의 위치**.
|
|
304
|
+
*
|
|
305
|
+
* SessionStart hook은 상태를 물어볼 곳이 필요하고, 그곳은 지금 도는 이 실행파일이다.
|
|
306
|
+
* 그 CLI가 다시 선택된 build로 넘기므로(`runtime use development` 포함) hook이 build를
|
|
307
|
+
* 고르는 일은 없다.
|
|
308
|
+
*/
|
|
309
|
+
const hostPaths = () => ({ ...defaultPaths(), entry: fileURLToPath(import.meta.url) });
|
|
244
310
|
/**
|
|
245
311
|
* 이 binding이 가리키는 주소. 자체 호스팅은 발견 단계가 이미 알아냈으므로 같은 값을 쓴다 —
|
|
246
312
|
* 여기서 다시 추측하면 두 곳이 다른 주소를 말하게 된다.
|
|
@@ -294,17 +360,60 @@ const ACTIVE_SOURCE_KEY = 'active-source';
|
|
|
294
360
|
* digest처럼 조립을 하지 않는 명령도 같은 자리를 읽어야 하는데, 그걸 알아내려고 discovery와
|
|
295
361
|
* probe를 다시 돌리면 화면 하나 그리자고 외부를 친다. 그래서 monitor가 돌 때 적어 둔다.
|
|
296
362
|
*/
|
|
297
|
-
async function
|
|
298
|
-
await store.scope('monitor').set(ACTIVE_SOURCE_KEY,
|
|
363
|
+
async function rememberMonitorSources(store, sourceIds) {
|
|
364
|
+
await store.scope('monitor').set(ACTIVE_SOURCE_KEY, JSON.stringify([...sourceIds]));
|
|
299
365
|
}
|
|
300
366
|
/**
|
|
301
|
-
* 마지막으로 붙었던
|
|
302
|
-
*
|
|
367
|
+
* 마지막으로 붙었던 통로들.
|
|
368
|
+
*
|
|
369
|
+
* 예전에는 문자열 하나였다 — 통로가 하나뿐이라는 전제였고, 코드와 작업 항목이 다른 곳에
|
|
370
|
+
* 있는 프로젝트에서 그 전제가 깨졌다. 옛 기록(문자열 하나)도 그대로 읽는다: 형식이
|
|
371
|
+
* 바뀌었다는 이유로 이전 설치의 shadow 기록이 사라진 것처럼 보이면 안 된다.
|
|
303
372
|
*/
|
|
373
|
+
async function activeMonitorSources(store) {
|
|
374
|
+
const raw = await store.scope('monitor').get(ACTIVE_SOURCE_KEY);
|
|
375
|
+
if (!raw)
|
|
376
|
+
return ['github-poll'];
|
|
377
|
+
try {
|
|
378
|
+
const parsed = JSON.parse(raw);
|
|
379
|
+
if (Array.isArray(parsed) && parsed.every((id) => typeof id === 'string')) {
|
|
380
|
+
return parsed.length > 0 ? parsed : ['github-poll'];
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
// 옛 형식이다 — 문자열 하나
|
|
385
|
+
}
|
|
386
|
+
return [raw];
|
|
387
|
+
}
|
|
388
|
+
/** 통로별 scope. 기록이 하나뿐이던 시절의 호출부는 첫 통로를 본다. */
|
|
389
|
+
async function activeMonitorScopes(store) {
|
|
390
|
+
return (await activeMonitorSources(store)).map(monitorScope);
|
|
391
|
+
}
|
|
304
392
|
async function activeMonitorScope(store) {
|
|
305
|
-
|
|
306
|
-
return monitorScope(sourceId ?? 'github-poll');
|
|
393
|
+
return (await activeMonitorScopes(store))[0];
|
|
307
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
* JAM 을 부르는 방법. **한 곳에서만 정한다** — 감시와 작업 조사가 서로 다른 방법으로
|
|
397
|
+
* JAM 을 부르면 한쪽만 되는 상태가 생긴다(실제로 그랬다: 감시 경로는 이 값을 아예 넘기지
|
|
398
|
+
* 않아 선언된 JAM binding 이 통로를 만들지 못했다).
|
|
399
|
+
*/
|
|
400
|
+
const jamLauncher = () => process.env.ASC_JAM_PATH
|
|
401
|
+
? { command: process.env.ASC_JAM_PATH, args: [] }
|
|
402
|
+
: { command: 'npx', args: ['--yes', '@jam-mcp/launcher'] };
|
|
403
|
+
/** 조립 입력에 실을 JAM 갈래. */
|
|
404
|
+
const jamComposition = (projectRoot) => {
|
|
405
|
+
const launcher = jamLauncher();
|
|
406
|
+
return { jam: { command: launcher.command, args: [...launcher.args, 'serve'], cwd: projectRoot } };
|
|
407
|
+
};
|
|
408
|
+
/** 이 빌드가 아는 adapter 들. 감시와 작업 조사가 같은 목록을 본다. */
|
|
409
|
+
const monitorAdapters = () => {
|
|
410
|
+
const launcher = jamLauncher();
|
|
411
|
+
return [
|
|
412
|
+
new GitHubAdapter(),
|
|
413
|
+
new GitLabAdapter(),
|
|
414
|
+
new JamAdapter(process.env.ASC_JAM_PATH ? {} : { command: launcher.command, args: launcher.args }),
|
|
415
|
+
];
|
|
416
|
+
};
|
|
308
417
|
/** user-owned ASC home. */
|
|
309
418
|
const ascHome = () => process.env.ASC_HOME ?? join(homedir(), '.asc');
|
|
310
419
|
const DECISION_ERROR = {
|
|
@@ -419,12 +528,23 @@ export async function runAscCommand(argv, entry = 'runtime') {
|
|
|
419
528
|
}
|
|
420
529
|
const { values, positionals } = parsed;
|
|
421
530
|
// 실행 중인 버전을 묻는 유일한 공식 통로. 설치 안내가 버전을 핀으로 고정하는데
|
|
422
|
-
// 정작 지금 도는 것이 무엇인지 물을 방법이 없었다 (
|
|
531
|
+
// 정작 지금 도는 것이 무엇인지 물을 방법이 없었다 (Windows 실전 실측 ASC-1).
|
|
423
532
|
if (values.version || positionals[0] === 'version') {
|
|
424
533
|
console.log(RELEASE_VERSION);
|
|
425
534
|
return 0;
|
|
426
535
|
}
|
|
427
|
-
|
|
536
|
+
try {
|
|
537
|
+
return await runParsedCommand(values, positionals, entry, argv);
|
|
538
|
+
}
|
|
539
|
+
finally {
|
|
540
|
+
// **어느 명령이든** 자기가 띄운 도구 자식을 닫는다 (JAM MCP 서버 등).
|
|
541
|
+
//
|
|
542
|
+
// 예전에는 `proceed` 한 곳에서만 닫았다. 감시 경로가 JAM 통로를 열게 되자
|
|
543
|
+
// `monitor scan` 과 `runtime tick` 이 할 일을 다 하고도 종료하지 못했고, 등록된
|
|
544
|
+
// 서비스가 회차마다 그 프로세스를 하나씩 남겼다 — 실측에서 10분 넘게 살아 있었다.
|
|
545
|
+
// 닫는 자리를 명령마다 두면 언젠가 또 빠진다. 나가는 문은 하나다.
|
|
546
|
+
await closeToolClients();
|
|
547
|
+
}
|
|
428
548
|
}
|
|
429
549
|
function parseArgsOrThrow(argv) {
|
|
430
550
|
return parseArgs({
|
|
@@ -457,6 +577,7 @@ function parseArgsOrThrow(argv) {
|
|
|
457
577
|
intent: { type: 'string' },
|
|
458
578
|
grant: { type: 'string' },
|
|
459
579
|
'interval-min': { type: 'string' },
|
|
580
|
+
detach: { type: 'boolean' },
|
|
460
581
|
'delta-min': { type: 'string' },
|
|
461
582
|
'reconcile-min': { type: 'string' },
|
|
462
583
|
'census-min': { type: 'string' },
|
|
@@ -465,6 +586,11 @@ function parseArgsOrThrow(argv) {
|
|
|
465
586
|
validator: { type: 'string' },
|
|
466
587
|
result: { type: 'string' },
|
|
467
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 },
|
|
468
594
|
blocker: { type: 'string', multiple: true },
|
|
469
595
|
risk: { type: 'string', multiple: true },
|
|
470
596
|
evidence: { type: 'string', multiple: true },
|
|
@@ -563,7 +689,15 @@ async function runParsedCommand(values, positionals, entry, argv) {
|
|
|
563
689
|
// 선택된 build로 넘길 것이 있으면 여기서 넘긴다. **선택 자체를 다루는 명령은 넘기지
|
|
564
690
|
// 않는다** — 잘못 가리키는 선택을 고치거나 들여다보는 명령이 그 선택 때문에 못 돌면
|
|
565
691
|
// 사람이 갇힌다.
|
|
566
|
-
|
|
692
|
+
// 기계 수준 명령은 **프로젝트와 무관하다.** 붙지 않은 자리에서도 답해야 하고,
|
|
693
|
+
// 등록된 서비스는 어느 프로젝트 안에서 도는 것이 아니다 (설계 §4.1).
|
|
694
|
+
const machineLevelRuntime = group === 'runtime' &&
|
|
695
|
+
(command === 'use' ||
|
|
696
|
+
command === 'status' ||
|
|
697
|
+
command === 'list' ||
|
|
698
|
+
command === 'service' ||
|
|
699
|
+
(command === 'tick' && Boolean(values.all)));
|
|
700
|
+
if (!machineLevelRuntime) {
|
|
567
701
|
const redispatched = await redispatchIfNeeded(argv);
|
|
568
702
|
if (redispatched !== null)
|
|
569
703
|
return redispatched;
|
|
@@ -576,17 +710,29 @@ async function runParsedCommand(values, positionals, entry, argv) {
|
|
|
576
710
|
if (group === 'runtime' && (command === 'use' || command === 'status')) {
|
|
577
711
|
return runRuntimeSelect(command, positionals[2], positionals[3], values);
|
|
578
712
|
}
|
|
713
|
+
if (group === 'runtime' && command === 'list')
|
|
714
|
+
return runRuntimeList(values);
|
|
715
|
+
if (group === 'runtime' && command === 'service')
|
|
716
|
+
return runRuntimeService(positionals[2], values);
|
|
717
|
+
// 기계 전체 회차 — 등록된 서비스가 부르는 갈래다.
|
|
718
|
+
if (group === 'runtime' && command === 'tick' && values.all)
|
|
719
|
+
return runRuntimeTickAll(values);
|
|
579
720
|
if (group === 'host')
|
|
580
721
|
return runHost(command, positionals[2], positionals[3], values);
|
|
581
722
|
// setup은 **붙기 전에도** 답을 줘야 한다. 아래 discoverRoot 실패는 exit 2로 끊는데,
|
|
582
723
|
// 그러면 "아직 안 붙었다"를 확인하려고 부른 명령이 안 붙었다는 이유로 죽는다.
|
|
583
724
|
if (group === 'setup')
|
|
584
725
|
return runSetup(command, values, entry);
|
|
726
|
+
// `front open` 도 같은 이유로 여기서 답한다. Host lifecycle이 부르는 갈래라
|
|
727
|
+
// **붙지 않은 자리에서 실패하면 안 된다** — 그 자리는 대부분 ASC와 무관한 프로젝트이고,
|
|
728
|
+
// 거기서 exit 2를 내는 것은 남의 세션에 오류를 얹는 일이다 (C-11 불변식 ⑪).
|
|
729
|
+
if (group === 'front' && command === 'open')
|
|
730
|
+
return runFrontOpen(values);
|
|
585
731
|
// adopt는 **붙기 전의 명령이다.** 붙을 Profile을 만드는 것이 일이므로 attach를 요구하면
|
|
586
732
|
// 순서가 뒤집힌다. 나머지 profile 명령은 아래 attach 경로에 그대로 남는다.
|
|
587
733
|
if (group === 'profile' && command === 'adopt')
|
|
588
734
|
return runProfileAdopt(values, entry);
|
|
589
|
-
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)) {
|
|
590
736
|
console.error(`Unknown command: ${group}\n\n${USAGE}`);
|
|
591
737
|
return 2;
|
|
592
738
|
}
|
|
@@ -628,6 +774,9 @@ async function runParsedCommand(values, positionals, entry, argv) {
|
|
|
628
774
|
// 새 대화가 붙었을 때 지금 상태를 되찾는다 (C-12 §4). 읽기만 한다.
|
|
629
775
|
if (group === 'front')
|
|
630
776
|
return runFront(command, values, store, root);
|
|
777
|
+
// 밖에 물은 것이 실제로 전달됐는가. 읽기만 한다.
|
|
778
|
+
if (group === 'coordination')
|
|
779
|
+
return runCoordination(command, values, store, root, guard.runtime);
|
|
631
780
|
// 사람에게 올릴 자격이 있는가 (C-13). 자격 없으면 request가 만들어지지 않는다.
|
|
632
781
|
if (group === 'escalate')
|
|
633
782
|
return runEscalate(command, target, values, store, guard.runtime);
|
|
@@ -947,7 +1096,7 @@ async function runInit(values) {
|
|
|
947
1096
|
installRoot: installRoot(),
|
|
948
1097
|
externalProfileRoot: externalProfileRoot(),
|
|
949
1098
|
...(attachedRoot ? { ascRoot: attachedRoot } : {}),
|
|
950
|
-
hosts: [{ id: 'claude', installed: await verifyInstalled(
|
|
1099
|
+
hosts: [{ id: 'claude', installed: await verifyInstalled(hostPaths()) }],
|
|
951
1100
|
bindings,
|
|
952
1101
|
declaredPolicies: declaredPolicies(attachedRoot ? await attachedRuntime(attachedRoot) : undefined),
|
|
953
1102
|
});
|
|
@@ -1057,11 +1206,18 @@ async function runRuntimeSelect(command, mode, source, values) {
|
|
|
1057
1206
|
if (command === 'status') {
|
|
1058
1207
|
const selection = await readRuntimeSelection(home);
|
|
1059
1208
|
const target = await resolveRuntimeTarget(selection);
|
|
1209
|
+
// 어느 build를 쓰는지(C-14)와 지금 관측하고 있는지(C-12)는 다른 사실이다. 한 화면에
|
|
1210
|
+
// 같이 두되 섞지 않는다 — 붙지 않은 곳에서는 아래 절이 통째로 없다.
|
|
1211
|
+
const background = await backgroundHere(values);
|
|
1212
|
+
// 서비스 등록은 workspace 와 무관한 기계의 사실이다. 같은 화면에 두되 섞지 않는다.
|
|
1213
|
+
const service = await serviceHealth(values);
|
|
1060
1214
|
if (values.json) {
|
|
1061
1215
|
console.log(JSON.stringify({
|
|
1062
1216
|
selection: selection ?? null,
|
|
1063
1217
|
target,
|
|
1064
1218
|
file: selectionPath(home),
|
|
1219
|
+
...(service ? { service } : {}),
|
|
1220
|
+
...(background ? { background } : {}),
|
|
1065
1221
|
// 해법은 데이터로 준다 — agent가 산문에서 경로를 추론하지 않는다
|
|
1066
1222
|
...('code' in target ? { action: remediationAction(target) } : {}),
|
|
1067
1223
|
}, null, 2));
|
|
@@ -1073,6 +1229,11 @@ async function runRuntimeSelect(command, mode, source, values) {
|
|
|
1073
1229
|
return 1;
|
|
1074
1230
|
}
|
|
1075
1231
|
console.log(runtimeSelectionLine(target));
|
|
1232
|
+
if (service)
|
|
1233
|
+
console.log(service.line);
|
|
1234
|
+
if (background)
|
|
1235
|
+
for (const line of renderBackground(background))
|
|
1236
|
+
console.log(line);
|
|
1076
1237
|
return 0;
|
|
1077
1238
|
}
|
|
1078
1239
|
if (mode !== 'package' && mode !== 'development') {
|
|
@@ -1178,7 +1339,7 @@ async function detectSetupState(values, entry) {
|
|
|
1178
1339
|
// "붙어 있음"으로 넘기면 plan이 applied를 답하며 실패할 proceed를 준다 (실측 ASC-2).
|
|
1179
1340
|
const attachmentBroken = ascRoot ? (await inspectSetup(ascRoot)).attachment === 'BROKEN' : false;
|
|
1180
1341
|
const scope = values.scope === 'project' ? 'project' : 'local';
|
|
1181
|
-
const hostReport = await verifyInstall(
|
|
1342
|
+
const hostReport = await verifyInstall(hostPaths());
|
|
1182
1343
|
return {
|
|
1183
1344
|
entry,
|
|
1184
1345
|
projectRoot,
|
|
@@ -1189,11 +1350,129 @@ async function detectSetupState(values, entry) {
|
|
|
1189
1350
|
profileCandidates: await availableProfiles(installRoot(), externalProfileRoot()),
|
|
1190
1351
|
scope,
|
|
1191
1352
|
host: [{ id: 'claude', status: hostReport.status }],
|
|
1353
|
+
// Profile 이 작업 도구를 선언했으면 그 준비 상태까지 본다 (설계 §9.3).
|
|
1354
|
+
...(await workBindingState(ascRoot, projectRoot)),
|
|
1355
|
+
// 이 기계의 지속 등록. **별도 onboarding 을 만들지 않는다** — 같은 계획에 함께 든다.
|
|
1356
|
+
...(await persistentRuntimeState(values)),
|
|
1192
1357
|
// **bootstrap으로 들어왔을 때만 본다.** 설치된 runtime이 자기를 다시 설치할 이유가
|
|
1193
1358
|
// 없고, 그 축을 안 그리면 plan은 설치된 `asc` 를 전제하지도 않는다 (C-14 §3.4).
|
|
1194
1359
|
...(entry === 'bootstrap' ? { stableRuntime: await detectStableInstall(nodeProcessRunner) } : {}),
|
|
1195
1360
|
};
|
|
1196
1361
|
}
|
|
1362
|
+
/**
|
|
1363
|
+
* 지금 도는 것이 **설치된 패키지인가.**
|
|
1364
|
+
*
|
|
1365
|
+
* 등록물은 지금 도는 진입점의 절대 경로를 박는다. checkout 이나 일회용 prefix 에서
|
|
1366
|
+
* 등록하면 사라질 경로를 기계에 남기는 일이 된다 — 등록은 이 기계에 오래 남을 설치본이
|
|
1367
|
+
* 스스로 할 때만 의미가 있다.
|
|
1368
|
+
*/
|
|
1369
|
+
function runningFromInstalledPackage() {
|
|
1370
|
+
return fileURLToPath(import.meta.url).replace(/\\/g, '/').includes('/node_modules/@asc-agent/runtime/');
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* 지금 이 실행에서 기계 등록을 다뤄도 되는가.
|
|
1374
|
+
*
|
|
1375
|
+
* **기계 서비스는 HOME 으로 격리되지 않는다.** ASC_HOME 을 옮겨도 launchd·Task
|
|
1376
|
+
* Scheduler·systemd 는 같은 기계 하나를 본다 — 그래서 격리된 환경에서 setup 을 통째로
|
|
1377
|
+
* 돌려 보는 검증에는 이 축을 끄는 길이 필요하다. 그 길이 없으면 검증이 실행한 기계에
|
|
1378
|
+
* 진짜 서비스를 남긴다.
|
|
1379
|
+
*/
|
|
1380
|
+
function serviceRegistrationAllowed() {
|
|
1381
|
+
if (process.env.ASC_SERVICE === 'off')
|
|
1382
|
+
return false;
|
|
1383
|
+
return runningFromInstalledPackage();
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* 이 기계에 ASC runtime 이 등록돼 있는가 (설계 §6, Gate 7).
|
|
1387
|
+
*
|
|
1388
|
+
* 물어볼 수 없으면 이 축을 그리지 않는다 — 모르는 것을 "해야 한다"로 적으면 설치되지
|
|
1389
|
+
* 않은 기계에서 setup 이 영영 끝나지 않는다.
|
|
1390
|
+
*/
|
|
1391
|
+
async function persistentRuntimeState(values) {
|
|
1392
|
+
if (!serviceRegistrationAllowed())
|
|
1393
|
+
return {};
|
|
1394
|
+
const adapter = serviceAdapter();
|
|
1395
|
+
if (!adapter)
|
|
1396
|
+
return {};
|
|
1397
|
+
const plan = await planPersistentRuntime(adapter, serviceCommand(serviceInterval(values))).catch(() => null);
|
|
1398
|
+
if (!plan)
|
|
1399
|
+
return {};
|
|
1400
|
+
return {
|
|
1401
|
+
persistentRuntime: {
|
|
1402
|
+
action: plan.action,
|
|
1403
|
+
adapter: adapter.id,
|
|
1404
|
+
...(plan.state.kind !== 'CURRENT' && 'detail' in plan.state && plan.state.detail
|
|
1405
|
+
? { detail: plan.state.detail }
|
|
1406
|
+
: {}),
|
|
1407
|
+
},
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Profile 이 선언한 작업 도구가 지금 쓸 수 있는가 (설계 §9.3).
|
|
1412
|
+
*
|
|
1413
|
+
* **선언이 없으면 아무것도 보지 않는다** — 선언되지 않은 도구를 준비시키는 것은
|
|
1414
|
+
* 사람이 하지 않은 결정을 대신 내리는 일이다.
|
|
1415
|
+
*
|
|
1416
|
+
* 판정은 그 도구가 한다. 여기서 하는 것은 그 판정을 setup 이 읽을 수 있는 모양으로
|
|
1417
|
+
* 옮기는 것뿐이고, 도구를 실행하지 못하면 이 축을 그리지 않는다(모르는 것을
|
|
1418
|
+
* "준비 안 됨"으로 적지 않는다).
|
|
1419
|
+
*/
|
|
1420
|
+
async function workBindingState(ascRoot, projectRoot) {
|
|
1421
|
+
if (!ascRoot)
|
|
1422
|
+
return {};
|
|
1423
|
+
const runtime = await attachedRuntime(ascRoot);
|
|
1424
|
+
const declared = (runtime?.layers.profile.bindings ?? []).find((binding) => binding.role === 'work');
|
|
1425
|
+
// 지금 준비 상태를 물어볼 수 있는 도구는 JAM 뿐이다. 다른 adapter 가 생기면 여기가 는다.
|
|
1426
|
+
if (!declared || declared.adapter !== 'jam')
|
|
1427
|
+
return {};
|
|
1428
|
+
const launcher = jamLauncher();
|
|
1429
|
+
const adapter = new JamAdapter(process.env.ASC_JAM_PATH ? {} : { command: launcher.command, args: launcher.args });
|
|
1430
|
+
const status = await adapter
|
|
1431
|
+
.runtime({ projectRoot, env: process.env })
|
|
1432
|
+
.catch(() => ({ state: 'UNAVAILABLE', detail: 'could not ask' }));
|
|
1433
|
+
if (status.state === 'AVAILABLE') {
|
|
1434
|
+
return { workBinding: { adapter: 'jam', resource: declared.resource, ready: true } };
|
|
1435
|
+
}
|
|
1436
|
+
const remedy = adapter.lastRemedy();
|
|
1437
|
+
// 도구를 아예 못 불렀으면 이 축을 그리지 않는다 — 그 상태에서 "고쳐야 한다"고 적으면
|
|
1438
|
+
// 설치되지 않은 기계에서 setup 이 영영 끝나지 않는다.
|
|
1439
|
+
if (!remedy)
|
|
1440
|
+
return {};
|
|
1441
|
+
const version = await jamVersion(projectRoot);
|
|
1442
|
+
return {
|
|
1443
|
+
workBinding: {
|
|
1444
|
+
adapter: 'jam',
|
|
1445
|
+
resource: declared.resource,
|
|
1446
|
+
ready: false,
|
|
1447
|
+
remedy: remedy.kind,
|
|
1448
|
+
detail: remedy.detail,
|
|
1449
|
+
...(version ? { version } : {}),
|
|
1450
|
+
},
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* JAM 이 말한 자기 버전. **여기서 정하지 않는다** — 두 제품의 릴리스를 묶지 않기 위해서다.
|
|
1455
|
+
* 못 읽으면 undefined 이고, 그러면 계획이 그 도구를 부르지 않는다.
|
|
1456
|
+
*/
|
|
1457
|
+
async function jamVersion(projectRoot) {
|
|
1458
|
+
const launcher = jamLauncher();
|
|
1459
|
+
const { command, args } = resolveJamCommand(process.env, launcher.command, launcher.args);
|
|
1460
|
+
try {
|
|
1461
|
+
const { stdout } = await execFileAsync(command, [...args, 'doctor', '--json'], { cwd: projectRoot });
|
|
1462
|
+
return JSON.parse(stdout).axes?.packageVersion;
|
|
1463
|
+
}
|
|
1464
|
+
catch (error) {
|
|
1465
|
+
const stdout = error.stdout;
|
|
1466
|
+
if (!stdout)
|
|
1467
|
+
return undefined;
|
|
1468
|
+
try {
|
|
1469
|
+
return JSON.parse(stdout).axes?.packageVersion;
|
|
1470
|
+
}
|
|
1471
|
+
catch {
|
|
1472
|
+
return undefined;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1197
1476
|
/**
|
|
1198
1477
|
* `npm` 을 shim으로 부르지 않는다.
|
|
1199
1478
|
*
|
|
@@ -1374,6 +1653,28 @@ async function runSetupLifecycle(command, values, entry) {
|
|
|
1374
1653
|
if (code !== 0)
|
|
1375
1654
|
throw new Error(`attach 실패 (exit ${code})`);
|
|
1376
1655
|
},
|
|
1656
|
+
registerPersistentRuntime: async () => {
|
|
1657
|
+
const adapter = serviceAdapter();
|
|
1658
|
+
if (!adapter)
|
|
1659
|
+
return;
|
|
1660
|
+
try {
|
|
1661
|
+
await adapter.install(serviceCommand(serviceInterval(values)));
|
|
1662
|
+
console.log(`Persistent runtime registered with ${adapter.id}.`);
|
|
1663
|
+
}
|
|
1664
|
+
catch (error) {
|
|
1665
|
+
// **등록 실패는 attach 실패가 아니다.** 기계 등록은 이 프로젝트가 붙는 것과
|
|
1666
|
+
// 다른 축이고, 서비스 관리자가 없거나 거절하는 환경은 흔하다. 조용히 넘기지도
|
|
1667
|
+
// 않는다 — 무엇이 안 됐는지 말하고, 상태는 `runtime status` 가 계속 답한다.
|
|
1668
|
+
console.error(`Persistent runtime could not be registered with ${adapter.id}: ${String(error)}`);
|
|
1669
|
+
}
|
|
1670
|
+
},
|
|
1671
|
+
// 그 도구의 공식 setup 을 부른다. ASC 가 그 설정을 손으로 조립하지 않는다.
|
|
1672
|
+
setupWorkBinding: async (change) => {
|
|
1673
|
+
const outcome = await healJam({ cwd: process.cwd(), version: change.version });
|
|
1674
|
+
console.log(healLine(outcome));
|
|
1675
|
+
if (outcome.kind === 'FAILED')
|
|
1676
|
+
throw new Error(healLine(outcome));
|
|
1677
|
+
},
|
|
1377
1678
|
installHost: async (change) => {
|
|
1378
1679
|
const code = await runHost('claude', 'install', undefined, { ...values, json: false });
|
|
1379
1680
|
if (code !== 0)
|
|
@@ -1645,11 +1946,23 @@ async function runProfile(command, values, root) {
|
|
|
1645
1946
|
async function scmFor(resolved) {
|
|
1646
1947
|
if (!resolved)
|
|
1647
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
|
+
}
|
|
1648
1961
|
const token = await discoverToken();
|
|
1649
1962
|
if (!token)
|
|
1650
1963
|
return undefined;
|
|
1651
1964
|
const sourceRefs = {};
|
|
1652
|
-
for (const source of
|
|
1965
|
+
for (const source of sources) {
|
|
1653
1966
|
if (source.ref)
|
|
1654
1967
|
sourceRefs[source.id] = { ref: source.ref };
|
|
1655
1968
|
}
|
|
@@ -1668,7 +1981,7 @@ async function runHost(provider, command, target, values) {
|
|
|
1668
1981
|
console.error(`Unknown host: ${provider ?? '(none)'} — only claude is supported today`);
|
|
1669
1982
|
return 2;
|
|
1670
1983
|
}
|
|
1671
|
-
const paths =
|
|
1984
|
+
const paths = hostPaths();
|
|
1672
1985
|
switch (command) {
|
|
1673
1986
|
case 'install': {
|
|
1674
1987
|
const before = await verifyInstall(paths);
|
|
@@ -2084,13 +2397,7 @@ function dependencyCandidates(workItem) {
|
|
|
2084
2397
|
*/
|
|
2085
2398
|
async function buildWorkIngress(store, root, runtime, resolved) {
|
|
2086
2399
|
const { root: projectRoot } = await discoverProjectRoot(process.cwd());
|
|
2087
|
-
|
|
2088
|
-
const jamCommand = process.env.ASC_JAM_PATH ? undefined : { command: 'npx', args: ['--yes', '@jam-mcp/launcher'] };
|
|
2089
|
-
const adapters = [
|
|
2090
|
-
new GitHubAdapter(),
|
|
2091
|
-
new GitLabAdapter(),
|
|
2092
|
-
new JamAdapter(jamCommand ?? {}),
|
|
2093
|
-
];
|
|
2400
|
+
const adapters = monitorAdapters();
|
|
2094
2401
|
const declared = resolved?.layers.profile.bindings ?? [];
|
|
2095
2402
|
const plan = await composeBindings({
|
|
2096
2403
|
context: { projectRoot, env: process.env },
|
|
@@ -2111,7 +2418,7 @@ async function buildWorkIngress(store, root, runtime, resolved) {
|
|
|
2111
2418
|
// capability 해석은 후보가 유일할 때 이미 스스로 풀린다.
|
|
2112
2419
|
roles: rolesFor(plan, declared),
|
|
2113
2420
|
perPage: 30,
|
|
2114
|
-
|
|
2421
|
+
...jamComposition(projectRoot),
|
|
2115
2422
|
endpointFor: (binding) => endpointOf(adapters, binding),
|
|
2116
2423
|
});
|
|
2117
2424
|
const work = ports.resourceContext;
|
|
@@ -2374,7 +2681,7 @@ async function runSession(command, target, values, store, resolved) {
|
|
|
2374
2681
|
switch (command) {
|
|
2375
2682
|
case 'issue': {
|
|
2376
2683
|
// 누락과 오값은 다른 실수다 — "필수"라고 답하면 이미 준 사람은 자기가 무엇을
|
|
2377
|
-
// 틀렸는지 모른다 (
|
|
2684
|
+
// 틀렸는지 모른다 (Windows 실전 실측 ASC-3).
|
|
2378
2685
|
const roleChoices = SessionRole.options.join('|');
|
|
2379
2686
|
if (values.role === undefined || !values.goal) {
|
|
2380
2687
|
console.error(`--role and --goal are required (role: ${roleChoices})`);
|
|
@@ -2817,6 +3124,25 @@ const CLOSURE_CHECKLIST = 'closureChecklist';
|
|
|
2817
3124
|
/** Orchestration Audit (C-10). 회수 후에도 남는 기록이라 Progress와 다른 자리에 산다. */
|
|
2818
3125
|
const auditLedger = (store) => new AuditLedger(store.scope('audit'));
|
|
2819
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
|
+
}
|
|
2820
3146
|
/** Bounded Query (B-25). 결정권 판정에 Profile 책임 지도가 필요하다. */
|
|
2821
3147
|
const queryLedger = (store, resolved) => new QueryLedger(store.scope('query'), resolved?.ownership);
|
|
2822
3148
|
/**
|
|
@@ -3046,17 +3372,7 @@ function parseEnumArg(raw, allowed, flag) {
|
|
|
3046
3372
|
console.error(`Invalid value for --${flag}: ${String(raw)} (allowed: ${allowed.join('|')})`);
|
|
3047
3373
|
return null;
|
|
3048
3374
|
}
|
|
3049
|
-
|
|
3050
|
-
/**
|
|
3051
|
-
* 감시 엔진을 세운다. **조립과 출력을 나눈다** — 상시 Runtime(C-12)이 같은 엔진을
|
|
3052
|
-
* 주기적으로 부르려면 조립이 명령 출력 안에 갇혀 있으면 안 된다.
|
|
3053
|
-
*/
|
|
3054
|
-
async function buildMonitorEngine(values, store, resolved) {
|
|
3055
|
-
const token = await discoverToken();
|
|
3056
|
-
if (!token) {
|
|
3057
|
-
console.error('No GitHub token found. Set ASC_GITHUB_TOKEN, or run `gh auth login`.');
|
|
3058
|
-
return { ok: false, code: 2 };
|
|
3059
|
-
}
|
|
3375
|
+
async function buildMonitorEngines(values, store, resolved) {
|
|
3060
3376
|
// 감시 대상도, 누가 나인지도, 누가 승인자인지도 전부 설정에서 온다.
|
|
3061
3377
|
// 명령줄로 받던 값들은 각자 제자리(Profile·Override)를 찾았다.
|
|
3062
3378
|
const repo = resolved.layers.profile.project.repository;
|
|
@@ -3072,79 +3388,329 @@ async function buildMonitorEngine(values, store, resolved) {
|
|
|
3072
3388
|
// provider를 CLI가 고르지 않는다. 선언된 요구와 실제 remote에서 Binding이 풀리고,
|
|
3073
3389
|
// 갈리면 고르지 않고 말한다 — silent substitution 0 (C-11 §7).
|
|
3074
3390
|
const { root: projectRoot } = await discoverProjectRoot(process.cwd());
|
|
3075
|
-
const adapters =
|
|
3391
|
+
const adapters = monitorAdapters();
|
|
3076
3392
|
const declared = resolved.layers.profile.bindings ?? [];
|
|
3077
3393
|
const plan = await composeBindings({
|
|
3078
3394
|
context: { projectRoot, env: process.env },
|
|
3079
3395
|
adapters,
|
|
3080
3396
|
roles: declared.map((b) => ({ adapterId: b.adapter, resource: b.resource, role: b.role })),
|
|
3081
3397
|
});
|
|
3082
|
-
const
|
|
3398
|
+
const built = await buildObservationChannels({
|
|
3399
|
+
plan,
|
|
3400
|
+
perPage: 30,
|
|
3401
|
+
...jamComposition(projectRoot),
|
|
3402
|
+
endpointFor: (binding) => endpointOf(adapters, binding),
|
|
3403
|
+
});
|
|
3404
|
+
// canonical.read 처럼 한 곳이어야 의미가 서는 것은 예전 경로 그대로 역할로 고른다.
|
|
3405
|
+
const singular = await buildRuntimePorts({
|
|
3083
3406
|
plan,
|
|
3084
3407
|
roles: rolesFor(plan, declared),
|
|
3085
3408
|
perPage: 30,
|
|
3409
|
+
...jamComposition(projectRoot),
|
|
3086
3410
|
endpointFor: (binding) => endpointOf(adapters, binding),
|
|
3087
3411
|
});
|
|
3088
|
-
if (
|
|
3412
|
+
if (built.channels.length === 0) {
|
|
3089
3413
|
console.error('No monitoring channel could be built:');
|
|
3090
|
-
for (const reason of
|
|
3414
|
+
for (const reason of built.unavailable)
|
|
3091
3415
|
console.error(` - ${reason}`);
|
|
3092
3416
|
console.error('\nDeclare which provider takes which role in the Profile bindings.');
|
|
3093
3417
|
return { ok: false, code: 2 };
|
|
3094
3418
|
}
|
|
3095
3419
|
const canonicalPaths = resolved.layers.profile.canonical.sources.flatMap((source) => source.paths);
|
|
3096
3420
|
const myRoles = resolved.layers.override?.monitor.roles ?? [];
|
|
3097
|
-
const
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3421
|
+
const channels = built.channels.map((channel) => {
|
|
3422
|
+
const changeContext = channel.changeContext;
|
|
3423
|
+
const engine = new MonitorEngine({
|
|
3424
|
+
store,
|
|
3425
|
+
source: channel.eventSource,
|
|
3426
|
+
// 정본 대조는 채널이 아니라 프로젝트의 사실이다 — 역할로 고른 하나를 함께 쓴다.
|
|
3427
|
+
...(singular.scm ? { scm: singular.scm } : {}),
|
|
3428
|
+
...(channel.inventory ? { inventory: channel.inventory } : {}),
|
|
3429
|
+
// 밖에서 알아 온 사실을 실제로 공급한다 (C-07 §2~§4). 이것이 없으면 Relevance·
|
|
3430
|
+
// Shadow·Material Change가 코드에만 있고 실행 경로에는 없다.
|
|
3431
|
+
...(changeContext
|
|
3432
|
+
? {
|
|
3433
|
+
observe: buildEventObservation({
|
|
3434
|
+
change: changeContext,
|
|
3435
|
+
...(resolved.ownership ? { ownership: resolved.ownership } : {}),
|
|
3436
|
+
...(myRoles.length ? { myRoles } : {}),
|
|
3437
|
+
...(canonicalPaths.length ? { canonicalPaths } : {}),
|
|
3438
|
+
}),
|
|
3439
|
+
}
|
|
3440
|
+
: {}),
|
|
3441
|
+
// scope는 실제로 붙은 source가 정한다 — 채널이 둘이면 기록도 둘로 갈린다.
|
|
3442
|
+
observations: new ObservationLedger(store.scope(monitorScope(channel.eventSource.id))),
|
|
3443
|
+
// 조사 단계가 요청하는 통로들. 없는 것은 그 단계가 판정 불성립으로 남는다 (C-07 §6.2).
|
|
3444
|
+
// 없는 것을 있는 척하지 않는다.
|
|
3445
|
+
investigation: {
|
|
3446
|
+
...(channel.resourceContext ? { resource: channel.resourceContext } : {}),
|
|
3447
|
+
...(changeContext ? { change: changeContext } : {}),
|
|
3448
|
+
},
|
|
3449
|
+
// Core는 Profile을 모른다 — 조사에 필요한 프로젝트 사실을 Surface가 꺼내 준다.
|
|
3450
|
+
investigationContext: () => ({
|
|
3451
|
+
...(resolved.ownership ? { ownership: resolved.ownership } : {}),
|
|
3452
|
+
canonicalPaths,
|
|
3453
|
+
}),
|
|
3454
|
+
config: resolved.monitor,
|
|
3455
|
+
authorizedApprover: approver,
|
|
3456
|
+
canonicalSources: resolved.canonicalSources,
|
|
3457
|
+
// 처음 돌 때 과거를 통째로 긁으면 사람이 읽을 수 없다 (OM §18)
|
|
3458
|
+
...(values.backfill ? {} : { startFrom: new Date().toISOString() }),
|
|
3459
|
+
});
|
|
3460
|
+
return {
|
|
3461
|
+
engine,
|
|
3462
|
+
sourceId: channel.eventSource.id,
|
|
3463
|
+
label: `${channel.role ?? '(undeclared)'} · ${channel.adapterId}:${channel.resource}`,
|
|
3464
|
+
};
|
|
3133
3465
|
});
|
|
3134
|
-
await
|
|
3466
|
+
await rememberMonitorSources(store, channels.map((channel) => channel.sourceId));
|
|
3135
3467
|
// **밀려난 후보를 말한다.** 실 프로젝트 관측에서 나온 것이다: primary가 자격 없음으로
|
|
3136
3468
|
// 빠지고 mirror에 조용히 붙으면 "감시가 도는데 아무것도 안 잡히는" 상태가 된다.
|
|
3137
3469
|
// 고르는 것 자체는 막지 않되, 무엇이 밀렸는지 모르게 두지 않는다 (C-11 §7).
|
|
3138
3470
|
const skipped = plan.bindings.filter((b) => b.state !== 'AVAILABLE' && b.state !== 'DEGRADED');
|
|
3139
3471
|
if (skipped.length > 0) {
|
|
3140
|
-
console.error(
|
|
3472
|
+
console.error('Some candidates could not be used:');
|
|
3141
3473
|
for (const binding of skipped) {
|
|
3142
3474
|
console.error(` - ${binding.adapterId}:${binding.resource} — ${binding.state}${binding.detail ? ` (${binding.detail})` : ''}`);
|
|
3143
3475
|
}
|
|
3144
3476
|
}
|
|
3145
|
-
for (const reason of
|
|
3477
|
+
for (const reason of built.unavailable)
|
|
3146
3478
|
console.error(` - ${reason}`);
|
|
3147
|
-
return { ok: true,
|
|
3479
|
+
return { ok: true, channels, repo };
|
|
3480
|
+
}
|
|
3481
|
+
/**
|
|
3482
|
+
* 이 자리에서 background runtime이 돌고 있는가.
|
|
3483
|
+
*
|
|
3484
|
+
* **붙지 않았으면 없는 것으로 답한다** (`null`). `runtime status` 는 붙기 전에도 답해야
|
|
3485
|
+
* 하는 명령이라(C-14 §5) 여기서 attach를 요구하면 그 성질이 깨진다.
|
|
3486
|
+
*/
|
|
3487
|
+
async function backgroundHere(values) {
|
|
3488
|
+
const root = await discoverRoot(process.cwd(), values.root).catch(() => null);
|
|
3489
|
+
if (!root)
|
|
3490
|
+
return null;
|
|
3491
|
+
try {
|
|
3492
|
+
const scope = new MarkdownStateStore(root).scope('runtime');
|
|
3493
|
+
// 기본 주기 1분에서 나온 회수 기준. start가 다른 주기를 썼다면 그쪽이 더 길 뿐이고,
|
|
3494
|
+
// 살아 있는 lease를 죽은 것으로 읽지는 않는다 — heartbeat가 회차마다 갱신된다.
|
|
3495
|
+
return await readBackground(scope, staleAfter(60_000));
|
|
3496
|
+
}
|
|
3497
|
+
catch {
|
|
3498
|
+
// 읽지 못한 것을 "안 돌고 있다"로 적지 않는다 (C-12 불변식 ⑫).
|
|
3499
|
+
return null;
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
/**
|
|
3503
|
+
* 이 기계의 지속 등록 상태 (설계 §13.1).
|
|
3504
|
+
*
|
|
3505
|
+
* PID 는 여기 없다 — 등록물이 정본이고 프로세스는 갈아 끼우는 실행 인스턴스다
|
|
3506
|
+
* (설계 §4.2). 물어볼 수 없으면 `null` 이고, 그때 화면에 그 줄이 없다.
|
|
3507
|
+
*/
|
|
3508
|
+
async function serviceHealth(values) {
|
|
3509
|
+
const adapter = serviceAdapter();
|
|
3510
|
+
if (!adapter)
|
|
3511
|
+
return null;
|
|
3512
|
+
const plan = await planPersistentRuntime(adapter, serviceCommand(serviceInterval(values))).catch(() => null);
|
|
3513
|
+
if (!plan)
|
|
3514
|
+
return null;
|
|
3515
|
+
return { adapter: adapter.id, action: plan.action, line: persistentRuntimeLine(adapter.id, plan) };
|
|
3516
|
+
}
|
|
3517
|
+
/**
|
|
3518
|
+
* 이 기계가 아는 workspace 들 (설계 §6).
|
|
3519
|
+
*
|
|
3520
|
+
* **두 번째 등록부를 읽지 않는다** — workspace index 하나에서 계산한다.
|
|
3521
|
+
*/
|
|
3522
|
+
async function machineWorkspaces() {
|
|
3523
|
+
const home = ascHome();
|
|
3524
|
+
const index = await readIndex(home);
|
|
3525
|
+
return viewWorkspaces(Object.values(index.workspaces).map((workspace) => ({
|
|
3526
|
+
workspaceId: workspace.workspaceId,
|
|
3527
|
+
root: join(home, 'workspaces', workspace.workspaceId),
|
|
3528
|
+
aliases: workspace.aliases,
|
|
3529
|
+
locators: locatorsOf(index, workspace.workspaceId).map((entry) => entry.locator),
|
|
3530
|
+
})), (path) => existsSync(path));
|
|
3531
|
+
}
|
|
3532
|
+
/**
|
|
3533
|
+
* `asc runtime list` — 기계 전체 화면 (설계 §13.2).
|
|
3534
|
+
*
|
|
3535
|
+
* workspace 마다 `cd` 해서 status 를 반복하게 만들지 않는다. 여기서 밖을 치지 않는다 —
|
|
3536
|
+
* 이것은 읽기이고, 관측은 회차가 한다.
|
|
3537
|
+
*/
|
|
3538
|
+
async function runRuntimeList(values) {
|
|
3539
|
+
const views = await machineWorkspaces();
|
|
3540
|
+
if (values.json) {
|
|
3541
|
+
console.log(JSON.stringify({ workspaces: views }, null, 2));
|
|
3542
|
+
return 0;
|
|
3543
|
+
}
|
|
3544
|
+
for (const line of renderWorkspaces(views))
|
|
3545
|
+
console.log(line);
|
|
3546
|
+
return 0;
|
|
3547
|
+
}
|
|
3548
|
+
/** 이 OS 의 등록 통로. 모르는 OS 면 `null` — 없는 것을 있는 척하지 않는다. */
|
|
3549
|
+
function serviceAdapter() {
|
|
3550
|
+
switch (serviceAdapterFor(process.platform)) {
|
|
3551
|
+
case 'launchd':
|
|
3552
|
+
return launchdAdapter();
|
|
3553
|
+
case 'schtasks':
|
|
3554
|
+
return schtasksAdapter();
|
|
3555
|
+
case 'systemd-user':
|
|
3556
|
+
return systemdUserAdapter();
|
|
3557
|
+
default:
|
|
3558
|
+
return null;
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
/**
|
|
3562
|
+
* 등록물이 실행할 명령. **한 회차만 돈다.**
|
|
3563
|
+
*
|
|
3564
|
+
* 지금 도는 실행 파일과 진입점을 그대로 쓴다 — 어느 build 를 쓸지는 그 진입점이 다시
|
|
3565
|
+
* 정하므로(C-14), 등록물이 build 를 고르는 일은 없다.
|
|
3566
|
+
*/
|
|
3567
|
+
function serviceCommand(intervalSeconds) {
|
|
3568
|
+
return {
|
|
3569
|
+
program: process.execPath,
|
|
3570
|
+
args: [fileURLToPath(import.meta.url), 'runtime', 'tick', '--all'],
|
|
3571
|
+
intervalSeconds,
|
|
3572
|
+
...serviceEnvironment(),
|
|
3573
|
+
logPath: join(ascHome(), 'service.log'),
|
|
3574
|
+
};
|
|
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
|
+
}
|
|
3590
|
+
const serviceInterval = (values) => values['interval-min'] === undefined ? 5 * 60 : Math.max(60, Number(values['interval-min']) * 60);
|
|
3591
|
+
/**
|
|
3592
|
+
* `asc runtime service` — 이 기계의 지속 등록 (설계 §4).
|
|
3593
|
+
*
|
|
3594
|
+
* **workspace 마다 하나가 아니다.** 사용자/기계당 하나이고, 그 하나가 아는 workspace
|
|
3595
|
+
* 전부를 돌본다. 등록물은 우리가 만든 것만 다룬다.
|
|
3596
|
+
*/
|
|
3597
|
+
async function runRuntimeService(command, values) {
|
|
3598
|
+
const adapter = serviceAdapter();
|
|
3599
|
+
if (!adapter) {
|
|
3600
|
+
const detail = `no user-scope service manager for ${process.platform}`;
|
|
3601
|
+
if (values.json)
|
|
3602
|
+
console.log(JSON.stringify({ supported: false, detail }, null, 2));
|
|
3603
|
+
else
|
|
3604
|
+
console.error(`Persistent runtime: ${detail}`);
|
|
3605
|
+
// 못 하는 것을 "했다"로 적지 않는다. 다만 이것이 오류는 아니다.
|
|
3606
|
+
return 0;
|
|
3607
|
+
}
|
|
3608
|
+
const wanted = serviceCommand(serviceInterval(values));
|
|
3609
|
+
if (command === undefined || command === 'status') {
|
|
3610
|
+
const plan = await planPersistentRuntime(adapter, wanted);
|
|
3611
|
+
if (values.json) {
|
|
3612
|
+
console.log(JSON.stringify({ adapter: adapter.id, ...plan }, null, 2));
|
|
3613
|
+
return 0;
|
|
3614
|
+
}
|
|
3615
|
+
console.log(persistentRuntimeLine(adapter.id, plan));
|
|
3616
|
+
return 0;
|
|
3617
|
+
}
|
|
3618
|
+
if (command === 'install') {
|
|
3619
|
+
const plan = await planPersistentRuntime(adapter, wanted);
|
|
3620
|
+
if (plan.action === 'unsupported') {
|
|
3621
|
+
console.error(persistentRuntimeLine(adapter.id, plan));
|
|
3622
|
+
return 1;
|
|
3623
|
+
}
|
|
3624
|
+
if (plan.action === 'none') {
|
|
3625
|
+
// 멱등이다 — 같은 것을 다시 등록하지 않는다
|
|
3626
|
+
console.log(persistentRuntimeLine(adapter.id, plan));
|
|
3627
|
+
return 0;
|
|
3628
|
+
}
|
|
3629
|
+
await adapter.install(wanted);
|
|
3630
|
+
console.log(`Persistent runtime registered with ${adapter.id} — one pass every ${wanted.intervalSeconds / 60} min.`);
|
|
3631
|
+
return 0;
|
|
3632
|
+
}
|
|
3633
|
+
if (command === 'uninstall') {
|
|
3634
|
+
await adapter.uninstall();
|
|
3635
|
+
console.log(`Persistent runtime registration removed from ${adapter.id}.`);
|
|
3636
|
+
return 0;
|
|
3637
|
+
}
|
|
3638
|
+
console.error(`Unknown runtime service command: ${command}\n\n${USAGE}`);
|
|
3639
|
+
return 2;
|
|
3640
|
+
}
|
|
3641
|
+
/**
|
|
3642
|
+
* 기계 전체 한 회차 (설계 §7).
|
|
3643
|
+
*
|
|
3644
|
+
* 싱글턴은 **판단하지 않는다** — 어느 workspace 가 지금 돌 수 있는지만 고르고, 회차 자체는
|
|
3645
|
+
* 기존 경로가 그대로 돈다. workspace 마다 별개의 짧은 프로세스로 도는 이유는 둘이다:
|
|
3646
|
+
* 한 workspace 의 실패가 다른 workspace 를 세우지 않고, 기존 lease·cursor·dedupe 경로를
|
|
3647
|
+
* 한 줄도 바꾸지 않고 그대로 쓴다.
|
|
3648
|
+
*
|
|
3649
|
+
* DORMANT 는 건너뛴다. 살아 있는 checkout 이 없는 자리를 대신해 외부에 묻지 않는다.
|
|
3650
|
+
*/
|
|
3651
|
+
async function runRuntimeTickAll(values) {
|
|
3652
|
+
// 기계 수준 lease. workspace lease 와 다른 소유 영역이다 (설계 §5.1) — 저쪽은 회차
|
|
3653
|
+
// 하나를, 이쪽은 이 기계의 runtime 을 직렬화한다. 등록된 서비스와 사람이 친 명령이
|
|
3654
|
+
// 겹치면 늦게 온 쪽이 조용히 물러난다 (C-12 불변식 ⑥).
|
|
3655
|
+
const interval = serviceInterval(values) * 1000;
|
|
3656
|
+
const lease = new RuntimeLease({
|
|
3657
|
+
scope: fileScope(join(ascHome(), 'runtime-lease.json')),
|
|
3658
|
+
owner: `${process.pid}@${hostname()}`,
|
|
3659
|
+
staleMs: staleAfter(interval),
|
|
3660
|
+
});
|
|
3661
|
+
if (!(await lease.acquire())) {
|
|
3662
|
+
const held = await lease.read();
|
|
3663
|
+
const detail = held.kind === 'HELD' ? ` (pid ${held.record.pid})` : '';
|
|
3664
|
+
if (values.json)
|
|
3665
|
+
console.log(JSON.stringify({ ran: [], skipped: [], busy: true }, null, 2));
|
|
3666
|
+
else
|
|
3667
|
+
console.log(`Another machine-wide pass is already running${detail} — leaving it to that one.`);
|
|
3668
|
+
return 0;
|
|
3669
|
+
}
|
|
3670
|
+
try {
|
|
3671
|
+
return await tickAllWorkspaces(values, lease);
|
|
3672
|
+
}
|
|
3673
|
+
finally {
|
|
3674
|
+
await lease.release();
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
async function tickAllWorkspaces(values, lease) {
|
|
3678
|
+
const views = await machineWorkspaces();
|
|
3679
|
+
const due = dueWorkspaces(views);
|
|
3680
|
+
const skipped = views.filter((view) => view.health !== 'ACTIVE');
|
|
3681
|
+
const results = [];
|
|
3682
|
+
for (const workspace of due) {
|
|
3683
|
+
const child = spawnSync(process.execPath, [fileURLToPath(import.meta.url), 'runtime', 'tick'], {
|
|
3684
|
+
cwd: workspace.cwd,
|
|
3685
|
+
stdio: values.json ? 'ignore' : 'inherit',
|
|
3686
|
+
env: process.env,
|
|
3687
|
+
});
|
|
3688
|
+
results.push({ workspaceId: workspace.workspaceId, code: child.status ?? 1 });
|
|
3689
|
+
// 회차가 길어져도 이 기계의 lease 는 살아 있어야 한다 — 갱신하지 않으면 도는 중에
|
|
3690
|
+
// 죽은 것으로 보이고 두 번째 프로세스가 끼어든다.
|
|
3691
|
+
await lease.renew();
|
|
3692
|
+
}
|
|
3693
|
+
const skippedViews = skipped.map((view) => ({ workspaceId: view.workspaceId, health: view.health }));
|
|
3694
|
+
const summary = summarizePass(results, skippedViews);
|
|
3695
|
+
if (values.json) {
|
|
3696
|
+
console.log(JSON.stringify({
|
|
3697
|
+
ran: results,
|
|
3698
|
+
// 건너뛴 것을 조용히 빼지 않는다 — "아무 일도 없었다"와 다른 사실이다
|
|
3699
|
+
skipped: skippedViews,
|
|
3700
|
+
outcome: summary.outcome,
|
|
3701
|
+
failed: summary.failed,
|
|
3702
|
+
}, null, 2));
|
|
3703
|
+
}
|
|
3704
|
+
else {
|
|
3705
|
+
for (const result of results)
|
|
3706
|
+
console.log(`${result.workspaceId}: pass exited ${result.code}`);
|
|
3707
|
+
for (const view of skipped)
|
|
3708
|
+
console.log(`${view.workspaceId}: ${view.health} — not observed this pass`);
|
|
3709
|
+
console.log(summary.line);
|
|
3710
|
+
}
|
|
3711
|
+
// 한 workspace 가 실패해도 다른 workspace 는 전부 돌았다. 그러나 실패했다는 사실은 회차의
|
|
3712
|
+
// 종료 코드에 남는다 — OS 가 보는 것은 그것 하나뿐이다 (P0-R2).
|
|
3713
|
+
return summary.code;
|
|
3148
3714
|
}
|
|
3149
3715
|
/**
|
|
3150
3716
|
* 상시 Runtime (C-12). 대화를 켜 두지 않고 **상태를 지속시키고 계산을 짧게 돌린다.**
|
|
@@ -3153,7 +3719,7 @@ async function buildMonitorEngine(values, store, resolved) {
|
|
|
3153
3719
|
* 재기동해도 cursor·lease·observation이 저장소에 있으므로 같은 사건을 다시 만들지 않는다.
|
|
3154
3720
|
*/
|
|
3155
3721
|
async function runRuntime(command, values, store, renderer, resolved) {
|
|
3156
|
-
if (command !== 'start' && command !== 'tick') {
|
|
3722
|
+
if (command !== 'start' && command !== 'tick' && command !== 'stop') {
|
|
3157
3723
|
console.error(`Unknown runtime command: ${command ?? '(none)'}\n\n${USAGE}`);
|
|
3158
3724
|
return 2;
|
|
3159
3725
|
}
|
|
@@ -3161,10 +3727,17 @@ async function runRuntime(command, values, store, renderer, resolved) {
|
|
|
3161
3727
|
console.error('This only runs inside an attached project. Run `asc init --profile <id>` first.');
|
|
3162
3728
|
return 2;
|
|
3163
3729
|
}
|
|
3164
|
-
|
|
3730
|
+
// 멈추는 것은 감시를 조립하지 않아도 된다 — 외부 자격이 상해 있어도 끌 수 있어야 한다.
|
|
3731
|
+
if (command === 'stop')
|
|
3732
|
+
return stopBackground(store);
|
|
3733
|
+
// 떨어져 나가는 것은 감시를 조립하기 **전에** 한다. 부모가 외부를 한 번 치고 나서
|
|
3734
|
+
// 자식이 또 치면 같은 회차를 두 번 부르는 셈이다.
|
|
3735
|
+
if (command === 'start' && values.detach)
|
|
3736
|
+
return detachRuntime(values);
|
|
3737
|
+
const built = await buildMonitorEngines(values, store, resolved);
|
|
3165
3738
|
if (!built.ok)
|
|
3166
3739
|
return built.code;
|
|
3167
|
-
const {
|
|
3740
|
+
const { channels } = built;
|
|
3168
3741
|
const minutes = (value, fallback) => value === undefined ? fallback * 60_000 : Number(value) * 60_000;
|
|
3169
3742
|
// 주기는 Core 상수가 아니다 (C-12 불변식 ③). 명령줄이 정하고, 기본값은 여기 산다.
|
|
3170
3743
|
const schedule = {
|
|
@@ -3179,33 +3752,46 @@ async function runRuntime(command, values, store, renderer, resolved) {
|
|
|
3179
3752
|
log: (line) => console.log(line),
|
|
3180
3753
|
lastRunAt: {
|
|
3181
3754
|
read: async () => {
|
|
3182
|
-
const raw = await scope.get(
|
|
3755
|
+
const raw = await scope.get(LAST_RUN_KEY);
|
|
3183
3756
|
return raw ? JSON.parse(raw) : {};
|
|
3184
3757
|
},
|
|
3185
3758
|
write: async (kind, at) => {
|
|
3186
|
-
const raw = await scope.get(
|
|
3759
|
+
const raw = await scope.get(LAST_RUN_KEY);
|
|
3187
3760
|
const current = raw ? JSON.parse(raw) : {};
|
|
3188
|
-
await scope.set(
|
|
3761
|
+
await scope.set(LAST_RUN_KEY, JSON.stringify({ ...current, [kind]: at }));
|
|
3189
3762
|
},
|
|
3190
3763
|
},
|
|
3191
3764
|
actions: {
|
|
3765
|
+
// 채널마다 돈다. 한 채널이 터져도 나머지는 계속한다 — 자격 하나가 상했다고
|
|
3766
|
+
// 다른 통로까지 조용해지면 그것이 "변경 없음"으로 읽힌다 (C-12 불변식 ⑫).
|
|
3192
3767
|
delta: async () => {
|
|
3193
|
-
const
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3768
|
+
for (const channel of channels) {
|
|
3769
|
+
const outcome = await channel.engine.scan().catch((error) => {
|
|
3770
|
+
console.error(` [${channel.label}] scan failed — ${String(error)}`);
|
|
3771
|
+
return null;
|
|
3772
|
+
});
|
|
3773
|
+
if (!outcome || outcome.skipped)
|
|
3774
|
+
continue;
|
|
3775
|
+
console.log(` [${channel.label}] detected ${outcome.detected} · logged ${outcome.logged} · packets ${outcome.packets.length}`);
|
|
3776
|
+
}
|
|
3197
3777
|
},
|
|
3198
3778
|
reconcile: async () => {
|
|
3199
|
-
const
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3779
|
+
for (const channel of channels) {
|
|
3780
|
+
const sweep = await channel.engine.reconcile().catch((error) => {
|
|
3781
|
+
console.error(` [${channel.label}] reconcile failed — ${String(error)}`);
|
|
3782
|
+
return null;
|
|
3783
|
+
});
|
|
3784
|
+
if (sweep && !sweep.skipped && !sweep.complete) {
|
|
3785
|
+
console.error(` [${channel.label}] reconcile did not complete${sweep.detail ? ` — ${sweep.detail}` : ''}`);
|
|
3786
|
+
}
|
|
3203
3787
|
}
|
|
3204
3788
|
},
|
|
3205
3789
|
census: async () => {
|
|
3206
|
-
const
|
|
3207
|
-
|
|
3208
|
-
|
|
3790
|
+
for (const channel of channels) {
|
|
3791
|
+
const sweep = await channel.engine.census().catch(() => null);
|
|
3792
|
+
if (sweep && !sweep.skipped && sweep.missing.length > 0) {
|
|
3793
|
+
console.log(` [${channel.label}] ${sweep.missing.length} known items absent from this listing`);
|
|
3794
|
+
}
|
|
3209
3795
|
}
|
|
3210
3796
|
},
|
|
3211
3797
|
digest: async () => {
|
|
@@ -3215,20 +3801,128 @@ async function runRuntime(command, values, store, renderer, resolved) {
|
|
|
3215
3801
|
},
|
|
3216
3802
|
},
|
|
3217
3803
|
});
|
|
3804
|
+
const intervalMs = minutes(values['interval-min'], 1);
|
|
3805
|
+
// 회수 기준은 주기가 정한다 — 주기보다 짧은 만료는 자기 lease를 죽은 것으로 읽는다.
|
|
3806
|
+
const staleMs = staleAfter(intervalMs);
|
|
3807
|
+
const lease = new RuntimeLease({ scope, owner: `${process.pid}@${hostname()}`, staleMs });
|
|
3808
|
+
// cron이 부르는 tick과 떨어져 나간 루프가 겹쳐 돌면 같은 회차를 둘이 한다.
|
|
3809
|
+
// 같은 문을 지나게 해서 하나만 돌린다 (C-12 불변식 ⑥).
|
|
3810
|
+
if (!(await lease.acquire())) {
|
|
3811
|
+
const held = await lease.read();
|
|
3812
|
+
console.log(held.kind === 'HELD'
|
|
3813
|
+
? `Another runtime already holds this workspace (pid ${held.record.pid}) — leaving it to that one.`
|
|
3814
|
+
: 'Another runtime already holds this workspace — leaving it to that one.');
|
|
3815
|
+
return 0;
|
|
3816
|
+
}
|
|
3218
3817
|
if (command === 'tick') {
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3818
|
+
try {
|
|
3819
|
+
const outcome = await orchestrator.tick();
|
|
3820
|
+
console.log(renderTick(outcome));
|
|
3821
|
+
return outcome.failures.length > 0 ? 1 : 0;
|
|
3822
|
+
}
|
|
3823
|
+
finally {
|
|
3824
|
+
await lease.release();
|
|
3825
|
+
}
|
|
3222
3826
|
}
|
|
3223
|
-
const intervalMs = minutes(values['interval-min'], 1);
|
|
3224
3827
|
console.log(`Always-on runtime started — one pass every ${intervalMs / 60_000} min. Ctrl+C to stop.`);
|
|
3828
|
+
// 자는 동안에도 멈출 수 있어야 한다. 깨우지 않으면 `runtime stop` 이 한 주기를 통째로
|
|
3829
|
+
// 기다리고, 주기가 30분이면 30분 동안 "멈추는 중"이 된다.
|
|
3830
|
+
let wake = null;
|
|
3225
3831
|
const stop = () => {
|
|
3226
3832
|
console.log('Stopping — a pass still running is carried to the next start by its lease.');
|
|
3227
3833
|
orchestrator.stop();
|
|
3834
|
+
wake?.();
|
|
3228
3835
|
};
|
|
3229
3836
|
process.once('SIGINT', stop);
|
|
3230
3837
|
process.once('SIGTERM', stop);
|
|
3231
|
-
|
|
3838
|
+
try {
|
|
3839
|
+
await orchestrator.run(intervalMs, async (ms) => {
|
|
3840
|
+
// 회차마다 살아 있다고 말한다. 밀려났으면 조용히 물러난다 — 두 루프가 같은
|
|
3841
|
+
// workspace를 갈아 대는 것보다 하나가 도는 편이 낫다.
|
|
3842
|
+
if (!(await lease.renew())) {
|
|
3843
|
+
console.log('Another runtime took this workspace — stopping.');
|
|
3844
|
+
orchestrator.stop();
|
|
3845
|
+
return;
|
|
3846
|
+
}
|
|
3847
|
+
await new Promise((resolve) => {
|
|
3848
|
+
// **unref 하지 않는다.** 예전에는 여기서 타이머를 unref 했는데, 그러면 이벤트
|
|
3849
|
+
// 루프를 붙잡는 것이 아무것도 없어 첫 잠에서 프로세스가 끝난다 — Node 는
|
|
3850
|
+
// `unsettled top-level await` 로 exit 13 을 낸다. 상시로 돌아야 하는 루프가
|
|
3851
|
+
// 조용히 한 회차만 돌고 죽던 원인이다. 멈출 때는 아래 wake 가 타이머를 지운다.
|
|
3852
|
+
const timer = setTimeout(resolve, ms);
|
|
3853
|
+
wake = () => {
|
|
3854
|
+
clearTimeout(timer);
|
|
3855
|
+
resolve();
|
|
3856
|
+
};
|
|
3857
|
+
});
|
|
3858
|
+
wake = null;
|
|
3859
|
+
});
|
|
3860
|
+
}
|
|
3861
|
+
finally {
|
|
3862
|
+
await lease.release();
|
|
3863
|
+
}
|
|
3864
|
+
return 0;
|
|
3865
|
+
}
|
|
3866
|
+
/**
|
|
3867
|
+
* 이 터미널이 닫혀도 계속 보게 한다 (C-12 §0).
|
|
3868
|
+
*
|
|
3869
|
+
* scheduler 제품을 설치하지 않는다 (불변식 ④) — 같은 명령을 떨어져 나간 프로세스로 다시
|
|
3870
|
+
* 띄울 뿐이다. cron·launchd·Task Scheduler로 `asc runtime tick` 을 부르는 길도 그대로
|
|
3871
|
+
* 열려 있고, 둘은 같은 lease를 지나므로 겹쳐 돌지 않는다.
|
|
3872
|
+
*/
|
|
3873
|
+
async function detachRuntime(values) {
|
|
3874
|
+
// 지금 프로세스가 받은 인자를 그대로 쓰되 --detach 만 뺀다. 옵션을 다시 조립하면
|
|
3875
|
+
// 언젠가 부모와 자식이 다른 주기로 돌게 된다.
|
|
3876
|
+
const args = process.argv.slice(2).filter((arg) => arg !== '--detach');
|
|
3877
|
+
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), ...args], {
|
|
3878
|
+
detached: true,
|
|
3879
|
+
stdio: 'ignore',
|
|
3880
|
+
cwd: process.cwd(),
|
|
3881
|
+
env: process.env,
|
|
3882
|
+
});
|
|
3883
|
+
child.unref();
|
|
3884
|
+
if (child.pid === undefined) {
|
|
3885
|
+
console.error('Could not start a background runtime.');
|
|
3886
|
+
return 1;
|
|
3887
|
+
}
|
|
3888
|
+
console.log(`Background runtime started (pid ${child.pid}). \`asc runtime status\` says whether it is observing.`);
|
|
3889
|
+
console.log('`asc runtime stop` ends it.');
|
|
3890
|
+
void values;
|
|
3891
|
+
return 0;
|
|
3892
|
+
}
|
|
3893
|
+
/**
|
|
3894
|
+
* 도는 runtime에게 끝내라고 한다.
|
|
3895
|
+
*
|
|
3896
|
+
* **lease를 지우는 것으로 멈추지 않는다** — 그러면 프로세스는 살아 있는데 아무도 그것을
|
|
3897
|
+
* 모르는 상태가 된다. 신호를 보내고, 그 프로세스가 자기 lease를 놓게 한다.
|
|
3898
|
+
*/
|
|
3899
|
+
async function stopBackground(store) {
|
|
3900
|
+
const scope = store.scope('runtime');
|
|
3901
|
+
const state = await new RuntimeLease({ scope, owner: '(reader)' }).read();
|
|
3902
|
+
if (state.kind === 'FREE') {
|
|
3903
|
+
console.log('No background runtime is registered here.');
|
|
3904
|
+
return 0;
|
|
3905
|
+
}
|
|
3906
|
+
if (state.kind === 'STALE') {
|
|
3907
|
+
// 죽은 프로세스가 남긴 기록이다. 신호를 보낼 곳이 없으므로 기록만 치운다.
|
|
3908
|
+
await scope.delete('runtime-lease');
|
|
3909
|
+
console.log(`Cleared a stale lease from pid ${state.record.pid} — nothing was running.`);
|
|
3910
|
+
return 0;
|
|
3911
|
+
}
|
|
3912
|
+
try {
|
|
3913
|
+
process.kill(state.record.pid, 'SIGTERM');
|
|
3914
|
+
}
|
|
3915
|
+
catch (error) {
|
|
3916
|
+
// 이미 죽은 프로세스다. 기록만 남았으므로 치운다 — 못 지우면 그 사실을 말한다.
|
|
3917
|
+
if (error.code !== 'ESRCH') {
|
|
3918
|
+
console.error(`Could not signal pid ${state.record.pid}: ${String(error)}`);
|
|
3919
|
+
return 1;
|
|
3920
|
+
}
|
|
3921
|
+
await scope.delete('runtime-lease');
|
|
3922
|
+
console.log(`pid ${state.record.pid} was already gone — cleared its lease.`);
|
|
3923
|
+
return 0;
|
|
3924
|
+
}
|
|
3925
|
+
console.log(`Asked pid ${state.record.pid} to finish its pass and stop.`);
|
|
3232
3926
|
return 0;
|
|
3233
3927
|
}
|
|
3234
3928
|
/**
|
|
@@ -3465,6 +4159,225 @@ async function runFreeze(group, command, values, store) {
|
|
|
3465
4159
|
${USAGE}`);
|
|
3466
4160
|
return 2;
|
|
3467
4161
|
}
|
|
4162
|
+
/** 판정 결과에서 workspace 신원만 꺼낸다. 갈래마다 모양이 달라 한 곳에서 접는다. */
|
|
4163
|
+
function workspaceOf(resolution) {
|
|
4164
|
+
return resolution.kind === 'REGISTERED' || resolution.kind === 'LINKED_WORKTREE'
|
|
4165
|
+
? { workspaceId: resolution.workspaceId, locator: resolution.locator }
|
|
4166
|
+
: null;
|
|
4167
|
+
}
|
|
4168
|
+
/**
|
|
4169
|
+
* Host 세션이 열렸다 (C-12 §4). **이 명령은 실패하지 않는다.**
|
|
4170
|
+
*
|
|
4171
|
+
* Host lifecycle이 부르는 자리라 세 가지를 지킨다:
|
|
4172
|
+
*
|
|
4173
|
+
* 붙지 않은 자리 아무 말도 하지 않고 exit 0 — 남의 프로젝트를 방해하지 않는다
|
|
4174
|
+
* 상태를 못 읽는 자리 왜인지 말한다 — 조용한 빈 화면을 주지 않는다 (불변식 ⑰)
|
|
4175
|
+
* 미등록 linked worktree 공용 resolver가 풀고 등록까지 한다 (C-11 §1.3)
|
|
4176
|
+
*
|
|
4177
|
+
* **workspace 신원을 여기서 다시 구현하지 않는다.** `resolveRoot` 하나만 부른다.
|
|
4178
|
+
*/
|
|
4179
|
+
async function runFrontOpen(values) {
|
|
4180
|
+
const emit = async (opening) => {
|
|
4181
|
+
const lines = frontOpeningLines(opening);
|
|
4182
|
+
if (values.json) {
|
|
4183
|
+
// payload는 Claude Code가 SessionStart에서 읽는 봉투다. 다른 host는 lines를 쓴다.
|
|
4184
|
+
console.log(JSON.stringify({ kind: opening.kind, lines, payload: sessionStartPayload(lines) }, null, 2));
|
|
4185
|
+
return 0;
|
|
4186
|
+
}
|
|
4187
|
+
for (const line of lines)
|
|
4188
|
+
console.log(line);
|
|
4189
|
+
return 0;
|
|
4190
|
+
};
|
|
4191
|
+
const resolution = await resolveRoot(process.cwd(), values.root).catch(() => null);
|
|
4192
|
+
const workspace = resolution ? workspaceOf(resolution) : null;
|
|
4193
|
+
// EXPLICIT·PROJECT_LOCAL 은 workspace id가 없지만 붙은 자리다 — 신원 없이도 상태는 읽는다.
|
|
4194
|
+
const root = resolution && resolution.kind !== 'UNRESOLVED' ? resolution.root : null;
|
|
4195
|
+
if (!root)
|
|
4196
|
+
return emit({ kind: 'NOT_ASC' });
|
|
4197
|
+
const store = new MarkdownStateStore(root);
|
|
4198
|
+
return emit(await openFront({
|
|
4199
|
+
// 신원을 모르는 자리(project-local)라도 붙은 것은 붙은 것이다
|
|
4200
|
+
workspace: workspace ?? { workspaceId: '(project-local)', locator: root },
|
|
4201
|
+
restore: async () => {
|
|
4202
|
+
const scope = await activeMonitorScope(store);
|
|
4203
|
+
return restoreFront({
|
|
4204
|
+
store,
|
|
4205
|
+
pending: await new LocalOperator({ store }).list({}),
|
|
4206
|
+
escalations: await escalationLedger(store).pending(),
|
|
4207
|
+
health: evaluateHealth(await new CoverageLedger(store.scope(scope)).health(), new Date().toISOString(), HEALTH_THRESHOLDS),
|
|
4208
|
+
...(workspace ? { workspace } : {}),
|
|
4209
|
+
// 안에서 할 일이 없는 것과 밖에서 답이 안 온 것은 다른 사실이다
|
|
4210
|
+
coordination: await coordinationNow(store),
|
|
4211
|
+
bindings: claudeBindings(store),
|
|
4212
|
+
});
|
|
4213
|
+
},
|
|
4214
|
+
}));
|
|
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
|
+
}
|
|
3468
4381
|
async function runFront(command, values, store, root) {
|
|
3469
4382
|
if (command !== undefined && command !== 'status') {
|
|
3470
4383
|
console.error(`Unknown front command: ${command}
|
|
@@ -3473,14 +4386,16 @@ ${USAGE}`);
|
|
|
3473
4386
|
return 2;
|
|
3474
4387
|
}
|
|
3475
4388
|
const scope = await activeMonitorScope(store);
|
|
3476
|
-
|
|
3477
|
-
|
|
4389
|
+
// **index를 직접 뒤지지 않는다.** 모든 root 판정이 지나는 같은 문을 쓴다 — 그래야
|
|
4390
|
+
// 미등록 linked worktree도 같은 workspace로 풀리고, 그 자리가 등록까지 된다 (C-11 §1.3).
|
|
4391
|
+
const located = workspaceOf(await resolveRoot(process.cwd(), values.root));
|
|
3478
4392
|
const state = await restoreFront({
|
|
3479
4393
|
store,
|
|
3480
4394
|
pending: await new LocalOperator({ store }).list({}),
|
|
3481
4395
|
escalations: await escalationLedger(store).pending(),
|
|
3482
4396
|
health: evaluateHealth(await new CoverageLedger(store.scope(scope)).health(), new Date().toISOString(), HEALTH_THRESHOLDS),
|
|
3483
4397
|
...(located ? { workspace: { workspaceId: located.workspaceId, locator: located.locator } } : {}),
|
|
4398
|
+
coordination: await coordinationNow(store),
|
|
3484
4399
|
// 도는 세션을 누가 집고 있는지. --physical 을 다시 물어보게 하지 않는다 (L-4).
|
|
3485
4400
|
bindings: claudeBindings(store),
|
|
3486
4401
|
});
|
|
@@ -3508,51 +4423,71 @@ async function runMonitor(command, values, store, renderer, resolved) {
|
|
|
3508
4423
|
console.error(`Monitoring is not running — ${readJudgment.detail}`);
|
|
3509
4424
|
return 2;
|
|
3510
4425
|
}
|
|
3511
|
-
const built = await
|
|
4426
|
+
const built = await buildMonitorEngines(values, store, resolved);
|
|
3512
4427
|
if (!built.ok)
|
|
3513
4428
|
return built.code;
|
|
3514
|
-
const {
|
|
4429
|
+
const { channels, repo } = built;
|
|
4430
|
+
// 채널마다 자기 회차를 돈다. **한 채널의 실패가 다른 채널을 세우지 않는다** — 코드 쪽이
|
|
4431
|
+
// 자격을 잃었다고 작업 항목 쪽까지 못 보게 되면, 그것이 곧 "변화 없음"으로 읽힌다.
|
|
4432
|
+
let worst = 0;
|
|
3515
4433
|
// 어디까지 확인했는지 보여주기만 한다. 판정하지도, 무엇을 고치지도 않는다.
|
|
3516
4434
|
if (command === 'status') {
|
|
3517
|
-
const health = await engine.health();
|
|
3518
|
-
console.log(values.json ? JSON.stringify(health, null, 2) : renderHealth(repo, health).join('\n'));
|
|
3519
|
-
return 0;
|
|
3520
|
-
}
|
|
3521
|
-
if (command === 'reconcile' || command === 'census') {
|
|
3522
|
-
const sweep = command === 'reconcile' ? await engine.reconcile() : await engine.census();
|
|
3523
|
-
if (sweep.skipped) {
|
|
3524
|
-
console.log('Another run is in progress. The next pass will pick it up.');
|
|
3525
|
-
return 0;
|
|
3526
|
-
}
|
|
3527
4435
|
if (values.json) {
|
|
3528
|
-
|
|
3529
|
-
|
|
4436
|
+
const perChannel = [];
|
|
4437
|
+
for (const channel of channels) {
|
|
4438
|
+
perChannel.push({ channel: channel.label, source: channel.sourceId, health: await channel.engine.health() });
|
|
4439
|
+
}
|
|
4440
|
+
console.log(JSON.stringify({ repo, channels: perChannel }, null, 2));
|
|
4441
|
+
return 0;
|
|
3530
4442
|
}
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
console.log(
|
|
3534
|
-
console.log(' (whether that is deletion, permissions, visibility or a query error is not judged here — a person looks)');
|
|
3535
|
-
}
|
|
3536
|
-
if (!sweep.complete) {
|
|
3537
|
-
// 완주하지 못한 회차를 성공으로 보이면 "확인했다"는 거짓말이 된다
|
|
3538
|
-
console.error(`The listing did not complete${sweep.detail ? ` — ${sweep.detail}` : ''}. This pass makes no judgement about disappearances.`);
|
|
3539
|
-
return 1;
|
|
4443
|
+
for (const channel of channels) {
|
|
4444
|
+
console.log(`[${channel.label}]`);
|
|
4445
|
+
console.log(renderHealth(repo, await channel.engine.health()).join('\n'));
|
|
3540
4446
|
}
|
|
3541
4447
|
return 0;
|
|
3542
4448
|
}
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
4449
|
+
if (command === 'reconcile' || command === 'census') {
|
|
4450
|
+
for (const channel of channels) {
|
|
4451
|
+
const sweep = command === 'reconcile' ? await channel.engine.reconcile() : await channel.engine.census();
|
|
4452
|
+
if (sweep.skipped) {
|
|
4453
|
+
console.log(`[${channel.label}] another run is in progress. The next pass will pick it up.`);
|
|
4454
|
+
continue;
|
|
4455
|
+
}
|
|
4456
|
+
if (values.json) {
|
|
4457
|
+
console.log(JSON.stringify({ channel: channel.label, ...sweep }, null, 2));
|
|
4458
|
+
}
|
|
4459
|
+
else {
|
|
4460
|
+
console.log(`[${channel.label}] ${sweep.kind}: ${sweep.seen} listed · ${sweep.changed} changed · ${sweep.packets.length} packets`);
|
|
4461
|
+
if (sweep.missing.length > 0) {
|
|
4462
|
+
console.log(` ${sweep.missing.length} known items absent from this listing: ${sweep.missing.join(', ')}`);
|
|
4463
|
+
console.log(' (whether that is deletion, permissions, visibility or a query error is not judged here — a person looks)');
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
if (!sweep.complete) {
|
|
4467
|
+
// 완주하지 못한 회차를 성공으로 보이면 "확인했다"는 거짓말이 된다
|
|
4468
|
+
console.error(`[${channel.label}] the listing did not complete${sweep.detail ? ` — ${sweep.detail}` : ''}. This pass makes no judgement about disappearances.`);
|
|
4469
|
+
worst = 1;
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
return worst;
|
|
3547
4473
|
}
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
4474
|
+
let packets = 0;
|
|
4475
|
+
for (const channel of channels) {
|
|
4476
|
+
const outcome = await channel.engine.scan();
|
|
4477
|
+
if (outcome.skipped) {
|
|
4478
|
+
console.log(`[${channel.label}] another scan is in progress. The next pass will pick it up.`);
|
|
4479
|
+
continue;
|
|
4480
|
+
}
|
|
4481
|
+
console.log(`[${channel.label}] 감지 ${outcome.detected} · 중복 ${outcome.duplicates} · 기록 ${outcome.logged} · ` +
|
|
4482
|
+
`보고서 ${outcome.packets.length} · 재시도 ${outcome.retries.length}`);
|
|
4483
|
+
packets += outcome.packets.length;
|
|
4484
|
+
}
|
|
4485
|
+
if (packets > 0) {
|
|
3551
4486
|
const operator = new LocalOperator({ store });
|
|
3552
4487
|
console.log();
|
|
3553
4488
|
console.log(renderer.renderList(await operator.list()).text);
|
|
3554
4489
|
}
|
|
3555
|
-
return
|
|
4490
|
+
return worst;
|
|
3556
4491
|
}
|
|
3557
4492
|
/**
|
|
3558
4493
|
* 외부로 내보내는 경로. 승인과 분리돼 있는 이유는 OM §11.8 — 승인은 내용에 동의한
|