polydeukes 0.8.0 → 0.9.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/covenant/shell-mod.d.ts +15 -4
- package/dist/covenant/shell-mod.js +63 -8
- package/dist/covenant/transcript-mod.js +8 -5
- package/dist/docs/how-to/connect-surfaces.ko.md +19 -7
- package/dist/docs/how-to/connect-surfaces.md +24 -12
- package/dist/docs/index.json +96 -96
- package/dist/docs/reference/cli/init.ko.md +10 -7
- package/dist/docs/reference/cli/init.md +16 -11
- package/dist/docs/reference/packages/adapter-codex.ko.md +32 -15
- package/dist/docs/reference/packages/adapter-codex.md +37 -19
- package/dist/docs/reference/packages/polydeukes.ko.md +2 -2
- package/dist/docs/reference/packages/polydeukes.md +3 -3
- package/dist/docs/troubleshooting.ko.md +9 -5
- package/dist/docs/troubleshooting.md +10 -6
- package/package.json +2 -2
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Analyzes the command-line strings of *shell* tool calls (names and arg keys are injected
|
|
5
5
|
* values, never source literals) per simple command: the fixed detection rules catch writes
|
|
6
6
|
* to a protected path, undecidable structures (opaque mentions, opaque write targets) fail
|
|
7
|
-
* closed,
|
|
7
|
+
* closed, read-only proof absolves safe reads, and every other protected-path
|
|
8
8
|
* mention breaks — "mention + unproven = block". It judges only its own axis: a non-shell
|
|
9
9
|
* tool call is upheld, since the tool axis belongs to the self-mod meta-covenant and
|
|
10
10
|
* run-all co-existence depends on that boundary.
|
|
@@ -18,7 +18,7 @@ import type { CovenantRegistration, MetaCovenantRegistration } from './dispatch.
|
|
|
18
18
|
* `protectedPaths` are literal path strings; `shellToolNames` are the tool names whose
|
|
19
19
|
* calls carry shell lines; `commandArgNames` are the `args` keys those lines live under;
|
|
20
20
|
* `readOnlyCommands` are allowlist entries — space-separated word sequences (`'cat'`,
|
|
21
|
-
* `'git
|
|
21
|
+
* `'git status'`). Empty-string entries in every list are ignored (an unguarded `''` would
|
|
22
22
|
* match every path / tool / arg / command).
|
|
23
23
|
*/
|
|
24
24
|
export type ShellModificationSpec = {
|
|
@@ -38,6 +38,16 @@ export type ShellModificationSpec = {
|
|
|
38
38
|
* redirect-free truncating write. `git status`/`git grep` reject `--output`, so they stay.
|
|
39
39
|
*/
|
|
40
40
|
export declare const DEFAULT_READ_ONLY_COMMANDS: string[];
|
|
41
|
+
/**
|
|
42
|
+
* True when a configured allowlist has the same effective entries as the shipped default.
|
|
43
|
+
* Conditional readers belong to that default contract and stay disabled for replacements.
|
|
44
|
+
*/
|
|
45
|
+
export declare function usesDefaultReadOnlyCommands(commands: string[]): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Prove the finite argument-sensitive readers that cannot be represented by a leading-word
|
|
48
|
+
* allowlist. Every word must be transparent because these readers inspect their later words.
|
|
49
|
+
*/
|
|
50
|
+
export declare function matchesConditionalReadOnlyCommand(command: SimpleCommand): boolean;
|
|
41
51
|
/**
|
|
42
52
|
* True when the command's leading words match the allowlist entry's word sequence. Exported
|
|
43
53
|
* so the transcript judge's allowlist clause absolves reads by this exact comparison instead
|
|
@@ -59,8 +69,9 @@ export declare function judgeShellModification(input: CovenantInput, spec: Shell
|
|
|
59
69
|
/**
|
|
60
70
|
* `ShellModRegistrationSpec` — the assembly values baked into the registration. The call
|
|
61
71
|
* set is not among them: the dispatcher supplies it to the judge at call time.
|
|
62
|
-
* `readOnlyCommands` REPLACES {@link DEFAULT_READ_ONLY_COMMANDS} when given — no merge
|
|
63
|
-
*
|
|
72
|
+
* `readOnlyCommands` REPLACES {@link DEFAULT_READ_ONLY_COMMANDS} when given — no merge.
|
|
73
|
+
* Any replacement, including a superset made by spreading the default, disables the finite
|
|
74
|
+
* argument-sensitive readers because their proof belongs to the exact shipped default.
|
|
64
75
|
*/
|
|
65
76
|
export type ShellModRegistrationSpec = {
|
|
66
77
|
protectedPaths: string[];
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Analyzes the command-line strings of *shell* tool calls (names and arg keys are injected
|
|
5
5
|
* values, never source literals) per simple command: the fixed detection rules catch writes
|
|
6
6
|
* to a protected path, undecidable structures (opaque mentions, opaque write targets) fail
|
|
7
|
-
* closed,
|
|
7
|
+
* closed, read-only proof absolves safe reads, and every other protected-path
|
|
8
8
|
* mention breaks — "mention + unproven = block". It judges only its own axis: a non-shell
|
|
9
9
|
* tool call is upheld, since the tool axis belongs to the self-mod meta-covenant and
|
|
10
10
|
* run-all co-existence depends on that boundary.
|
|
@@ -39,6 +39,58 @@ export const DEFAULT_READ_ONLY_COMMANDS = [
|
|
|
39
39
|
'git status',
|
|
40
40
|
'git grep',
|
|
41
41
|
];
|
|
42
|
+
const FIND_WRITE_OR_EXECUTE_ACTIONS = new Set([
|
|
43
|
+
'-delete',
|
|
44
|
+
'-exec',
|
|
45
|
+
'-execdir',
|
|
46
|
+
'-ok',
|
|
47
|
+
'-okdir',
|
|
48
|
+
'-fprint',
|
|
49
|
+
'-fprint0',
|
|
50
|
+
'-fprintf',
|
|
51
|
+
'-fls',
|
|
52
|
+
]);
|
|
53
|
+
function normalizedReadOnlyCommands(commands) {
|
|
54
|
+
return [
|
|
55
|
+
...new Set(commands
|
|
56
|
+
.map((entry) => entry
|
|
57
|
+
.split(/\s+/)
|
|
58
|
+
.filter((word) => word !== '')
|
|
59
|
+
.join(' '))
|
|
60
|
+
.filter((entry) => entry !== '')),
|
|
61
|
+
].sort();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* True when a configured allowlist has the same effective entries as the shipped default.
|
|
65
|
+
* Conditional readers belong to that default contract and stay disabled for replacements.
|
|
66
|
+
*/
|
|
67
|
+
export function usesDefaultReadOnlyCommands(commands) {
|
|
68
|
+
const configured = normalizedReadOnlyCommands(commands);
|
|
69
|
+
const shipped = normalizedReadOnlyCommands(DEFAULT_READ_ONLY_COMMANDS);
|
|
70
|
+
return (configured.length === shipped.length && configured.every((entry, i) => entry === shipped[i]));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Prove the finite argument-sensitive readers that cannot be represented by a leading-word
|
|
74
|
+
* allowlist. Every word must be transparent because these readers inspect their later words.
|
|
75
|
+
*/
|
|
76
|
+
export function matchesConditionalReadOnlyCommand(command) {
|
|
77
|
+
if (command.words.length === 0 || command.words.some((word) => word.opaque))
|
|
78
|
+
return false;
|
|
79
|
+
const name = commandBasename(command.words[0]);
|
|
80
|
+
if (name === 'git')
|
|
81
|
+
return command.words[1]?.text === 'ls-files';
|
|
82
|
+
if (name === 'find') {
|
|
83
|
+
return !command.words.some((word) => FIND_WRITE_OR_EXECUTE_ACTIONS.has(word.text));
|
|
84
|
+
}
|
|
85
|
+
if (name !== 'sed')
|
|
86
|
+
return false;
|
|
87
|
+
const script = command.words[2]?.text;
|
|
88
|
+
return (command.words[1]?.text === '-n' &&
|
|
89
|
+
script !== undefined &&
|
|
90
|
+
/^\d+(?:,\d+)?p$/.test(script) &&
|
|
91
|
+
command.words.length > 3 &&
|
|
92
|
+
command.words.slice(3).every((word) => !word.text.startsWith('-')));
|
|
93
|
+
}
|
|
42
94
|
// The rule set is fixed, not injectable: dropping a rule from an assembly would be a
|
|
43
95
|
// detection hole, and no consumer needs a subset.
|
|
44
96
|
const MUTATION_RULES = [redirectWriteRule, teeRule, sedInPlaceRule];
|
|
@@ -65,9 +117,9 @@ export function matchesReadOnlyEntry(command, entry) {
|
|
|
65
117
|
* Judge one simple command. Returns the break reason, or null when the command contributes
|
|
66
118
|
* to uphold. The clause order below is normative: each clause exists to be reached before
|
|
67
119
|
* the next one can absolve. `lineFullyRead` is false when the line carried a span the
|
|
68
|
-
* tokenizer could not read, which withholds
|
|
120
|
+
* tokenizer could not read, which withholds read-only proof.
|
|
69
121
|
*/
|
|
70
|
-
function judgeCommand(command, protectedPaths, readOnlyEntries, lineFullyRead) {
|
|
122
|
+
function judgeCommand(command, protectedPaths, readOnlyEntries, conditionalReadersEnabled, lineFullyRead) {
|
|
71
123
|
// (a) Precise rules: a detected mutation whose target carries a protected path breaks.
|
|
72
124
|
for (const rule of MUTATION_RULES) {
|
|
73
125
|
for (const target of rule.detect(command)) {
|
|
@@ -100,16 +152,18 @@ function judgeCommand(command, protectedPaths, readOnlyEntries, lineFullyRead) {
|
|
|
100
152
|
if (command.redirects.some((r) => r.operator.includes('>') && r.target.opaque)) {
|
|
101
153
|
return `opaque redirect target alongside protected path ${mentioned}`;
|
|
102
154
|
}
|
|
103
|
-
// (e) Read-only
|
|
104
|
-
// (`eval`/`sh -c …`) re-parses its string args, so it can never be
|
|
105
|
-
// if it was injected into the allowlist. Its mention falls through
|
|
155
|
+
// (e) Read-only proof: the allowlist or a finite argument-sensitive reader absolves the
|
|
156
|
+
// mention. A nested shell (`eval`/`sh -c …`) re-parses its string args, so it can never be
|
|
157
|
+
// proven read-only even if it was injected into the allowlist. Its mention falls through
|
|
158
|
+
// to the backstop. A line
|
|
106
159
|
// carrying an unread span is refused the same way: what the scanner never read could be
|
|
107
160
|
// anything, so no head vouches for it.
|
|
108
161
|
const first = command.words[0];
|
|
109
162
|
const firstBasename = first !== undefined ? commandBasename(first) : '';
|
|
110
163
|
if (lineFullyRead &&
|
|
111
164
|
!isNestedShellCommand(firstBasename) &&
|
|
112
|
-
readOnlyEntries.some((entry) => matchesReadOnlyEntry(command, entry))
|
|
165
|
+
(readOnlyEntries.some((entry) => matchesReadOnlyEntry(command, entry)) ||
|
|
166
|
+
(conditionalReadersEnabled && matchesConditionalReadOnlyCommand(command)))) {
|
|
113
167
|
return null;
|
|
114
168
|
}
|
|
115
169
|
// (f) Backstop — mention + unproven = block.
|
|
@@ -133,6 +187,7 @@ export function judgeShellModification(input, spec) {
|
|
|
133
187
|
const readOnlyEntries = spec.readOnlyCommands
|
|
134
188
|
.map((entry) => entry.split(/\s+/).filter((word) => word !== ''))
|
|
135
189
|
.filter((entry) => entry.length > 0);
|
|
190
|
+
const conditionalReadersEnabled = usesDefaultReadOnlyCommands(spec.readOnlyCommands);
|
|
136
191
|
for (const call of input.toolCalls) {
|
|
137
192
|
if (!shellToolNames.includes(call.name)) {
|
|
138
193
|
continue;
|
|
@@ -167,7 +222,7 @@ export function judgeShellModification(input, spec) {
|
|
|
167
222
|
}
|
|
168
223
|
}
|
|
169
224
|
for (const command of commands) {
|
|
170
|
-
const reason = judgeCommand(command, protectedPaths, readOnlyEntries, unread.length === 0);
|
|
225
|
+
const reason = judgeCommand(command, protectedPaths, readOnlyEntries, conditionalReadersEnabled, unread.length === 0);
|
|
171
226
|
if (reason !== null)
|
|
172
227
|
return { upheld: false, reason };
|
|
173
228
|
}
|
|
@@ -13,7 +13,7 @@ import { isNestedShellCommand, tokenizeCommandLine } from './bash-line.js';
|
|
|
13
13
|
import { pathCandidates, pathSegments, provenChangePath, resolveDotSegments, someStringValue, untokenizableLineCandidates, } from './mention.js';
|
|
14
14
|
import { commandBasename, redirectWriteRule, sedInPlaceRule, teeRule } from './mutation-rules.js';
|
|
15
15
|
import { outcomeFromVerdict, UNJUDGEABLE_OUTCOME } from './run-covenant.js';
|
|
16
|
-
import { DEFAULT_READ_ONLY_COMMANDS, matchesReadOnlyEntry } from './shell-mod.js';
|
|
16
|
+
import { DEFAULT_READ_ONLY_COMMANDS, matchesConditionalReadOnlyCommand, matchesReadOnlyEntry, usesDefaultReadOnlyCommands, } from './shell-mod.js';
|
|
17
17
|
// The rule set is fixed, assembled exactly as shell-mod assembles it: dropping one would be
|
|
18
18
|
// a detection hole, and the two judges must not diverge on what counts as a write.
|
|
19
19
|
const MUTATION_RULES = [redirectWriteRule, teeRule, sedInPlaceRule];
|
|
@@ -55,6 +55,7 @@ function resolveTranscript(spec) {
|
|
|
55
55
|
readOnlyEntries: spec.readOnlyCommands
|
|
56
56
|
.map((entry) => entry.split(/\s+/).filter((word) => word !== ''))
|
|
57
57
|
.filter((entry) => entry.length > 0),
|
|
58
|
+
conditionalReadersEnabled: usesDefaultReadOnlyCommands(spec.readOnlyCommands),
|
|
58
59
|
};
|
|
59
60
|
}
|
|
60
61
|
/**
|
|
@@ -96,7 +97,7 @@ function argsNameTranscript(value, transcript) {
|
|
|
96
97
|
* it has to break before the allowlist gets a chance to absolve it. The letters skip `(c)`
|
|
97
98
|
* because this ladder has no opaque-mention clause, matching the shell ladder's numbering
|
|
98
99
|
* rather than closing the gap. `lineFullyRead` is false when the line carried a span the
|
|
99
|
-
* tokenizer could not read, which withholds
|
|
100
|
+
* tokenizer could not read, which withholds read-only proof.
|
|
100
101
|
*/
|
|
101
102
|
function judgeCommand(command, transcript, lineFullyRead) {
|
|
102
103
|
// (a) Precise rules: a detected mutation whose target is the transcript breaks.
|
|
@@ -118,15 +119,17 @@ function judgeCommand(command, transcript, lineFullyRead) {
|
|
|
118
119
|
if (command.redirects.some((r) => r.operator.includes('>') && r.target.opaque)) {
|
|
119
120
|
return `opaque redirect target alongside the session transcript ${transcript.path}`;
|
|
120
121
|
}
|
|
121
|
-
// (e) Read-only
|
|
122
|
-
// nested shell (`eval`/`sh -c …`) re-parses its string args,
|
|
122
|
+
// (e) Read-only proof: the allowlist or a finite argument-sensitive reader absolves the
|
|
123
|
+
// mention in every spelling. A nested shell (`eval`/`sh -c …`) re-parses its string args,
|
|
124
|
+
// so it is never provably a read.
|
|
123
125
|
// A line carrying an unread span is refused the same way: reading the session is free, but
|
|
124
126
|
// only on a line we finished reading.
|
|
125
127
|
const first = command.words[0];
|
|
126
128
|
const firstBasename = first !== undefined ? commandBasename(first) : '';
|
|
127
129
|
if (lineFullyRead &&
|
|
128
130
|
!isNestedShellCommand(firstBasename) &&
|
|
129
|
-
transcript.readOnlyEntries.some((entry) => matchesReadOnlyEntry(command, entry))
|
|
131
|
+
(transcript.readOnlyEntries.some((entry) => matchesReadOnlyEntry(command, entry)) ||
|
|
132
|
+
(transcript.conditionalReadersEnabled && matchesConditionalReadOnlyCommand(command)))) {
|
|
130
133
|
return null;
|
|
131
134
|
}
|
|
132
135
|
// (f) Backstop — mention + unproven = block.
|
|
@@ -56,23 +56,35 @@ Codex에서 개발할 때 씁니다.
|
|
|
56
56
|
제공합니다. 먼저 `pdks init`으로 초기 파일을 만든 뒤 Codex 등록 산출물을 씁니다.
|
|
57
57
|
3. Codex에서 `/hooks`로 생성된 훅을 승인합니다. 승인하기 전까지는 훅을 건너뜁니다.
|
|
58
58
|
|
|
59
|
-
Codex 프로젝트에는 `.codex/hooks/covenant-pretooluse.mjs` 위임자와 `.codex/hooks.json`의
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
Codex 프로젝트에는 `.codex/hooks/covenant-pretooluse.mjs` 위임자와 `.codex/hooks.json`의
|
|
60
|
+
`PreToolUse`, `UserPromptSubmit`, `PostToolUse`, `SessionEnd` 항목이 생깁니다. 이 JSON은
|
|
61
|
+
덮어쓰지 않고 병합합니다. 사용자 항목, 같은 항목의 다른 handler, 다른 이벤트, 설치기가 모르는
|
|
62
|
+
키는 그대로 둡니다. 초기 설정은 기본적으로 `.codex/hooks`를 보호합니다.
|
|
62
63
|
|
|
63
64
|
**승인은 선택이 아닙니다.** Codex는 훅 정의의 해시로 신뢰를 기록하므로, 새로 쓴 훅은 검토
|
|
64
65
|
대상으로 표시되고 누군가 승인하기 전까지 건너뛰어집니다. 그때까지는 아무것도 판정되지
|
|
65
66
|
않습니다. `init`은 실행할 때마다 바이트가 같은 명령 문자열을 쓰므로, 다시 설치해도 이미 받은
|
|
66
67
|
승인이 무효가 되지 않습니다.
|
|
67
68
|
|
|
68
|
-
Codex는 모든 파일 편집을 `apply_patch` 하나로 정규화하고, 경로 인자가 아니라 패치 텍스트를
|
|
69
|
+
Codex는 훅에 도달하는 모든 파일 편집을 `apply_patch` 하나로 정규화하고, 경로 인자가 아니라 패치 텍스트를
|
|
69
70
|
보냅니다. `Edit`과 `Write`는 훅 파일에 적을 수 있는 matcher 별칭이며 도구 이름으로 도착하지
|
|
70
71
|
않습니다. 패치 하나가 여러 파일을 건드리면 파일마다 IR 원소 하나가 실리고, 그중 하나라도
|
|
71
72
|
차단되면 호출 전체가 차단됩니다.
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
**승인된 훅도 Code Mode는 덮지 못합니다.** codex-cli 0.154에서 Code Mode `exec` 호출과 그
|
|
75
|
+
JavaScript 안에 중첩된 도구 호출은 `PreToolUse`에 도달하지 않으므로
|
|
76
|
+
([openai/codex#23411](https://github.com/openai/codex/issues/23411)), 그 경로로 이루어진 편집은
|
|
77
|
+
`/hooks`에 훅이 Active로 표시되는 동안에도 판정되지도 기록되지도 않습니다. `init`이 이 사실을
|
|
78
|
+
`note:` 줄로 출력하고, [패키지 레퍼런스](../reference/packages/adapter-codex.ko.md#limits)가
|
|
79
|
+
다른 선언된 한계와 함께 나열합니다.
|
|
80
|
+
|
|
81
|
+
Codex의 대화 기록 형식은 계속 불안정하므로 해석하지 않습니다. 대신 `UserPromptSubmit`이
|
|
82
|
+
시각을 붙인 사람 메시지를, `PostToolUse`가 완료된 도구 호출을 `.polydeukes/codex-sessions/`
|
|
83
|
+
아래의 어댑터 소유 파일에 기록하고 `SessionEnd`가 지웁니다. 의도한 차단을 풀려면 설정된 증인
|
|
84
|
+
토큰을 첫 줄에 단독으로 보낸 뒤 호출을 다시 시도합니다. 복구 메시지가
|
|
85
|
+
`UserPromptSubmit` 증거가 없다고 알리면 재시도가 증인 밸브에 닿지 못하므로 자신의 터미널을
|
|
86
|
+
사용합니다. 세션 어댑터를 한 프로젝트에 둘 이상 설치하면 호출마다 판정기가 두 번 실행될 수
|
|
87
|
+
있습니다.
|
|
76
88
|
|
|
77
89
|
<a id="change-set-surface"></a>
|
|
78
90
|
## 변경 집합 표면
|
|
@@ -58,24 +58,36 @@ Use this when the project is developed in Codex.
|
|
|
58
58
|
runs `pdks init` for the scaffold, then writes the Codex registration artifacts.
|
|
59
59
|
3. Approve the generated hook with `/hooks` in Codex. Until you do, it is skipped.
|
|
60
60
|
|
|
61
|
-
A Codex tree gets a delegator at `.codex/hooks/covenant-pretooluse.mjs` and
|
|
62
|
-
`.codex/hooks.json`.
|
|
63
|
-
|
|
64
|
-
`.codex/hooks`
|
|
61
|
+
A Codex tree gets a delegator at `.codex/hooks/covenant-pretooluse.mjs` and entries for
|
|
62
|
+
`PreToolUse`, `UserPromptSubmit`, `PostToolUse`, and `SessionEnd` in `.codex/hooks.json`.
|
|
63
|
+
That JSON is merged, not overwritten: user entries, sibling handlers, other events, and keys
|
|
64
|
+
the installer does not know stay where they are. The scaffold config protects `.codex/hooks`
|
|
65
|
+
by default.
|
|
65
66
|
|
|
66
67
|
**Approval is not optional.** Codex records trust against the hash of a hook's definition, so a
|
|
67
68
|
newly written hook is listed for review and skipped until someone approves it — until then
|
|
68
69
|
nothing is judged. `init` writes a byte-identical command string on every run, so a re-install
|
|
69
70
|
does not invalidate an approval you already gave.
|
|
70
71
|
|
|
71
|
-
Codex normalises every file edit into one tool, `apply_patch`, and sends
|
|
72
|
-
than a path argument. `Edit` and `Write` are matcher aliases you may
|
|
73
|
-
they never arrive as the tool name. One patch that touches several
|
|
74
|
-
per file, and any one of them blocking blocks the whole call.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
Codex normalises every file edit that reaches the hook into one tool, `apply_patch`, and sends
|
|
73
|
+
the patch text rather than a path argument. `Edit` and `Write` are matcher aliases you may
|
|
74
|
+
write in the hooks file; they never arrive as the tool name. One patch that touches several
|
|
75
|
+
files carries one IR element per file, and any one of them blocking blocks the whole call.
|
|
76
|
+
|
|
77
|
+
**An approved hook does not cover Code Mode.** In codex-cli 0.154 a Code Mode `exec` dispatch,
|
|
78
|
+
and the tool calls nested in its JavaScript, do not reach `PreToolUse`
|
|
79
|
+
([openai/codex#23411](https://github.com/openai/codex/issues/23411)), so an edit made that way
|
|
80
|
+
is neither judged nor logged even while `/hooks` shows the hook Active. `init` prints this as a
|
|
81
|
+
`note:` line; the [package reference](../reference/packages/adapter-codex.md#limits)
|
|
82
|
+
lists it with the other declared limits.
|
|
83
|
+
|
|
84
|
+
Codex's transcript format remains unstable and is never parsed. Instead, `UserPromptSubmit`
|
|
85
|
+
records timestamped human messages and `PostToolUse` records completed tool calls in an
|
|
86
|
+
adapter-owned file under `.polydeukes/codex-sessions/`; `SessionEnd` removes it. To release an
|
|
87
|
+
intentional block, send the configured witness token alone on the first line, then retry the
|
|
88
|
+
call. If the recovery message says no `UserPromptSubmit` evidence was recorded, use your own
|
|
89
|
+
terminal because the retry cannot reach the witness valve. Installing more than one session
|
|
90
|
+
adapter in one project can run the judge twice per call.
|
|
79
91
|
|
|
80
92
|
|
|
81
93
|
<a id="change-set-surface"></a>
|