claude-dev-env 8.43.4 → 8.43.6
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/_shared/advisor/advisor-protocol.md +2 -2
- package/_shared/advisor/reference/cli-chain.md +9 -12
- package/_shared/advisor/reference/lifecycle.md +1 -1
- package/_shared/advisor/reference/third-party-bind.md +3 -3
- package/bin/install.mjs +39 -3
- package/bin/install.shared-settings.test.mjs +46 -0
- package/bin/install.test.mjs +1 -1
- package/docs/second-claude-account.md +11 -13
- package/package.json +1 -1
- package/scripts/_code_review_test_support.py +29 -39
- package/scripts/claude_account_worker.py +46 -297
- package/scripts/claude_account_worker_process.py +18 -154
- package/scripts/claude_account_worker_report.py +8 -4
- package/scripts/claude_chain_usage.py +7 -271
- package/scripts/codec_forwarding_test_support.py +11 -65
- package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +6 -24
- package/scripts/invoke_code_review.py +34 -56
- package/scripts/policy_lint/config/approved_test_pairs.py +1 -0
- package/scripts/resolve_worker_spawn.py +40 -72
- package/scripts/test_account_broker_guard.py +0 -4
- package/scripts/test_claude_account_worker.py +105 -350
- package/scripts/test_claude_chain_usage.py +50 -510
- package/scripts/test_dispatcher_profile_import.py +5 -9
- package/scripts/test_invoke_code_review.py +7 -6
- package/scripts/test_invoke_code_review_chain.py +26 -0
- package/scripts/test_invoke_code_review_cli.py +8 -8
- package/scripts/test_invoke_code_review_codec.py +3 -11
- package/scripts/test_invoke_code_review_contract.py +5 -1
- package/scripts/test_resolve_worker_spawn.py +118 -265
- package/scripts/test_resolve_worker_spawn_codec.py +9 -21
- package/scripts/tests/test_code_review_constants.py +2 -2
- package/scripts/tests/test_policy_lint_rules_pairing.py +13 -0
- package/scripts/claude_account_choice.py +0 -422
- package/scripts/claude_chain_runner.py +0 -1289
- package/scripts/test_claude_account_choice.py +0 -355
- package/scripts/test_claude_chain_runner.py +0 -1839
|
@@ -138,8 +138,8 @@ Drift signals and the per-host re-spawn / re-bind steps: [`reference/lifecycle.m
|
|
|
138
138
|
|
|
139
139
|
## CLI chain
|
|
140
140
|
|
|
141
|
-
The shared runner is `python "$HOME/.claude/scripts/
|
|
142
|
-
|
|
141
|
+
The shared runner is `python "$HOME/.claude/scripts/account_broker.py" run --product claude --report <path> -- claude <args...>`.
|
|
142
|
+
Account choice, failover, the tier-to-alias table, brief piping, and `--resume` session handling: [`reference/cli-chain.md`](reference/cli-chain.md).
|
|
143
143
|
|
|
144
144
|
**Third-party host:** the primary bind and consult path; the walk order and fail-closed rule live in [`reference/third-party-bind.md`](reference/third-party-bind.md).
|
|
145
145
|
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
# CLI Claude-chain
|
|
2
2
|
|
|
3
3
|
Detail behind the `## CLI chain` section of [`advisor-protocol.md`](../advisor-protocol.md).
|
|
4
|
-
The shared runner is `python "$HOME/.claude/scripts/
|
|
4
|
+
The shared runner is `python "$HOME/.claude/scripts/account_broker.py" run --product claude --report <path> -- claude <args...>`.
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## Account choice
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
**Root advisor bind and consult** (the third-party host path): ordered-account mode. A non-usage failure terminates with `terminal_status=advisor_blocked`, exit code 4 on the CLI.
|
|
14
|
-
|
|
15
|
-
**General chain calls** (non-root automation): keep the default usage-ranked mode.
|
|
8
|
+
The broker applies the main account guard and chooses an eligible extra account
|
|
9
|
+
by remaining usage. On a usage-limit response, it tries another account. If no
|
|
10
|
+
account has room, it reports a reset time and exits with code 3. A non-usage
|
|
11
|
+
Claude failure has `JobOutcome.status == "advisor_blocked"`; the CLI exits with
|
|
12
|
+
code 4. The report path receives the broker's decision and attempt events.
|
|
16
13
|
|
|
17
14
|
## Tier-to-alias map
|
|
18
15
|
|
|
@@ -38,8 +35,8 @@ Drop the file once the consult completes.
|
|
|
38
35
|
|
|
39
36
|
## Session resume
|
|
40
37
|
|
|
41
|
-
Read the `session_id` out of the first call's JSON events.
|
|
38
|
+
Read the `session_id` out of the first call's JSON events or `JobOutcome.session_id`.
|
|
42
39
|
Pass it to `-p --resume <session_id> --output-format json` on every later consult — `-p` stays on the resume call too, since it is still a non-interactive invocation.
|
|
43
|
-
A session store belongs to the
|
|
40
|
+
A session store belongs to the account that minted it. The broker keeps account affinity when that account has room. If the account reaches a usage limit, a `--resume` on another account can fail.
|
|
44
41
|
Treat that failure as starting over.
|
|
45
42
|
Resend the charter plus a compact recap of the consults since the last one, capture the new `session_id` the fresh call returns, and continue from there.
|
|
@@ -22,6 +22,6 @@ Every other consumer reaches it by message alone; spawn, respawn, and shutdown b
|
|
|
22
22
|
|
|
23
23
|
The orchestrating session owns the CLI advisor bind for the whole run: first bind, re-bind on drift or lost `session_id`, and fail-closed report when the bound path cannot serve.
|
|
24
24
|
|
|
25
|
-
**Re-bind on drift.** If a reply shows a stale picture, the task pivots, or `--resume` fails after a usage-limit failover
|
|
25
|
+
**Re-bind on drift.** If a reply shows a stale picture, the task pivots, or `--resume` fails after a usage-limit failover, re-bind Opus through `python "$HOME/.claude/scripts/account_broker.py" run --product claude --report <path> -- claude <args...>` with the charter plus a compact recap of consults so far.
|
|
26
26
|
Capture the new `session_id`, and log a fresh Opus walk with `result: "cli"` on success, then Astra when that rung is open. An Astra re-bind uses `codex_astra_advisor.py --bind --enable-astra` with the same charter and recap, and records `result: "codex"` on success.
|
|
27
27
|
Executors keep reporting to the orchestrating session; advisor binding stays with that session alone.
|
|
@@ -22,15 +22,15 @@ When the Astra flag is off, follow the Claude-chain steps below.
|
|
|
22
22
|
3. **CLI bind (primary path):** for Opus, pipe a charter file into:
|
|
23
23
|
|
|
24
24
|
```
|
|
25
|
-
python "$HOME/.claude/scripts/
|
|
25
|
+
python "$HOME/.claude/scripts/account_broker.py" run --product claude --report <path> -- claude -p --model <alias> --effort <effort> --output-format json
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
Use `--model opus --effort` with the value of `ADVISOR_EFFORT` (default `xhigh`) on Opus.
|
|
29
29
|
User-facing wording follows [`rules/asd-ste100-language.md`](../../../rules/asd-ste100-language.md).
|
|
30
|
-
|
|
30
|
+
Account choice, failover, and the `advisor_blocked` status are in [`cli-chain.md`](cli-chain.md).
|
|
31
31
|
4. Stop at the first successful bind.
|
|
32
32
|
Record `{tier, result: "cli"}` for Opus or `{tier: "Astra", result: "codex"}` for the Astra helper, and set `selected_tier` to that tier.
|
|
33
|
-
Persist `session_id` from the JSON events
|
|
33
|
+
Persist `session_id` from the JSON events or `JobOutcome.session_id`; reply text is the `type == "result"` event's `.result` field.
|
|
34
34
|
Run every bind and every later consult with cwd set to the repo root the work is for. Claude sessions are project-scoped by working directory.
|
|
35
35
|
5. **Fail closed:** when every candidate fails (chain exhausted, `advisor_blocked`, or model unavailable), set `selected_tier = null` and a `fallback_reason`, report that the advisor is unreachable, and **stop**. ENDORSE / CORRECTION / PLAN / STOP come only from a bound advisor.
|
|
36
36
|
6. Assemble and paste each executor's Advisor block from [`advisor-block.md`](advisor-block.md). Executors report to the orchestrating session; that session consults the bound advisor and relays the four-signal reply.
|
package/bin/install.mjs
CHANGED
|
@@ -1363,15 +1363,16 @@ function commandRunsHookOfRootSharingSettings(normalizedCommand, relativePath, s
|
|
|
1363
1363
|
while (tailStart !== -1) {
|
|
1364
1364
|
const characterAfterPath = normalizedCommand[tailStart + hookPathTail.length];
|
|
1365
1365
|
if (characterAfterPath === undefined || commandArgumentBoundary.test(characterAfterPath)) {
|
|
1366
|
-
let
|
|
1366
|
+
let rootPath;
|
|
1367
1367
|
if (characterAfterPath === '"' || characterAfterPath === "'") {
|
|
1368
|
-
|
|
1368
|
+
rootPath = quotedRootPathBeforeTail(normalizedCommand.slice(0, tailStart), characterAfterPath);
|
|
1369
1369
|
} else {
|
|
1370
|
+
let rootStart = tailStart;
|
|
1370
1371
|
while (rootStart > 0 && !commandArgumentBoundary.test(normalizedCommand[rootStart - 1])) {
|
|
1371
1372
|
rootStart--;
|
|
1372
1373
|
}
|
|
1374
|
+
rootPath = normalizedCommand.slice(rootStart, tailStart);
|
|
1373
1375
|
}
|
|
1374
|
-
const rootPath = normalizedCommand.slice(rootStart, tailStart);
|
|
1375
1376
|
const rootSettingsRealPath = rootPath
|
|
1376
1377
|
? realPathOrNull(join(rootPath, SETTINGS_FILE_NAME))
|
|
1377
1378
|
: null;
|
|
@@ -1384,6 +1385,41 @@ function commandRunsHookOfRootSharingSettings(normalizedCommand, relativePath, s
|
|
|
1384
1385
|
return false;
|
|
1385
1386
|
}
|
|
1386
1387
|
|
|
1388
|
+
/**
|
|
1389
|
+
* How each quote style of a hook path argument joins the segments around an
|
|
1390
|
+
* escaped character, keyed by the quote, in the forward-slash command form.
|
|
1391
|
+
*/
|
|
1392
|
+
const QUOTED_ARGUMENT_ESCAPES = {
|
|
1393
|
+
'"': { segmentJoin: '"^%"', joinedCharacter: '%' },
|
|
1394
|
+
"'": { segmentJoin: "'/''", joinedCharacter: "'" },
|
|
1395
|
+
};
|
|
1396
|
+
|
|
1397
|
+
/**
|
|
1398
|
+
* Decode the root part of a quoted argument that `commandArgumentFromPath`
|
|
1399
|
+
* wrote, read from the command text that ends where the hook path tail starts.
|
|
1400
|
+
*
|
|
1401
|
+
* The win32 form splits a path at each `%` into `"a"^%"b"`. The POSIX form
|
|
1402
|
+
* escapes each `'` as `'\''`, which reads `'/''` once backslashes become
|
|
1403
|
+
* forward slashes.
|
|
1404
|
+
*
|
|
1405
|
+
* @param {string} commandBeforeTail Normalized command text up to the hook path tail.
|
|
1406
|
+
* @param {string} closingQuote The quote that closes the argument after the tail.
|
|
1407
|
+
* @returns {string} The root path with each escaped character restored.
|
|
1408
|
+
*/
|
|
1409
|
+
function quotedRootPathBeforeTail(commandBeforeTail, closingQuote) {
|
|
1410
|
+
const { segmentJoin, joinedCharacter } = QUOTED_ARGUMENT_ESCAPES[closingQuote];
|
|
1411
|
+
const segments = [];
|
|
1412
|
+
let segmentEnd = commandBeforeTail.length;
|
|
1413
|
+
let openingQuote = commandBeforeTail.lastIndexOf(closingQuote, segmentEnd - 1);
|
|
1414
|
+
while (commandBeforeTail.slice(openingQuote - segmentJoin.length + 1, openingQuote + 1) === segmentJoin) {
|
|
1415
|
+
segments.unshift(commandBeforeTail.slice(openingQuote + 1, segmentEnd));
|
|
1416
|
+
segmentEnd = openingQuote - segmentJoin.length + 1;
|
|
1417
|
+
openingQuote = commandBeforeTail.lastIndexOf(closingQuote, segmentEnd - 1);
|
|
1418
|
+
}
|
|
1419
|
+
segments.unshift(commandBeforeTail.slice(openingQuote + 1, segmentEnd));
|
|
1420
|
+
return segments.join(joinedCharacter);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1387
1423
|
function commandTailEndsAtManagedHook(normalizedCommand, relativePath) {
|
|
1388
1424
|
if (commandHoldsPathAtBoundary(normalizedCommand, `/.claude/hooks/${relativePath}`)) {
|
|
1389
1425
|
return true;
|
|
@@ -243,3 +243,49 @@ test('the merge prunes a quoted hook of a sharing root whose path holds a space'
|
|
|
243
243
|
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
244
244
|
}
|
|
245
245
|
});
|
|
246
|
+
|
|
247
|
+
for (const { shape, rootDirectoryName, platform } of [
|
|
248
|
+
{ shape: 'a percent sign', rootDirectoryName: 'Jane 100% Doe', platform: 'win32' },
|
|
249
|
+
{ shape: 'an apostrophe', rootDirectoryName: "Jane O'Doe", platform: 'linux' },
|
|
250
|
+
]) {
|
|
251
|
+
test(`the merge prunes a ${platform}-quoted hook of a sharing root whose path holds ${shape}`, t => {
|
|
252
|
+
const sandboxRoot = mkdtempSync(join(tmpdir(), 'cdev-shared-settings-escape-'));
|
|
253
|
+
try {
|
|
254
|
+
const installRoot = join(sandboxRoot, 'install-root');
|
|
255
|
+
const sharingRoot = join(sandboxRoot, rootDirectoryName, 'profiles', 'second');
|
|
256
|
+
for (const eachRoot of [installRoot, sharingRoot]) mkdirSync(eachRoot, { recursive: true });
|
|
257
|
+
const settingsPath = join(installRoot, 'settings.json');
|
|
258
|
+
writeFileSync(settingsPath, '{}\n');
|
|
259
|
+
try {
|
|
260
|
+
symlinkSync(settingsPath, join(sharingRoot, 'settings.json'), 'file');
|
|
261
|
+
} catch (linkError) {
|
|
262
|
+
t.skip(`this host cannot create a file symlink (${linkError.code})`);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const hooksConfig = {
|
|
266
|
+
hooks: {
|
|
267
|
+
SessionStart: [{
|
|
268
|
+
matcher: '',
|
|
269
|
+
hooks: [{ type: 'command', command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/start.py' }],
|
|
270
|
+
}],
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
const forwardSlashed = rootPath => rootPath.replace(/\\/g, '/');
|
|
274
|
+
const sharingHookPath = `${forwardSlashed(sharingRoot)}/hooks/session/start.py`;
|
|
275
|
+
const sharingCommand = `python3 ${commandArgumentFromPath(sharingHookPath, platform)}`;
|
|
276
|
+
const settings = {
|
|
277
|
+
hooks: {
|
|
278
|
+
SessionStart: [{ matcher: '', hooks: [{ type: 'command', command: sharingCommand }] }],
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
mergeHooksIntoSettings(settings, hooksConfig, forwardSlashed(installRoot), 'python3', settingsPath);
|
|
283
|
+
|
|
284
|
+
const allCommands = settings.hooks.SessionStart.flatMap(group => group.hooks.map(hook => hook.command));
|
|
285
|
+
assert.ok(!allCommands.includes(sharingCommand), `the sharing root entry ${sharingCommand} is pruned`);
|
|
286
|
+
assert.equal(allCommands.length, 1, 'only the install root entry stays after the merge');
|
|
287
|
+
} finally {
|
|
288
|
+
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
package/bin/install.test.mjs
CHANGED
|
@@ -216,7 +216,7 @@ test('core includeDirectories ships _shared and scripts for advisor protocol and
|
|
|
216
216
|
);
|
|
217
217
|
assert.ok(
|
|
218
218
|
CORE_INCLUDE_DIRECTORIES.includes('scripts'),
|
|
219
|
-
'scripts must ship with --only core so
|
|
219
|
+
'scripts must ship with --only core so account_broker.py is available for advisor CLI fallback',
|
|
220
220
|
);
|
|
221
221
|
});
|
|
222
222
|
|
|
@@ -9,12 +9,12 @@ keeps its own sign-in and history.
|
|
|
9
9
|
| File | What it does |
|
|
10
10
|
|---|---|
|
|
11
11
|
| `scripts/claude_account_profile.py` | Links each shared entry of `~/.claude` into the profile `~/.claude-profiles/ev`, moves stale copies into `.replaced/<time>/`, and writes the `claude-ev.cmd` launcher into `~/.local/bin` |
|
|
12
|
-
| `scripts/
|
|
13
|
-
| `scripts/claude_account_worker.py` | Runs one headless Claude worker
|
|
14
|
-
| `scripts/claude_account_worker_process.py` |
|
|
12
|
+
| `scripts/account_broker.py` | Reads the account roster and usage meters, chooses an account, and runs Claude with that account's home |
|
|
13
|
+
| `scripts/claude_account_worker.py` | Runs one headless Claude worker through the broker |
|
|
14
|
+
| `scripts/claude_account_worker_process.py` | Sends the brief to the broker and measures the run |
|
|
15
15
|
| `scripts/claude_account_worker_report.py` | Builds and writes the JSON report a worker leaves behind |
|
|
16
16
|
| `scripts/dev_env_scripts_constants/claude_account_worker_constants.py` | The worker's flag names, defaults, exit codes, and report keys |
|
|
17
|
-
| `scripts/dev_env_scripts_constants/claude_account_constants.py` | The profile name, the entries that stay per account, and the
|
|
17
|
+
| `scripts/dev_env_scripts_constants/claude_account_constants.py` | The profile name, the entries that stay per account, and the account thresholds |
|
|
18
18
|
|
|
19
19
|
The launcher sets `CLAUDE_CONFIG_DIR` to the profile and passes every argument to
|
|
20
20
|
`claude`, so `claude-ev -p "..."` runs like `claude -p "..."` on the second
|
|
@@ -43,19 +43,17 @@ to spend leftover usage that expires soon.
|
|
|
43
43
|
|---|---|
|
|
44
44
|
| Main week resets within 24 hours, main under 90% of its week, main under 50% of its 5-hour window | main |
|
|
45
45
|
| Otherwise, second under 95% of its week and under 90% of its 5-hour window | second |
|
|
46
|
-
| Second meter unreadable |
|
|
46
|
+
| Second meter unreadable | wait until the meter can be read |
|
|
47
47
|
| Otherwise | wait, with the next reset time |
|
|
48
48
|
|
|
49
49
|
An unreadable main meter never picks main.
|
|
50
50
|
|
|
51
51
|
```
|
|
52
|
-
python packages/claude-dev-env/scripts/
|
|
53
|
-
{"account": "second", "config_dir": "C:\\Users\\me\\.claude-profiles\\ev", "reason": "...",
|
|
54
|
-
"meters": {"main": {"session_used_percent": 12.0, "session_resets_at": "...",
|
|
55
|
-
"weekly_used_percent": 34.0, "weekly_resets_at": "..."},
|
|
56
|
-
"second": null}}
|
|
52
|
+
python packages/claude-dev-env/scripts/account_broker.py choose --product claude
|
|
57
53
|
```
|
|
58
54
|
|
|
59
|
-
The
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
The JSON output has a `decision` object and an `accounts` list. Each account's
|
|
56
|
+
`meters` holds its remaining percent and reset time for the 5-hour window and
|
|
57
|
+
the week. An unreadable meter appears as `null`. A wait decision has
|
|
58
|
+
`action: "wait"` and a `resets_at` timestamp. The broker exits with code 3 on
|
|
59
|
+
wait. The worker writes the same reset time to its report.
|
package/package.json
CHANGED
|
@@ -24,13 +24,8 @@ _SCRIPTS_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
|
24
24
|
if _SCRIPTS_DIRECTORY not in sys.path:
|
|
25
25
|
sys.path.insert(0, _SCRIPTS_DIRECTORY)
|
|
26
26
|
|
|
27
|
-
import claude_chain_runner as chain_runner # noqa: E402
|
|
28
27
|
import invoke_code_review as invoker # noqa: E402
|
|
29
|
-
from
|
|
30
|
-
from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
|
|
31
|
-
TERMINAL_STATUS_CHAIN_EXHAUSTED,
|
|
32
|
-
TERMINAL_STATUS_SERVED,
|
|
33
|
-
)
|
|
28
|
+
from dev_env_scripts_constants.account_broker_constants import JobOutcome, Product, WAIT_EXIT_CODE
|
|
34
29
|
from dev_env_scripts_constants.code_review_constants import ( # noqa: E402
|
|
35
30
|
CLI_SESSION_MODEL_FLAG,
|
|
36
31
|
CODE_REVIEW_MODEL_ALIAS,
|
|
@@ -111,27 +106,29 @@ def claude_served(
|
|
|
111
106
|
*,
|
|
112
107
|
returncode: int = FIXTURE_CHAIN_RETURNCODE,
|
|
113
108
|
stdout: str = FIXTURE_CHAIN_STDOUT,
|
|
114
|
-
) ->
|
|
115
|
-
return
|
|
116
|
-
served_command=FIXTURE_SERVED_COMMAND,
|
|
109
|
+
) -> JobOutcome:
|
|
110
|
+
return JobOutcome(
|
|
117
111
|
returncode=returncode,
|
|
118
112
|
stdout=stdout,
|
|
119
113
|
stderr="",
|
|
120
|
-
|
|
121
|
-
|
|
114
|
+
account_name=FIXTURE_SERVED_COMMAND,
|
|
115
|
+
attempts=((FIXTURE_SERVED_COMMAND, "served"),),
|
|
116
|
+
status="served" if returncode == 0 else "advisor_blocked",
|
|
117
|
+
session_id=None,
|
|
118
|
+
wait_reset_at=None,
|
|
122
119
|
)
|
|
123
120
|
|
|
124
121
|
|
|
125
|
-
def claude_failed() ->
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
returncode=FIXTURE_FAILED_RETURNCODE,
|
|
122
|
+
def claude_failed() -> JobOutcome:
|
|
123
|
+
return JobOutcome(
|
|
124
|
+
returncode=WAIT_EXIT_CODE,
|
|
129
125
|
stdout="",
|
|
130
|
-
stderr="
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
126
|
+
stderr="",
|
|
127
|
+
account_name=None,
|
|
128
|
+
attempts=((FIXTURE_SERVED_COMMAND, "usage_limited"),),
|
|
129
|
+
status="exhausted",
|
|
130
|
+
session_id=None,
|
|
131
|
+
wait_reset_at=None,
|
|
135
132
|
)
|
|
136
133
|
|
|
137
134
|
|
|
@@ -179,7 +176,7 @@ class SeamCallLog:
|
|
|
179
176
|
@dataclass
|
|
180
177
|
class _SeamConfiguration:
|
|
181
178
|
host_profile: str
|
|
182
|
-
claude_outcome:
|
|
179
|
+
claude_outcome: JobOutcome | BaseException | None
|
|
183
180
|
should_dirty_tree_on_chain: bool
|
|
184
181
|
working_directory: Path | None
|
|
185
182
|
|
|
@@ -220,23 +217,21 @@ def _build_host_profile_seam(
|
|
|
220
217
|
|
|
221
218
|
def _build_claude_seam(
|
|
222
219
|
call_log: SeamCallLog, configuration: _SeamConfiguration
|
|
223
|
-
) -> Callable[...,
|
|
220
|
+
) -> Callable[..., JobOutcome]:
|
|
224
221
|
def fake_claude(
|
|
225
|
-
all_claude_arguments: list[str],
|
|
226
|
-
) ->
|
|
222
|
+
product: Product, all_claude_arguments: list[str], **options: object
|
|
223
|
+
) -> JobOutcome:
|
|
224
|
+
assert product is Product.CLAUDE
|
|
227
225
|
call_log.claude_calls += 1
|
|
228
|
-
call_log.claude_arguments = list(all_claude_arguments)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
timeout=timeout_seconds,
|
|
234
|
-
check=False,
|
|
235
|
-
)
|
|
226
|
+
call_log.claude_arguments = list(all_claude_arguments[1:])
|
|
227
|
+
call_log.is_stdin_empty = options.get("stdin_text") == ""
|
|
228
|
+
if options.get("cwd") is not None:
|
|
229
|
+
call_log.claude_working_directory = Path(options["cwd"])
|
|
230
|
+
call_log.all_observed_working_directories.append(Path(options["cwd"]))
|
|
236
231
|
_record_dirty_tree(configuration)
|
|
237
232
|
if isinstance(configuration.claude_outcome, BaseException):
|
|
238
233
|
raise configuration.claude_outcome
|
|
239
|
-
assert isinstance(configuration.claude_outcome,
|
|
234
|
+
assert isinstance(configuration.claude_outcome, JobOutcome)
|
|
240
235
|
return configuration.claude_outcome
|
|
241
236
|
|
|
242
237
|
return fake_claude
|
|
@@ -284,18 +279,13 @@ def _apply_seams(
|
|
|
284
279
|
"review_claude_runner",
|
|
285
280
|
_build_claude_seam(call_log, configuration),
|
|
286
281
|
)
|
|
287
|
-
monkeypatch.setattr(
|
|
288
|
-
chain_runner,
|
|
289
|
-
"chain_subprocess_runner",
|
|
290
|
-
_build_subprocess_seam(call_log),
|
|
291
|
-
)
|
|
292
282
|
|
|
293
283
|
|
|
294
284
|
def install_seams(
|
|
295
285
|
monkeypatch: pytest.MonkeyPatch,
|
|
296
286
|
*,
|
|
297
287
|
host_profile: str = HOST_PROFILE_CLAUDE,
|
|
298
|
-
claude_outcome:
|
|
288
|
+
claude_outcome: JobOutcome | BaseException | None = None,
|
|
299
289
|
should_dirty_tree_on_chain: bool = False,
|
|
300
290
|
working_directory: Path | None = None,
|
|
301
291
|
) -> SeamCallLog:
|