Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 27 additions & 12 deletions docs/user/global-host-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ The profile schema is the existing repository check/test environment surface:
| `FKST_DEVLOOP_INTEGRATION_BRANCH` | `github-devloop` | Per-device integration branch. |
| `FKST_DEVLOOP_INTAKE_MILESTONE_NUMBERS` | optional | Comma-separated GitHub milestone numbers eligible for an initial issue claim. |
| `FKST_DEVLOOP_LOCAL_TEST_COMMAND` | `github-devloop` | Repository-root local verification gate run by implement/fix workers before handoff. |
| `FKST_DEVLOOP_CACHE_PREPARATION_COMMAND` | optional | Trusted-base cache preparation run for each implementation worktree before Codex starts. |
| `FKST_DEVLOOP_CACHE_PREPARATION_COMMAND` | optional | Trusted-base cache preparation run before Codex and before each candidate or detached-base local verification. |

Those five keys together are the whole trusted-author allowlist: `FKST_GITHUB_BOT_LOGIN`
(a required anchor) ∪ `FKST_DEVLOOP_MANAGED_BOT_LOGINS` ∪ `FKST_GITHUB_AUTHORIZED_LOGINS`,
Expand Down Expand Up @@ -124,17 +124,32 @@ command shape is not safely preflightable; it does not execute the test suite du
## Implementation cache preparation

`FKST_DEVLOOP_CACHE_PREPARATION_COMMAND` optionally names a repository-owned executable or task target
that hydrates build caches in an implementation worktree. `github-devloop` runs it after refreshing
`.fkst/substrate-ref` and before starting the Codex wall-clock deadline. The command runs from the
trusted supervisor project root with the candidate path in
`FKST_DEVLOOP_CACHE_PREPARATION_WORKTREE`, never from candidate-controlled content. The command has
a 10-minute timeout; a nonzero exit fails the implementation attempt loudly.

The command must be idempotent because redelivery or a later implementation attempt can run it again
for an existing worktree. It must treat the candidate path as untrusted data and must not execute
candidate-controlled build scripts. Persistent cache ownership and reuse remain repository concerns,
so trusted base logic can use the repository's native cache mechanism without teaching
`github-devloop` about `.lake`, `node_modules`, `target`, or other toolchain-specific directories.
that hydrates build caches for a worktree. `github-devloop` runs it after refreshing
`.fkst/substrate-ref` and before starting the Codex wall-clock deadline, then runs it again immediately
before each candidate local verification. A detached raw-base attribution probe receives the same
preparation after its tree has been fully materialized and before its local gate runs. The command
runs from the trusted supervisor project root with the worktree path in
`FKST_DEVLOOP_CACHE_PREPARATION_WORKTREE`, never from candidate-controlled content. The command has a
10-minute timeout; a nonzero exit fails the implementation attempt loudly.

The command must be idempotent and concurrency-safe because redelivery, repeated verification, or
another worktree can run it concurrently. It must derive artifact reuse and invalidation from the
repository's complete native build action inputs, including the materialized tree, toolchain,
dependencies, configuration, and relevant environment. A repeated identical action may reuse
artifacts; a changed input must invalidate the affected artifacts. It must treat the worktree path as
untrusted data and must not execute candidate-controlled build scripts. Persistent cache ownership
and reuse remain repository concerns, so trusted base logic can use the repository's native cache
mechanism without teaching `github-devloop` about `.lake`, `node_modules`, `target`, or other
toolchain-specific directories. Cache preparation never substitutes a verification verdict: every
candidate and detached-base local gate still executes and produces its own typed result.

This repository's canonical provider is `scripts/warm_pinned_bin.sh`. For a Cargo worktree, it uses
Git's absolute common-directory identity to connect the ignored worktree `target` path to the source
checkout's `target`. Cargo remains the artifact authority: its native fingerprints invalidate source,
dependency, toolchain, configuration, and relevant environment changes, and its target lock provides
concurrent single-flight compilation. The provider does not invoke Cargo against candidate content.
For a non-Cargo worktree that needs the pinned framework binary, it reads `.fkst/substrate-ref` from
the trusted project root, never from the candidate worktree, before using the pinned binary cache.

## Repository checks and tests

Expand Down
7 changes: 4 additions & 3 deletions docs/user/host-profile.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ FKST_DEVLOOP_ROLLUP_MERGE=auto
# default is scripts/run.sh test-affected; configure the repository's local CI-equivalent gate
# when that executable is absent. Multi-step logic belongs inside the target, not this value.
# export FKST_DEVLOOP_LOCAL_TEST_COMMAND='make preflight'
# Optional idempotent cache hydration run from trusted base content before Codex starts.
# The candidate path is available as FKST_DEVLOOP_CACHE_PREPARATION_WORKTREE.
# export FKST_DEVLOOP_CACHE_PREPARATION_COMMAND='make prepare-cache'
# Optional idempotent, concurrency-safe cache hydration run from trusted base content before Codex
# and before each candidate or detached-base local gate. The current worktree path is available as
# FKST_DEVLOOP_CACHE_PREPARATION_WORKTREE; cache identity must cover the complete native build action.
# export FKST_DEVLOOP_CACHE_PREPARATION_COMMAND='scripts/warm_pinned_bin.sh'

# Optional: skip local BIN freshness builds when another process manages the build.
# FKST_NO_AUTOBUILD=1
8 changes: 6 additions & 2 deletions packages/github-devloop/departments/implement/harvest.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ local devloop_commands = require("devloop.commands")
local config = require("devloop.config")
local payloads_builders = require("devloop.payloads.builders")
local branch_progress = require("departments.implement.branch_progress")
local cache_preparation = require("departments.implement.cache_preparation")
local substrate_pin = require("departments.implement.substrate_pin")
local local_iteration_result = require("departments.implement.local_iteration_result")
local local_iteration_verdict = require("departments.implement.local_iteration_verdict")
Expand Down Expand Up @@ -180,7 +181,8 @@ function M.implementation_refusal_outcome(ready, receipt, attempt, started_at, e
}
end

local function execute_local_iteration_check(worktree, base_head, observe_worktree, exec)
local function execute_local_iteration_check(worktree, base_head, observe_worktree, exec, prepare_cache)
(prepare_cache or cache_preparation.run)(worktree)
local quoted_worktree = devloop_base._shell_single_quote(worktree)
local command = "cd " .. quoted_worktree
if observe_worktree then
Expand Down Expand Up @@ -208,7 +210,8 @@ function M.local_iteration_check(worktree, base_head, deps)
if base_head == nil or tostring(base_head) == "" then
error("github-devloop: local-iteration-base-missing: candidate base head is required")
end
return execute_local_iteration_check(worktree, base_head, true, deps and deps.exec or nil)
return execute_local_iteration_check(
worktree, base_head, true, deps and deps.exec or nil, deps and deps.prepare_cache or nil)
end

local function worktree_unavailability_from_command(result)
Expand Down Expand Up @@ -467,6 +470,7 @@ function M.clean_branch_head(base_head, branch)
end

function M.commit_dirty_worktree(repo, issue_number, ready, worktree, branch)
cache_preparation.run(worktree)
local add_result = devloop_commands.git_add_all(worktree, 30)
if add_result.exit_code ~= 0 then
error("github-devloop: git-add-failed: git add failed: " .. tostring(add_result.stderr))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,23 @@ return {

t.is_true(calls[1].cmd:find("export BASE='abc123' && make preflight", 1, true) ~= nil)
end,

test_local_gate_prepares_the_exact_worktree_before_verification = function()
local sequence = {}
mock_local_test_command()

harvest.local_iteration_check("/tmp/fkst worktree", "abc123", {
prepare_cache = function(worktree)
table.insert(sequence, "prepare:" .. worktree)
end,
exec = function()
table.insert(sequence, "verify")
return { stdout = "", stderr = "FKST_LOCAL_ITERATION_RESULT:v2:PASS:NONE\n", exit_code = 0 }
end,
})

t.eq(#sequence, 2)
t.eq(sequence[1], "prepare:/tmp/fkst worktree")
t.eq(sequence[2], "verify")
end,
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local h = require("tests.devloop_helpers")
local codex_jsonl = require("testkit_internal.codex_jsonl")
local t = h.t
local ready = h.ready
local opts = h.opts
Expand All @@ -12,31 +13,34 @@ local mock_git_status = h.mock_git_status
local mock_git_commit = h.mock_git_commit
local mock_result_checkpoint = h.mock_result_checkpoint
local mock_branch_diff_paths = h.mock_branch_diff_paths
local mock_force_clean = h.mock_force_clean
local count_calls = h.count_calls

local cache_command = "make prepare-cache"
local cache_command = "scripts/warm_pinned_bin.sh"
local cache_command_env = 'printf %s "$FKST_DEVLOOP_CACHE_PREPARATION_COMMAND"'
local project_root_env = 'printf %s "$FKST_PROJECT_ROOT"'
local trusted_repository_root = "/trusted/repository"
local current_base_pin = "2222222222222222222222222222222222222222"
local stale_branch_pin = "1111111111111111111111111111111111111111"

local function mock_cache_command(result)
t.mock_command(cache_command_env, {
stdout = cache_command,
stderr = "",
exit_code = 0,
})
t.mock_command(cache_command, result or {
stdout = "cache ready\n",
stderr = "",
exit_code = 0,
})
t.mock_command(project_root_env, {
stdout = trusted_repository_root,
stderr = "",
exit_code = 0,
})
local function mock_cache_command(result, invocation_count)
for _ = 1, invocation_count or 1 do
t.mock_command(cache_command_env, {
stdout = cache_command,
stderr = "",
exit_code = 0,
})
t.mock_command(cache_command, result or {
stdout = "cache ready\n",
stderr = "",
exit_code = 0,
})
t.mock_command(project_root_env, {
stdout = trusted_repository_root,
stderr = "",
exit_code = 0,
})
end
end

local function mock_cache_command_unset()
Expand All @@ -56,13 +60,14 @@ local function command_index(needle)
return nil
end

local function command_call(needle)
for _, call in ipairs(t.command_calls()) do
local function command_indices(needle)
local indices = {}
for index, call in ipairs(t.command_calls()) do
if tostring(call.rendered or ""):find(needle, 1, true) ~= nil then
return call
table.insert(indices, index)
end
end
return nil
return indices
end

local function command_env(call, name)
Expand All @@ -81,29 +86,78 @@ local function mock_successful_candidate(event)
mock_git_commit("def456", branch)
end

local function mock_candidate_local_red()
t.mock_command("FKST_IMPLEMENTATION_WORKTREE_RESULT:v1:ENTERED", {
stdout = "",
stderr = "FKST_LOCAL_ITERATION_RESULT:v2:FAIL:SEMANTIC\ncandidate failed\n",
exit_code = 1,
})
end

local function mock_codex_success_without_local_iteration(message)
t.mock_command("codex exec", {
stdout = codex_jsonl.final_message(message),
stderr = "",
exit_code = 0,
})
end

local function mock_base_probe(worktree)
local base_probe = worktree .. "-base-probe"
for _ = 1, 2 do
mock_force_clean(base_probe)
end
t.mock_command("mkdir -p", { stdout = "", stderr = "", exit_code = 0 })
t.mock_command("git worktree add --detach", {
stdout = "Preparing worktree (detached HEAD abc123)\n",
stderr = "",
exit_code = 0,
})
t.mock_command("rev-parse HEAD", { stdout = "abc123\n", stderr = "", exit_code = 0 })
t.mock_command("status --porcelain", { stdout = "", stderr = "", exit_code = 0 })
t.mock_command("ls-files", { stdout = "", stderr = "", exit_code = 0 })
t.mock_command("ls-tree", { stdout = "", stderr = "", exit_code = 0 })
t.mock_command("FKST_IMPLEMENTATION_WORKTREE_RESULT:v1:ENTERED", {
stdout = "",
stderr = "FKST_LOCAL_ITERATION_RESULT:v2:PASS:NONE\n",
exit_code = 0,
})
end

return {
test_cache_preparation_runs_after_substrate_refresh_and_before_codex = function()
local event = ready()
mock_issue_implement({ "fkst-dev:ready", "fkst-dev:thinking" })
local worktree = mock_fresh_implement_worktree(nil, current_base_pin, stale_branch_pin)
mock_cache_command()
mock_cache_command(nil, 2)
mock_successful_candidate(event)

local result = run_implement(event, opts("implement-cache-preparation-order"))

t.eq(result.exit_code, 0)
local pin_refresh = command_index("commit -m 'chore: refresh fkst-substrate pin'")
local preparation = command_index(cache_command)
local preparations = command_indices(cache_command)
local codex = command_index("codex exec")
local verification = command_index("scripts/run.sh test-affected")
t.is_true(pin_refresh ~= nil)
t.is_true(preparation ~= nil)
t.eq(#preparations, 2)
t.is_true(codex ~= nil)
t.is_true(pin_refresh < preparation)
t.is_true(preparation < codex)
t.eq(count_calls(cache_command), 1)
local preparation_call = command_call(cache_command)
t.eq(preparation_call.cwd, trusted_repository_root)
t.eq(command_env(preparation_call, "FKST_DEVLOOP_CACHE_PREPARATION_WORKTREE"), worktree)
t.is_true(verification ~= nil)
t.is_true(pin_refresh < preparations[1])
t.is_true(preparations[1] < codex)
t.is_true(codex < preparations[2])
t.is_true(preparations[2] < verification)
t.eq(count_calls(cache_command), 2)
local preparation_calls = {}
for _, call in ipairs(t.command_calls()) do
if tostring(call.rendered or ""):find(cache_command, 1, true) ~= nil then
table.insert(preparation_calls, call)
end
end
for _, preparation_call in ipairs(preparation_calls) do
t.eq(preparation_call.cwd, trusted_repository_root)
t.eq(command_env(preparation_call, "FKST_DEVLOOP_CACHE_PREPARATION_WORKTREE"), worktree)
end
end,

test_cache_preparation_unset_preserves_implementation_flow = function()
Expand Down Expand Up @@ -139,12 +193,35 @@ return {
t.is_true(tostring(result.error):find("cache-preparation-failed", 1, true) ~= nil)
end,

test_cache_preparation_failure_after_codex_stops_before_staging = function()
local event = ready()
mock_issue_implement({ "fkst-dev:ready", "fkst-dev:thinking" })
mock_fresh_implement_worktree()
mock_cache_command(nil, 1)
mock_cache_command({
stdout = "",
stderr = "target is not ignored",
exit_code = 1,
})
mock_implement_codex(0, "implementation changed cache tracking")
mock_git_status(" M .gitignore\n")

local result = run_implement(event, opts("implement-cache-preparation-before-staging"))

t.eq(result.exit_code, 1)
t.eq(count_calls(cache_command), 2)
t.eq(count_calls("add -A"), 1)
t.eq(count_calls("commit -m 'Implement github-devloop ready state'"), 0)
t.eq(count_calls("codex exec"), 1)
t.is_true(tostring(result.error):find("cache-preparation-failed", 1, true) ~= nil)
end,

test_cache_preparation_runs_for_reused_worktree_cache = function()
local event = ready()
local branch = deterministic_branch_for(event)
mock_issue_implement({ "fkst-dev:ready" })
local worktree = mock_existing_empty_implement_worktree_reuse(nil, branch, "1")
mock_cache_command()
mock_cache_command(nil, 2)
mock_implement_codex(0, "committed implementation from warm cache")
mock_git_status("")
mock_branch_diff_paths("packages/github-devloop/core.lua\n")
Expand All @@ -164,11 +241,56 @@ return {

t.eq(result.exit_code, 0)
t.eq(count_calls("git worktree add"), 0)
t.eq(count_calls(cache_command), 1)
t.eq(count_calls(cache_command), 2)
t.eq(count_calls("codex exec"), 1)
t.is_true(command_index(cache_command) < command_index("codex exec"))
local preparation_call = command_call(cache_command)
t.eq(preparation_call.cwd, trusted_repository_root)
t.eq(command_env(preparation_call, "FKST_DEVLOOP_CACHE_PREPARATION_WORKTREE"), worktree)
local preparations = command_indices(cache_command)
t.is_true(preparations[1] < command_index("codex exec"))
t.is_true(command_index("codex exec") < preparations[2])
t.is_true(preparations[2] < command_index("scripts/run.sh test-affected"))
for _, call in ipairs(t.command_calls()) do
if tostring(call.rendered or ""):find(cache_command, 1, true) ~= nil then
t.eq(call.cwd, trusted_repository_root)
t.eq(command_env(call, "FKST_DEVLOOP_CACHE_PREPARATION_WORKTREE"), worktree)
end
end
end,

test_cache_preparation_covers_candidate_and_detached_base_without_reusing_verdicts = function()
local event = ready()
local branch = deterministic_branch_for(event)
mock_issue_implement({ "fkst-dev:ready", "fkst-dev:thinking" })
local worktree = mock_fresh_implement_worktree()
mock_cache_command(nil, 4)
mock_codex_success_without_local_iteration("implemented with a semantic regression")
mock_git_status(" M packages/github-devloop/core.lua\n")
mock_git_commit("def456", branch)
mock_candidate_local_red()
mock_base_probe(worktree)

local result = run_implement(event, opts("implement-cache-preparation-base-probe"))

t.eq(result.exit_code, 0)
t.eq(count_calls(cache_command), 4)
t.eq(count_calls("scripts/run.sh test-affected"), 2)
local preparations = command_indices(cache_command)
local verifications = command_indices("scripts/run.sh test-affected")
t.is_true(preparations[1] < command_index("codex exec"))
t.is_true(command_index("codex exec") < preparations[2])
t.is_true(preparations[2] < preparations[3])
t.is_true(preparations[3] < verifications[1])
t.is_true(verifications[1] < preparations[4])
t.is_true(preparations[4] < verifications[2])

local preparation_worktrees = {}
for _, call in ipairs(t.command_calls()) do
if tostring(call.rendered or ""):find(cache_command, 1, true) ~= nil then
table.insert(preparation_worktrees,
command_env(call, "FKST_DEVLOOP_CACHE_PREPARATION_WORKTREE"))
end
end
t.eq(preparation_worktrees[1], worktree)
t.eq(preparation_worktrees[2], worktree)
t.eq(preparation_worktrees[3], worktree)
t.is_true(tostring(preparation_worktrees[4]):find(worktree .. "-base-probe-", 1, true) ~= nil)
end,
}
Loading
Loading