@moretea-labs/forge 1.7.0 → 1.7.1
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/AGENTS.md +2 -0
- package/CHANGELOG.md +13 -0
- package/CLAUDE.md +2 -0
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/agents/openai.yaml +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +2 -0
- package/assets/plugin-registry.v1.json +3 -3
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/check-architecture-sync.sh +17 -0
- package/assets/templates/helpers/inspect-project-state.ts +8 -6
- package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
- package/assets/templates/helpers/workflow-contract.ts +5 -1
- package/docs/architecture/CURRENT.md +12 -4
- package/docs/forge-plugin-management.md +1 -1
- package/docs/operations/controller-performance-and-502.md +19 -0
- package/docs/operations/homebrew.md +2 -2
- package/package.json +5 -2
- package/scripts/architecture-event.ts +2 -820
- package/scripts/architecture-queue.sh +2 -645
- package/scripts/archive-architecture-request.sh +2 -242
- package/scripts/archive-workflow.sh +1 -291
- package/scripts/benchmark-work-efficiency.ts +0 -0
- package/scripts/capability-config.ts +2 -392
- package/scripts/capability-resolver.ts +2 -503
- package/scripts/capture-plan.sh +1 -332
- package/scripts/check-agent-tooling.sh +2 -1531
- package/scripts/check-architecture-sync.sh +2 -302
- package/scripts/check-brain-manifest.sh +2 -253
- package/scripts/check-context-files.sh +2 -47
- package/scripts/check-deploy-sql-order.sh +2 -115
- package/scripts/check-runtime-architecture.mjs +243 -16
- package/scripts/check-skill-version.ts +3 -191
- package/scripts/check-source-duplication.mjs +61 -0
- package/scripts/check-stable-baseline.ts +0 -0
- package/scripts/check-task-sync.sh +2 -63
- package/scripts/check-task-workflow.sh +2 -1000
- package/scripts/codex-handoff-resume.sh +2 -267
- package/scripts/context-contract-sync.sh +2 -514
- package/scripts/contract-run.ts +2 -463
- package/scripts/contract-worktree.sh +1 -702
- package/scripts/ensure-task-workflow.sh +1 -1192
- package/scripts/inspect-project-state.ts +4 -279
- package/scripts/lib/project-init-lib.sh +7 -2
- package/scripts/lib/run-forge-helper.sh +19 -0
- package/scripts/lib/run-forge-helper.ts +26 -0
- package/scripts/migrate-project-template.sh +1 -33
- package/scripts/migrate-workflow-docs.ts +4 -412
- package/scripts/new-plan.sh +1 -180
- package/scripts/new-spec.sh +1 -47
- package/scripts/new-sprint.sh +1 -46
- package/scripts/plan-to-todo.sh +1 -771
- package/scripts/prepare-codex-handoff.sh +2 -201
- package/scripts/prepare-handoff.sh +1 -78
- package/scripts/refresh-current-status.sh +2 -467
- package/scripts/run-governed-gate.ts +3 -0
- package/scripts/select-agent-context-blocks.sh +2 -64
- package/scripts/ship-worktrees.sh +1 -531
- package/scripts/sprint-backlog.sh +1 -834
- package/scripts/stage-runtime-release.ts +2 -1
- package/scripts/summarize-failures.sh +2 -123
- package/scripts/switch-plan.sh +1 -150
- package/scripts/sync-brain-docs.sh +2 -379
- package/scripts/sync-generated-authority.mjs +175 -0
- package/scripts/verify-contract.sh +2 -593
- package/scripts/verify-sprint.sh +1 -500
- package/scripts/workflow-contract.ts +4 -201
- package/scripts/workstream-sync.sh +2 -261
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/commands/plugin.ts +12 -3
- package/src/cli/controller/check-runner.ts +7 -2
- package/src/cli/controller/context-pack.ts +46 -21
- package/src/cli/github/contracts.ts +16 -0
- package/src/cli/github/github.ts +2 -16
- package/src/cli/github/session.ts +2 -1
- package/src/cli/local-bridge/facade-api.ts +37 -47
- package/src/cli/local-bridge/server.ts +2 -2
- package/src/cli/mcp/legacy-tool-service.ts +10 -33
- package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
- package/src/cli/mcp/repository-tools.ts +85 -89
- package/src/cli/mcp/server.ts +57 -13
- package/src/cli/mcp/tool-contract.ts +14 -0
- package/src/cli/mcp/toolset.ts +2 -2
- package/src/cli/mcp/transports/http.ts +23 -0
- package/src/cli/repositories/command-classifier.ts +2 -2
- package/src/cli/repositories/command-executor.ts +24 -6
- package/src/cli/repositories/repository-snapshot.ts +5 -2
- package/src/cli/repositories/structured-git.ts +57 -3
- package/src/cli/repository/inspector.ts +11 -3
- package/src/runtime/context/generic-lsp-provider.ts +1 -1
- package/src/runtime/context/semantic-navigation-contract.ts +61 -0
- package/src/runtime/context/semantic-navigation.ts +9 -61
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +18 -25
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
- package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
- package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
- package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
- package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
- package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
- package/src/runtime/control-plane/execution/work-handle-store.ts +1 -1
- package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
- package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
- package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
- package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
- package/src/runtime/control-plane/execution/work-task-receipt.ts +4 -43
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +13 -3
- package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
- package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
- package/src/runtime/control-plane/facade/controller-round-relay.ts +164 -37
- package/src/runtime/control-plane/facade/controller-session-store.ts +183 -24
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +15 -1
- package/src/runtime/control-plane/facade/goal-workloop.ts +329 -148
- package/src/runtime/control-plane/facade/index.ts +2 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +90 -22
- package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
- package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
- package/src/runtime/control-plane/facade/types.ts +68 -5
- package/src/runtime/control-plane/facade/work-contract-store.ts +3 -209
- package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +14 -7
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +5 -1
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +13 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +15 -6
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +54 -5
- package/src/runtime/control-plane/launcher/thin-launcher.ts +56 -7
- package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
- package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
- package/src/runtime/control-plane/routing/route-policy.ts +18 -6
- package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
- package/src/runtime/evidence/process-check-execution.ts +18 -0
- package/src/runtime/execution/jobs/child-reference.ts +6 -2
- package/src/runtime/execution/process-runtime/check-result.ts +53 -18
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
- package/src/runtime/execution/process-runtime/command-facade.ts +39 -17
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +20 -3
- package/src/runtime/execution/process-runtime/persisted-check.ts +4 -1
- package/src/runtime/execution/process-runtime/runtime.ts +10 -0
- package/src/runtime/execution/process-runtime/types.ts +2 -12
- package/src/runtime/execution/work-activity.ts +21 -0
- package/src/runtime/execution/workers/executor.ts +18 -13
- package/src/runtime/gateway/mcp/execution-tools.ts +35 -2930
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
- package/src/runtime/gateway/mcp/router.ts +2 -2
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +21 -2
- package/src/runtime/gateway/mcp/runtime-tools.ts +805 -1009
- package/src/runtime/plugins/app-store-connect-adapter.ts +93 -28
- package/src/runtime/plugins/browser-adapter.ts +256 -1083
- package/src/runtime/plugins/browser-handoff-host.ts +2 -25
- package/src/runtime/plugins/browser-handoff.ts +18 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +121 -16
- package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
- package/src/runtime/plugins/browser-native-session-service.ts +222 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +2 -0
- package/src/runtime/plugins/browser-runtime.ts +10 -0
- package/src/runtime/plugins/browser-session-store.ts +121 -0
- package/src/runtime/plugins/browser-session-types.ts +98 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +4 -4
- package/src/runtime/plugins/external-adapter.ts +92 -58
- package/src/runtime/plugins/macos-capability-broker.ts +0 -8
- package/src/runtime/plugins/store.ts +66 -9
- package/src/runtime/plugins/types.ts +3 -1
- package/src/runtime/recovery/maintenance-executor.ts +61 -25
- package/src/runtime/root/package-connector-service.ts +46 -4
- package/src/runtime/root/package-runtime-release.ts +1 -1
- package/src/runtime/root/package-runtime-service.ts +33 -2
- package/src/runtime/root/release-manifest.ts +137 -16
- package/src/runtime/root/release-materialize.ts +109 -35
- package/src/runtime/root/types.ts +10 -0
- package/src/runtime/standalone-recovery/core.ts +425 -194
- package/src/runtime/standalone-recovery/entry.ts +1 -1
- package/src/runtime/standalone-recovery/installer.ts +1 -1
- package/src/runtime/workflow/schedules/engine.ts +113 -87
- package/src/runtime/workflow/schedules/settlement.ts +26 -23
- package/src/runtime/workflow/schedules/store.ts +79 -10
|
@@ -1,534 +1,4 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
set -euo pipefail
|
|
3
|
-
|
|
4
3
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
-
|
|
6
|
-
:
|
|
7
|
-
elif [[ "$SCRIPT_DIR" == */.ai/harness/scripts ]]; then
|
|
8
|
-
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
9
|
-
else
|
|
10
|
-
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
11
|
-
fi
|
|
12
|
-
cd "$REPO_ROOT"
|
|
13
|
-
|
|
14
|
-
usage() {
|
|
15
|
-
cat <<'USAGE_EOF'
|
|
16
|
-
Usage:
|
|
17
|
-
scripts/ship-worktrees.sh [--target <branch>] [--remote <name>] [--slug <slug>] [--ready] [--dry-run]
|
|
18
|
-
scripts/ship-worktrees.sh --local-merge [--target <branch>] [--slug <slug>] [--dry-run]
|
|
19
|
-
scripts/ship-worktrees.sh --cleanup-merged [--target <branch>] [--slug <slug>] [--discard-scaffold-only] [--dry-run]
|
|
20
|
-
|
|
21
|
-
Default mode validates finished contract worktrees, commits them through
|
|
22
|
-
contract-worktree finish --no-merge, pushes their codex/* branches, and opens
|
|
23
|
-
draft PRs. It does not fast-forward main by default.
|
|
24
|
-
USAGE_EOF
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
json_escape() {
|
|
28
|
-
local value="$1"
|
|
29
|
-
value="${value//\\/\\\\}"
|
|
30
|
-
value="${value//\"/\\\"}"
|
|
31
|
-
value="${value//$'\n'/\\n}"
|
|
32
|
-
value="${value//$'\r'/\\r}"
|
|
33
|
-
value="${value//$'\t'/\\t}"
|
|
34
|
-
printf '%s' "$value"
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
policy_get() {
|
|
38
|
-
local jq_path="$1"
|
|
39
|
-
local default_value="${2:-}"
|
|
40
|
-
local value=""
|
|
41
|
-
|
|
42
|
-
if [[ -f ".ai/harness/policy.json" ]] && command -v jq >/dev/null 2>&1; then
|
|
43
|
-
value="$(jq -r "$jq_path // empty" ".ai/harness/policy.json" 2>/dev/null || true)"
|
|
44
|
-
if [[ -n "$value" ]]; then
|
|
45
|
-
printf '%s' "$value"
|
|
46
|
-
return 0
|
|
47
|
-
fi
|
|
48
|
-
fi
|
|
49
|
-
|
|
50
|
-
printf '%s' "$default_value"
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
normalize_slug() {
|
|
54
|
-
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g'
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
plan_slug_from_path() {
|
|
58
|
-
local plan_file="$1"
|
|
59
|
-
local base slug
|
|
60
|
-
base="$(basename "$plan_file")"
|
|
61
|
-
slug="$(printf '%s' "$base" | sed -E 's/^plan-[0-9]{8}-[0-9]{4}-//; s/\.md$//')"
|
|
62
|
-
normalize_slug "${slug:-contract-task}"
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
is_linked_worktree() {
|
|
66
|
-
local git_dir
|
|
67
|
-
git_dir="$(git rev-parse --git-dir 2>/dev/null || true)"
|
|
68
|
-
[[ "$git_dir" == *".git/worktrees/"* ]]
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
current_branch() {
|
|
72
|
-
git branch --show-current 2>/dev/null || true
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
load_workflow_state() {
|
|
76
|
-
if [[ -f ".ai/hooks/lib/workflow-state.sh" ]]; then
|
|
77
|
-
# shellcheck source=/dev/null
|
|
78
|
-
. ".ai/hooks/lib/workflow-state.sh"
|
|
79
|
-
fi
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
run_cmd() {
|
|
83
|
-
echo "[Ship] $*"
|
|
84
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
85
|
-
return 0
|
|
86
|
-
fi
|
|
87
|
-
"$@"
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
fail() {
|
|
91
|
-
echo "ship-worktrees: $*" >&2
|
|
92
|
-
exit 1
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
list_contract_worktrees() {
|
|
96
|
-
local branch_prefix="$1"
|
|
97
|
-
git worktree list --porcelain | awk -v prefix="refs/heads/${branch_prefix}" '
|
|
98
|
-
$1 == "worktree" { path = $2; next }
|
|
99
|
-
$1 == "branch" && index($2, prefix) == 1 {
|
|
100
|
-
branch = $2
|
|
101
|
-
sub(/^refs\/heads\//, "", branch)
|
|
102
|
-
print branch "\t" path
|
|
103
|
-
}
|
|
104
|
-
'
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
dirty_paths_for_worktree() {
|
|
108
|
-
local worktree="$1"
|
|
109
|
-
{
|
|
110
|
-
git -C "$worktree" diff --name-only
|
|
111
|
-
git -C "$worktree" diff --cached --name-only
|
|
112
|
-
git -C "$worktree" ls-files --others --exclude-standard
|
|
113
|
-
} | sed '/^$/d' | sort -u
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
is_scaffold_path() {
|
|
117
|
-
local path="$1"
|
|
118
|
-
case "$path" in
|
|
119
|
-
tasks/todos.md|plans/plan-*.md|tasks/contracts/*.contract.md|tasks/reviews/*.review.md|tasks/notes/*.notes.md|.ai/harness/active-plan|.ai/harness/active-worktree|.claude/.active-plan|.ai/harness/worktrees/*.json)
|
|
120
|
-
return 0
|
|
121
|
-
;;
|
|
122
|
-
esac
|
|
123
|
-
return 1
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
non_scaffold_dirty_paths() {
|
|
127
|
-
local worktree="$1" path
|
|
128
|
-
while IFS= read -r path; do
|
|
129
|
-
[[ -n "$path" ]] || continue
|
|
130
|
-
is_scaffold_path "$path" || printf '%s\n' "$path"
|
|
131
|
-
done < <(dirty_paths_for_worktree "$worktree")
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
print_dirty_paths() {
|
|
135
|
-
local worktree="$1"
|
|
136
|
-
dirty_paths_for_worktree "$worktree" | sed 's/^/ - /' >&2
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
fail_dirty_merged_worktree() {
|
|
140
|
-
local branch="$1" path="$2" non_scaffold
|
|
141
|
-
echo "ship-worktrees: dirty merged linked worktree: $branch at $path" >&2
|
|
142
|
-
echo "ship-worktrees: branch ancestry only proves committed changes are in $TARGET_BRANCH; these worktree changes are still outside $TARGET_BRANCH." >&2
|
|
143
|
-
echo "ship-worktrees: pick/apply/commit useful changes before cleanup; do not use tgz, reset --hard, git clean, or stash as closeout." >&2
|
|
144
|
-
echo "ship-worktrees: dirty paths:" >&2
|
|
145
|
-
print_dirty_paths "$path"
|
|
146
|
-
|
|
147
|
-
non_scaffold="$(non_scaffold_dirty_paths "$path")"
|
|
148
|
-
if [[ -z "$non_scaffold" ]]; then
|
|
149
|
-
echo "ship-worktrees: if these are generated plan/contract/review/notes scaffold only, rerun with --discard-scaffold-only." >&2
|
|
150
|
-
else
|
|
151
|
-
echo "ship-worktrees: --discard-scaffold-only is blocked by non-scaffold paths:" >&2
|
|
152
|
-
printf '%s\n' "$non_scaffold" | sed 's/^/ - /' >&2
|
|
153
|
-
fi
|
|
154
|
-
return 1
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
discard_scaffold_dirty_paths() {
|
|
158
|
-
local worktree="$1" path
|
|
159
|
-
local tracked_paths=()
|
|
160
|
-
local untracked_paths=()
|
|
161
|
-
|
|
162
|
-
while IFS= read -r path; do
|
|
163
|
-
[[ -n "$path" ]] || continue
|
|
164
|
-
if git -C "$worktree" ls-files --error-unmatch -- "$path" >/dev/null 2>&1; then
|
|
165
|
-
tracked_paths+=("$path")
|
|
166
|
-
else
|
|
167
|
-
untracked_paths+=("$path")
|
|
168
|
-
fi
|
|
169
|
-
done < <(dirty_paths_for_worktree "$worktree")
|
|
170
|
-
|
|
171
|
-
if [[ "${#tracked_paths[@]}" -gt 0 ]]; then
|
|
172
|
-
run_cmd git -C "$worktree" reset -- "${tracked_paths[@]}"
|
|
173
|
-
run_cmd git -C "$worktree" checkout -- "${tracked_paths[@]}"
|
|
174
|
-
fi
|
|
175
|
-
|
|
176
|
-
for path in "${untracked_paths[@]}"; do
|
|
177
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
178
|
-
echo "[Ship] would remove scaffold file: $worktree/$path"
|
|
179
|
-
else
|
|
180
|
-
rm -f "$worktree/$path"
|
|
181
|
-
fi
|
|
182
|
-
done
|
|
183
|
-
|
|
184
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
185
|
-
echo "[Ship] Would discard scaffold-only changes in $worktree"
|
|
186
|
-
else
|
|
187
|
-
echo "[Ship] Discarded scaffold-only changes in $worktree"
|
|
188
|
-
fi
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
guard_dirty_merged_worktree() {
|
|
192
|
-
local branch="$1" path="$2" non_scaffold
|
|
193
|
-
[[ -z "$(git -C "$path" status --porcelain=v1 --untracked-files=all)" ]] && return 0
|
|
194
|
-
|
|
195
|
-
if [[ "$DISCARD_SCAFFOLD_ONLY" -eq 0 ]]; then
|
|
196
|
-
fail_dirty_merged_worktree "$branch" "$path"
|
|
197
|
-
return 1
|
|
198
|
-
fi
|
|
199
|
-
|
|
200
|
-
non_scaffold="$(non_scaffold_dirty_paths "$path")"
|
|
201
|
-
if [[ -n "$non_scaffold" ]]; then
|
|
202
|
-
echo "ship-worktrees: refusing --discard-scaffold-only for dirty merged linked worktree with non-scaffold paths: $branch at $path" >&2
|
|
203
|
-
printf '%s\n' "$non_scaffold" | sed 's/^/ - /' >&2
|
|
204
|
-
return 1
|
|
205
|
-
fi
|
|
206
|
-
|
|
207
|
-
discard_scaffold_dirty_paths "$path"
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
active_plan_or_empty() {
|
|
211
|
-
local active_plan=""
|
|
212
|
-
if declare -F get_active_plan >/dev/null 2>&1; then
|
|
213
|
-
active_plan="$(get_active_plan 2>/dev/null || true)"
|
|
214
|
-
elif [[ -f ".ai/harness/active-plan" ]]; then
|
|
215
|
-
active_plan="$(cat ".ai/harness/active-plan" 2>/dev/null | xargs)"
|
|
216
|
-
fi
|
|
217
|
-
printf '%s' "$active_plan"
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
active_slug_or_empty() {
|
|
221
|
-
local active_plan
|
|
222
|
-
active_plan="$(active_plan_or_empty)"
|
|
223
|
-
[[ -n "$active_plan" ]] || return 0
|
|
224
|
-
plan_slug_from_path "$active_plan"
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
require_finish_ready() {
|
|
228
|
-
local contract_file="" checks_file=".ai/harness/checks/latest.json" checks_error=""
|
|
229
|
-
|
|
230
|
-
[[ -x "scripts/contract-worktree.sh" ]] || fail "scripts/contract-worktree.sh is missing or not executable"
|
|
231
|
-
[[ -x "scripts/verify-sprint.sh" ]] || fail "scripts/verify-sprint.sh is missing or not executable"
|
|
232
|
-
|
|
233
|
-
load_workflow_state
|
|
234
|
-
if declare -F workflow_active_contract >/dev/null 2>&1; then
|
|
235
|
-
contract_file="$(workflow_active_contract 2>/dev/null || true)"
|
|
236
|
-
fi
|
|
237
|
-
if declare -F workflow_checks_file >/dev/null 2>&1; then
|
|
238
|
-
checks_file="$(workflow_checks_file)"
|
|
239
|
-
fi
|
|
240
|
-
|
|
241
|
-
[[ -n "$contract_file" && -f "$contract_file" ]] || fail "active sprint contract is missing"
|
|
242
|
-
|
|
243
|
-
run_cmd bash "scripts/verify-sprint.sh"
|
|
244
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
245
|
-
return 0
|
|
246
|
-
fi
|
|
247
|
-
|
|
248
|
-
if declare -F workflow_checks_pass >/dev/null 2>&1; then
|
|
249
|
-
if ! checks_error="$(workflow_checks_pass "$checks_file" "$contract_file")"; then
|
|
250
|
-
fail "$checks_error"
|
|
251
|
-
fi
|
|
252
|
-
elif ! grep -Eq '"status"[[:space:]]*:[[:space:]]*"pass"' "$checks_file"; then
|
|
253
|
-
fail "structured checks are not passing in $checks_file"
|
|
254
|
-
fi
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
finish_contract_worktree() {
|
|
258
|
-
local merge_mode="$1"
|
|
259
|
-
require_finish_ready
|
|
260
|
-
if [[ "$merge_mode" == "local" ]]; then
|
|
261
|
-
run_cmd bash "scripts/contract-worktree.sh" finish --target "$TARGET_BRANCH"
|
|
262
|
-
else
|
|
263
|
-
run_cmd bash "scripts/contract-worktree.sh" finish --no-merge --target "$TARGET_BRANCH"
|
|
264
|
-
fi
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
pr_title_for_branch() {
|
|
268
|
-
local branch="$1"
|
|
269
|
-
local active_plan title
|
|
270
|
-
active_plan="$(active_plan_or_empty)"
|
|
271
|
-
if [[ -n "$active_plan" && -f "$active_plan" ]]; then
|
|
272
|
-
title="$(awk '/^# / { sub(/^# /, ""); print; exit }' "$active_plan" | sed -E 's/^Plan:[[:space:]]*//')"
|
|
273
|
-
fi
|
|
274
|
-
title="${title:-Ship ${branch}}"
|
|
275
|
-
printf '%s' "$title"
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
pr_body_for_branch() {
|
|
279
|
-
local branch="$1"
|
|
280
|
-
cat <<EOF_BODY
|
|
281
|
-
Automated forge ship for \`${branch}\`.
|
|
282
|
-
|
|
283
|
-
Checks:
|
|
284
|
-
- Forge /review review artifact recommends pass.
|
|
285
|
-
- External acceptance is recorded in the sprint review.
|
|
286
|
-
- \`bash scripts/verify-sprint.sh\` passed before \`contract-worktree.sh finish --no-merge\`.
|
|
287
|
-
|
|
288
|
-
This PR intentionally does not merge \`${TARGET_BRANCH}\` locally.
|
|
289
|
-
EOF_BODY
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
push_branch() {
|
|
293
|
-
local branch="$1"
|
|
294
|
-
git remote get-url "$REMOTE_NAME" >/dev/null 2>&1 || fail "remote not found: $REMOTE_NAME"
|
|
295
|
-
run_cmd git push -u "$REMOTE_NAME" "$branch"
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
create_or_report_pr() {
|
|
299
|
-
local branch="$1"
|
|
300
|
-
local gh_bin existing title body output status
|
|
301
|
-
local args=()
|
|
302
|
-
gh_bin="${FORGE_GH_BIN:-gh}"
|
|
303
|
-
command -v "$gh_bin" >/dev/null 2>&1 || fail "gh is required for default PR ship mode"
|
|
304
|
-
|
|
305
|
-
existing="$("$gh_bin" pr list --base "$TARGET_BRANCH" --head "$branch" --json url --jq '.[0].url // ""' 2>/dev/null || true)"
|
|
306
|
-
if [[ -n "$existing" ]]; then
|
|
307
|
-
echo "[Ship] PR already exists for $branch: $existing"
|
|
308
|
-
return 0
|
|
309
|
-
fi
|
|
310
|
-
|
|
311
|
-
title="$(pr_title_for_branch "$branch")"
|
|
312
|
-
body="$(pr_body_for_branch "$branch")"
|
|
313
|
-
args=(pr create --base "$TARGET_BRANCH" --head "$branch" --title "$title" --body "$body")
|
|
314
|
-
if [[ "$DRAFT_PR" -eq 1 ]]; then
|
|
315
|
-
args+=(--draft)
|
|
316
|
-
fi
|
|
317
|
-
|
|
318
|
-
echo "[Ship] $gh_bin ${args[*]}"
|
|
319
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
320
|
-
return 0
|
|
321
|
-
fi
|
|
322
|
-
|
|
323
|
-
if output="$("$gh_bin" "${args[@]}" 2>&1)"; then
|
|
324
|
-
[[ -z "$output" ]] || printf '%s\n' "$output"
|
|
325
|
-
return 0
|
|
326
|
-
fi
|
|
327
|
-
|
|
328
|
-
status=$?
|
|
329
|
-
existing="$("$gh_bin" pr list --base "$TARGET_BRANCH" --head "$branch" --json url --jq '.[0].url // ""' 2>/dev/null || true)"
|
|
330
|
-
if [[ -n "$existing" ]]; then
|
|
331
|
-
echo "[Ship] PR already exists for $branch after create failure: $existing"
|
|
332
|
-
return 0
|
|
333
|
-
fi
|
|
334
|
-
|
|
335
|
-
[[ -z "$output" ]] || printf '%s\n' "$output" >&2
|
|
336
|
-
fail "gh pr create failed for $branch (exit $status)"
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
ship_linked_pr() {
|
|
340
|
-
local branch
|
|
341
|
-
branch="$(current_branch)"
|
|
342
|
-
[[ -n "$branch" ]] || fail "detached HEAD is not supported"
|
|
343
|
-
[[ "$branch" != "$TARGET_BRANCH" ]] || fail "refusing to ship target branch as linked worktree"
|
|
344
|
-
case "$branch" in
|
|
345
|
-
"$BRANCH_PREFIX"*) ;;
|
|
346
|
-
*) fail "linked ship expects branch prefix $BRANCH_PREFIX, got $branch" ;;
|
|
347
|
-
esac
|
|
348
|
-
|
|
349
|
-
finish_contract_worktree "pr"
|
|
350
|
-
push_branch "$branch"
|
|
351
|
-
create_or_report_pr "$branch"
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
ship_linked_local_merge() {
|
|
355
|
-
local branch
|
|
356
|
-
branch="$(current_branch)"
|
|
357
|
-
[[ -n "$branch" ]] || fail "detached HEAD is not supported"
|
|
358
|
-
[[ "$branch" != "$TARGET_BRANCH" ]] || fail "refusing to local-merge target branch as linked worktree"
|
|
359
|
-
finish_contract_worktree "local"
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
ship_primary_dirty_pr() {
|
|
363
|
-
local status active_slug base_slug branch message
|
|
364
|
-
status="$(git status --porcelain=v1 --untracked-files=all)"
|
|
365
|
-
[[ -n "$status" ]] || return 0
|
|
366
|
-
|
|
367
|
-
[[ "$(current_branch)" == "$TARGET_BRANCH" ]] || fail "main closeout must start from $TARGET_BRANCH"
|
|
368
|
-
active_slug="$(active_slug_or_empty)"
|
|
369
|
-
base_slug="${SLUG_OVERRIDE:-$active_slug}"
|
|
370
|
-
[[ -n "$base_slug" ]] || fail "main worktree has changes; pass --slug or keep an active plan so ship can name the closeout branch"
|
|
371
|
-
base_slug="$(normalize_slug "$base_slug")"
|
|
372
|
-
branch="${BRANCH_PREFIX}${base_slug}-main-closeout"
|
|
373
|
-
message="chore(ship): close out ${base_slug}"
|
|
374
|
-
|
|
375
|
-
if git show-ref --verify --quiet "refs/heads/$branch"; then
|
|
376
|
-
fail "closeout branch already exists: $branch"
|
|
377
|
-
fi
|
|
378
|
-
|
|
379
|
-
run_cmd git switch -c "$branch"
|
|
380
|
-
run_cmd git add -A
|
|
381
|
-
run_cmd git commit -m "$message"
|
|
382
|
-
push_branch "$branch"
|
|
383
|
-
create_or_report_pr "$branch"
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
ship_primary_pr() {
|
|
387
|
-
local branch path shipped=0
|
|
388
|
-
local child_args=()
|
|
389
|
-
[[ "$DRY_RUN" -eq 1 ]] && child_args+=(--dry-run)
|
|
390
|
-
[[ "$DRAFT_PR" -eq 0 ]] && child_args+=(--ready)
|
|
391
|
-
while IFS=$'\t' read -r branch path; do
|
|
392
|
-
[[ -n "$branch" && -n "$path" ]] || continue
|
|
393
|
-
[[ "$(cd "$path" && pwd -P)" != "$(pwd -P)" ]] || continue
|
|
394
|
-
echo "[Ship] Shipping linked worktree $branch at $path with PR mode"
|
|
395
|
-
(cd "$path" && bash "scripts/ship-worktrees.sh" --target "$TARGET_BRANCH" --remote "$REMOTE_NAME" "${child_args[@]}")
|
|
396
|
-
shipped=1
|
|
397
|
-
done < <(list_contract_worktrees "$BRANCH_PREFIX")
|
|
398
|
-
|
|
399
|
-
ship_primary_dirty_pr
|
|
400
|
-
|
|
401
|
-
if [[ "$shipped" -eq 0 && -z "$(git status --porcelain=v1 --untracked-files=all)" ]]; then
|
|
402
|
-
echo "[Ship] Nothing to ship."
|
|
403
|
-
fi
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
ship_primary_local_merge() {
|
|
407
|
-
local branch path slug shipped=0
|
|
408
|
-
local child_args=()
|
|
409
|
-
[[ "$DRY_RUN" -eq 1 ]] && child_args+=(--dry-run)
|
|
410
|
-
while IFS=$'\t' read -r branch path; do
|
|
411
|
-
[[ -n "$branch" && -n "$path" ]] || continue
|
|
412
|
-
[[ "$(cd "$path" && pwd -P)" != "$(pwd -P)" ]] || continue
|
|
413
|
-
slug="${branch#${BRANCH_PREFIX}}"
|
|
414
|
-
echo "[Ship] Shipping linked worktree $branch at $path with local merge mode"
|
|
415
|
-
(cd "$path" && bash "scripts/ship-worktrees.sh" --local-merge --target "$TARGET_BRANCH" "${child_args[@]}")
|
|
416
|
-
run_cmd bash "scripts/contract-worktree.sh" cleanup --slug "$slug" --target "$TARGET_BRANCH"
|
|
417
|
-
shipped=1
|
|
418
|
-
done < <(list_contract_worktrees "$BRANCH_PREFIX")
|
|
419
|
-
|
|
420
|
-
if [[ "$shipped" -eq 0 ]]; then
|
|
421
|
-
echo "[Ship] No linked contract worktrees found for local merge."
|
|
422
|
-
fi
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
cleanup_merged() {
|
|
426
|
-
local branch path slug cleaned=0
|
|
427
|
-
! is_linked_worktree || fail "--cleanup-merged must run from the target primary worktree"
|
|
428
|
-
|
|
429
|
-
while IFS=$'\t' read -r branch path; do
|
|
430
|
-
[[ -n "$branch" && -n "$path" ]] || continue
|
|
431
|
-
slug="${branch#${BRANCH_PREFIX}}"
|
|
432
|
-
if git merge-base --is-ancestor "$branch" "$TARGET_BRANCH" >/dev/null 2>&1; then
|
|
433
|
-
guard_dirty_merged_worktree "$branch" "$path" || exit 1
|
|
434
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
435
|
-
run_cmd bash "scripts/contract-worktree.sh" cleanup --slug "$slug" --target "$TARGET_BRANCH" --dry-run
|
|
436
|
-
else
|
|
437
|
-
run_cmd bash "scripts/contract-worktree.sh" cleanup --slug "$slug" --target "$TARGET_BRANCH"
|
|
438
|
-
fi
|
|
439
|
-
cleaned=1
|
|
440
|
-
else
|
|
441
|
-
echo "[Ship] Skipped unmerged branch: $branch"
|
|
442
|
-
fi
|
|
443
|
-
done < <(list_contract_worktrees "$BRANCH_PREFIX")
|
|
444
|
-
|
|
445
|
-
if [[ "$cleaned" -eq 0 ]]; then
|
|
446
|
-
echo "[Ship] No merged contract worktrees to clean."
|
|
447
|
-
fi
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
MODE="pr"
|
|
451
|
-
TARGET_BRANCH=""
|
|
452
|
-
REMOTE_NAME="origin"
|
|
453
|
-
SLUG_OVERRIDE=""
|
|
454
|
-
DRAFT_PR=1
|
|
455
|
-
DRY_RUN=0
|
|
456
|
-
DISCARD_SCAFFOLD_ONLY=0
|
|
457
|
-
|
|
458
|
-
while [[ $# -gt 0 ]]; do
|
|
459
|
-
case "$1" in
|
|
460
|
-
--target)
|
|
461
|
-
[[ -n "${2:-}" ]] || fail "--target requires a value"
|
|
462
|
-
TARGET_BRANCH="$2"
|
|
463
|
-
shift 2
|
|
464
|
-
;;
|
|
465
|
-
--remote)
|
|
466
|
-
[[ -n "${2:-}" ]] || fail "--remote requires a value"
|
|
467
|
-
REMOTE_NAME="$2"
|
|
468
|
-
shift 2
|
|
469
|
-
;;
|
|
470
|
-
--slug)
|
|
471
|
-
[[ -n "${2:-}" ]] || fail "--slug requires a value"
|
|
472
|
-
SLUG_OVERRIDE="$(normalize_slug "$2")"
|
|
473
|
-
shift 2
|
|
474
|
-
;;
|
|
475
|
-
--ready)
|
|
476
|
-
DRAFT_PR=0
|
|
477
|
-
shift
|
|
478
|
-
;;
|
|
479
|
-
--draft)
|
|
480
|
-
DRAFT_PR=1
|
|
481
|
-
shift
|
|
482
|
-
;;
|
|
483
|
-
--dry-run)
|
|
484
|
-
DRY_RUN=1
|
|
485
|
-
shift
|
|
486
|
-
;;
|
|
487
|
-
--local-merge)
|
|
488
|
-
MODE="local-merge"
|
|
489
|
-
shift
|
|
490
|
-
;;
|
|
491
|
-
--cleanup-merged)
|
|
492
|
-
MODE="cleanup-merged"
|
|
493
|
-
shift
|
|
494
|
-
;;
|
|
495
|
-
--discard-scaffold-only)
|
|
496
|
-
DISCARD_SCAFFOLD_ONLY=1
|
|
497
|
-
shift
|
|
498
|
-
;;
|
|
499
|
-
--help|-h)
|
|
500
|
-
usage
|
|
501
|
-
exit 0
|
|
502
|
-
;;
|
|
503
|
-
*)
|
|
504
|
-
fail "unknown argument: $1"
|
|
505
|
-
;;
|
|
506
|
-
esac
|
|
507
|
-
done
|
|
508
|
-
|
|
509
|
-
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || fail "not inside a git repository"
|
|
510
|
-
TARGET_BRANCH="${TARGET_BRANCH:-$(policy_get '.worktree_strategy.merge_back.target' 'main')}"
|
|
511
|
-
BRANCH_PREFIX="$(policy_get '.worktree_strategy.branch_prefix' 'codex/')"
|
|
512
|
-
|
|
513
|
-
case "$MODE" in
|
|
514
|
-
pr)
|
|
515
|
-
if is_linked_worktree; then
|
|
516
|
-
ship_linked_pr
|
|
517
|
-
else
|
|
518
|
-
ship_primary_pr
|
|
519
|
-
fi
|
|
520
|
-
;;
|
|
521
|
-
local-merge)
|
|
522
|
-
if is_linked_worktree; then
|
|
523
|
-
ship_linked_local_merge
|
|
524
|
-
else
|
|
525
|
-
ship_primary_local_merge
|
|
526
|
-
fi
|
|
527
|
-
;;
|
|
528
|
-
cleanup-merged)
|
|
529
|
-
cleanup_merged
|
|
530
|
-
;;
|
|
531
|
-
*)
|
|
532
|
-
fail "unsupported mode: $MODE"
|
|
533
|
-
;;
|
|
534
|
-
esac
|
|
4
|
+
exec "$SCRIPT_DIR/lib/run-forge-helper.sh" "ship-worktrees.sh" "$@"
|