agent-harness 0.28.2 → 0.28.4
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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +14 -0
- data/lib/agent_harness/model_compatibility.rb +9 -0
- data/lib/agent_harness/providers/codex.rb +95 -33
- data/lib/agent_harness/providers/kilocode.rb +82 -24
- data/lib/agent_harness/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8d0d7ed4f6ef8391d0b495868dd13f0fbfb84ee496c63ccabdd2900bba04f3d0
|
|
4
|
+
data.tar.gz: 0c8dd8c73feee507a47397dce38c1575889d25413cf4fb93fe66407e736f18c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f22bd0d258e06b594fd56a2cb523f6823d975988002f9b50338b0e46d1ebabd478c90f43fd3acaae8dced8842635c249e8d7b27a02fad45e528fc060b59143c1
|
|
7
|
+
data.tar.gz: 6d8b8f987d8fb747c134743729ed47c105bb4ba0689c350bd8868674c07a3da17df84cfe6a64a137403b022032800fb21477a7319d02820ee996fe5f8b3bd367
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,20 @@
|
|
|
6
6
|
* **auth:** add provider-owned PKCE code-exchange API for Claude OAuth (`AgentHarness::Authentication.exchange_code`). Takes an authorization code plus PKCE verifier (and `redirect_uri`/`client_id`), posts an `authorization_code` grant to the Claude token endpoint, and persists the resulting access/refresh tokens in the native `claudeAiOauth` shape. Adds `exchange_code_supported?` and a `code_exchange` key to `auth_capabilities` ([#266](https://github.com/viamin/agent-harness/issues/266)).
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
## [0.28.4](https://github.com/viamin/agent-harness/compare/agent-harness/v0.28.3...agent-harness/v0.28.4) (2026-07-10)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* Codex model_compatibility returns 'unknown' for models definitively unsupported with subscription auth (e.g. gpt-5.5-pro) ([#287](https://github.com/viamin/agent-harness/issues/287)) ([a6f87ce](https://github.com/viamin/agent-harness/commit/a6f87ce07a2514e069559d57d8496dd323bd0891))
|
|
15
|
+
|
|
16
|
+
## [0.28.3](https://github.com/viamin/agent-harness/compare/agent-harness/v0.28.2...agent-harness/v0.28.3) (2026-07-10)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* Kilocode: default external_directory permission blocks /tmp scratch files, silently killing non-interactive runs ([#285](https://github.com/viamin/agent-harness/issues/285)) ([d703c0a](https://github.com/viamin/agent-harness/commit/d703c0abb7b91fb7a5ba8081e612486edff5e8f5))
|
|
22
|
+
|
|
9
23
|
## [0.28.2](https://github.com/viamin/agent-harness/compare/agent-harness/v0.28.1...agent-harness/v0.28.2) (2026-07-10)
|
|
10
24
|
|
|
11
25
|
|
|
@@ -31,12 +31,21 @@ module AgentHarness
|
|
|
31
31
|
# result. Distinct from :unknown_model — the runner *does* know the
|
|
32
32
|
# model; it just cannot confirm the installed CLI is new enough.
|
|
33
33
|
UNKNOWN_CLI_VERSION_REASON = :cli_version_unknown
|
|
34
|
+
# Issued when the runner needs an auth mode to answer definitively for
|
|
35
|
+
# an auth-gated model but the caller did not supply one. Pairs with
|
|
36
|
+
# :supported_auth_modes on the result details. Distinct from
|
|
37
|
+
# :unknown_model — the runner *does* know the model; it just cannot
|
|
38
|
+
# confirm the requested auth mode is allowed for it.
|
|
39
|
+
UNKNOWN_AUTH_MODE_REASON = :auth_mode_unknown
|
|
34
40
|
# Issued when the runner supports the model but the installed CLI is too
|
|
35
41
|
# old. Pairs with :minimum_cli_version on the result.
|
|
36
42
|
UNSUPPORTED_CLI_VERSION_REASON = :cli_version_too_old
|
|
37
43
|
# Issued when the runner supports the model but the requested auth mode
|
|
38
44
|
# is not part of the runner's contract for it.
|
|
39
45
|
UNSUPPORTED_AUTH_MODE_REASON = :auth_mode_not_supported
|
|
46
|
+
# Issued when the runner accepts the requested auth mode generally, but
|
|
47
|
+
# the specific model is not available under that auth mode.
|
|
48
|
+
UNSUPPORTED_AUTH_MODE_FOR_MODEL_REASON = :unsupported_auth_mode_for_model
|
|
40
49
|
# Default supported reason.
|
|
41
50
|
SUPPORTED_REASON = :supported
|
|
42
51
|
|
|
@@ -27,9 +27,11 @@ module AgentHarness
|
|
|
27
27
|
# instead of relying on whichever model the CLI's default points at.
|
|
28
28
|
DEFAULT_COMPATIBLE_MODEL_ID = "gpt-5-codex"
|
|
29
29
|
|
|
30
|
-
# Known CLI-gated model facts. Each entry
|
|
31
|
-
# CLI version required to drive
|
|
32
|
-
#
|
|
30
|
+
# Known CLI-gated model facts. Each entry may express a minimum Codex
|
|
31
|
+
# CLI version required to drive the model, and/or auth-mode
|
|
32
|
+
# restrictions that are part of the durable runner contract. Keep
|
|
33
|
+
# entries here only when the requirement is durable runner contract
|
|
34
|
+
# knowledge — not
|
|
33
35
|
# provider-side experiments or one-off CLI defaults.
|
|
34
36
|
#
|
|
35
37
|
# The +gpt-5.5+ entry tracks the failure class observed in
|
|
@@ -37,7 +39,8 @@ module AgentHarness
|
|
|
37
39
|
# CLI builds (e.g. 0.115.x) could not drive the +gpt-5.5+ family.
|
|
38
40
|
MODEL_COMPATIBILITY_FACTS = {
|
|
39
41
|
"gpt-5.5" => {minimum_cli_version: "0.116.0"},
|
|
40
|
-
"gpt-5.5-codex" => {minimum_cli_version: "0.116.0"}
|
|
42
|
+
"gpt-5.5-codex" => {minimum_cli_version: "0.116.0"},
|
|
43
|
+
"gpt-5.5-pro" => {auth_modes: [:api_key].freeze}
|
|
41
44
|
}.each_value(&:freeze).freeze
|
|
42
45
|
|
|
43
46
|
# Models that the runner contract considers supported on every Codex
|
|
@@ -276,16 +279,25 @@ module AgentHarness
|
|
|
276
279
|
#
|
|
277
280
|
# Returns an {AgentHarness::ModelCompatibility::Result} for the
|
|
278
281
|
# combination of +model_id+, +auth_mode+, and installed
|
|
279
|
-
# +cli_version+. The contract surfaces
|
|
282
|
+
# +cli_version+. The contract surfaces these concrete outcomes:
|
|
280
283
|
#
|
|
281
|
-
# 1. **Supported** — the model is in {BASELINE_SUPPORTED_MODELS}
|
|
282
|
-
# a known CLI-gated model whose
|
|
283
|
-
#
|
|
284
|
+
# 1. **Supported** — the model is in {BASELINE_SUPPORTED_MODELS}, or
|
|
285
|
+
# a known CLI-gated model whose stated restrictions are met (a
|
|
286
|
+
# minimum Codex CLI version and/or an auth-mode restriction; some
|
|
287
|
+
# gated models express only one of these).
|
|
288
|
+
# 2. **Unsupported due to auth mode** — the requested auth mode is
|
|
289
|
+
# not accepted by the runner at all, or is not available for the
|
|
290
|
+
# specific model (e.g. an api-key-only model requested with
|
|
291
|
+
# subscription auth).
|
|
292
|
+
# 3. **Unsupported due to CLI version** — the model is known to need
|
|
284
293
|
# a newer Codex CLI than was supplied; the result carries
|
|
285
294
|
# +:minimum_cli_version+ so callers can act on it.
|
|
286
|
-
#
|
|
287
|
-
# contract
|
|
288
|
-
#
|
|
295
|
+
# 4. **Unknown / dynamic** — the model is not in this static
|
|
296
|
+
# contract, or a gated model was queried without the gating input
|
|
297
|
+
# needed to answer definitively (an auth mode for an
|
|
298
|
+
# auth-restricted model, or an installed CLI version for a
|
|
299
|
+
# version-gated model). Callers must treat this as "ask the
|
|
300
|
+
# provider" rather than as approval.
|
|
289
301
|
#
|
|
290
302
|
# @param model_id [String, Symbol]
|
|
291
303
|
# @param auth_mode [Symbol, nil] :api_key or :subscription
|
|
@@ -312,42 +324,94 @@ module AgentHarness
|
|
|
312
324
|
|
|
313
325
|
gated_fact = MODEL_COMPATIBILITY_FACTS[normalized_model_id]
|
|
314
326
|
if gated_fact
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
+
supported_auth_modes = gated_fact[:auth_modes]
|
|
328
|
+
if normalized_auth_mode && supported_auth_modes && !supported_auth_modes.include?(normalized_auth_mode)
|
|
329
|
+
return AgentHarness::ModelCompatibility.build_result(
|
|
330
|
+
runner: provider_name,
|
|
331
|
+
model_id: normalized_model_id,
|
|
332
|
+
auth_mode: normalized_auth_mode,
|
|
333
|
+
cli_version: normalized_cli_version,
|
|
334
|
+
supported: false,
|
|
335
|
+
reason: AgentHarness::ModelCompatibility::UNSUPPORTED_AUTH_MODE_FOR_MODEL_REASON,
|
|
336
|
+
fallback_model_id: DEFAULT_COMPATIBLE_MODEL_ID,
|
|
337
|
+
source: :static_contract,
|
|
338
|
+
details: {supported_auth_modes: supported_auth_modes}
|
|
339
|
+
)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# An auth-gated model queried without an auth mode must stay
|
|
343
|
+
# explicit. Returning :supported here would re-introduce the
|
|
344
|
+
# exact permissive false-positive this contract is designed to
|
|
345
|
+
# prevent — `auth_mode` defaults to nil on the public API, so a
|
|
346
|
+
# caller that queries availability without an auth mode and
|
|
347
|
+
# treats `supported? == true` as approval could then schedule an
|
|
348
|
+
# api-key-only model (e.g. gpt-5.5-pro) under subscription. This
|
|
349
|
+
# mirrors the sibling CLI-version dimension below: when the
|
|
350
|
+
# gating input is missing, surface :unknown with the allowed
|
|
351
|
+
# auth modes attached so callers can decide deliberately.
|
|
352
|
+
if supported_auth_modes && normalized_auth_mode.nil?
|
|
327
353
|
return AgentHarness::ModelCompatibility.unknown_result(
|
|
328
354
|
runner: provider_name,
|
|
329
355
|
model_id: normalized_model_id,
|
|
330
356
|
auth_mode: normalized_auth_mode,
|
|
331
357
|
cli_version: normalized_cli_version,
|
|
332
|
-
reason: AgentHarness::ModelCompatibility::
|
|
333
|
-
minimum_cli_version: minimum_version,
|
|
334
|
-
cli_version_requirement: requirement.to_s,
|
|
358
|
+
reason: AgentHarness::ModelCompatibility::UNKNOWN_AUTH_MODE_REASON,
|
|
335
359
|
fallback_model_id: DEFAULT_COMPATIBLE_MODEL_ID,
|
|
336
|
-
source: :static_contract
|
|
360
|
+
source: :static_contract,
|
|
361
|
+
details: {supported_auth_modes: supported_auth_modes}
|
|
337
362
|
)
|
|
338
363
|
end
|
|
339
364
|
|
|
340
|
-
|
|
365
|
+
minimum_version = gated_fact[:minimum_cli_version]
|
|
366
|
+
if minimum_version
|
|
367
|
+
requirement = Gem::Requirement.new(">= #{minimum_version}")
|
|
368
|
+
comparable_version = comparable_cli_version(normalized_cli_version)
|
|
369
|
+
|
|
370
|
+
# A CLI-gated model without a comparable installed version must
|
|
371
|
+
# stay explicit. Returning :supported here would re-introduce the
|
|
372
|
+
# exact `gpt-5.5` failure class this contract is designed to
|
|
373
|
+
# prevent — a caller that cannot supply a version would get
|
|
374
|
+
# `supported? == true` and may still schedule a run onto an old
|
|
375
|
+
# CLI (e.g. 0.115.x). Surface :unknown with the requirement
|
|
376
|
+
# attached so callers can decide deliberately.
|
|
377
|
+
if comparable_version.nil?
|
|
378
|
+
return AgentHarness::ModelCompatibility.unknown_result(
|
|
379
|
+
runner: provider_name,
|
|
380
|
+
model_id: normalized_model_id,
|
|
381
|
+
auth_mode: normalized_auth_mode,
|
|
382
|
+
cli_version: normalized_cli_version,
|
|
383
|
+
reason: AgentHarness::ModelCompatibility::UNKNOWN_CLI_VERSION_REASON,
|
|
384
|
+
minimum_cli_version: minimum_version,
|
|
385
|
+
cli_version_requirement: requirement.to_s,
|
|
386
|
+
fallback_model_id: DEFAULT_COMPATIBLE_MODEL_ID,
|
|
387
|
+
source: :static_contract
|
|
388
|
+
)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
unless requirement.satisfied_by?(comparable_version)
|
|
392
|
+
return AgentHarness::ModelCompatibility.build_result(
|
|
393
|
+
runner: provider_name,
|
|
394
|
+
model_id: normalized_model_id,
|
|
395
|
+
auth_mode: normalized_auth_mode,
|
|
396
|
+
cli_version: normalized_cli_version,
|
|
397
|
+
supported: false,
|
|
398
|
+
reason: AgentHarness::ModelCompatibility::UNSUPPORTED_CLI_VERSION_REASON,
|
|
399
|
+
minimum_cli_version: minimum_version,
|
|
400
|
+
cli_version_requirement: requirement.to_s,
|
|
401
|
+
fallback_model_id: DEFAULT_COMPATIBLE_MODEL_ID,
|
|
402
|
+
source: :static_contract
|
|
403
|
+
)
|
|
404
|
+
end
|
|
405
|
+
|
|
341
406
|
return AgentHarness::ModelCompatibility.build_result(
|
|
342
407
|
runner: provider_name,
|
|
343
408
|
model_id: normalized_model_id,
|
|
344
409
|
auth_mode: normalized_auth_mode,
|
|
345
410
|
cli_version: normalized_cli_version,
|
|
346
|
-
supported:
|
|
347
|
-
reason: AgentHarness::ModelCompatibility::
|
|
411
|
+
supported: true,
|
|
412
|
+
reason: AgentHarness::ModelCompatibility::SUPPORTED_REASON,
|
|
348
413
|
minimum_cli_version: minimum_version,
|
|
349
414
|
cli_version_requirement: requirement.to_s,
|
|
350
|
-
fallback_model_id: DEFAULT_COMPATIBLE_MODEL_ID,
|
|
351
415
|
source: :static_contract
|
|
352
416
|
)
|
|
353
417
|
end
|
|
@@ -359,8 +423,6 @@ module AgentHarness
|
|
|
359
423
|
cli_version: normalized_cli_version,
|
|
360
424
|
supported: true,
|
|
361
425
|
reason: AgentHarness::ModelCompatibility::SUPPORTED_REASON,
|
|
362
|
-
minimum_cli_version: minimum_version,
|
|
363
|
-
cli_version_requirement: requirement.to_s,
|
|
364
426
|
source: :static_contract
|
|
365
427
|
)
|
|
366
428
|
end
|
|
@@ -13,6 +13,18 @@ module AgentHarness
|
|
|
13
13
|
DEFAULT_VERSION = "7.1.3"
|
|
14
14
|
SUPPORTED_VERSION_REQUIREMENT = "= #{DEFAULT_VERSION}"
|
|
15
15
|
STRUCTURED_EVENT_TYPES = %w[text error step_finish result usage].freeze
|
|
16
|
+
# Kilo CLI (an OpenCode fork) ships the same external_directory
|
|
17
|
+
# permission category as OpenCode, defaulting to "ask" for anything
|
|
18
|
+
# outside the project dir. In non-interactive execution there is no
|
|
19
|
+
# human to answer the prompt, so reads of scratch files under /tmp are
|
|
20
|
+
# auto-rejected and the agent silently loses access to its own scratch
|
|
21
|
+
# output, killing the run without a recoverable error. This default rule
|
|
22
|
+
# broadens the allowlist to all of /tmp so the agent can read back files
|
|
23
|
+
# it (or its sub-agents) wrote there. See #282 (precedent: #277/#280).
|
|
24
|
+
DEFAULT_PERMISSION_EXTERNAL_DIRECTORY_PATTERN = "/tmp/**"
|
|
25
|
+
DEFAULT_PERMISSION_CONFIG = {
|
|
26
|
+
"external_directory" => {DEFAULT_PERMISSION_EXTERNAL_DIRECTORY_PATTERN => "allow"}
|
|
27
|
+
}.freeze
|
|
16
28
|
USAGE_EVENT_TYPES = %w[result usage].freeze
|
|
17
29
|
TOKEN_USAGE_KEYS = %w[
|
|
18
30
|
input_tokens
|
|
@@ -106,21 +118,6 @@ module AgentHarness
|
|
|
106
118
|
end
|
|
107
119
|
end
|
|
108
120
|
|
|
109
|
-
# Kilo CLI (an opencode fork) ships the same `external_directory`
|
|
110
|
-
# permission category as OpenCode, defaulting to `ask` for anything
|
|
111
|
-
# outside the project dir. In non-interactive execution there is no human
|
|
112
|
-
# to answer the resulting permission prompt, so reads/writes of scratch
|
|
113
|
-
# files under `/tmp/*` via the dedicated read/write/edit tools are
|
|
114
|
-
# auto-rejected — even though bash I/O to the same paths succeeds (shell
|
|
115
|
-
# redirection is not a tool call). The agent then silently loses access to
|
|
116
|
-
# its own scratch output and never completes the task. This default rule
|
|
117
|
-
# broadens the allowlist to all of `/tmp` so the agent can read back files
|
|
118
|
-
# it (or its sub-agents) wrote there.
|
|
119
|
-
DEFAULT_PERMISSION_EXTERNAL_DIRECTORY_PATTERN = "/tmp/**"
|
|
120
|
-
DEFAULT_PERMISSION_RULE = {
|
|
121
|
-
"external_directory" => {DEFAULT_PERMISSION_EXTERNAL_DIRECTORY_PATTERN => "allow"}
|
|
122
|
-
}.freeze
|
|
123
|
-
|
|
124
121
|
def name
|
|
125
122
|
"kilocode"
|
|
126
123
|
end
|
|
@@ -185,10 +182,9 @@ module AgentHarness
|
|
|
185
182
|
provider_name = options[:provider_name] || "openai"
|
|
186
183
|
model_id = options[:model_id]
|
|
187
184
|
|
|
188
|
-
config = {provider
|
|
189
|
-
config[
|
|
190
|
-
config
|
|
191
|
-
apply_default_external_directory_permission(config)
|
|
185
|
+
config = {"provider" => {provider_name => {}}}
|
|
186
|
+
config["model"] = "#{provider_name}/#{model_id}" if model_id
|
|
187
|
+
apply_default_external_directory_permission(config, options)
|
|
192
188
|
|
|
193
189
|
config.to_json
|
|
194
190
|
end
|
|
@@ -219,6 +215,31 @@ module AgentHarness
|
|
|
219
215
|
cmd
|
|
220
216
|
end
|
|
221
217
|
|
|
218
|
+
# Materialize the permissive external_directory permission (and any other
|
|
219
|
+
# runtime config extras) to +~/.config/kilocode/kilo.json+ before the CLI
|
|
220
|
+
# boots. This is the runtime path that actually fixes #282: without it the
|
|
221
|
+
# permission lives only in {config_file_content}'s return value, which no
|
|
222
|
+
# execution caller invokes, so Kilo would boot with stock "ask" defaults
|
|
223
|
+
# and auto-reject /tmp reads in non-interactive runs. Mirrors the OpenCode
|
|
224
|
+
# precedent in #280.
|
|
225
|
+
def build_execution_preparation(options)
|
|
226
|
+
runtime = options[:provider_runtime]
|
|
227
|
+
return nil unless runtime
|
|
228
|
+
|
|
229
|
+
config_payload = kilocode_config_payload(runtime)
|
|
230
|
+
return nil unless config_payload
|
|
231
|
+
|
|
232
|
+
ExecutionPreparation.new(
|
|
233
|
+
file_writes: [
|
|
234
|
+
{
|
|
235
|
+
path: kilocode_config_path,
|
|
236
|
+
content: serialize_kilocode_config(config_payload),
|
|
237
|
+
mode: 0o600
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
)
|
|
241
|
+
end
|
|
242
|
+
|
|
222
243
|
def parse_response(result, duration:)
|
|
223
244
|
output = result.stdout
|
|
224
245
|
tokens = nil
|
|
@@ -349,12 +370,49 @@ module AgentHarness
|
|
|
349
370
|
|
|
350
371
|
private
|
|
351
372
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
373
|
+
# Default-merge a permissive external_directory permission into the
|
|
374
|
+
# generated Kilo config unless the caller takes responsibility for
|
|
375
|
+
# permission config. A caller-supplied permission (passed via the
|
|
376
|
+
# +:permission+ option or already present on the config under the
|
|
377
|
+
# +"permission"+ key, e.g. from runtime metadata config extras) is
|
|
378
|
+
# honored verbatim and never overridden, and an invalid or empty
|
|
379
|
+
# caller permission is ignored in favor of the default rule.
|
|
380
|
+
def apply_default_external_directory_permission(config, options = {})
|
|
381
|
+
caller_permission = options[:permission] || options["permission"] || config["permission"]
|
|
382
|
+
if caller_permission.is_a?(Hash) && !caller_permission.empty?
|
|
383
|
+
config["permission"] = deep_dup(caller_permission)
|
|
384
|
+
return
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
config["permission"] = deep_dup(DEFAULT_PERMISSION_CONFIG)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def kilocode_config_path
|
|
391
|
+
"~/.config/kilocode/kilo.json"
|
|
392
|
+
end
|
|
356
393
|
|
|
357
|
-
|
|
394
|
+
def kilocode_config_payload(runtime)
|
|
395
|
+
metadata = runtime.metadata
|
|
396
|
+
config_extras = metadata[:config] || metadata["config"] || {}
|
|
397
|
+
unless config_extras.is_a?(Hash)
|
|
398
|
+
raise ArgumentError,
|
|
399
|
+
"Kilocode runtime metadata config must be a Hash of provider-specific extras (got #{config_extras.class})"
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
payload = stringify_keys(config_extras)
|
|
403
|
+
payload["model"] = runtime.model if runtime.model
|
|
404
|
+
apply_default_external_directory_permission(payload)
|
|
405
|
+
payload.empty? ? nil : payload
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def serialize_kilocode_config(payload)
|
|
409
|
+
JSON.pretty_generate(payload)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def stringify_keys(hash)
|
|
413
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
414
|
+
result[key.to_s] = value
|
|
415
|
+
end
|
|
358
416
|
end
|
|
359
417
|
|
|
360
418
|
def heartbeat_hook_script(heartbeat_file_path)
|