@moretea-labs/forge 1.7.0 → 1.7.2
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 +26 -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 +125 -101
- 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 +69 -29
- 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 +150 -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 +815 -1015
- 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 +3 -2
- package/src/runtime/root/package-runtime-service.ts +48 -3
- 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,837 +1,4 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
set -euo pipefail
|
|
3
|
-
|
|
4
3
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
-
|
|
6
|
-
cd "$REPO_ROOT"
|
|
7
|
-
elif [[ "$SCRIPT_DIR" == */.ai/harness/scripts ]]; then
|
|
8
|
-
cd "$SCRIPT_DIR/../../.."
|
|
9
|
-
else
|
|
10
|
-
cd "$SCRIPT_DIR/.."
|
|
11
|
-
fi
|
|
12
|
-
|
|
13
|
-
usage() {
|
|
14
|
-
cat <<'USAGE_EOF'
|
|
15
|
-
Usage:
|
|
16
|
-
scripts/sprint-backlog.sh init --slug <slug> [--title <title>]
|
|
17
|
-
scripts/sprint-backlog.sh status
|
|
18
|
-
scripts/sprint-backlog.sh next
|
|
19
|
-
scripts/sprint-backlog.sh start-task [--task <index|task>] [--execute] [--force] [--sprint <file>]
|
|
20
|
-
scripts/sprint-backlog.sh complete-task --task <index|task> [--plan <plan-file>] [--sprint <file>]
|
|
21
|
-
|
|
22
|
-
Program-level sprint backlog helper. PRDs live in plans/prds/ as the upper
|
|
23
|
-
planning layer; sprints live in plans/sprints/ as ordered execution backlogs.
|
|
24
|
-
Each backlog row is expanded with /plan before the existing plan -> contract
|
|
25
|
-
-> worktree flow. tasks/todos.md stays the deferred-goal ledger.
|
|
26
|
-
|
|
27
|
-
start-task reserves the next (or named) pending backlog row and can capture a
|
|
28
|
-
thin plan seed. The coding agent must still use `/plan` to expand the row into
|
|
29
|
-
a decision-complete plan before code edits.
|
|
30
|
-
--sprint overrides the active-sprint marker (still confined to the sprints
|
|
31
|
-
dir), which finish back-fill uses inside worktrees where the runtime marker
|
|
32
|
-
is absent.
|
|
33
|
-
|
|
34
|
-
Exit codes: 0 success; 1 error; 2 usage error; 3 no pending backlog task (next/start-task).
|
|
35
|
-
USAGE_EOF
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
policy_file=".ai/harness/policy.json"
|
|
39
|
-
|
|
40
|
-
policy_get() {
|
|
41
|
-
local jq_path="$1"
|
|
42
|
-
local default_value="$2"
|
|
43
|
-
|
|
44
|
-
if [[ -f "$policy_file" ]] && command -v jq >/dev/null 2>&1; then
|
|
45
|
-
local value
|
|
46
|
-
value="$(jq -r "$jq_path // empty" "$policy_file" 2>/dev/null || true)"
|
|
47
|
-
if [[ -n "$value" ]]; then
|
|
48
|
-
printf '%s' "$value"
|
|
49
|
-
return 0
|
|
50
|
-
fi
|
|
51
|
-
fi
|
|
52
|
-
|
|
53
|
-
printf '%s' "$default_value"
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
sprints_dir="$(policy_get '.sprints.dir' 'plans/sprints')"
|
|
57
|
-
marker_file="$(policy_get '.sprints.active_marker_file' '.ai/harness/sprint/active-sprint')"
|
|
58
|
-
template_file="$(policy_get '.sprints.template_file' '.claude/templates/sprint.template.md')"
|
|
59
|
-
|
|
60
|
-
normalize_slug() {
|
|
61
|
-
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g'
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
# Trim without xargs: xargs chokes on unbalanced quotes in user-edited text.
|
|
65
|
-
trim() {
|
|
66
|
-
sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//'
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
extract_status() {
|
|
70
|
-
local file="$1"
|
|
71
|
-
awk '/\*\*Status\*\*:/ {sub(/^.*\*\*Status\*\*: */, ""); gsub(/\r/, ""); print; exit}' "$file" | trim
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
# Explicit sprint override (--sprint) for callers running where the runtime
|
|
75
|
-
# marker does not exist, e.g. finish back-fill inside a contract worktree.
|
|
76
|
-
sprint_override=""
|
|
77
|
-
|
|
78
|
-
sprint_file_under_sprints_dir() {
|
|
79
|
-
local sprint_file="$1"
|
|
80
|
-
local sprints_real sprint_dir_real
|
|
81
|
-
|
|
82
|
-
case "$sprint_file" in
|
|
83
|
-
"$sprints_dir"/*) ;;
|
|
84
|
-
*) return 1 ;;
|
|
85
|
-
esac
|
|
86
|
-
case "$sprint_file" in
|
|
87
|
-
*..*) return 1 ;;
|
|
88
|
-
esac
|
|
89
|
-
[[ -f "$sprint_file" ]] || return 1
|
|
90
|
-
[[ ! -L "$sprint_file" ]] || return 1
|
|
91
|
-
|
|
92
|
-
sprints_real="$(cd -P "$sprints_dir" 2>/dev/null && pwd)" || return 1
|
|
93
|
-
sprint_dir_real="$(cd -P "$(dirname "$sprint_file")" 2>/dev/null && pwd)" || return 1
|
|
94
|
-
case "$sprint_dir_real" in
|
|
95
|
-
"$sprints_real"|"$sprints_real"/*) ;;
|
|
96
|
-
*) return 1 ;;
|
|
97
|
-
esac
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
active_sprint_file() {
|
|
101
|
-
local sprint_file
|
|
102
|
-
if [[ -n "$sprint_override" ]]; then
|
|
103
|
-
sprint_file="$sprint_override"
|
|
104
|
-
else
|
|
105
|
-
[[ -f "$marker_file" ]] || return 1
|
|
106
|
-
sprint_file="$(trim < "$marker_file" 2>/dev/null)"
|
|
107
|
-
fi
|
|
108
|
-
[[ -n "$sprint_file" ]] || return 1
|
|
109
|
-
# Containment: the marker is repo-controlled, but complete-task rewrites the
|
|
110
|
-
# file it points at, so never follow it outside the sprints dir.
|
|
111
|
-
sprint_file_under_sprints_dir "$sprint_file" || return 1
|
|
112
|
-
printf '%s' "$sprint_file"
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
require_active_sprint() {
|
|
116
|
-
local sprint_file
|
|
117
|
-
if ! sprint_file="$(active_sprint_file)"; then
|
|
118
|
-
if [[ -n "$sprint_override" ]]; then
|
|
119
|
-
echo "sprint-backlog: --sprint does not resolve to a sprint file under ${sprints_dir}: $sprint_override" >&2
|
|
120
|
-
else
|
|
121
|
-
echo "sprint-backlog: no active sprint (marker: $marker_file)" >&2
|
|
122
|
-
fi
|
|
123
|
-
exit 1
|
|
124
|
-
fi
|
|
125
|
-
printf '%s' "$sprint_file"
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
# Serialize read-modify-write mutations: two concurrent complete-task or
|
|
129
|
-
# start-task calls would otherwise both render from the same snapshot and the
|
|
130
|
-
# second mv would drop the first writer's update.
|
|
131
|
-
BACKLOG_LOCK_DIR=""
|
|
132
|
-
|
|
133
|
-
release_backlog_lock() {
|
|
134
|
-
if [[ -n "$BACKLOG_LOCK_DIR" ]]; then
|
|
135
|
-
rmdir "$BACKLOG_LOCK_DIR" 2>/dev/null || true
|
|
136
|
-
BACKLOG_LOCK_DIR=""
|
|
137
|
-
fi
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
acquire_backlog_lock() {
|
|
141
|
-
local attempts=0
|
|
142
|
-
local max_attempts="${FORGE_BACKLOG_LOCK_ATTEMPTS:-100}"
|
|
143
|
-
local sleep_seconds="${FORGE_BACKLOG_LOCK_SLEEP_SECONDS:-0.1}"
|
|
144
|
-
case "$max_attempts" in
|
|
145
|
-
''|*[!0-9]*) max_attempts=100 ;;
|
|
146
|
-
esac
|
|
147
|
-
[[ "$sleep_seconds" =~ ^[0-9]+([.][0-9]+)?$ ]] || sleep_seconds=0.1
|
|
148
|
-
BACKLOG_LOCK_DIR="$(dirname "$marker_file")/.backlog-lock"
|
|
149
|
-
mkdir -p "$(dirname "$BACKLOG_LOCK_DIR")"
|
|
150
|
-
until mkdir "$BACKLOG_LOCK_DIR" 2>/dev/null; do
|
|
151
|
-
# Reclaim only when the stale dir actually goes away; a non-empty lock dir
|
|
152
|
-
# must fall through to the timeout instead of hot-looping.
|
|
153
|
-
if [[ -n "$(find "$BACKLOG_LOCK_DIR" -maxdepth 0 -mmin +1 2>/dev/null)" ]] \
|
|
154
|
-
&& rmdir "$BACKLOG_LOCK_DIR" 2>/dev/null; then
|
|
155
|
-
echo "sprint-backlog: reclaiming stale backlog lock: $BACKLOG_LOCK_DIR" >&2
|
|
156
|
-
continue
|
|
157
|
-
fi
|
|
158
|
-
attempts=$((attempts + 1))
|
|
159
|
-
if [[ "$attempts" -ge "$max_attempts" ]]; then
|
|
160
|
-
echo "sprint-backlog: timed out acquiring backlog lock: $BACKLOG_LOCK_DIR" >&2
|
|
161
|
-
exit 1
|
|
162
|
-
fi
|
|
163
|
-
sleep "$sleep_seconds"
|
|
164
|
-
done
|
|
165
|
-
trap release_backlog_lock EXIT INT TERM
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
# Backlog rows live between '## Backlog' and the next '## ' heading:
|
|
169
|
-
# | 1 | [ ] | task-slug | contract | acceptance | plan |
|
|
170
|
-
# Output: index<TAB>status<TAB>task<TAB>mode<TAB>acceptance<TAB>plan
|
|
171
|
-
backlog_rows() {
|
|
172
|
-
local file="$1"
|
|
173
|
-
awk -F '|' '
|
|
174
|
-
/^## Backlog[[:space:]]*$/ { in_section = 1; next }
|
|
175
|
-
in_section && /^## / { exit }
|
|
176
|
-
!in_section { next }
|
|
177
|
-
/^\|[[:space:]]*[0-9]+[[:space:]]*\|/ {
|
|
178
|
-
for (i = 2; i <= 7; i++) {
|
|
179
|
-
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $i)
|
|
180
|
-
}
|
|
181
|
-
printf "%s\t%s\t%s\t%s\t%s\t%s\n", $2, $3, $4, $5, $6, $7
|
|
182
|
-
}
|
|
183
|
-
' "$file"
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
backlog_counts() {
|
|
187
|
-
local file="$1"
|
|
188
|
-
backlog_rows "$file" | awk -F '\t' '
|
|
189
|
-
{ total++ }
|
|
190
|
-
$2 ~ /^\[[xX]\]$/ { done++ }
|
|
191
|
-
END { printf "%d %d\n", done + 0, total + 0 }
|
|
192
|
-
'
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
next_pending_row() {
|
|
196
|
-
local file="$1"
|
|
197
|
-
backlog_rows "$file" | awk -F '\t' '$2 == "[ ]" { print; exit }'
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
render_sprint_file() {
|
|
201
|
-
local target="$1"
|
|
202
|
-
local slug="$2"
|
|
203
|
-
local title="$3"
|
|
204
|
-
local timestamp="$4"
|
|
205
|
-
|
|
206
|
-
if [[ ! -f "$template_file" ]]; then
|
|
207
|
-
mkdir -p "$(dirname "$template_file")"
|
|
208
|
-
cat > "$template_file" <<'SPRINT_TEMPLATE_EOF'
|
|
209
|
-
# Sprint: {{SPRINT_TITLE}}
|
|
210
|
-
|
|
211
|
-
> **Status**: Draft
|
|
212
|
-
> **Slug**: {{SPRINT_SLUG}}
|
|
213
|
-
> **Created**: {{TIMESTAMP}}
|
|
214
|
-
> **Updated**: {{TIMESTAMP}}
|
|
215
|
-
> **Source PRD**: (optional) `plans/prds/<prd>.prd.md`
|
|
216
|
-
> **Source Spec**: `docs/spec.md`
|
|
217
|
-
> **Goal Mode**: incremental
|
|
218
|
-
|
|
219
|
-
Program-level sprint container. The Source PRD summary and ordered backlog
|
|
220
|
-
decompose product intent into task-contract slices; each backlog row is a
|
|
221
|
-
long-task waypoint that must be expanded with `/plan` before code edits.
|
|
222
|
-
`tasks/todos.md` stays the deferred-goal ledger and never carries this backlog.
|
|
223
|
-
|
|
224
|
-
## PRD
|
|
225
|
-
|
|
226
|
-
Summarize or link the upper-layer PRD here. Keep the full PRD in `plans/prds/`.
|
|
227
|
-
|
|
228
|
-
### Problem
|
|
229
|
-
|
|
230
|
-
- ...
|
|
231
|
-
|
|
232
|
-
### Users
|
|
233
|
-
|
|
234
|
-
- ...
|
|
235
|
-
|
|
236
|
-
### Success Criteria
|
|
237
|
-
|
|
238
|
-
- ...
|
|
239
|
-
|
|
240
|
-
### Acceptance Scenarios
|
|
241
|
-
|
|
242
|
-
- ...
|
|
243
|
-
|
|
244
|
-
### Non-goals
|
|
245
|
-
|
|
246
|
-
- ...
|
|
247
|
-
|
|
248
|
-
## Architecture Notes
|
|
249
|
-
|
|
250
|
-
### Capabilities Touched
|
|
251
|
-
|
|
252
|
-
- ...
|
|
253
|
-
|
|
254
|
-
### Dependency Order
|
|
255
|
-
|
|
256
|
-
- ...
|
|
257
|
-
|
|
258
|
-
### Risks
|
|
259
|
-
|
|
260
|
-
- ...
|
|
261
|
-
|
|
262
|
-
## Backlog
|
|
263
|
-
|
|
264
|
-
Ordered execution queue; keep rows in dependency order. Mode `contract` runs
|
|
265
|
-
the full plan -> contract -> worktree flow; `inline` allows primary-tree
|
|
266
|
-
execution for small tasks. Every row needs a concrete acceptance line.
|
|
267
|
-
|
|
268
|
-
| # | Status | Task | Mode | Acceptance | Plan |
|
|
269
|
-
|---|--------|------|------|------------|------|
|
|
270
|
-
| 1 | [ ] | {{SPRINT_SLUG}}-task-1 | contract | Replace with a machine-checkable acceptance line | (pending) |
|
|
271
|
-
|
|
272
|
-
## Execution Log
|
|
273
|
-
|
|
274
|
-
Keep this section last; `scripts/sprint-backlog.sh complete-task` appends rows here.
|
|
275
|
-
|
|
276
|
-
| When | Task | Plan | Result |
|
|
277
|
-
|------|------|------|--------|
|
|
278
|
-
SPRINT_TEMPLATE_EOF
|
|
279
|
-
fi
|
|
280
|
-
|
|
281
|
-
# Literal placeholder replacement via index/substr: sed/gsub replacement
|
|
282
|
-
# strings treat |, &, \ and newlines as metacharacters, so free-text titles
|
|
283
|
-
# must never reach them. Render to a temp file so a failure cannot leave a
|
|
284
|
-
# half-written sprint file behind.
|
|
285
|
-
local tmp_file
|
|
286
|
-
tmp_file="$(mktemp)"
|
|
287
|
-
if ! SPRINT_SLUG="$slug" SPRINT_TITLE="$title" SPRINT_TS="$timestamp" awk '
|
|
288
|
-
function replace_all(line, ph, val, out, i) {
|
|
289
|
-
out = ""
|
|
290
|
-
while ((i = index(line, ph)) > 0) {
|
|
291
|
-
out = out substr(line, 1, i - 1) val
|
|
292
|
-
line = substr(line, i + length(ph))
|
|
293
|
-
}
|
|
294
|
-
return out line
|
|
295
|
-
}
|
|
296
|
-
{
|
|
297
|
-
line = $0
|
|
298
|
-
line = replace_all(line, "{{SPRINT_SLUG}}", ENVIRON["SPRINT_SLUG"])
|
|
299
|
-
line = replace_all(line, "{{SPRINT_TITLE}}", ENVIRON["SPRINT_TITLE"])
|
|
300
|
-
line = replace_all(line, "{{TIMESTAMP}}", ENVIRON["SPRINT_TS"])
|
|
301
|
-
print line
|
|
302
|
-
}
|
|
303
|
-
' "$template_file" > "$tmp_file"; then
|
|
304
|
-
rm -f "$tmp_file"
|
|
305
|
-
echo "sprint-backlog: failed to render sprint template" >&2
|
|
306
|
-
exit 1
|
|
307
|
-
fi
|
|
308
|
-
mv "$tmp_file" "$target"
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
cmd_init() {
|
|
312
|
-
local slug=""
|
|
313
|
-
local title=""
|
|
314
|
-
|
|
315
|
-
while [[ $# -gt 0 ]]; do
|
|
316
|
-
case "$1" in
|
|
317
|
-
--slug)
|
|
318
|
-
[[ -n "${2:-}" ]] || { echo "sprint-backlog: --slug requires a value" >&2; exit 2; }
|
|
319
|
-
slug="$2"
|
|
320
|
-
shift 2
|
|
321
|
-
;;
|
|
322
|
-
--title)
|
|
323
|
-
[[ -n "${2:-}" ]] || { echo "sprint-backlog: --title requires a value" >&2; exit 2; }
|
|
324
|
-
title="$2"
|
|
325
|
-
shift 2
|
|
326
|
-
;;
|
|
327
|
-
*)
|
|
328
|
-
echo "sprint-backlog: unknown init argument: $1" >&2
|
|
329
|
-
usage >&2
|
|
330
|
-
exit 2
|
|
331
|
-
;;
|
|
332
|
-
esac
|
|
333
|
-
done
|
|
334
|
-
|
|
335
|
-
[[ -n "$slug" ]] || { echo "sprint-backlog: init requires --slug" >&2; usage >&2; exit 2; }
|
|
336
|
-
slug="$(normalize_slug "$slug")"
|
|
337
|
-
[[ -n "$slug" ]] || { echo "sprint-backlog: slug is empty after normalization" >&2; exit 2; }
|
|
338
|
-
[[ -n "$title" ]] || title="$slug"
|
|
339
|
-
# Headings are single-line; fold control characters out of free-text titles.
|
|
340
|
-
title="$(printf '%s' "$title" | tr '\n\r\t' ' ')"
|
|
341
|
-
|
|
342
|
-
local existing existing_status
|
|
343
|
-
if existing="$(active_sprint_file)"; then
|
|
344
|
-
existing_status="$(extract_status "$existing")"
|
|
345
|
-
case "$existing_status" in
|
|
346
|
-
Done|Archived)
|
|
347
|
-
;;
|
|
348
|
-
*)
|
|
349
|
-
echo "sprint-backlog: active sprint already exists with status ${existing_status:-unknown}: $existing" >&2
|
|
350
|
-
echo "sprint-backlog: complete or archive it before starting a new sprint" >&2
|
|
351
|
-
exit 1
|
|
352
|
-
;;
|
|
353
|
-
esac
|
|
354
|
-
fi
|
|
355
|
-
|
|
356
|
-
mkdir -p "$sprints_dir" "$(dirname "$marker_file")"
|
|
357
|
-
|
|
358
|
-
local timestamp file_stamp sprint_file counter
|
|
359
|
-
timestamp="$(date '+%Y-%m-%d %H:%M')"
|
|
360
|
-
file_stamp="$(date +%Y%m%d-%H%M)"
|
|
361
|
-
sprint_file="${sprints_dir}/${file_stamp}-${slug}.sprint.md"
|
|
362
|
-
counter=2
|
|
363
|
-
while [[ -e "$sprint_file" ]]; do
|
|
364
|
-
sprint_file="${sprints_dir}/${file_stamp}-${slug}-v${counter}.sprint.md"
|
|
365
|
-
counter=$((counter + 1))
|
|
366
|
-
done
|
|
367
|
-
|
|
368
|
-
render_sprint_file "$sprint_file" "$slug" "$title" "$timestamp"
|
|
369
|
-
printf '%s' "$sprint_file" > "$marker_file"
|
|
370
|
-
|
|
371
|
-
echo "Created draft sprint: $sprint_file"
|
|
372
|
-
echo "Active sprint marker: $marker_file"
|
|
373
|
-
echo "Fill PRD, Architecture Notes, and Backlog, then set Status to Approved before execution."
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
cmd_status() {
|
|
377
|
-
local sprint_file status done total next_task
|
|
378
|
-
if ! sprint_file="$(active_sprint_file)"; then
|
|
379
|
-
echo "sprint: (none)"
|
|
380
|
-
return 0
|
|
381
|
-
fi
|
|
382
|
-
|
|
383
|
-
status="$(extract_status "$sprint_file")"
|
|
384
|
-
read -r done total <<<"$(backlog_counts "$sprint_file")"
|
|
385
|
-
next_task="$(next_pending_row "$sprint_file" | awk -F '\t' '{ print $3 }')"
|
|
386
|
-
|
|
387
|
-
echo "sprint: $sprint_file"
|
|
388
|
-
echo "status: ${status:-unknown}"
|
|
389
|
-
echo "tasks_done: $done"
|
|
390
|
-
echo "tasks_total: $total"
|
|
391
|
-
echo "next_task: ${next_task:-(none)}"
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
cmd_next() {
|
|
395
|
-
local sprint_file row
|
|
396
|
-
sprint_file="$(require_active_sprint)"
|
|
397
|
-
row="$(next_pending_row "$sprint_file")"
|
|
398
|
-
if [[ -z "$row" ]]; then
|
|
399
|
-
echo "next_task: (none)"
|
|
400
|
-
exit 3
|
|
401
|
-
fi
|
|
402
|
-
|
|
403
|
-
printf '%s\n' "$row" | awk -F '\t' '{
|
|
404
|
-
printf "index: %s\ntask: %s\nmode: %s\nacceptance: %s\nplan: %s\n", $1, $3, $4, $5, $6
|
|
405
|
-
}'
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
cmd_complete_task() {
|
|
409
|
-
local task_ref=""
|
|
410
|
-
local plan_file=""
|
|
411
|
-
|
|
412
|
-
while [[ $# -gt 0 ]]; do
|
|
413
|
-
case "$1" in
|
|
414
|
-
--task)
|
|
415
|
-
[[ -n "${2:-}" ]] || { echo "sprint-backlog: --task requires a value" >&2; exit 2; }
|
|
416
|
-
task_ref="$2"
|
|
417
|
-
shift 2
|
|
418
|
-
;;
|
|
419
|
-
--plan)
|
|
420
|
-
[[ -n "${2:-}" ]] || { echo "sprint-backlog: --plan requires a value" >&2; exit 2; }
|
|
421
|
-
plan_file="$2"
|
|
422
|
-
shift 2
|
|
423
|
-
;;
|
|
424
|
-
--sprint)
|
|
425
|
-
[[ -n "${2:-}" ]] || { echo "sprint-backlog: --sprint requires a value" >&2; exit 2; }
|
|
426
|
-
sprint_override="$2"
|
|
427
|
-
shift 2
|
|
428
|
-
;;
|
|
429
|
-
*)
|
|
430
|
-
echo "sprint-backlog: unknown complete-task argument: $1" >&2
|
|
431
|
-
usage >&2
|
|
432
|
-
exit 2
|
|
433
|
-
;;
|
|
434
|
-
esac
|
|
435
|
-
done
|
|
436
|
-
|
|
437
|
-
[[ -n "$task_ref" ]] || { echo "sprint-backlog: complete-task requires --task" >&2; usage >&2; exit 2; }
|
|
438
|
-
|
|
439
|
-
local sprint_file target_row target_index target_status target_task target_plan plan_cell match_count
|
|
440
|
-
sprint_file="$(require_active_sprint)"
|
|
441
|
-
acquire_backlog_lock
|
|
442
|
-
|
|
443
|
-
# task_ref travels via ENVIRON (awk -v reprocesses backslash escapes).
|
|
444
|
-
match_count="$(backlog_rows "$sprint_file" | TASK_REF="$task_ref" awk -F '\t' '$1 == ENVIRON["TASK_REF"] || $3 == ENVIRON["TASK_REF"] { count++ } END { print count + 0 }')"
|
|
445
|
-
if [[ "$match_count" -eq 0 ]]; then
|
|
446
|
-
echo "sprint-backlog: no backlog row matches task '$task_ref' in $sprint_file" >&2
|
|
447
|
-
exit 1
|
|
448
|
-
fi
|
|
449
|
-
if [[ "$match_count" -gt 1 ]]; then
|
|
450
|
-
echo "sprint-backlog: task reference '$task_ref' is ambiguous (${match_count} backlog rows match); fix duplicate indices or task names first" >&2
|
|
451
|
-
exit 1
|
|
452
|
-
fi
|
|
453
|
-
|
|
454
|
-
target_row="$(backlog_rows "$sprint_file" | TASK_REF="$task_ref" awk -F '\t' '$1 == ENVIRON["TASK_REF"] || $3 == ENVIRON["TASK_REF"] { print; exit }')"
|
|
455
|
-
|
|
456
|
-
target_index="$(printf '%s' "$target_row" | cut -f1)"
|
|
457
|
-
target_status="$(printf '%s' "$target_row" | cut -f2)"
|
|
458
|
-
target_task="$(printf '%s' "$target_row" | cut -f3)"
|
|
459
|
-
target_plan="$(printf '%s' "$target_row" | cut -f6)"
|
|
460
|
-
|
|
461
|
-
if [[ "$target_status" != "[ ]" ]]; then
|
|
462
|
-
echo "sprint-backlog: backlog task '$target_task' (row $target_index) is already complete" >&2
|
|
463
|
-
exit 1
|
|
464
|
-
fi
|
|
465
|
-
|
|
466
|
-
plan_cell="$target_plan"
|
|
467
|
-
if [[ -n "$plan_file" ]]; then
|
|
468
|
-
plan_cell="\`${plan_file}\`"
|
|
469
|
-
fi
|
|
470
|
-
|
|
471
|
-
local timestamp tmp_file
|
|
472
|
-
timestamp="$(date '+%Y-%m-%d %H:%M')"
|
|
473
|
-
tmp_file="$(mktemp)"
|
|
474
|
-
# plan_cell and target_task travel via ENVIRON: awk -v reprocesses C
|
|
475
|
-
# escapes, so a backslash in either would split or mismatch the table row.
|
|
476
|
-
# The rewrite matches index AND task so a duplicate index can never flip a
|
|
477
|
-
# different row than the one resolved above.
|
|
478
|
-
if ! PLAN_CELL="$plan_cell" TARGET_TASK="$target_task" awk -F '|' -v target="$target_index" -v ts="$timestamp" '
|
|
479
|
-
BEGIN { in_section = 0; rewritten = 0 }
|
|
480
|
-
/^> \*\*Updated\*\*:/ {
|
|
481
|
-
print "> **Updated**: " ts
|
|
482
|
-
next
|
|
483
|
-
}
|
|
484
|
-
/^## Backlog[[:space:]]*$/ { in_section = 1; print; next }
|
|
485
|
-
in_section && /^## / { in_section = 0 }
|
|
486
|
-
{
|
|
487
|
-
if (in_section && !rewritten && $0 ~ /^\|[[:space:]]*[0-9]+[[:space:]]*\|/) {
|
|
488
|
-
idx = $2; task = $4; mode = $5; acceptance = $6
|
|
489
|
-
gsub(/^[[:space:]]+|[[:space:]]+$/, "", idx)
|
|
490
|
-
gsub(/^[[:space:]]+|[[:space:]]+$/, "", task)
|
|
491
|
-
gsub(/^[[:space:]]+|[[:space:]]+$/, "", mode)
|
|
492
|
-
gsub(/^[[:space:]]+|[[:space:]]+$/, "", acceptance)
|
|
493
|
-
if (idx == target && task == ENVIRON["TARGET_TASK"]) {
|
|
494
|
-
printf "| %s | [x] | %s | %s | %s | %s |\n", idx, task, mode, acceptance, ENVIRON["PLAN_CELL"]
|
|
495
|
-
rewritten = 1
|
|
496
|
-
next
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
print
|
|
500
|
-
}
|
|
501
|
-
END { exit rewritten ? 0 : 1 }
|
|
502
|
-
' "$sprint_file" > "$tmp_file"; then
|
|
503
|
-
rm -f "$tmp_file"
|
|
504
|
-
echo "sprint-backlog: failed to rewrite backlog row (row not rewritten; check the table for malformed cells)" >&2
|
|
505
|
-
exit 1
|
|
506
|
-
fi
|
|
507
|
-
mv "$tmp_file" "$sprint_file"
|
|
508
|
-
|
|
509
|
-
if ! grep -Eq '^## Execution Log[[:space:]]*$' "$sprint_file"; then
|
|
510
|
-
{
|
|
511
|
-
echo
|
|
512
|
-
echo "## Execution Log"
|
|
513
|
-
echo
|
|
514
|
-
echo "| When | Task | Plan | Result |"
|
|
515
|
-
echo "|------|------|------|--------|"
|
|
516
|
-
} >> "$sprint_file"
|
|
517
|
-
fi
|
|
518
|
-
printf '| %s | %s | %s | done |\n' "$timestamp" "$target_task" "${plan_cell:-(none)}" >> "$sprint_file"
|
|
519
|
-
|
|
520
|
-
clear_in_flight "$target_task"
|
|
521
|
-
|
|
522
|
-
local done total
|
|
523
|
-
read -r done total <<<"$(backlog_counts "$sprint_file")"
|
|
524
|
-
echo "Completed backlog task '$target_task' (row $target_index) in $sprint_file"
|
|
525
|
-
echo "Backlog progress: ${done}/${total}"
|
|
526
|
-
if [[ "$done" -eq "$total" ]]; then
|
|
527
|
-
echo "All backlog tasks complete. Set the sprint Status to Done after review."
|
|
528
|
-
fi
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
# In-flight markers live under the gitignored runtime dir so duplicate
|
|
532
|
-
# start-task calls are refused without writing tracked files in the primary
|
|
533
|
-
# tree (contract rows must stay merge-pure until finish back-fills them).
|
|
534
|
-
in_flight_dir() {
|
|
535
|
-
printf '%s/in-flight' "$(dirname "$marker_file")"
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
in_flight_marker_for() {
|
|
539
|
-
printf '%s/%s' "$(in_flight_dir)" "$(normalize_slug "$1")"
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
task_in_flight() {
|
|
543
|
-
[[ -f "$(in_flight_marker_for "$1")" ]]
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
record_in_flight() {
|
|
547
|
-
mkdir -p "$(in_flight_dir)"
|
|
548
|
-
printf '%s' "${2:-capturing}" > "$(in_flight_marker_for "$1")"
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
clear_in_flight() {
|
|
552
|
-
rm -f "$(in_flight_marker_for "$1")" 2>/dev/null || true
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
# Drop markers whose backlog row is gone or already complete (finish back-fill
|
|
556
|
-
# runs in the worktree and cannot clean the primary tree's markers).
|
|
557
|
-
prune_in_flight_markers() {
|
|
558
|
-
local file="$1"
|
|
559
|
-
local marker task_slug keep _idx status task _rest
|
|
560
|
-
[[ -d "$(in_flight_dir)" ]] || return 0
|
|
561
|
-
for marker in "$(in_flight_dir)"/*; do
|
|
562
|
-
[[ -e "$marker" ]] || continue
|
|
563
|
-
task_slug="$(basename "$marker")"
|
|
564
|
-
keep=0
|
|
565
|
-
while IFS=$'\t' read -r _idx status task _rest; do
|
|
566
|
-
if [[ "$(normalize_slug "$task")" == "$task_slug" && "$status" == "[ ]" ]]; then
|
|
567
|
-
keep=1
|
|
568
|
-
break
|
|
569
|
-
fi
|
|
570
|
-
done < <(backlog_rows "$file")
|
|
571
|
-
[[ "$keep" -eq 1 ]] || rm -f "$marker"
|
|
572
|
-
done
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
# Fill only the Plan cell of one backlog row (status untouched); used by
|
|
576
|
-
# start-task so the backlog shows in-flight work.
|
|
577
|
-
set_row_plan_cell() {
|
|
578
|
-
local sprint_file="$1"
|
|
579
|
-
local target_index="$2"
|
|
580
|
-
local target_task="$3"
|
|
581
|
-
local plan_cell="$4"
|
|
582
|
-
local timestamp tmp_file
|
|
583
|
-
timestamp="$(date '+%Y-%m-%d %H:%M')"
|
|
584
|
-
tmp_file="$(mktemp)"
|
|
585
|
-
if ! PLAN_CELL="$plan_cell" TARGET_TASK="$target_task" awk -F '|' -v target="$target_index" -v ts="$timestamp" '
|
|
586
|
-
BEGIN { in_section = 0; rewritten = 0 }
|
|
587
|
-
/^> \*\*Updated\*\*:/ {
|
|
588
|
-
print "> **Updated**: " ts
|
|
589
|
-
next
|
|
590
|
-
}
|
|
591
|
-
/^## Backlog[[:space:]]*$/ { in_section = 1; print; next }
|
|
592
|
-
in_section && /^## / { in_section = 0 }
|
|
593
|
-
{
|
|
594
|
-
if (in_section && !rewritten && $0 ~ /^\|[[:space:]]*[0-9]+[[:space:]]*\|/) {
|
|
595
|
-
idx = $2; status = $3; task = $4; mode = $5; acceptance = $6
|
|
596
|
-
gsub(/^[[:space:]]+|[[:space:]]+$/, "", idx)
|
|
597
|
-
gsub(/^[[:space:]]+|[[:space:]]+$/, "", status)
|
|
598
|
-
gsub(/^[[:space:]]+|[[:space:]]+$/, "", task)
|
|
599
|
-
gsub(/^[[:space:]]+|[[:space:]]+$/, "", mode)
|
|
600
|
-
gsub(/^[[:space:]]+|[[:space:]]+$/, "", acceptance)
|
|
601
|
-
if (idx == target && task == ENVIRON["TARGET_TASK"]) {
|
|
602
|
-
printf "| %s | %s | %s | %s | %s | %s |\n", idx, status, task, mode, acceptance, ENVIRON["PLAN_CELL"]
|
|
603
|
-
rewritten = 1
|
|
604
|
-
next
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
print
|
|
608
|
-
}
|
|
609
|
-
END { exit rewritten ? 0 : 1 }
|
|
610
|
-
' "$sprint_file" > "$tmp_file"; then
|
|
611
|
-
rm -f "$tmp_file"
|
|
612
|
-
echo "sprint-backlog: failed to update backlog plan cell (row not rewritten; check the table for malformed cells)" >&2
|
|
613
|
-
exit 1
|
|
614
|
-
fi
|
|
615
|
-
mv "$tmp_file" "$sprint_file"
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
cmd_start_task() {
|
|
619
|
-
local task_ref=""
|
|
620
|
-
local execute=0
|
|
621
|
-
local force=0
|
|
622
|
-
|
|
623
|
-
while [[ $# -gt 0 ]]; do
|
|
624
|
-
case "$1" in
|
|
625
|
-
--task)
|
|
626
|
-
[[ -n "${2:-}" ]] || { echo "sprint-backlog: --task requires a value" >&2; exit 2; }
|
|
627
|
-
task_ref="$2"
|
|
628
|
-
shift 2
|
|
629
|
-
;;
|
|
630
|
-
--execute)
|
|
631
|
-
execute=1
|
|
632
|
-
shift
|
|
633
|
-
;;
|
|
634
|
-
--force)
|
|
635
|
-
force=1
|
|
636
|
-
shift
|
|
637
|
-
;;
|
|
638
|
-
--sprint)
|
|
639
|
-
[[ -n "${2:-}" ]] || { echo "sprint-backlog: --sprint requires a value" >&2; exit 2; }
|
|
640
|
-
sprint_override="$2"
|
|
641
|
-
shift 2
|
|
642
|
-
;;
|
|
643
|
-
*)
|
|
644
|
-
echo "sprint-backlog: unknown start-task argument: $1" >&2
|
|
645
|
-
usage >&2
|
|
646
|
-
exit 2
|
|
647
|
-
;;
|
|
648
|
-
esac
|
|
649
|
-
done
|
|
650
|
-
|
|
651
|
-
local sprint_file sprint_status
|
|
652
|
-
sprint_file="$(require_active_sprint)"
|
|
653
|
-
sprint_status="$(extract_status "$sprint_file")"
|
|
654
|
-
case "$sprint_status" in
|
|
655
|
-
Approved|Executing)
|
|
656
|
-
;;
|
|
657
|
-
*)
|
|
658
|
-
echo "sprint-backlog: sprint status is ${sprint_status:-unknown}; approve the sprint before starting tasks" >&2
|
|
659
|
-
exit 1
|
|
660
|
-
;;
|
|
661
|
-
esac
|
|
662
|
-
|
|
663
|
-
[[ -f "scripts/capture-plan.sh" ]] || { echo "sprint-backlog: scripts/capture-plan.sh not found" >&2; exit 1; }
|
|
664
|
-
|
|
665
|
-
acquire_backlog_lock
|
|
666
|
-
prune_in_flight_markers "$sprint_file"
|
|
667
|
-
|
|
668
|
-
local target_row match_count candidate_task
|
|
669
|
-
if [[ -n "$task_ref" ]]; then
|
|
670
|
-
match_count="$(backlog_rows "$sprint_file" | TASK_REF="$task_ref" awk -F '\t' '$1 == ENVIRON["TASK_REF"] || $3 == ENVIRON["TASK_REF"] { count++ } END { print count + 0 }')"
|
|
671
|
-
if [[ "$match_count" -eq 0 ]]; then
|
|
672
|
-
echo "sprint-backlog: no backlog row matches task '$task_ref' in $sprint_file" >&2
|
|
673
|
-
exit 1
|
|
674
|
-
fi
|
|
675
|
-
if [[ "$match_count" -gt 1 ]]; then
|
|
676
|
-
echo "sprint-backlog: task reference '$task_ref' is ambiguous (${match_count} backlog rows match); fix duplicate indices or task names first" >&2
|
|
677
|
-
exit 1
|
|
678
|
-
fi
|
|
679
|
-
target_row="$(backlog_rows "$sprint_file" | TASK_REF="$task_ref" awk -F '\t' '$1 == ENVIRON["TASK_REF"] || $3 == ENVIRON["TASK_REF"] { print; exit }')"
|
|
680
|
-
else
|
|
681
|
-
# Auto-select skips rows that are already in flight so repeated runs walk
|
|
682
|
-
# the queue instead of duplicating an active task.
|
|
683
|
-
target_row=""
|
|
684
|
-
while IFS= read -r candidate_row; do
|
|
685
|
-
[[ -n "$candidate_row" ]] || continue
|
|
686
|
-
candidate_task="$(printf '%s' "$candidate_row" | cut -f3)"
|
|
687
|
-
if ! task_in_flight "$candidate_task"; then
|
|
688
|
-
target_row="$candidate_row"
|
|
689
|
-
break
|
|
690
|
-
fi
|
|
691
|
-
done < <(backlog_rows "$sprint_file" | awk -F '\t' '$2 == "[ ]"')
|
|
692
|
-
if [[ -z "$target_row" ]]; then
|
|
693
|
-
if [[ -n "$(next_pending_row "$sprint_file")" ]]; then
|
|
694
|
-
echo "sprint-backlog: every pending backlog task is already in flight; finish one or rerun with --task <ref> --force" >&2
|
|
695
|
-
fi
|
|
696
|
-
echo "next_task: (none)"
|
|
697
|
-
exit 3
|
|
698
|
-
fi
|
|
699
|
-
fi
|
|
700
|
-
|
|
701
|
-
local target_index target_status target_task target_mode target_acceptance
|
|
702
|
-
target_index="$(printf '%s' "$target_row" | cut -f1)"
|
|
703
|
-
target_status="$(printf '%s' "$target_row" | cut -f2)"
|
|
704
|
-
target_task="$(printf '%s' "$target_row" | cut -f3)"
|
|
705
|
-
target_mode="$(printf '%s' "$target_row" | cut -f4)"
|
|
706
|
-
target_acceptance="$(printf '%s' "$target_row" | cut -f5)"
|
|
707
|
-
|
|
708
|
-
if [[ "$target_status" != "[ ]" ]]; then
|
|
709
|
-
echo "sprint-backlog: backlog task '$target_task' (row $target_index) is already complete" >&2
|
|
710
|
-
exit 1
|
|
711
|
-
fi
|
|
712
|
-
|
|
713
|
-
if task_in_flight "$target_task" && [[ "$force" -ne 1 ]]; then
|
|
714
|
-
echo "sprint-backlog: backlog task '$target_task' is already in flight (recorded: $(cat "$(in_flight_marker_for "$target_task")" 2>/dev/null || printf 'capturing')); rerun with --force to restart it" >&2
|
|
715
|
-
exit 1
|
|
716
|
-
fi
|
|
717
|
-
record_in_flight "$target_task" "capturing"
|
|
718
|
-
|
|
719
|
-
# Do not hold the backlog lock across capture-plan: with --execute it can
|
|
720
|
-
# run git worktree setup for minutes and the stale-reclaim would hand the
|
|
721
|
-
# lock to a second writer.
|
|
722
|
-
release_backlog_lock
|
|
723
|
-
|
|
724
|
-
local body_file capture_output plan_path
|
|
725
|
-
body_file="$(mktemp)"
|
|
726
|
-
cat > "$body_file" <<BODY_EOF
|
|
727
|
-
# Sprint Task: ${target_task}
|
|
728
|
-
|
|
729
|
-
## Context
|
|
730
|
-
|
|
731
|
-
- Sprint: \`${sprint_file}\`
|
|
732
|
-
- Backlog row: ${target_index}
|
|
733
|
-
- Mode: ${target_mode}
|
|
734
|
-
- Read the sprint Source PRD and Architecture Notes before implementation.
|
|
735
|
-
- The sprint row is a long-task waypoint, not a detailed implementation plan.
|
|
736
|
-
|
|
737
|
-
## Goal
|
|
738
|
-
|
|
739
|
-
Deliver backlog task \`${target_task}\` so that the acceptance line holds: ${target_acceptance}
|
|
740
|
-
|
|
741
|
-
## Planning Expansion
|
|
742
|
-
|
|
743
|
-
Before editing code, use \`/plan\` to expand this sprint row into a decision-complete implementation plan. The \`/plan\` pass should read the sprint file, preserve the acceptance line, name concrete files or commands, and produce the detailed \`plans/plan-*.md\` body that drives contract execution.
|
|
744
|
-
|
|
745
|
-
## Task Breakdown
|
|
746
|
-
|
|
747
|
-
- [ ] Run \`/plan\` for backlog task \`${target_task}\` using sprint \`${sprint_file}\` and acceptance: ${target_acceptance}
|
|
748
|
-
- [ ] Capture the approved \`/plan\` output with \`scripts/capture-plan.sh --source forge-plan --source-ref sprint:${sprint_file}#${target_task}\`
|
|
749
|
-
- [ ] Verify acceptance: ${target_acceptance}
|
|
750
|
-
BODY_EOF
|
|
751
|
-
|
|
752
|
-
local -a capture_args
|
|
753
|
-
capture_args=(
|
|
754
|
-
--slug "$target_task"
|
|
755
|
-
--title "Sprint task: ${target_task}"
|
|
756
|
-
--status Approved
|
|
757
|
-
--source forge-sprint
|
|
758
|
-
--orchestration-kind sprint-task
|
|
759
|
-
--source-ref "sprint:${sprint_file}#${target_task}"
|
|
760
|
-
--body-file "$body_file"
|
|
761
|
-
)
|
|
762
|
-
if [[ "$execute" -eq 1 ]]; then
|
|
763
|
-
capture_args+=(--execute)
|
|
764
|
-
fi
|
|
765
|
-
|
|
766
|
-
# Inline-mode rows execute in the primary tree: suppress the automatic
|
|
767
|
-
# contract worktree for them.
|
|
768
|
-
if [[ "$target_mode" == "inline" ]]; then
|
|
769
|
-
capture_output="$(FORGE_DISABLE_CONTRACT_WORKTREE=1 bash scripts/capture-plan.sh "${capture_args[@]}" 2>&1)" || {
|
|
770
|
-
printf '%s\n' "$capture_output" >&2
|
|
771
|
-
rm -f "$body_file"
|
|
772
|
-
clear_in_flight "$target_task"
|
|
773
|
-
echo "sprint-backlog: capture-plan failed for task '$target_task'" >&2
|
|
774
|
-
exit 1
|
|
775
|
-
}
|
|
776
|
-
else
|
|
777
|
-
capture_output="$(bash scripts/capture-plan.sh "${capture_args[@]}" 2>&1)" || {
|
|
778
|
-
printf '%s\n' "$capture_output" >&2
|
|
779
|
-
rm -f "$body_file"
|
|
780
|
-
clear_in_flight "$target_task"
|
|
781
|
-
echo "sprint-backlog: capture-plan failed for task '$target_task'" >&2
|
|
782
|
-
exit 1
|
|
783
|
-
}
|
|
784
|
-
fi
|
|
785
|
-
rm -f "$body_file"
|
|
786
|
-
printf '%s\n' "$capture_output"
|
|
787
|
-
|
|
788
|
-
plan_path="$(printf '%s\n' "$capture_output" | sed -nE 's/^Captured plan: (.+)$/\1/p' | head -1)"
|
|
789
|
-
if [[ -z "$plan_path" ]]; then
|
|
790
|
-
echo "sprint-backlog: warning: could not resolve captured plan path; backlog Plan cell unchanged" >&2
|
|
791
|
-
return 0
|
|
792
|
-
fi
|
|
793
|
-
record_in_flight "$target_task" "$plan_path"
|
|
794
|
-
|
|
795
|
-
if [[ "$target_mode" == "inline" ]]; then
|
|
796
|
-
acquire_backlog_lock
|
|
797
|
-
set_row_plan_cell "$sprint_file" "$target_index" "$target_task" "\`${plan_path}\`"
|
|
798
|
-
echo "Backlog row ${target_index} ('${target_task}') now references ${plan_path}"
|
|
799
|
-
else
|
|
800
|
-
# Contract mode: the plan moves into a worktree branched from HEAD, so
|
|
801
|
-
# writing the Plan cell here would dirty the primary tree and block the
|
|
802
|
-
# eventual --ff-only merge back. The finish back-fill writes the row
|
|
803
|
-
# (status + plan) atomically with the merged slice instead.
|
|
804
|
-
echo "Backlog row ${target_index} ('${target_task}') stays (pending); contract-worktree finish back-fills the Plan cell after merge."
|
|
805
|
-
fi
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
[[ $# -gt 0 ]] || { usage >&2; exit 2; }
|
|
809
|
-
|
|
810
|
-
command="$1"
|
|
811
|
-
shift
|
|
812
|
-
|
|
813
|
-
case "$command" in
|
|
814
|
-
init)
|
|
815
|
-
cmd_init "$@"
|
|
816
|
-
;;
|
|
817
|
-
status)
|
|
818
|
-
cmd_status "$@"
|
|
819
|
-
;;
|
|
820
|
-
next)
|
|
821
|
-
cmd_next "$@"
|
|
822
|
-
;;
|
|
823
|
-
start-task)
|
|
824
|
-
cmd_start_task "$@"
|
|
825
|
-
;;
|
|
826
|
-
complete-task)
|
|
827
|
-
cmd_complete_task "$@"
|
|
828
|
-
;;
|
|
829
|
-
--help|-h|help)
|
|
830
|
-
usage
|
|
831
|
-
;;
|
|
832
|
-
*)
|
|
833
|
-
echo "sprint-backlog: unknown command: $command" >&2
|
|
834
|
-
usage >&2
|
|
835
|
-
exit 2
|
|
836
|
-
;;
|
|
837
|
-
esac
|
|
4
|
+
exec "$SCRIPT_DIR/lib/run-forge-helper.sh" "sprint-backlog.sh" "$@"
|