agent-control-plane 0.2.0 → 0.4.9

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.
Files changed (59) hide show
  1. package/README.md +69 -19
  2. package/assets/workflow-catalog.json +1 -1
  3. package/bin/pr-risk.sh +22 -7
  4. package/bin/sync-pr-labels.sh +1 -1
  5. package/hooks/heartbeat-hooks.sh +125 -12
  6. package/hooks/issue-reconcile-hooks.sh +1 -1
  7. package/hooks/pr-reconcile-hooks.sh +1 -1
  8. package/npm/bin/agent-control-plane.js +296 -61
  9. package/package.json +11 -7
  10. package/tools/bin/agent-github-update-labels +36 -2
  11. package/tools/bin/agent-project-catch-up-merged-prs +4 -2
  12. package/tools/bin/agent-project-cleanup-session +49 -5
  13. package/tools/bin/agent-project-heartbeat-loop +119 -1471
  14. package/tools/bin/agent-project-publish-issue-pr +6 -3
  15. package/tools/bin/agent-project-reconcile-issue-session +78 -106
  16. package/tools/bin/agent-project-reconcile-pr-session +166 -143
  17. package/tools/bin/agent-project-retry-state +18 -7
  18. package/tools/bin/agent-project-run-claude-session +10 -0
  19. package/tools/bin/agent-project-run-codex-resilient +99 -14
  20. package/tools/bin/agent-project-run-codex-session +16 -5
  21. package/tools/bin/agent-project-run-kilo-session +10 -0
  22. package/tools/bin/agent-project-run-openclaw-session +10 -0
  23. package/tools/bin/agent-project-run-opencode-session +10 -0
  24. package/tools/bin/agent-project-sync-source-repo-main +163 -0
  25. package/tools/bin/agent-project-worker-status +10 -7
  26. package/tools/bin/cleanup-worktree.sh +6 -1
  27. package/tools/bin/flow-config-lib.sh +1257 -34
  28. package/tools/bin/flow-resident-worker-lib.sh +119 -1
  29. package/tools/bin/flow-shell-lib.sh +56 -0
  30. package/tools/bin/github-core-rate-limit-state.sh +77 -0
  31. package/tools/bin/github-write-outbox.sh +470 -0
  32. package/tools/bin/heartbeat-loop-cache-lib.sh +164 -0
  33. package/tools/bin/heartbeat-loop-counting-lib.sh +306 -0
  34. package/tools/bin/heartbeat-loop-pr-strategy-lib.sh +199 -0
  35. package/tools/bin/heartbeat-loop-scheduling-lib.sh +506 -0
  36. package/tools/bin/heartbeat-loop-worker-lib.sh +319 -0
  37. package/tools/bin/heartbeat-recovery-preflight.sh +12 -1
  38. package/tools/bin/heartbeat-safe-auto.sh +56 -3
  39. package/tools/bin/install-project-launchd.sh +17 -2
  40. package/tools/bin/project-init.sh +21 -1
  41. package/tools/bin/project-launchd-bootstrap.sh +16 -9
  42. package/tools/bin/project-runtimectl.sh +46 -2
  43. package/tools/bin/reconcile-bootstrap-lib.sh +113 -0
  44. package/tools/bin/resident-issue-controller-lib.sh +448 -0
  45. package/tools/bin/scaffold-profile.sh +61 -3
  46. package/tools/bin/start-pr-fix-worker.sh +47 -10
  47. package/tools/bin/start-resident-issue-loop.sh +28 -439
  48. package/tools/dashboard/app.js +37 -1
  49. package/tools/dashboard/dashboard_snapshot.py +65 -26
  50. package/tools/templates/pr-fix-template.md +3 -1
  51. package/tools/templates/pr-merge-repair-template.md +2 -1
  52. package/SKILL.md +0 -149
  53. package/references/architecture.md +0 -217
  54. package/references/commands.md +0 -128
  55. package/references/control-plane-map.md +0 -124
  56. package/references/docs-map.md +0 -73
  57. package/references/release-checklist.md +0 -65
  58. package/references/repo-map.md +0 -36
  59. package/tools/bin/split-retained-slice.sh +0 -124
@@ -14,7 +14,12 @@ Create a new installed project profile, profile templates, and profile notes.
14
14
 
15
15
  Options:
16
16
  --profile-id <id> Profile id, e.g. billing-api
17
- --repo-slug <owner/repo> GitHub repo slug
17
+ --repo-slug <owner/repo> Forge repo slug
18
+ --forge-provider <github|gitea> Forge provider (default: github)
19
+ --gitea-base-url <url> Base URL for a local/self-hosted Gitea instance
20
+ --gitea-token <token> Gitea API token written to profile runtime.env
21
+ --gitea-username <user> Gitea username written to profile runtime.env
22
+ --gitea-password <pass> Gitea password written to profile runtime.env
18
23
  --profile-home <path> Profile registry root (default: ~/.agent-runtime/control-plane/profiles)
19
24
  --repo-root <path> Canonical repo root
20
25
  --agent-repo-root <path> Agent-owned anchor repo root (defaults to repo root)
@@ -40,6 +45,11 @@ EOF
40
45
 
41
46
  profile_id=""
42
47
  repo_slug=""
48
+ forge_provider="github"
49
+ gitea_base_url=""
50
+ gitea_token=""
51
+ gitea_username=""
52
+ gitea_password=""
43
53
  profile_home=""
44
54
  repo_root=""
45
55
  agent_repo_root=""
@@ -63,6 +73,11 @@ while [[ $# -gt 0 ]]; do
63
73
  case "$1" in
64
74
  --profile-id) profile_id="${2:-}"; shift 2 ;;
65
75
  --repo-slug) repo_slug="${2:-}"; shift 2 ;;
76
+ --forge-provider) forge_provider="${2:-}"; shift 2 ;;
77
+ --gitea-base-url) gitea_base_url="${2:-}"; shift 2 ;;
78
+ --gitea-token) gitea_token="${2:-}"; shift 2 ;;
79
+ --gitea-username) gitea_username="${2:-}"; shift 2 ;;
80
+ --gitea-password) gitea_password="${2:-}"; shift 2 ;;
66
81
  --profile-home) profile_home="${2:-}"; shift 2 ;;
67
82
  --repo-root) repo_root="${2:-}"; shift 2 ;;
68
83
  --agent-repo-root) agent_repo_root="${2:-}"; shift 2 ;;
@@ -104,6 +119,14 @@ case "$coding_worker" in
104
119
  ;;
105
120
  esac
106
121
 
122
+ case "$forge_provider" in
123
+ github|gitea) ;;
124
+ *)
125
+ echo "--forge-provider must be github or gitea" >&2
126
+ exit 1
127
+ ;;
128
+ esac
129
+
107
130
  case "$claude_effort" in
108
131
  low|medium|high|max) ;;
109
132
  *)
@@ -133,6 +156,7 @@ profile_home="${profile_home:-$(resolve_flow_profile_registry_root)}"
133
156
  profiles_dir="${profile_home}"
134
157
  profile_dir="${profiles_dir}/${profile_id}"
135
158
  profile_yaml="${profile_dir}/control-plane.yaml"
159
+ profile_runtime_env="${profile_dir}/runtime.env"
136
160
  profile_templates_dir="${profile_dir}/templates"
137
161
  profile_readme="${profile_dir}/README.md"
138
162
 
@@ -228,9 +252,9 @@ session_naming:
228
252
  pr_worktree_branch_prefix: "${pr_worktree_branch_prefix}"
229
253
  managed_pr_branch_globs: "${managed_pr_branch_globs}"
230
254
  queue:
231
- source: "github"
255
+ source: "${forge_provider}"
232
256
  issue_labels:
233
- ready: "agent-ready"
257
+ ready: ""
234
258
  running: "agent-running"
235
259
  blocked: "agent-blocked"
236
260
  heavy: "agent-e2e-heavy"
@@ -366,7 +390,38 @@ policies:
366
390
  EOF
367
391
  }
368
392
 
393
+ write_profile_runtime_env() {
394
+ local target_file="${1:?target file required}"
395
+
396
+ : >"$target_file"
397
+ {
398
+ printf 'ACP_FORGE_PROVIDER=%s\n' "${forge_provider}"
399
+ printf 'F_LOSNING_FORGE_PROVIDER=%s\n' "${forge_provider}"
400
+ if [[ "${forge_provider}" == "gitea" ]]; then
401
+ if [[ -n "${gitea_base_url}" ]]; then
402
+ printf 'ACP_GITEA_BASE_URL=%s\n' "${gitea_base_url}"
403
+ printf 'GITEA_BASE_URL=%s\n' "${gitea_base_url}"
404
+ fi
405
+ if [[ -n "${gitea_token}" ]]; then
406
+ printf 'ACP_GITEA_TOKEN=%s\n' "${gitea_token}"
407
+ printf 'GITEA_TOKEN=%s\n' "${gitea_token}"
408
+ fi
409
+ if [[ -n "${gitea_username}" ]]; then
410
+ printf 'ACP_GITEA_USERNAME=%s\n' "${gitea_username}"
411
+ printf 'GITEA_USERNAME=%s\n' "${gitea_username}"
412
+ fi
413
+ if [[ -n "${gitea_password}" ]]; then
414
+ printf 'ACP_GITEA_PASSWORD=%s\n' "${gitea_password}"
415
+ printf 'GITEA_PASSWORD=%s\n' "${gitea_password}"
416
+ fi
417
+ printf 'ACP_SOURCE_SYNC_REMOTE=gitea\n'
418
+ printf 'F_LOSNING_SOURCE_SYNC_REMOTE=gitea\n'
419
+ fi
420
+ } >"$target_file"
421
+ }
422
+
369
423
  write_profile_yaml "$profile_yaml"
424
+ write_profile_runtime_env "$profile_runtime_env"
370
425
  write_profile_readme "$profile_readme"
371
426
 
372
427
  if compgen -G "${flow_skill_dir}/tools/templates/*.md" >/dev/null; then
@@ -375,15 +430,18 @@ fi
375
430
 
376
431
  profile_home_real="$(mkdir -p "$profile_home" && cd "$profile_home" && pwd -P)"
377
432
  profile_yaml_real="$(cd "$(dirname "$profile_yaml")" && pwd -P)/$(basename "$profile_yaml")"
433
+ profile_runtime_env_real="$(cd "$(dirname "$profile_runtime_env")" && pwd -P)/$(basename "$profile_runtime_env")"
378
434
  profile_templates_dir_real="$(cd "$profile_templates_dir" && pwd -P)"
379
435
  profile_readme_real="$(cd "$(dirname "$profile_readme")" && pwd -P)/$(basename "$profile_readme")"
380
436
 
381
437
  printf 'PROFILE_ID=%s\n' "$profile_id"
382
438
  printf 'PROFILE_HOME=%s\n' "$profile_home_real"
383
439
  printf 'PROFILE_YAML=%s\n' "$profile_yaml_real"
440
+ printf 'PROFILE_RUNTIME_ENV=%s\n' "$profile_runtime_env_real"
384
441
  printf 'PROFILE_TEMPLATE_DIR=%s\n' "$profile_templates_dir_real"
385
442
  printf 'PROFILE_README=%s\n' "$profile_readme_real"
386
443
  printf 'REPO_SLUG=%s\n' "$repo_slug"
444
+ printf 'FORGE_PROVIDER=%s\n' "$forge_provider"
387
445
  printf 'CODING_WORKER=%s\n' "$coding_worker"
388
446
  printf 'NEXT_STEP=ACP_PROJECT_ID=%s bash %s/tools/bin/render-flow-config.sh\n' "$profile_id" "$flow_skill_dir"
389
447
  printf 'NEXT_STEP=bash %s/tools/bin/sync-shared-agent-home.sh\n' "$flow_skill_dir"
@@ -169,11 +169,41 @@ PR_MISSING_REASONS_TEXT="$(jq -r '.missingReasons[]? | "- " + .' <<<"$RISK_JSON"
169
169
  if [[ -z "$PR_MISSING_REASONS_TEXT" ]]; then
170
170
  PR_MISSING_REASONS_TEXT="- none"
171
171
  fi
172
- PR_PULL_JSON="$(flow_github_api_repo "${REPO_SLUG}" "pulls/${PR_NUMBER}")"
173
- PR_HEAD_SHA="$(jq -r '.head.sha' <<<"$PR_PULL_JSON")"
174
- PR_MERGEABLE_STATUS="$(jq -r 'if .mergeable == null then "UNKNOWN" else (.mergeable | tostring | ascii_upcase) end' <<<"$PR_PULL_JSON")"
172
+ PR_PULL_JSON="$(flow_github_api_repo "${REPO_SLUG}" "pulls/${PR_NUMBER}" 2>/dev/null || printf '{}\n')"
173
+ PR_HEAD_SHA="$(jq -r '.head.sha // .headRefOid // ""' <<<"$PR_PULL_JSON")"
174
+ PR_MERGEABLE_STATUS="$(jq -r 'if .mergeable == null then "UNKNOWN" else (.mergeable | tostring | ascii_upcase) end' <<<"$PR_PULL_JSON" 2>/dev/null || printf 'UNKNOWN\n')"
175
+
176
+ pr_comments_json() {
177
+ local review_route="pulls/${PR_NUMBER}/comments"
178
+ local issue_route="issues/${PR_NUMBER}/comments"
179
+ local payload=""
180
+
181
+ if flow_using_gitea; then
182
+ payload="$(flow_github_api_repo "${REPO_SLUG}" "${issue_route}" 2>/dev/null || true)"
183
+ else
184
+ payload="$(flow_github_api_repo "${REPO_SLUG}" "${review_route}" 2>/dev/null || true)"
185
+ fi
186
+
187
+ if jq -e 'type == "array"' >/dev/null 2>&1 <<<"${payload}"; then
188
+ printf '%s\n' "${payload}"
189
+ return 0
190
+ fi
191
+
192
+ printf '[]\n'
193
+ }
194
+
195
+ pr_issue_comments_json() {
196
+ local payload=""
197
+ payload="$(flow_github_api_repo "${REPO_SLUG}" "issues/${PR_NUMBER}/comments" 2>/dev/null || true)"
198
+ if jq -e 'type == "array"' >/dev/null 2>&1 <<<"${payload}"; then
199
+ printf '%s\n' "${payload}"
200
+ return 0
201
+ fi
202
+ printf '[]\n'
203
+ }
204
+
175
205
  PR_REVIEW_FINDINGS_TEXT="$(
176
- flow_github_api_repo "${REPO_SLUG}" "pulls/${PR_NUMBER}/comments" \
206
+ pr_comments_json \
177
207
  | jq -r --arg head_sha "$PR_HEAD_SHA" '
178
208
  map(select(
179
209
  (.user.login == "chatgpt-codex-connector[bot]")
@@ -195,7 +225,7 @@ PR_REVIEW_FINDINGS_TEXT="$(
195
225
  '
196
226
  )"
197
227
  PR_BLOCKER_SUMMARY_TEXT="$(
198
- flow_github_api_repo "${REPO_SLUG}" "issues/${PR_NUMBER}/comments" \
228
+ pr_issue_comments_json \
199
229
  | jq -r '
200
230
  map(select((.body // "") | startswith("## PR final review blocker")))
201
231
  | if length == 0 then
@@ -225,12 +255,17 @@ PR_LOCAL_HOST_BLOCKER_SUMMARY_TEXT="$(latest_history_artifact_content "host-bloc
225
255
 
226
256
  WORKTREE_OUT="$("${WORKSPACE_DIR}/bin/new-pr-worktree.sh" "$PR_NUMBER" "$PR_HEAD_REF")"
227
257
  WORKTREE="$(awk -F= '/^WORKTREE=/{print $2}' <<<"$WORKTREE_OUT")"
258
+ PR_BASE_REMOTE="$(flow_resolve_forge_primary_remote "${WORKTREE}" "${REPO_SLUG}" 2>/dev/null || true)"
259
+ if [[ -z "${PR_BASE_REMOTE}" ]]; then
260
+ PR_BASE_REMOTE="origin"
261
+ fi
262
+ PR_BASE_TRACKING_REF="${PR_BASE_REMOTE}/${PR_BASE_REF}"
228
263
  PR_HOST_MERGE_STATUS="not-applicable"
229
264
  PR_HOST_MERGE_SUMMARY_TEXT="- not-applicable"
230
265
 
231
266
  materialize_host_merge_repair() {
232
267
  local merge_output=""
233
- if merge_output="$(git -C "$WORKTREE" merge --no-commit --no-ff "origin/${PR_BASE_REF}" 2>&1)"; then
268
+ if merge_output="$(git -C "$WORKTREE" merge --no-commit --no-ff "${PR_BASE_TRACKING_REF}" 2>&1)"; then
234
269
  PR_HOST_MERGE_STATUS="clean"
235
270
  if [[ -n "$merge_output" ]]; then
236
271
  PR_HOST_MERGE_SUMMARY_TEXT="$(printf '%s\n' "$merge_output")"
@@ -271,14 +306,14 @@ else
271
306
  PR_CONFLICT_PATHS_TEXT="$(
272
307
  (
273
308
  cd "$WORKTREE"
274
- base_sha="$(git merge-base HEAD "origin/${PR_BASE_REF}" 2>/dev/null || true)"
309
+ base_sha="$(git merge-base HEAD "${PR_BASE_TRACKING_REF}" 2>/dev/null || true)"
275
310
  if [[ -z "$base_sha" ]]; then
276
311
  printf '%s\n' "- unable to compute merge-base"
277
312
  exit 0
278
313
  fi
279
314
 
280
315
  conflict_paths="$(
281
- git merge-tree "$base_sha" HEAD "origin/${PR_BASE_REF}" \
316
+ git merge-tree "$base_sha" HEAD "${PR_BASE_TRACKING_REF}" \
282
317
  | awk '
283
318
  /^changed in both$/ { capture=1; next }
284
319
  capture && /^( base| our| their) / {
@@ -333,6 +368,7 @@ PR_HOST_MERGE_SUMMARY_TEXT="$PR_HOST_MERGE_SUMMARY_TEXT" \
333
368
  PR_REPO_ROOT="$PR_REPO_ROOT" \
334
369
  PR_DEPENDENCY_SOURCE_ROOT="$PR_DEPENDENCY_SOURCE_ROOT" \
335
370
  PR_WORKTREE="$WORKTREE" \
371
+ PR_BASE_TRACKING_REF="$PR_BASE_TRACKING_REF" \
336
372
  PR_WEB_PLAYWRIGHT_COMMAND="$WEB_PLAYWRIGHT_COMMAND" \
337
373
  REPO_SLUG="$REPO_SLUG" \
338
374
  TEMPLATE_FILE="$TEMPLATE_FILE" \
@@ -359,11 +395,11 @@ let requiredTargetedVerificationText = '- none';
359
395
  let preApprovedVerificationFallbacksText = '- none';
360
396
  try {
361
397
  const worktree = process.env.PR_WORKTREE || '';
362
- const baseRef = process.env.PR_BASE_REF || 'main';
398
+ const baseTrackingRef = process.env.PR_BASE_TRACKING_REF || `origin/${process.env.PR_BASE_REF || 'main'}`;
363
399
  if (worktree) {
364
400
  const changedFiles = execFileSync(
365
401
  'git',
366
- ['-C', worktree, 'diff', '--name-only', '--diff-filter=ACMR', `origin/${baseRef}...HEAD`],
402
+ ['-C', worktree, 'diff', '--name-only', '--diff-filter=ACMR', `${baseTrackingRef}...HEAD`],
367
403
  { encoding: 'utf8' },
368
404
  )
369
405
  .split('\n')
@@ -423,6 +459,7 @@ const replacements = {
423
459
  '{PR_URL}': process.env.PR_URL || '',
424
460
  '{PR_HEAD_REF}': process.env.PR_HEAD_REF || '',
425
461
  '{PR_BASE_REF}': process.env.PR_BASE_REF || '',
462
+ '{PR_BASE_TRACKING_REF}': process.env.PR_BASE_TRACKING_REF || '',
426
463
  '{PR_BODY}': process.env.PR_BODY || '',
427
464
  '{REPO_SLUG}': process.env.REPO_SLUG || '',
428
465
  '{PR_RISK}': process.env.PR_RISK || '',