@asc-agent/runtime 0.1.0 → 0.2.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/dist/cli/asc.js CHANGED
@@ -12,7 +12,7 @@ import { existsSync, realpathSync } from 'node:fs';
12
12
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
13
13
  import { fileURLToPath } from 'node:url';
14
14
  import { homedir } from 'node:os';
15
- import { dirname, join, resolve } from 'node:path';
15
+ import { basename, dirname, join, resolve } from 'node:path';
16
16
  import { GitHubClient, discoverToken } from "../adapters/github/client.js";
17
17
  import { GitHubChangeContext, GitHubInventory, GitHubResourceContext } from "../adapters/github/context.js";
18
18
  import { GitHubEventSource } from "../adapters/github/event-source.js";
@@ -25,6 +25,7 @@ import { Executor } from "../core/execution/executor.js";
25
25
  import { GrantService } from "../core/execution/grant.js";
26
26
  import { DecisionKind } from "../core/model/entities.js";
27
27
  import { discoverProjectRoot, excludeFromGit, identitiesTemplate, overrideTemplate, writeIfAbsent } from "../core/attach/init.js";
28
+ import { AdoptError, buildAdoptedProfile } from "../core/attach/adopt.js";
28
29
  import { locatorsOf, lookupLocator, readIndex, register, writeIndex } from "../core/workspace/index-store.js";
29
30
  import { adoptionLine, judgeAdoption, migrate } from "../core/workspace/migrate.js";
30
31
  import { newWorkspaceId, normalizeRemote, recoverCandidates, recoverLines } from "../core/workspace/identity.js";
@@ -47,6 +48,7 @@ import { buildEventObservation } from "../composition/observe.js";
47
48
  import { availableProfiles, planBootstrap, renderPlan } from "../core/attach/bootstrap.js";
48
49
  import { detectStableInstall, installStableRuntime, verifyStableInstall, } from "../core/distribution/runtime-install.js";
49
50
  import { readRuntimeSelection, remediationAction, remediationLines, resolveRuntimeTarget, runtimeSelectionLine, selectionPath, writeRuntimeSelection, } from "../core/distribution/runtime-select.js";
51
+ import { portableCommand, shorthandCommand } from "../core/distribution/release.js";
50
52
  import { preflight } from "../core/operator/preflight.js";
51
53
  import { lookupAuthority } from "../core/policy/ownership.js";
52
54
  import { renderProgress } from "../core/operator/render.js";
@@ -66,137 +68,139 @@ import { FreezeLedger, freezeLines, judgeAction } from "../core/policy/remote-fr
66
68
  import { SessionRuntime } from "../core/runtime/session.js";
67
69
  import { Checkpoint, Handoff, SessionRole } from "../core/model/entities.js";
68
70
  import { archiveLock, bootstrapGuard, buildLock, compareLock, loadLayers, resolveRuntime } from "../core/resolver/load.js";
71
+ import { ProfileSourceError } from "../core/resolver/profile-source.js";
69
72
  import { renderAscMd, renderControllerMd } from "../core/resolver/render.js";
70
- import { ProfileLock } from "../schemas/profile.js";
73
+ import { ProfileLock, ProjectProfile } from "../schemas/profile.js";
71
74
  import { LocalOperator } from "../core/operator/local-operator.js";
72
75
  import { loadIdentityMap } from "./identity-config.js";
73
- const USAGE = `asc — Agent Session Control
74
-
75
- asc proceed [--session <id>] [--goal <text>] [--json]
76
-
77
- asc inbox list [--all] [--priority P0|P1|P2] [--json]
78
- asc inbox show <REQUEST_ID> [--json]
79
- asc inbox trace <REQUEST_ID> [--json] # how it got here — an exploratory trace
80
- asc inbox digest [--flush] [--json] # batched view (P0 stays separate)
81
- asc inbox latest [--priority P0|P1|P2] [--json]
82
- asc inbox decide <REQUEST_ID> <approve|revise|defer|dismiss|queue> --as <actor>
83
- [--revision <text>] [--expect <version>]
84
-
85
- asc grant issue <REQUEST_ID> --action <key> --target <ref> --as <actor>
86
- [--grant-id <id>] [--expires <iso>]
87
- asc grant run <GRANT_ID> [--run-id <id>]
88
-
89
- asc monitor scan [--backfill] [--as <controller>] # fast path
90
- asc monitor reconcile [--as <controller>] # recover what was missed — re-list
91
- asc monitor census [--as <controller>] # full reconcile + detect disappearances
92
- asc monitor status [--json] # how far coverage has been confirmed
93
-
94
- asc profile resolve --profile <id> [--preset <id>] [--install <path>] [--write]
95
-
96
- asc runtime start [--interval-min <n>] [--delta-min <n>] [--reconcile-min <n>]
97
- [--census-min <n>] [--digest-min <n>]
98
- asc runtime tick
99
- asc runtime status [--json] # which build is in use (C-14 §4)
100
- asc runtime use package
101
- asc runtime use development <checkout> # run a built checkout instead
102
- asc front [status] [--json]
103
- asc escalate open <S-ID> --predicate <p>... --question <t> --blocked <node>...
104
- --evidence <ref>... [--blocked-scope <path>...] [--previous <ESC-ID> --why <t>]
105
- asc escalate list
106
- asc escalate resolve <ESC-ID>
107
- asc freeze [status]
108
- asc freeze on --reason <text> [--offline]
109
- asc freeze defer --id <id> --intent <text> [--evidence <ref>]
110
- asc freeze release --id <id>
111
- asc thaw
112
- asc workspace list
113
- asc workspace migrate [--force]
114
- asc init [--profile <id>] [--preset <id>] [--install <path>]
115
- [--scope local|project] [--workspace <W-id>]
116
- # without --profile: report what was detected, then stop
117
-
118
- asc setup status [--json]
119
- asc setup plan [--profile <id>] [--scope local|project] [--json]
120
- # says what it would change — changes nothing
121
- asc setup apply [--profile <id>] [--scope local|project] [--json]
122
- asc setup --agent # non-interactive apply. stdout is a single JSON document
123
-
124
- asc session issue <ID> --role <role> --goal <text> [--block <id>]
125
- [--parent <S-ID>] [--issued-by <principal>]
126
- asc session pause <S-ID> --position <t> --next <t> [--physical <id>]
127
- [--judgment <t>] [--blocker <t>] [--risk <t>] [--evidence <ref>]
128
- asc session validate <target S-ID> --validator <validator S-ID> --result PASS|FAIL [--finding <t>]
129
- asc session audit <S-ID>
130
- asc session report <S-ID> [--json]
131
- asc session decision <S-ID> --class <c> --selected <t> --why <t>... --evidence <ref>...
132
- [--alternative <t>...] [--ownership <scope>...] [--verification <t>...]
133
- [--boundary <glob>...] [--exception <item>...]
134
- [--criteria <text>...] [--owner <role>]
135
- [--domain <decision-domain>...] [--authority <domain>=<role>...]
136
- [--dependency <text>...]
137
- asc session start <ID>
138
- asc session pause <ID> --position <text> --next <text> [--done <task>...]
139
- asc session resume <ID>
140
- asc session done <ID> --verified <text> --next <text> [--done <task>...]
141
- [--changed <path>...] [--unresolved <text>...]
142
- asc session list
143
-
144
- asc controller collect
145
-
146
- asc closure list [<S-ID>]
147
- asc closure confirm <S-ID> --item <id>...
148
-
149
- asc preflight --path <p>... (--role <r> | --session <S-ID>) [--json]
150
-
151
- asc query open <X-ID> --session <S-ID> --domain <decision-domain>
152
- --question <text> [--context <text>] [--default <text>]
153
- [--blocking <text>] [--expect-response DECIDE|ANSWER]
154
- [--in-reply-to <X-ID>]
155
- asc query answer <X-ID> --kind DECIDE|ANSWER|ESCALATE --by <role> --body <text>
156
- [--to <authority>]
157
- asc query list [--json]
158
-
159
- asc progress show [<S-ID>]
160
- asc progress report <S-ID> --physical <id> --phase <text>
161
- [--milestone <text>...] [--next <text>] [--unresolved <text>...]
162
- [--decision none|later|now] [--decision-ref <text>]
163
- [--verifier none|running|pass|fail] [--verifier-detail <text>] [--terminal]
164
-
165
- asc host claude install [--force] # --force: overwrite ASC files a person has edited
166
- asc host claude uninstall|probe [--report <cap>=<bool>...]
167
- asc host claude guard
168
- asc host claude bind <S-ID> --physical <id> [--principal <p>] [--worker <id>] [--kind <k>] [--force]
169
- asc host claude release <S-ID> --physical <id>
170
- asc host claude contract <S-ID>
171
-
172
- Options
173
- --root <path> runtime directory (otherwise: registered workspace, then repo-local .asc)
174
- --json machine-readable output
175
- --as <actor> who is deciding. Must be mapped as an approver
176
- --revision what was changed, when approving with revisions
177
- --expect the version you read. Rejected if it changed since
178
- --action action key to emit (e.g. github.issue_comment.create)
179
- --target target reference (e.g. owner/repo#19)
180
- --backfill sweep history on the first run (default: from now on)
181
- --profile Project Profile id
182
- --preset Operational Preset id
183
- --install ASC installation path (default: where this CLI lives)
184
- --write actually write the artefacts (default: preview)
185
- --role planner|researcher|implementer|verifier
186
- --goal the single goal of this session
187
- --boundary write scope (must be narrower than the Profile's)
188
- --exception SOFT DENY item allowed for this session only
189
- --criteria a verifiable done-criterion (repeatable)
190
- --path output path to check (preflight, repeatable)
191
- --item confirmed closure item id (closure confirm, repeatable)
192
- --phase one line on what is happening right now (progress report)
193
- --milestone a meaningfully finished chunk (repeatable)
194
- --decision does a person need to decide: none|later|now
195
- --verifier independent verification state: none|running|pass|fail
196
- --terminal final report stays as the closing screen after collect
197
-
198
- decide assumes a person is operating it, and only checks that the name given with --as
199
- is registered as an approver. Approval is not permission to publish: anything reaching an
76
+ const USAGE = `asc — Agent Session Control
77
+
78
+ asc proceed [--session <id>] [--goal <text>] [--json]
79
+
80
+ asc inbox list [--all] [--priority P0|P1|P2] [--json]
81
+ asc inbox show <REQUEST_ID> [--json]
82
+ asc inbox trace <REQUEST_ID> [--json] # how it got here — an exploratory trace
83
+ asc inbox digest [--flush] [--json] # batched view (P0 stays separate)
84
+ asc inbox latest [--priority P0|P1|P2] [--json]
85
+ asc inbox decide <REQUEST_ID> <approve|revise|defer|dismiss|queue> --as <actor>
86
+ [--revision <text>] [--expect <version>]
87
+
88
+ asc grant issue <REQUEST_ID> --action <key> --target <ref> --as <actor>
89
+ [--grant-id <id>] [--expires <iso>]
90
+ asc grant run <GRANT_ID> [--run-id <id>]
91
+
92
+ asc monitor scan [--backfill] [--as <controller>] # fast path
93
+ asc monitor reconcile [--as <controller>] # recover what was missed — re-list
94
+ asc monitor census [--as <controller>] # full reconcile + detect disappearances
95
+ asc monitor status [--json] # how far coverage has been confirmed
96
+
97
+ asc profile adopt [--id <name>] [--json] # make a profile for this repository
98
+ asc profile resolve --profile <id> [--preset <id>] [--install <path>] [--write]
99
+
100
+ asc runtime start [--interval-min <n>] [--delta-min <n>] [--reconcile-min <n>]
101
+ [--census-min <n>] [--digest-min <n>]
102
+ asc runtime tick
103
+ asc runtime status [--json] # which build is in use (C-14 §4)
104
+ asc runtime use package
105
+ asc runtime use development <checkout> # run a built checkout instead
106
+ asc front [status] [--json]
107
+ asc escalate open <S-ID> --predicate <p>... --question <t> --blocked <node>...
108
+ --evidence <ref>... [--blocked-scope <path>...] [--previous <ESC-ID> --why <t>]
109
+ asc escalate list
110
+ asc escalate resolve <ESC-ID>
111
+ asc freeze [status]
112
+ asc freeze on --reason <text> [--offline]
113
+ asc freeze defer --id <id> --intent <text> [--evidence <ref>]
114
+ asc freeze release --id <id>
115
+ asc thaw
116
+ asc workspace list
117
+ asc workspace migrate [--force]
118
+ asc init [--profile <id>] [--preset <id>] [--install <path>]
119
+ [--scope local|project] [--workspace <W-id>]
120
+ # without --profile: report what was detected, then stop
121
+
122
+ asc setup status [--json]
123
+ asc setup plan [--profile <id>] [--scope local|project] [--json]
124
+ # says what it would change — changes nothing
125
+ asc setup apply [--profile <id>] [--scope local|project] [--json]
126
+ asc setup --agent # non-interactive apply. stdout is a single JSON document
127
+
128
+ asc session issue <ID> --role <role> --goal <text> [--block <id>]
129
+ [--parent <S-ID>] [--issued-by <principal>]
130
+ asc session pause <S-ID> --position <t> --next <t> [--physical <id>]
131
+ [--judgment <t>] [--blocker <t>] [--risk <t>] [--evidence <ref>]
132
+ asc session validate <target S-ID> --validator <validator S-ID> --result PASS|FAIL [--finding <t>]
133
+ asc session audit <S-ID>
134
+ asc session report <S-ID> [--json]
135
+ asc session decision <S-ID> --class <c> --selected <t> --why <t>... --evidence <ref>...
136
+ [--alternative <t>...] [--ownership <scope>...] [--verification <t>...]
137
+ [--boundary <glob>...] [--exception <item>...]
138
+ [--criteria <text>...] [--owner <role>]
139
+ [--domain <decision-domain>...] [--authority <domain>=<role>...]
140
+ [--dependency <text>...]
141
+ asc session start <ID>
142
+ asc session pause <ID> --position <text> --next <text> [--done <task>...]
143
+ asc session resume <ID>
144
+ asc session done <ID> --verified <text> --next <text> [--done <task>...]
145
+ [--changed <path>...] [--unresolved <text>...]
146
+ asc session list
147
+
148
+ asc controller collect
149
+
150
+ asc closure list [<S-ID>]
151
+ asc closure confirm <S-ID> --item <id>...
152
+
153
+ asc preflight --path <p>... (--role <r> | --session <S-ID>) [--json]
154
+
155
+ asc query open <X-ID> --session <S-ID> --domain <decision-domain>
156
+ --question <text> [--context <text>] [--default <text>]
157
+ [--blocking <text>] [--expect-response DECIDE|ANSWER]
158
+ [--in-reply-to <X-ID>]
159
+ asc query answer <X-ID> --kind DECIDE|ANSWER|ESCALATE --by <role> --body <text>
160
+ [--to <authority>]
161
+ asc query list [--json]
162
+
163
+ asc progress show [<S-ID>]
164
+ asc progress report <S-ID> --physical <id> --phase <text>
165
+ [--milestone <text>...] [--next <text>] [--unresolved <text>...]
166
+ [--decision none|later|now] [--decision-ref <text>]
167
+ [--verifier none|running|pass|fail] [--verifier-detail <text>] [--terminal]
168
+
169
+ asc host claude install [--force] # --force: overwrite ASC files a person has edited
170
+ asc host claude uninstall|probe [--report <cap>=<bool>...]
171
+ asc host claude guard
172
+ asc host claude bind <S-ID> --physical <id> [--principal <p>] [--worker <id>] [--kind <k>] [--force]
173
+ asc host claude release <S-ID> --physical <id>
174
+ asc host claude contract <S-ID>
175
+
176
+ Options
177
+ --root <path> runtime directory (otherwise: registered workspace, then repo-local .asc)
178
+ --json machine-readable output
179
+ --as <actor> who is deciding. Must be mapped as an approver
180
+ --revision what was changed, when approving with revisions
181
+ --expect the version you read. Rejected if it changed since
182
+ --action action key to emit (e.g. github.issue_comment.create)
183
+ --target target reference (e.g. owner/repo#19)
184
+ --backfill sweep history on the first run (default: from now on)
185
+ --profile Project Profile id
186
+ --preset Operational Preset id
187
+ --install ASC installation path (default: where this CLI lives)
188
+ --write actually write the artefacts (default: preview)
189
+ --role planner|researcher|implementer|verifier
190
+ --goal the single goal of this session
191
+ --boundary write scope (must be narrower than the Profile's)
192
+ --exception SOFT DENY item allowed for this session only
193
+ --criteria a verifiable done-criterion (repeatable)
194
+ --path output path to check (preflight, repeatable)
195
+ --item confirmed closure item id (closure confirm, repeatable)
196
+ --phase one line on what is happening right now (progress report)
197
+ --milestone a meaningfully finished chunk (repeatable)
198
+ --decision does a person need to decide: none|later|now
199
+ --verifier independent verification state: none|running|pass|fail
200
+ --terminal final report — stays as the closing screen after collect
201
+
202
+ decide assumes a person is operating it, and only checks that the name given with --as
203
+ is registered as an approver. Approval is not permission to publish: anything reaching an
200
204
  external system goes out through a separate Execution Grant.`;
201
205
  /**
202
206
  * 지금 여기가 어느 ASC runtime인가. **모든 명령이 같은 문을 지난다** (C-11 §3, B-45).
@@ -300,6 +304,34 @@ const DECISION_ERROR = {
300
304
  * 진입이 둘이어도 판단은 하나다. 그래서 export이고, 그래서 아래 자동 실행은 이 파일이
301
305
  * 진짜 진입점일 때만 돈다.
302
306
  */
307
+ /**
308
+ * 설정을 읽다 실패한 것을 **사람이 읽을 문장**으로 바꾼다.
309
+ *
310
+ * 여기가 없으면 Profile 하나가 잘못됐을 때 사용자가 보는 것은 Node의 stack dump다 —
311
+ * 내부 파일 이름과 프레임이 줄줄이 나오고, 정작 "무엇을 고쳐야 하는지"는 없다.
312
+ * 독립 검증이 다섯 갈래(충돌·깨진 JSON·디렉터리가 아닌 것·EISDIR·긴 id)에서 같은 모양을
313
+ * 관측했다. 예상 못 한 오류는 그대로 던진다 — 삼키면 그게 더 나쁘다.
314
+ */
315
+ function explainConfigError(error) {
316
+ if (error instanceof ProfileSourceError)
317
+ return error.message;
318
+ const failure = error;
319
+ const path = failure?.path ? ` (${failure.path})` : '';
320
+ switch (failure?.code) {
321
+ case 'ENOENT':
322
+ return `That profile is not there${path}. \`asc setup status\` lists what is.`;
323
+ case 'EISDIR':
324
+ return `A profile has to be a file, and that is a directory${path}.`;
325
+ case 'EACCES':
326
+ case 'EPERM':
327
+ return `No permission to read that profile${path}.`;
328
+ default:
329
+ break;
330
+ }
331
+ if (error instanceof SyntaxError)
332
+ return `That profile is not valid JSON — ${error.message}`;
333
+ return null;
334
+ }
303
335
  export async function runAscCommand(argv, entry = 'runtime') {
304
336
  const { values, positionals } = parseArgs({
305
337
  args: argv,
@@ -427,6 +459,10 @@ export async function runAscCommand(argv, entry = 'runtime') {
427
459
  // 그러면 "아직 안 붙었다"를 확인하려고 부른 명령이 안 붙었다는 이유로 죽는다.
428
460
  if (group === 'setup')
429
461
  return runSetup(command, values, entry);
462
+ // adopt는 **붙기 전의 명령이다.** 붙을 Profile을 만드는 것이 일이므로 attach를 요구하면
463
+ // 순서가 뒤집힌다. 나머지 profile 명령은 아래 attach 경로에 그대로 남는다.
464
+ if (group === 'profile' && command === 'adopt')
465
+ return runProfileAdopt(values, entry);
430
466
  if (!['inbox', 'grant', 'monitor', 'runtime', 'front', 'freeze', 'thaw', 'escalate', 'profile', 'session', 'controller', 'proceed', 'progress', 'preflight', 'closure', 'query'].includes(group)) {
431
467
  console.error(`Unknown command: ${group}\n\n${USAGE}`);
432
468
  return 2;
@@ -616,7 +652,7 @@ async function attachLocalWorkspace(projectRoot, git, declaredWorkspace) {
616
652
  return existing.root;
617
653
  }
618
654
  const remotes = git ? await gitRemotes(projectRoot) : [];
619
- const aliases = remotes.map(normalizeRemote).filter((alias) => alias !== null);
655
+ const aliases = remoteAliases(remotes);
620
656
  // 사람이 "이건 그 프로젝트다"라고 말한 경우 — 이어붙인다. 추론이 아니라 선언이다.
621
657
  if (declaredWorkspace) {
622
658
  const known = index.workspaces[declaredWorkspace];
@@ -635,7 +671,7 @@ async function attachLocalWorkspace(projectRoot, git, declaredWorkspace) {
635
671
  console.log(`Registered this location under workspace ${known.workspaceId} — runtime lives at ${root}`);
636
672
  return root;
637
673
  }
638
- const hits = recoverCandidates(Object.values(index.workspaces), remotes);
674
+ const hits = recoverCandidates(Object.values(index.workspaces), aliases);
639
675
  if (hits.length > 0) {
640
676
  // 붙일지는 사람이 정한다. 여기서 이어붙이면 남의 workspace를 조용히 가져올 수 있다.
641
677
  for (const line of recoverLines(hits))
@@ -658,20 +694,33 @@ async function attachLocalWorkspace(projectRoot, git, declaredWorkspace) {
658
694
  console.log(`Recognisable as: ${aliases.join(', ')}`);
659
695
  return root;
660
696
  }
661
- /** 모든 remote를 evidence로 모은다. origin을 primary로 단정하지 않는다 (C-11 불변식 ④). */
697
+ /**
698
+ * 모든 remote를 evidence로 모은다. origin을 primary로 단정하지 않는다 (C-11 불변식 ④).
699
+ *
700
+ * 이름을 함께 든다 — identity alias는 이름이 필요 없지만, `profile adopt` 는 어느 remote가
701
+ * 이 프로젝트를 대표하는지 골라야 하고 `git remote -v` 의 출력 순서는 알파벳순이라
702
+ * "첫 줄이 origin"이 아니다.
703
+ */
662
704
  async function gitRemotes(projectRoot) {
663
705
  try {
664
706
  const { stdout } = await execFileAsync('git', ['-C', projectRoot, 'remote', '-v']);
665
- const urls = stdout
666
- .split(/\r?\n/)
667
- .map((line) => line.split(/\s+/)[1])
668
- .filter((url) => Boolean(url));
669
- return [...new Set(urls)];
707
+ const seen = new Map();
708
+ for (const line of stdout.split(/\r?\n/)) {
709
+ const [name, url] = line.split(/\s+/);
710
+ if (!name || !url || seen.has(`${name}${url}`))
711
+ continue;
712
+ seen.set(`${name}${url}`, { name, url });
713
+ }
714
+ return [...seen.values()];
670
715
  }
671
716
  catch {
672
717
  return [];
673
718
  }
674
719
  }
720
+ /** identity alias는 이름을 쓰지 않는다 — URL만 정규화한다. */
721
+ const remoteAliases = (remotes) => [...new Set(remotes.map((remote) => remote.url))]
722
+ .map(normalizeRemote)
723
+ .filter((alias) => alias !== null);
675
724
  /**
676
725
  * 저장소 안에 있던 `.asc/` 를 사용자 소유 공간으로 옮긴다 (C-11 §6).
677
726
  *
@@ -679,8 +728,8 @@ async function gitRemotes(projectRoot) {
679
728
  */
680
729
  async function runWorkspace(command, values) {
681
730
  if (command !== 'migrate' && command !== 'list') {
682
- console.error(`Unknown workspace command: ${command ?? '(none)'}
683
-
731
+ console.error(`Unknown workspace command: ${command ?? '(none)'}
732
+
684
733
  ${USAGE}`);
685
734
  return 2;
686
735
  }
@@ -726,7 +775,7 @@ ${USAGE}`);
726
775
  workspaceId,
727
776
  root: target,
728
777
  locator: { path: projectRoot, platform: process.platform, observedAt: new Date().toISOString() },
729
- aliases: remotes.map(normalizeRemote).filter((alias) => alias !== null),
778
+ aliases: remoteAliases(remotes),
730
779
  now: new Date().toISOString(),
731
780
  }));
732
781
  console.log(`Copied and verified ${outcome.plan.entries} entries into ${target} (workspace ${workspaceId}).`);
@@ -773,6 +822,7 @@ async function runInit(values) {
773
822
  const plan = await planBootstrap({
774
823
  cwd: process.cwd(),
775
824
  installRoot: installRoot(),
825
+ externalProfileRoot: externalProfileRoot(),
776
826
  ...(attachedRoot ? { ascRoot: attachedRoot } : {}),
777
827
  hosts: [{ id: 'claude', installed: await verifyInstalled(defaultPaths()) }],
778
828
  bindings,
@@ -788,6 +838,24 @@ async function runInit(values) {
788
838
  }
789
839
  const { root: projectRoot, git } = await discoverProjectRoot(process.cwd());
790
840
  console.log(`Project: ${projectRoot}${git ? '' : ' (not a git repository)'}`);
841
+ // **읽을 수 있는 Profile인지 먼저 본다.** 아래부터는 `.git/info/exclude` 를 고치고
842
+ // 템플릿을 만드는 등 세상을 바꾸는 일이고, 그 뒤에 Profile이 잘못된 것을 알면 반쯤 만든
843
+ // `.asc/` 가 남는다 — 독립 검증이 실제로 그 상태를 만들었다. 여기서 멈추면 아무것도 남지 않는다.
844
+ try {
845
+ await loadLayers({
846
+ installRoot: installRoot(),
847
+ externalProfileRoot: externalProfileRoot(),
848
+ profileId: values.profile,
849
+ });
850
+ }
851
+ catch (error) {
852
+ const explained = explainConfigError(error);
853
+ if (explained === null)
854
+ throw error;
855
+ console.error(explained);
856
+ console.error('Nothing was changed.');
857
+ return 2;
858
+ }
791
859
  const ascRoot = scope === 'project'
792
860
  ? join(projectRoot, '.asc')
793
861
  : await attachLocalWorkspace(projectRoot, git, values.workspace);
@@ -824,6 +892,7 @@ async function attachedRuntime(ascRoot) {
824
892
  const outcome = await bootstrapGuard({
825
893
  ascRoot,
826
894
  installRoot: installRoot(),
895
+ externalProfileRoot: externalProfileRoot(),
827
896
  capabilities: CAPABILITIES,
828
897
  adapters: ADAPTER_VERSIONS,
829
898
  ascVersion: ASC_VERSION,
@@ -851,7 +920,7 @@ function declaredPolicies(resolved) {
851
920
  }
852
921
  return declared;
853
922
  }
854
- const ASC_VERSION = '0.1.0';
923
+ const ASC_VERSION = '0.2.0';
855
924
  const CAPABILITIES = ['scm.github', 'state.markdown', 'approval.local'];
856
925
  const ADAPTER_VERSIONS = { 'scm.github': ASC_VERSION, 'state.markdown': ASC_VERSION };
857
926
  /**
@@ -985,7 +1054,7 @@ async function detectSetupState(values, entry) {
985
1054
  git,
986
1055
  ...(ascRoot ? { ascRoot } : {}),
987
1056
  ...(values.profile ? { requestedProfile: values.profile } : {}),
988
- profileCandidates: await availableProfiles(installRoot()),
1057
+ profileCandidates: await availableProfiles(installRoot(), externalProfileRoot()),
989
1058
  scope,
990
1059
  host: [{ id: 'claude', status: hostReport.status }],
991
1060
  // **bootstrap으로 들어왔을 때만 본다.** 설치된 runtime이 자기를 다시 설치할 이유가
@@ -1101,6 +1170,7 @@ async function inspectSetup(root) {
1101
1170
  const outcome = await bootstrapGuard({
1102
1171
  ascRoot: root,
1103
1172
  installRoot: installRoot(),
1173
+ externalProfileRoot: externalProfileRoot(),
1104
1174
  capabilities: CAPABILITIES,
1105
1175
  adapters: ADAPTER_VERSIONS,
1106
1176
  ascVersion: ASC_VERSION,
@@ -1115,6 +1185,9 @@ async function inspectSetup(root) {
1115
1185
  const runtime = outcome.ok ? outcome.runtime : undefined;
1116
1186
  return assessSetup({
1117
1187
  attachment,
1188
+ ...(runtime
1189
+ ? { profile: { id: runtime.layers.profile.id, origin: runtime.layers.profileOrigin } }
1190
+ : {}),
1118
1191
  hasApprovers: Object.keys(await loadIdentityMap(root)).length > 0,
1119
1192
  hasControllerIdentities: Object.keys(runtime?.controllerIdentities ?? {}).length > 0,
1120
1193
  hasMonitorIdentities: (runtime?.monitor.identities?.length ?? 0) > 0,
@@ -1129,10 +1202,18 @@ const hasToken = async () => (await discoverToken()) !== null;
1129
1202
  */
1130
1203
  /** 이 저장소(또는 설치된 패키지)의 뿌리. profiles/ · presets/ 를 여기서 읽는다. */
1131
1204
  const installRoot = () => join(dirname(fileURLToPath(import.meta.url)), '..');
1205
+ /**
1206
+ * 사용자 소유 Profile 디렉터리. 팀이 나눠 갖는 실 Profile이 여기 온다 —
1207
+ * 배포본에는 예시만 있고, 남의 프로젝트 설정은 패키지에 실리지 않는다.
1208
+ *
1209
+ * **경로를 아는 것은 Surface의 몫이다.** Core에 홈을 알려 주지 않는다 (C-11).
1210
+ */
1211
+ const externalProfileRoot = () => join(ascHome(), 'profiles');
1132
1212
  async function checkBootstrap(root) {
1133
1213
  const outcome = await bootstrapGuard({
1134
1214
  ascRoot: root,
1135
1215
  installRoot: installRoot(),
1216
+ externalProfileRoot: externalProfileRoot(),
1136
1217
  capabilities: CAPABILITIES,
1137
1218
  adapters: ADAPTER_VERSIONS,
1138
1219
  ascVersion: ASC_VERSION,
@@ -1160,6 +1241,74 @@ async function checkBootstrap(root) {
1160
1241
  console.error('\nOnce you have checked it, re-lock with `asc profile resolve --write`.');
1161
1242
  return { code: 2 };
1162
1243
  }
1244
+ /**
1245
+ * 지금 이 저장소를 설명하는 Profile을 사용자 소유 공간에 만든다 (P0).
1246
+ *
1247
+ * **이것이 되물음을 없앤다.** 배포본에 담긴 Profile은 예시뿐이고, 그래서 URL만 받은 agent는
1248
+ * `ASC_PROFILE_SELECTION_REQUIRED` 앞에서 고를 것이 없어 사람에게 물었다. 여기서 만드는 것은
1249
+ * git remote가 증명하는 사실뿐이다 — 정본 branch·role 경계·정책은 짓지 않는다 (adopt.ts 주석).
1250
+ *
1251
+ * 쓰는 곳은 `$ASC_HOME/profiles/<id>/` 이고 저장소는 건드리지 않는다. 이미 있으면 덮지 않고
1252
+ * 멈춘다 — 남이 쓰던 Profile을 조용히 갈아 끼우는 것이 이 명령의 일이 아니다.
1253
+ */
1254
+ async function runProfileAdopt(values, entry) {
1255
+ const asJson = Boolean(values.json) || Boolean(values.agent);
1256
+ const { root: projectRoot, git } = await discoverProjectRoot(process.cwd());
1257
+ const remotes = git ? await gitRemotes(projectRoot) : [];
1258
+ let adopted;
1259
+ try {
1260
+ adopted = buildAdoptedProfile({
1261
+ dirName: basename(projectRoot),
1262
+ remotes,
1263
+ // provider를 아는 것은 Adapter를 아는 이 층이다 (C-09 §6.1). host 하나로 단정하는
1264
+ // 것은 여기까지고, 모르는 host는 `git` 이라고만 적는다 — 그것이 사실이다.
1265
+ scmForHost: (host) => (host === 'github.com' ? 'github' : 'git'),
1266
+ ...(values.id ? { requestedId: values.id } : {}),
1267
+ });
1268
+ }
1269
+ catch (error) {
1270
+ if (!(error instanceof AdoptError))
1271
+ throw error;
1272
+ console.error(error.message);
1273
+ return 2;
1274
+ }
1275
+ // 스스로 만든 것이 스키마를 통과하는지 **쓰기 전에** 본다. 통과하지 못하는 파일을 놓고
1276
+ // 나가면 그 다음 명령이 남의 설정 오류처럼 죽는다 (95250da가 닫은 것과 같은 모양).
1277
+ const parsed = ProjectProfile.safeParse(adopted.profile);
1278
+ if (!parsed.success) {
1279
+ console.error(`Built a profile that ASC itself rejects — this is a bug in \`profile adopt\`:`);
1280
+ for (const issue of parsed.error.issues)
1281
+ console.error(` - ${issue.path.join('.')}: ${issue.message}`);
1282
+ return 1;
1283
+ }
1284
+ const dir = join(externalProfileRoot(), adopted.id);
1285
+ const path = join(dir, 'profile.json');
1286
+ if (existsSync(path)) {
1287
+ console.error(`A profile called '${adopted.id}' is already there: ${path}\n` +
1288
+ `Attach with it (\`asc setup apply --profile ${adopted.id}\`), or adopt under another name with --id <name>.`);
1289
+ return 1;
1290
+ }
1291
+ await mkdir(dir, { recursive: true });
1292
+ await writeFile(path, `${JSON.stringify(adopted.profile, null, 2)}\n`, 'utf8');
1293
+ // 다음 한 걸음은 두 형태로 준다 — agent는 portable, 사람은 display (C-14 불변식 ⑯).
1294
+ // 여기서는 설치 상태를 다시 관측하지 않는다: 이 명령이 도는 방식이 곧 그 답이다.
1295
+ const args = ['setup', 'apply', '--profile', adopted.id];
1296
+ const action = {
1297
+ type: 'apply_setup',
1298
+ display: shorthandCommand(args),
1299
+ portable: entry === 'bootstrap' ? portableCommand(args) : shorthandCommand(args),
1300
+ };
1301
+ if (asJson) {
1302
+ console.log(JSON.stringify({ id: adopted.id, path, project: parsed.data.project, warnings: adopted.warnings, nextActions: [action.portable], actions: [action] }, null, 2));
1303
+ return 0;
1304
+ }
1305
+ console.log(`Adopted ${projectRoot} as profile '${adopted.id}' — ${path}`);
1306
+ console.log(`Project: ${parsed.data.project.scm} ${parsed.data.project.repository}`);
1307
+ for (const warning of adopted.warnings)
1308
+ console.log(` note: ${warning}`);
1309
+ console.log(`\nAttach with it: ${action.display}`);
1310
+ return 0;
1311
+ }
1163
1312
  /**
1164
1313
  * 계층을 합쳐 산출물 셋을 만든다. 기본은 미리보기다 — 무엇이 바뀌는지 보고 나서 쓴다.
1165
1314
  * lock이 어긋나면 알리기만 하고 저절로 맞추지 않는다 (OM §4.9).
@@ -1176,6 +1325,7 @@ async function runProfile(command, values, root) {
1176
1325
  const installPath = values.install ?? installRoot();
1177
1326
  const layers = await loadLayers({
1178
1327
  installRoot: installPath,
1328
+ externalProfileRoot: externalProfileRoot(),
1179
1329
  profileId: values.profile,
1180
1330
  ...(values.preset ? { presetId: values.preset } : {}),
1181
1331
  overridePath: join(root, 'override.json'),
@@ -2724,15 +2874,15 @@ async function runFreeze(group, command, values, store) {
2724
2874
  console.log(removed ? `${id} removed from the list — execution goes through the approval path` : `${id} was not found`);
2725
2875
  return removed ? 0 : 1;
2726
2876
  }
2727
- console.error(`Unknown freeze command: ${command}
2728
-
2877
+ console.error(`Unknown freeze command: ${command}
2878
+
2729
2879
  ${USAGE}`);
2730
2880
  return 2;
2731
2881
  }
2732
2882
  async function runFront(command, values, store, root) {
2733
2883
  if (command !== undefined && command !== 'status') {
2734
- console.error(`Unknown front command: ${command}
2735
-
2884
+ console.error(`Unknown front command: ${command}
2885
+
2736
2886
  ${USAGE}`);
2737
2887
  return 2;
2738
2888
  }
@@ -2910,5 +3060,15 @@ const invokedDirectly = (() => {
2910
3060
  return false;
2911
3061
  }
2912
3062
  })();
2913
- if (invokedDirectly)
2914
- process.exitCode = await runAscCommand(process.argv.slice(2));
3063
+ if (invokedDirectly) {
3064
+ try {
3065
+ process.exitCode = await runAscCommand(process.argv.slice(2));
3066
+ }
3067
+ catch (error) {
3068
+ const explained = explainConfigError(error);
3069
+ if (explained === null)
3070
+ throw error; // 모르는 고장은 감추지 않는다
3071
+ console.error(explained);
3072
+ process.exitCode = 1;
3073
+ }
3074
+ }