@asc-agent/runtime 0.7.0 → 0.8.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/guard.js +308 -215
- package/dist/adapters/claude-code/install.d.ts +18 -0
- package/dist/adapters/claude-code/install.js +66 -0
- package/dist/adapters/claude-code/skill.js +114 -18
- package/dist/adapters/gitlab/client.d.ts +10 -0
- package/dist/adapters/gitlab/client.js +17 -1
- package/dist/adapters/gitlab/scm.d.ts +31 -0
- package/dist/adapters/gitlab/scm.js +341 -6
- package/dist/cli/asc.js +902 -74
- package/dist/core/attach/bootstrap.js +4 -4
- package/dist/core/attach/setup.js +8 -8
- package/dist/core/distribution/release.d.ts +3 -3
- package/dist/core/distribution/release.js +1 -1
- package/dist/core/distribution/runtime-install.js +1 -1
- package/dist/core/execution/executor.d.ts +44 -0
- package/dist/core/execution/executor.js +108 -14
- package/dist/core/execution/grant.d.ts +8 -0
- package/dist/core/execution/grant.js +2 -0
- package/dist/core/execution/remote-review.d.ts +117 -0
- package/dist/core/execution/remote-review.js +176 -0
- package/dist/core/model/entities.d.ts +69 -0
- package/dist/core/model/entities.js +46 -0
- package/dist/core/model/transitions.d.ts +1 -1
- package/dist/core/policy/execution-mode.d.ts +136 -0
- package/dist/core/policy/execution-mode.js +127 -0
- package/dist/core/policy/remote-freeze.d.ts +2 -2
- package/dist/ports/scm.d.ts +28 -0
- package/dist/schemas/profile.d.ts +6 -6
- package/package.json +1 -1
package/dist/cli/asc.js
CHANGED
|
@@ -44,7 +44,7 @@ import { CLAUDE_PROVIDER, CLAUDE_SCOPE, claudeBindings } from "../adapters/claud
|
|
|
44
44
|
import { readHeartbeat } from "../adapters/claude-code/observer.js";
|
|
45
45
|
import { workerContract, workerSettings } from "../adapters/claude-code/guard.js";
|
|
46
46
|
import { applyHostReport, assessReadiness, probe } from "../adapters/claude-code/probe.js";
|
|
47
|
-
import { defaultPaths, install, installReportLines, uninstall, verifyInstall, verifyInstalled, } from "../adapters/claude-code/install.js";
|
|
47
|
+
import { controlPlaneAccess, defaultPaths, install, installReportLines, uninstall, verifyInstall, verifyInstalled, } from "../adapters/claude-code/install.js";
|
|
48
48
|
import { MonitorEngine } from "../core/monitor/engine.js";
|
|
49
49
|
import { CoverageLedger, renderHealth } from "../core/monitor/coverage.js";
|
|
50
50
|
import { evaluateHealth, healthAlertLines } from "../core/monitor/health-alerts.js";
|
|
@@ -94,6 +94,8 @@ import { EscalationLedger, escalationLines } from "../core/runtime/escalation.js
|
|
|
94
94
|
import { deriveExecutionState, executionLine } from "../core/runtime/execution-state.js";
|
|
95
95
|
import { buildFinalReport, renderFinalReport } from "../core/runtime/report.js";
|
|
96
96
|
import { FreezeLedger, freezeLines, judgeAction } from "../core/policy/remote-freeze.js";
|
|
97
|
+
import { ExecutionMode, enforcementOf, judgeAutoReadiness, modeLine, readExecutionMode, writeExecutionMode, } from "../core/policy/execution-mode.js";
|
|
98
|
+
import { reviewExternalAction, reviewLines } from "../core/execution/remote-review.js";
|
|
97
99
|
import { SessionRuntime } from "../core/runtime/session.js";
|
|
98
100
|
import { Checkpoint, Handoff, SessionRole } from "../core/model/entities.js";
|
|
99
101
|
import { archiveLock, bootstrapGuard, buildLock, compareLock, loadLayers, resolveRuntime } from "../core/resolver/load.js";
|
|
@@ -104,20 +106,57 @@ import { LocalOperator } from "../core/operator/local-operator.js";
|
|
|
104
106
|
import { loadIdentityMap } from "./identity-config.js";
|
|
105
107
|
const USAGE = `asc — Agent Session Control
|
|
106
108
|
|
|
109
|
+
Lifecycle
|
|
110
|
+
asc setup make this machine and this project ready to use
|
|
111
|
+
asc status what is set up, what is running, what is blocked
|
|
112
|
+
asc update install the newest release and verify it
|
|
113
|
+
asc refresh re-converge this runtime's own integration
|
|
114
|
+
asc uninstall remove the product; your state stays
|
|
115
|
+
|
|
116
|
+
Execution
|
|
117
|
+
asc mode who executes: MANUAL (you) or AUTO (ASC)
|
|
118
|
+
asc mode auto turn on managed execution — refuses unless the path is usable
|
|
119
|
+
asc mode manual step back to advisory. Recorded, with who said so
|
|
120
|
+
|
|
121
|
+
Work
|
|
122
|
+
asc work start [WORK] start or resume the work, inside a contract
|
|
123
|
+
asc work status [S-ID] where it is right now
|
|
124
|
+
asc work publish send the approved result outside
|
|
125
|
+
asc work publish --review read the target, the SHA and the binding — change nothing
|
|
126
|
+
asc work finish [S-ID] hand off, close, collect — one command
|
|
127
|
+
asc work pause|resume|inspect [S-ID]
|
|
128
|
+
|
|
129
|
+
Human decisions
|
|
130
|
+
asc inbox what is waiting for a person
|
|
131
|
+
asc inbox show <REQUEST_ID>
|
|
132
|
+
asc inbox decide <REQUEST_ID> <approve|revise|defer|dismiss|queue> --as <actor>
|
|
133
|
+
|
|
134
|
+
Runtime
|
|
135
|
+
asc runtime status which build is in use, and whether it observes
|
|
136
|
+
asc runtime use package | development <checkout>
|
|
137
|
+
|
|
138
|
+
Options
|
|
139
|
+
--json machine-readable output. stdout is a single JSON document
|
|
140
|
+
--as <actor> who is deciding. Must be mapped as an approver
|
|
141
|
+
--root <path> runtime directory (otherwise: registered workspace, then repo-local .asc)
|
|
142
|
+
|
|
143
|
+
asc help --advanced the internal primitives these commands are built on
|
|
144
|
+
`;
|
|
145
|
+
const ADVANCED_USAGE = `asc — advanced surface
|
|
146
|
+
|
|
147
|
+
These are the primitives the public commands are built on. A healthy path does not
|
|
148
|
+
require them: \`setup\` · \`status\` · \`work\` · \`inbox\` · \`mode\` cover normal use.
|
|
149
|
+
They stay because recovery, diagnosis and scripting need them.
|
|
150
|
+
|
|
107
151
|
asc proceed [--session <id>] [--work <WORK-ID>] [--goal <text>] [--json]
|
|
108
152
|
|
|
109
|
-
asc inbox list [--all] [--priority P0|P1|P2] [--json]
|
|
110
|
-
asc inbox show <REQUEST_ID> [--json]
|
|
111
153
|
asc inbox trace <REQUEST_ID> [--json] # how it got here — an exploratory trace
|
|
112
154
|
asc inbox digest [--flush] [--json] # batched view (P0 stays separate)
|
|
113
155
|
asc inbox latest [--priority P0|P1|P2] [--json]
|
|
114
|
-
asc inbox decide <REQUEST_ID> <approve|revise|defer|dismiss|queue> --as <actor>
|
|
115
|
-
[--revision <text>] [--expect <version>]
|
|
116
156
|
|
|
117
157
|
asc grant issue <REQUEST_ID> --action <key> --target <ref> --as <actor>
|
|
118
158
|
[--grant-id <id>] [--expires <iso>]
|
|
119
159
|
asc grant issue --session <S-ID> --action <key> --target <ref> --body-file <path> --as <actor>
|
|
120
|
-
# what a session produced, sent out because a person said so
|
|
121
160
|
asc grant run <GRANT_ID> [--run-id <id>]
|
|
122
161
|
|
|
123
162
|
asc monitor scan [--backfill] [--as <controller>] # fast path
|
|
@@ -137,13 +176,11 @@ const USAGE = `asc — Agent Session Control
|
|
|
137
176
|
asc runtime service [status] [--json] # the machine's persistent registration
|
|
138
177
|
asc runtime service install|uninstall [--interval-min <n>]
|
|
139
178
|
asc runtime stop # ask the background runtime to finish its pass
|
|
140
|
-
asc runtime status [--json] # which build is in use, and whether it observes
|
|
141
|
-
asc runtime use package
|
|
142
|
-
asc runtime use development <checkout> # run a built checkout instead
|
|
143
179
|
|
|
144
|
-
asc update
|
|
145
|
-
asc
|
|
146
|
-
asc
|
|
180
|
+
asc update check|plan [--json] # what is installed, what is published
|
|
181
|
+
asc refresh check|plan [--json] # what integration is behind, changing nothing
|
|
182
|
+
asc uninstall plan [--json] # what would be removed, and what stays
|
|
183
|
+
|
|
147
184
|
asc front [status] [--json]
|
|
148
185
|
asc front open [--json] # a host session opened here — what is waiting
|
|
149
186
|
asc escalate open <S-ID> --predicate <p>... --question <t> --blocked <node>...
|
|
@@ -157,39 +194,31 @@ const USAGE = `asc — Agent Session Control
|
|
|
157
194
|
asc thaw
|
|
158
195
|
asc workspace list
|
|
159
196
|
asc workspace migrate [--force]
|
|
160
|
-
asc init [--profile <id>] [--preset <id>] [--install <path>]
|
|
161
|
-
[--scope local|project] [--workspace <W-id>]
|
|
162
|
-
# without --profile: report what was detected, then stop
|
|
163
197
|
|
|
164
198
|
asc setup status [--json]
|
|
165
199
|
asc setup identity [--role controller|monitor|both] [--actor <channel:actor>]
|
|
166
|
-
# 지금 이 사람을 승인 권한자로 세우고 재고정까지 한다
|
|
167
200
|
asc setup plan [--profile <id>] [--scope local|project] [--json]
|
|
168
|
-
# says what it would change — changes nothing
|
|
169
201
|
asc setup apply [--profile <id>] [--scope local|project] [--json]
|
|
170
|
-
asc setup apply --json # non-interactive apply. stdout is a single JSON document
|
|
171
202
|
|
|
172
203
|
asc session plan [--id <S-ID>] [--role <role>] [--goal <text>] [--boundary <glob>...]
|
|
173
204
|
[--criteria <text>...] [--owner <role>] [--provenance <f>=<STATUS>[:<src>]...]
|
|
174
205
|
[--json] # is this draft issuable? changes nothing
|
|
175
206
|
asc session issue <ID> --role <role> --goal <text> [--block <id>]
|
|
176
207
|
[--parent <S-ID>] [--issued-by <principal>]
|
|
177
|
-
asc session pause <S-ID> --position <t> --next <t> [--physical <id>]
|
|
178
|
-
[--judgment <t>] [--blocker <t>] [--risk <t>] [--evidence <ref>]
|
|
179
208
|
asc session validate <target S-ID> --validator <validator S-ID> --result PASS|FAIL [--finding <t>]
|
|
180
209
|
asc session audit <S-ID>
|
|
181
210
|
asc session report <S-ID> [--json]
|
|
182
211
|
asc session decision <S-ID> --class <c> --selected <t> --why <t>... --evidence <ref>...
|
|
183
212
|
[--alternative <t>...] [--ownership <scope>...] [--verification <t>...]
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
213
|
+
[--boundary <glob>...] [--exception <item>...]
|
|
214
|
+
[--criteria <text>...] [--owner <role>]
|
|
215
|
+
[--domain <decision-domain>...] [--authority <domain>=<role>...]
|
|
216
|
+
[--dependency <text>...]
|
|
188
217
|
asc session start <ID>
|
|
189
218
|
asc session pause <ID> --position <text> --next <text> [--done <task>...]
|
|
190
219
|
asc session resume <ID>
|
|
191
220
|
asc session done <ID> --verified <text> --next <text> [--done <task>...]
|
|
192
|
-
|
|
221
|
+
[--changed <path>...] [--unresolved <text>...] [--physical <id>]
|
|
193
222
|
asc session list
|
|
194
223
|
|
|
195
224
|
asc controller collect
|
|
@@ -210,7 +239,6 @@ const USAGE = `asc — Agent Session Control
|
|
|
210
239
|
asc coordination [status] [--json] # what was asked outside, and whether it reached anyone
|
|
211
240
|
asc coordination publish --grant <G-ID> --query <ID> --title <text> --body-file <path>
|
|
212
241
|
[--audience <who>] [--known <objectId>] [--work <ref>] [--json]
|
|
213
|
-
# publishing is an outward write — it goes through an approved grant
|
|
214
242
|
asc coordination observe [--json] # did anything come back on what we published
|
|
215
243
|
|
|
216
244
|
asc progress show [<S-ID>]
|
|
@@ -227,7 +255,6 @@ const USAGE = `asc — Agent Session Control
|
|
|
227
255
|
asc host claude contract <S-ID>
|
|
228
256
|
|
|
229
257
|
Options
|
|
230
|
-
--root <path> runtime directory (otherwise: registered workspace, then repo-local .asc)
|
|
231
258
|
--json machine-readable output
|
|
232
259
|
--as <actor> who is deciding. Must be mapped as an approver
|
|
233
260
|
--revision what was changed, when approving with revisions
|
|
@@ -241,7 +268,7 @@ Options
|
|
|
241
268
|
--write actually write the artefacts (default: preview)
|
|
242
269
|
--role planner|researcher|implementer|verifier
|
|
243
270
|
--goal the single goal of this session
|
|
244
|
-
--work work item to investigate before proposing a contract
|
|
271
|
+
--work work item to investigate before proposing a contract
|
|
245
272
|
--actor who you are, as <channel>:<actor> (asc setup identity)
|
|
246
273
|
--boundary write scope (must be narrower than the Profile's)
|
|
247
274
|
--exception SOFT DENY item allowed for this session only
|
|
@@ -253,10 +280,7 @@ Options
|
|
|
253
280
|
--decision does a person need to decide: none|later|now
|
|
254
281
|
--verifier independent verification state: none|running|pass|fail
|
|
255
282
|
--terminal final report — stays as the closing screen after collect
|
|
256
|
-
|
|
257
|
-
decide assumes a person is operating it, and only checks that the name given with --as
|
|
258
|
-
is registered as an approver. Approval is not permission to publish: anything reaching an
|
|
259
|
-
external system goes out through a separate Execution Grant.`;
|
|
283
|
+
`;
|
|
260
284
|
/**
|
|
261
285
|
* 지금 여기가 어느 ASC runtime인가. **모든 명령이 같은 문을 지난다** (C-11 §3, B-45).
|
|
262
286
|
*
|
|
@@ -432,7 +456,7 @@ const ascHome = () => process.env.ASC_HOME ?? join(homedir(), '.asc');
|
|
|
432
456
|
const DECISION_ERROR = {
|
|
433
457
|
NOT_FOUND: '요청을 찾지 못했다.',
|
|
434
458
|
FORBIDDEN_ACTOR: '승인 권한자가 아니다. .asc/identities.json 에 `"이름": ["local:계정"]` 형태로 매핑을 추가하라 ' +
|
|
435
|
-
'(현재 상태는 `asc
|
|
459
|
+
'(현재 상태는 `asc status`).',
|
|
436
460
|
NOT_ALLOWED_DECISION: '이 요청이 허용하지 않는 결정이다.',
|
|
437
461
|
EXPIRED: '만료된 요청이다.',
|
|
438
462
|
ALREADY_DECIDED: '이미 결정된 요청이다.',
|
|
@@ -466,7 +490,7 @@ function explainConfigError(error) {
|
|
|
466
490
|
const path = failure?.path ? ` (${failure.path})` : '';
|
|
467
491
|
switch (failure?.code) {
|
|
468
492
|
case 'ENOENT':
|
|
469
|
-
return `That profile is not there${path}. \`asc
|
|
493
|
+
return `That profile is not there${path}. \`asc status\` lists what is.`;
|
|
470
494
|
case 'EISDIR':
|
|
471
495
|
return `A profile has to be a file, and that is a directory${path}.`;
|
|
472
496
|
case 'EACCES':
|
|
@@ -672,16 +696,24 @@ function parseArgsOrThrow(argv) {
|
|
|
672
696
|
expires: { type: 'string' },
|
|
673
697
|
revision: { type: 'string' },
|
|
674
698
|
expect: { type: 'string' },
|
|
699
|
+
advanced: { type: 'boolean', default: false },
|
|
700
|
+
review: { type: 'boolean', default: false },
|
|
675
701
|
help: { type: 'boolean', short: 'h', default: false },
|
|
676
702
|
},
|
|
677
703
|
});
|
|
678
704
|
}
|
|
679
705
|
async function runParsedCommand(values, positionals, entry, argv) {
|
|
680
706
|
const [group, command, target, extra] = positionals;
|
|
681
|
-
|
|
682
|
-
|
|
707
|
+
// 기본 화면은 정상 표면만 보여준다. 내부 primitive 는 물어본 사람에게만 (§54).
|
|
708
|
+
if (values.help || group === undefined || group === 'help') {
|
|
709
|
+
console.log(values.advanced || command === 'advanced' ? ADVANCED_USAGE : USAGE);
|
|
683
710
|
return 0;
|
|
684
711
|
}
|
|
712
|
+
// 옛 이름은 그대로 돌되 새 이름을 말한다 (§58·§59). **명령이 실제로 돌기 전에** 말한다 —
|
|
713
|
+
// 뒤에서 말하면 그 명령이 다른 이유로 멈췄을 때 사람은 이름이 바뀐 것을 끝내 모른다.
|
|
714
|
+
const renamed = RENAMED[group === 'progress' && command === 'show' ? 'progress show' : group];
|
|
715
|
+
if (renamed)
|
|
716
|
+
console.error(`Deprecated. Use \`${renamed}\`.`);
|
|
685
717
|
// **지원 하한을 먼저 답한다** (C-14 §3). `engines` 는 npm에게 하는 말이라 기본값에서
|
|
686
718
|
// 경고로만 나가고, 그러면 "경고 뒤에 그래도 돌아감"이 된다 — 사용자는 자기가 지원
|
|
687
719
|
// 범위 안인지 끝내 모른다. 여기서 한 번, 결정적으로 답한다.
|
|
@@ -717,6 +749,8 @@ async function runParsedCommand(values, positionals, entry, argv) {
|
|
|
717
749
|
// 교체하는 셈이고, 교체 도중 그 파일들이 사라진다. 붙지 않은 자리에서도 답해야 하므로
|
|
718
750
|
// 기계 수준 명령과 같은 자리에 둔다.
|
|
719
751
|
const machineLevelRuntime = group === 'update' ||
|
|
752
|
+
// 제품을 걷어내는 일도 갈아 끼우는 일과 같다 — 없애려는 그 build 로 넘기지 않는다.
|
|
753
|
+
group === 'uninstall' ||
|
|
720
754
|
(group === 'runtime' &&
|
|
721
755
|
(command === 'use' ||
|
|
722
756
|
command === 'status' ||
|
|
@@ -728,8 +762,6 @@ async function runParsedCommand(values, positionals, entry, argv) {
|
|
|
728
762
|
if (redispatched !== null)
|
|
729
763
|
return redispatched;
|
|
730
764
|
}
|
|
731
|
-
if (group === 'init')
|
|
732
|
-
return runInit(values);
|
|
733
765
|
if (group === 'workspace')
|
|
734
766
|
return runWorkspace(command, values);
|
|
735
767
|
// 어느 build를 쓸지는 **프로젝트와 무관하다** — 붙기 전에도 답해야 한다 (C-14 §5).
|
|
@@ -748,6 +780,21 @@ async function runParsedCommand(values, positionals, entry, argv) {
|
|
|
748
780
|
// 갈아 끼우는 일은 붙은 프로젝트와 무관하다 — 어느 자리에서 쳐도 같은 답이어야 한다.
|
|
749
781
|
if (group === 'update')
|
|
750
782
|
return runUpdate(command, values);
|
|
783
|
+
// 이 runtime 이 소유한 integration 만 지금 상태로 되맞춘다. 버전은 그대로다.
|
|
784
|
+
if (group === 'refresh')
|
|
785
|
+
return runRefresh(command, values);
|
|
786
|
+
// 제품과 제품이 심은 것을 걷어낸다. 사용자 상태는 남는다.
|
|
787
|
+
if (group === 'uninstall')
|
|
788
|
+
return runUninstall(command, values);
|
|
789
|
+
// 첫 진단 표면. 붙지 않은 자리에서도 답해야 한다.
|
|
790
|
+
if (group === 'status')
|
|
791
|
+
return runStatus(values);
|
|
792
|
+
// `asc init` 의 자리는 `asc setup` 이 이어받았다. 옛 이름은 두 minor 동안 그대로 답한다 —
|
|
793
|
+
// **하던 일을 그대로 하면서** 새 이름을 말한다 (§58). 같은 이름에 다른 동작을 넣으면
|
|
794
|
+
// 그것은 alias 가 아니라 조용한 계약 변경이다: `asc init --profile <id>` 는 이 저장소를
|
|
795
|
+
// 붙이는 명령이고, `asc setup` 은 기계까지 준비시키는 더 넓은 명령이다.
|
|
796
|
+
if (group === 'init')
|
|
797
|
+
return runInit(values);
|
|
751
798
|
// setup은 **붙기 전에도** 답을 줘야 한다. 아래 discoverRoot 실패는 exit 2로 끊는데,
|
|
752
799
|
// 그러면 "아직 안 붙었다"를 확인하려고 부른 명령이 안 붙었다는 이유로 죽는다.
|
|
753
800
|
if (group === 'setup')
|
|
@@ -761,7 +808,7 @@ async function runParsedCommand(values, positionals, entry, argv) {
|
|
|
761
808
|
// 순서가 뒤집힌다. 나머지 profile 명령은 아래 attach 경로에 그대로 남는다.
|
|
762
809
|
if (group === 'profile' && command === 'adopt')
|
|
763
810
|
return runProfileAdopt(values, entry);
|
|
764
|
-
if (!['inbox', 'grant', 'monitor', 'runtime', 'front', 'coordination', 'freeze', 'thaw', 'escalate', 'profile', 'session', 'controller', 'proceed', 'progress', 'preflight', 'closure', 'query'].includes(group)) {
|
|
811
|
+
if (!['inbox', 'grant', 'monitor', 'runtime', 'front', 'coordination', 'freeze', 'thaw', 'escalate', 'profile', 'session', 'controller', 'proceed', 'progress', 'preflight', 'closure', 'query', 'mode', 'work'].includes(group)) {
|
|
765
812
|
console.error(`Unknown command: ${group}\n\n${USAGE}`);
|
|
766
813
|
return 2;
|
|
767
814
|
}
|
|
@@ -776,12 +823,23 @@ async function runParsedCommand(values, positionals, entry, argv) {
|
|
|
776
823
|
const priority = values.priority;
|
|
777
824
|
if (group === 'profile')
|
|
778
825
|
return runProfile(command, values, root);
|
|
826
|
+
// 실행을 누가 하는가 (Axis C). Agent Management 도 Decision Authority 도 바꾸지 않는다.
|
|
827
|
+
//
|
|
828
|
+
// **lock drift 앞에서도 답해야 한다** (E-02·§16). enforcement 를 낮추는 공식 출구가
|
|
829
|
+
// 설정이 어긋났다는 이유로 막히면 그것이 곧 출구 없는 AUTO 다. 그래서 아래 bootstrap
|
|
830
|
+
// 문보다 앞에 선다 — 권한 판정은 이 명령 안에서 Core 가 그대로 한다.
|
|
831
|
+
if (group === 'mode')
|
|
832
|
+
return runMode(command, values, store, root, await attachedRuntime(root));
|
|
779
833
|
// attach된 프로젝트라면 Run을 시작하기 전에 지금 설정이 lock과 같은지 본다 (OM §4.9)
|
|
780
834
|
const guard = await checkBootstrap(root);
|
|
781
835
|
if (guard.code !== 0)
|
|
782
836
|
return guard.code;
|
|
783
|
-
|
|
784
|
-
|
|
837
|
+
// 정상 작업 표면. 안쪽 단계는 그대로 남고, 사람이 그 순서를 외우지 않는다.
|
|
838
|
+
if (group === 'work')
|
|
839
|
+
return runWork(command, target, values, store, root, guard.runtime);
|
|
840
|
+
if (group === 'proceed') {
|
|
841
|
+
return withDeprecation('asc work start', values, () => runProceed(values, store, root, guard.runtime));
|
|
842
|
+
}
|
|
785
843
|
if (group === 'session')
|
|
786
844
|
return runSession(command, target, values, store, guard.runtime);
|
|
787
845
|
if (group === 'controller')
|
|
@@ -790,8 +848,11 @@ async function runParsedCommand(values, positionals, entry, argv) {
|
|
|
790
848
|
return runClosure(command, target, values, store);
|
|
791
849
|
if (group === 'query')
|
|
792
850
|
return runQuery(command, target, values, store, guard.runtime);
|
|
793
|
-
if (group === 'progress')
|
|
794
|
-
return
|
|
851
|
+
if (group === 'progress') {
|
|
852
|
+
return command === 'show'
|
|
853
|
+
? withDeprecation('asc work status', values, () => runProgress(command, target, values, store))
|
|
854
|
+
: runProgress(command, target, values, store);
|
|
855
|
+
}
|
|
795
856
|
if (group === 'preflight')
|
|
796
857
|
return runPreflight(values, store, guard.runtime);
|
|
797
858
|
if (group === 'grant')
|
|
@@ -812,7 +873,9 @@ async function runParsedCommand(values, positionals, entry, argv) {
|
|
|
812
873
|
// 원격을 얼린다·녹인다. 로컬 작업은 얼리지 않는다 (지시 §27).
|
|
813
874
|
if (group === 'freeze' || group === 'thaw')
|
|
814
875
|
return runFreeze(group, command, values, store);
|
|
815
|
-
|
|
876
|
+
// 남은 것은 inbox 다. 이름만 치면 목록이다 — 사람이 물은 것은 "무엇이 기다리는가" 이고,
|
|
877
|
+
// 그 답을 얻으려고 하위 명령을 하나 더 외우게 하지 않는다 (§49).
|
|
878
|
+
switch (command ?? 'list') {
|
|
816
879
|
case 'list': {
|
|
817
880
|
const items = await operator.list({ all: Boolean(values.all), ...(priority ? { priority } : {}) });
|
|
818
881
|
console.log(values.json ? JSON.stringify(items, null, 2) : renderer.renderList(items).text);
|
|
@@ -1192,7 +1255,7 @@ async function runInit(values) {
|
|
|
1192
1255
|
// 이 출력은 지나가면 끝이므로 다시 보는 법도 함께 알린다 (B-21).
|
|
1193
1256
|
console.log(`\n${renderSetup(await inspectSetup(ascRoot))}`);
|
|
1194
1257
|
console.log('\nAttached. Issue the first session with `asc session issue`.');
|
|
1195
|
-
console.log('You can see this summary again any time with `asc
|
|
1258
|
+
console.log('You can see this summary again any time with `asc status`.');
|
|
1196
1259
|
return 0;
|
|
1197
1260
|
}
|
|
1198
1261
|
/** 붙어 있으면 무엇으로 붙었는지 읽는다. 안 붙었거나 어긋났으면 없는 것으로 본다. */
|
|
@@ -1335,7 +1398,11 @@ async function runSetup(command, values, entry = 'runtime') {
|
|
|
1335
1398
|
return runSetupIdentity(values);
|
|
1336
1399
|
if (values.agent)
|
|
1337
1400
|
return runSetupLifecycle('apply', values, entry);
|
|
1338
|
-
|
|
1401
|
+
// `asc setup` 은 **준비시키는** 명령이다 (§19). 진단은 `asc status` 가 맡는다 —
|
|
1402
|
+
// 같은 이름이 어제는 보고 오늘은 바꾸는 것이면 사람이 둘 중 무엇인지 매번 확인해야 한다.
|
|
1403
|
+
if (command === undefined)
|
|
1404
|
+
return runSetupLifecycle('apply', values, entry);
|
|
1405
|
+
if (command !== 'status') {
|
|
1339
1406
|
console.error(`Unknown setup command: ${command}\n\n${USAGE}`);
|
|
1340
1407
|
return 2;
|
|
1341
1408
|
}
|
|
@@ -1743,7 +1810,7 @@ async function jamVersion(projectRoot) {
|
|
|
1743
1810
|
* `npm` 을 shim으로 부르지 않는다.
|
|
1744
1811
|
*
|
|
1745
1812
|
* Windows에서 `npm` 은 `npm.cmd` 이고, Node는 보안 수정 이후 shell 없이 `.cmd` 를 실행하지
|
|
1746
|
-
* 않는다 — 그대로 두면 `asc
|
|
1813
|
+
* 않는다 — 그대로 두면 `asc status` 가 전역 설치를 조회하지 못하고 "설치 안 됨"으로
|
|
1747
1814
|
* 잘못 답한다. shell을 켜는 것은 답이 아니다(인자가 escape 없이 이어붙는다). npm의 진입
|
|
1748
1815
|
* JS를 찾아 지금 도는 node로 직접 돌리면 세 OS에서 같은 실행 경로가 된다.
|
|
1749
1816
|
*
|
|
@@ -1819,7 +1886,7 @@ async function runSetupIdentity(values) {
|
|
|
1819
1886
|
// drift 때문에 profile 을 못 읽어 영영 못 닫는다.
|
|
1820
1887
|
const attachedProfile = values.profile ?? (await lockedProfileId(root));
|
|
1821
1888
|
if (!attachedProfile) {
|
|
1822
|
-
console.error('붙어 있는 Profile 을 알 수 없다 — `asc
|
|
1889
|
+
console.error('붙어 있는 Profile 을 알 수 없다 — `asc status` 를 보고, 필요하면 --profile 로 지목하라.');
|
|
1823
1890
|
return 1;
|
|
1824
1891
|
}
|
|
1825
1892
|
const identitiesPath = join(root, IDENTITY_FILE);
|
|
@@ -2518,8 +2585,14 @@ async function runHost(provider, command, target, values) {
|
|
|
2518
2585
|
}
|
|
2519
2586
|
const claimed = await bindings.claim(spec, at);
|
|
2520
2587
|
if (!claimed.ok) {
|
|
2521
|
-
|
|
2522
|
-
|
|
2588
|
+
// 부딪힌 상대가 **어느 세션인지** 말한다. 같은 Run 이 다른 세션을 잡고 있는 경우와
|
|
2589
|
+
// 이 세션을 다른 Run 이 잡고 있는 경우는 사람이 할 일이 다르다.
|
|
2590
|
+
const other = claimed.current.logicalSessionId;
|
|
2591
|
+
console.error(other === target
|
|
2592
|
+
? `RUNTIME_CONFLICT: ${target} 은 이미 ${claimed.current.physicalSessionId} 가 잡고 있다. ` +
|
|
2593
|
+
'죽은 세션이 확실하면 --force 로 rebind하라.'
|
|
2594
|
+
: `RUNTIME_CONFLICT: 이 Run(${physical}) 은 지금 ${other} 을 잡고 있다. ` +
|
|
2595
|
+
`한 Run 은 한 세션만 잡는다 — 먼저 놓아라: asc host claude release ${other} --physical ${physical}`);
|
|
2523
2596
|
return 1;
|
|
2524
2597
|
}
|
|
2525
2598
|
await recordExecution('host bind');
|
|
@@ -3982,6 +4055,667 @@ const serviceInterval = (values) => values['interval-min'] === undefined ? 5 * 6
|
|
|
3982
4055
|
*
|
|
3983
4056
|
* 순서는 계획이 정하고(`planUpdate`), 여기서는 그대로 실행한다.
|
|
3984
4057
|
*/
|
|
4058
|
+
/**
|
|
4059
|
+
* 옛 이름으로 들어온 명령. **하던 일은 그대로 하고**, 새 이름을 알려 준다 (§58·§59).
|
|
4060
|
+
*
|
|
4061
|
+
* `--json` 은 문서 하나라는 계약이 있다 — 그래서 안내를 그 문서 **안에** 넣는다.
|
|
4062
|
+
* 사람에게는 stderr 한 줄이다. stdout 은 옛 형태 그대로 남아야 기존 스크립트가 안 깨진다.
|
|
4063
|
+
*/
|
|
4064
|
+
async function withDeprecation(replacement, values, run) {
|
|
4065
|
+
if (!values.json)
|
|
4066
|
+
return run();
|
|
4067
|
+
const captured = [];
|
|
4068
|
+
const log = console.log;
|
|
4069
|
+
console.log = (...parts) => void captured.push(parts.join(' '));
|
|
4070
|
+
let code;
|
|
4071
|
+
try {
|
|
4072
|
+
code = await run();
|
|
4073
|
+
}
|
|
4074
|
+
finally {
|
|
4075
|
+
console.log = log;
|
|
4076
|
+
}
|
|
4077
|
+
const text = captured.join('\n');
|
|
4078
|
+
let document;
|
|
4079
|
+
try {
|
|
4080
|
+
document = JSON.parse(text);
|
|
4081
|
+
}
|
|
4082
|
+
catch {
|
|
4083
|
+
document = undefined;
|
|
4084
|
+
}
|
|
4085
|
+
if (document !== undefined && document !== null && !Array.isArray(document) && typeof document === 'object') {
|
|
4086
|
+
console.log(JSON.stringify({ ...document, deprecated: true, replacement }, null, 2));
|
|
4087
|
+
}
|
|
4088
|
+
else if (text) {
|
|
4089
|
+
// 문서 하나가 아니면 형태를 바꾸지 않는다 — 안내는 이미 stderr 로 나갔다.
|
|
4090
|
+
console.log(text);
|
|
4091
|
+
}
|
|
4092
|
+
return code;
|
|
4093
|
+
}
|
|
4094
|
+
/** 옛 이름 → 새 이름. 두 minor 동안 여기 남는다 (§58). */
|
|
4095
|
+
const RENAMED = {
|
|
4096
|
+
init: 'asc setup',
|
|
4097
|
+
proceed: 'asc work start',
|
|
4098
|
+
'progress show': 'asc work status',
|
|
4099
|
+
};
|
|
4100
|
+
/**
|
|
4101
|
+
* AUTO 로 갈 수 있는지 판정할 재료를 **이미 관측되는 사실에서** 모은다 (§8).
|
|
4102
|
+
*
|
|
4103
|
+
* 새 health 저장소를 만들지 않는다. 여기 있는 것은 전부 다른 명령이 이미 보여 주는 것이고,
|
|
4104
|
+
* 이 함수는 그것을 한 판정에 모으기만 한다.
|
|
4105
|
+
*/
|
|
4106
|
+
async function observeReadiness(root, runtime) {
|
|
4107
|
+
// ① 관리된 쓰기 경로가 조립되는가. 없으면 AUTO 는 막기만 하고 내보내지는 못하는 mode 다.
|
|
4108
|
+
// **설정 파일이 있다는 것으로 답하지 않는다** — Composition 이 실제로 만든 Port 가 근거다.
|
|
4109
|
+
const outward = root ? await composedPorts(runtime).then((ports) => ports.scm ?? null).catch(() => null) : null;
|
|
4110
|
+
const axes = [
|
|
4111
|
+
outward
|
|
4112
|
+
? { axis: 'executor', state: 'READY', detail: outward.id }
|
|
4113
|
+
: { axis: 'executor', state: 'MISSING', detail: 'no binding provides an outward write path' },
|
|
4114
|
+
];
|
|
4115
|
+
// ② 막을 것을 실제로 막을 수 있는가. hook 이 없으면 AUTO 는 이름뿐이다.
|
|
4116
|
+
const host = await verifyInstall(hostPaths());
|
|
4117
|
+
axes.push({
|
|
4118
|
+
axis: 'guard',
|
|
4119
|
+
state: host.status === 'INSTALLED_CURRENT'
|
|
4120
|
+
? 'READY'
|
|
4121
|
+
: host.hookRegistered
|
|
4122
|
+
? 'DEGRADED'
|
|
4123
|
+
: 'MISSING',
|
|
4124
|
+
detail: host.status,
|
|
4125
|
+
});
|
|
4126
|
+
// ③ 그 상태에서 사람이 ASC 를 계속 부를 수 있는가 — 0.7.1 이 갇혔던 자리다.
|
|
4127
|
+
const access = await controlPlaneAccess(hostPaths());
|
|
4128
|
+
axes.push({
|
|
4129
|
+
axis: 'control-plane',
|
|
4130
|
+
state: access.denied ? 'BLOCKED_BY_HOST' : access.allowed ? 'READY' : 'MISSING',
|
|
4131
|
+
...(access.detail ? { detail: access.detail } : {}),
|
|
4132
|
+
});
|
|
4133
|
+
return axes;
|
|
4134
|
+
}
|
|
4135
|
+
/** provider 에게 "이걸 할 수 있는가" 를 물을 때 쓰는 행위 목록. 화면 표시용이다. */
|
|
4136
|
+
const EXTERNAL_ACTIONS = [
|
|
4137
|
+
'git.push',
|
|
4138
|
+
'coordination.publish',
|
|
4139
|
+
'gitlab.mr.create',
|
|
4140
|
+
'gitlab.mr.merge',
|
|
4141
|
+
'gitlab.note.create',
|
|
4142
|
+
'gitlab.issue.update',
|
|
4143
|
+
'github.issue_comment.create',
|
|
4144
|
+
];
|
|
4145
|
+
/**
|
|
4146
|
+
* `asc status` — 처음 묻는 자리 (§21·§22).
|
|
4147
|
+
*
|
|
4148
|
+
* **새 SSOT 를 만들지 않는다.** 여기 나오는 사실은 전부 다른 곳이 이미 아는 것이고,
|
|
4149
|
+
* 이 명령은 그것들을 한 화면에 모은다. 그리고 증거보다 강하게 말하지 않는다:
|
|
4150
|
+
* 붙어 있다는 것이 건강하다는 뜻이 아니고, AUTO 라는 것이 나갈 길이 있다는 뜻이 아니다.
|
|
4151
|
+
*/
|
|
4152
|
+
async function runStatus(values) {
|
|
4153
|
+
const resolution = await resolveRoot(process.cwd(), values.root);
|
|
4154
|
+
const root = resolution.kind === 'UNRESOLVED' ? null : resolution.root;
|
|
4155
|
+
const setup = root
|
|
4156
|
+
? await inspectSetup(root)
|
|
4157
|
+
: assessSetup({
|
|
4158
|
+
attachment: 'UNATTACHED',
|
|
4159
|
+
hasApprovers: false,
|
|
4160
|
+
hasControllerIdentities: false,
|
|
4161
|
+
hasMonitorIdentities: false,
|
|
4162
|
+
hasScmToken: await hasToken(),
|
|
4163
|
+
});
|
|
4164
|
+
const runtime = root ? await attachedRuntime(root) : undefined;
|
|
4165
|
+
const host = await verifyInstall(hostPaths());
|
|
4166
|
+
const selection = await readRuntimeSelection(ascHome());
|
|
4167
|
+
const build = await resolveRuntimeTarget(selection);
|
|
4168
|
+
const service = await serviceHealth(values);
|
|
4169
|
+
const background = await backgroundHere(values);
|
|
4170
|
+
const store = root ? new MarkdownStateStore(root) : null;
|
|
4171
|
+
const mode = store ? await readExecutionMode(store.scope('policy')) : null;
|
|
4172
|
+
const readiness = judgeAutoReadiness(await observeReadiness(root, runtime));
|
|
4173
|
+
// 밖을 읽을 수 있는가 · 밖에 쓸 수 있는가. 두 답 모두 조립 결과에서 나온다.
|
|
4174
|
+
const ports = root ? await composedPorts(runtime).catch(() => null) : null;
|
|
4175
|
+
const external = {
|
|
4176
|
+
read: ports?.eventSource?.id ?? ports?.inventory?.id ?? null,
|
|
4177
|
+
write: ports?.scm
|
|
4178
|
+
? {
|
|
4179
|
+
id: ports.scm.id,
|
|
4180
|
+
// 할 수 있는 행위와, 그 중 되돌려 읽을 수 있는 행위. 둘은 다른 사실이고,
|
|
4181
|
+
// 화면이 그것을 뭉개면 사람이 확인되지 않는 쓰기를 확인된 것으로 읽는다.
|
|
4182
|
+
actions: EXTERNAL_ACTIONS.filter((action) => ports.scm.supports?.(action) ?? false),
|
|
4183
|
+
verifiable: EXTERNAL_ACTIONS.filter((action) => ports.scm.verifies?.(action) ?? false),
|
|
4184
|
+
}
|
|
4185
|
+
: null,
|
|
4186
|
+
unavailable: ports?.unavailable ?? [],
|
|
4187
|
+
};
|
|
4188
|
+
const sessions = store
|
|
4189
|
+
? (await store.list('session')).filter((session) => session.status === 'ACTIVE' || session.status === 'PAUSED')
|
|
4190
|
+
: [];
|
|
4191
|
+
const waiting = store ? await new LocalOperator({ store }).list({}) : [];
|
|
4192
|
+
// 무엇이 지금 걸려 있는가. 사실에서만 뽑는다 — 여기서 추측을 만들지 않는다.
|
|
4193
|
+
const degraded = [];
|
|
4194
|
+
if (setup.attachment !== 'READY' && root)
|
|
4195
|
+
degraded.push(`attachment ${setup.attachment}`);
|
|
4196
|
+
if (host.status !== 'INSTALLED_CURRENT')
|
|
4197
|
+
degraded.push(`host integration ${host.status}`);
|
|
4198
|
+
if (mode?.mode === 'AUTO' && !readiness.ready) {
|
|
4199
|
+
for (const axis of readiness.blocking)
|
|
4200
|
+
degraded.push(`AUTO ${axis.axis} ${axis.state}`);
|
|
4201
|
+
}
|
|
4202
|
+
if (service?.action === 'install')
|
|
4203
|
+
degraded.push('this machine has no persistent registration');
|
|
4204
|
+
const next = (() => {
|
|
4205
|
+
if (!root)
|
|
4206
|
+
return 'asc setup';
|
|
4207
|
+
if (setup.attachment === 'LOCK_DRIFT')
|
|
4208
|
+
return 'asc setup — the configuration moved away from the lock';
|
|
4209
|
+
if (host.status !== 'INSTALLED_CURRENT' && host.status !== 'INSTALLED_MODIFIED')
|
|
4210
|
+
return 'asc refresh';
|
|
4211
|
+
if (mode?.mode === 'AUTO' && !readiness.ready) {
|
|
4212
|
+
return 'asc mode manual — or fix what AUTO needs, then `asc mode auto`';
|
|
4213
|
+
}
|
|
4214
|
+
if (waiting.length > 0)
|
|
4215
|
+
return 'asc inbox';
|
|
4216
|
+
if (sessions.length > 0)
|
|
4217
|
+
return `asc work status ${sessions[0].id}`;
|
|
4218
|
+
return 'asc work start <WORK>';
|
|
4219
|
+
})();
|
|
4220
|
+
if (values.json) {
|
|
4221
|
+
console.log(JSON.stringify({
|
|
4222
|
+
version: RELEASE_VERSION,
|
|
4223
|
+
runtime: resolution,
|
|
4224
|
+
build: 'code' in build ? { error: build } : build,
|
|
4225
|
+
installation: host.status,
|
|
4226
|
+
setup,
|
|
4227
|
+
executionMode: mode ? { mode: mode.mode, chosen: mode.chosen } : null,
|
|
4228
|
+
autoReadiness: { ready: readiness.ready, axes: readiness.axes },
|
|
4229
|
+
external,
|
|
4230
|
+
work: sessions.map((session) => ({ id: session.id, status: session.status, role: session.role })),
|
|
4231
|
+
awaitingHuman: waiting.length,
|
|
4232
|
+
...(service ? { service } : {}),
|
|
4233
|
+
...(background ? { background } : {}),
|
|
4234
|
+
degraded,
|
|
4235
|
+
nextAction: next,
|
|
4236
|
+
}, null, 2));
|
|
4237
|
+
return 0;
|
|
4238
|
+
}
|
|
4239
|
+
console.log(`asc ${RELEASE_VERSION}`);
|
|
4240
|
+
if (!('code' in build))
|
|
4241
|
+
console.log(runtimeSelectionLine(build));
|
|
4242
|
+
console.log(`Installation: ${host.status}`);
|
|
4243
|
+
console.log(resolutionLine(resolution));
|
|
4244
|
+
console.log(renderSetup(setup));
|
|
4245
|
+
console.log('');
|
|
4246
|
+
console.log(`Execution Mode: ${mode ? `${mode.mode}${mode.chosen ? '' : ' (never chosen — nothing is being enforced)'}` : '(not attached)'}`);
|
|
4247
|
+
// AUTO 는 HITL 의 반대가 아니다 — 실행을 누가 하느냐일 뿐이라는 것을 화면이 말한다.
|
|
4248
|
+
console.log(' Mode decides who executes. It never decides what a person must approve.');
|
|
4249
|
+
for (const axis of readiness.axes) {
|
|
4250
|
+
console.log(` ${axis.state.padEnd(16)} ${axis.axis}${axis.detail ? ` — ${axis.detail}` : ''}`);
|
|
4251
|
+
}
|
|
4252
|
+
console.log(readiness.ready ? ' AUTO READY' : ' AUTO NOT AVAILABLE');
|
|
4253
|
+
if (root) {
|
|
4254
|
+
console.log('');
|
|
4255
|
+
console.log(`External read: ${external.read ?? 'none assembled'}`);
|
|
4256
|
+
console.log(`External write: ${external.write ? `${external.write.id} — ${external.write.actions.join(', ') || 'no known action'}` : 'none assembled'}`);
|
|
4257
|
+
if (external.write) {
|
|
4258
|
+
console.log(` read-back available for: ${external.write.verifiable.join(', ') || 'nothing'}`);
|
|
4259
|
+
}
|
|
4260
|
+
for (const reason of external.unavailable.slice(0, 3))
|
|
4261
|
+
console.log(` ${reason}`);
|
|
4262
|
+
}
|
|
4263
|
+
console.log('');
|
|
4264
|
+
if (sessions.length > 0) {
|
|
4265
|
+
console.log('Work in progress:');
|
|
4266
|
+
for (const session of sessions)
|
|
4267
|
+
console.log(` ${session.id} ${session.status} — ${session.goal ?? ''}`);
|
|
4268
|
+
}
|
|
4269
|
+
else if (root) {
|
|
4270
|
+
console.log('Work in progress: none');
|
|
4271
|
+
}
|
|
4272
|
+
if (waiting.length > 0)
|
|
4273
|
+
console.log(`Waiting for a person: ${waiting.length} (asc inbox)`);
|
|
4274
|
+
if (service)
|
|
4275
|
+
console.log(`Background: ${service.line}`);
|
|
4276
|
+
if (background)
|
|
4277
|
+
for (const line of renderBackground(background))
|
|
4278
|
+
console.log(line);
|
|
4279
|
+
if (degraded.length > 0) {
|
|
4280
|
+
console.log('');
|
|
4281
|
+
console.log('Degraded:');
|
|
4282
|
+
for (const reason of degraded)
|
|
4283
|
+
console.log(` - ${reason}`);
|
|
4284
|
+
}
|
|
4285
|
+
console.log('');
|
|
4286
|
+
console.log(`Next: ${next}`);
|
|
4287
|
+
// 진단이지 실패가 아니다 — 막힌 것이 있어도 0이다.
|
|
4288
|
+
return 0;
|
|
4289
|
+
}
|
|
4290
|
+
/**
|
|
4291
|
+
* `asc mode` — 실행을 누가 하는가 (Axis C).
|
|
4292
|
+
*
|
|
4293
|
+
* 이 명령이 바꾸는 것은 **실행 경로 하나**다. 세션의 주인·범위·진행도, 무엇을 사람이
|
|
4294
|
+
* 결정해야 하는지도 바꾸지 않는다 (AM-02 · H-01 · H-02).
|
|
4295
|
+
*/
|
|
4296
|
+
async function runMode(command, values, store, root, runtime) {
|
|
4297
|
+
const scope = store.scope('policy');
|
|
4298
|
+
const current = await readExecutionMode(scope);
|
|
4299
|
+
const readiness = judgeAutoReadiness(await observeReadiness(root, runtime));
|
|
4300
|
+
const show = (state, extra = {}) => {
|
|
4301
|
+
if (values.json) {
|
|
4302
|
+
console.log(JSON.stringify({
|
|
4303
|
+
mode: state.mode ?? null,
|
|
4304
|
+
chosen: state.chosen,
|
|
4305
|
+
...(state.degraded ? { degraded: state.degraded } : {}),
|
|
4306
|
+
enforcement: enforcementOf(state),
|
|
4307
|
+
autoReadiness: { ready: readiness.ready, axes: readiness.axes },
|
|
4308
|
+
...extra,
|
|
4309
|
+
}, null, 2));
|
|
4310
|
+
return;
|
|
4311
|
+
}
|
|
4312
|
+
console.log(modeLine(state));
|
|
4313
|
+
for (const axis of readiness.axes) {
|
|
4314
|
+
console.log(` ${axis.state.padEnd(16)} ${axis.axis}${axis.detail ? ` — ${axis.detail}` : ''}`);
|
|
4315
|
+
}
|
|
4316
|
+
console.log(readiness.ready ? ' AUTO READY' : ' AUTO NOT AVAILABLE');
|
|
4317
|
+
for (const [key, value] of Object.entries(extra))
|
|
4318
|
+
console.log(`${key}: ${String(value)}`);
|
|
4319
|
+
};
|
|
4320
|
+
if (command === undefined || command === 'status') {
|
|
4321
|
+
show(current);
|
|
4322
|
+
return 0;
|
|
4323
|
+
}
|
|
4324
|
+
if (command !== 'manual' && command !== 'auto') {
|
|
4325
|
+
console.error(`Unknown mode: ${command} — use \`asc mode manual\` or \`asc mode auto\`.`);
|
|
4326
|
+
return 2;
|
|
4327
|
+
}
|
|
4328
|
+
const wanted = command === 'auto' ? 'AUTO' : 'MANUAL';
|
|
4329
|
+
if (wanted === 'AUTO') {
|
|
4330
|
+
// E-01 — 나갈 길이 실제로 열려 있을 때만 켠다. 하나라도 아니면 **지금 mode 를 그대로 둔다**.
|
|
4331
|
+
if (!readiness.ready) {
|
|
4332
|
+
if (values.json) {
|
|
4333
|
+
console.log(JSON.stringify({
|
|
4334
|
+
mode: current.mode ?? null,
|
|
4335
|
+
chosen: current.chosen,
|
|
4336
|
+
requested: 'AUTO',
|
|
4337
|
+
applied: false,
|
|
4338
|
+
autoReadiness: { ready: false, axes: readiness.axes },
|
|
4339
|
+
}, null, 2));
|
|
4340
|
+
}
|
|
4341
|
+
else {
|
|
4342
|
+
console.error('AUTO is not available — the approved execution path is not usable here:');
|
|
4343
|
+
for (const axis of readiness.blocking) {
|
|
4344
|
+
console.error(` ${axis.state} ${axis.axis}${axis.detail ? ` — ${axis.detail}` : ''}`);
|
|
4345
|
+
}
|
|
4346
|
+
console.error(`Staying in ${current.mode ?? 'the current state'}. Nothing was changed.`);
|
|
4347
|
+
}
|
|
4348
|
+
return 1;
|
|
4349
|
+
}
|
|
4350
|
+
const record = await writeExecutionMode(scope, 'AUTO', values.as);
|
|
4351
|
+
await store.appendHistory({
|
|
4352
|
+
at: record.since ?? new Date().toISOString(),
|
|
4353
|
+
actor: values.as ?? 'unattributed',
|
|
4354
|
+
kind: 'execution_mode',
|
|
4355
|
+
ref: 'execution-mode',
|
|
4356
|
+
detail: `${current.mode ?? current.degraded ?? 'unknown'} → AUTO`,
|
|
4357
|
+
});
|
|
4358
|
+
show({ ...record, chosen: true }, { applied: 'true' });
|
|
4359
|
+
return 0;
|
|
4360
|
+
}
|
|
4361
|
+
// MANUAL 로 내려가는 것은 **막지 않는다**.
|
|
4362
|
+
//
|
|
4363
|
+
// 이전 회차에는 여기에 Request → Inbox → 승인 → 소진을 세워 뒀다. 그것은 같은 셸을
|
|
4364
|
+
// 쥔 Agent 를 막으려는 장치였는데, 같은 셸이면 `asc inbox decide` 도 칠 수 있다 —
|
|
4365
|
+
// 실제로 그 안내를 우리가 화면에 찍어 주고 있었다. 막지 못하는 것을 막는 척하면서
|
|
4366
|
+
// 사람에게만 세 걸음을 물리는 구조였다. ASC 의 위협 모델은 "협조적이지만 실수하는
|
|
4367
|
+
// Agent" 이고, 적대적 Agent 로부터 ASC 자신을 지키는 일은 Host/OS 신뢰 경계의 몫이다.
|
|
4368
|
+
//
|
|
4369
|
+
// 대신 **크게 남긴다**: 누가 그렇게 했다고 말하는지, 언제 바뀌었는지가 기록에 남고
|
|
4370
|
+
// 화면에 나온다. 실수하는 Agent 에게 필요한 것은 잠금이 아니라 드러남이다.
|
|
4371
|
+
const record = await writeExecutionMode(scope, 'MANUAL', values.as);
|
|
4372
|
+
await store.appendHistory({
|
|
4373
|
+
at: record.since ?? new Date().toISOString(),
|
|
4374
|
+
actor: values.as ?? 'unattributed',
|
|
4375
|
+
kind: 'execution_mode',
|
|
4376
|
+
ref: 'execution-mode',
|
|
4377
|
+
detail: `${current.mode ?? current.degraded ?? 'unknown'} → MANUAL`,
|
|
4378
|
+
});
|
|
4379
|
+
show({ ...record, chosen: true }, { applied: 'true' });
|
|
4380
|
+
return 0;
|
|
4381
|
+
}
|
|
4382
|
+
/**
|
|
4383
|
+
* `asc refresh` — 버전은 그대로 두고, 이 runtime 이 소유한 integration 만 지금 상태로
|
|
4384
|
+
* 되맞춘다 (§26·§27).
|
|
4385
|
+
*
|
|
4386
|
+
* ```text
|
|
4387
|
+
* refresh != setup Profile·workspace·identity 를 다시 추론하지 않는다
|
|
4388
|
+
* refresh != repair-all session·binding·grant·inbox 를 건드리지 않는다
|
|
4389
|
+
* refresh != reset 지우고 다시 만들지 않는다
|
|
4390
|
+
* ```
|
|
4391
|
+
*
|
|
4392
|
+
* 그래서 이 함수가 부르는 것은 둘뿐이다: host 설치물, 기계 등록물.
|
|
4393
|
+
*/
|
|
4394
|
+
async function runRefresh(command, values) {
|
|
4395
|
+
if (command !== undefined && command !== 'check' && command !== 'plan') {
|
|
4396
|
+
console.error(`Unknown refresh command: ${command}\n\n${USAGE}`);
|
|
4397
|
+
return 2;
|
|
4398
|
+
}
|
|
4399
|
+
const host = await verifyInstall(hostPaths());
|
|
4400
|
+
const access = await controlPlaneAccess(hostPaths());
|
|
4401
|
+
const service = await serviceHealth(values);
|
|
4402
|
+
const steps = [];
|
|
4403
|
+
if (host.status !== 'INSTALLED_CURRENT')
|
|
4404
|
+
steps.push(`host integration (${host.status})`);
|
|
4405
|
+
if (!access.allowed && !access.denied)
|
|
4406
|
+
steps.push('control-plane allow rule');
|
|
4407
|
+
if (service && (service.action === 'reinstall' || service.action === 'update'))
|
|
4408
|
+
steps.push(`service registration (${service.action})`);
|
|
4409
|
+
if (command === 'check' || command === 'plan') {
|
|
4410
|
+
if (values.json) {
|
|
4411
|
+
console.log(JSON.stringify({ version: RELEASE_VERSION, host: host.status, controlPlane: access, ...(service ? { service } : {}), steps }, null, 2));
|
|
4412
|
+
}
|
|
4413
|
+
else {
|
|
4414
|
+
for (const line of installReportLines(host))
|
|
4415
|
+
console.log(line);
|
|
4416
|
+
if (service)
|
|
4417
|
+
console.log(service.line);
|
|
4418
|
+
console.log(steps.length === 0 ? 'Nothing to converge.' : `Would converge: ${steps.join(', ')}`);
|
|
4419
|
+
}
|
|
4420
|
+
return 0;
|
|
4421
|
+
}
|
|
4422
|
+
let worst = 0;
|
|
4423
|
+
// 사람이 고친 설치물은 덮지 않는다 — 그 규칙은 host install 이 그대로 진다 (L-5).
|
|
4424
|
+
worst = Math.max(worst, await runHost('claude', 'install', undefined, { ...values, json: false }));
|
|
4425
|
+
worst = Math.max(worst, await convergeService(values));
|
|
4426
|
+
// 마지막은 언제나 확인이다. 계획했던 것이 실제로 사라졌는가.
|
|
4427
|
+
const after = await verifyInstall(hostPaths());
|
|
4428
|
+
for (const line of installReportLines(after))
|
|
4429
|
+
console.log(line);
|
|
4430
|
+
if (after.status !== 'INSTALLED_CURRENT' && after.status !== 'INSTALLED_MODIFIED') {
|
|
4431
|
+
console.error('refresh: the host integration is still not current.');
|
|
4432
|
+
return 1;
|
|
4433
|
+
}
|
|
4434
|
+
console.log(`asc ${RELEASE_VERSION} — integration is current. Version unchanged.`);
|
|
4435
|
+
return worst;
|
|
4436
|
+
}
|
|
4437
|
+
/**
|
|
4438
|
+
* `asc uninstall` — 제품과 제품이 심은 것을 걷어낸다. **사용자 상태는 남는다** (§30·§32).
|
|
4439
|
+
*
|
|
4440
|
+
* 순서가 계약이다: 등록물 → host 설치물 → 확인 → 설치본. 설치본을 먼저 지우면 그 다음
|
|
4441
|
+
* 단계를 수행할 실행물이 없다.
|
|
4442
|
+
*
|
|
4443
|
+
* `~/.asc` 는 지우지 않는다. purge 표면은 만들지 않는다 — 지울 이유가 실제로 확인되기
|
|
4444
|
+
* 전까지 되돌릴 수 없는 명령을 두지 않는다.
|
|
4445
|
+
*/
|
|
4446
|
+
async function runUninstall(command, values) {
|
|
4447
|
+
if (command !== undefined && command !== 'plan') {
|
|
4448
|
+
console.error(`Unknown uninstall command: ${command}\n\n${USAGE}`);
|
|
4449
|
+
return 2;
|
|
4450
|
+
}
|
|
4451
|
+
const host = await verifyInstall(hostPaths());
|
|
4452
|
+
const adapter = serviceAdapter();
|
|
4453
|
+
const installed = await detectStableInstall(nodeProcessRunner, RELEASE_VERSION);
|
|
4454
|
+
const home = ascHome();
|
|
4455
|
+
// **돌고 있는 일을 몰래 버리지 않는다** (0.8.0 §Q). uninstall 은 제품을 걷어내는
|
|
4456
|
+
// 명령이지 일을 끝내는 명령이 아니다 — 살아 있는 세션이나 물리 결합이 있으면 그것을
|
|
4457
|
+
// 어떻게 할지는 사람이 정한다. 새 lifecycle 상태를 만들지 않고, 이미 있는 상태를 읽는다.
|
|
4458
|
+
const here = await discoverRoot(process.cwd(), values.root);
|
|
4459
|
+
const live = here ? await liveWork(here) : { sessions: [], bindings: [] };
|
|
4460
|
+
const busy = live.sessions.length > 0 || live.bindings.length > 0;
|
|
4461
|
+
if (command === 'plan') {
|
|
4462
|
+
const payload = {
|
|
4463
|
+
remove: {
|
|
4464
|
+
service: adapter?.id ?? null,
|
|
4465
|
+
hostIntegration: host.status !== 'NOT_INSTALLED',
|
|
4466
|
+
runtime: installed.installedVersion ?? null,
|
|
4467
|
+
},
|
|
4468
|
+
preserve: { state: home, note: 'profiles, workspaces, sessions, audit, evidence and identities all stay' },
|
|
4469
|
+
...(busy ? { blockedBy: { sessions: live.sessions, bindings: live.bindings } } : {}),
|
|
4470
|
+
};
|
|
4471
|
+
if (values.json)
|
|
4472
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
4473
|
+
else {
|
|
4474
|
+
console.log('Would remove:');
|
|
4475
|
+
if (adapter)
|
|
4476
|
+
console.log(` the persistent registration (${adapter.id})`);
|
|
4477
|
+
if (host.status !== 'NOT_INSTALLED')
|
|
4478
|
+
console.log(' the ASC files and hook registration in ~/.claude');
|
|
4479
|
+
if (installed.installedVersion)
|
|
4480
|
+
console.log(` the installed runtime (${RUNTIME_PACKAGE}@${installed.installedVersion})`);
|
|
4481
|
+
console.log(`Would keep: ${home} — profiles, workspaces, sessions, audit, evidence, identities`);
|
|
4482
|
+
if (busy) {
|
|
4483
|
+
console.log('');
|
|
4484
|
+
console.log('Would refuse: work is still running here —');
|
|
4485
|
+
for (const id of live.sessions)
|
|
4486
|
+
console.log(` session ${id}`);
|
|
4487
|
+
for (const id of live.bindings)
|
|
4488
|
+
console.log(` a run is holding ${id}`);
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
return 0;
|
|
4492
|
+
}
|
|
4493
|
+
if (busy) {
|
|
4494
|
+
console.error('Work is still running in this workspace, and uninstalling would abandon it:');
|
|
4495
|
+
for (const id of live.sessions)
|
|
4496
|
+
console.error(` session ${id}`);
|
|
4497
|
+
for (const id of live.bindings)
|
|
4498
|
+
console.error(` a physical run is holding ${id}`);
|
|
4499
|
+
console.error('');
|
|
4500
|
+
console.error(`Finish or pause it first: \`asc work finish ${live.sessions[0] ?? '<S-ID>'} --verified "…" --next "…"\``);
|
|
4501
|
+
console.error('Nothing was removed.');
|
|
4502
|
+
return 2;
|
|
4503
|
+
}
|
|
4504
|
+
let worst = 0;
|
|
4505
|
+
if (adapter) {
|
|
4506
|
+
await adapter.uninstall().catch((error) => {
|
|
4507
|
+
console.error(`service: could not unregister with ${adapter.id}: ${String(error)}`);
|
|
4508
|
+
worst = 1;
|
|
4509
|
+
});
|
|
4510
|
+
console.log(`service: unregistered (${adapter.id})`);
|
|
4511
|
+
}
|
|
4512
|
+
const removedHost = await uninstall(hostPaths());
|
|
4513
|
+
for (const path of removedHost.removed)
|
|
4514
|
+
console.log(`removed: ${path}`);
|
|
4515
|
+
for (const keep of removedHost.kept)
|
|
4516
|
+
console.log(`kept: ${keep.path} — ${keep.reason}`);
|
|
4517
|
+
const afterHost = await verifyInstall(hostPaths());
|
|
4518
|
+
if (afterHost.hookRegistered) {
|
|
4519
|
+
console.error('host: an ASC hook registration is still in settings.json.');
|
|
4520
|
+
worst = 1;
|
|
4521
|
+
}
|
|
4522
|
+
// 설치본은 마지막이다. 개발 checkout 에서 돌고 있으면 그것은 우리가 설치한 것이 아니다.
|
|
4523
|
+
if (!installed.installedVersion) {
|
|
4524
|
+
console.log('runtime: nothing was installed by npm on this machine.');
|
|
4525
|
+
}
|
|
4526
|
+
else {
|
|
4527
|
+
const removal = await nodeProcessRunner('npm', ['uninstall', '-g', RUNTIME_PACKAGE]);
|
|
4528
|
+
if (!removal.ok) {
|
|
4529
|
+
console.error(`runtime: could not remove ${RUNTIME_PACKAGE} — ${removal.stderr.trim() || removal.stdout.trim()}`);
|
|
4530
|
+
console.error(`Remove it directly: npm uninstall -g ${RUNTIME_PACKAGE}`);
|
|
4531
|
+
worst = 1;
|
|
4532
|
+
}
|
|
4533
|
+
else {
|
|
4534
|
+
console.log(`runtime: removed ${RUNTIME_PACKAGE}@${installed.installedVersion}`);
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
console.log('');
|
|
4538
|
+
console.log(`Your state stays: ${home}`);
|
|
4539
|
+
console.log('Profiles, workspaces, sessions, audit, evidence and identities are untouched.');
|
|
4540
|
+
console.log(`Install it again later and they are all still there: ${portableCommand(['setup', 'apply'])}`);
|
|
4541
|
+
return worst;
|
|
4542
|
+
}
|
|
4543
|
+
/**
|
|
4544
|
+
* 지금 이 workspace 에서 돌고 있는 일. **읽기만 한다.**
|
|
4545
|
+
*
|
|
4546
|
+
* 두 가지를 본다: 아직 끝나지 않은 논리 세션과, 그 세션을 집고 있는 물리 Run. 어느
|
|
4547
|
+
* 하나라도 있으면 제품을 걷어내는 것은 그 일을 버리는 것이 된다.
|
|
4548
|
+
*/
|
|
4549
|
+
async function liveWork(root) {
|
|
4550
|
+
try {
|
|
4551
|
+
const store = new MarkdownStateStore(root);
|
|
4552
|
+
const sessions = (await store.list('session'))
|
|
4553
|
+
.filter((session) => session.status === 'ACTIVE' || session.status === 'PAUSED')
|
|
4554
|
+
.map((session) => session.id);
|
|
4555
|
+
const held = (await claudeBindings(store).current()).map((binding) => binding.logicalSessionId);
|
|
4556
|
+
return { sessions, bindings: held.filter((id) => !sessions.includes(id)) };
|
|
4557
|
+
}
|
|
4558
|
+
catch {
|
|
4559
|
+
// 읽지 못한 것을 "없다" 로 적지 않는다 — 모르면 막는 쪽이 안전하다.
|
|
4560
|
+
return { sessions: ['(could not be read)'], bindings: [] };
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
/**
|
|
4564
|
+
* `asc work` — 정상 작업 표면 (§34~§42).
|
|
4565
|
+
*
|
|
4566
|
+
* **새 Work entity 를 만들지 않는다.** 이 함수 아래에서 도는 것은 전부 기존 경로다 —
|
|
4567
|
+
* 계약 초안·세션·물리 결합·preflight·진행·handoff·collect·Grant·Executor. 달라지는 것은
|
|
4568
|
+
* 사람이 그 순서를 외우지 않아도 된다는 것 하나다.
|
|
4569
|
+
*/
|
|
4570
|
+
async function runWork(command, target, values, store, root, runtime) {
|
|
4571
|
+
/** 지금 도는 세션. 지목이 없으면 하나일 때만 고른다 — 여럿이면 고르지 않는다. */
|
|
4572
|
+
const currentSession = async () => {
|
|
4573
|
+
if (target)
|
|
4574
|
+
return target;
|
|
4575
|
+
if (values.session)
|
|
4576
|
+
return values.session;
|
|
4577
|
+
const active = (await store.list('session')).filter((session) => session.status === 'ACTIVE');
|
|
4578
|
+
if (active.length === 1)
|
|
4579
|
+
return active[0].id;
|
|
4580
|
+
if (active.length === 0)
|
|
4581
|
+
return null;
|
|
4582
|
+
console.error(`More than one session is active — say which: ${active.map((session) => session.id).join(', ')}`);
|
|
4583
|
+
return null;
|
|
4584
|
+
};
|
|
4585
|
+
switch (command) {
|
|
4586
|
+
// 일을 시작한다. 안에서 도는 것: work ingress → 계약 초안 → 발급/재개 → 물리 결합 → preflight.
|
|
4587
|
+
case 'start':
|
|
4588
|
+
return runProceed({ ...values, ...(target ? { work: target } : {}) }, store, root, runtime);
|
|
4589
|
+
case 'status':
|
|
4590
|
+
return runProgress('show', target ?? values.session, values, store);
|
|
4591
|
+
case 'inspect': {
|
|
4592
|
+
const session = await currentSession();
|
|
4593
|
+
if (!session) {
|
|
4594
|
+
console.error('Which session? `asc work inspect <S-ID>`');
|
|
4595
|
+
return 2;
|
|
4596
|
+
}
|
|
4597
|
+
// 계약·범위·완료조건·결정권·검증·감사가 한 화면에 있어야 한다 (§38).
|
|
4598
|
+
const report = await runSession('report', session, values, store, runtime);
|
|
4599
|
+
if (report !== 0)
|
|
4600
|
+
return report;
|
|
4601
|
+
return runSession('audit', session, values, store, runtime);
|
|
4602
|
+
}
|
|
4603
|
+
case 'pause':
|
|
4604
|
+
case 'resume': {
|
|
4605
|
+
const session = await currentSession();
|
|
4606
|
+
if (!session) {
|
|
4607
|
+
console.error(`Which session? \`asc work ${command} <S-ID>\``);
|
|
4608
|
+
return 2;
|
|
4609
|
+
}
|
|
4610
|
+
return runSession(command, session, values, store, runtime);
|
|
4611
|
+
}
|
|
4612
|
+
// 끝낸다. 사람이 두 단계를 알아야 하는 구조를 여기서 끝낸다 (§41).
|
|
4613
|
+
case 'finish': {
|
|
4614
|
+
const session = await currentSession();
|
|
4615
|
+
if (!session) {
|
|
4616
|
+
console.error('Which session? `asc work finish <S-ID> --verified <text> --next <text>`');
|
|
4617
|
+
return 2;
|
|
4618
|
+
}
|
|
4619
|
+
const done = await runSession('done', session, values, store, runtime);
|
|
4620
|
+
if (done !== 0)
|
|
4621
|
+
return done;
|
|
4622
|
+
// handoff 가 쓰였으면 거두는 것까지가 이 명령의 몫이다 — 상태·차단 해제·보관.
|
|
4623
|
+
return runController('collect', values, store, runtime);
|
|
4624
|
+
}
|
|
4625
|
+
// 밖으로 내보낸다. Grant 를 없애는 것이 아니라 그 위에 서는 공식 표면이다 (§42).
|
|
4626
|
+
//
|
|
4627
|
+
// 순서가 계약이다 (0.8.0 §D):
|
|
4628
|
+
//
|
|
4629
|
+
// 읽기만 하는 원격 검수 → 결정권 → Grant → 원자적 CLAIM → 실행 직전 재검수
|
|
4630
|
+
// → 외부 변경 한 번 → 되돌려 읽기 → 감사
|
|
4631
|
+
//
|
|
4632
|
+
// 앞의 검수는 **승인을 다시 받는 자리가 아니다** (§I). 사람이 "게시해" 라고 한 것은
|
|
4633
|
+
// 결정권을 이미 해결했다. 여기서 보는 것은 사실이다 — 그 대상이 이 결합의 원격인지,
|
|
4634
|
+
// 승인한 commit 이 아직 그 commit 인지, 같은 것이 이미 올라가 있지는 않은지.
|
|
4635
|
+
case 'publish': {
|
|
4636
|
+
if (!values.action || !values.target) {
|
|
4637
|
+
console.error('Usage: asc work publish [S-ID] --action <key> --target <ref> --body-file <path> --as <actor>');
|
|
4638
|
+
console.error(' asc work publish … --review # read the facts, change nothing');
|
|
4639
|
+
return 2;
|
|
4640
|
+
}
|
|
4641
|
+
const outward = await externalWritePort(runtime);
|
|
4642
|
+
if (!outward) {
|
|
4643
|
+
console.error('밖으로 내보낼 통로가 없다 — 이 행위를 수행할 결합이 Profile 에 없다.');
|
|
4644
|
+
console.error('지금 무엇이 풀리는지: asc status');
|
|
4645
|
+
return 2;
|
|
4646
|
+
}
|
|
4647
|
+
const payload = values['body-file']
|
|
4648
|
+
? await readFile(values['body-file'], 'utf8').catch(() => null)
|
|
4649
|
+
: '';
|
|
4650
|
+
if (payload === null) {
|
|
4651
|
+
console.error(`내용을 읽지 못했다: ${String(values['body-file'])}`);
|
|
4652
|
+
return 2;
|
|
4653
|
+
}
|
|
4654
|
+
const action = { action: values.action, target: values.target, payload };
|
|
4655
|
+
// ① 읽기만 하는 검수. MANUAL 이든 AUTO 든 같은 판정이고, 화면만 다르다 (§E).
|
|
4656
|
+
const bound = bindingIdentity(runtime, outward.id);
|
|
4657
|
+
const facts = outward.review
|
|
4658
|
+
? await outward.review(action)
|
|
4659
|
+
: {
|
|
4660
|
+
provider: outward.id,
|
|
4661
|
+
capability: outward.supports?.(action.action) ?? true,
|
|
4662
|
+
verifiable: outward.verifies?.(action.action) ?? false,
|
|
4663
|
+
unknown: ['this write path cannot be read before use'],
|
|
4664
|
+
};
|
|
4665
|
+
const enforcing = enforcementOf(await readExecutionMode(store.scope('policy'))) === 'ENFORCE';
|
|
4666
|
+
const review = reviewExternalAction({
|
|
4667
|
+
action: action.action,
|
|
4668
|
+
target: action.target,
|
|
4669
|
+
facts,
|
|
4670
|
+
...(bound ? { basis: { resource: bound } } : {}),
|
|
4671
|
+
...(enforcing ? { requireVerification: true } : {}),
|
|
4672
|
+
});
|
|
4673
|
+
for (const line of reviewLines(review))
|
|
4674
|
+
console.log(line);
|
|
4675
|
+
for (const [key, value] of Object.entries(facts.observed ?? {})) {
|
|
4676
|
+
if (value !== undefined)
|
|
4677
|
+
console.log(` ${key}: ${value}`);
|
|
4678
|
+
}
|
|
4679
|
+
// 읽기만 물었으면 여기서 끝이다 — 이 경로로는 아무것도 나가지 않는다.
|
|
4680
|
+
if (values.review)
|
|
4681
|
+
return review.verdict === 'NOT_EXECUTABLE' ? 1 : 0;
|
|
4682
|
+
if (review.verdict !== 'READY') {
|
|
4683
|
+
console.error('');
|
|
4684
|
+
console.error(review.verdict === 'NOT_EXECUTABLE'
|
|
4685
|
+
? 'This cannot go out as it stands. Nothing was sent.'
|
|
4686
|
+
: 'Facts here need a person to look — nothing was sent. Widen or correct the action, then run it again.');
|
|
4687
|
+
return 1;
|
|
4688
|
+
}
|
|
4689
|
+
const session = await currentSession();
|
|
4690
|
+
if (!session) {
|
|
4691
|
+
console.error('Which session? `asc work publish --session <S-ID> ...`');
|
|
4692
|
+
return 2;
|
|
4693
|
+
}
|
|
4694
|
+
if (!values['body-file'] || !values.as) {
|
|
4695
|
+
// **호출됐다는 사실이 승인이 아니다** (§R). 내보낼 내용은 사람이 준 것이어야 하고,
|
|
4696
|
+
// 누가 정했는지는 이름으로 남아야 한다. 그 둘이 없으면 Grant 는 만들어지지 않는다.
|
|
4697
|
+
console.error('--body-file <path> 와 --as <actor> 가 필요하다 — 내보낼 내용과 그것을 정한 사람이다.');
|
|
4698
|
+
console.error('Agent 가 스스로 부른 것은 승인이 아니다.');
|
|
4699
|
+
return 2;
|
|
4700
|
+
}
|
|
4701
|
+
// ② 승인이 딛고 선 사실을 못 박는다 (§L). 가지 이름이 아니라 그때의 commit 이다.
|
|
4702
|
+
const basis = {
|
|
4703
|
+
...(facts.observed?.['local.head'] ? { sourceSha: facts.observed['local.head'] } : {}),
|
|
4704
|
+
...(facts.observed?.['remote.sha'] ? { remoteBaseline: facts.observed['remote.sha'] } : {}),
|
|
4705
|
+
...(bound ? { resource: bound } : {}),
|
|
4706
|
+
};
|
|
4707
|
+
const grantId = values['grant-id'] ?? `G-${String(Date.now()).slice(-4)}`;
|
|
4708
|
+
const issued = await runGrant('issue', undefined, { ...values, session, 'grant-id': grantId, basis }, store, root, runtime);
|
|
4709
|
+
if (issued !== 0)
|
|
4710
|
+
return issued;
|
|
4711
|
+
// ③ Grant → CLAIM → 재검수 → 실행 1회 → 되돌려 읽기 → 감사. 한 번 쓰고 소진된다.
|
|
4712
|
+
return runGrant('run', grantId, values, store, root, runtime);
|
|
4713
|
+
}
|
|
4714
|
+
default:
|
|
4715
|
+
console.error(`Unknown work command: ${command ?? '(none)'}\n\n${USAGE}`);
|
|
4716
|
+
return 2;
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
3985
4719
|
async function runUpdate(command, values) {
|
|
3986
4720
|
if (command !== undefined && command !== 'check' && command !== 'plan') {
|
|
3987
4721
|
console.error(`Unknown update command: ${command}\n\n${USAGE}`);
|
|
@@ -4055,17 +4789,16 @@ async function applyUpdate(plan, values) {
|
|
|
4055
4789
|
console.log(`installed: ${RUNTIME_PACKAGE}@${target}`);
|
|
4056
4790
|
let worst = 0;
|
|
4057
4791
|
// host 설치물은 버전마다 내용이 바뀐다. 새 runtime 에 낡은 hook 을 남기지 않는다.
|
|
4058
|
-
//
|
|
4059
|
-
//
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
worst = Math.max(worst, await convergeService(values));
|
|
4792
|
+
//
|
|
4793
|
+
// **새로 설치된 build 가 자기 내용을 쓴다.** 이 프로세스는 아직 갈아 끼우기 **전의**
|
|
4794
|
+
// build 이고, 그 build 의 `hookScript()` 는 옛 내용을 만든다 — 실기계에서 update 가
|
|
4795
|
+
// "host: …/SKILL.md" 를 적고 끝났는데 probe 는 여전히 INSTALLED_STALE 이었다. 갱신했다고
|
|
4796
|
+
// 말하면서 옛 내용을 다시 쓴 것이다.
|
|
4797
|
+
//
|
|
4798
|
+
// 사람이 고친 것을 덮지 않는 규칙은 그쪽(`host claude install`)이 그대로 진다.
|
|
4799
|
+
// 새 build 가 자기 integration 을 맞춘다 — 그것이 `asc refresh` 이고, update 는 그것을
|
|
4800
|
+
// 부를 뿐이다 (§25). 두 명령이 각자 host 를 갱신하면 언젠가 서로 다른 것을 쓴다.
|
|
4801
|
+
worst = Math.max(worst, await refreshWithNewRuntime());
|
|
4069
4802
|
// 마지막은 언제나 확인이다.
|
|
4070
4803
|
const health = await detectStableInstall(nodeProcessRunner, target);
|
|
4071
4804
|
if (health.status !== 'CURRENT') {
|
|
@@ -4123,6 +4856,38 @@ function diffState(before, after) {
|
|
|
4123
4856
|
changed.push(path);
|
|
4124
4857
|
return changed.sort();
|
|
4125
4858
|
}
|
|
4859
|
+
/**
|
|
4860
|
+
* 새 build 에게 자기 integration 을 맞추게 한다 — 곧 새 build 의 `asc refresh` 다 (§25).
|
|
4861
|
+
*
|
|
4862
|
+
* 지금 도는 프로세스로 부르지 않는 이유는 하나다: 이 프로세스는 교체되기 **전의** build 이고,
|
|
4863
|
+
* 그 build 가 만들어 내는 hook·skill 은 옛 내용이다. 0.7.0 에서 실제로 그랬다 — update 가
|
|
4864
|
+
* "host: …/SKILL.md" 를 적고 끝났는데 probe 는 여전히 INSTALLED_STALE 이었다.
|
|
4865
|
+
*
|
|
4866
|
+
* 전역 실행물을 못 찾으면 갱신하지 않고 그 사실을 말한다. 조용히 건너뛰면 낡은 hook 이
|
|
4867
|
+
* 새 runtime 옆에 남고, 그 조합은 아무도 시험한 적이 없다.
|
|
4868
|
+
*/
|
|
4869
|
+
async function refreshWithNewRuntime() {
|
|
4870
|
+
const entry = await globalRuntimeEntry();
|
|
4871
|
+
if (!entry) {
|
|
4872
|
+
console.error('refresh: could not find the installed runtime to refresh with — run `asc refresh`');
|
|
4873
|
+
return 1;
|
|
4874
|
+
}
|
|
4875
|
+
const child = spawnSync(process.execPath, [entry, 'refresh'], { encoding: 'utf8' });
|
|
4876
|
+
const output = `${child.stdout ?? ''}${child.stderr ?? ''}`;
|
|
4877
|
+
for (const line of output.split('\n')) {
|
|
4878
|
+
if (line.startsWith('installed:') ||
|
|
4879
|
+
line.startsWith('skipped:') ||
|
|
4880
|
+
line.startsWith('service:') ||
|
|
4881
|
+
line.startsWith('Install state:')) {
|
|
4882
|
+
console.log(`refresh: ${line}`);
|
|
4883
|
+
}
|
|
4884
|
+
}
|
|
4885
|
+
if (child.status !== 0) {
|
|
4886
|
+
console.error(`refresh: failed${output.trim() ? ` — ${output.trim().split('\n').at(-1)}` : ''}`);
|
|
4887
|
+
return 1;
|
|
4888
|
+
}
|
|
4889
|
+
return 0;
|
|
4890
|
+
}
|
|
4126
4891
|
/**
|
|
4127
4892
|
* 되돌린다. **되돌릴 자리가 있을 때만** — 없던 것으로 되돌릴 수는 없고, 그때는 돌던 것이
|
|
4128
4893
|
* 없었다는 사실을 그대로 말한다.
|
|
@@ -4892,6 +5657,15 @@ async function runCoordinationPublish(values, store, resolved) {
|
|
|
4892
5657
|
objectId,
|
|
4893
5658
|
}));
|
|
4894
5659
|
const workReference = typeof values.work === 'string' ? values.work : undefined;
|
|
5660
|
+
// **밖을 바꾸기 전에 계약을 집는다** (0.8.0 §O). 예전에는 게시가 성공한 **뒤에**
|
|
5661
|
+
// CLAIM 했다 — 그 사이에 다른 Run 이 같은 계약으로 들어오면 같은 글이 두 번 나갈 수
|
|
5662
|
+
// 있었다. 원자적 CLAIM 을 통과한 하나만 게시로 넘어간다. 게시가 실패하면 그 계약은
|
|
5663
|
+
// 태워진다: 같은 승인으로 다시 시도하지 않는 것이 이 계약의 뜻이다.
|
|
5664
|
+
const claimed = await applyTransition(store, 'grant', grantId, (g) => transitionGrant(g, 'CLAIMED', 'executor', { claimedBy: `cli-${process.pid}` }));
|
|
5665
|
+
if (!claimed.ok) {
|
|
5666
|
+
console.error(`${grantId} 를 집지 못했다 — 다른 Run 이 이미 집었거나 상태가 움직였다.`);
|
|
5667
|
+
return 1;
|
|
5668
|
+
}
|
|
4895
5669
|
const outcome = await publishOnce({
|
|
4896
5670
|
queryId,
|
|
4897
5671
|
publicPayload: { title, body },
|
|
@@ -4900,16 +5674,12 @@ async function runCoordinationPublish(values, store, resolved) {
|
|
|
4900
5674
|
...(workReference ? { workReference } : {}),
|
|
4901
5675
|
}, { surface, bindingRole: 'coordination-surface' });
|
|
4902
5676
|
if (outcome.ok) {
|
|
4903
|
-
// 한 번 쓴 계약은 다시 쓰이지 않는다 (OM §11.5 single_use).
|
|
4904
|
-
// 뒤에 옮긴다 — 먼저 옮기면 실패한 게시가 계약만 태운다.
|
|
5677
|
+
// 한 번 쓴 계약은 다시 쓰이지 않는다 (OM §11.5 single_use).
|
|
4905
5678
|
const consumedAt = new Date().toISOString();
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
consumedAt,
|
|
4911
|
-
}));
|
|
4912
|
-
}
|
|
5679
|
+
await applyTransition(store, 'grant', grantId, (g) => transitionGrant(g, 'EXECUTED', 'executor', {
|
|
5680
|
+
resultRef: outcome.identity.objectId,
|
|
5681
|
+
consumedAt,
|
|
5682
|
+
}));
|
|
4913
5683
|
const recorded = await recordPublication(coordinationLedger(store), outcome);
|
|
4914
5684
|
if (values.json) {
|
|
4915
5685
|
console.log(JSON.stringify({ publish: outcome, recorded: recorded.ok }, null, 2));
|
|
@@ -4920,6 +5690,16 @@ async function runCoordinationPublish(values, store, resolved) {
|
|
|
4920
5690
|
console.log(recorded.ok ? 'recorded as communication evidence' : 'already recorded');
|
|
4921
5691
|
return 0;
|
|
4922
5692
|
}
|
|
5693
|
+
// 집은 계약은 나가지 않았어도 소진된 것으로 닫는다 — 같은 승인으로 다시 시도하지
|
|
5694
|
+
// 않기 위해서다. 무엇이 실패했는지는 History 에 남는다.
|
|
5695
|
+
await applyTransition(store, 'grant', grantId, (g) => transitionGrant(g, 'INVALIDATED', 'executor'));
|
|
5696
|
+
await store.appendHistory({
|
|
5697
|
+
at: new Date().toISOString(),
|
|
5698
|
+
actor: `cli-${process.pid}`,
|
|
5699
|
+
kind: 'grant_invalidated',
|
|
5700
|
+
ref: grantId,
|
|
5701
|
+
detail: publishLine(outcome),
|
|
5702
|
+
});
|
|
4923
5703
|
if (values.json)
|
|
4924
5704
|
console.log(JSON.stringify({ publish: outcome }, null, 2));
|
|
4925
5705
|
else
|
|
@@ -5152,7 +5932,7 @@ async function runGrant(command, target, values, store, root, runtime) {
|
|
|
5152
5932
|
const outward = await externalWritePort(runtime);
|
|
5153
5933
|
if (!outward) {
|
|
5154
5934
|
console.error('밖으로 내보낼 통로가 없다 — 이 행위를 수행할 결합이 Profile 에 없다.');
|
|
5155
|
-
console.error('지금 무엇이 풀리는지: asc
|
|
5935
|
+
console.error('지금 무엇이 풀리는지: asc status');
|
|
5156
5936
|
return 2;
|
|
5157
5937
|
}
|
|
5158
5938
|
if (outward.supports && !outward.supports(values.action)) {
|
|
@@ -5162,6 +5942,16 @@ async function runGrant(command, target, values, store, root, runtime) {
|
|
|
5162
5942
|
}
|
|
5163
5943
|
// 발급도 승인 권한자만 할 수 있다 — 외부로 나가는 권한이 여기서 만들어지기 때문이다
|
|
5164
5944
|
const grants = new GrantService(store, new LocalIdentityBinding(await loadIdentityMap(root)));
|
|
5945
|
+
// **범위를 계약에 못 박는다** (0.8.0 보정 P1-3). 이 결합이 가리키는 원격이 곧 이
|
|
5946
|
+
// 승인의 실행 범위다 — 행위 하나를 승인했다는 사실이 다른 저장소까지 열어 주지
|
|
5947
|
+
// 않는다. 호출자가 이미 근거를 준 경우에는 그것을 그대로 둔다.
|
|
5948
|
+
const scoped = (() => {
|
|
5949
|
+
const given = values.basis;
|
|
5950
|
+
const bound = bindingIdentity(runtime, outward.id);
|
|
5951
|
+
if (given?.resource || !bound)
|
|
5952
|
+
return given;
|
|
5953
|
+
return { ...(given ?? {}), resource: bound };
|
|
5954
|
+
})();
|
|
5165
5955
|
if (fromSession) {
|
|
5166
5956
|
// 사람이 지금 내보내라고 한 것이 승인이다. 그 말과 함께 온 내용이 payload 이고,
|
|
5167
5957
|
// 여기서 지어내지 않는다 — 사람이 본 적 없는 글이 사람의 이름을 달고 나가면 안 된다.
|
|
@@ -5178,6 +5968,10 @@ async function runGrant(command, target, values, store, root, runtime) {
|
|
|
5178
5968
|
const forSession = await grants.issueForSession({
|
|
5179
5969
|
grantId: values['grant-id'] ?? `G-${String(Date.now()).slice(-4)}`,
|
|
5180
5970
|
sessionId: fromSession,
|
|
5971
|
+
// 검수가 읽어 온 사실을 승인에 못 박는다 (0.8.0 §L). 없으면 없는 대로 둔다 —
|
|
5972
|
+
// 없는 기준선을 지어내면 재검수가 아무것도 지키지 못한다. 범위(resource)만은
|
|
5973
|
+
// 결합에서 채운다: 그것이 이 승인이 미치는 곳의 경계다.
|
|
5974
|
+
...(scoped ? { basis: scoped } : {}),
|
|
5181
5975
|
issuedBy: values.as,
|
|
5182
5976
|
channel: 'local',
|
|
5183
5977
|
action: values.action,
|
|
@@ -5201,6 +5995,7 @@ async function runGrant(command, target, values, store, root, runtime) {
|
|
|
5201
5995
|
const issued = await grants.issue({
|
|
5202
5996
|
grantId: values['grant-id'] ?? `G-${String(Date.now()).slice(-4)}`,
|
|
5203
5997
|
requestId: target,
|
|
5998
|
+
...(scoped ? { basis: scoped } : {}),
|
|
5204
5999
|
issuedBy: values.as,
|
|
5205
6000
|
channel: 'local',
|
|
5206
6001
|
action: values.action,
|
|
@@ -5234,19 +6029,31 @@ async function runGrant(command, target, values, store, root, runtime) {
|
|
|
5234
6029
|
const scm = await externalWritePort(runtime);
|
|
5235
6030
|
if (!scm) {
|
|
5236
6031
|
console.error('밖으로 내보낼 통로가 없다 — Profile bindings 에 외부 쓰기를 제공하는 결합이 필요하다.');
|
|
5237
|
-
console.error('지금 무엇이 풀리는지: asc
|
|
6032
|
+
console.error('지금 무엇이 풀리는지: asc status');
|
|
5238
6033
|
return 2;
|
|
5239
6034
|
}
|
|
6035
|
+
// 강제가 서 있는 자리에서는 되돌려 읽을 수 없는 행위를 실행하지 않는다 (P1-2).
|
|
6036
|
+
const enforcing = enforcementOf(await readExecutionMode(store.scope('policy'))) === 'ENFORCE';
|
|
5240
6037
|
const outcome = await new Executor({
|
|
5241
6038
|
store,
|
|
5242
6039
|
scm,
|
|
5243
6040
|
runId: values['run-id'] ?? `cli-${process.pid}`,
|
|
6041
|
+
...(enforcing ? { requireVerification: true } : {}),
|
|
5244
6042
|
}).run(target);
|
|
5245
6043
|
if (outcome.ok) {
|
|
5246
6044
|
console.log(`EXECUTED — ${outcome.resultRef}`);
|
|
5247
6045
|
return 0;
|
|
5248
6046
|
}
|
|
6047
|
+
// 실패의 종류를 뭉개지 않는다 (0.8.0 §P) — 다음 행동이 저마다 다르다.
|
|
5249
6048
|
console.error(`${outcome.reason}${'detail' in outcome ? `: ${outcome.detail}` : ''}`);
|
|
6049
|
+
if (outcome.reason === 'UNCERTAIN') {
|
|
6050
|
+
console.error('밖에 나갔는지 알 수 없다. 다시 실행하지 마라 — 먼저 원격을 읽어 확인하고,');
|
|
6051
|
+
console.error('그 뒤에 사람이 새 Grant 를 낸다. 이 Grant 는 집힌 채로 남아 재사용되지 않는다.');
|
|
6052
|
+
}
|
|
6053
|
+
if (outcome.reason === 'NOT_VERIFIED') {
|
|
6054
|
+
console.error(`나간 것: ${outcome.resultRef} — 그러나 되돌려 읽은 것이 기대와 다르다.`);
|
|
6055
|
+
console.error('성공으로 적지 않는다. 원격을 직접 확인하라.');
|
|
6056
|
+
}
|
|
5250
6057
|
return 1;
|
|
5251
6058
|
}
|
|
5252
6059
|
default:
|
|
@@ -5254,6 +6061,17 @@ async function runGrant(command, target, values, store, root, runtime) {
|
|
|
5254
6061
|
return 2;
|
|
5255
6062
|
}
|
|
5256
6063
|
}
|
|
6064
|
+
/**
|
|
6065
|
+
* 이 작업이 가리키는 원격의 신원 (0.8.0 §K).
|
|
6066
|
+
*
|
|
6067
|
+
* **deny-list 가 아니다.** 검수가 "이 대상이 우리가 맡은 그 원격인가" 를 묻기 위한
|
|
6068
|
+
* 기준점이고, 어긋나면 Agent 가 스스로 범위를 넓히는 대신 사람에게 올라간다.
|
|
6069
|
+
* 결합이 여럿이면 고르지 않는다 — 고르는 순간 그것이 곧 조용한 범위 확장이다.
|
|
6070
|
+
*/
|
|
6071
|
+
function bindingIdentity(runtime, adapterId) {
|
|
6072
|
+
const declared = (runtime?.layers.profile.bindings ?? []).filter((binding) => binding.adapter === adapterId);
|
|
6073
|
+
return declared.length === 1 ? declared[0].resource : undefined;
|
|
6074
|
+
}
|
|
5257
6075
|
/**
|
|
5258
6076
|
* 승인된 행위가 실제로 나갈 통로 (C-09 · OM §11.5).
|
|
5259
6077
|
*
|
|
@@ -5261,6 +6079,16 @@ async function runGrant(command, target, values, store, root, runtime) {
|
|
|
5261
6079
|
* 수술이 된다. 없으면 `null` 이고, 없는 것을 있는 척하지 않는다.
|
|
5262
6080
|
*/
|
|
5263
6081
|
async function externalWritePort(runtime) {
|
|
6082
|
+
return (await composedPorts(runtime)).scm ?? null;
|
|
6083
|
+
}
|
|
6084
|
+
/**
|
|
6085
|
+
* 이 workspace 의 결합이 지금 실제로 조립되는가 (§62·§63).
|
|
6086
|
+
*
|
|
6087
|
+
* **선언이 아니라 조립 결과가 근거다.** 토큰 하나가 환경에 있다는 사실로 "외부 쓰기 가능"
|
|
6088
|
+
* 이라고 적으면, 그 표시는 실제 실행 경로와 어긋난 채로 사람을 안심시킨다 — 0.7 의 D-01 이
|
|
6089
|
+
* 그 형태였다. 여기서 나오는 것은 Composition 이 만든 Port 와, 만들지 못한 이유다.
|
|
6090
|
+
*/
|
|
6091
|
+
async function composedPorts(runtime) {
|
|
5264
6092
|
const { root: projectRoot } = await discoverProjectRoot(process.cwd());
|
|
5265
6093
|
const adapters = monitorAdapters();
|
|
5266
6094
|
const declared = runtime?.layers.profile.bindings ?? [];
|
|
@@ -5282,7 +6110,7 @@ async function externalWritePort(runtime) {
|
|
|
5282
6110
|
: {}),
|
|
5283
6111
|
endpointFor: (binding) => endpointOf(adapters, binding),
|
|
5284
6112
|
});
|
|
5285
|
-
return ports
|
|
6113
|
+
return ports;
|
|
5286
6114
|
}
|
|
5287
6115
|
/** `owner/repo#19` 에서 저장소만. 짧은 참조를 풀 때 쓴다. */
|
|
5288
6116
|
function repoOf(target) {
|
|
@@ -5293,7 +6121,7 @@ const GRANT_ERROR = {
|
|
|
5293
6121
|
REQUEST_NOT_FOUND: '요청을 찾지 못했다.',
|
|
5294
6122
|
NOT_APPROVED: '아직 승인되지 않은 요청이다. 승인 먼저 받아야 한다.',
|
|
5295
6123
|
FORBIDDEN_ISSUER: '계약을 발급할 권한이 없다. .asc/identities.json 에 `"이름": ["local:계정"]` 형태로 매핑을 추가하라 ' +
|
|
5296
|
-
'(현재 상태는 `asc
|
|
6124
|
+
'(현재 상태는 `asc status`).',
|
|
5297
6125
|
NO_PAYLOAD: '내보낼 내용이 없다.',
|
|
5298
6126
|
SESSION_NOT_FOUND: '그 세션을 찾지 못했다.',
|
|
5299
6127
|
SESSION_NOT_RUNNABLE: '아직 시작하지 않은 세션이다 — 내보낼 결과가 없다.',
|