@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.
Files changed (175) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +13 -0
  3. package/CLAUDE.md +2 -0
  4. package/THIRD_PARTY_NOTICES.md +1 -1
  5. package/agents/openai.yaml +1 -1
  6. package/assets/partials-agents/02-operating-mode.partial.md +2 -0
  7. package/assets/plugin-registry.v1.json +3 -3
  8. package/assets/skill-version.json +2 -2
  9. package/assets/templates/helpers/check-architecture-sync.sh +17 -0
  10. package/assets/templates/helpers/inspect-project-state.ts +8 -6
  11. package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
  12. package/assets/templates/helpers/workflow-contract.ts +5 -1
  13. package/docs/architecture/CURRENT.md +12 -4
  14. package/docs/forge-plugin-management.md +1 -1
  15. package/docs/operations/controller-performance-and-502.md +19 -0
  16. package/docs/operations/homebrew.md +2 -2
  17. package/package.json +5 -2
  18. package/scripts/architecture-event.ts +2 -820
  19. package/scripts/architecture-queue.sh +2 -645
  20. package/scripts/archive-architecture-request.sh +2 -242
  21. package/scripts/archive-workflow.sh +1 -291
  22. package/scripts/benchmark-work-efficiency.ts +0 -0
  23. package/scripts/capability-config.ts +2 -392
  24. package/scripts/capability-resolver.ts +2 -503
  25. package/scripts/capture-plan.sh +1 -332
  26. package/scripts/check-agent-tooling.sh +2 -1531
  27. package/scripts/check-architecture-sync.sh +2 -302
  28. package/scripts/check-brain-manifest.sh +2 -253
  29. package/scripts/check-context-files.sh +2 -47
  30. package/scripts/check-deploy-sql-order.sh +2 -115
  31. package/scripts/check-runtime-architecture.mjs +243 -16
  32. package/scripts/check-skill-version.ts +3 -191
  33. package/scripts/check-source-duplication.mjs +61 -0
  34. package/scripts/check-stable-baseline.ts +0 -0
  35. package/scripts/check-task-sync.sh +2 -63
  36. package/scripts/check-task-workflow.sh +2 -1000
  37. package/scripts/codex-handoff-resume.sh +2 -267
  38. package/scripts/context-contract-sync.sh +2 -514
  39. package/scripts/contract-run.ts +2 -463
  40. package/scripts/contract-worktree.sh +1 -702
  41. package/scripts/ensure-task-workflow.sh +1 -1192
  42. package/scripts/inspect-project-state.ts +4 -279
  43. package/scripts/lib/project-init-lib.sh +7 -2
  44. package/scripts/lib/run-forge-helper.sh +19 -0
  45. package/scripts/lib/run-forge-helper.ts +26 -0
  46. package/scripts/migrate-project-template.sh +1 -33
  47. package/scripts/migrate-workflow-docs.ts +4 -412
  48. package/scripts/new-plan.sh +1 -180
  49. package/scripts/new-spec.sh +1 -47
  50. package/scripts/new-sprint.sh +1 -46
  51. package/scripts/plan-to-todo.sh +1 -771
  52. package/scripts/prepare-codex-handoff.sh +2 -201
  53. package/scripts/prepare-handoff.sh +1 -78
  54. package/scripts/refresh-current-status.sh +2 -467
  55. package/scripts/run-governed-gate.ts +3 -0
  56. package/scripts/select-agent-context-blocks.sh +2 -64
  57. package/scripts/ship-worktrees.sh +1 -531
  58. package/scripts/sprint-backlog.sh +1 -834
  59. package/scripts/stage-runtime-release.ts +2 -1
  60. package/scripts/summarize-failures.sh +2 -123
  61. package/scripts/switch-plan.sh +1 -150
  62. package/scripts/sync-brain-docs.sh +2 -379
  63. package/scripts/sync-generated-authority.mjs +175 -0
  64. package/scripts/verify-contract.sh +2 -593
  65. package/scripts/verify-sprint.sh +1 -500
  66. package/scripts/workflow-contract.ts +4 -201
  67. package/scripts/workstream-sync.sh +2 -261
  68. package/src/cli/agent-jobs/executor-health.ts +1 -1
  69. package/src/cli/commands/plugin.ts +12 -3
  70. package/src/cli/controller/check-runner.ts +7 -2
  71. package/src/cli/controller/context-pack.ts +46 -21
  72. package/src/cli/github/contracts.ts +16 -0
  73. package/src/cli/github/github.ts +2 -16
  74. package/src/cli/github/session.ts +2 -1
  75. package/src/cli/local-bridge/facade-api.ts +37 -47
  76. package/src/cli/local-bridge/server.ts +2 -2
  77. package/src/cli/mcp/legacy-tool-service.ts +10 -33
  78. package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
  79. package/src/cli/mcp/repository-tools.ts +85 -89
  80. package/src/cli/mcp/server.ts +57 -13
  81. package/src/cli/mcp/tool-contract.ts +14 -0
  82. package/src/cli/mcp/toolset.ts +2 -2
  83. package/src/cli/mcp/transports/http.ts +23 -0
  84. package/src/cli/repositories/command-classifier.ts +2 -2
  85. package/src/cli/repositories/command-executor.ts +24 -6
  86. package/src/cli/repositories/repository-snapshot.ts +5 -2
  87. package/src/cli/repositories/structured-git.ts +57 -3
  88. package/src/cli/repository/inspector.ts +11 -3
  89. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  90. package/src/runtime/context/semantic-navigation-contract.ts +61 -0
  91. package/src/runtime/context/semantic-navigation.ts +9 -61
  92. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +18 -25
  93. package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
  94. package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
  95. package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
  96. package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
  97. package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
  98. package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
  99. package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
  100. package/src/runtime/control-plane/execution/work-handle-store.ts +1 -1
  101. package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
  102. package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
  103. package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
  104. package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
  105. package/src/runtime/control-plane/execution/work-task-receipt.ts +4 -43
  106. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +13 -3
  107. package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
  108. package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
  109. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
  110. package/src/runtime/control-plane/facade/controller-round-relay.ts +164 -37
  111. package/src/runtime/control-plane/facade/controller-session-store.ts +183 -24
  112. package/src/runtime/control-plane/facade/goal-workloop-access.ts +15 -1
  113. package/src/runtime/control-plane/facade/goal-workloop.ts +329 -148
  114. package/src/runtime/control-plane/facade/index.ts +2 -0
  115. package/src/runtime/control-plane/facade/plan-contract-store.ts +90 -22
  116. package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
  117. package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
  118. package/src/runtime/control-plane/facade/types.ts +68 -5
  119. package/src/runtime/control-plane/facade/work-contract-store.ts +3 -209
  120. package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
  121. package/src/runtime/control-plane/global-scheduler/maintenance.ts +14 -7
  122. package/src/runtime/control-plane/global-scheduler/scheduler.ts +5 -1
  123. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +13 -0
  124. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +15 -6
  125. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +54 -5
  126. package/src/runtime/control-plane/launcher/thin-launcher.ts +56 -7
  127. package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
  128. package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
  129. package/src/runtime/control-plane/routing/route-policy.ts +18 -6
  130. package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
  131. package/src/runtime/evidence/process-check-execution.ts +18 -0
  132. package/src/runtime/execution/jobs/child-reference.ts +6 -2
  133. package/src/runtime/execution/process-runtime/check-result.ts +53 -18
  134. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
  135. package/src/runtime/execution/process-runtime/command-facade.ts +39 -17
  136. package/src/runtime/execution/process-runtime/lightweight-managed.ts +20 -3
  137. package/src/runtime/execution/process-runtime/persisted-check.ts +4 -1
  138. package/src/runtime/execution/process-runtime/runtime.ts +10 -0
  139. package/src/runtime/execution/process-runtime/types.ts +2 -12
  140. package/src/runtime/execution/work-activity.ts +21 -0
  141. package/src/runtime/execution/workers/executor.ts +18 -13
  142. package/src/runtime/gateway/mcp/execution-tools.ts +35 -2930
  143. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
  144. package/src/runtime/gateway/mcp/router.ts +2 -2
  145. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +21 -2
  146. package/src/runtime/gateway/mcp/runtime-tools.ts +805 -1009
  147. package/src/runtime/plugins/app-store-connect-adapter.ts +93 -28
  148. package/src/runtime/plugins/browser-adapter.ts +256 -1083
  149. package/src/runtime/plugins/browser-handoff-host.ts +2 -25
  150. package/src/runtime/plugins/browser-handoff.ts +18 -3
  151. package/src/runtime/plugins/browser-macos-bridge.ts +121 -16
  152. package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
  153. package/src/runtime/plugins/browser-native-session-service.ts +222 -0
  154. package/src/runtime/plugins/browser-runtime-contract.ts +2 -0
  155. package/src/runtime/plugins/browser-runtime.ts +10 -0
  156. package/src/runtime/plugins/browser-session-store.ts +121 -0
  157. package/src/runtime/plugins/browser-session-types.ts +98 -0
  158. package/src/runtime/plugins/desktop-operator-registration.ts +4 -4
  159. package/src/runtime/plugins/external-adapter.ts +92 -58
  160. package/src/runtime/plugins/macos-capability-broker.ts +0 -8
  161. package/src/runtime/plugins/store.ts +66 -9
  162. package/src/runtime/plugins/types.ts +3 -1
  163. package/src/runtime/recovery/maintenance-executor.ts +61 -25
  164. package/src/runtime/root/package-connector-service.ts +46 -4
  165. package/src/runtime/root/package-runtime-release.ts +1 -1
  166. package/src/runtime/root/package-runtime-service.ts +33 -2
  167. package/src/runtime/root/release-manifest.ts +137 -16
  168. package/src/runtime/root/release-materialize.ts +109 -35
  169. package/src/runtime/root/types.ts +10 -0
  170. package/src/runtime/standalone-recovery/core.ts +425 -194
  171. package/src/runtime/standalone-recovery/entry.ts +1 -1
  172. package/src/runtime/standalone-recovery/installer.ts +1 -1
  173. package/src/runtime/workflow/schedules/engine.ts +113 -87
  174. package/src/runtime/workflow/schedules/settlement.ts +26 -23
  175. package/src/runtime/workflow/schedules/store.ts +79 -10
@@ -1,705 +1,4 @@
1
1
  #!/bin/bash
2
2
  set -euo pipefail
3
-
4
3
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
- if REPO_ROOT="$(git -C "$SCRIPT_DIR/.." rev-parse --show-toplevel 2>/dev/null)"; then
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/contract-worktree.sh start --plan <plan-file> [--path <worktree-path>] [--branch <branch-name>]
18
- scripts/contract-worktree.sh finish [--merge|--no-merge] [--target <branch>] [--message <commit-message>]
19
- scripts/contract-worktree.sh cleanup --slug <slug> [--target <branch>] [--dry-run]
20
- scripts/contract-worktree.sh status
21
- USAGE_EOF
22
- }
23
-
24
- json_escape() {
25
- local value="$1"
26
- value="${value//\\/\\\\}"
27
- value="${value//\"/\\\"}"
28
- value="${value//$'\n'/\\n}"
29
- value="${value//$'\r'/\\r}"
30
- value="${value//$'\t'/\\t}"
31
- printf '%s' "$value"
32
- }
33
-
34
- policy_get() {
35
- local jq_path="$1"
36
- local default_value="${2:-}"
37
- local value=""
38
-
39
- if [[ -f ".ai/harness/policy.json" ]] && command -v jq >/dev/null 2>&1; then
40
- value="$(jq -r "$jq_path // empty" ".ai/harness/policy.json" 2>/dev/null || true)"
41
- if [[ -n "$value" ]]; then
42
- printf '%s' "$value"
43
- return 0
44
- fi
45
- fi
46
-
47
- printf '%s' "$default_value"
48
- }
49
-
50
- check_architecture_freshness() {
51
- local target_branch="$1"
52
- local mode
53
-
54
- if [[ -x "scripts/check-architecture-sync.sh" ]]; then
55
- bash "scripts/check-architecture-sync.sh" --target "$target_branch"
56
- return $?
57
- fi
58
-
59
- mode="$(policy_get '.architecture.freshness_gate' 'advisory')"
60
- if [[ "$mode" == "strict" ]]; then
61
- echo "contract-worktree: strict architecture freshness gate failed: missing scripts/check-architecture-sync.sh" >&2
62
- return 1
63
- fi
64
-
65
- echo "contract-worktree: WARN missing scripts/check-architecture-sync.sh; skipping advisory architecture freshness gate" >&2
66
- return 0
67
- }
68
-
69
- normalize_slug() {
70
- printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g'
71
- }
72
-
73
- ACTIVE_PLAN_MARKER=".ai/harness/active-plan"
74
- LEGACY_ACTIVE_PLAN_MARKER=".claude/.active-plan"
75
- ACTIVE_WORKTREE_MARKER=".ai/harness/active-worktree"
76
-
77
- derive_slug_from_plan() {
78
- local plan_file="$1"
79
- local plan_base slug
80
- plan_base="$(basename "$plan_file")"
81
- slug="$(printf '%s' "$plan_base" | sed -E 's/^plan-[0-9]{8}-[0-9]{4}-//; s/\.md$//')"
82
- normalize_slug "${slug:-contract-task}"
83
- }
84
-
85
- derive_original_artifact_stem_from_plan() {
86
- local plan_file="$1"
87
- local plan_base stem
88
- plan_base="$(basename "$plan_file")"
89
- stem="$(printf '%s' "$plan_base" | sed -E 's/^plan-//; s/\.md$//')"
90
- if [[ "$stem" =~ ^[0-9]{8}-[0-9]{4}-.+ ]]; then
91
- printf '%s' "$stem"
92
- else
93
- derive_slug_from_plan "$plan_file"
94
- fi
95
- }
96
-
97
- is_transient_plan_slug() {
98
- case "$1" in
99
- think-plan-[0-9]*|codex-plan-[0-9]*|approved-plan-[0-9]*)
100
- return 0
101
- ;;
102
- esac
103
- return 1
104
- }
105
-
106
- derive_title_slug_from_plan() {
107
- local plan_file="$1"
108
- local title slug
109
- [[ -f "$plan_file" ]] || return 1
110
- title="$(awk '
111
- /^# Plan:[[:space:]]*/ {
112
- sub(/^# Plan:[[:space:]]*/, "")
113
- print
114
- exit
115
- }
116
- ' "$plan_file" | xargs)"
117
- [[ -n "$title" ]] || return 1
118
- slug="$(normalize_slug "$title")"
119
- [[ -n "$slug" ]] || return 1
120
- printf '%s' "$slug"
121
- }
122
-
123
- derive_artifact_stem_from_plan() {
124
- local plan_file="$1"
125
- local stem stamp slug title_slug
126
- stem="$(derive_original_artifact_stem_from_plan "$plan_file")"
127
- if [[ "$stem" =~ ^[0-9]{8}-[0-9]{4}-.+ ]]; then
128
- stamp="$(printf '%s' "$stem" | sed -E 's/^([0-9]{8}-[0-9]{4})-.+$/\1/')"
129
- slug="$(printf '%s' "$stem" | sed -E 's/^[0-9]{8}-[0-9]{4}-//')"
130
- if is_transient_plan_slug "$slug"; then
131
- title_slug="$(derive_title_slug_from_plan "$plan_file" || true)"
132
- if [[ -n "$title_slug" && "$title_slug" != "$slug" ]]; then
133
- printf '%s-%s' "$stamp" "$title_slug"
134
- return 0
135
- fi
136
- fi
137
- printf '%s' "$stem"
138
- else
139
- derive_slug_from_plan "$plan_file"
140
- fi
141
- }
142
-
143
- is_linked_worktree() {
144
- local git_dir
145
- git_dir="$(git rev-parse --git-dir 2>/dev/null || true)"
146
- [[ "$git_dir" == *".git/worktrees/"* ]]
147
- }
148
-
149
- find_worktree_for_branch() {
150
- local branch="$1"
151
- git worktree list --porcelain | awk -v branch_ref="refs/heads/${branch}" '
152
- $1 == "worktree" { path = $2; next }
153
- $1 == "branch" && $2 == branch_ref { print path; exit }
154
- '
155
- }
156
-
157
- default_worktree_path() {
158
- local slug="$1"
159
- local parent repo_name
160
- parent="$(dirname "$REPO_ROOT")"
161
- repo_name="$(basename "$REPO_ROOT")"
162
- printf '%s/%s-wt-%s' "$parent" "$repo_name" "$slug"
163
- }
164
-
165
- write_start_metadata() {
166
- local slug="$1"
167
- local plan_file="$2"
168
- local branch_name="$3"
169
- local worktree_path="$4"
170
- local base_branch="$5"
171
- local metadata_dir=".ai/harness/worktrees"
172
- local metadata_file="${metadata_dir}/${slug}.json"
173
-
174
- mkdir -p "$metadata_dir"
175
- cat > "$metadata_file" <<EOF_METADATA
176
- {
177
- "slug": "$(json_escape "$slug")",
178
- "plan": "$(json_escape "$plan_file")",
179
- "branch": "$(json_escape "$branch_name")",
180
- "worktree": "$(json_escape "$worktree_path")",
181
- "source_repo": "$(json_escape "$REPO_ROOT")",
182
- "base_branch": "$(json_escape "$base_branch")",
183
- "started_at": "$(date '+%Y-%m-%dT%H:%M:%S%z')"
184
- }
185
- EOF_METADATA
186
- }
187
-
188
- copy_plan_into_worktree() {
189
- local plan_file="$1"
190
- local worktree_path="$2"
191
- local target_plan="$worktree_path/$plan_file"
192
-
193
- mkdir -p "$(dirname "$target_plan")"
194
- cp "$plan_file" "$target_plan"
195
- }
196
-
197
- remove_copied_untracked_source_plan() {
198
- local plan_file="$1"
199
- local worktree_path="$2"
200
-
201
- if git ls-files --others --exclude-standard -- "$plan_file" | grep -Fxq "$plan_file" \
202
- && cmp -s "$plan_file" "$worktree_path/$plan_file"; then
203
- rm -f "$plan_file"
204
- echo "[ContractWorktree] Moved untracked source plan into contract worktree: $plan_file"
205
- fi
206
- }
207
-
208
- marker_points_to_plan() {
209
- local marker_file="$1"
210
- local plan_file="$2"
211
- local marker_plan
212
-
213
- [[ -f "$marker_file" ]] || return 1
214
- marker_plan="$(cat "$marker_file" 2>/dev/null | xargs)"
215
- [[ "$marker_plan" == "$plan_file" || "$marker_plan" == "./$plan_file" ]]
216
- }
217
-
218
- clear_primary_markers_for_transferred_plan() {
219
- local plan_file="$1"
220
-
221
- if marker_points_to_plan "$ACTIVE_PLAN_MARKER" "$plan_file" \
222
- || marker_points_to_plan "$LEGACY_ACTIVE_PLAN_MARKER" "$plan_file"; then
223
- rm -f "$ACTIVE_PLAN_MARKER" "$LEGACY_ACTIVE_PLAN_MARKER" "$ACTIVE_WORKTREE_MARKER"
224
- echo "[ContractWorktree] Cleared primary active markers for transferred plan: $plan_file"
225
- fi
226
- }
227
-
228
- start_worktree() {
229
- local plan_file=""
230
- local worktree_path=""
231
- local branch_name=""
232
- local run_plan_to_todo=1
233
-
234
- while [[ $# -gt 0 ]]; do
235
- case "$1" in
236
- --plan)
237
- [[ -n "${2:-}" ]] || { echo "contract-worktree: --plan requires a value" >&2; exit 2; }
238
- plan_file="${2#./}"
239
- shift 2
240
- ;;
241
- --path)
242
- [[ -n "${2:-}" ]] || { echo "contract-worktree: --path requires a value" >&2; exit 2; }
243
- worktree_path="$2"
244
- shift 2
245
- ;;
246
- --branch)
247
- [[ -n "${2:-}" ]] || { echo "contract-worktree: --branch requires a value" >&2; exit 2; }
248
- branch_name="$2"
249
- shift 2
250
- ;;
251
- --no-plan-to-todo)
252
- run_plan_to_todo=0
253
- shift
254
- ;;
255
- --help|-h)
256
- usage
257
- exit 0
258
- ;;
259
- *)
260
- echo "contract-worktree: unknown start argument: $1" >&2
261
- usage
262
- exit 2
263
- ;;
264
- esac
265
- done
266
-
267
- [[ -n "$plan_file" ]] || { echo "contract-worktree: start requires --plan" >&2; exit 2; }
268
- [[ -f "$plan_file" ]] || { echo "contract-worktree: plan file not found: $plan_file" >&2; exit 2; }
269
-
270
- if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
271
- echo "contract-worktree: not inside a git repository" >&2
272
- exit 2
273
- fi
274
-
275
- if is_linked_worktree; then
276
- echo "[ContractWorktree] Already in a linked worktree: $REPO_ROOT"
277
- return 0
278
- fi
279
-
280
- local slug branch_prefix base_branch existing_worktree
281
- slug="$(derive_slug_from_plan "$plan_file")"
282
- branch_prefix="$(policy_get '.worktree_strategy.branch_prefix' 'codex/')"
283
- base_branch="$(policy_get '.worktree_strategy.base_branch' 'main')"
284
- branch_name="${branch_name:-${branch_prefix}${slug}}"
285
- worktree_path="${worktree_path:-$(default_worktree_path "$slug")}"
286
-
287
- existing_worktree="$(find_worktree_for_branch "$branch_name" || true)"
288
- if [[ -n "$existing_worktree" ]]; then
289
- worktree_path="$existing_worktree"
290
- echo "[ContractWorktree] Reusing existing worktree: $worktree_path"
291
- elif git show-ref --verify --quiet "refs/heads/$branch_name"; then
292
- git worktree add "$worktree_path" "$branch_name"
293
- echo "[ContractWorktree] Added worktree for existing branch: $worktree_path"
294
- else
295
- git worktree add "$worktree_path" -b "$branch_name" HEAD
296
- echo "[ContractWorktree] Created worktree: $worktree_path"
297
- fi
298
-
299
- copy_plan_into_worktree "$plan_file" "$worktree_path"
300
- remove_copied_untracked_source_plan "$plan_file" "$worktree_path"
301
- clear_primary_markers_for_transferred_plan "$plan_file"
302
-
303
- mkdir -p "$worktree_path/.ai/harness/worktrees"
304
- (
305
- cd "$worktree_path"
306
- write_start_metadata "$slug" "$plan_file" "$branch_name" "$worktree_path" "$base_branch"
307
- if [[ "$run_plan_to_todo" -eq 1 && -f "scripts/plan-to-todo.sh" ]]; then
308
- FORGE_CONTRACT_WORKTREE=1 bash "scripts/plan-to-todo.sh" --plan "$plan_file"
309
- fi
310
- )
311
-
312
- echo "[ContractWorktree] Branch: $branch_name"
313
- echo "[ContractWorktree] Plan: $worktree_path/$plan_file"
314
- }
315
-
316
- status_worktree() {
317
- if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
318
- echo "[ContractWorktree] Not in a git repository"
319
- return 0
320
- fi
321
-
322
- if is_linked_worktree; then
323
- echo "[ContractWorktree] linked worktree"
324
- else
325
- echo "[ContractWorktree] primary worktree"
326
- fi
327
-
328
- echo "branch: $(git branch --show-current 2>/dev/null || true)"
329
- echo "root: $REPO_ROOT"
330
- }
331
-
332
- is_local_runtime_marker_path() {
333
- case "$1" in
334
- .ai/harness/active-plan|.ai/harness/active-worktree|.claude/.active-plan)
335
- return 0
336
- ;;
337
- esac
338
- return 1
339
- }
340
-
341
- check_scope_against_contract() {
342
- local contract_file="$1"
343
- local changed_paths path blocked=0
344
-
345
- [[ -f "$contract_file" ]] || return 0
346
- if [[ ! -f ".ai/hooks/lib/workflow-state.sh" ]]; then
347
- return 0
348
- fi
349
-
350
- # shellcheck source=/dev/null
351
- . ".ai/hooks/lib/workflow-state.sh"
352
-
353
- changed_paths="$(
354
- {
355
- git -c core.quotePath=false diff --name-only
356
- git -c core.quotePath=false diff --cached --name-only
357
- git -c core.quotePath=false ls-files --others --exclude-standard
358
- } | awk 'NF && !seen[$0]++'
359
- )"
360
-
361
- while IFS= read -r path; do
362
- [[ -n "$path" ]] || continue
363
- if is_local_runtime_marker_path "$path"; then
364
- continue
365
- fi
366
- if ! workflow_contract_allows_path "$contract_file" "$path"; then
367
- echo "[ContractWorktree] Changed path is outside active contract allowed_paths: $path" >&2
368
- blocked=1
369
- fi
370
- done <<< "$changed_paths"
371
-
372
- [[ "$blocked" -eq 0 ]]
373
- }
374
-
375
- clean_matching_untracked_target_files() {
376
- local target_worktree="$1"
377
- local source_branch="$2"
378
- local path tmp_file
379
-
380
- while IFS= read -r path; do
381
- [[ -n "$path" ]] || continue
382
- tmp_file="$(mktemp)"
383
- if git -C "$target_worktree" show "${source_branch}:${path}" > "$tmp_file" 2>/dev/null \
384
- && cmp -s "$tmp_file" "$target_worktree/$path"; then
385
- rm -f "$target_worktree/$path"
386
- echo "[ContractWorktree] Removed matching untracked target file before merge: $path"
387
- fi
388
- rm -f "$tmp_file"
389
- done < <(git -C "$target_worktree" ls-files --others --exclude-standard)
390
- }
391
-
392
- clean_local_runtime_markers() {
393
- rm -f .ai/harness/active-plan .ai/harness/active-worktree .claude/.active-plan
394
- }
395
-
396
- latest_plan_for_slug() {
397
- local slug="$1"
398
- local latest
399
- latest="$(find plans -maxdepth 1 -type f -name "plan-*-${slug}.md" 2>/dev/null | sort | tail -1)"
400
- [[ -n "$latest" ]] || return 1
401
- printf '%s' "$latest"
402
- }
403
-
404
- archive_finished_workflow() {
405
- local plan_file="$1"
406
-
407
- [[ -n "$plan_file" ]] || { echo "contract-worktree: no active plan found to archive" >&2; exit 1; }
408
- [[ -f "$plan_file" ]] || { echo "contract-worktree: active plan not found for archive: $plan_file" >&2; exit 1; }
409
- [[ -x "scripts/archive-workflow.sh" ]] || { echo "contract-worktree: scripts/archive-workflow.sh is missing or not executable" >&2; exit 1; }
410
-
411
- echo "[ContractWorktree] Archiving completed workflow before merge: $plan_file"
412
- bash "scripts/archive-workflow.sh" --plan "$plan_file" --outcome Completed
413
- }
414
-
415
- finish_worktree() {
416
- local merge_back=1
417
- local target_branch
418
- local commit_message=""
419
-
420
- target_branch="$(policy_get '.worktree_strategy.merge_back.target' 'main')"
421
-
422
- while [[ $# -gt 0 ]]; do
423
- case "$1" in
424
- --merge)
425
- merge_back=1
426
- shift
427
- ;;
428
- --no-merge)
429
- merge_back=0
430
- shift
431
- ;;
432
- --target)
433
- [[ -n "${2:-}" ]] || { echo "contract-worktree: --target requires a value" >&2; exit 2; }
434
- target_branch="$2"
435
- shift 2
436
- ;;
437
- --message|-m)
438
- [[ -n "${2:-}" ]] || { echo "contract-worktree: --message requires a value" >&2; exit 2; }
439
- commit_message="$2"
440
- shift 2
441
- ;;
442
- --help|-h)
443
- usage
444
- exit 0
445
- ;;
446
- *)
447
- echo "contract-worktree: unknown finish argument: $1" >&2
448
- usage
449
- exit 2
450
- ;;
451
- esac
452
- done
453
-
454
- if ! is_linked_worktree; then
455
- echo "contract-worktree: finish must run from the linked contract worktree" >&2
456
- exit 1
457
- fi
458
-
459
- local current_branch slug active_plan contract_file target_worktree artifact_stem
460
- current_branch="$(git branch --show-current)"
461
- [[ -n "$current_branch" ]] || { echo "contract-worktree: detached HEAD is not supported" >&2; exit 1; }
462
- [[ "$current_branch" != "$target_branch" ]] || { echo "contract-worktree: already on target branch $target_branch" >&2; exit 1; }
463
- slug="$(normalize_slug "${current_branch##*/}")"
464
- commit_message="${commit_message:-feat(contract): complete ${slug}}"
465
-
466
- if [[ -f ".ai/hooks/lib/workflow-state.sh" ]]; then
467
- # shellcheck source=/dev/null
468
- . ".ai/hooks/lib/workflow-state.sh"
469
- active_plan="$(get_active_plan || true)"
470
- if [[ -n "$active_plan" ]]; then
471
- contract_file="$(workflow_active_contract || true)"
472
- fi
473
- fi
474
-
475
- if [[ -z "${active_plan:-}" ]]; then
476
- active_plan="$(latest_plan_for_slug "$slug" || true)"
477
- fi
478
- if [[ -n "${active_plan:-}" && -z "${contract_file:-}" ]]; then
479
- artifact_stem="$(derive_artifact_stem_from_plan "$active_plan")"
480
- if [[ -f "tasks/contracts/${artifact_stem}.contract.md" ]] || [[ ! -f "tasks/contracts/${slug}.contract.md" ]]; then
481
- contract_file="tasks/contracts/${artifact_stem}.contract.md"
482
- fi
483
- fi
484
- contract_file="${contract_file:-tasks/contracts/${slug}.contract.md}"
485
-
486
- [[ -n "$contract_file" && -f "$contract_file" ]] || { echo "contract-worktree: no active sprint contract found" >&2; exit 1; }
487
-
488
- check_architecture_freshness "$target_branch"
489
- bash "scripts/verify-sprint.sh"
490
- check_scope_against_contract "$contract_file"
491
- archive_finished_workflow "$active_plan"
492
- clean_local_runtime_markers
493
- backfill_sprint_backlog "$active_plan" || true
494
-
495
- if ! git diff --quiet || ! git diff --cached --quiet || [[ -n "$(git ls-files --others --exclude-standard)" ]]; then
496
- git add -A
497
- git commit -m "$commit_message"
498
- else
499
- echo "[ContractWorktree] No tracked changes to commit."
500
- fi
501
-
502
- if [[ "$merge_back" -eq 0 ]]; then
503
- echo "[ContractWorktree] Merge skipped by --no-merge."
504
- return 0
505
- fi
506
-
507
- target_worktree="$(find_worktree_for_branch "$target_branch" || true)"
508
- [[ -n "$target_worktree" ]] || { echo "contract-worktree: target branch has no checked-out worktree: $target_branch" >&2; exit 1; }
509
-
510
- clean_matching_untracked_target_files "$target_worktree" "$current_branch"
511
-
512
- if [[ -n "$(git -C "$target_worktree" status --porcelain=v1 --untracked-files=all)" ]]; then
513
- echo "contract-worktree: target worktree is dirty, refusing merge: $target_worktree" >&2
514
- exit 1
515
- fi
516
-
517
- git -C "$target_worktree" merge --ff-only "$current_branch"
518
- echo "[ContractWorktree] Merged $current_branch into $target_branch at $target_worktree"
519
- }
520
-
521
- # Warn-only sprint backlog back-fill: plans captured via sprint-backlog
522
- # start-task carry "> **Source Ref**: sprint:<file>#<task>". After the
523
- # workflow archives, flip that backlog row so the update merges with the
524
- # slice. Any failure warns and never blocks finish.
525
- backfill_sprint_backlog() {
526
- local plan_file="$1"
527
- local archived_plan source_ref sprint_path task_ref
528
-
529
- [[ -f "scripts/sprint-backlog.sh" ]] || return 0
530
-
531
- archived_plan="plans/archive/$(basename "$plan_file")"
532
- if [[ ! -f "$archived_plan" ]]; then
533
- # Archive may have renamed on collision (-vN suffix); fall back to the
534
- # newest archived file sharing the stem, then to the original path.
535
- archived_plan="$(find plans/archive -maxdepth 1 -type f -name "$(basename "$plan_file" .md)*.md" 2>/dev/null | sort | tail -1)"
536
- fi
537
- [[ -n "$archived_plan" && -f "$archived_plan" ]] || archived_plan="$plan_file"
538
- if [[ ! -f "$archived_plan" ]]; then
539
- echo "[ContractWorktree] Warning: cannot resolve archived plan for sprint back-fill: $plan_file" >&2
540
- return 0
541
- fi
542
-
543
- source_ref="$(awk '/^> \*\*Source Ref\*\*:/ {sub(/^> \*\*Source Ref\*\*:[[:space:]]*/, ""); gsub(/\r/, ""); print; exit}' "$archived_plan" 2>/dev/null | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
544
- case "$source_ref" in
545
- sprint:*#*)
546
- ;;
547
- *)
548
- return 0
549
- ;;
550
- esac
551
-
552
- # Split on the FIRST '#': the sprint path is slug-generated and cannot
553
- # contain '#', while the task name is free text and may.
554
- sprint_path="${source_ref#sprint:}"
555
- task_ref="${sprint_path#*#}"
556
- sprint_path="${sprint_path%%#*}"
557
-
558
- if bash scripts/sprint-backlog.sh complete-task --sprint "$sprint_path" --task "$task_ref" --plan "$archived_plan"; then
559
- echo "[ContractWorktree] Sprint backlog updated: $sprint_path ($task_ref)"
560
- else
561
- echo "[ContractWorktree] Warning: sprint backlog back-fill failed for $sprint_path ($task_ref); update the row manually." >&2
562
- fi
563
- return 0
564
- }
565
-
566
- cleanup_worktree() {
567
- local slug=""
568
- local target_branch
569
- local dry_run=0
570
-
571
- target_branch="$(policy_get '.worktree_strategy.merge_back.target' 'main')"
572
-
573
- while [[ $# -gt 0 ]]; do
574
- case "$1" in
575
- --slug)
576
- [[ -n "${2:-}" ]] || { echo "contract-worktree: --slug requires a value" >&2; exit 2; }
577
- slug="$2"
578
- shift 2
579
- ;;
580
- --target)
581
- [[ -n "${2:-}" ]] || { echo "contract-worktree: --target requires a value" >&2; exit 2; }
582
- target_branch="$2"
583
- shift 2
584
- ;;
585
- --dry-run)
586
- dry_run=1
587
- shift
588
- ;;
589
- --help|-h)
590
- usage
591
- exit 0
592
- ;;
593
- *)
594
- echo "contract-worktree: unknown cleanup argument: $1" >&2
595
- usage
596
- exit 2
597
- ;;
598
- esac
599
- done
600
-
601
- [[ -n "$slug" ]] || { echo "contract-worktree: cleanup requires --slug" >&2; exit 2; }
602
- slug="${slug#codex/}"
603
- slug="$(normalize_slug "$slug")"
604
- [[ -n "$slug" ]] || { echo "contract-worktree: slug is empty after normalization" >&2; exit 2; }
605
-
606
- if is_linked_worktree; then
607
- echo "contract-worktree: cleanup must run from the target primary worktree, not a linked contract worktree" >&2
608
- exit 1
609
- fi
610
-
611
- local target_worktree current_root branch_prefix branch_name worktree_path metadata_file
612
- branch_prefix="$(policy_get '.worktree_strategy.branch_prefix' 'codex/')"
613
- branch_name="${branch_prefix}${slug}"
614
- metadata_file=".ai/harness/worktrees/${slug}.json"
615
- target_worktree="$(find_worktree_for_branch "$target_branch" || true)"
616
- [[ -n "$target_worktree" ]] || { echo "contract-worktree: target branch has no checked-out worktree: $target_branch" >&2; exit 1; }
617
-
618
- current_root="$(pwd -P)"
619
- target_worktree="$(cd "$target_worktree" && pwd -P)"
620
- if [[ "$current_root" != "$target_worktree" ]]; then
621
- echo "contract-worktree: cleanup must run from target worktree $target_worktree" >&2
622
- exit 1
623
- fi
624
-
625
- worktree_path="$(find_worktree_for_branch "$branch_name" || true)"
626
- if [[ -n "$worktree_path" ]]; then
627
- worktree_path="$(cd "$worktree_path" && pwd -P)"
628
- case "$current_root" in
629
- "$worktree_path"|"$worktree_path"/*)
630
- echo "contract-worktree: refusing to remove current working directory: $worktree_path" >&2
631
- exit 1
632
- ;;
633
- esac
634
- fi
635
-
636
- if git show-ref --verify --quiet "refs/heads/$branch_name"; then
637
- if ! git merge-base --is-ancestor "$branch_name" "$target_branch" >/dev/null 2>&1; then
638
- echo "contract-worktree: branch $branch_name is not fully merged into $target_branch; refusing cleanup" >&2
639
- exit 1
640
- fi
641
- else
642
- echo "[ContractWorktree] Branch already absent, skipping: $branch_name"
643
- fi
644
-
645
- if [[ -n "$worktree_path" ]]; then
646
- if [[ -n "$(git -C "$worktree_path" status --porcelain=v1 --untracked-files=all)" ]]; then
647
- echo "contract-worktree: linked worktree is dirty, refusing cleanup: $worktree_path" >&2
648
- echo "contract-worktree: pick/apply/commit useful changes first; scaffold-only discard belongs in scripts/ship-worktrees.sh --cleanup-merged --discard-scaffold-only" >&2
649
- exit 1
650
- fi
651
- else
652
- echo "[ContractWorktree] Worktree already absent, skipping: $branch_name"
653
- fi
654
-
655
- if [[ "$dry_run" -eq 1 ]]; then
656
- echo "[ContractWorktree] dry-run cleanup slug=$slug target=$target_branch"
657
- echo "[ContractWorktree] would remove worktree: ${worktree_path:-"(absent)"}"
658
- echo "[ContractWorktree] would delete branch: $branch_name"
659
- echo "[ContractWorktree] would remove metadata: $metadata_file"
660
- return 0
661
- fi
662
-
663
- if [[ -n "$worktree_path" ]]; then
664
- git worktree remove "$worktree_path"
665
- echo "[ContractWorktree] Removed worktree: $worktree_path"
666
- fi
667
-
668
- if git show-ref --verify --quiet "refs/heads/$branch_name"; then
669
- git branch -d "$branch_name"
670
- echo "[ContractWorktree] Deleted branch: $branch_name"
671
- fi
672
-
673
- if [[ -e "$metadata_file" ]]; then
674
- rm -f "$metadata_file"
675
- echo "[ContractWorktree] Removed metadata: $metadata_file"
676
- else
677
- echo "[ContractWorktree] Metadata already absent, skipping: $metadata_file"
678
- fi
679
- }
680
-
681
- command_name="${1:-status}"
682
- shift || true
683
-
684
- case "$command_name" in
685
- start)
686
- start_worktree "$@"
687
- ;;
688
- finish)
689
- finish_worktree "$@"
690
- ;;
691
- cleanup)
692
- cleanup_worktree "$@"
693
- ;;
694
- status)
695
- status_worktree
696
- ;;
697
- --help|-h|help)
698
- usage
699
- ;;
700
- *)
701
- echo "contract-worktree: unknown command: $command_name" >&2
702
- usage
703
- exit 2
704
- ;;
705
- esac
4
+ exec "$SCRIPT_DIR/lib/run-forge-helper.sh" "contract-worktree.sh" "$@"