@svayam-opensource/prj 0.5.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/LICENSE +21 -0
- package/README.md +123 -0
- package/agent/harness-manifest.yaml +225 -0
- package/agent/session-protocol.md +116 -0
- package/bin/prj +21 -0
- package/package.json +41 -0
- package/prj +2381 -0
- package/scripts/add-repo.sh +126 -0
- package/scripts/cancel.sh +157 -0
- package/scripts/close-knowledge.sh +250 -0
- package/scripts/close-project.sh +233 -0
- package/scripts/create-task.sh +226 -0
- package/scripts/install-deps.sh +292 -0
- package/scripts/join.sh +89 -0
- package/scripts/lib.sh +841 -0
- package/scripts/merge-task.sh +163 -0
- package/scripts/onboard-repo.sh +275 -0
- package/scripts/pause.sh +80 -0
- package/scripts/project-access.sh +34 -0
- package/scripts/propose-knowledge.sh +168 -0
- package/scripts/release-to-public.sh +185 -0
- package/scripts/render-harness.sh +151 -0
- package/scripts/resume.sh +103 -0
- package/scripts/seed.sh +774 -0
- package/scripts/sync-from-publish.sh +193 -0
- package/scripts/sync.sh +90 -0
- package/scripts/test-merge.sh +100 -0
- package/scripts/validate/check_knowledge.py +158 -0
- package/scripts/validate/check_privacy.py +211 -0
- package/scripts/validate/check_protocol.py +117 -0
- package/scripts/validate/check_secrets.py +175 -0
- package/scripts/validate/run.py +391 -0
- package/setup.sh +529 -0
package/scripts/seed.sh
ADDED
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Script: seed
|
|
3
|
+
# Purpose: Initialize a per-project workspace under $AGENT_WORK_ROOT/<PROJECT_ID>/.
|
|
4
|
+
# Clones the ORG GOVERNANCE repo and each impacted code repo into that
|
|
5
|
+
# workspace on the project branch. The HOME workspace stays on the
|
|
6
|
+
# default branch throughout — never switches.
|
|
7
|
+
# Usage: bash seed.sh [--non-interactive] <github_project_url> <assignee>
|
|
8
|
+
# Compliance: C01 for all validation gates (POL-056 to POL-075)
|
|
9
|
+
#
|
|
10
|
+
# Flags:
|
|
11
|
+
# --non-interactive Skip all interactive prompts. Uses $DEFAULT_CODE_BRANCH
|
|
12
|
+
# as the base branch for every linked repo, and aborts
|
|
13
|
+
# (instead of prompting) if leftover state is detected.
|
|
14
|
+
#
|
|
15
|
+
# Lifecycle invariants (Direction A):
|
|
16
|
+
# - Home workspace stays on $DEFAULT_BRANCH. No `git checkout` of any
|
|
17
|
+
# project branch happens here.
|
|
18
|
+
# - All project branch work lives in $AGENT_WORK_ROOT/<PROJECT_ID>/<workspace_repo>/
|
|
19
|
+
# (a separate clone of ORG GOVERNANCE).
|
|
20
|
+
# - Home's default branch gets a minimal projects/<PROJECT_ID>/.gitkeep stub
|
|
21
|
+
# so the registry entry has a folder for the validator. Full scaffolding
|
|
22
|
+
# (project.yaml, agent.md, knowledge/) lives on the project branch in the
|
|
23
|
+
# per-project workspace, and arrives in default via the close-project merge.
|
|
24
|
+
#
|
|
25
|
+
# Resilience:
|
|
26
|
+
# - Pre-conditions: home is on default, clean, no leftover state.
|
|
27
|
+
# - Tracked side effects roll back on error: created paths removed, pushed
|
|
28
|
+
# remote branches deleted, local registry commit reset.
|
|
29
|
+
|
|
30
|
+
set -euo pipefail
|
|
31
|
+
source "$(dirname "$0")/lib.sh"
|
|
32
|
+
load_config
|
|
33
|
+
|
|
34
|
+
# ── Inputs ────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
NON_INTERACTIVE=false
|
|
37
|
+
ARGS=()
|
|
38
|
+
for arg in "$@"; do
|
|
39
|
+
case "$arg" in
|
|
40
|
+
--non-interactive) NON_INTERACTIVE=true ;;
|
|
41
|
+
*) ARGS+=("$arg") ;;
|
|
42
|
+
esac
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
GITHUB_PROJECT_URL="${ARGS[0]:-}"
|
|
46
|
+
ASSIGNEE="${ARGS[1]:-}"
|
|
47
|
+
|
|
48
|
+
[[ -n "$GITHUB_PROJECT_URL" ]] || hard_stop "Usage: $0 [--non-interactive] <github_project_url> <assignee>"
|
|
49
|
+
[[ -n "$ASSIGNEE" ]] || hard_stop "Usage: $0 [--non-interactive] <github_project_url> <assignee>"
|
|
50
|
+
|
|
51
|
+
[[ -n "$ORG_REPO_URL" ]] \
|
|
52
|
+
|| hard_stop "org_repo_url not set in org-config.yaml. Run ./setup.sh first."
|
|
53
|
+
|
|
54
|
+
echo "=== seed: $GITHUB_PROJECT_URL"
|
|
55
|
+
echo " Assignee: $ASSIGNEE"
|
|
56
|
+
echo " Agent work root: $AGENT_WORK_ROOT"
|
|
57
|
+
echo ""
|
|
58
|
+
|
|
59
|
+
# ── Rollback machinery ────────────────────────────────────────────────────────
|
|
60
|
+
# Track artifacts created during this run so they can be reversed on failure.
|
|
61
|
+
# Each list entry uses '<path>|<value>' to avoid bash3 associative arrays.
|
|
62
|
+
|
|
63
|
+
CREATED_LOCAL_BRANCHES=()
|
|
64
|
+
PUSHED_REMOTE_BRANCHES=()
|
|
65
|
+
CREATED_WORKTREES=()
|
|
66
|
+
CREATED_PATHS=()
|
|
67
|
+
HOME_PRE_SEED_SHA=""
|
|
68
|
+
SEED_OK=0
|
|
69
|
+
|
|
70
|
+
run_rollback() {
|
|
71
|
+
local exit_code=$?
|
|
72
|
+
if [[ "$SEED_OK" == "1" ]]; then return 0; fi
|
|
73
|
+
if [[ ${#CREATED_LOCAL_BRANCHES[@]} -eq 0 \
|
|
74
|
+
&& ${#PUSHED_REMOTE_BRANCHES[@]} -eq 0 \
|
|
75
|
+
&& ${#CREATED_WORKTREES[@]} -eq 0 \
|
|
76
|
+
&& ${#CREATED_PATHS[@]} -eq 0 \
|
|
77
|
+
&& -z "$HOME_PRE_SEED_SHA" ]]; then
|
|
78
|
+
return 0
|
|
79
|
+
fi
|
|
80
|
+
echo ""
|
|
81
|
+
warn "seed.sh failed (exit $exit_code). Rolling back partial state..."
|
|
82
|
+
|
|
83
|
+
# Delete pushed remote branches (these may exist if Phase B/C succeeded
|
|
84
|
+
# before a later phase failed).
|
|
85
|
+
for ((i=${#PUSHED_REMOTE_BRANCHES[@]}-1; i>=0; i--)); do
|
|
86
|
+
local entry="${PUSHED_REMOTE_BRANCHES[$i]}"
|
|
87
|
+
local path="${entry%%|*}"
|
|
88
|
+
local branch="${entry#*|}"
|
|
89
|
+
git -C "$path" push origin --delete "$branch" 2>/dev/null || true
|
|
90
|
+
done
|
|
91
|
+
|
|
92
|
+
# Reset HOME workspace if we made a local commit but haven't pushed it.
|
|
93
|
+
if [[ -n "$HOME_PRE_SEED_SHA" ]]; then
|
|
94
|
+
local current_home_sha
|
|
95
|
+
current_home_sha=$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || echo "")
|
|
96
|
+
if [[ "$current_home_sha" != "$HOME_PRE_SEED_SHA" ]]; then
|
|
97
|
+
info "Reverting home workspace commit ($current_home_sha → $HOME_PRE_SEED_SHA)"
|
|
98
|
+
git -C "$REPO_ROOT" reset --hard "$HOME_PRE_SEED_SHA" 2>/dev/null || true
|
|
99
|
+
git -C "$REPO_ROOT" clean -fd projects 2>/dev/null || true
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Local branches in clones (mostly cosmetic — the clones themselves are
|
|
104
|
+
# deleted below, but switch off first to keep git happy).
|
|
105
|
+
for ((i=${#CREATED_LOCAL_BRANCHES[@]}-1; i>=0; i--)); do
|
|
106
|
+
local entry="${CREATED_LOCAL_BRANCHES[$i]}"
|
|
107
|
+
local path="${entry%%|*}"
|
|
108
|
+
local branch="${entry#*|}"
|
|
109
|
+
if [[ -d "$path/.git" ]]; then
|
|
110
|
+
local current=$(git -C "$path" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
111
|
+
if [[ "$current" == "$branch" ]]; then
|
|
112
|
+
git -C "$path" checkout - 2>/dev/null \
|
|
113
|
+
|| git -C "$path" checkout "$DEFAULT_BRANCH" 2>/dev/null \
|
|
114
|
+
|| git -C "$path" checkout main 2>/dev/null || true
|
|
115
|
+
fi
|
|
116
|
+
git -C "$path" branch -D "$branch" 2>/dev/null || true
|
|
117
|
+
fi
|
|
118
|
+
done
|
|
119
|
+
|
|
120
|
+
# Worktrees (ADR-0001 Phase 2): remove each worktree from its base repo,
|
|
121
|
+
# then delete the now-unchecked-out branch. Must run BEFORE the path rm
|
|
122
|
+
# below so the base's worktree registry stays consistent. rm + prune is the
|
|
123
|
+
# fallback if `worktree remove` refuses.
|
|
124
|
+
for ((i=${#CREATED_WORKTREES[@]}-1; i>=0; i--)); do
|
|
125
|
+
local wentry="${CREATED_WORKTREES[$i]}"
|
|
126
|
+
local wbase="${wentry%%|*}"; local wrest="${wentry#*|}"
|
|
127
|
+
local wpath="${wrest%%|*}"; local wbranch="${wrest#*|}"
|
|
128
|
+
git -C "$wbase" worktree remove --force "$wpath" 2>/dev/null \
|
|
129
|
+
|| { rm -rf "$wpath"; git -C "$wbase" worktree prune 2>/dev/null || true; }
|
|
130
|
+
git -C "$wbase" branch -D "$wbranch" 2>/dev/null || true
|
|
131
|
+
done
|
|
132
|
+
|
|
133
|
+
# Created filesystem paths (reverse order — innermost first)
|
|
134
|
+
for ((i=${#CREATED_PATHS[@]}-1; i>=0; i--)); do
|
|
135
|
+
[[ -n "${CREATED_PATHS[$i]}" ]] && rm -rf "${CREATED_PATHS[$i]}"
|
|
136
|
+
done
|
|
137
|
+
|
|
138
|
+
warn "Rollback complete. Home workspace restored, remote branches deleted."
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
trap 'run_rollback' EXIT
|
|
142
|
+
|
|
143
|
+
# ── Pre-conditions on the HOME workspace ─────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
cd "$REPO_ROOT"
|
|
146
|
+
|
|
147
|
+
CURRENT_HOME_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
148
|
+
if [[ "$CURRENT_HOME_BRANCH" != "$DEFAULT_BRANCH" ]]; then
|
|
149
|
+
hard_stop "Home workspace is on '$CURRENT_HOME_BRANCH', expected '$DEFAULT_BRANCH'.
|
|
150
|
+
The home workspace must stay on $DEFAULT_BRANCH (project branches live only in
|
|
151
|
+
per-project workspaces under \$AGENT_WORK_ROOT). Switch back first:
|
|
152
|
+
git checkout $DEFAULT_BRANCH"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
if [[ -n "$(git status --porcelain)" ]]; then
|
|
156
|
+
hard_stop "Home workspace has uncommitted changes. Commit or stash first."
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
# Fetch + ff-pull default to ensure we're current with origin.
|
|
160
|
+
info "Fetching latest $DEFAULT_BRANCH from origin..."
|
|
161
|
+
git fetch origin "$DEFAULT_BRANCH" >/dev/null 2>&1 || true
|
|
162
|
+
if ! git pull --ff-only origin "$DEFAULT_BRANCH" >/dev/null 2>&1; then
|
|
163
|
+
warn "Could not fast-forward home workspace. Proceeding with local state."
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# ── C01 Validation Gates ──────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
echo "[ C01 ] Validating GitHub Project..."
|
|
169
|
+
|
|
170
|
+
PROJECT_NUMBER=$(echo "$GITHUB_PROJECT_URL" | grep -oE '/projects/[0-9]+' | grep -oE '[0-9]+') \
|
|
171
|
+
|| hard_stop "Cannot extract project number from: $GITHUB_PROJECT_URL"
|
|
172
|
+
[[ -n "$PROJECT_NUMBER" ]] || hard_stop "Cannot extract project number from: $GITHUB_PROJECT_URL"
|
|
173
|
+
|
|
174
|
+
if echo "$GITHUB_PROJECT_URL" | grep -q '/orgs/'; then
|
|
175
|
+
PROJECT_OWNER=$(echo "$GITHUB_PROJECT_URL" | sed 's|.*/orgs/\([^/]*\)/.*|\1|')
|
|
176
|
+
OWNER_FIELD="organization"
|
|
177
|
+
else
|
|
178
|
+
PROJECT_OWNER=$(echo "$GITHUB_PROJECT_URL" | sed 's|.*/users/\([^/]*\)/.*|\1|')
|
|
179
|
+
OWNER_FIELD="user"
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
info "Owner: $PROJECT_OWNER ($OWNER_FIELD), Project #$PROJECT_NUMBER"
|
|
183
|
+
|
|
184
|
+
PROJECT_DATA=$(gh api graphql -f query="
|
|
185
|
+
query {
|
|
186
|
+
${OWNER_FIELD}(login: \"$PROJECT_OWNER\") {
|
|
187
|
+
projectV2(number: $PROJECT_NUMBER) {
|
|
188
|
+
id
|
|
189
|
+
title
|
|
190
|
+
shortDescription
|
|
191
|
+
items(first: 50) {
|
|
192
|
+
nodes {
|
|
193
|
+
content {
|
|
194
|
+
... on Issue { url repository { url } }
|
|
195
|
+
... on PullRequest { url repository { url } }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}") || hard_stop "GitHub Project not found or not accessible. Check URL and permissions."
|
|
202
|
+
|
|
203
|
+
PROJECT_TITLE=$(echo "$PROJECT_DATA" | python3 -c "
|
|
204
|
+
import sys, json
|
|
205
|
+
d = json.load(sys.stdin)
|
|
206
|
+
p = list(d['data'].values())[0]['projectV2']
|
|
207
|
+
print(p.get('title') or '')
|
|
208
|
+
")
|
|
209
|
+
[[ -n "$PROJECT_TITLE" ]] || hard_stop "GitHub Project has no name."
|
|
210
|
+
info "Project title: $PROJECT_TITLE"
|
|
211
|
+
|
|
212
|
+
ITEM_COUNT=$(echo "$PROJECT_DATA" | python3 -c "
|
|
213
|
+
import sys, json
|
|
214
|
+
d = json.load(sys.stdin)
|
|
215
|
+
p = list(d['data'].values())[0]['projectV2']
|
|
216
|
+
print(sum(1 for i in p['items']['nodes'] if i.get('content')))
|
|
217
|
+
")
|
|
218
|
+
[[ "$ITEM_COUNT" -gt 0 ]] || hard_stop "GitHub Project has no linked Issues or PRs."
|
|
219
|
+
info "Linked items: $ITEM_COUNT"
|
|
220
|
+
|
|
221
|
+
PROJECT_DESC=$(echo "$PROJECT_DATA" | python3 -c "
|
|
222
|
+
import sys, json
|
|
223
|
+
d = json.load(sys.stdin)
|
|
224
|
+
p = list(d['data'].values())[0]['projectV2']
|
|
225
|
+
print(p.get('shortDescription') or '')
|
|
226
|
+
" 2>/dev/null || echo "")
|
|
227
|
+
[[ -n "$PROJECT_DESC" ]] || warn "Project has no description"
|
|
228
|
+
|
|
229
|
+
echo "[ C01 ] Validation passed."
|
|
230
|
+
echo ""
|
|
231
|
+
|
|
232
|
+
# ── Compute project ID ────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
SHORT_SLUG=$(slugify "$PROJECT_TITLE")
|
|
235
|
+
# A title that is all-punctuation / non-ASCII / '..' slugifies to empty, which
|
|
236
|
+
# would compose a malformed 'PRJ-NNN-' / 'brnch-NNN-' (§5 slug-empty finding).
|
|
237
|
+
[[ -n "$SHORT_SLUG" ]] \
|
|
238
|
+
|| hard_stop "Project title '$PROJECT_TITLE' produced an empty slug. Rename the GitHub Project to include ASCII alphanumerics."
|
|
239
|
+
LAST_ISSUED=$(yaml_get "$REGISTRY" "last_issued")
|
|
240
|
+
NNN=$(printf "%03d" $((LAST_ISSUED + 1)))
|
|
241
|
+
PROJECT_ID="PRJ-${NNN}-${SHORT_SLUG}"
|
|
242
|
+
# POL-069 (scheme B): the project branch is keyed on the GitHub project NUMBER
|
|
243
|
+
# (not the registry NNN) — e.g. PRJ-27-<slug> for project PRJ-013-<slug>. seed
|
|
244
|
+
# stores this in registry/project.yaml so project_branch_for_id reads it
|
|
245
|
+
# everywhere (existing brnch-NNN projects keep their stored name).
|
|
246
|
+
BRANCH="PRJ-${PROJECT_NUMBER}-${SHORT_SLUG}"
|
|
247
|
+
TODAY=$(today)
|
|
248
|
+
NEW_LAST_ISSUED=$((LAST_ISSUED + 1))
|
|
249
|
+
|
|
250
|
+
PROJECT_WORK_ROOT="$AGENT_WORK_ROOT/$PROJECT_ID"
|
|
251
|
+
ORG_GOV_CLONE="$PROJECT_WORK_ROOT/$WORKSPACE_REPO"
|
|
252
|
+
|
|
253
|
+
echo "Project ID : $PROJECT_ID"
|
|
254
|
+
echo "Branch : $BRANCH"
|
|
255
|
+
echo "Per-project root : $PROJECT_WORK_ROOT"
|
|
256
|
+
echo "ORG GOV clone : $ORG_GOV_CLONE"
|
|
257
|
+
echo ""
|
|
258
|
+
|
|
259
|
+
# ── Leftover-state detection ──────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
LEFTOVER=()
|
|
262
|
+
|
|
263
|
+
if git -C "$REPO_ROOT" rev-parse --verify "$BRANCH" &>/dev/null; then
|
|
264
|
+
LEFTOVER+=("local branch '$BRANCH' in home workspace")
|
|
265
|
+
fi
|
|
266
|
+
if git ls-remote --exit-code --heads origin "$BRANCH" &>/dev/null; then
|
|
267
|
+
LEFTOVER+=("remote branch 'origin/$BRANCH' on $ORG_REPO_URL")
|
|
268
|
+
fi
|
|
269
|
+
if [[ -d "$PROJECT_WORK_ROOT" ]]; then
|
|
270
|
+
LEFTOVER+=("per-project workspace at '$PROJECT_WORK_ROOT'")
|
|
271
|
+
fi
|
|
272
|
+
if [[ -d "$REPO_ROOT/projects/$PROJECT_ID" ]]; then
|
|
273
|
+
LEFTOVER+=("home stub folder 'projects/$PROJECT_ID/' on $DEFAULT_BRANCH")
|
|
274
|
+
fi
|
|
275
|
+
if python3 - "$REGISTRY" "$PROJECT_ID" <<'PY'
|
|
276
|
+
import sys, yaml
|
|
277
|
+
c = yaml.safe_load(open(sys.argv[1])) or {}
|
|
278
|
+
ids = [p.get('id') for p in (c.get('projects') or []) if p]
|
|
279
|
+
sys.exit(0 if sys.argv[2] in ids else 1)
|
|
280
|
+
PY
|
|
281
|
+
then
|
|
282
|
+
LEFTOVER+=("registry entry for '$PROJECT_ID'")
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
if [[ ${#LEFTOVER[@]} -gt 0 ]]; then
|
|
286
|
+
echo ""
|
|
287
|
+
warn "Detected leftover state from a previous failed run:"
|
|
288
|
+
for item in "${LEFTOVER[@]}"; do echo " - $item"; done
|
|
289
|
+
echo ""
|
|
290
|
+
if $NON_INTERACTIVE; then
|
|
291
|
+
hard_stop "Leftover state detected and --non-interactive is set.
|
|
292
|
+
Clean up manually, then re-run."
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
echo "Options:"
|
|
296
|
+
echo " (a) Clean up these artifacts and start fresh"
|
|
297
|
+
echo " (b) Abort — inspect manually"
|
|
298
|
+
echo ""
|
|
299
|
+
printf "Choose [a/b]: "
|
|
300
|
+
read -r choice </dev/tty
|
|
301
|
+
case "$choice" in
|
|
302
|
+
a|A)
|
|
303
|
+
info "Cleaning up partial state..."
|
|
304
|
+
# Remote branch on workspace repo
|
|
305
|
+
git push origin --delete "$BRANCH" 2>/dev/null || true
|
|
306
|
+
# Local branch (should not exist on home in the new model — we never
|
|
307
|
+
# create one there — but clean defensively)
|
|
308
|
+
git branch -D "$BRANCH" 2>/dev/null || true
|
|
309
|
+
# Per-project workspace
|
|
310
|
+
rm -rf "$PROJECT_WORK_ROOT"
|
|
311
|
+
# Home stub folder (was created by a prior partial seed)
|
|
312
|
+
rm -rf "$REPO_ROOT/projects/$PROJECT_ID"
|
|
313
|
+
# Stray registry entry
|
|
314
|
+
python3 - "$REGISTRY" "$PROJECT_ID" <<'PY' 2>/dev/null || true
|
|
315
|
+
import sys, yaml
|
|
316
|
+
registry, pid = sys.argv[1:]
|
|
317
|
+
with open(registry) as f: c = yaml.safe_load(f) or {}
|
|
318
|
+
c['projects'] = [p for p in (c.get('projects') or []) if p and p.get('id') != pid]
|
|
319
|
+
with open(registry, 'w') as f:
|
|
320
|
+
yaml.dump(c, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
321
|
+
PY
|
|
322
|
+
git checkout -- registry.yaml 2>/dev/null || true
|
|
323
|
+
info "Cleanup complete. Continuing seed..."
|
|
324
|
+
echo ""
|
|
325
|
+
;;
|
|
326
|
+
*)
|
|
327
|
+
hard_stop "Aborted at user request."
|
|
328
|
+
;;
|
|
329
|
+
esac
|
|
330
|
+
fi
|
|
331
|
+
|
|
332
|
+
# ── Discover linked repos + prompt for base branches ─────────────────────────
|
|
333
|
+
|
|
334
|
+
# The workspace repo is an implicit participant in every project and must
|
|
335
|
+
# never be processed as a code repo (POL-057); otherwise its code-repo clone
|
|
336
|
+
# path collides with the gov clone and seed false-positives on "branch already
|
|
337
|
+
# exists" (line ~645). Filter it out of discovered repos here.
|
|
338
|
+
REPO_URLS=$(PROJECT_DATA="$PROJECT_DATA" python3 <<'PY'
|
|
339
|
+
import os, json, re
|
|
340
|
+
org = (os.environ.get('GITHUB_ORG') or '').lower()
|
|
341
|
+
wsrepo = (os.environ.get('WORKSPACE_REPO') or '').lower()
|
|
342
|
+
orgurl = (os.environ.get('ORG_REPO_URL') or '').lower()
|
|
343
|
+
|
|
344
|
+
def slug(u):
|
|
345
|
+
if not u: return ''
|
|
346
|
+
u = re.sub(r'\.git$', '', u.strip().lower()).rstrip('/')
|
|
347
|
+
m = re.search(r'[:/]([^/:]+)/([^/]+)$', u) # owner/name from ssh or https
|
|
348
|
+
return f'{m.group(1)}/{m.group(2)}' if m else u
|
|
349
|
+
|
|
350
|
+
ws = set()
|
|
351
|
+
if org and wsrepo: ws.add(f'{org}/{wsrepo}')
|
|
352
|
+
if orgurl: ws.add(slug(orgurl))
|
|
353
|
+
|
|
354
|
+
d = json.loads(os.environ.get('PROJECT_DATA') or '{}')
|
|
355
|
+
p = list(d['data'].values())[0]['projectV2']
|
|
356
|
+
seen = set()
|
|
357
|
+
for i in p['items']['nodes']:
|
|
358
|
+
c = i.get('content') or {}
|
|
359
|
+
r = (c.get('repository') or {}).get('url')
|
|
360
|
+
if not r or r in seen:
|
|
361
|
+
continue
|
|
362
|
+
if slug(r) in ws: # workspace repo is implicit (POL-057) — skip
|
|
363
|
+
continue
|
|
364
|
+
seen.add(r)
|
|
365
|
+
print(r)
|
|
366
|
+
PY
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
REPO_URL_LIST=()
|
|
370
|
+
REPO_BASE_LIST=()
|
|
371
|
+
|
|
372
|
+
if [[ -n "$REPO_URLS" ]]; then
|
|
373
|
+
while IFS= read -r line; do
|
|
374
|
+
[[ -z "$line" ]] && continue
|
|
375
|
+
REPO_URL_LIST+=("$line")
|
|
376
|
+
done <<< "$REPO_URLS"
|
|
377
|
+
|
|
378
|
+
for repo_url in "${REPO_URL_LIST[@]}"; do
|
|
379
|
+
# Pre-flight (ADR-0001): read the repo's real branches up front so a wrong
|
|
380
|
+
# repo or base is obvious HERE — before any registry commit or worktree —
|
|
381
|
+
# instead of failing mid-Phase-C and rolling back. Default to the repo's
|
|
382
|
+
# actual default branch, not the org-wide one.
|
|
383
|
+
_heads=$(git ls-remote --heads "$repo_url" 2>/dev/null | sed -E 's#.*refs/heads/##')
|
|
384
|
+
[[ -n "$_heads" ]] || hard_stop "Could not read branches of '$repo_url' (wrong repo URL, or no access?)."
|
|
385
|
+
_default=$(git ls-remote --symref "$repo_url" HEAD 2>/dev/null \
|
|
386
|
+
| sed -nE 's#^ref:[[:space:]]+refs/heads/([^[:space:]]+)[[:space:]]+HEAD#\1#p')
|
|
387
|
+
[[ -n "$_default" ]] || _default="$DEFAULT_CODE_BRANCH"
|
|
388
|
+
if $NON_INTERACTIVE; then
|
|
389
|
+
base="$_default"
|
|
390
|
+
echo " Base branch for '$repo_url': $base (--non-interactive)"
|
|
391
|
+
else
|
|
392
|
+
echo " branches in $repo_url: $(echo "$_heads" | tr '\n' ' ')"
|
|
393
|
+
printf " Base branch for '%s' [%s]: " "$repo_url" "$_default"
|
|
394
|
+
read -r input_base </dev/tty
|
|
395
|
+
base="${input_base:-$_default}"
|
|
396
|
+
fi
|
|
397
|
+
grep -qx "$base" <<< "$_heads" \
|
|
398
|
+
|| hard_stop "Base branch '$base' not found in '$repo_url'. Available: $(echo "$_heads" | tr '\n' ' '). (Wrong repo or base?)"
|
|
399
|
+
REPO_BASE_LIST+=("$base")
|
|
400
|
+
done
|
|
401
|
+
else
|
|
402
|
+
warn "No repos detected from linked items — project.yaml repos[] will be a placeholder."
|
|
403
|
+
fi
|
|
404
|
+
|
|
405
|
+
get_repo_base() {
|
|
406
|
+
local target="$1" i
|
|
407
|
+
for ((i=0; i<${#REPO_URL_LIST[@]}; i++)); do
|
|
408
|
+
if [[ "${REPO_URL_LIST[$i]}" == "$target" ]]; then
|
|
409
|
+
echo "${REPO_BASE_LIST[$i]}"
|
|
410
|
+
return 0
|
|
411
|
+
fi
|
|
412
|
+
done
|
|
413
|
+
echo "$DEFAULT_CODE_BRANCH"
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
CURRENT_USER=$(git config user.email 2>/dev/null || echo "$ASSIGNEE")
|
|
417
|
+
|
|
418
|
+
is_authorized_for_project "$GITHUB_PROJECT_URL" "$ASSIGNEE" \
|
|
419
|
+
|| hard_stop "Not authorized: '$CURRENT_USER' needs write access to the GitHub Project ($GITHUB_PROJECT_URL) to seed it."
|
|
420
|
+
|
|
421
|
+
# ── Phase A: HOME workspace, default branch — registry stub + folder stub ──
|
|
422
|
+
# We commit locally but do NOT push yet — pushing happens at the very end
|
|
423
|
+
# once every other phase has succeeded. If something fails in B/C, we just
|
|
424
|
+
# git reset --hard back to the pre-seed SHA (recorded below).
|
|
425
|
+
|
|
426
|
+
HOME_PRE_SEED_SHA=$(git -C "$REPO_ROOT" rev-parse HEAD)
|
|
427
|
+
|
|
428
|
+
info "Phase A: updating home registry + creating projects/$PROJECT_ID/ stub..."
|
|
429
|
+
|
|
430
|
+
python3 - "$REGISTRY" "$PROJECT_ID" "$BRANCH" "$ASSIGNEE" "$TODAY" "$GITHUB_PROJECT_URL" "$NEW_LAST_ISSUED" "$PROJECT_OWNER" "$CURRENT_USER" <<'PY'
|
|
431
|
+
import sys, yaml
|
|
432
|
+
registry, pid, branch, assignee, today, gh_url, new_last, owner, seeded = sys.argv[1:]
|
|
433
|
+
with open(registry) as f: c = yaml.safe_load(f) or {}
|
|
434
|
+
c['last_issued'] = int(new_last)
|
|
435
|
+
if not c.get('projects'): c['projects'] = []
|
|
436
|
+
c['projects'].append({
|
|
437
|
+
'id': pid,
|
|
438
|
+
'branch': branch,
|
|
439
|
+
'github_project': gh_url,
|
|
440
|
+
'github_owner': owner,
|
|
441
|
+
'assigned_to': assignee,
|
|
442
|
+
'seeded_by': seeded,
|
|
443
|
+
'created_at': today,
|
|
444
|
+
'status': 'active',
|
|
445
|
+
})
|
|
446
|
+
# Drop any matching pre_assignment now that we have a real registry entry.
|
|
447
|
+
c['pre_assignments'] = [a for a in (c.get('pre_assignments') or [])
|
|
448
|
+
if a and a.get('github_project') != gh_url]
|
|
449
|
+
with open(registry, 'w') as f:
|
|
450
|
+
yaml.dump(c, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
451
|
+
PY
|
|
452
|
+
|
|
453
|
+
mkdir -p "$REPO_ROOT/projects/$PROJECT_ID"
|
|
454
|
+
cat > "$REPO_ROOT/projects/$PROJECT_ID/.gitkeep" <<EOF
|
|
455
|
+
# Active project — full content lives on branch '$BRANCH'.
|
|
456
|
+
#
|
|
457
|
+
# This folder is a stub on $DEFAULT_BRANCH so the registry entry has a folder
|
|
458
|
+
# to point at (validator requirement). The full project content (project.yaml,
|
|
459
|
+
# agent.md, knowledge/, etc.) lives on branch '$BRANCH' inside the per-project
|
|
460
|
+
# workspace at:
|
|
461
|
+
#
|
|
462
|
+
# $AGENT_WORK_ROOT/$PROJECT_ID/$WORKSPACE_REPO/projects/$PROJECT_ID/
|
|
463
|
+
#
|
|
464
|
+
# On close-project, the project branch merges back to $DEFAULT_BRANCH and the
|
|
465
|
+
# full content arrives here, overwriting this stub.
|
|
466
|
+
EOF
|
|
467
|
+
|
|
468
|
+
git -C "$REPO_ROOT" add registry.yaml "projects/$PROJECT_ID/.gitkeep"
|
|
469
|
+
git -C "$REPO_ROOT" commit -m "seed: register project $PROJECT_ID (assigned to $ASSIGNEE)" >/dev/null
|
|
470
|
+
info " ✓ home commit recorded locally (will push after all phases succeed)"
|
|
471
|
+
|
|
472
|
+
# ── Phase B: per-project workspace — clone ORG GOVERNANCE on project branch ──
|
|
473
|
+
|
|
474
|
+
info "Phase B: cloning ORG GOVERNANCE into per-project workspace..."
|
|
475
|
+
|
|
476
|
+
mkdir -p "$PROJECT_WORK_ROOT"
|
|
477
|
+
CREATED_PATHS+=("$PROJECT_WORK_ROOT")
|
|
478
|
+
|
|
479
|
+
# ADR-0001 Phase 2: materialize the per-project governance workspace as a
|
|
480
|
+
# WORKTREE of the home clone (REPO_ROOT), not a fresh local clone. A worktree
|
|
481
|
+
# already carries REPO_ROOT's just-committed registry stub and shares its
|
|
482
|
+
# origin remote, so the old clone + remote re-point is unnecessary. The new
|
|
483
|
+
# project branch is created off $DEFAULT_BRANCH (the home branch).
|
|
484
|
+
git -C "$REPO_ROOT" worktree add -b "$BRANCH" "$ORG_GOV_CLONE" "$DEFAULT_BRANCH" >/dev/null 2>&1 \
|
|
485
|
+
|| hard_stop "Failed to create governance worktree at $ORG_GOV_CLONE"
|
|
486
|
+
CREATED_WORKTREES+=("$REPO_ROOT|$ORG_GOV_CLONE|$BRANCH")
|
|
487
|
+
|
|
488
|
+
# Carry the developer's git identity into the per-project workspace so commits
|
|
489
|
+
# and C01 authorization here reflect the seeder, not the ambient global.
|
|
490
|
+
set_clone_identity "$ORG_GOV_CLONE"
|
|
491
|
+
info " ✓ created branch '$BRANCH' in ORG GOV worktree"
|
|
492
|
+
|
|
493
|
+
# ── Phase B.1: scaffold projects/<PID>/* inside the clone ────────────────────
|
|
494
|
+
|
|
495
|
+
PROJECT_DIR="$ORG_GOV_CLONE/projects/$PROJECT_ID"
|
|
496
|
+
rm -f "$PROJECT_DIR/.gitkeep" # we're about to write real content
|
|
497
|
+
mkdir -p "$PROJECT_DIR"/{requirements,environment,knowledge}
|
|
498
|
+
|
|
499
|
+
# todo.md from template. The template header placeholder is the full
|
|
500
|
+
# project-id pattern 'PRJ-NNN-<slug>'; substitute the concrete PROJECT_ID.
|
|
501
|
+
TODO_TEMPLATE="$ORG_GOV_CLONE/framework/knowledge/guidance/todo-template.md"
|
|
502
|
+
if [[ -f "$TODO_TEMPLATE" ]]; then
|
|
503
|
+
sed "s/PRJ-NNN-<slug>/$PROJECT_ID/g" "$TODO_TEMPLATE" > "$PROJECT_DIR/knowledge/todo.md"
|
|
504
|
+
fi
|
|
505
|
+
|
|
506
|
+
# Build repos[] YAML fragment
|
|
507
|
+
REPOS_BLOCK=""
|
|
508
|
+
if [[ ${#REPO_URL_LIST[@]} -gt 0 ]]; then
|
|
509
|
+
for repo_url in "${REPO_URL_LIST[@]}"; do
|
|
510
|
+
base=$(get_repo_base "$repo_url")
|
|
511
|
+
REPOS_BLOCK+=" - url: $repo_url"$'\n'
|
|
512
|
+
REPOS_BLOCK+=" role: primary"$'\n'
|
|
513
|
+
REPOS_BLOCK+=" base_branch: $base"$'\n'
|
|
514
|
+
REPOS_BLOCK+=" added_at: $TODAY"$'\n'
|
|
515
|
+
REPOS_BLOCK+=" added_reason: ~"$'\n'
|
|
516
|
+
done
|
|
517
|
+
else
|
|
518
|
+
REPOS_BLOCK=" - url: ~"$'\n'
|
|
519
|
+
REPOS_BLOCK+=" role: primary"$'\n'
|
|
520
|
+
REPOS_BLOCK+=" base_branch: $DEFAULT_CODE_BRANCH"$'\n'
|
|
521
|
+
REPOS_BLOCK+=" added_at: ~"$'\n'
|
|
522
|
+
REPOS_BLOCK+=" added_reason: ~"$'\n'
|
|
523
|
+
fi
|
|
524
|
+
|
|
525
|
+
# Quote string scalars for YAML safety.
|
|
526
|
+
# Escape backslash FIRST, then the double-quote, so an untrusted value ending
|
|
527
|
+
# in '\' (e.g. a GitHub Project title) cannot escape the closing quote and
|
|
528
|
+
# inject YAML (C10). Order matters: backslash before quote.
|
|
529
|
+
yaml_quote() {
|
|
530
|
+
printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
|
531
|
+
}
|
|
532
|
+
Q_PROJECT_ID=$(yaml_quote "$PROJECT_ID")
|
|
533
|
+
Q_SHORT_SLUG=$(yaml_quote "$SHORT_SLUG")
|
|
534
|
+
Q_GITHUB_PROJECT_URL=$(yaml_quote "$GITHUB_PROJECT_URL")
|
|
535
|
+
Q_PROJECT_TITLE=$(yaml_quote "$PROJECT_TITLE")
|
|
536
|
+
Q_ASSIGNEE=$(yaml_quote "$ASSIGNEE")
|
|
537
|
+
Q_CURRENT_USER=$(yaml_quote "$CURRENT_USER")
|
|
538
|
+
Q_BRANCH=$(yaml_quote "$BRANCH")
|
|
539
|
+
|
|
540
|
+
cat > "$PROJECT_DIR/project.yaml" <<YAML
|
|
541
|
+
id: $Q_PROJECT_ID
|
|
542
|
+
slug: $Q_SHORT_SLUG
|
|
543
|
+
branch: $Q_BRANCH
|
|
544
|
+
description: ~
|
|
545
|
+
github_project: $Q_GITHUB_PROJECT_URL
|
|
546
|
+
github_project_name: $Q_PROJECT_TITLE
|
|
547
|
+
assigned_to: $Q_ASSIGNEE
|
|
548
|
+
seeded_by: $Q_CURRENT_USER
|
|
549
|
+
status: active
|
|
550
|
+
created_at: $TODAY
|
|
551
|
+
started_at: $TODAY
|
|
552
|
+
completed_at: ~
|
|
553
|
+
paused_at: ~
|
|
554
|
+
cancelled_at: ~
|
|
555
|
+
cancellation_reason: ~
|
|
556
|
+
repos:
|
|
557
|
+
$REPOS_BLOCK
|
|
558
|
+
knowledge_status: ~
|
|
559
|
+
knowledge_pr: ~
|
|
560
|
+
agent_config:
|
|
561
|
+
model: auto
|
|
562
|
+
provider: cursor
|
|
563
|
+
YAML
|
|
564
|
+
|
|
565
|
+
cat > "$PROJECT_DIR/agent.md" <<MD
|
|
566
|
+
# $PROJECT_TITLE — Project Agent Entry Point
|
|
567
|
+
# Project: $PROJECT_ID | Branch: $BRANCH
|
|
568
|
+
|
|
569
|
+
This file is the project-specific entrypoint. Combined with the framework's
|
|
570
|
+
universal session-start protocol (CLAUDE.md / AGENTS.md / etc. at repo root),
|
|
571
|
+
it tells you everything you need to start work on $PROJECT_ID.
|
|
572
|
+
|
|
573
|
+
## Working Directory
|
|
574
|
+
|
|
575
|
+
Your per-project workspace lives at:
|
|
576
|
+
|
|
577
|
+
$PROJECT_WORK_ROOT/
|
|
578
|
+
|
|
579
|
+
Inside it:
|
|
580
|
+
|
|
581
|
+
- \`$WORKSPACE_REPO/\` — clone of ORG GOVERNANCE on branch \`$BRANCH\`. This is where
|
|
582
|
+
you are right now. \`projects/$PROJECT_ID/\` here is your project metadata workspace.
|
|
583
|
+
$([[ ${#REPO_URL_LIST[@]} -gt 0 ]] && for u in "${REPO_URL_LIST[@]}"; do
|
|
584
|
+
rn=$(get_repo_name "$u"); echo "- \`$rn/\` — clone of $u on branch \`$BRANCH\`. Code changes go here.";
|
|
585
|
+
done)
|
|
586
|
+
|
|
587
|
+
## Knowledge Layer Priority
|
|
588
|
+
|
|
589
|
+
1. **Org-wide knowledge** → \`$WORKSPACE_REPO/knowledge/\` (read-only this project)
|
|
590
|
+
2. **This project** → \`$WORKSPACE_REPO/projects/$PROJECT_ID/knowledge/\`
|
|
591
|
+
3. **Repo-local** → \`<repo>/knowledge/\` in each cloned code repo
|
|
592
|
+
4. **Your developer preferences** → \`$AGENT_WORK_ROOT/preferences/<your-gh-login>.md\`
|
|
593
|
+
- At session start, run \`gh api user --jq .login\` to determine your handle.
|
|
594
|
+
- Load only the file matching your handle.
|
|
595
|
+
|
|
596
|
+
## Session Start Checklist (C01)
|
|
597
|
+
|
|
598
|
+
1. Verify you are authorized: you have **write access to this project's linked
|
|
599
|
+
GitHub Project** (the authorization source of truth; an owner grants it via
|
|
600
|
+
\`./prj manage assign\`). \`assigned_to\` in \`project.yaml\` is a display
|
|
601
|
+
cache, not the gate. On a task sub-branch, confirm its assignee is you.
|
|
602
|
+
2. Verify \`status: active\` in \`project.yaml\`.
|
|
603
|
+
3. Read \`projects/$PROJECT_ID/knowledge/todo.md\` and surface \`## Open\`
|
|
604
|
+
items before planning new work.
|
|
605
|
+
4. Load all four knowledge layers fresh.
|
|
606
|
+
|
|
607
|
+
## Operational Workflow
|
|
608
|
+
|
|
609
|
+
1. Pick an issue from the GitHub Project board: $GITHUB_PROJECT_URL
|
|
610
|
+
2. Start a task sub-branch: \`./prj task <issue-url>\`
|
|
611
|
+
(creates \`$BRANCH/<task-slug>\` in this clone + each code repo clone)
|
|
612
|
+
3. Do code work in the cloned code repos on the task sub-branch.
|
|
613
|
+
Capture decisions, exceptions, and policy notes in
|
|
614
|
+
\`projects/$PROJECT_ID/knowledge/\` as you go (not at session end).
|
|
615
|
+
Capture intermediate to-dos in \`projects/$PROJECT_ID/knowledge/todo.md\`
|
|
616
|
+
under \`## Open\` as they arise.
|
|
617
|
+
4. When the task is complete: \`./prj merge\` (merges sub-branch into \`$BRANCH\`).
|
|
618
|
+
5. When the whole project is complete: \`./prj close\` (merges \`$BRANCH\` back to
|
|
619
|
+
$DEFAULT_BRANCH in ORG GOVERNANCE, archives, fires knowledge-proposal PR).
|
|
620
|
+
|
|
621
|
+
## Do Not
|
|
622
|
+
|
|
623
|
+
- Never hand-manage task state — tasks are GitHub Issues on the board; use \`./prj task\` / \`./prj merge\`.
|
|
624
|
+
- Create GitHub Issues unilaterally — those are humans-only.
|
|
625
|
+
- Touch \`$WORKSPACE_REPO/knowledge/\` — read-only this project.
|
|
626
|
+
- Push the project branch from the home ORG GOVERNANCE checkout — that
|
|
627
|
+
checkout stays on $DEFAULT_BRANCH. All project-branch work happens here.
|
|
628
|
+
MD
|
|
629
|
+
|
|
630
|
+
# Per-tool agent rule files: copy framework-level files into the project
|
|
631
|
+
# workspace, substituting org values + project ID. The per-project copies have
|
|
632
|
+
# baked-in values (no <ORG_NAME> tokens) so the agent has full context.
|
|
633
|
+
TOOL_FILES=(
|
|
634
|
+
"AGENTS.md"
|
|
635
|
+
"CONVENTIONS.md"
|
|
636
|
+
".cursor/rules/agent.mdc"
|
|
637
|
+
".clinerules/agent.md"
|
|
638
|
+
".windsurf/rules/agent.md"
|
|
639
|
+
".github/copilot-instructions.md"
|
|
640
|
+
".gemini/styleguide.md"
|
|
641
|
+
".continue/rules.md"
|
|
642
|
+
"CLAUDE.md"
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
for rel in "${TOOL_FILES[@]}"; do
|
|
646
|
+
src="$ORG_GOV_CLONE/framework/$rel"
|
|
647
|
+
dst="$PROJECT_DIR/$rel"
|
|
648
|
+
[[ -f "$src" ]] || continue
|
|
649
|
+
mkdir -p "$(dirname "$dst")"
|
|
650
|
+
# Substitute org values + the per-project ID/branch/paths.
|
|
651
|
+
ORG_NAME_V="$ORG_NAME" ORG_SHORT_NAME_V="$ORG_SHORT_NAME" \
|
|
652
|
+
ORG_SLUG_V="$ORG_SLUG" ORG_SLUG_LOWER_V="$ORG_SLUG_LOWER" \
|
|
653
|
+
GITHUB_ORG_V="$GITHUB_ORG" WORKSPACE_REPO_V="$WORKSPACE_REPO" \
|
|
654
|
+
DEFAULT_BRANCH_V="$DEFAULT_BRANCH" DEFAULT_CODE_BRANCH_V="$DEFAULT_CODE_BRANCH" \
|
|
655
|
+
AGENT_WORK_ROOT_V="$AGENT_WORK_ROOT" \
|
|
656
|
+
POLICY_OWNER_EMAIL_V="$POLICY_OWNER_EMAIL" \
|
|
657
|
+
PROJECT_ID_V="$PROJECT_ID" BRANCH_V="$BRANCH" \
|
|
658
|
+
perl -pe '
|
|
659
|
+
s|<ORG_NAME>|$ENV{ORG_NAME_V}|g;
|
|
660
|
+
s|<ORG_SHORT_NAME>|$ENV{ORG_SHORT_NAME_V}|g;
|
|
661
|
+
s|<ORG_SLUG>|$ENV{ORG_SLUG_V}|g;
|
|
662
|
+
s|<org_slug>|$ENV{ORG_SLUG_LOWER_V}|g;
|
|
663
|
+
s|<GITHUB_ORG>|$ENV{GITHUB_ORG_V}|g;
|
|
664
|
+
s|<WORKSPACE_REPO>|$ENV{WORKSPACE_REPO_V}|g;
|
|
665
|
+
s|<DEFAULT_BRANCH>|$ENV{DEFAULT_BRANCH_V}|g;
|
|
666
|
+
s|<DEFAULT_CODE_BRANCH>|$ENV{DEFAULT_CODE_BRANCH_V}|g;
|
|
667
|
+
s|<AGENT_WORK_ROOT>|$ENV{AGENT_WORK_ROOT_V}|g;
|
|
668
|
+
s|<POLICY_OWNER_EMAIL>|$ENV{POLICY_OWNER_EMAIL_V}|g;
|
|
669
|
+
s|<PROJECT_ID>|$ENV{PROJECT_ID_V}|g;
|
|
670
|
+
' "$src" > "$dst"
|
|
671
|
+
done
|
|
672
|
+
|
|
673
|
+
info " ✓ scaffolded $PROJECT_DIR"
|
|
674
|
+
|
|
675
|
+
# Commit the scaffold on the project branch
|
|
676
|
+
git -C "$ORG_GOV_CLONE" add "projects/$PROJECT_ID"
|
|
677
|
+
git -C "$ORG_GOV_CLONE" commit -m "seed: scaffold project content for $PROJECT_ID" >/dev/null
|
|
678
|
+
|
|
679
|
+
# ── Phase C: clone code repos into per-project workspace ─────────────────────
|
|
680
|
+
|
|
681
|
+
if [[ ${#REPO_URL_LIST[@]} -gt 0 ]]; then
|
|
682
|
+
info "Phase C: cloning code repos into $PROJECT_WORK_ROOT/..."
|
|
683
|
+
for repo_url in "${REPO_URL_LIST[@]}"; do
|
|
684
|
+
REPO_NAME=$(get_repo_name "$repo_url")
|
|
685
|
+
REPO_BASE=$(get_repo_base "$repo_url")
|
|
686
|
+
REPO_DIR="$PROJECT_WORK_ROOT/$REPO_NAME"
|
|
687
|
+
|
|
688
|
+
info " setting up $repo_url → $REPO_DIR (worktree, base: $REPO_BASE)..."
|
|
689
|
+
# ADR-0001 Phase 2: one shared base clone per repo, project branch as a
|
|
690
|
+
# worktree off the base branch.
|
|
691
|
+
REPO_BASE_CLONE="$(base_clone_dir "$repo_url")"
|
|
692
|
+
if [[ ! -e "$REPO_BASE_CLONE/.git" ]]; then
|
|
693
|
+
mkdir -p "$(dirname "$REPO_BASE_CLONE")"
|
|
694
|
+
git_clone_retry "$repo_url" "$REPO_BASE_CLONE" \
|
|
695
|
+
|| hard_stop "Clone failed for $repo_url (after retries — check network/repo size)"
|
|
696
|
+
fi
|
|
697
|
+
git -C "$REPO_BASE_CLONE" fetch origin "$REPO_BASE" >/dev/null 2>&1 \
|
|
698
|
+
|| git -C "$REPO_BASE_CLONE" fetch origin >/dev/null 2>&1 || true
|
|
699
|
+
git -C "$REPO_BASE_CLONE" show-ref --verify --quiet "refs/remotes/origin/$REPO_BASE" \
|
|
700
|
+
|| hard_stop "Base branch '$REPO_BASE' not found in $repo_url"
|
|
701
|
+
if git -C "$REPO_BASE_CLONE" show-ref --verify --quiet "refs/heads/$BRANCH" \
|
|
702
|
+
|| git -C "$REPO_BASE_CLONE" show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
|
|
703
|
+
hard_stop "Branch '$BRANCH' already exists in $repo_url — investigate."
|
|
704
|
+
fi
|
|
705
|
+
git -C "$REPO_BASE_CLONE" worktree add -b "$BRANCH" "$REPO_DIR" "origin/$REPO_BASE" >/dev/null 2>&1 \
|
|
706
|
+
|| hard_stop "Failed to create worktree for $repo_url on '$BRANCH'"
|
|
707
|
+
CREATED_WORKTREES+=("$REPO_BASE_CLONE|$REPO_DIR|$BRANCH")
|
|
708
|
+
set_clone_identity "$REPO_DIR"
|
|
709
|
+
git -C "$REPO_DIR" push -u origin "$BRANCH" >/dev/null 2>&1 \
|
|
710
|
+
|| hard_stop "Failed to push '$BRANCH' to $repo_url"
|
|
711
|
+
PUSHED_REMOTE_BRANCHES+=("$REPO_DIR|$BRANCH")
|
|
712
|
+
info " ✓ branch '$BRANCH' pushed"
|
|
713
|
+
done
|
|
714
|
+
fi
|
|
715
|
+
|
|
716
|
+
# ── Phase D: push everything ─────────────────────────────────────────────────
|
|
717
|
+
|
|
718
|
+
info "Phase D: pushing project branch and home registry update..."
|
|
719
|
+
|
|
720
|
+
# Push project branch from ORG GOV clone first (so origin has the entry)
|
|
721
|
+
git -C "$ORG_GOV_CLONE" push -u origin "$BRANCH" >/dev/null 2>&1 \
|
|
722
|
+
|| hard_stop "Failed to push '$BRANCH' to $ORG_REPO_URL"
|
|
723
|
+
PUSHED_REMOTE_BRANCHES+=("$ORG_GOV_CLONE|$BRANCH")
|
|
724
|
+
info " ✓ pushed $BRANCH to $ORG_REPO_URL"
|
|
725
|
+
|
|
726
|
+
# Push home's default branch (the registry update + stub folder commit)
|
|
727
|
+
git -C "$REPO_ROOT" push origin "$DEFAULT_BRANCH" >/dev/null 2>&1 \
|
|
728
|
+
|| hard_stop "Failed to push $DEFAULT_BRANCH from home workspace"
|
|
729
|
+
info " ✓ pushed $DEFAULT_BRANCH (registry update) to $ORG_REPO_URL"
|
|
730
|
+
|
|
731
|
+
# ── Done — disarm rollback ───────────────────────────────────────────────────
|
|
732
|
+
|
|
733
|
+
SEED_OK=1
|
|
734
|
+
|
|
735
|
+
# ── First-session prompt ─────────────────────────────────────────────────────
|
|
736
|
+
|
|
737
|
+
FIRST_PROMPT="Start project $PROJECT_ID. I'm working in $ORG_GOV_CLONE on branch $BRANCH. Follow your session-start protocol: read org-config.yaml, then read projects/$PROJECT_ID/agent.md, then knowledge/policies/agentic-development-policy.md, then surface any \\\`## Open\\\` items from projects/$PROJECT_ID/knowledge/todo.md before planning work."
|
|
738
|
+
|
|
739
|
+
cat <<EOF
|
|
740
|
+
|
|
741
|
+
=== Project $PROJECT_ID initialized.
|
|
742
|
+
|
|
743
|
+
ID : $PROJECT_ID
|
|
744
|
+
Branch : $BRANCH
|
|
745
|
+
Assignee : $ASSIGNEE
|
|
746
|
+
GitHub : $GITHUB_PROJECT_URL
|
|
747
|
+
|
|
748
|
+
Workspace layout:
|
|
749
|
+
$PROJECT_WORK_ROOT/
|
|
750
|
+
└── $WORKSPACE_REPO/ ← ORG GOVERNANCE clone (you cd here)
|
|
751
|
+
EOF
|
|
752
|
+
if [[ ${#REPO_URL_LIST[@]} -gt 0 ]]; then
|
|
753
|
+
for repo_url in "${REPO_URL_LIST[@]}"; do
|
|
754
|
+
REPO_NAME=$(get_repo_name "$repo_url")
|
|
755
|
+
echo " └── $REPO_NAME/ ← code repo clone (code changes here)"
|
|
756
|
+
done
|
|
757
|
+
fi
|
|
758
|
+
|
|
759
|
+
cat <<EOF
|
|
760
|
+
|
|
761
|
+
The home workspace stayed on '$DEFAULT_BRANCH' throughout. All project-branch
|
|
762
|
+
work happens inside the per-project workspace above.
|
|
763
|
+
|
|
764
|
+
────────────────────────────────────────────────────────────────────────
|
|
765
|
+
Next step — paste this in your shell:
|
|
766
|
+
|
|
767
|
+
cd $ORG_GOV_CLONE
|
|
768
|
+
|
|
769
|
+
Then start your agent session with this prompt:
|
|
770
|
+
|
|
771
|
+
$FIRST_PROMPT
|
|
772
|
+
|
|
773
|
+
────────────────────────────────────────────────────────────────────────
|
|
774
|
+
EOF
|