@polydeukes/adapter-claude-code 0.5.0 → 0.6.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.ko.md +33 -14
- package/README.md +33 -14
- package/dist/index.d.ts +9 -10
- package/dist/index.js +8 -9
- package/dist/run-adapter-path.d.ts +32 -21
- package/dist/run-adapter-path.js +2 -2
- package/dist/session-channel-reader.d.ts +21 -0
- package/dist/session-channel-reader.js +81 -0
- package/dist/session-source-reader.d.ts +20 -0
- package/dist/session-source-reader.js +31 -0
- package/dist/session-vocabulary.d.ts +5 -1
- package/dist/session-vocabulary.js +2 -2
- package/dist/transcript.d.ts +7 -3
- package/dist/transcript.js +4 -35
- package/dist/up-translate.d.ts +3 -0
- package/dist/up-translate.js +27 -2
- package/package.json +6 -5
- package/dist/precedent.d.ts +0 -31
- package/dist/precedent.js +0 -57
package/README.ko.md
CHANGED
|
@@ -1,22 +1,41 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@polydeukes/adapter-claude-code`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](./README.md) · **한국어**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
이 어댑터는 Claude Code PreToolUse 페이로드를 약속(covenant) 입력 IR로 변환합니다.
|
|
6
|
+
우산 패키지가 세션 표면에 주입할 읽기 함수를 만드는 팩터리도 제공합니다.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
<a id="overview"></a>
|
|
9
|
+
## 개요
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
공개 계약 심볼은 다음과 같습니다.
|
|
10
12
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
13
|
+
- `runAdapterPath`
|
|
14
|
+
- `sessionSourceReader`
|
|
15
|
+
- `sessionChannelReader`
|
|
16
|
+
- `transcriptPathFromPayload`
|
|
17
|
+
- `transcriptFromJsonlFile`
|
|
18
|
+
- `COMMAND_ARGS`
|
|
19
|
+
- `MUTATING_TOOLS`
|
|
20
|
+
- `SHELL_TOOLS`
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
<a id="examples"></a>
|
|
23
|
+
## 예제
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
```ts
|
|
26
|
+
import { runAdapterPath } from '@polydeukes/adapter-claude-code';
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
const outcome = await runAdapterPath({
|
|
29
|
+
rawPayload: '{}',
|
|
30
|
+
telemetryPath: '.polydeukes/roi.log',
|
|
31
|
+
dispatch: async () => ({ exitCode: 0, results: [] }),
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
<a id="see-also"></a>
|
|
36
|
+
## 같이 보기
|
|
37
|
+
|
|
38
|
+
- [`@polydeukes/adapter-claude-code` 패키지
|
|
39
|
+
레퍼런스](../../docs/reference/packages/adapter-claude-code.ko.md)
|
|
40
|
+
- [`polydeukes/claude-code`](../../docs/reference/packages/polydeukes.ko.md#polydeukes-entry-points)
|
|
41
|
+
- [`@polydeukes/covenant`](../../docs/reference/packages/covenant.ko.md)
|
package/README.md
CHANGED
|
@@ -1,22 +1,41 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@polydeukes/adapter-claude-code`
|
|
2
2
|
|
|
3
|
-
**English** · [한국어](
|
|
3
|
+
**English** · [한국어](./README.ko.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This adapter translates Claude Code PreToolUse payloads into covenant input IR and provides the
|
|
6
|
+
session-side reader factories the umbrella injects.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
<a id="overview"></a>
|
|
9
|
+
## Overview
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
Public contract symbols include:
|
|
10
12
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
13
|
+
- `runAdapterPath`
|
|
14
|
+
- `sessionSourceReader`
|
|
15
|
+
- `sessionChannelReader`
|
|
16
|
+
- `transcriptPathFromPayload`
|
|
17
|
+
- `transcriptFromJsonlFile`
|
|
18
|
+
- `COMMAND_ARGS`
|
|
19
|
+
- `MUTATING_TOOLS`
|
|
20
|
+
- `SHELL_TOOLS`
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
<a id="examples"></a>
|
|
23
|
+
## Examples
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
```ts
|
|
26
|
+
import { runAdapterPath } from '@polydeukes/adapter-claude-code';
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
const outcome = await runAdapterPath({
|
|
29
|
+
rawPayload: '{}',
|
|
30
|
+
telemetryPath: '.polydeukes/roi.log',
|
|
31
|
+
dispatch: async () => ({ exitCode: 0, results: [] }),
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
<a id="see-also"></a>
|
|
36
|
+
## See also
|
|
37
|
+
|
|
38
|
+
- [`@polydeukes/adapter-claude-code` package
|
|
39
|
+
reference](../../docs/reference/packages/adapter-claude-code.md)
|
|
40
|
+
- [`polydeukes/claude-code`](../../docs/reference/packages/polydeukes.md#polydeukes-entry-points)
|
|
41
|
+
- [`@polydeukes/covenant`](../../docs/reference/packages/covenant.md)
|
package/dist/index.d.ts
CHANGED
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
* @polydeukes/adapter-claude-code — up-translates Claude Code PreToolUse hook
|
|
3
3
|
* payloads into the agent-neutral covenant input IR.
|
|
4
4
|
*
|
|
5
|
-
* Pre-alpha.
|
|
6
|
-
*
|
|
7
|
-
* vocabulary is translated away before it
|
|
5
|
+
* Pre-alpha. Translation plus this surface's supply bodies — the readers a composition root
|
|
6
|
+
* injects, which open files and spawn nothing. Agent and tool literals live here by design:
|
|
7
|
+
* this package is the boundary where Claude Code's vocabulary is translated away before it
|
|
8
|
+
* reaches the core.
|
|
8
9
|
*/
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
11
|
-
export { type
|
|
12
|
-
export { COMMAND_ARGS, MUTATING_TOOLS, SHELL_TOOLS, transcriptPathFromPayload, } from './session-vocabulary.
|
|
13
|
-
export {
|
|
14
|
-
export { buildCovenantInput, type ClaudePreToolUsePayload, type TranslatedEvent, translateEvent, } from './up-translate.js';
|
|
15
|
-
export { type VirtualPostState, virtualPostState } from './virtual-post-state.js';
|
|
10
|
+
export { type AdapterPathOutcome, type DispatchAdapterView, type RunAdapterPathSpec, runAdapterPath, } from './run-adapter-path.ts';
|
|
11
|
+
export { type SessionChannelReaderSpec, sessionChannelReader, } from './session-channel-reader.ts';
|
|
12
|
+
export { type SessionSourceReaderSpec, sessionSourceReader } from './session-source-reader.ts';
|
|
13
|
+
export { COMMAND_ARGS, MUTATING_TOOLS, SHELL_TOOLS, type TranscriptPathFromPayloadSpec, transcriptPathFromPayload, } from './session-vocabulary.ts';
|
|
14
|
+
export { type TranscriptFromJsonlFileSpec, transcriptFromJsonlFile } from './transcript.ts';
|
package/dist/index.js
CHANGED
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
* @polydeukes/adapter-claude-code — up-translates Claude Code PreToolUse hook
|
|
3
3
|
* payloads into the agent-neutral covenant input IR.
|
|
4
4
|
*
|
|
5
|
-
* Pre-alpha.
|
|
6
|
-
*
|
|
7
|
-
* vocabulary is translated away before it
|
|
5
|
+
* Pre-alpha. Translation plus this surface's supply bodies — the readers a composition root
|
|
6
|
+
* injects, which open files and spawn nothing. Agent and tool literals live here by design:
|
|
7
|
+
* this package is the boundary where Claude Code's vocabulary is translated away before it
|
|
8
|
+
* reaches the core.
|
|
8
9
|
*/
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
11
|
-
export {
|
|
10
|
+
export { runAdapterPath, } from './run-adapter-path.js';
|
|
11
|
+
export { sessionChannelReader, } from './session-channel-reader.js';
|
|
12
|
+
export { sessionSourceReader } from './session-source-reader.js';
|
|
12
13
|
export { COMMAND_ARGS, MUTATING_TOOLS, SHELL_TOOLS, transcriptPathFromPayload, } from './session-vocabulary.js';
|
|
13
|
-
export {
|
|
14
|
-
export { buildCovenantInput, translateEvent, } from './up-translate.js';
|
|
15
|
-
export { virtualPostState } from './virtual-post-state.js';
|
|
14
|
+
export { transcriptFromJsonlFile } from './transcript.js';
|
|
@@ -3,21 +3,42 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Composes translation → injected dispatch → funnel-supplement recording so every
|
|
5
5
|
* adapter-path call leaves exactly one telemetry row when summed with downstream
|
|
6
|
-
* records. I/O lives here and only here — the
|
|
6
|
+
* records. I/O lives here and only here — the translation modules stay pure.
|
|
7
7
|
*/
|
|
8
|
+
import { type DispatchOutcome } from '@polydeukes/core';
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
+
* The part of a dispatch result this path reads — a structural view, not a second protocol.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Derived from core's `DispatchOutcome` by `Pick`, so it cannot drift from the protocol type:
|
|
13
|
+
* a field renamed there stops compiling here. It is narrower on purpose — the entries carry
|
|
14
|
+
* a telemetry word this path never reads, and demanding it would make every dispatcher the
|
|
15
|
+
* adapter accepts name a core type its own consumers may not be able to resolve.
|
|
14
16
|
*/
|
|
15
|
-
export type
|
|
17
|
+
export type DispatchAdapterView = {
|
|
18
|
+
exitCode: DispatchOutcome['exitCode'];
|
|
19
|
+
results: readonly unknown[];
|
|
20
|
+
};
|
|
21
|
+
/** {@link runAdapterPath} input — one payload, where to record, and the dispatch seam. */
|
|
22
|
+
export type RunAdapterPathSpec = {
|
|
23
|
+
/** Raw hook stdin — one PreToolUse payload as a JSON string. */
|
|
24
|
+
rawPayload: string;
|
|
25
|
+
/** Where adapter-level records append. */
|
|
26
|
+
telemetryPath: string;
|
|
27
|
+
/**
|
|
28
|
+
* Injected dispatch seam — the assembler binds the real dispatcher here.
|
|
29
|
+
*
|
|
30
|
+
* Typed by what this path reads, not by what the dispatcher returns: the supplement rule
|
|
31
|
+
* below needs the exit code and whether any covenant produced an entry. A dispatcher
|
|
32
|
+
* carrying more per entry — core's `DispatchOutcome`, which adds the telemetry word the
|
|
33
|
+
* wrapper already recorded — satisfies this structurally, so binding one costs no cast.
|
|
34
|
+
*/
|
|
35
|
+
dispatch: (stdinPayload: string) => Promise<DispatchAdapterView>;
|
|
36
|
+
/** Label for adapter-level records. Default: 'adapter-claude-code'. */
|
|
37
|
+
adapterLabel?: string;
|
|
38
|
+
};
|
|
39
|
+
/** {@link runAdapterPath} result — the exit code the hook process leaves with. */
|
|
40
|
+
export type AdapterPathOutcome = {
|
|
16
41
|
exitCode: 0 | 2;
|
|
17
|
-
results: {
|
|
18
|
-
label: string;
|
|
19
|
-
exitCode: 0 | 2;
|
|
20
|
-
}[];
|
|
21
42
|
};
|
|
22
43
|
/**
|
|
23
44
|
* Run one PreToolUse payload through the adapter path.
|
|
@@ -29,14 +50,4 @@ export type DispatchOutcome = {
|
|
|
29
50
|
* `exitCode 0 && results.length 0 → one adapter passed record`; every other outcome
|
|
30
51
|
* appends nothing because downstream already recorded, so nothing double-counts.
|
|
31
52
|
*/
|
|
32
|
-
export declare function runAdapterPath(spec:
|
|
33
|
-
/** Raw hook stdin — one PreToolUse payload as a JSON string. */
|
|
34
|
-
rawPayload: string;
|
|
35
|
-
telemetryPath: string;
|
|
36
|
-
/** Injected dispatch seam — the assembler binds the real dispatcher here. */
|
|
37
|
-
dispatch: (stdinPayload: string) => Promise<DispatchOutcome>;
|
|
38
|
-
/** Label for adapter-level records. Default: 'adapter-claude-code'. */
|
|
39
|
-
adapterLabel?: string;
|
|
40
|
-
}): Promise<{
|
|
41
|
-
exitCode: 0 | 2;
|
|
42
|
-
}>;
|
|
53
|
+
export declare function runAdapterPath(spec: RunAdapterPathSpec): Promise<AdapterPathOutcome>;
|
package/dist/run-adapter-path.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Composes translation → injected dispatch → funnel-supplement recording so every
|
|
5
5
|
* adapter-path call leaves exactly one telemetry row when summed with downstream
|
|
6
|
-
* records. I/O lives here and only here — the
|
|
6
|
+
* records. I/O lives here and only here — the translation modules stay pure.
|
|
7
7
|
*/
|
|
8
8
|
import { readFileSync } from 'node:fs';
|
|
9
|
-
import { appendRecordFailOpen, EXIT_BREAK_BLOCKING, EXIT_UPHOLD } from '@polydeukes/core';
|
|
9
|
+
import { appendRecordFailOpen, EXIT_BREAK_BLOCKING, EXIT_UPHOLD, } from '@polydeukes/core';
|
|
10
10
|
import { collectFileChanges } from './file-changes.js';
|
|
11
11
|
import { buildCovenantInput } from './up-translate.js';
|
|
12
12
|
/** Default label for adapter-level telemetry records. */
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session surface's supply body for the spawn sidecar channel — the subagent records
|
|
3
|
+
* this host keeps beside the session's own transcript.
|
|
4
|
+
*/
|
|
5
|
+
import type { ChannelReader } from '@polydeukes/core';
|
|
6
|
+
/** {@link sessionChannelReader} input — the session's transcript, when the host named one. */
|
|
7
|
+
export type SessionChannelReaderSpec = {
|
|
8
|
+
transcriptPath?: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* A reader over the spawn sidecar: a channel kind in, the spawn-record list as JSON text or
|
|
12
|
+
* absence out.
|
|
13
|
+
*
|
|
14
|
+
* A transcript at `<dir>/<sessionId>.jsonl` keeps its subagent records at
|
|
15
|
+
* `<dir>/<sessionId>/subagents/agent-*.meta.json`, one object per file. The three answers
|
|
16
|
+
* are three facts: the parsed records as one JSON array, `'[]'` when the directory is there
|
|
17
|
+
* and holds no record (the channel observed no spawn), and `undefined` when there is no
|
|
18
|
+
* channel at all. A record that will not parse shrinks the evidence rather than poisoning
|
|
19
|
+
* it — failing the whole channel would let one corrupt file erase a spawn that happened.
|
|
20
|
+
*/
|
|
21
|
+
export declare function sessionChannelReader(spec: SessionChannelReaderSpec): ChannelReader;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session surface's supply body for the spawn sidecar channel — the subagent records
|
|
3
|
+
* this host keeps beside the session's own transcript.
|
|
4
|
+
*/
|
|
5
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { basename, dirname, isAbsolute, join } from 'node:path';
|
|
7
|
+
/** The channel kind this surface carries; any other kind is absent. */
|
|
8
|
+
const SIDECAR = 'sidecar';
|
|
9
|
+
/** The file extension the transcript is named with, stripped to get the session id. */
|
|
10
|
+
const TRANSCRIPT_SUFFIX = '.jsonl';
|
|
11
|
+
/** One subagent record per file, under the session's own directory beside the transcript. */
|
|
12
|
+
const SUBAGENTS_DIR = 'subagents';
|
|
13
|
+
const META_PREFIX = 'agent-';
|
|
14
|
+
const META_SUFFIX = '.meta.json';
|
|
15
|
+
/**
|
|
16
|
+
* A reader over the spawn sidecar: a channel kind in, the spawn-record list as JSON text or
|
|
17
|
+
* absence out.
|
|
18
|
+
*
|
|
19
|
+
* A transcript at `<dir>/<sessionId>.jsonl` keeps its subagent records at
|
|
20
|
+
* `<dir>/<sessionId>/subagents/agent-*.meta.json`, one object per file. The three answers
|
|
21
|
+
* are three facts: the parsed records as one JSON array, `'[]'` when the directory is there
|
|
22
|
+
* and holds no record (the channel observed no spawn), and `undefined` when there is no
|
|
23
|
+
* channel at all. A record that will not parse shrinks the evidence rather than poisoning
|
|
24
|
+
* it — failing the whole channel would let one corrupt file erase a spawn that happened.
|
|
25
|
+
*/
|
|
26
|
+
export function sessionChannelReader(spec) {
|
|
27
|
+
return (kind) => {
|
|
28
|
+
if (kind !== SIDECAR)
|
|
29
|
+
return undefined;
|
|
30
|
+
const { transcriptPath } = spec;
|
|
31
|
+
// The path is the host's own fact, and every real host hands an absolute one. A
|
|
32
|
+
// relative or empty spelling would resolve the sidecar against the hook's cwd, where
|
|
33
|
+
// a checked-out `subagents/` directory could pass its records off as this session's
|
|
34
|
+
// spawn evidence — so anything non-absolute is channel absence.
|
|
35
|
+
if (transcriptPath === undefined || !isAbsolute(transcriptPath))
|
|
36
|
+
return undefined;
|
|
37
|
+
const sessionId = basename(transcriptPath, TRANSCRIPT_SUFFIX);
|
|
38
|
+
const dir = join(dirname(transcriptPath), sessionId, SUBAGENTS_DIR);
|
|
39
|
+
let entries;
|
|
40
|
+
try {
|
|
41
|
+
// Directory entries rather than names: a host can leave a directory whose name matches
|
|
42
|
+
// the record pattern, and reading it as a file would take the whole channel down.
|
|
43
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
44
|
+
.filter((entry) => entry.isFile() &&
|
|
45
|
+
entry.name.startsWith(META_PREFIX) &&
|
|
46
|
+
entry.name.endsWith(META_SUFFIX))
|
|
47
|
+
.map((entry) => entry.name)
|
|
48
|
+
.sort();
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
// Only "no such directory" is channel absence. Anything else — a permission refusal
|
|
52
|
+
// above all — throws, so it reaches the root's fail-closed path instead of passing
|
|
53
|
+
// for a session that never spawned.
|
|
54
|
+
const { code } = error;
|
|
55
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
56
|
+
return undefined;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
const records = [];
|
|
60
|
+
for (const name of entries) {
|
|
61
|
+
let text;
|
|
62
|
+
try {
|
|
63
|
+
text = readFileSync(join(dir, name), 'utf-8');
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
// A record deleted between the listing and the read is one fewer witness; a read
|
|
67
|
+
// the host refused is the same fail-closed fact as the directory case above.
|
|
68
|
+
if (error.code === 'ENOENT')
|
|
69
|
+
continue;
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
records.push(JSON.parse(text));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// A record this host wrote half-way is one fewer witness, never a failed channel.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return JSON.stringify(records);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session surface's supply body for named file sources — the working tree as this
|
|
3
|
+
* surface observes it.
|
|
4
|
+
*/
|
|
5
|
+
import type { SourceReader } from '@polydeukes/core';
|
|
6
|
+
/** {@link sessionSourceReader} input — the repository the paths are relative to. */
|
|
7
|
+
export type SessionSourceReaderSpec = {
|
|
8
|
+
repoRoot: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* A reader over the working tree under `repoRoot`: repo-relative path in, the file's text or
|
|
12
|
+
* absence out.
|
|
13
|
+
*
|
|
14
|
+
* Absence covers every shape the disk can hold under a planned path that is not a readable
|
|
15
|
+
* text — no entry at all, a directory, a path whose parent is a file, and bytes carrying a
|
|
16
|
+
* NUL, which no declaration can parse and which a utf-8 decode would hand on as a lossy
|
|
17
|
+
* string. Everything else throws, so a permission refusal fails the run closed rather than
|
|
18
|
+
* passing for a file that is not there.
|
|
19
|
+
*/
|
|
20
|
+
export declare function sessionSourceReader(spec: SessionSourceReaderSpec): SourceReader;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session surface's supply body for named file sources — the working tree as this
|
|
3
|
+
* surface observes it.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
/**
|
|
8
|
+
* A reader over the working tree under `repoRoot`: repo-relative path in, the file's text or
|
|
9
|
+
* absence out.
|
|
10
|
+
*
|
|
11
|
+
* Absence covers every shape the disk can hold under a planned path that is not a readable
|
|
12
|
+
* text — no entry at all, a directory, a path whose parent is a file, and bytes carrying a
|
|
13
|
+
* NUL, which no declaration can parse and which a utf-8 decode would hand on as a lossy
|
|
14
|
+
* string. Everything else throws, so a permission refusal fails the run closed rather than
|
|
15
|
+
* passing for a file that is not there.
|
|
16
|
+
*/
|
|
17
|
+
export function sessionSourceReader(spec) {
|
|
18
|
+
return (path) => {
|
|
19
|
+
let bytes;
|
|
20
|
+
try {
|
|
21
|
+
bytes = readFileSync(join(spec.repoRoot, path));
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
const { code } = error;
|
|
25
|
+
if (code === 'ENOENT' || code === 'EISDIR' || code === 'ENOTDIR')
|
|
26
|
+
return undefined;
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
return bytes.includes(0) ? undefined : bytes.toString('utf-8');
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -18,6 +18,10 @@ export declare const MUTATING_TOOLS: string[];
|
|
|
18
18
|
export declare const SHELL_TOOLS: string[];
|
|
19
19
|
/** `tool_input` keys a shell tool's command line travels in. */
|
|
20
20
|
export declare const COMMAND_ARGS: string[];
|
|
21
|
+
/** {@link transcriptPathFromPayload} input — one raw PreToolUse payload as a JSON string. */
|
|
22
|
+
export type TranscriptPathFromPayloadSpec = {
|
|
23
|
+
rawPayload: string;
|
|
24
|
+
};
|
|
21
25
|
/**
|
|
22
26
|
* Read the live transcript path out of a raw PreToolUse payload.
|
|
23
27
|
*
|
|
@@ -28,4 +32,4 @@ export declare const COMMAND_ARGS: string[];
|
|
|
28
32
|
* witness valve rather than opening it. A payload this function
|
|
29
33
|
* cannot parse is still dispatched — `runAdapterPath` owns that verdict.
|
|
30
34
|
*/
|
|
31
|
-
export declare function transcriptPathFromPayload(
|
|
35
|
+
export declare function transcriptPathFromPayload(spec: TranscriptPathFromPayloadSpec): string | undefined;
|
|
@@ -29,10 +29,10 @@ export const COMMAND_ARGS = ['command'];
|
|
|
29
29
|
* witness valve rather than opening it. A payload this function
|
|
30
30
|
* cannot parse is still dispatched — `runAdapterPath` owns that verdict.
|
|
31
31
|
*/
|
|
32
|
-
export function transcriptPathFromPayload(
|
|
32
|
+
export function transcriptPathFromPayload(spec) {
|
|
33
33
|
let parsed;
|
|
34
34
|
try {
|
|
35
|
-
parsed = JSON.parse(rawPayload);
|
|
35
|
+
parsed = JSON.parse(spec.rawPayload);
|
|
36
36
|
}
|
|
37
37
|
catch {
|
|
38
38
|
return undefined;
|
package/dist/transcript.d.ts
CHANGED
|
@@ -16,18 +16,22 @@ import { type CanonicalTranscript } from '@polydeukes/core';
|
|
|
16
16
|
* silently — a broken line never aborts the rest of the scan and never throws.
|
|
17
17
|
*/
|
|
18
18
|
export declare function transcriptFromJsonl(text: string): CanonicalTranscript;
|
|
19
|
+
/** {@link transcriptFromJsonlFile} input — the transcript file to read. */
|
|
20
|
+
export type TranscriptFromJsonlFileSpec = {
|
|
21
|
+
path: string;
|
|
22
|
+
};
|
|
19
23
|
/**
|
|
20
24
|
* Read a transcript file and parse it.
|
|
21
25
|
*
|
|
22
26
|
* ANY read failure — missing file, permission, directory — answers `undefined`, never a
|
|
23
27
|
* throw. It is deliberately NOT an empty transcript: the two are different facts, and
|
|
24
28
|
* collapsing them hid the more likely one. An empty transcript is a session that has said
|
|
25
|
-
* nothing yet, and judging against it is correct. An unreadable one is no
|
|
26
|
-
* at all, so
|
|
29
|
+
* nothing yet, and judging against it is correct. An unreadable one is no session channel
|
|
30
|
+
* at all, so a history declaration must skip rather than demand evidence from a session
|
|
27
31
|
* nobody can read — while the witness valve reads the same absence and stays shut, leaving
|
|
28
32
|
* a dead end with no message naming the cause.
|
|
29
33
|
*
|
|
30
34
|
* Either way the valve turns off, never open: `undefined` leaves the dispatcher on its
|
|
31
35
|
* `noopTranscript` default.
|
|
32
36
|
*/
|
|
33
|
-
export declare function transcriptFromJsonlFile(
|
|
37
|
+
export declare function transcriptFromJsonlFile(spec: TranscriptFromJsonlFileSpec): CanonicalTranscript | undefined;
|
package/dist/transcript.js
CHANGED
|
@@ -34,32 +34,6 @@ function toUserMessage(entry) {
|
|
|
34
34
|
timestampMs: Number.isFinite(parsed) ? parsed : undefined,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
/**
|
|
38
|
-
* Extract subagent invocations from one entry.
|
|
39
|
-
*
|
|
40
|
-
* Detection keys on the *field*, not the tool name (the real tool has been renamed
|
|
41
|
-
* Task → Agent): any `tool_use` block whose `input.subagent_type` is a string is an
|
|
42
|
-
* invocation. A block that cannot prove its kind is dropped (evidence reduction).
|
|
43
|
-
*/
|
|
44
|
-
function toSubagentInvocations(entry) {
|
|
45
|
-
if (entry.type !== 'assistant' || !isPlainObject(entry.message)) {
|
|
46
|
-
return [];
|
|
47
|
-
}
|
|
48
|
-
const content = entry.message.content;
|
|
49
|
-
if (!Array.isArray(content)) {
|
|
50
|
-
return [];
|
|
51
|
-
}
|
|
52
|
-
const invocations = [];
|
|
53
|
-
for (const block of content) {
|
|
54
|
-
if (isPlainObject(block) &&
|
|
55
|
-
block.type === 'tool_use' &&
|
|
56
|
-
isPlainObject(block.input) &&
|
|
57
|
-
typeof block.input.subagent_type === 'string') {
|
|
58
|
-
invocations.push({ kind: block.input.subagent_type });
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return invocations;
|
|
62
|
-
}
|
|
63
37
|
/**
|
|
64
38
|
* Extract tool calls from one entry.
|
|
65
39
|
*
|
|
@@ -129,7 +103,6 @@ function toToolResults(entry) {
|
|
|
129
103
|
*/
|
|
130
104
|
export function transcriptFromJsonl(text) {
|
|
131
105
|
const userMessages = [];
|
|
132
|
-
const subagentInvocations = [];
|
|
133
106
|
const observedCalls = [];
|
|
134
107
|
const outcomes = new Map();
|
|
135
108
|
for (const line of text.split('\n')) {
|
|
@@ -147,7 +120,6 @@ export function transcriptFromJsonl(text) {
|
|
|
147
120
|
if (message !== undefined) {
|
|
148
121
|
userMessages.push(message);
|
|
149
122
|
}
|
|
150
|
-
subagentInvocations.push(...toSubagentInvocations(entry));
|
|
151
123
|
observedCalls.push(...toToolCalls(entry));
|
|
152
124
|
for (const result of toToolResults(entry)) {
|
|
153
125
|
// First result wins. Real transcripts carry no duplicate reference within one file
|
|
@@ -169,9 +141,6 @@ export function transcriptFromJsonl(text) {
|
|
|
169
141
|
// call's nested args — so a consumer mutating a result cannot corrupt what later
|
|
170
142
|
// queries read (the same alias-safety contract the core transcriptFromInput upholds).
|
|
171
143
|
return {
|
|
172
|
-
findSubagentInvocations: (kind) => subagentInvocations
|
|
173
|
-
.filter((invocation) => kind === undefined || invocation.kind === kind)
|
|
174
|
-
.map((invocation) => ({ ...invocation })),
|
|
175
144
|
findUserMessages: () => userMessages.map((message) => ({ ...message })),
|
|
176
145
|
findToolCalls: (name) => toolCalls
|
|
177
146
|
.filter((call) => name === undefined || call.name === name)
|
|
@@ -184,18 +153,18 @@ export function transcriptFromJsonl(text) {
|
|
|
184
153
|
* ANY read failure — missing file, permission, directory — answers `undefined`, never a
|
|
185
154
|
* throw. It is deliberately NOT an empty transcript: the two are different facts, and
|
|
186
155
|
* collapsing them hid the more likely one. An empty transcript is a session that has said
|
|
187
|
-
* nothing yet, and judging against it is correct. An unreadable one is no
|
|
188
|
-
* at all, so
|
|
156
|
+
* nothing yet, and judging against it is correct. An unreadable one is no session channel
|
|
157
|
+
* at all, so a history declaration must skip rather than demand evidence from a session
|
|
189
158
|
* nobody can read — while the witness valve reads the same absence and stays shut, leaving
|
|
190
159
|
* a dead end with no message naming the cause.
|
|
191
160
|
*
|
|
192
161
|
* Either way the valve turns off, never open: `undefined` leaves the dispatcher on its
|
|
193
162
|
* `noopTranscript` default.
|
|
194
163
|
*/
|
|
195
|
-
export function transcriptFromJsonlFile(
|
|
164
|
+
export function transcriptFromJsonlFile(spec) {
|
|
196
165
|
let text;
|
|
197
166
|
try {
|
|
198
|
-
text = readFileSync(path, 'utf-8');
|
|
167
|
+
text = readFileSync(spec.path, 'utf-8');
|
|
199
168
|
}
|
|
200
169
|
catch {
|
|
201
170
|
return undefined;
|
package/dist/up-translate.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type ClaudePreToolUsePayload = {
|
|
|
23
23
|
session_id?: string;
|
|
24
24
|
transcript_path?: string;
|
|
25
25
|
cwd?: string;
|
|
26
|
+
agent_type?: string;
|
|
26
27
|
tool_name: string;
|
|
27
28
|
tool_input: Record<string, unknown>;
|
|
28
29
|
};
|
|
@@ -66,6 +67,8 @@ export declare function translateEvent(payload: unknown): TranslatedEvent;
|
|
|
66
67
|
* always `[]` — witness evidence arrives through the transcript, not this path. If any
|
|
67
68
|
* payload fails classification the whole build fails closed with the blocking exit
|
|
68
69
|
* code — a silent drop would be a bypass vector.
|
|
70
|
+
*
|
|
71
|
+
* `actor` comes from {@link actorOf} over the first payload: one observation has one actor.
|
|
69
72
|
*/
|
|
70
73
|
export declare function buildCovenantInput(payloads: unknown[]): {
|
|
71
74
|
ok: true;
|
package/dist/up-translate.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* the barrel is an initialization cycle. The barrel re-exports this
|
|
11
11
|
* module, so every consumer outside the package still reaches it at the same path.
|
|
12
12
|
*/
|
|
13
|
-
import { EXIT_BREAK_BLOCKING } from '@polydeukes/core';
|
|
13
|
+
import { EXIT_BREAK_BLOCKING, isPlainObject } from '@polydeukes/core';
|
|
14
14
|
import { parsePayloadEnvelope } from './payload-envelope.js';
|
|
15
15
|
/**
|
|
16
16
|
* Up-translate one Claude Code payload into an IR fragment (pure).
|
|
@@ -33,6 +33,23 @@ export function translateEvent(payload) {
|
|
|
33
33
|
}
|
|
34
34
|
return { ok: true, kind: 'toolCall', value: { name: toolName, args: toolInput } };
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The actor one payload proves (pure).
|
|
38
|
+
*
|
|
39
|
+
* The host writes `agent_type` at the envelope's top level when the hook fires inside a
|
|
40
|
+
* subagent call, so a non-empty string there is the subagent kind and anything else — an
|
|
41
|
+
* absent field, a non-string, the empty string — is the main session, the positive `{}`.
|
|
42
|
+
* `tool_input` is the agent's own arguments and is never read here: a call could then name
|
|
43
|
+
* any actor it liked.
|
|
44
|
+
*/
|
|
45
|
+
function actorOf(payload) {
|
|
46
|
+
if (!isPlainObject(payload))
|
|
47
|
+
return {};
|
|
48
|
+
const agentType = payload.agent_type;
|
|
49
|
+
if (typeof agentType !== 'string' || agentType === '')
|
|
50
|
+
return {};
|
|
51
|
+
return { agentType };
|
|
52
|
+
}
|
|
36
53
|
/**
|
|
37
54
|
* Fold a sequence of payloads into one {@link CovenantInput} (pure).
|
|
38
55
|
*
|
|
@@ -40,9 +57,17 @@ export function translateEvent(payload) {
|
|
|
40
57
|
* always `[]` — witness evidence arrives through the transcript, not this path. If any
|
|
41
58
|
* payload fails classification the whole build fails closed with the blocking exit
|
|
42
59
|
* code — a silent drop would be a bypass vector.
|
|
60
|
+
*
|
|
61
|
+
* `actor` comes from {@link actorOf} over the first payload: one observation has one actor.
|
|
43
62
|
*/
|
|
44
63
|
export function buildCovenantInput(payloads) {
|
|
45
|
-
const input = {
|
|
64
|
+
const input = {
|
|
65
|
+
toolCalls: [],
|
|
66
|
+
subagentSpawns: [],
|
|
67
|
+
userMessages: [],
|
|
68
|
+
// No payload is no observation, so there is no actor to prove — not the main session.
|
|
69
|
+
...(payloads.length > 0 && { actor: actorOf(payloads[0]) }),
|
|
70
|
+
};
|
|
46
71
|
for (let index = 0; index < payloads.length; index++) {
|
|
47
72
|
const translated = translateEvent(payloads[index]);
|
|
48
73
|
if (translated.ok !== true) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polydeukes/adapter-claude-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Polydeukes adapter for Claude Code — up-translates PreToolUse hook payloads into the agent-neutral covenant input IR. Alpha.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,13 +28,14 @@
|
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
|
-
"dependencies": {
|
|
32
|
-
"@polydeukes/core": "^0.5.0"
|
|
33
|
-
},
|
|
34
31
|
"devDependencies": {
|
|
35
32
|
"@types/node": "^24.0.0",
|
|
36
33
|
"typescript": "7.0.2",
|
|
37
|
-
"vitest": "^4.1.0"
|
|
34
|
+
"vitest": "^4.1.0",
|
|
35
|
+
"@polydeukes/core": "^0.6.1"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@polydeukes/core": "^0.6.1"
|
|
38
39
|
},
|
|
39
40
|
"scripts": {
|
|
40
41
|
"build": "tsc -p tsconfig.build.json",
|
package/dist/precedent.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adapter-owned precedent evidence evaluator.
|
|
3
|
-
*
|
|
4
|
-
* The core owns the `command` evidence vocabulary and validates only the container
|
|
5
|
-
* shape of everything else; ecosystem values — spawn kinds and tool names — are this
|
|
6
|
-
* adapter's vocabulary, so this package validates and judges them (the
|
|
7
|
-
* `resolveGitAdapterSettings` namespace precedent). The covenant assembly injects this
|
|
8
|
-
* evaluator; `undefined` is the handshake that makes assembly fail closed on a key no
|
|
9
|
-
* adapter recognizes.
|
|
10
|
-
*/
|
|
11
|
-
import type { CanonicalTranscript } from '@polydeukes/core';
|
|
12
|
-
/**
|
|
13
|
-
* Judge one `requirePrecedent` evidence object against the session.
|
|
14
|
-
*
|
|
15
|
-
* - `subagent`: exact spawn-kind equality (a kind is a value, not a pattern).
|
|
16
|
-
* - `tool`: the observed tool names matched as a regular expression.
|
|
17
|
-
* - anything else: `undefined` — outside this adapter's vocabulary, including the core's
|
|
18
|
-
* own `command` key, which the covenant compiler evaluates itself.
|
|
19
|
-
*
|
|
20
|
-
* Both vocabularies require the call to have RUN and reported success: a call the
|
|
21
|
-
* covenant blocked, one the human refused, and one that simply failed carry the same
|
|
22
|
-
* outcome, and none of them did the work the discipline demands. The spawn
|
|
23
|
-
* axis therefore reads the joined tool calls — the spawn query carries no outcome —
|
|
24
|
-
* identifying a spawn by the same field the transcript provider does.
|
|
25
|
-
*
|
|
26
|
-
* A malformed value of a known key (non-string, non-compiling pattern) is also
|
|
27
|
-
* `undefined`: the adapter cannot evaluate it, and assembly must fail loud. Answering
|
|
28
|
-
* `false` instead would make the gate permanently unsatisfiable — no amount of actually
|
|
29
|
-
* doing the required action could ever open it, with nothing diagnosing why.
|
|
30
|
-
*/
|
|
31
|
-
export declare function evaluatePrecedent(evidence: Record<string, unknown>, transcript: CanonicalTranscript): boolean | undefined;
|
package/dist/precedent.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adapter-owned precedent evidence evaluator.
|
|
3
|
-
*
|
|
4
|
-
* The core owns the `command` evidence vocabulary and validates only the container
|
|
5
|
-
* shape of everything else; ecosystem values — spawn kinds and tool names — are this
|
|
6
|
-
* adapter's vocabulary, so this package validates and judges them (the
|
|
7
|
-
* `resolveGitAdapterSettings` namespace precedent). The covenant assembly injects this
|
|
8
|
-
* evaluator; `undefined` is the handshake that makes assembly fail closed on a key no
|
|
9
|
-
* adapter recognizes.
|
|
10
|
-
*/
|
|
11
|
-
/**
|
|
12
|
-
* Judge one `requirePrecedent` evidence object against the session.
|
|
13
|
-
*
|
|
14
|
-
* - `subagent`: exact spawn-kind equality (a kind is a value, not a pattern).
|
|
15
|
-
* - `tool`: the observed tool names matched as a regular expression.
|
|
16
|
-
* - anything else: `undefined` — outside this adapter's vocabulary, including the core's
|
|
17
|
-
* own `command` key, which the covenant compiler evaluates itself.
|
|
18
|
-
*
|
|
19
|
-
* Both vocabularies require the call to have RUN and reported success: a call the
|
|
20
|
-
* covenant blocked, one the human refused, and one that simply failed carry the same
|
|
21
|
-
* outcome, and none of them did the work the discipline demands. The spawn
|
|
22
|
-
* axis therefore reads the joined tool calls — the spawn query carries no outcome —
|
|
23
|
-
* identifying a spawn by the same field the transcript provider does.
|
|
24
|
-
*
|
|
25
|
-
* A malformed value of a known key (non-string, non-compiling pattern) is also
|
|
26
|
-
* `undefined`: the adapter cannot evaluate it, and assembly must fail loud. Answering
|
|
27
|
-
* `false` instead would make the gate permanently unsatisfiable — no amount of actually
|
|
28
|
-
* doing the required action could ever open it, with nothing diagnosing why.
|
|
29
|
-
*/
|
|
30
|
-
export function evaluatePrecedent(evidence, transcript) {
|
|
31
|
-
if ('subagent' in evidence) {
|
|
32
|
-
const kind = evidence.subagent;
|
|
33
|
-
if (typeof kind !== 'string') {
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
return transcript
|
|
37
|
-
.findToolCalls()
|
|
38
|
-
.some((call) => call.succeeded === true && call.args.subagent_type === kind);
|
|
39
|
-
}
|
|
40
|
-
if ('tool' in evidence) {
|
|
41
|
-
const pattern = evidence.tool;
|
|
42
|
-
if (typeof pattern !== 'string') {
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
let matcher;
|
|
46
|
-
try {
|
|
47
|
-
matcher = new RegExp(pattern);
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
return undefined;
|
|
51
|
-
}
|
|
52
|
-
return transcript
|
|
53
|
-
.findToolCalls()
|
|
54
|
-
.some((call) => call.succeeded === true && matcher.test(call.name));
|
|
55
|
-
}
|
|
56
|
-
return undefined;
|
|
57
|
-
}
|