agent-control-plane 0.1.12 → 0.1.13
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/hooks/heartbeat-hooks.sh +50 -0
- package/hooks/issue-reconcile-hooks.sh +46 -0
- package/npm/bin/agent-control-plane.js +89 -8
- package/package.json +1 -1
- package/tools/bin/agent-project-run-codex-session +58 -0
- package/tools/bin/branch-verification-guard.sh +15 -2
- package/tools/bin/ensure-runtime-sync.sh +5 -5
- package/tools/bin/start-issue-worker.sh +34 -24
- package/tools/bin/sync-shared-agent-home.sh +24 -10
- package/tools/templates/issue-prompt-template.md +4 -1
package/hooks/heartbeat-hooks.sh
CHANGED
|
@@ -15,6 +15,8 @@ FLOW_TOOLS_DIR="${FLOW_SKILL_DIR}/tools/bin"
|
|
|
15
15
|
DETACHED_LAUNCH_BIN="${FLOW_TOOLS_DIR}/agent-project-detached-launch"
|
|
16
16
|
RESIDENT_ISSUE_LOOP_BIN="${FLOW_TOOLS_DIR}/start-resident-issue-loop.sh"
|
|
17
17
|
REPO_SLUG="$(flow_resolve_repo_slug "${CONFIG_YAML}")"
|
|
18
|
+
AGENT_REPO_ROOT="$(flow_resolve_agent_repo_root "${CONFIG_YAML}")"
|
|
19
|
+
DEFAULT_BRANCH="$(flow_resolve_default_branch "${CONFIG_YAML}")"
|
|
18
20
|
STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
|
|
19
21
|
PENDING_LAUNCH_DIR="${ACP_PENDING_LAUNCH_DIR:-${F_LOSNING_PENDING_LAUNCH_DIR:-${STATE_ROOT}/pending-launches}}"
|
|
20
22
|
AGENT_PR_PREFIXES_JSON="$(flow_managed_pr_prefixes_json "${CONFIG_YAML}")"
|
|
@@ -24,6 +26,51 @@ AGENT_EXCLUSIVE_LABEL="${AGENT_EXCLUSIVE_LABEL:-agent-exclusive}"
|
|
|
24
26
|
CODING_WORKER="${ACP_CODING_WORKER:-${F_LOSNING_CODING_WORKER:-codex}}"
|
|
25
27
|
HEARTBEAT_ISSUE_JSON_CACHE_DIR="${TMPDIR:-/tmp}/heartbeat-issue-json.$$"
|
|
26
28
|
|
|
29
|
+
heartbeat_issue_retry_state_file() {
|
|
30
|
+
local issue_id="${1:?issue id required}"
|
|
31
|
+
printf '%s/retries/issues/%s.env\n' "${STATE_ROOT}" "${issue_id}"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
heartbeat_reason_requires_baseline_change() {
|
|
35
|
+
local reason="${1:-}"
|
|
36
|
+
case "${reason}" in
|
|
37
|
+
verification-guard-blocked|no-publishable-commits|no-publishable-delta)
|
|
38
|
+
return 0
|
|
39
|
+
;;
|
|
40
|
+
*)
|
|
41
|
+
return 1
|
|
42
|
+
;;
|
|
43
|
+
esac
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
heartbeat_current_baseline_head_sha() {
|
|
47
|
+
local head_sha=""
|
|
48
|
+
if [[ -d "${AGENT_REPO_ROOT}" ]]; then
|
|
49
|
+
head_sha="$(git -C "${AGENT_REPO_ROOT}" rev-parse --verify --quiet "origin/${DEFAULT_BRANCH}" 2>/dev/null || true)"
|
|
50
|
+
if [[ -z "${head_sha}" ]]; then
|
|
51
|
+
head_sha="$(git -C "${AGENT_REPO_ROOT}" rev-parse --verify --quiet "${DEFAULT_BRANCH}" 2>/dev/null || true)"
|
|
52
|
+
fi
|
|
53
|
+
fi
|
|
54
|
+
printf '%s\n' "${head_sha}"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
heartbeat_retry_reason_is_baseline_blocked() {
|
|
58
|
+
local issue_id="${1:?issue id required}"
|
|
59
|
+
local reason="${2:-}"
|
|
60
|
+
local state_file baseline_head current_head
|
|
61
|
+
|
|
62
|
+
heartbeat_reason_requires_baseline_change "${reason}" || return 1
|
|
63
|
+
state_file="$(heartbeat_issue_retry_state_file "${issue_id}")"
|
|
64
|
+
[[ -f "${state_file}" ]] || return 1
|
|
65
|
+
|
|
66
|
+
baseline_head="$(awk -F= '/^BASELINE_HEAD_SHA=/{print substr($0, index($0, "=") + 1); exit}' "${state_file}" 2>/dev/null | tr -d '\r' || true)"
|
|
67
|
+
[[ -n "${baseline_head}" ]] || return 1
|
|
68
|
+
current_head="$(heartbeat_current_baseline_head_sha)"
|
|
69
|
+
[[ -n "${current_head}" ]] || return 1
|
|
70
|
+
|
|
71
|
+
[[ "${baseline_head}" == "${current_head}" ]]
|
|
72
|
+
}
|
|
73
|
+
|
|
27
74
|
heartbeat_issue_json_cached() {
|
|
28
75
|
local issue_id="${1:?issue id required}"
|
|
29
76
|
local cache_file=""
|
|
@@ -152,6 +199,9 @@ heartbeat_issue_blocked_recovery_reason() {
|
|
|
152
199
|
retry_out="$("${FLOW_TOOLS_DIR}/retry-state.sh" issue "$issue_id" get 2>/dev/null || true)"
|
|
153
200
|
retry_reason="$(awk -F= '/^LAST_REASON=/{print $2}' <<<"${retry_out:-}")"
|
|
154
201
|
if [[ -n "${retry_reason:-}" && "${retry_reason}" != "issue-worker-blocked" ]]; then
|
|
202
|
+
if heartbeat_retry_reason_is_baseline_blocked "${issue_id}" "${retry_reason}"; then
|
|
203
|
+
return 0
|
|
204
|
+
fi
|
|
155
205
|
printf '%s\n' "$retry_reason"
|
|
156
206
|
return 0
|
|
157
207
|
fi
|
|
@@ -12,7 +12,9 @@ ADAPTER_BIN_DIR="${FLOW_SKILL_DIR}/bin"
|
|
|
12
12
|
FLOW_TOOLS_DIR="${FLOW_SKILL_DIR}/tools/bin"
|
|
13
13
|
REPO_SLUG="$(flow_resolve_repo_slug "${CONFIG_YAML}")"
|
|
14
14
|
AGENT_ROOT="$(flow_resolve_agent_root "${CONFIG_YAML}")"
|
|
15
|
+
AGENT_REPO_ROOT="$(flow_resolve_agent_repo_root "${CONFIG_YAML}")"
|
|
15
16
|
STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
|
|
17
|
+
DEFAULT_BRANCH="$(flow_resolve_default_branch "${CONFIG_YAML}")"
|
|
16
18
|
RUNS_ROOT="$(flow_resolve_runs_root "${CONFIG_YAML}")"
|
|
17
19
|
BLOCKED_RECOVERY_STATE_DIR="${STATE_ROOT}/blocked-recovery-issues"
|
|
18
20
|
|
|
@@ -26,6 +28,49 @@ issue_clear_blocked_recovery_state() {
|
|
|
26
28
|
rm -f "${BLOCKED_RECOVERY_STATE_DIR}/${ISSUE_ID}.env" 2>/dev/null || true
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
issue_retry_state_file() {
|
|
32
|
+
printf '%s/retries/issues/%s.env\n' "${STATE_ROOT}" "${ISSUE_ID}"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
issue_reason_requires_baseline_change() {
|
|
36
|
+
local reason="${1:-}"
|
|
37
|
+
case "${reason}" in
|
|
38
|
+
verification-guard-blocked|no-publishable-commits|no-publishable-delta)
|
|
39
|
+
return 0
|
|
40
|
+
;;
|
|
41
|
+
*)
|
|
42
|
+
return 1
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
issue_current_baseline_head_sha() {
|
|
48
|
+
local head_sha=""
|
|
49
|
+
if [[ -d "${AGENT_REPO_ROOT}" ]]; then
|
|
50
|
+
head_sha="$(git -C "${AGENT_REPO_ROOT}" rev-parse --verify --quiet "origin/${DEFAULT_BRANCH}" 2>/dev/null || true)"
|
|
51
|
+
if [[ -z "${head_sha}" ]]; then
|
|
52
|
+
head_sha="$(git -C "${AGENT_REPO_ROOT}" rev-parse --verify --quiet "${DEFAULT_BRANCH}" 2>/dev/null || true)"
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
printf '%s\n' "${head_sha}"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
issue_record_retry_baseline_gate() {
|
|
59
|
+
local reason="${1:-}"
|
|
60
|
+
local state_file head_sha tmp_file
|
|
61
|
+
|
|
62
|
+
issue_reason_requires_baseline_change "${reason}" || return 0
|
|
63
|
+
state_file="$(issue_retry_state_file)"
|
|
64
|
+
[[ -f "${state_file}" ]] || return 0
|
|
65
|
+
head_sha="$(issue_current_baseline_head_sha)"
|
|
66
|
+
[[ -n "${head_sha}" ]] || return 0
|
|
67
|
+
|
|
68
|
+
tmp_file="$(mktemp)"
|
|
69
|
+
grep -v '^BASELINE_HEAD_SHA=' "${state_file}" >"${tmp_file}" || true
|
|
70
|
+
printf 'BASELINE_HEAD_SHA=%s\n' "${head_sha}" >>"${tmp_file}"
|
|
71
|
+
mv "${tmp_file}" "${state_file}"
|
|
72
|
+
}
|
|
73
|
+
|
|
29
74
|
issue_has_schedule_cadence() {
|
|
30
75
|
local issue_json issue_body
|
|
31
76
|
issue_json="$(flow_github_issue_view_json "${REPO_SLUG}" "${ISSUE_ID}" 2>/dev/null || true)"
|
|
@@ -155,6 +200,7 @@ issue_schedule_retry() {
|
|
|
155
200
|
return 0
|
|
156
201
|
fi
|
|
157
202
|
"${FLOW_TOOLS_DIR}/retry-state.sh" issue "$ISSUE_ID" schedule "$reason" >/dev/null || true
|
|
203
|
+
issue_record_retry_baseline_gate "${reason}"
|
|
158
204
|
}
|
|
159
205
|
|
|
160
206
|
issue_mark_ready() {
|
|
@@ -156,7 +156,7 @@ function createExecutionContext(stage) {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
function runScriptWithContext(context, scriptRelativePath, forwardedArgs, options = {}) {
|
|
159
|
-
const scriptPath = path.join(packageRoot, scriptRelativePath);
|
|
159
|
+
const scriptPath = options.scriptPath || path.join(packageRoot, scriptRelativePath);
|
|
160
160
|
const stdio = options.stdio || "inherit";
|
|
161
161
|
const result = spawnSync("bash", [scriptPath, ...forwardedArgs], {
|
|
162
162
|
stdio,
|
|
@@ -175,11 +175,78 @@ function runScriptWithContext(context, scriptRelativePath, forwardedArgs, option
|
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
function resolvePersistentSourceHome(context) {
|
|
179
|
+
if (process.env.ACP_PROJECT_RUNTIME_SOURCE_HOME) {
|
|
180
|
+
return process.env.ACP_PROJECT_RUNTIME_SOURCE_HOME;
|
|
181
|
+
}
|
|
182
|
+
if (fs.existsSync(path.join(packageRoot, ".git"))) {
|
|
183
|
+
return packageRoot;
|
|
184
|
+
}
|
|
185
|
+
return context.runtimeHome;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function runtimeSkillRoot(context) {
|
|
189
|
+
return path.join(context.runtimeHome, "skills", "openclaw", skillName);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function createRuntimeExecutionContext(context) {
|
|
193
|
+
const stableSkillRoot = runtimeSkillRoot(context);
|
|
194
|
+
const persistentSourceHome = resolvePersistentSourceHome(context);
|
|
195
|
+
const runtimeScriptEnv = {
|
|
196
|
+
ACP_PROJECT_RUNTIME_SYNC_SCRIPT:
|
|
197
|
+
context.env.ACP_PROJECT_RUNTIME_SYNC_SCRIPT || path.join(stableSkillRoot, "tools", "bin", "sync-shared-agent-home.sh"),
|
|
198
|
+
ACP_PROJECT_RUNTIME_ENSURE_SYNC_SCRIPT:
|
|
199
|
+
context.env.ACP_PROJECT_RUNTIME_ENSURE_SYNC_SCRIPT || path.join(stableSkillRoot, "tools", "bin", "ensure-runtime-sync.sh"),
|
|
200
|
+
ACP_PROJECT_RUNTIME_BOOTSTRAP_SCRIPT:
|
|
201
|
+
context.env.ACP_PROJECT_RUNTIME_BOOTSTRAP_SCRIPT || path.join(stableSkillRoot, "tools", "bin", "project-launchd-bootstrap.sh"),
|
|
202
|
+
ACP_PROJECT_RUNTIME_SUPERVISOR_SCRIPT:
|
|
203
|
+
context.env.ACP_PROJECT_RUNTIME_SUPERVISOR_SCRIPT || path.join(stableSkillRoot, "tools", "bin", "project-runtime-supervisor.sh"),
|
|
204
|
+
ACP_PROJECT_RUNTIME_KICK_SCRIPT:
|
|
205
|
+
context.env.ACP_PROJECT_RUNTIME_KICK_SCRIPT || path.join(stableSkillRoot, "tools", "bin", "kick-scheduler.sh")
|
|
206
|
+
};
|
|
207
|
+
return {
|
|
208
|
+
...context,
|
|
209
|
+
stableSkillRoot,
|
|
210
|
+
persistentSourceHome,
|
|
211
|
+
env: {
|
|
212
|
+
...context.env,
|
|
213
|
+
SHARED_AGENT_HOME: context.runtimeHome,
|
|
214
|
+
AGENT_CONTROL_PLANE_ROOT: stableSkillRoot,
|
|
215
|
+
ACP_ROOT: stableSkillRoot,
|
|
216
|
+
AGENT_FLOW_SOURCE_ROOT: stableSkillRoot,
|
|
217
|
+
ACP_PROJECT_INIT_SOURCE_HOME: persistentSourceHome,
|
|
218
|
+
ACP_PROJECT_RUNTIME_SOURCE_HOME: persistentSourceHome,
|
|
219
|
+
ACP_DASHBOARD_SOURCE_HOME: persistentSourceHome,
|
|
220
|
+
...runtimeScriptEnv
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function syncRuntimeHome(context, options = {}) {
|
|
226
|
+
const result = runScriptWithContext(context, "tools/bin/sync-shared-agent-home.sh", [], {
|
|
227
|
+
stdio: options.stdio || "inherit"
|
|
228
|
+
});
|
|
229
|
+
if (result.status !== 0) {
|
|
230
|
+
throw new Error("failed to sync runtime home before command execution");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
178
234
|
function runCommand(scriptRelativePath, forwardedArgs) {
|
|
179
235
|
const stage = stageSharedHome();
|
|
180
236
|
const context = createExecutionContext(stage);
|
|
181
237
|
|
|
182
238
|
try {
|
|
239
|
+
if (scriptRelativePath !== "tools/bin/sync-shared-agent-home.sh") {
|
|
240
|
+
syncRuntimeHome(context, { stdio: "inherit" });
|
|
241
|
+
const runtimeContext = createRuntimeExecutionContext(context);
|
|
242
|
+
const runtimeScriptPath = path.join(runtimeContext.stableSkillRoot, scriptRelativePath);
|
|
243
|
+
const result = runScriptWithContext(runtimeContext, scriptRelativePath, forwardedArgs, {
|
|
244
|
+
stdio: "inherit",
|
|
245
|
+
scriptPath: runtimeScriptPath
|
|
246
|
+
});
|
|
247
|
+
return result.status;
|
|
248
|
+
}
|
|
249
|
+
|
|
183
250
|
const result = runScriptWithContext(context, scriptRelativePath, forwardedArgs, { stdio: "inherit" });
|
|
184
251
|
return result.status;
|
|
185
252
|
} finally {
|
|
@@ -1470,8 +1537,8 @@ async function maybeRunFinalSetupFixups(options, scopedContext, config, currentS
|
|
|
1470
1537
|
runtimeStartReason = `doctor-${doctorKv.DOCTOR_STATUS || "not-ok"}`;
|
|
1471
1538
|
} else {
|
|
1472
1539
|
actions.push("runtime-start");
|
|
1473
|
-
runSetupStep(scopedContext, "Start the runtime", "tools/bin/project-runtimectl.sh", ["start", "--profile-id", config.profileId]);
|
|
1474
|
-
const runtimeStatusOutput = runSetupStep(scopedContext, "Read back runtime status", "tools/bin/project-runtimectl.sh", ["status", "--profile-id", config.profileId]);
|
|
1540
|
+
runSetupStep(scopedContext, "Start the runtime", "tools/bin/project-runtimectl.sh", ["start", "--profile-id", config.profileId], { useRuntimeCopy: true });
|
|
1541
|
+
const runtimeStatusOutput = runSetupStep(scopedContext, "Read back runtime status", "tools/bin/project-runtimectl.sh", ["status", "--profile-id", config.profileId], { useRuntimeCopy: true });
|
|
1475
1542
|
runtimeStatusKv = parseKvOutput(runtimeStatusOutput);
|
|
1476
1543
|
runtimeStartStatus = "ok";
|
|
1477
1544
|
runtimeStartReason = "";
|
|
@@ -1480,7 +1547,7 @@ async function maybeRunFinalSetupFixups(options, scopedContext, config, currentS
|
|
|
1480
1547
|
|
|
1481
1548
|
if (config.installLaunchd && process.platform === "darwin" && launchdInstallStatus !== "ok" && runtimeStartStatus === "ok") {
|
|
1482
1549
|
actions.push("launchd-install");
|
|
1483
|
-
runSetupStep(scopedContext, "Install macOS autostart", "tools/bin/install-project-launchd.sh", ["--profile-id", config.profileId]);
|
|
1550
|
+
runSetupStep(scopedContext, "Install macOS autostart", "tools/bin/install-project-launchd.sh", ["--profile-id", config.profileId], { useRuntimeCopy: true });
|
|
1484
1551
|
launchdInstallStatus = "ok";
|
|
1485
1552
|
launchdInstallReason = "";
|
|
1486
1553
|
}
|
|
@@ -1603,7 +1670,21 @@ async function collectSetupConfig(options, context) {
|
|
|
1603
1670
|
|
|
1604
1671
|
function runSetupStep(context, title, scriptRelativePath, args, options = {}) {
|
|
1605
1672
|
console.log(`\n== ${title} ==`);
|
|
1606
|
-
|
|
1673
|
+
let executionContext = context;
|
|
1674
|
+
let scriptPath = undefined;
|
|
1675
|
+
|
|
1676
|
+
if (options.useRuntimeCopy) {
|
|
1677
|
+
syncRuntimeHome(context, { stdio: "pipe" });
|
|
1678
|
+
executionContext = createRuntimeExecutionContext(context);
|
|
1679
|
+
scriptPath = path.join(executionContext.stableSkillRoot, scriptRelativePath);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
const result = runScriptWithContext(executionContext, scriptRelativePath, args, {
|
|
1683
|
+
stdio: "pipe",
|
|
1684
|
+
env: options.env,
|
|
1685
|
+
cwd: options.cwd,
|
|
1686
|
+
scriptPath
|
|
1687
|
+
});
|
|
1607
1688
|
if (result.status !== 0) {
|
|
1608
1689
|
printFailureDetails(result);
|
|
1609
1690
|
throw new Error(`${title} failed`);
|
|
@@ -1868,8 +1949,8 @@ async function runSetupFlow(forwardedArgs) {
|
|
|
1868
1949
|
runtimeStartReason = "gh-auth-not-ready";
|
|
1869
1950
|
console.log("runtime start skipped: GitHub CLI is not authenticated yet. Run `gh auth login` and start the runtime afterwards.");
|
|
1870
1951
|
} else {
|
|
1871
|
-
runSetupStep(scopedContext, "Start the runtime", "tools/bin/project-runtimectl.sh", ["start", "--profile-id", config.profileId]);
|
|
1872
|
-
const runtimeStatusOutput = runSetupStep(scopedContext, "Read back runtime status", "tools/bin/project-runtimectl.sh", ["status", "--profile-id", config.profileId]);
|
|
1952
|
+
runSetupStep(scopedContext, "Start the runtime", "tools/bin/project-runtimectl.sh", ["start", "--profile-id", config.profileId], { useRuntimeCopy: true });
|
|
1953
|
+
const runtimeStatusOutput = runSetupStep(scopedContext, "Read back runtime status", "tools/bin/project-runtimectl.sh", ["status", "--profile-id", config.profileId], { useRuntimeCopy: true });
|
|
1873
1954
|
runtimeStatusKv = parseKvOutput(runtimeStatusOutput);
|
|
1874
1955
|
runtimeStartStatus = "ok";
|
|
1875
1956
|
runtimeStartReason = "";
|
|
@@ -1886,7 +1967,7 @@ async function runSetupFlow(forwardedArgs) {
|
|
|
1886
1967
|
launchdInstallReason = "runtime-not-started";
|
|
1887
1968
|
console.log("launchd install skipped: runtime was not started successfully in this setup run.");
|
|
1888
1969
|
} else {
|
|
1889
|
-
runSetupStep(scopedContext, "Install macOS autostart", "tools/bin/install-project-launchd.sh", ["--profile-id", config.profileId]);
|
|
1970
|
+
runSetupStep(scopedContext, "Install macOS autostart", "tools/bin/install-project-launchd.sh", ["--profile-id", config.profileId], { useRuntimeCopy: true });
|
|
1890
1971
|
launchdInstallStatus = "ok";
|
|
1891
1972
|
launchdInstallReason = "";
|
|
1892
1973
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-control-plane",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Help a repo keep GitHub-driven coding agents running reliably without constant human babysitting",
|
|
5
5
|
"homepage": "https://github.com/ducminhnguyen0319/agent-control-plane",
|
|
6
6
|
"bugs": {
|
|
@@ -167,6 +167,8 @@ printf -v codex_bin_q '%q' "$codex_bin"
|
|
|
167
167
|
printf -v runner_bin_q '%q' "$runner_bin"
|
|
168
168
|
printf -v safe_profile_q '%q' "$safe_profile"
|
|
169
169
|
printf -v bypass_profile_q '%q' "$bypass_profile"
|
|
170
|
+
helper_bin_dir="${artifact_dir}/worker-bin"
|
|
171
|
+
printf -v helper_bin_dir_q '%q' "$helper_bin_dir"
|
|
170
172
|
|
|
171
173
|
{
|
|
172
174
|
printf 'TASK_KIND=%s\n' "$task_kind_q"
|
|
@@ -274,6 +276,61 @@ cat >"$inner_script" <<EOF
|
|
|
274
276
|
set -euo pipefail
|
|
275
277
|
${runtime_exports}
|
|
276
278
|
${context_exports}cd ${worktree_realpath_q}
|
|
279
|
+
bootstrap_codex_helper_env() {
|
|
280
|
+
local helper_bin_dir=${helper_bin_dir_q}
|
|
281
|
+
local openspec_shim=""
|
|
282
|
+
|
|
283
|
+
mkdir -p "\${helper_bin_dir}"
|
|
284
|
+
|
|
285
|
+
if [[ -d ${worktree_realpath_q}/node_modules/.bin ]]; then
|
|
286
|
+
export PATH="${worktree_realpath_q}/node_modules/.bin:\${PATH}"
|
|
287
|
+
fi
|
|
288
|
+
export PATH="\${helper_bin_dir}:\${PATH}"
|
|
289
|
+
|
|
290
|
+
if command -v openspec >/dev/null 2>&1; then
|
|
291
|
+
return 0
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
if [[ ! -d ${worktree_realpath_q}/openspec ]]; then
|
|
295
|
+
return 0
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
openspec_shim="\${helper_bin_dir}/openspec"
|
|
299
|
+
cat >"\${openspec_shim}" <<'SHIM'
|
|
300
|
+
#!/usr/bin/env bash
|
|
301
|
+
set -euo pipefail
|
|
302
|
+
|
|
303
|
+
repo_root="\${ACP_REPO_ROOT:-\${F_LOSNING_REPO_ROOT:-\$(pwd)}}"
|
|
304
|
+
openspec_root="\${repo_root}/openspec"
|
|
305
|
+
|
|
306
|
+
if [[ ! -d "\${openspec_root}" ]]; then
|
|
307
|
+
echo "openspec directory not found: \${openspec_root}" >&2
|
|
308
|
+
exit 1
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
command_name="\${1:-}"
|
|
312
|
+
case "\${command_name}" in
|
|
313
|
+
list)
|
|
314
|
+
shift || true
|
|
315
|
+
if [[ "\${1:-}" == "--specs" ]]; then
|
|
316
|
+
find "\${openspec_root}/specs" -mindepth 1 -maxdepth 1 -type d 2>/dev/null \
|
|
317
|
+
| xargs -n1 basename 2>/dev/null \
|
|
318
|
+
| sort
|
|
319
|
+
exit 0
|
|
320
|
+
fi
|
|
321
|
+
find "\${openspec_root}/changes" -mindepth 1 -maxdepth 1 -type d ! -name archive 2>/dev/null \
|
|
322
|
+
| xargs -n1 basename 2>/dev/null \
|
|
323
|
+
| sort
|
|
324
|
+
exit 0
|
|
325
|
+
;;
|
|
326
|
+
*)
|
|
327
|
+
echo "openspec shim only supports 'list' and 'list --specs'; use direct file reads for other operations" >&2
|
|
328
|
+
exit 64
|
|
329
|
+
;;
|
|
330
|
+
esac
|
|
331
|
+
SHIM
|
|
332
|
+
chmod +x "\${openspec_shim}"
|
|
333
|
+
}
|
|
277
334
|
reset_sandbox_run_dir() {
|
|
278
335
|
mkdir -p ${sandbox_run_dir_q}
|
|
279
336
|
find ${sandbox_run_dir_q} -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
|
|
@@ -331,6 +388,7 @@ record_final_git_state() {
|
|
|
331
388
|
} >>"\${tmp_file}"
|
|
332
389
|
mv "\${tmp_file}" ${meta_file_q}
|
|
333
390
|
}
|
|
391
|
+
bootstrap_codex_helper_env
|
|
334
392
|
reset_sandbox_run_dir
|
|
335
393
|
set +e
|
|
336
394
|
bash ${runner_bin_q} \\
|
|
@@ -108,6 +108,7 @@ const dependencyInputsChanged = files.some(isDependencyManifest);
|
|
|
108
108
|
const apiTouched = productNonTestFiles.some((file) => /^apps\/api\//.test(file));
|
|
109
109
|
const webTouched = productNonTestFiles.some((file) => /^apps\/web\//.test(file));
|
|
110
110
|
const mobileTouched = productNonTestFiles.some((file) => /^apps\/mobile\//.test(file));
|
|
111
|
+
const apiProductFiles = productNonTestFiles.filter((file) => /^apps\/api\//.test(file));
|
|
111
112
|
const packageNames = [
|
|
112
113
|
...new Set(
|
|
113
114
|
productNonTestFiles
|
|
@@ -199,11 +200,23 @@ const changedTestCoverage = changedTestFiles.map((file) => {
|
|
|
199
200
|
return { file, anchors, covered };
|
|
200
201
|
});
|
|
201
202
|
const missingChangedTestFiles = changedTestCoverage.filter(({ covered }) => !covered);
|
|
203
|
+
const apiChangedTests = changedTestCoverage.filter(({ file }) => /^apps\/api\//.test(file));
|
|
202
204
|
|
|
203
205
|
const rootTypecheck = hasCommand(/\bpnpm (?:run )?typecheck\b/, /\bturbo\b.*\btypecheck\b/);
|
|
204
206
|
const rootBuild = hasCommand(/\bpnpm (?:run )?build\b/, /\bturbo\b.*\bbuild\b/);
|
|
205
207
|
const rootLint = hasCommand(/\bpnpm (?:run )?lint\b/, /\bturbo\b.*\blint\b/);
|
|
206
208
|
const rootTest = hasCommand(/\bpnpm (?:run )?test\b/, /\bturbo\b.*\btest\b/);
|
|
209
|
+
const scopedApiTypecheck =
|
|
210
|
+
hasScopedCommand(apiScopePattern, /\btypecheck\b/, /\btsc --noemit\b/, /\btsc --noemit\b/);
|
|
211
|
+
const scopedApiConfidence =
|
|
212
|
+
hasScopedCommand(apiScopePattern, /\blint\b/, /\bbuild\b/, /\btest\b/, /\bjest\b/, /\bvitest\b/);
|
|
213
|
+
const apiNarrowSliceTargetedCoverage =
|
|
214
|
+
apiProductFiles.length > 0 &&
|
|
215
|
+
apiProductFiles.length <= 2 &&
|
|
216
|
+
apiProductFiles.every((file) => /(?:^|\/)(?:services?|utils?|helpers?|policies?)\/|(?:\.service|\.util|\.helper|\.policy)\.[cm]?[jt]s$/.test(file)) &&
|
|
217
|
+
apiChangedTests.length > 0 &&
|
|
218
|
+
apiChangedTests.every(({ covered }) => covered) &&
|
|
219
|
+
scopedApiConfidence;
|
|
207
220
|
|
|
208
221
|
const reasons = [];
|
|
209
222
|
if (passedCommands.length === 0) {
|
|
@@ -242,10 +255,10 @@ if (localeTouched) {
|
|
|
242
255
|
}
|
|
243
256
|
|
|
244
257
|
if (apiTouched) {
|
|
245
|
-
if (!(
|
|
258
|
+
if (!(scopedApiTypecheck || rootTypecheck || apiNarrowSliceTargetedCoverage)) {
|
|
246
259
|
reasons.push('missing API typecheck or repo typecheck for API changes');
|
|
247
260
|
}
|
|
248
|
-
if (!(
|
|
261
|
+
if (!(scopedApiConfidence || rootBuild || rootLint || rootTest)) {
|
|
249
262
|
reasons.push('missing API confidence verification (lint, build, or test) for API changes');
|
|
250
263
|
}
|
|
251
264
|
}
|
|
@@ -84,6 +84,11 @@ resolve_source_skill_dir() {
|
|
|
84
84
|
local skill_name=""
|
|
85
85
|
local root="${1:?source home required}"
|
|
86
86
|
|
|
87
|
+
if flow_is_skill_root "${root}"; then
|
|
88
|
+
printf '%s\n' "${root}"
|
|
89
|
+
return 0
|
|
90
|
+
fi
|
|
91
|
+
|
|
87
92
|
for skill_name in "$(flow_canonical_skill_name)" "$(flow_compat_skill_alias)"; do
|
|
88
93
|
[[ -n "${skill_name}" ]] || continue
|
|
89
94
|
candidate="${root}/skills/openclaw/${skill_name}"
|
|
@@ -93,11 +98,6 @@ resolve_source_skill_dir() {
|
|
|
93
98
|
fi
|
|
94
99
|
done
|
|
95
100
|
|
|
96
|
-
if flow_is_skill_root "${root}"; then
|
|
97
|
-
printf '%s\n' "${root}"
|
|
98
|
-
return 0
|
|
99
|
-
fi
|
|
100
|
-
|
|
101
101
|
return 1
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -727,38 +727,48 @@ for (const line of body.split(/\r?\n/).slice(0, 40)) {
|
|
|
727
727
|
}
|
|
728
728
|
}
|
|
729
729
|
|
|
730
|
-
if (commands.length === 0 && repoRoot) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
addCommand('pnpm test');
|
|
738
|
-
} else if (fs.existsSync(path.join(repoRoot, 'yarn.lock'))) {
|
|
739
|
-
addCommand('yarn test');
|
|
740
|
-
} else {
|
|
741
|
-
addCommand('npm test');
|
|
730
|
+
if (commands.length === 0 && repoRoot) {
|
|
731
|
+
const packageJsonPath = path.join(repoRoot, 'package.json');
|
|
732
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
733
|
+
try {
|
|
734
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
735
|
+
if (packageJson?.scripts?.smoke) {
|
|
736
|
+
addCommand('# If this cycle changes smoke runners, CLI entrypoints, or operator commands, also run the matching smoke command from the repo instructions.');
|
|
742
737
|
}
|
|
738
|
+
} catch (_error) {
|
|
739
|
+
// Ignore parse errors and fall through to generic guidance.
|
|
743
740
|
}
|
|
744
|
-
} catch (_error) {
|
|
745
|
-
// Ignore parse errors and fall through to generic guidance.
|
|
746
741
|
}
|
|
747
742
|
}
|
|
748
|
-
}
|
|
749
743
|
|
|
750
|
-
if (commands.length === 0) {
|
|
751
|
-
|
|
752
|
-
|
|
744
|
+
if (commands.length === 0) {
|
|
745
|
+
addCommand('# Pick the narrowest relevant local verification for the files you touch.');
|
|
746
|
+
addCommand('# Do not default to repo-wide pnpm test unless the issue body explicitly requires it.');
|
|
747
|
+
addCommand('# Examples:');
|
|
748
|
+
addCommand('# pnpm --filter @<repo>/api test -- --runInBand <target-spec>');
|
|
749
|
+
addCommand('# pnpm --filter @<repo>/api typecheck');
|
|
750
|
+
addCommand('# pnpm --filter @<repo>/web test -- --run <target-spec>');
|
|
751
|
+
addCommand('# pnpm --filter @<repo>/web typecheck');
|
|
752
|
+
addCommand('# pnpm --filter @<repo>/mobile test -- --runInBand <target-spec>');
|
|
753
|
+
addCommand('# pnpm --filter @<repo>/mobile typecheck');
|
|
754
|
+
addCommand('# pnpm --filter @<repo>/<package> test -- --run <target-spec>');
|
|
755
|
+
addCommand('# pnpm --filter @<repo>/<package> typecheck');
|
|
756
|
+
addCommand('# After each successful command, record it with record-verification.sh exactly as shown below.');
|
|
757
|
+
}
|
|
753
758
|
|
|
754
759
|
const escapeDoubleQuotes = (value) => value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
755
760
|
const snippet = commands
|
|
756
|
-
.map((command) =>
|
|
757
|
-
command
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
761
|
+
.map((command) => {
|
|
762
|
+
if (command.startsWith('#')) {
|
|
763
|
+
return command;
|
|
764
|
+
}
|
|
765
|
+
return (
|
|
766
|
+
command + '\n' +
|
|
767
|
+
'bash "$ACP_FLOW_TOOLS_DIR/record-verification.sh" --run-dir "$ACP_RUN_DIR" --status pass --command "' +
|
|
768
|
+
escapeDoubleQuotes(command) +
|
|
769
|
+
'"'
|
|
770
|
+
);
|
|
771
|
+
})
|
|
762
772
|
.join('\n\n');
|
|
763
773
|
|
|
764
774
|
process.stdout.write(snippet);
|
|
@@ -32,12 +32,21 @@ if [[ ! -d "${FLOW_SKILL_SOURCE}" && -n "${COMPAT_FLOW_SKILL_ALIAS}" ]]; then
|
|
|
32
32
|
fi
|
|
33
33
|
|
|
34
34
|
FLOW_SKILL_SOURCE="$(cd "${FLOW_SKILL_SOURCE}" && pwd -P)"
|
|
35
|
+
SOURCE_HOME="$(cd "${SOURCE_HOME}" && pwd -P)"
|
|
36
|
+
|
|
37
|
+
remove_tree_force() {
|
|
38
|
+
local target="${1:-}"
|
|
39
|
+
[[ -n "${target}" ]] || return 0
|
|
40
|
+
[[ -e "${target}" || -L "${target}" ]] || return 0
|
|
41
|
+
chmod -R u+w "${target}" 2>/dev/null || true
|
|
42
|
+
rm -rf "${target}" 2>/dev/null || true
|
|
43
|
+
}
|
|
35
44
|
|
|
36
45
|
sync_tree_copy_mode() {
|
|
37
46
|
local source_dir="${1:?source dir required}"
|
|
38
47
|
local target_dir="${2:?target dir required}"
|
|
39
48
|
[[ -d "${source_dir}" ]] || return 0
|
|
40
|
-
|
|
49
|
+
remove_tree_force "${target_dir}"
|
|
41
50
|
mkdir -p "${target_dir}"
|
|
42
51
|
(
|
|
43
52
|
cd "${source_dir}"
|
|
@@ -57,13 +66,15 @@ sync_tree_into_target() {
|
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
sync_skill_copies() {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
if ! flow_is_skill_root "${SOURCE_HOME}"; then
|
|
70
|
+
sync_tree_into_target "${FLOW_SKILL_SOURCE}" "${SOURCE_FLOW_CANONICAL_ALIAS}"
|
|
71
|
+
if [[ -n "${SOURCE_FLOW_COMPAT_ALIAS}" ]]; then
|
|
72
|
+
sync_tree_into_target "${FLOW_SKILL_SOURCE}" "${SOURCE_FLOW_COMPAT_ALIAS}"
|
|
73
|
+
fi
|
|
65
74
|
fi
|
|
66
75
|
|
|
76
|
+
sync_tree_into_target "${FLOW_SKILL_SOURCE}" "${FLOW_SKILL_TARGET}"
|
|
77
|
+
|
|
67
78
|
if [[ -n "${TARGET_FLOW_COMPAT_ALIAS}" ]]; then
|
|
68
79
|
sync_tree_into_target "${FLOW_SKILL_SOURCE}" "${TARGET_FLOW_COMPAT_ALIAS}"
|
|
69
80
|
fi
|
|
@@ -200,18 +211,21 @@ sync_tree_rsync() {
|
|
|
200
211
|
}
|
|
201
212
|
|
|
202
213
|
reset_runtime_skill_targets() {
|
|
203
|
-
|
|
214
|
+
remove_tree_force "${FLOW_SKILL_TARGET}"
|
|
204
215
|
if [[ -n "${TARGET_FLOW_COMPAT_ALIAS}" ]]; then
|
|
205
|
-
|
|
216
|
+
remove_tree_force "${TARGET_FLOW_COMPAT_ALIAS}"
|
|
206
217
|
fi
|
|
207
218
|
}
|
|
208
219
|
|
|
209
220
|
reset_source_skill_targets() {
|
|
221
|
+
if flow_is_skill_root "${SOURCE_HOME}"; then
|
|
222
|
+
return 0
|
|
223
|
+
fi
|
|
210
224
|
if [[ "${FLOW_SKILL_SOURCE}" != "${SOURCE_FLOW_CANONICAL_ALIAS}" ]]; then
|
|
211
|
-
|
|
225
|
+
remove_tree_force "${SOURCE_FLOW_CANONICAL_ALIAS}"
|
|
212
226
|
fi
|
|
213
227
|
if [[ -n "${SOURCE_FLOW_COMPAT_ALIAS}" && "${FLOW_SKILL_SOURCE}" != "${SOURCE_FLOW_COMPAT_ALIAS}" ]]; then
|
|
214
|
-
|
|
228
|
+
remove_tree_force "${SOURCE_FLOW_COMPAT_ALIAS}"
|
|
215
229
|
fi
|
|
216
230
|
}
|
|
217
231
|
|
|
@@ -22,7 +22,10 @@ Follow this order:
|
|
|
22
22
|
bash "$ACP_FLOW_TOOLS_DIR/create-follow-up-issue.sh" --parent {ISSUE_ID} --title "..." --body-file /tmp/follow-up.md
|
|
23
23
|
```
|
|
24
24
|
3. Implement the smallest root-cause fix in this worktree only.
|
|
25
|
-
4. Run verification and record every successful command with `record-verification.sh`.
|
|
25
|
+
4. Run the narrowest relevant local verification for the files you changed, and record every successful command with `record-verification.sh`.
|
|
26
|
+
|
|
27
|
+
- Do not default to repo-wide verification such as `pnpm test` unless the issue body explicitly requires it.
|
|
28
|
+
- If unrelated repo-wide suites are already red, keep the cycle focused on targeted verification for your slice and let the host verification guard decide whether publication is safe.
|
|
26
29
|
|
|
27
30
|
```bash
|
|
28
31
|
{ISSUE_VERIFICATION_COMMAND_SNIPPET}
|