Skip to content

Commit d92968f

Browse files
authored
Fix agent-loop release-set authority in dogfood replays (#116)
* ci: make agent-loop the release-set root authority * ci: let agent-loop own sibling dependency versions * ci: bind replay evidence to resolved first-party versions * ci: verify resolved agent-loop release set * ci: reuse release-set authority check for issue 101 * docs: separate release constraints from replay evidence * ci: record resolved siblings instead of constraining them * ci: keep only direct replay toolchain authority * ci: keep only direct issue 101 toolchain authority * docs: record resolved release set as evidence
1 parent 28dd3f7 commit d92968f

7 files changed

Lines changed: 210 additions & 42 deletions

File tree

.github/workflows/agent-loop-issue-101-dogfood.yml

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,24 +50,23 @@ jobs:
5050
coverage: none
5151
tools: composer:v2
5252

53-
- name: Validate frozen input and released toolchain
53+
- name: Validate frozen input and release-set authority
5454
run: |
5555
set -euo pipefail
5656
issue='harness/tools/agent-loop/dogfood/issue-101.json'
5757
composer='harness/tools/agent-loop/composer.json'
5858
test "$(jq -r '.target_base_commit' "${issue}")" = "$(git -C target rev-parse HEAD)"
5959
test "$(jq -r '.toolchain.agent_skills_commit' "${issue}")" = "$(git -C skills rev-parse HEAD)"
60-
test "$(jq -r '.toolchain.agent_loop_release' "${issue}")" = "$(jq -r '.["require-dev"]["voku/agent-loop"]' "${composer}")"
61-
test "$(jq -r '.toolchain.agent_kanban_release' "${issue}")" = "$(jq -r '.["require-dev"]["voku/agent-kanban"]' "${composer}")"
62-
test "$(jq -r '.toolchain.agent_learning_release' "${issue}")" = "$(jq -r '.["require-dev"]["voku/agent-learning"]' "${composer}")"
63-
test "$(jq -r '.toolchain.agent_map_release' "${issue}")" = "$(jq -r '.["require-dev"]["voku/agent-map"]' "${composer}")"
64-
test "$(jq -r '.toolchain.agent_recall_compiler_release' "${issue}")" = "$(jq -r '.["require-dev"]["voku/agent-recall-compiler"]' "${composer}")"
65-
test "$(jq -r '.toolchain.agent_session_release' "${issue}")" = "$(jq -r '.["require-dev"]["voku/agent-session"]' "${composer}")"
66-
67-
- name: Install exact released agent toolchain
60+
php harness/tools/agent-loop/verify-release-set.php "${issue}" "${composer}"
61+
62+
- name: Install and verify released agent toolchain
6863
run: |
6964
set -euo pipefail
65+
issue='harness/tools/agent-loop/dogfood/issue-101.json'
66+
composer='harness/tools/agent-loop/composer.json'
67+
lock='harness/tools/agent-loop/composer.lock'
7068
composer update --working-dir=harness/tools/agent-loop --no-interaction --prefer-dist --no-progress
69+
php harness/tools/agent-loop/verify-release-set.php "${issue}" "${composer}" "${lock}"
7170
composer update --working-dir=target --no-interaction --prefer-dist --no-progress
7271
7372
- name: Freeze agent-map context from issue text before any fix

.github/workflows/agent-loop-real-issue-dogfood.yml

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,29 +49,23 @@ jobs:
4949
coverage: none
5050
tools: composer:v2
5151

52-
- name: Validate frozen input and released toolchain
52+
- name: Validate frozen input and release-set authority
5353
run: |
5454
set -euo pipefail
5555
issue='harness/tools/agent-loop/dogfood/issue-60.json'
5656
composer='harness/tools/agent-loop/composer.json'
5757
test "$(jq -r '.target_base_commit' "${issue}")" = "$(git -C target rev-parse HEAD)"
5858
test "$(jq -r '.toolchain.agent_skills_commit' "${issue}")" = "$(git -C skills rev-parse HEAD)"
59-
test "$(jq -r '.toolchain.agent_loop_release' "${issue}")" = "$(jq -r '.["require-dev"]["voku/agent-loop"]' "${composer}")"
60-
test "$(jq -r '.toolchain.agent_kanban_release' "${issue}")" = "$(jq -r '.["require-dev"]["voku/agent-kanban"]' "${composer}")"
61-
test "$(jq -r '.toolchain.agent_learning_release' "${issue}")" = "$(jq -r '.["require-dev"]["voku/agent-learning"]' "${composer}")"
62-
test "$(jq -r '.toolchain.agent_map_release' "${issue}")" = "$(jq -r '.["require-dev"]["voku/agent-map"]' "${composer}")"
63-
test "$(jq -r '.toolchain.agent_recall_compiler_release' "${issue}")" = "$(jq -r '.["require-dev"]["voku/agent-recall-compiler"]' "${composer}")"
64-
test "$(jq -r '.toolchain.agent_session_release' "${issue}")" = "$(jq -r '.["require-dev"]["voku/agent-session"]' "${composer}")"
65-
66-
- name: Resolve and verify exact released agent toolchain
59+
php harness/tools/agent-loop/verify-release-set.php "${issue}" "${composer}"
60+
61+
- name: Resolve and verify released agent toolchain
6762
run: |
6863
set -euo pipefail
6964
issue='harness/tools/agent-loop/dogfood/issue-60.json'
65+
composer='harness/tools/agent-loop/composer.json'
7066
lock='harness/tools/agent-loop/composer.lock'
7167
composer update --working-dir=harness/tools/agent-loop --no-interaction --prefer-dist --no-progress
72-
expected="$(jq -r '.toolchain.resolved_lock_sha256' "${issue}")"
73-
actual="sha256:$(sha256sum "${lock}" | cut -d ' ' -f 1)"
74-
test "${actual}" = "${expected}"
68+
php harness/tools/agent-loop/verify-release-set.php "${issue}" "${composer}" "${lock}"
7569
7670
- name: Install historical target dependencies
7771
run: composer install --working-dir=target --no-interaction --prefer-dist --no-progress

tools/agent-loop/README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,22 @@ This directory is a separate Composer project on purpose.
44

55
`voku/agent-loop` is development tooling for this repository, not part of the package contract of `voku/simple-php-code-parser`. Keeping it out of the root `composer.json` avoids raising consumer PHP requirements, leaking agent tooling into downstream installs, and creating first-party dependency cycles such as `agent-map -> agent-loop -> agent-recall-compiler -> agent-map`.
66

7+
`voku/agent-loop` is also the single root authority for its `agent-*` runtime dependency set. This tool project constrains `agent-loop` directly and keeps `voku/simple-php-code-parser` direct because that is the package under test. Resolved sibling `agent-*` versions are replay evidence, not duplicate root constraints.
8+
79
## Real-issue replay
810

911
The first replay uses historical issue #60, `Update for use with PHP 8.4`.
1012

1113
The workflow freezes three things before context discovery:
1214

1315
- issue title/body and the pre-fix base commit `5156d5d74ca1bce275219f4571efd54ec44be911`;
14-
- the released agent toolchain: `agent-loop 0.16.5`, `agent-kanban 0.3.1`, `agent-learning 0.13.0`, `agent-map 0.8.1`, `agent-recall-compiler 0.13.2`, and `agent-session 0.6.0`;
16+
- `agent-loop 0.16.5` as the direct first-party release-set authority;
1517
- agent-skills commit `c7e9d8bdda59d957600bca8dc9f787f03286b277` and the `reproduce-before-fix` L2 recipe.
1618

19+
Before resolution, `verify-release-set.php` fails if the tool project or replay input reintroduces sibling `agent-*` version authority. After resolution, the same verifier requires the complete first-party release set to be present in `composer.lock` and reports the versions Composer actually selected.
20+
21+
If a replay needs byte-for-byte dependency identity rather than compatibility through the frozen `agent-loop` release, commit and install an exact lock file. Do not approximate a lock by copying transitive package versions into another JSON authority.
22+
1723
The issue input contains no knowledge of the later fix files. `issue-60-oracle.json` is read only after map search and Recall compilation have finished.
1824

1925
The historical fix in PR #84 changed:
@@ -27,4 +33,4 @@ A map miss is recorded as a finding rather than converted into a fake correctnes
2733

2834
## Evidence
2935

30-
GitHub Actions archives the generated tool `composer.lock`, resolved package list, map search output, Recall bundle/facts/system prompt, and post-context evaluation. The lock proves the exact released package set Composer resolved for the run; no sibling checkout or candidate path repository participates in normal replay evidence.
36+
GitHub Actions archives the generated tool `composer.lock`, resolved package list, map search output, Recall bundle/facts/system prompt, and post-context evaluation. The generated lock and package list record the exact set Composer resolved for that run; `agent-loop` remains the only first-party release-set constraint that the replay owns.

tools/agent-loop/composer.json

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,8 @@
44
"type": "project",
55
"license": "Apache-2.0",
66
"require-dev": {
7-
"voku/agent-kanban": "0.3.1",
8-
"voku/agent-learning": "0.13.0",
97
"voku/agent-loop": "0.16.5",
10-
"voku/agent-map": "0.8.1",
11-
"voku/agent-recall-compiler": "0.13.2",
12-
"voku/agent-session": "0.6.0",
13-
"voku/simple-cache": "6.1.0",
14-
"voku/simple-php-code-parser": "0.22.2",
15-
"voku/stop-words": "2.0.1"
8+
"voku/simple-php-code-parser": "0.22.2"
169
},
1710
"minimum-stability": "dev",
1811
"prefer-stable": true,

tools/agent-loop/dogfood/issue-101.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,6 @@
1111
"target_base_commit": "53f1b5085ee883560afa9326ee914f6b23acd6ae",
1212
"toolchain": {
1313
"agent_loop_release": "0.16.5",
14-
"agent_kanban_release": "0.3.1",
15-
"agent_learning_release": "0.13.0",
16-
"agent_map_release": "0.8.1",
17-
"agent_recall_compiler_release": "0.13.2",
18-
"agent_session_release": "0.6.0",
1914
"agent_skills_commit": "c7e9d8bdda59d957600bca8dc9f787f03286b277",
2015
"operating_prompt": "reproduce-before-fix"
2116
}

tools/agent-loop/dogfood/issue-60.json

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,7 @@
1111
"target_base_commit": "5156d5d74ca1bce275219f4571efd54ec44be911",
1212
"toolchain": {
1313
"agent_loop_release": "0.16.5",
14-
"agent_kanban_release": "0.3.1",
15-
"agent_learning_release": "0.13.0",
16-
"agent_map_release": "0.8.1",
17-
"agent_recall_compiler_release": "0.13.2",
18-
"agent_session_release": "0.6.0",
1914
"agent_skills_commit": "c7e9d8bdda59d957600bca8dc9f787f03286b277",
20-
"resolved_lock_sha256": "sha256:6079c76aec73038e61875292c242477f74ed8913920c3ecfc496bd0ea3157b82",
2115
"operating_prompt": "reproduce-before-fix"
2216
}
2317
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
const AGENT_LOOP_PACKAGE = 'voku/agent-loop';
6+
const OWNED_AGENT_PACKAGES = [
7+
'voku/agent-kanban',
8+
'voku/agent-learning',
9+
'voku/agent-map',
10+
'voku/agent-recall-compiler',
11+
'voku/agent-session',
12+
];
13+
const STALE_SIBLING_RELEASE_FIELDS = [
14+
'agent_kanban_release',
15+
'agent_learning_release',
16+
'agent_map_release',
17+
'agent_recall_compiler_release',
18+
'agent_session_release',
19+
];
20+
21+
if ($argc < 3 || $argc > 4) {
22+
fwrite(STDERR, "Usage: php tools/agent-loop/verify-release-set.php <issue.json> <composer.json> [composer.lock]\n");
23+
exit(2);
24+
}
25+
26+
try {
27+
$issue = readJsonObject($argv[1]);
28+
$composer = readJsonObject($argv[2]);
29+
$toolchain = requireArray($issue, 'toolchain', $argv[1]);
30+
$require = stringRequirements($composer['require'] ?? [], 'require', $argv[2]);
31+
$requireDev = stringRequirements($composer['require-dev'] ?? [], 'require-dev', $argv[2]);
32+
$rootRequirements = $require + $requireDev;
33+
34+
$expectedAgentLoop = requireString($toolchain, 'agent_loop_release', $argv[1]);
35+
$actualAgentLoop = $rootRequirements[AGENT_LOOP_PACKAGE] ?? null;
36+
if ($actualAgentLoop !== $expectedAgentLoop) {
37+
throw new \RuntimeException(sprintf(
38+
'%s must require %s %s; got %s.',
39+
$argv[2],
40+
AGENT_LOOP_PACKAGE,
41+
$expectedAgentLoop,
42+
$actualAgentLoop ?? '<missing>',
43+
));
44+
}
45+
46+
foreach (OWNED_AGENT_PACKAGES as $package) {
47+
if (isset($rootRequirements[$package])) {
48+
throw new \RuntimeException(sprintf(
49+
'%s must not constrain %s directly; %s owns the first-party release set.',
50+
$argv[2],
51+
$package,
52+
AGENT_LOOP_PACKAGE,
53+
));
54+
}
55+
}
56+
57+
foreach (STALE_SIBLING_RELEASE_FIELDS as $field) {
58+
if (array_key_exists($field, $toolchain)) {
59+
throw new \RuntimeException(sprintf(
60+
'%s.toolchain.%s duplicates transitive release-set authority; record the resolved lock instead.',
61+
$argv[1],
62+
$field,
63+
));
64+
}
65+
}
66+
67+
if ($argc === 4) {
68+
$lock = readJsonObject($argv[3]);
69+
$resolved = resolvedVersions($lock, $argv[3]);
70+
$resolvedAgentLoop = $resolved[AGENT_LOOP_PACKAGE] ?? null;
71+
if ($resolvedAgentLoop !== $expectedAgentLoop) {
72+
throw new \RuntimeException(sprintf(
73+
'Resolved %s must be %s; got %s.',
74+
AGENT_LOOP_PACKAGE,
75+
$expectedAgentLoop,
76+
$resolvedAgentLoop ?? '<missing>',
77+
));
78+
}
79+
80+
$releaseSet = [AGENT_LOOP_PACKAGE => $resolvedAgentLoop];
81+
foreach (OWNED_AGENT_PACKAGES as $package) {
82+
$version = $resolved[$package] ?? null;
83+
if ($version === null) {
84+
throw new \RuntimeException('Resolved release set is missing ' . $package . '.');
85+
}
86+
$releaseSet[$package] = $version;
87+
}
88+
ksort($releaseSet, SORT_STRING);
89+
90+
fwrite(STDOUT, json_encode(
91+
['resolved_agent_release_set' => $releaseSet],
92+
JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR,
93+
) . PHP_EOL);
94+
}
95+
} catch (Throwable $exception) {
96+
fwrite(STDERR, $exception->getMessage() . "\n");
97+
exit(1);
98+
}
99+
100+
/** @return array<string, mixed> */
101+
function readJsonObject(string $path): array
102+
{
103+
$content = file_get_contents($path);
104+
if ($content === false) {
105+
throw new \RuntimeException('Cannot read ' . $path . '.');
106+
}
107+
108+
$decoded = json_decode($content, true, 512, JSON_THROW_ON_ERROR);
109+
if (!is_array($decoded)) {
110+
throw new \RuntimeException($path . ' must contain a JSON object.');
111+
}
112+
113+
return $decoded;
114+
}
115+
116+
/**
117+
* @param array<string, mixed> $data
118+
* @return array<string, mixed>
119+
*/
120+
function requireArray(array $data, string $key, string $path): array
121+
{
122+
$value = $data[$key] ?? null;
123+
if (!is_array($value)) {
124+
throw new \RuntimeException(sprintf('%s.%s must be an object.', $path, $key));
125+
}
126+
127+
return $value;
128+
}
129+
130+
/** @param array<string, mixed> $data */
131+
function requireString(array $data, string $key, string $path): string
132+
{
133+
$value = $data[$key] ?? null;
134+
if (!is_string($value) || $value === '') {
135+
throw new \RuntimeException(sprintf('%s.%s must be a non-empty string.', $path, $key));
136+
}
137+
138+
return $value;
139+
}
140+
141+
/**
142+
* @param mixed $requirements
143+
* @return array<string, string>
144+
*/
145+
function stringRequirements(mixed $requirements, string $section, string $path): array
146+
{
147+
if (!is_array($requirements)) {
148+
throw new \RuntimeException(sprintf('%s.%s must be an object.', $path, $section));
149+
}
150+
151+
$result = [];
152+
foreach ($requirements as $package => $constraint) {
153+
if (!is_string($package) || !is_string($constraint)) {
154+
throw new \RuntimeException(sprintf('%s.%s must contain string package constraints.', $path, $section));
155+
}
156+
$result[$package] = $constraint;
157+
}
158+
159+
return $result;
160+
}
161+
162+
/**
163+
* @param array<string, mixed> $lock
164+
* @return array<string, string>
165+
*/
166+
function resolvedVersions(array $lock, string $path): array
167+
{
168+
$resolved = [];
169+
foreach (['packages', 'packages-dev'] as $section) {
170+
$packages = $lock[$section] ?? [];
171+
if (!is_array($packages)) {
172+
throw new \RuntimeException(sprintf('%s.%s must be an array.', $path, $section));
173+
}
174+
foreach ($packages as $package) {
175+
if (!is_array($package)) {
176+
continue;
177+
}
178+
$name = $package['name'] ?? null;
179+
$version = $package['version'] ?? null;
180+
if (is_string($name) && is_string($version)) {
181+
$resolved[$name] = $version;
182+
}
183+
}
184+
}
185+
186+
return $resolved;
187+
}

0 commit comments

Comments
 (0)