@asc-agent/runtime 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/adapters/claude-code/skill.js +14 -0
- package/dist/adapters/local/repo.js +60 -1
- package/dist/cli/asc.js +34 -7
- package/dist/core/distribution/node-runtime.d.ts +29 -0
- package/dist/core/distribution/node-runtime.js +35 -0
- package/dist/core/distribution/release.d.ts +3 -3
- package/dist/core/distribution/release.js +1 -1
- package/dist/core/operator/work-state.d.ts +6 -0
- package/dist/core/operator/work-state.js +48 -12
- package/dist/ports/local-repo.d.ts +23 -0
- package/package.json +2 -2
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.3.
|
|
10
|
+
npm install -g @asc-agent/runtime@0.3.1
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
npm owns the executable link (on Windows, npm's own `asc.cmd`). This package never edits
|
|
@@ -32,6 +32,10 @@ description: >-
|
|
|
32
32
|
Safely start, resume or continue a Logical Session in a project that has ASC attached.
|
|
33
33
|
Triggers — "proceed with ASC", "continue the ASC session", "asc proceed", "start work
|
|
34
34
|
with ASC", "ASC로 진행해", "ASC 세션 이어서", "ASC로 작업 시작", or the explicit /asc.
|
|
35
|
+
Also triggers on work-status questions in an ASC-attached project — "is this
|
|
36
|
+
implemented?", "what's left to do?", "what should I do now?", "현황 파악",
|
|
37
|
+
"이거 구현됐나", "남은 작업", "지금 뭐 해야 하나" — because answering those by
|
|
38
|
+
exploring the host directly skips ASC's freshness and evidence gates.
|
|
35
39
|
Do not use it in a project where ASC is not attached.
|
|
36
40
|
---
|
|
37
41
|
|
|
@@ -40,6 +44,16 @@ description: >-
|
|
|
40
44
|
This skill is a consumer of the ASC Generic Operator. The judgement belongs to the asc
|
|
41
45
|
CLI — here you call it, act on the typed outcome, and keep the contract.
|
|
42
46
|
|
|
47
|
+
## Work-status questions go through ASC first
|
|
48
|
+
|
|
49
|
+
"Is X implemented?", "what's the status of this work?", "what remains?" — in an
|
|
50
|
+
ASC-attached project, do NOT settle these by direct host exploration (git log, tracker
|
|
51
|
+
reads). Run \`asc proceed --work <KEY>\` first and read its WORK_STATE: it fetches the
|
|
52
|
+
canonical remote, grades the evidence, and refuses to conclude on a stale or key-only
|
|
53
|
+
observation — none of which ad-hoc exploration does. Direct exploration may supplement
|
|
54
|
+
the answer, never replace the judgement. This is mitigation at the model layer; the
|
|
55
|
+
freshness and evidence gates in the CLI remain the final safeguard.
|
|
56
|
+
|
|
43
57
|
## Entry — "proceed with ASC"
|
|
44
58
|
|
|
45
59
|
That phrase means: **check the current state, secure readiness, find the runnable work I
|
|
@@ -56,10 +56,19 @@ export class LocalRepoAdapter {
|
|
|
56
56
|
if (query.refHint) {
|
|
57
57
|
observation.refs = filterRefs(await this.#git(['for-each-ref', '--format=%(refname:short)', 'refs/heads', 'refs/remotes']), query.refHint);
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
// 신선도가 먼저다. 로컬 브랜치를 정본처럼 읽으면 원격이 전진한 사실을 모른 채
|
|
60
|
+
// "구현 증거가 없다"가 나온다 — 그것이 이 어댑터가 실전에서 낸 사고였다.
|
|
61
|
+
const canonical = await this.#freshCanonical(query);
|
|
62
|
+
const canonicalRef = canonical.ref;
|
|
63
|
+
observation.freshness = canonical.freshness;
|
|
60
64
|
if (canonicalRef) {
|
|
61
65
|
observation.canonicalRef = canonicalRef;
|
|
62
66
|
observation.mergedIntoCanonical = await this.#anyMerged(observation.refs, canonicalRef);
|
|
67
|
+
if (observation.mergedIntoCanonical !== true && observation.refs.length > 0) {
|
|
68
|
+
const equivalent = await this.#contentEquivalent(observation.refs, canonicalRef);
|
|
69
|
+
if (equivalent !== undefined)
|
|
70
|
+
observation.contentEquivalent = equivalent;
|
|
71
|
+
}
|
|
63
72
|
if (query.refHint) {
|
|
64
73
|
// 가지가 지워졌어도 이력은 남는다 — 커밋 메시지가 이 작업을 언급하는지 본다.
|
|
65
74
|
const log = await this.#git(['log', '--format=%h %s', `--grep=${query.refHint}`, '-n', '5', canonicalRef]);
|
|
@@ -98,6 +107,56 @@ export class LocalRepoAdapter {
|
|
|
98
107
|
}
|
|
99
108
|
return observation;
|
|
100
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* 정본 대조 기준과 그 신선도. Profile 이 remote 를 선언했으면 당겨 온 뒤 원격 추적
|
|
112
|
+
* ref 를 기준으로 삼는다 — fetch 는 읽기다(원격 write 가 아니다). 실패는 흡수하되
|
|
113
|
+
* FETCH_FAILED 로 남긴다: "당기지 못했다"와 "저장소가 없다"는 다른 사실이다.
|
|
114
|
+
*/
|
|
115
|
+
async #freshCanonical(query) {
|
|
116
|
+
const declared = query.canonicalRef;
|
|
117
|
+
if (query.remote && declared) {
|
|
118
|
+
const branch = declared.startsWith(`${query.remote}/`)
|
|
119
|
+
? declared.slice(query.remote.length + 1)
|
|
120
|
+
: declared;
|
|
121
|
+
const fetched = await this.#git(['fetch', query.remote, branch]);
|
|
122
|
+
const tracking = `${query.remote}/${branch}`;
|
|
123
|
+
if (fetched !== null)
|
|
124
|
+
return { ref: tracking, freshness: { state: 'FRESH' } };
|
|
125
|
+
// 당기지 못했어도 원격 추적 ref 가 있으면 그쪽이 로컬 브랜치보다 정본에 가깝다.
|
|
126
|
+
const trackingExists = (await this.#git(['rev-parse', '--verify', '--quiet', tracking])) !== null;
|
|
127
|
+
return {
|
|
128
|
+
ref: trackingExists ? tracking : declared,
|
|
129
|
+
freshness: { state: 'FETCH_FAILED', detail: `git fetch ${query.remote} ${branch} 실패` },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const ref = declared ?? (await this.#defaultCanonicalRef());
|
|
133
|
+
return { ref, freshness: { state: 'UNKNOWN', detail: '당겨 올 원격이 선언되지 않았다' } };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 조상은 아니지만 내용이 전부 정본에 있는가 (단일 커밋 squash·rebase·cherry-pick 등가).
|
|
137
|
+
* `git cherry` 는 patch-id 로 대조한다 — `-` 만 나오면 전부 반영, `+` 가 있으면 남은
|
|
138
|
+
* 커밋이 있다. 빈 출력은 가지가 정본과 같다는 뜻이라 반영으로 친다.
|
|
139
|
+
*
|
|
140
|
+
* 한계(검증자 실측): **여러 커밋을 하나로 합친 squash 는 못 잡는다** — 합쳐진 patch-id
|
|
141
|
+
* 는 개별 커밋 어느 것과도 일치하지 않는다. 그 경우 커밋 메시지의 키 언급(grep)이
|
|
142
|
+
* 남은 통로이고, 그것마저 없으면 이 관측은 반영 사실을 모른다.
|
|
143
|
+
*/
|
|
144
|
+
async #contentEquivalent(refs, canonicalRef) {
|
|
145
|
+
let measured = false;
|
|
146
|
+
for (const ref of refs) {
|
|
147
|
+
const out = await this.#git(['cherry', canonicalRef, ref]);
|
|
148
|
+
if (out === null)
|
|
149
|
+
continue;
|
|
150
|
+
measured = true;
|
|
151
|
+
const lines = out
|
|
152
|
+
.split('\n')
|
|
153
|
+
.map((line) => line.trim())
|
|
154
|
+
.filter((line) => line.length > 0);
|
|
155
|
+
if (lines.every((line) => line.startsWith('-')))
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
return measured ? false : undefined;
|
|
159
|
+
}
|
|
101
160
|
/**
|
|
102
161
|
* Profile 이 정본 ref 를 선언하지 않았을 때, 저장소 자신에게 묻는다 (origin/HEAD).
|
|
103
162
|
* 추측이 아니라 관측이다 — 없으면 없는 대로 둔다.
|
package/dist/cli/asc.js
CHANGED
|
@@ -13,7 +13,8 @@ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { homedir, userInfo } 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
|
+
import { MINIMUM_NODE_MAJOR, checkNodeRuntime, reexecWithCandidate, } from "../core/distribution/node-runtime.js";
|
|
17
|
+
import { RELEASE_VERSION } from "../core/distribution/release.js";
|
|
17
18
|
import { GitHubClient, discoverToken } from "../adapters/github/client.js";
|
|
18
19
|
import { GitHubChangeContext, GitHubInventory, GitHubResourceContext } from "../adapters/github/context.js";
|
|
19
20
|
import { GitHubEventSource } from "../adapters/github/event-source.js";
|
|
@@ -521,6 +522,17 @@ export async function runAscCommand(argv, entry = 'runtime') {
|
|
|
521
522
|
// 두면 그쪽에 정책이 생기고(C-14 불변식 ⑦), 그러면 두 진입의 답이 갈릴 수 있다.
|
|
522
523
|
const runnable = await checkNodeRuntime(nodeRuntimeDeps());
|
|
523
524
|
if (!runnable.ok) {
|
|
525
|
+
// 후보를 이미 찾았으면 처방 대신 실행한다 — 같은 명령, 같은 argv, 호환 Node (A6).
|
|
526
|
+
const reexec = reexecWithCandidate(runnable, argv, {
|
|
527
|
+
env: process.env,
|
|
528
|
+
entry: fileURLToPath(import.meta.url),
|
|
529
|
+
spawn: (path, args, env) => {
|
|
530
|
+
const child = spawnSync(path, args, { stdio: 'inherit', env: env });
|
|
531
|
+
return { status: child.status, signal: child.signal, ...(child.error ? { error: child.error } : {}) };
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
if (reexec !== null)
|
|
535
|
+
return reexec;
|
|
524
536
|
reportNodeRuntime(runnable, Boolean(values.json) || Boolean(values.agent));
|
|
525
537
|
return 1;
|
|
526
538
|
}
|
|
@@ -1007,7 +1019,9 @@ function declaredPolicies(resolved) {
|
|
|
1007
1019
|
}
|
|
1008
1020
|
return declared;
|
|
1009
1021
|
}
|
|
1010
|
-
|
|
1022
|
+
// RELEASE_VERSION 이 정본이다 — 여기 문자열을 따로 두면 릴리스마다 낡는다 (0.3.0 에서
|
|
1023
|
+
// 0.2.1 로 남아 lock 의 ascVersion 표기가 실제와 어긋났다).
|
|
1024
|
+
const ASC_VERSION = RELEASE_VERSION;
|
|
1011
1025
|
const CAPABILITIES = ['scm.github', 'state.markdown', 'approval.local'];
|
|
1012
1026
|
const ADAPTER_VERSIONS = { 'scm.github': ASC_VERSION, 'state.markdown': ASC_VERSION };
|
|
1013
1027
|
/**
|
|
@@ -1520,9 +1534,18 @@ async function runProfile(command, values, root) {
|
|
|
1520
1534
|
...(values.preset ? { presetId: values.preset } : {}),
|
|
1521
1535
|
overridePath: join(root, 'override.json'),
|
|
1522
1536
|
});
|
|
1523
|
-
// 무엇이 실제로 제공되는지는 붙어 있는 Adapter가 정한다
|
|
1524
|
-
|
|
1525
|
-
|
|
1537
|
+
// 무엇이 실제로 제공되는지는 붙어 있는 Adapter가 정한다 — 상수가 아니라 실측이다 (A5).
|
|
1538
|
+
// discover→probe 를 실제로 돌린다: glab 로그인 같은 상태 변화가 resolve 재실행으로
|
|
1539
|
+
// 반영된다 (re-probe 수단이 따로 필요 없다). 실측 실패는 그 갈래가 빠질 뿐이다.
|
|
1540
|
+
const composed = await composeBindings({ context: { projectRoot: root, env: process.env } }).catch(() => ({ bindings: [] }));
|
|
1541
|
+
for (const binding of composed.bindings) {
|
|
1542
|
+
console.log(`Binding: ${binding.adapterId}/${binding.resource} — ${binding.state}${binding.detail ? ` (${binding.detail})` : ''}`);
|
|
1543
|
+
}
|
|
1544
|
+
// lock 의 capability 표기는 아직 상수다 — 실측을 digest 재료로 쓰면 glab 로그인 여부에
|
|
1545
|
+
// 따라 lock 이 흔들린다. 실측을 lock 체계에 관통시키는 것은 P1 로 넘기고(재설계 영역),
|
|
1546
|
+
// 여기서는 probe 실측을 사람에게 보이는 것까지 한다. 실사용 read 경로(proceed)는
|
|
1547
|
+
// buildWorkIngress 가 이미 composeBindings 실측으로 조립한다.
|
|
1548
|
+
const result = resolveRuntime(layers, CAPABILITIES, ASC_VERSION);
|
|
1526
1549
|
if (!result.ok) {
|
|
1527
1550
|
console.error('resolve failed:');
|
|
1528
1551
|
for (const failure of result.failures) {
|
|
@@ -1535,7 +1558,7 @@ async function runProfile(command, values, root) {
|
|
|
1535
1558
|
const lock = buildLock({
|
|
1536
1559
|
runtime,
|
|
1537
1560
|
ascVersion: ASC_VERSION,
|
|
1538
|
-
adapters:
|
|
1561
|
+
adapters: ADAPTER_VERSIONS,
|
|
1539
1562
|
generatedAt,
|
|
1540
1563
|
});
|
|
1541
1564
|
console.log(`Layers: ${runtime.resolved.policy.layers.join(' → ')}`);
|
|
@@ -2068,7 +2091,10 @@ async function buildWorkIngress(store, root, runtime, resolved) {
|
|
|
2068
2091
|
return undefined;
|
|
2069
2092
|
// 저장소는 원격 provider 와 무관하게 본다. 이 한 줄이 P0-E 의 요점이다.
|
|
2070
2093
|
const repo = new LocalRepoAdapter({ cwd: projectRoot });
|
|
2071
|
-
const
|
|
2094
|
+
const canonicalSource = resolved?.layers.profile.canonical.sources[0];
|
|
2095
|
+
const canonicalRef = canonicalSource?.ref;
|
|
2096
|
+
// remote 를 버리면 로컬 브랜치를 정본처럼 읽는다 — 실전 오판의 경로였다.
|
|
2097
|
+
const canonicalRemote = canonicalSource?.remote;
|
|
2072
2098
|
const canonicalPaths = resolved?.layers.profile.canonical.sources.flatMap((source) => source.paths) ?? [];
|
|
2073
2099
|
const changeContext = ports.changeContext;
|
|
2074
2100
|
return {
|
|
@@ -2114,6 +2140,7 @@ async function buildWorkIngress(store, root, runtime, resolved) {
|
|
|
2114
2140
|
return repo.observe({
|
|
2115
2141
|
refHint: query.refHint,
|
|
2116
2142
|
...(canonicalRef ? { canonicalRef } : {}),
|
|
2143
|
+
...(canonicalRemote ? { remote: canonicalRemote } : {}),
|
|
2117
2144
|
...(paths.length > 0 ? { paths } : {}),
|
|
2118
2145
|
...(modules.length > 0 ? { modulePaths: modules } : {}),
|
|
2119
2146
|
});
|
|
@@ -42,3 +42,32 @@ export declare function majorOf(version: string): number | null;
|
|
|
42
42
|
* 후보를 찾고, 후보에 대해서만 `-v` 를 묻는다.
|
|
43
43
|
*/
|
|
44
44
|
export declare function checkNodeRuntime(deps: NodeRuntimeDeps): Promise<NodeRuntimeCheck>;
|
|
45
|
+
/**
|
|
46
|
+
* 재실행 감시자. 이 변수가 있으면 이미 후보 Node 로 한 번 건너뛴 프로세스다 —
|
|
47
|
+
* 거기서도 하한 미달이면 후보 자체가 낡은 것이고, 또 건너뛰면 무한 루프다.
|
|
48
|
+
*/
|
|
49
|
+
export declare const REEXEC_SENTINEL = "ASC_NODE_REEXEC";
|
|
50
|
+
export type ReexecSpawn = (path: string, args: string[], env: Record<string, string | undefined>) => {
|
|
51
|
+
status: number | null;
|
|
52
|
+
signal?: string | null;
|
|
53
|
+
error?: Error;
|
|
54
|
+
};
|
|
55
|
+
export type ReexecDeps = {
|
|
56
|
+
env: Record<string, string | undefined>;
|
|
57
|
+
spawn: ReexecSpawn;
|
|
58
|
+
/** 지금 이 CLI 의 entry 파일 — 같은 명령을 그대로 다시 돌린다. */
|
|
59
|
+
entry: string;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* 호환 Node 를 이미 찾았으면 그 Node 로 **이 명령을 그대로 다시 돌린다** (A6).
|
|
63
|
+
*
|
|
64
|
+
* 진단(NODE_RUNTIME_REQUIRED)은 정상 동작이었지만, 후보를 찾아 놓고도 매 호출
|
|
65
|
+
* PATH prefix 를 처방하는 것은 persistent 사용성 결함이다 — 처방 대신 실행한다.
|
|
66
|
+
* PATH·shell profile 은 여전히 건드리지 않는다(불변식 ⑰): 프로세스 안의 재실행일 뿐이다.
|
|
67
|
+
*
|
|
68
|
+
* null 은 "재실행하지 않았다" — 후보 없음, 이미 재실행된 프로세스(sentinel), 또는
|
|
69
|
+
* spawn 실패. 그때는 기존 안내가 그대로 나간다.
|
|
70
|
+
*/
|
|
71
|
+
export declare function reexecWithCandidate(check: Extract<NodeRuntimeCheck, {
|
|
72
|
+
ok: false;
|
|
73
|
+
}>, argv: readonly string[], deps: ReexecDeps): number | null;
|
|
@@ -73,3 +73,38 @@ export async function checkNodeRuntime(deps) {
|
|
|
73
73
|
candidates,
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* 재실행 감시자. 이 변수가 있으면 이미 후보 Node 로 한 번 건너뛴 프로세스다 —
|
|
78
|
+
* 거기서도 하한 미달이면 후보 자체가 낡은 것이고, 또 건너뛰면 무한 루프다.
|
|
79
|
+
*/
|
|
80
|
+
export const REEXEC_SENTINEL = 'ASC_NODE_REEXEC';
|
|
81
|
+
/**
|
|
82
|
+
* 호환 Node 를 이미 찾았으면 그 Node 로 **이 명령을 그대로 다시 돌린다** (A6).
|
|
83
|
+
*
|
|
84
|
+
* 진단(NODE_RUNTIME_REQUIRED)은 정상 동작이었지만, 후보를 찾아 놓고도 매 호출
|
|
85
|
+
* PATH prefix 를 처방하는 것은 persistent 사용성 결함이다 — 처방 대신 실행한다.
|
|
86
|
+
* PATH·shell profile 은 여전히 건드리지 않는다(불변식 ⑰): 프로세스 안의 재실행일 뿐이다.
|
|
87
|
+
*
|
|
88
|
+
* null 은 "재실행하지 않았다" — 후보 없음, 이미 재실행된 프로세스(sentinel), 또는
|
|
89
|
+
* spawn 실패. 그때는 기존 안내가 그대로 나간다.
|
|
90
|
+
*/
|
|
91
|
+
export function reexecWithCandidate(check, argv, deps) {
|
|
92
|
+
if (deps.env[REEXEC_SENTINEL])
|
|
93
|
+
return null;
|
|
94
|
+
const candidate = check.candidates.find((c) => {
|
|
95
|
+
const major = majorOf(c.version);
|
|
96
|
+
return major !== null && major >= MINIMUM_NODE_MAJOR;
|
|
97
|
+
});
|
|
98
|
+
if (!candidate)
|
|
99
|
+
return null;
|
|
100
|
+
const child = deps.spawn(candidate.path, [deps.entry, ...argv], {
|
|
101
|
+
...deps.env,
|
|
102
|
+
[REEXEC_SENTINEL]: '1',
|
|
103
|
+
});
|
|
104
|
+
if (child.error)
|
|
105
|
+
return null;
|
|
106
|
+
if (child.signal)
|
|
107
|
+
return 128 + (REEXEC_SIGNALS[child.signal] ?? 15);
|
|
108
|
+
return child.status ?? 0;
|
|
109
|
+
}
|
|
110
|
+
const REEXEC_SIGNALS = { SIGHUP: 1, SIGINT: 2, SIGTERM: 15 };
|
|
@@ -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.3.
|
|
5
|
-
export declare const RUNTIME_SPEC = "@asc-agent/runtime@0.3.
|
|
6
|
-
export declare const BOOTSTRAP_SPEC = "@asc-agent/bootstrap@0.3.
|
|
4
|
+
export declare const RELEASE_VERSION = "0.3.1";
|
|
5
|
+
export declare const RUNTIME_SPEC = "@asc-agent/runtime@0.3.1";
|
|
6
|
+
export declare const BOOTSTRAP_SPEC = "@asc-agent/bootstrap@0.3.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.3.
|
|
12
|
+
export const RELEASE_VERSION = '0.3.1';
|
|
13
13
|
export const RUNTIME_SPEC = `${RUNTIME_PACKAGE}@${RELEASE_VERSION}`;
|
|
14
14
|
export const BOOTSTRAP_SPEC = `${BOOTSTRAP_PACKAGE}@${RELEASE_VERSION}`;
|
|
15
15
|
/**
|
|
@@ -44,6 +44,12 @@ export type WorkStateInput = {
|
|
|
44
44
|
export type WorkStateResult = {
|
|
45
45
|
state: WorkState;
|
|
46
46
|
leaning?: DecidedState;
|
|
47
|
+
/**
|
|
48
|
+
* 구현 존재 증거의 등급. direct 는 정본 자체가 말하는 것(조상·정본 경로·내용 등가),
|
|
49
|
+
* proxy 는 키를 경유한 추정(언급 grep·작업 트리 잔재), none 은 아무것도 없다.
|
|
50
|
+
* "이 키 기준으로 못 찾았다"와 "구현이 없다"를 가르는 것이 이 칸이다.
|
|
51
|
+
*/
|
|
52
|
+
evidenceGrade: 'direct' | 'proxy' | 'none';
|
|
47
53
|
/** 판정의 근거. 사람이 그대로 읽는다. */
|
|
48
54
|
evidence: string[];
|
|
49
55
|
/** 보려 했으나 못 본 것. 판정을 뒤집지는 않지만 숨기지도 않는다. */
|
|
@@ -33,6 +33,7 @@ export function judgeWorkState(input) {
|
|
|
33
33
|
evidence,
|
|
34
34
|
limitations,
|
|
35
35
|
missing,
|
|
36
|
+
evidenceGrade: 'none',
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
const repo = input.repo;
|
|
@@ -49,7 +50,8 @@ export function judgeWorkState(input) {
|
|
|
49
50
|
// 언급은 그 자체로 증거가 아니다. 되돌리기만 있는 이력도 이 작업을 "언급"하고, 뒤이어
|
|
50
51
|
// 걷혀 나간 변경도 그렇다. 살아남은 것이 있어야 정본에 있다고 말할 수 있다.
|
|
51
52
|
const mentionSurvives = mentioned.length > 0 && repo.mentionedOnlyReverts !== true && repo.mentionedArtifactsPresent === true;
|
|
52
|
-
const
|
|
53
|
+
const directEvidence = repo.mergedIntoCanonical === true || onCanonical.length > 0 || repo.contentEquivalent === true;
|
|
54
|
+
const merged = directEvidence || mentionSurvives;
|
|
53
55
|
const hasBranch = repo.refs.length > 0;
|
|
54
56
|
const artifacts = Object.entries(repo.pathsExist).filter(([, exists]) => exists);
|
|
55
57
|
if (repo.canonicalRef)
|
|
@@ -58,6 +60,9 @@ export function judgeWorkState(input) {
|
|
|
58
60
|
evidence.push(`작업 가지: ${repo.refs.join(', ')}`);
|
|
59
61
|
if (repo.mergedIntoCanonical === true)
|
|
60
62
|
evidence.push('작업 가지가 정본에 병합돼 있다');
|
|
63
|
+
if (repo.contentEquivalent === true) {
|
|
64
|
+
evidence.push('작업 가지의 내용이 전부 정본에 반영돼 있다 (조상은 아니다 — rebase·squash 등가)');
|
|
65
|
+
}
|
|
61
66
|
if (onCanonical.length > 0)
|
|
62
67
|
evidence.push(`정본에 산출물이 있다: ${onCanonical.map(([p]) => p).join(', ')}`);
|
|
63
68
|
if (artifacts.length > 0)
|
|
@@ -85,6 +90,11 @@ export function judgeWorkState(input) {
|
|
|
85
90
|
limitations.push(`선행 작업 상태를 확인하지 못했다: ${unknownDependencies.map((d) => d.reference).join(', ')}`);
|
|
86
91
|
}
|
|
87
92
|
const implemented = merged || artifacts.length > 0;
|
|
93
|
+
const evidenceGrade = directEvidence
|
|
94
|
+
? 'direct'
|
|
95
|
+
: mentionSurvives || artifacts.length > 0
|
|
96
|
+
? 'proxy'
|
|
97
|
+
: 'none';
|
|
88
98
|
// ① 구현은 정본에 있는데 tracker 가 안 따라왔다. 여기서만 tracker 를 본다 — 그것도
|
|
89
99
|
// "끝났다고 말하지 않는다"는 사실로만. tracker 가 결론을 만드는 자리는 없다.
|
|
90
100
|
// 확정하려면 **살아 있는 산출물**이 있어야 한다: 병합 흔적만으로는 부분 병합·스캐폴드·
|
|
@@ -94,35 +104,54 @@ export function judgeWorkState(input) {
|
|
|
94
104
|
const artifactSurvives = repo.mergedIntoCanonical === true || onCanonical.length > 0 || repo.mentionedArtifactsPresent === true;
|
|
95
105
|
if (merged && input.trackerDone === false && artifactSurvives && repo.mentionedOnlyReverts !== true) {
|
|
96
106
|
limitations.push('인수 조건 전체가 지금도 충족되는지는 확인하지 않았다 — 여기서 말하는 것은 구현의 생존까지다');
|
|
97
|
-
|
|
107
|
+
// 측정된 반증은 언급-생존보다 무겁다: cherry 가 "가지에 정본 미반영 커밋이 남아
|
|
108
|
+
// 있다"고 말했으면, 언급 grep 만으로 "할 일은 상태 정리"를 확정하지 않는다.
|
|
109
|
+
if (repo.contentEquivalent === false) {
|
|
110
|
+
limitations.push('작업 가지에 정본에 반영되지 않은 커밋이 남아 있다 (patch 대조) — 상태 정리만 남았다고 확정하지 않는다');
|
|
111
|
+
return decided('IMPLEMENTED_STALE_TRACKER', evidence, limitations, { demote: true, grade: evidenceGrade });
|
|
112
|
+
}
|
|
113
|
+
return decided('IMPLEMENTED_STALE_TRACKER', evidence, limitations, { demote: false, grade: evidenceGrade });
|
|
98
114
|
}
|
|
99
115
|
if (merged && input.trackerDone === false) {
|
|
100
116
|
// 병합 흔적은 있는데 생존을 확인하지 못했다 — 새 구현을 시키지도, 끝났다고 하지도 않는다.
|
|
101
117
|
limitations.push('정본에 병합 흔적은 있으나 구현이 지금도 남아 있는지 확인하지 못했다');
|
|
102
|
-
return decided('IMPLEMENTATION_COMPLETE_BLOCKED_VERIFICATION', evidence, limitations, { demote: true });
|
|
118
|
+
return decided('IMPLEMENTATION_COMPLETE_BLOCKED_VERIFICATION', evidence, limitations, { demote: true, grade: evidenceGrade });
|
|
103
119
|
}
|
|
104
120
|
// ② 구현 증거는 있는데 남은 검증 경로가 막혔다.
|
|
105
121
|
if (implemented && input.change === 'UNAVAILABLE' && !merged) {
|
|
106
|
-
return decided('IMPLEMENTATION_COMPLETE_BLOCKED_VERIFICATION', evidence, limitations, { demote: true });
|
|
122
|
+
return decided('IMPLEMENTATION_COMPLETE_BLOCKED_VERIFICATION', evidence, limitations, { demote: true, grade: evidenceGrade });
|
|
107
123
|
}
|
|
108
124
|
// ③ 가지는 있는데 병합 전이고 선행 작업이 열려 있다.
|
|
109
125
|
if (hasBranch && !merged && openDependencies.length > 0) {
|
|
110
|
-
return decided('BLOCKED_DEPENDENCY', evidence, limitations, { demote: false });
|
|
126
|
+
return decided('BLOCKED_DEPENDENCY', evidence, limitations, { demote: false, grade: evidenceGrade });
|
|
111
127
|
}
|
|
112
128
|
if (!implemented && openDependencies.length > 0) {
|
|
113
|
-
return decided('BLOCKED_DEPENDENCY', evidence, limitations, { demote: false });
|
|
129
|
+
return decided('BLOCKED_DEPENDENCY', evidence, limitations, { demote: false, grade: evidenceGrade });
|
|
114
130
|
}
|
|
115
131
|
// ④ 검토가 답을 기다린다.
|
|
116
132
|
if (typeof input.change === 'object' && requestsResponse(input.change, input.comments)) {
|
|
117
|
-
return decided('REVIEW_RESPONSE_REQUIRED', evidence, limitations, { demote: true });
|
|
133
|
+
return decided('REVIEW_RESPONSE_REQUIRED', evidence, limitations, { demote: true, grade: evidenceGrade });
|
|
118
134
|
}
|
|
119
135
|
// ⑤ 구현 증거도 없고 막힌 것도 없다.
|
|
136
|
+
//
|
|
137
|
+
// "없다"는 신선한 정본에서만 성립한다. 당겨 오지 못한 관측 위의 "없음"은 원격이
|
|
138
|
+
// 이미 품고 있는 구현을 못 본 것일 수 있다 — 그때는 착수를 추천하지 않는다.
|
|
139
|
+
// fetch 실패는 저장소 부재가 아니다: missing 은 freshness 를 따로 가리킨다.
|
|
120
140
|
if (!implemented) {
|
|
121
|
-
|
|
122
|
-
|
|
141
|
+
if (repo.freshness?.state !== 'FRESH') {
|
|
142
|
+
limitations.push(`정본을 원격에서 당겨 오지 못한 관측이다 (${repo.freshness?.state ?? 'UNKNOWN'}${repo.freshness?.detail ? ` — ${repo.freshness.detail}` : ''}) — 이 위에서 "구현이 없다"를 확정하지 않는다`);
|
|
143
|
+
return { state: 'UNDECIDABLE', evidence, limitations, missing: ['canonical-freshness'], evidenceGrade };
|
|
144
|
+
}
|
|
145
|
+
evidence.push('이 작업 키를 직접 가리키는 증거를 확인하지 못했다');
|
|
146
|
+
const result = decided('ACTIONABLE', evidence, limitations, { demote: true, grade: evidenceGrade });
|
|
147
|
+
// 구조적 한계 — 키 대조는 proxy 다. 다른 키의 커밋이 이 작업의 인수 조건을 이미
|
|
148
|
+
// 충족했을 가능성은 여기서 대조하지 않았다. 표기는 하되 이 한 줄로 판정을 되돌리지는
|
|
149
|
+
// 않는다 (모든 관측에 항상 붙는 한계라, demote 재료로 쓰면 ACTIONABLE 이 사라진다).
|
|
150
|
+
result.limitations.push('다른 키·경로로 이미 충족됐을 가능성은 대조하지 않았다 — 키 기준 관측의 구조적 한계');
|
|
151
|
+
return result;
|
|
123
152
|
}
|
|
124
153
|
// 구현 증거는 있는데 위 어디에도 안 걸린다 — 남은 것은 검증이고, 무엇이 막혔는지는 모른다.
|
|
125
|
-
return decided('IMPLEMENTATION_COMPLETE_BLOCKED_VERIFICATION', evidence, limitations, { demote: true });
|
|
154
|
+
return decided('IMPLEMENTATION_COMPLETE_BLOCKED_VERIFICATION', evidence, limitations, { demote: true, grade: evidenceGrade });
|
|
126
155
|
}
|
|
127
156
|
/** 검토가 응답을 요구하는가. provider 어휘를 해석하지 않고 두 가지 표시만 본다. */
|
|
128
157
|
function requestsResponse(change, comments) {
|
|
@@ -139,7 +168,14 @@ function requestsResponse(change, comments) {
|
|
|
139
168
|
*/
|
|
140
169
|
function decided(state, evidence, limitations, options) {
|
|
141
170
|
if (options.demote && limitations.length > 0) {
|
|
142
|
-
return {
|
|
171
|
+
return {
|
|
172
|
+
state: 'DECIDABLE_WITH_LIMITATION',
|
|
173
|
+
leaning: state,
|
|
174
|
+
evidence,
|
|
175
|
+
limitations,
|
|
176
|
+
missing: [],
|
|
177
|
+
evidenceGrade: options.grade,
|
|
178
|
+
};
|
|
143
179
|
}
|
|
144
|
-
return { state, evidence, limitations, missing: [] };
|
|
180
|
+
return { state, evidence, limitations, missing: [], evidenceGrade: options.grade };
|
|
145
181
|
}
|
|
@@ -44,6 +44,24 @@ export type RepoObservation = {
|
|
|
44
44
|
* 않은 것을 확인했다고 하지 않는다.
|
|
45
45
|
*/
|
|
46
46
|
mentionedArtifactsPresent?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* refs 중 정본의 조상은 아니지만 **내용이 전부 정본에 반영된**(rebase·cherry-pick 등가)
|
|
49
|
+
* 가지가 있는가. SHA·이슈 키가 사라져도 내용이 살아 있으면 "미구현"이 아니다.
|
|
50
|
+
* 로컬에 가지가 남아 있을 때만 잴 수 있다 — 못 쟀으면 undefined.
|
|
51
|
+
*/
|
|
52
|
+
contentEquivalent?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* 정본 대조가 얼마나 신선한가. **관측했다 ≠ 신선하다** — 로컬만 읽은 관측은 원격이
|
|
55
|
+
* 전진한 사실을 모르고, 그 위에서 "구현 증거가 없다"는 결론은 성립하지 않는다.
|
|
56
|
+
*
|
|
57
|
+
* FRESH 원격을 당겨 온 뒤의 원격 추적 ref 를 봤다
|
|
58
|
+
* FETCH_FAILED 당기려 했으나 실패했다 — 관측은 낡았을 수 있다
|
|
59
|
+
* UNKNOWN 당길 대상을 몰랐다 (remote 미선언 등)
|
|
60
|
+
*/
|
|
61
|
+
freshness?: {
|
|
62
|
+
state: 'FRESH' | 'FETCH_FAILED' | 'UNKNOWN';
|
|
63
|
+
detail?: string;
|
|
64
|
+
};
|
|
47
65
|
/** git 자체를 쓸 수 없었던 이유. 있으면 이 관측은 비어 있다. */
|
|
48
66
|
unavailable?: string;
|
|
49
67
|
};
|
|
@@ -51,6 +69,11 @@ export type RepoQuery = {
|
|
|
51
69
|
/** ref 이름에서 찾을 조각. 보통 작업 항목 키. */
|
|
52
70
|
refHint?: string;
|
|
53
71
|
canonicalRef?: string;
|
|
72
|
+
/**
|
|
73
|
+
* canonicalRef 를 당겨 올 원격 이름 (Profile 의 canonical source 가 선언한 것).
|
|
74
|
+
* 있으면 observe 가 fetch 를 시도하고 원격 추적 ref 를 대조 기준으로 삼는다.
|
|
75
|
+
*/
|
|
76
|
+
remote?: string;
|
|
54
77
|
paths?: readonly string[];
|
|
55
78
|
/** 범위 재료로만 확인할 자리들. 증거 칸에 섞이지 않는다. */
|
|
56
79
|
modulePaths?: readonly string[];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asc-agent/runtime",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "ASC (Agent Session Control)
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "ASC (Agent Session Control) \u2014 local-first human-in-the-loop agent control plane: core, CLI, and adapters",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
7
7
|
"control-plane",
|