@asc-agent/runtime 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@ proceed-by-default, escalation, audit, the external-write guard, host integratio
7
7
  here.
8
8
 
9
9
  ```bash
10
- npm install -g @asc-agent/runtime@0.2.0
10
+ npm install -g @asc-agent/runtime@0.2.1
11
11
  ```
12
12
 
13
13
  npm owns the executable link (on Windows, npm's own `asc.cmd`). This package never edits
@@ -13,6 +13,10 @@
13
13
  // 몫이고, skill에 복제되는 순간 두 곳이 서로 다른 정책을 말하기 시작한다.
14
14
  // skill이 정하는 것은 다섯뿐이다 — 언제 어떤 표면을 부르는가 / 무엇을 모으는가 /
15
15
  // 무엇을 직접 판단하면 안 되는가 / depth를 언제 올리는가 / 언제 돌려주는가.
16
+ // 버전은 **한 곳에서만** 온다. 손으로 적으면 릴리스마다 여기가 뒤처지고, 이 문자열은
17
+ // 사용자의 `~/.claude/skills/` 에 실제로 쓰이므로 그 지연이 사용자의 명령이 된다 —
18
+ // 0.2.0 회차에 이 파일이 `@0.1.0` 을 들고 있었다.
19
+ import { BOOTSTRAP_SPEC } from "../../core/distribution/release.js";
16
20
  /** 설치 단위. 파일이 늘어도 manifest 기반 설치·제거 계약은 그대로다 (C-05 §5). */
17
21
  export function skillBundle() {
18
22
  return [
@@ -45,8 +49,12 @@ The user does not need to know this sequence. This is the one the skill follows.
45
49
  \`\`\`text
46
50
  0. Is ASC even installed? If \`asc\` is not on PATH, this machine has no runtime yet.
47
51
  Start from the bootstrap, not from \`asc\`:
48
- npx --yes @asc-agent/bootstrap@0.1.0 setup --agent
52
+ npx --yes ${BOOTSTRAP_SPEC} setup apply --json
49
53
  Its JSON carries \`actions[].portable\` — run that, never \`display\`.
54
+ If the host refuses to run that command at all, that is a permission boundary, not an
55
+ ASC failure. Do not try other shapes of the same thing. Show the person the exact
56
+ command and ask them to switch this session's permission mode so they can approve the
57
+ single run — a standing allow rule for the command was measured and did not work.
50
58
 
51
59
  1. Attached? asc setup status
52
60
  not yet → asc init (it shows profile candidates; a person chooses)
package/dist/cli/asc.d.ts CHANGED
@@ -6,4 +6,10 @@
6
6
  * 설치하는 것은 말이 안 되고, 그것을 계획에 넣으면 매 setup이 npm을 부른다.
7
7
  */
8
8
  export type AscEntry = 'runtime' | 'bootstrap';
9
+ /**
10
+ * bootstrap이 자기 USAGE에 적을 값. **거기서 손으로 적지 않게 하려고 내보낸다** —
11
+ * 두 패키지가 각자 버전 문자열을 들면 릴리스마다 한쪽이 뒤처지고, 그 지연은 곧
12
+ * 사용자가 실행하는 명령이 된다 (0.2.0 회차의 skill.ts가 그랬다).
13
+ */
14
+ export { BOOTSTRAP_SPEC } from '../core/distribution/release.ts';
9
15
  export declare function runAscCommand(argv: string[], entry?: AscEntry): Promise<number>;
package/dist/cli/asc.js CHANGED
@@ -8,11 +8,12 @@
8
8
  // 읽기 전용이다. 결정 제출은 사람의 명시적 의사표현을 받는 별도 경로로 나간다 (B-06).
9
9
  import { execFile, spawnSync } from 'node:child_process';
10
10
  import { parseArgs, promisify } from 'node:util';
11
- import { existsSync, realpathSync } from 'node:fs';
11
+ import { existsSync, readdirSync, 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
15
  import { basename, dirname, join, resolve } from 'node:path';
16
+ import { MINIMUM_NODE_MAJOR, checkNodeRuntime, } from "../core/distribution/node-runtime.js";
16
17
  import { GitHubClient, discoverToken } from "../adapters/github/client.js";
17
18
  import { GitHubChangeContext, GitHubInventory, GitHubResourceContext } from "../adapters/github/context.js";
18
19
  import { GitHubEventSource } from "../adapters/github/event-source.js";
@@ -123,7 +124,7 @@ const USAGE = `asc — Agent Session Control
123
124
  asc setup plan [--profile <id>] [--scope local|project] [--json]
124
125
  # says what it would change — changes nothing
125
126
  asc setup apply [--profile <id>] [--scope local|project] [--json]
126
- asc setup --agent # non-interactive apply. stdout is a single JSON document
127
+ asc setup apply --json # non-interactive apply. stdout is a single JSON document
127
128
 
128
129
  asc session issue <ID> --role <role> --goal <text> [--block <id>]
129
130
  [--parent <S-ID>] [--issued-by <principal>]
@@ -297,6 +298,12 @@ const DECISION_ERROR = {
297
298
  ALREADY_DECIDED: '이미 결정된 요청이다.',
298
299
  STALE: '읽은 뒤 요청이 바뀌었다. 다시 확인하고 결정하라.',
299
300
  };
301
+ /**
302
+ * bootstrap이 자기 USAGE에 적을 값. **거기서 손으로 적지 않게 하려고 내보낸다** —
303
+ * 두 패키지가 각자 버전 문자열을 들면 릴리스마다 한쪽이 뒤처지고, 그 지연은 곧
304
+ * 사용자가 실행하는 명령이 된다 (0.2.0 회차의 skill.ts가 그랬다).
305
+ */
306
+ export { BOOTSTRAP_SPEC } from "../core/distribution/release.js";
300
307
  /**
301
308
  * CLI 한 번의 실행. **다른 진입도 이 함수를 부른다** (C-14 불변식 ①).
302
309
  *
@@ -332,6 +339,54 @@ function explainConfigError(error) {
332
339
  return `That profile is not valid JSON — ${error.message}`;
333
340
  return null;
334
341
  }
342
+ /** 실제 파일시스템·프로세스를 물린다. Core는 이 중 아무것도 직접 하지 않는다. */
343
+ function nodeRuntimeDeps() {
344
+ return {
345
+ version: process.version,
346
+ exists: (path) => existsSync(path),
347
+ list: (path) => {
348
+ try {
349
+ return readdirSync(path);
350
+ }
351
+ catch {
352
+ // 없는 디렉터리는 "후보 없음"이다. 이 machine에 그 배치가 없을 뿐이다.
353
+ return [];
354
+ }
355
+ },
356
+ run: nodeProcessRunner,
357
+ home: homedir(),
358
+ join,
359
+ };
360
+ }
361
+ /**
362
+ * 못 돌린다는 사실과, 이 machine에서 실제로 쓸 수 있는 것을 함께 준다.
363
+ *
364
+ * 후보가 있으면 **같은 canonical 명령을 그 Node로 돌리는 형태**를 낸다 — 이것은 per-invocation
365
+ * 환경변수이지 PATH·profile 수정이 아니다 (불변식 ⑰). 이 형태마저 host가 실행을 거부하면
366
+ * 그때는 ASC의 문제가 아니라 host 경계이며, AGENTS.md가 그 자리를 정의한다.
367
+ */
368
+ function reportNodeRuntime(check, asJson) {
369
+ const actions = check.candidates.map((candidate) => ({
370
+ type: 'use_node_runtime',
371
+ display: `PATH="${dirname(candidate.path)}:$PATH" ${shorthandCommand(['setup', 'apply', '--json'])}`,
372
+ portable: `PATH="${dirname(candidate.path)}:$PATH" ${portableCommand(['setup', 'apply', '--json'])}`,
373
+ node: candidate,
374
+ }));
375
+ if (asJson) {
376
+ console.log(JSON.stringify({
377
+ code: check.code,
378
+ detail: check.detail,
379
+ runtime: { node: process.execPath, version: check.version, required: `>=${MINIMUM_NODE_MAJOR}` },
380
+ candidates: check.candidates,
381
+ nextActions: actions.map((action) => action.portable),
382
+ actions,
383
+ }, null, 2));
384
+ return;
385
+ }
386
+ console.error(check.detail);
387
+ for (const action of actions)
388
+ console.error(` ${action.node.version} at ${action.node.path}\n ${action.display}`);
389
+ }
335
390
  export async function runAscCommand(argv, entry = 'runtime') {
336
391
  const { values, positionals } = parseArgs({
337
392
  args: argv,
@@ -437,6 +492,17 @@ export async function runAscCommand(argv, entry = 'runtime') {
437
492
  console.log(USAGE);
438
493
  return 0;
439
494
  }
495
+ // **지원 하한을 먼저 답한다** (C-14 §3). `engines` 는 npm에게 하는 말이라 기본값에서
496
+ // 경고로만 나가고, 그러면 "경고 뒤에 그래도 돌아감"이 된다 — 사용자는 자기가 지원
497
+ // 범위 안인지 끝내 모른다. 여기서 한 번, 결정적으로 답한다.
498
+ //
499
+ // 이 자리인 이유: 설치된 `asc` 와 bootstrap이 **같은 문으로 들어온다**. bootstrap에
500
+ // 두면 그쪽에 정책이 생기고(C-14 불변식 ⑦), 그러면 두 진입의 답이 갈릴 수 있다.
501
+ const runnable = await checkNodeRuntime(nodeRuntimeDeps());
502
+ if (!runnable.ok) {
503
+ reportNodeRuntime(runnable, Boolean(values.json) || Boolean(values.agent));
504
+ return 1;
505
+ }
440
506
  // 선택된 build로 넘길 것이 있으면 여기서 넘긴다. **선택 자체를 다루는 명령은 넘기지
441
507
  // 않는다** — 잘못 가리키는 선택을 고치거나 들여다보는 명령이 그 선택 때문에 못 돌면
442
508
  // 사람이 갇힌다.
@@ -920,7 +986,7 @@ function declaredPolicies(resolved) {
920
986
  }
921
987
  return declared;
922
988
  }
923
- const ASC_VERSION = '0.2.0';
989
+ const ASC_VERSION = '0.2.1';
924
990
  const CAPABILITIES = ['scm.github', 'state.markdown', 'approval.local'];
925
991
  const ADAPTER_VERSIONS = { 'scm.github': ASC_VERSION, 'state.markdown': ASC_VERSION };
926
992
  /**
@@ -1050,6 +1116,7 @@ async function detectSetupState(values, entry) {
1050
1116
  const scope = values.scope === 'project' ? 'project' : 'local';
1051
1117
  const hostReport = await verifyInstall(defaultPaths());
1052
1118
  return {
1119
+ entry,
1053
1120
  projectRoot,
1054
1121
  git,
1055
1122
  ...(ascRoot ? { ascRoot } : {}),
@@ -1293,10 +1360,12 @@ async function runProfileAdopt(values, entry) {
1293
1360
  // 다음 한 걸음은 두 형태로 준다 — agent는 portable, 사람은 display (C-14 불변식 ⑯).
1294
1361
  // 여기서는 설치 상태를 다시 관측하지 않는다: 이 명령이 도는 방식이 곧 그 답이다.
1295
1362
  const args = ['setup', 'apply', '--profile', adopted.id];
1363
+ // portable은 agent가 그대로 실행한다 — 기계가 읽는 형태로 끝난다 (setup-plan.ts와 같은 규칙).
1364
+ const machine = [...args, '--json'];
1296
1365
  const action = {
1297
1366
  type: 'apply_setup',
1298
1367
  display: shorthandCommand(args),
1299
- portable: entry === 'bootstrap' ? portableCommand(args) : shorthandCommand(args),
1368
+ portable: entry === 'bootstrap' ? portableCommand(machine) : shorthandCommand(machine),
1300
1369
  };
1301
1370
  if (asJson) {
1302
1371
  console.log(JSON.stringify({ id: adopted.id, path, project: parsed.data.project, warnings: adopted.warnings, nextActions: [action.portable], actions: [action] }, null, 2));
@@ -1,6 +1,15 @@
1
1
  import type { StableInstallState } from '../distribution/runtime-install.ts';
2
2
  /** 세상의 스냅샷. 읽기만 해서 만든다. */
3
3
  export type SetupState = {
4
+ /**
5
+ * 이 판단이 **어느 실행물 안에서** 나오는가 (C-14 §3.4).
6
+ *
7
+ * `runtime` 이면 지금 도는 것이 곧 설치된 `asc` 다 — 그 사실은 관측할 것이 아니라
8
+ * 이미 아는 것이다. 예전에는 이 축이 없어 `stableRuntime` 의 부재를 "설치 안 됨"으로
9
+ * 읽었고, 그래서 설치된 `asc` 가 자기를 bootstrap이라고 말하며 agent에게 `npx …` 를
10
+ * 돌려줬다 (v0.2.0 registry 관측). 진입점 자체가 이미 답의 일부다.
11
+ */
12
+ entry: 'runtime' | 'bootstrap';
4
13
  projectRoot: string;
5
14
  git: boolean;
6
15
  /** 이미 붙어 있으면 그 runtime 뿌리. 없으면 안 붙은 것이다. */
@@ -22,11 +22,22 @@ export function computeSetupPlan(state) {
22
22
  ];
23
23
  // 지금 명령이 어디서 도는가. 설치된 `asc` 가 없으면 bootstrap이고, 그때 agent에게
24
24
  // `asc …` 를 실행하라고 주면 안 된다 (C-14 §3.4 · 불변식 ⑯).
25
- const mode = state.stableRuntime?.status === 'CURRENT' ? 'installed-runtime' : 'bootstrap';
26
- const command = (args) => ({
27
- display: shorthandCommand(args),
28
- portable: mode === 'installed-runtime' ? shorthandCommand(args) : portableCommand(args),
29
- });
25
+ //
26
+ // **진입점이 먼저다.** `asc` 들어왔다면 그 `asc` 는 이미 이 machine에 있다 — 그것을
27
+ // npm에게 물어볼 이유가 없다. bootstrap으로 들어왔을 때만 설치 축이 판정에 쓰인다.
28
+ const mode = state.entry === 'runtime' || state.stableRuntime?.status === 'CURRENT'
29
+ ? 'installed-runtime'
30
+ : 'bootstrap';
31
+ const command = (args) => {
32
+ // portable은 **agent가 그대로 실행하는 것**이므로 기계가 읽을 수 있는 형태로 끝나야
33
+ // 한다. `--json` 이 빠져 있으면 agent는 자기가 실행한 명령의 답을 산문으로 받는다 —
34
+ // "산문을 파싱하지 마라"고 적어 놓고 산문을 주는 꼴이었다. display는 사람 형태 그대로.
35
+ const machine = args.includes('--json') ? args : [...args, '--json'];
36
+ return {
37
+ display: shorthandCommand(args),
38
+ portable: mode === 'installed-runtime' ? shorthandCommand(machine) : portableCommand(machine),
39
+ };
40
+ };
30
41
  const changes = [];
31
42
  // stable runtime은 **프로젝트와도, profile 선택과도 무관하다.** 사람이 profile을
32
43
  // 고르는 중이어도 이 설치는 안전한 준비이고, 그것 때문에 통째로 WAIT 하지 않는다
@@ -0,0 +1,44 @@
1
+ import type { ProcessRunner } from './runtime-install.ts';
2
+ /** ASC가 요구하는 Node 하한. `engines` 와 같은 값이며 어긋나면 release:check가 잡는다. */
3
+ export declare const MINIMUM_NODE_MAJOR = 24;
4
+ export type NodeCandidate = {
5
+ path: string;
6
+ version: string;
7
+ };
8
+ export type NodeRuntimeCheck = {
9
+ ok: true;
10
+ version: string;
11
+ } | {
12
+ ok: false;
13
+ code: 'NODE_RUNTIME_REQUIRED';
14
+ version: string;
15
+ detail: string;
16
+ /**
17
+ * 이 machine에서 찾은, 하한을 넘는 Node. **비어 있는 것이 실패가 아니다** —
18
+ * 그때는 Node를 놓는 일 자체가 사람의 경계다.
19
+ */
20
+ candidates: NodeCandidate[];
21
+ };
22
+ export type NodeRuntimeDeps = {
23
+ /** 지금 이 프로세스의 Node 버전 (`process.version` 형태: `v22.23.2`). */
24
+ version: string;
25
+ /** 후보 경로가 실제로 있는가. */
26
+ exists: (path: string) => boolean;
27
+ /** 디렉터리 목록. 없으면 빈 배열 — 없는 것은 오류가 아니다. */
28
+ list: (path: string) => string[];
29
+ /** `<node> -v` 를 돌린다. 실패는 후보 탈락일 뿐이다. */
30
+ run: ProcessRunner;
31
+ /** 사용자 홈. nvm 배치를 찾는 데만 쓴다. */
32
+ home: string;
33
+ /** 경로를 잇는다 — 호출자가 `node:path` 를 준다 (core는 파일시스템을 모른다). */
34
+ join: (...parts: string[]) => string;
35
+ };
36
+ /** `v22.23.2` · `22.23.2` 둘 다 받는다. 못 읽으면 `null` — 추측하지 않는다. */
37
+ export declare function majorOf(version: string): number | null;
38
+ /**
39
+ * 돌릴 수 있는가, 못 돌린다면 무엇이 있는가.
40
+ *
41
+ * 하한을 넘으면 **아무것도 하지 않는다** — 정상 경로에 I/O를 얹지 않는다. 못 넘을 때만
42
+ * 후보를 찾고, 후보에 대해서만 `-v` 를 묻는다.
43
+ */
44
+ export declare function checkNodeRuntime(deps: NodeRuntimeDeps): Promise<NodeRuntimeCheck>;
@@ -0,0 +1,75 @@
1
+ // 이 프로세스를 돌리고 있는 Node가 ASC를 돌릴 수 있는가 (C-14 §3).
2
+ //
3
+ // **왜 판정이 필요한가**: `engines` 는 npm에게 하는 말이고, npm은 기본값에서 그것을
4
+ // 경고로만 낸다. 그래서 Node 22에서 설치하면 "무서운 경고 한 줄 → 그래도 돌아감"이 되고,
5
+ // 사용자는 자기가 지원 범위 안에 있는지 알 수 없다. 지원 하한은 결정적으로 답해야 한다.
6
+ //
7
+ // **여기서 하지 않는 것**: Node를 설치하지 않고, PATH·shell 설정을 고치지 않으며,
8
+ // version manager를 다루지 않는다. 이미 이 machine에 있는 것을 **best-effort로 찾아
9
+ // 알려 줄** 뿐이다 — 못 찾는 것은 정상이고, 그때는 사람이 답할 일이다.
10
+ import { RELEASE_VERSION } from "./release.js";
11
+ /** ASC가 요구하는 Node 하한. `engines` 와 같은 값이며 어긋나면 release:check가 잡는다. */
12
+ export const MINIMUM_NODE_MAJOR = 24;
13
+ /** `v22.23.2` · `22.23.2` 둘 다 받는다. 못 읽으면 `null` — 추측하지 않는다. */
14
+ export function majorOf(version) {
15
+ const match = /^v?(\d+)\./.exec(version.trim());
16
+ return match ? Number(match[1]) : null;
17
+ }
18
+ /**
19
+ * 이미 있는 Node를 찾아본다. **탐색이지 관리가 아니다.**
20
+ *
21
+ * Homebrew와 nvm의 관례적 배치만 본다. 여기에 없다고 해서 Node가 없는 것은 아니고,
22
+ * 그 경우 후보 없이 돌려주는 것이 정직한 답이다 — 없는 것을 지어내지 않는다.
23
+ */
24
+ function candidatePaths(deps) {
25
+ const found = [];
26
+ // Homebrew: /opt/homebrew/opt/node@26/bin/node · /usr/local/opt/node/bin/node
27
+ for (const prefix of ['/opt/homebrew/opt', '/usr/local/opt']) {
28
+ for (const entry of deps.list(prefix)) {
29
+ if (!/^node(@\d+)?$/.test(entry))
30
+ continue;
31
+ const path = deps.join(prefix, entry, 'bin', 'node');
32
+ if (deps.exists(path))
33
+ found.push(path);
34
+ }
35
+ }
36
+ // nvm: ~/.nvm/versions/node/v24.1.0/bin/node
37
+ const nvm = deps.join(deps.home, '.nvm', 'versions', 'node');
38
+ for (const entry of deps.list(nvm)) {
39
+ const path = deps.join(nvm, entry, 'bin', 'node');
40
+ if (deps.exists(path))
41
+ found.push(path);
42
+ }
43
+ return [...new Set(found)];
44
+ }
45
+ /**
46
+ * 돌릴 수 있는가, 못 돌린다면 무엇이 있는가.
47
+ *
48
+ * 하한을 넘으면 **아무것도 하지 않는다** — 정상 경로에 I/O를 얹지 않는다. 못 넘을 때만
49
+ * 후보를 찾고, 후보에 대해서만 `-v` 를 묻는다.
50
+ */
51
+ export async function checkNodeRuntime(deps) {
52
+ const major = majorOf(deps.version);
53
+ if (major !== null && major >= MINIMUM_NODE_MAJOR)
54
+ return { ok: true, version: deps.version };
55
+ const candidates = [];
56
+ for (const path of candidatePaths(deps)) {
57
+ const probed = await deps.run(path, ['-v']);
58
+ if (!probed.ok)
59
+ continue;
60
+ const version = probed.stdout.trim().split(/\r?\n/)[0] ?? '';
61
+ const found = majorOf(version);
62
+ if (found !== null && found >= MINIMUM_NODE_MAJOR)
63
+ candidates.push({ path, version });
64
+ }
65
+ return {
66
+ ok: false,
67
+ code: 'NODE_RUNTIME_REQUIRED',
68
+ version: deps.version,
69
+ detail: `ASC ${RELEASE_VERSION} needs Node ${MINIMUM_NODE_MAJOR} or newer; this process is ${deps.version}.` +
70
+ (candidates.length > 0
71
+ ? ' A newer Node is already on this machine — use it for the same command.'
72
+ : ' No newer Node was found in the usual places. Installing one is yours to do.'),
73
+ candidates,
74
+ };
75
+ }
@@ -1,9 +1,9 @@
1
1
  export declare const RUNTIME_PACKAGE = "@asc-agent/runtime";
2
2
  export declare const BOOTSTRAP_PACKAGE = "@asc-agent/bootstrap";
3
3
  /** runtime과 bootstrap은 초기 release에서 lockstep이다. */
4
- export declare const RELEASE_VERSION = "0.2.0";
5
- export declare const RUNTIME_SPEC = "@asc-agent/runtime@0.2.0";
6
- export declare const BOOTSTRAP_SPEC = "@asc-agent/bootstrap@0.2.0";
4
+ export declare const RELEASE_VERSION = "0.2.1";
5
+ export declare const RUNTIME_SPEC = "@asc-agent/runtime@0.2.1";
6
+ export declare const BOOTSTRAP_SPEC = "@asc-agent/bootstrap@0.2.1";
7
7
  /**
8
8
  * 아직 설치되지 않은 machine에서 그대로 실행되는 형태 (C-14 §3.4).
9
9
  *
@@ -9,7 +9,7 @@
9
9
  export const RUNTIME_PACKAGE = '@asc-agent/runtime';
10
10
  export const BOOTSTRAP_PACKAGE = '@asc-agent/bootstrap';
11
11
  /** runtime과 bootstrap은 초기 release에서 lockstep이다. */
12
- export const RELEASE_VERSION = '0.2.0';
12
+ export const RELEASE_VERSION = '0.2.1';
13
13
  export const RUNTIME_SPEC = `${RUNTIME_PACKAGE}@${RELEASE_VERSION}`;
14
14
  export const BOOTSTRAP_SPEC = `${BOOTSTRAP_PACKAGE}@${RELEASE_VERSION}`;
15
15
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asc-agent/runtime",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "ASC (Agent Session Control) — local-first human-in-the-loop agent control plane: core, CLI, and adapters",
5
5
  "keywords": [
6
6
  "agent",