@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.
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env bash
2
+ # Script: add-repo
3
+ # Purpose: Adds a new repository to an active project when scope expands.
4
+ # Usage: bash add-repo.sh <project_id> <repo_url> <role> <added_reason> [base_branch]
5
+ # Compliance: C02 (POL-062 to POL-066)
6
+
7
+ set -euo pipefail
8
+ source "$(dirname "$0")/lib.sh"
9
+ load_config
10
+
11
+ # ── Inputs ────────────────────────────────────────────────────────────────────
12
+
13
+ PROJECT_ID="${1:-}"
14
+ REPO_URL="${2:-}"
15
+ ROLE="${3:-}"
16
+ ADDED_REASON="${4:-}"
17
+ BASE_BRANCH="${5:-}"
18
+
19
+ [[ -n "$PROJECT_ID" ]] || hard_stop "Usage: $0 <project_id> <repo_url> <role> <added_reason> [base_branch]"
20
+ [[ -n "$REPO_URL" ]] || hard_stop "Usage: $0 <project_id> <repo_url> <role> <added_reason> [base_branch]"
21
+ [[ -n "$ROLE" ]] || hard_stop "Usage: $0 <project_id> <repo_url> <role> <added_reason> [base_branch]"
22
+ [[ -n "$ADDED_REASON" ]] || hard_stop "Usage: $0 <project_id> <repo_url> <role> <added_reason> [base_branch]"
23
+
24
+ # Validate role value
25
+ case "$ROLE" in
26
+ primary|dependency|read-only) ;;
27
+ *) hard_stop "Invalid role '$ROLE'. Must be: primary | dependency | read-only" ;;
28
+ esac
29
+
30
+ echo "=== add-repo: $PROJECT_ID"
31
+ echo " Repo: $REPO_URL"
32
+ echo " Role: $ROLE"
33
+ echo " Reason: $ADDED_REASON"
34
+ echo ""
35
+
36
+ PROJECT_YAML=$(get_project_yaml "$PROJECT_ID")
37
+ check_project_exists "$PROJECT_ID"
38
+
39
+ # ── Pre-conditions ────────────────────────────────────────────────────────────
40
+
41
+ require_project_status "$PROJECT_YAML" "active"
42
+
43
+ CURRENT_USER=$(git config user.email 2>/dev/null || echo "")
44
+ ASSIGNED_TO=$(yaml_get "$PROJECT_YAML" "assigned_to") # display/audit cache
45
+ GH_PROJECT=$(yaml_get "$PROJECT_YAML" "github_project")
46
+ is_authorized_for_project "$GH_PROJECT" "$ASSIGNED_TO" \
47
+ || hard_stop "Not authorized: '$CURRENT_USER' needs write access to the project's GitHub Project ($GH_PROJECT)."
48
+
49
+ # Check repo is not already in the project
50
+ python3 - "$PROJECT_YAML" "$REPO_URL" <<'PY'
51
+ import sys, yaml
52
+ c = yaml.safe_load(open(sys.argv[1]))
53
+ for r in (c.get('repos') or []):
54
+ if r and r.get('url') == sys.argv[2]:
55
+ print(f"Repo already in project: {sys.argv[2]}")
56
+ sys.exit(1)
57
+ PY
58
+
59
+ BRANCH=$(project_branch_for_id "$PROJECT_ID")
60
+ TODAY=$(today)
61
+
62
+ # Prompt for base_branch if not provided
63
+ if [[ -z "$BASE_BRANCH" ]]; then
64
+ printf " Base branch for '%s' [%s]: " "$REPO_URL" "$DEFAULT_CODE_BRANCH"
65
+ read -r input_base
66
+ BASE_BRANCH="${input_base:-$DEFAULT_CODE_BRANCH}"
67
+ fi
68
+
69
+ REPO_NAME=$(get_repo_name "$REPO_URL")
70
+ REPO_DIR="$AGENT_WORK_ROOT/$PROJECT_ID/$REPO_NAME"
71
+
72
+ # ── Clone and create branch ───────────────────────────────────────────────────
73
+
74
+ mkdir -p "$AGENT_WORK_ROOT/$PROJECT_ID"
75
+
76
+ if [[ -e "$REPO_DIR/.git" ]]; then
77
+ info "Already cloned — fetching..."
78
+ git -C "$REPO_DIR" fetch origin
79
+ else
80
+ info "Cloning $REPO_URL → $REPO_DIR..."
81
+ git clone "$REPO_URL" "$REPO_DIR" \
82
+ || hard_stop "Clone failed for $REPO_URL"
83
+ fi
84
+
85
+ git -C "$REPO_DIR" checkout "$BASE_BRANCH" \
86
+ || hard_stop "Base branch '$BASE_BRANCH' not found in $REPO_URL"
87
+ git -C "$REPO_DIR" pull origin "$BASE_BRANCH" 2>/dev/null || true
88
+
89
+ if git -C "$REPO_DIR" rev-parse --verify "$BRANCH" &>/dev/null; then
90
+ hard_stop "Branch '$BRANCH' already exists in $REPO_URL — investigate before proceeding."
91
+ fi
92
+
93
+ git -C "$REPO_DIR" checkout -b "$BRANCH"
94
+ git -C "$REPO_DIR" push -u origin "$BRANCH" \
95
+ || hard_stop "Failed to push '$BRANCH' to $REPO_URL"
96
+ info "Branch '$BRANCH' pushed to $REPO_URL"
97
+
98
+ # ── Update project.yaml repos[] ──────────────────────────────────────────────
99
+
100
+ python3 - "$PROJECT_YAML" "$REPO_URL" "$ROLE" "$BASE_BRANCH" "$TODAY" "$ADDED_REASON" <<'PY'
101
+ import sys, yaml
102
+ pf, url, role, base, today, reason = sys.argv[1:]
103
+ with open(pf) as f:
104
+ c = yaml.safe_load(f)
105
+ if not c.get('repos'):
106
+ c['repos'] = []
107
+ c['repos'].append({
108
+ 'url': url, 'role': role, 'base_branch': base,
109
+ 'added_at': today, 'added_reason': reason,
110
+ })
111
+ with open(pf, 'w') as f:
112
+ yaml.dump(c, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
113
+ PY
114
+
115
+ cd "$REPO_ROOT"
116
+ git checkout "$BRANCH"
117
+ git add "projects/$PROJECT_ID/project.yaml"
118
+ git commit -m "add-repo: $REPO_NAME to $PROJECT_ID"
119
+ git push origin "$BRANCH"
120
+
121
+ echo ""
122
+ echo "=== Repo added successfully!"
123
+ echo " Repo: $REPO_URL"
124
+ echo " Role: $ROLE"
125
+ echo " Base branch: $BASE_BRANCH"
126
+ echo " Local clone: $REPO_DIR"
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env bash
2
+ # Script: cancel
3
+ # Purpose: Cancels a project. Archives all branches. No knowledge close.
4
+ # Usage: bash cancel.sh <project_id> <cancellation_reason>
5
+ # Compliance: C01 for cancellation_reason requirement (POL-052, POL-070)
6
+
7
+ set -euo pipefail
8
+ source "$(dirname "$0")/lib.sh"
9
+ load_config
10
+
11
+ # ── Inputs ────────────────────────────────────────────────────────────────────
12
+
13
+ PROJECT_ID="${1:-}"
14
+ CANCELLATION_REASON="${2:-}"
15
+
16
+ [[ -n "$PROJECT_ID" ]] || hard_stop "Usage: $0 <project_id> <cancellation_reason>"
17
+ [[ -n "$CANCELLATION_REASON" ]] || hard_stop "cancellation_reason is required (C01)."
18
+
19
+ echo "=== cancel: $PROJECT_ID"
20
+ echo " Reason: $CANCELLATION_REASON"
21
+ echo ""
22
+
23
+ PROJECT_YAML=$(get_project_yaml "$PROJECT_ID")
24
+ check_project_exists "$PROJECT_ID"
25
+
26
+ # ── Pre-conditions ────────────────────────────────────────────────────────────
27
+
28
+ require_any_project_status "$PROJECT_YAML" "active" "paused"
29
+
30
+ # The person cancelling must be authorized on the project — assigned_to
31
+ # individual or a member of the assigned_to team (GitHub-Project write, POL-046).
32
+ # (#62/C11: the old 'locked_by' gate was dead — locked_by is never written by
33
+ # any script, so the guard short-circuited on empty → any user could cancel any
34
+ # project. Mirror the standard authz used by create-task.sh / seed.sh.)
35
+ CURRENT_USER=$(git config user.email 2>/dev/null || echo "")
36
+ ASSIGNED_TO=$(yaml_get "$PROJECT_YAML" "assigned_to") # display/audit cache
37
+ GH_PROJECT=$(yaml_get "$PROJECT_YAML" "github_project")
38
+ is_authorized_for_project "$GH_PROJECT" "$ASSIGNED_TO" \
39
+ || hard_stop "You ($CURRENT_USER) are not authorized on this project — you need write access to its GitHub Project ($GH_PROJECT)."
40
+
41
+ BRANCH=$(project_branch_for_id "$PROJECT_ID")
42
+
43
+ confirm "Cancelling '$PROJECT_ID' is irreversible (branches archived, not merged). Continue?"
44
+
45
+ # ── Archive all branches (continue-on-error, idempotent) ──────────────────────
46
+ # #64/H5: archiving is per-repo and must NOT leave inconsistent state. Each repo
47
+ # is handled independently: if the 'archive/<branch>' tag already exists the repo
48
+ # is treated as already-archived and skipped, so a re-run after a partial failure
49
+ # completes cleanly instead of hard-stopping on "tag exists". Failures are
50
+ # collected (never abort the loop) so every repo is attempted; the status flip
51
+ # below is gated on whether everything that was attempted actually succeeded.
52
+
53
+ ARCHIVE_FAILURES=() # human-readable label of each repo that did not archive
54
+
55
+ # Idempotent wrapper around archive_branch: skips when the archive tag already
56
+ # exists (local or remote), and converts a hard_stop into a recorded failure so
57
+ # the loop can continue. $1=repo label (for summary), $2=git dir, $3=branch.
58
+ archive_branch_safe() {
59
+ local label="$1" dir="$2" branch="$3" tag="archive/$branch"
60
+ if git -C "$dir" rev-parse --verify "refs/tags/$tag" &>/dev/null \
61
+ || git -C "$dir" ls-remote --exit-code --tags origin "$tag" &>/dev/null; then
62
+ info "Archive tag '$tag' already exists in $label — skipping (idempotent)."
63
+ git -C "$dir" branch -D "$branch" 2>/dev/null || true
64
+ return 0
65
+ fi
66
+ # archive_branch hard_stops (exit) on failure; run it in a subshell so a
67
+ # failure becomes a non-zero status we can record rather than aborting cancel.
68
+ if ( archive_branch "$dir" "$branch" ); then
69
+ return 0
70
+ fi
71
+ warn "Failed to archive branch '$branch' in $label — recorded; continuing."
72
+ ARCHIVE_FAILURES+=("$label")
73
+ return 1
74
+ }
75
+
76
+ # ── Archive workspace branch ──────────────────────────────────────────────────
77
+
78
+ echo "Archiving workspace branch..."
79
+ cd "$REPO_ROOT"
80
+ git fetch origin "$BRANCH" 2>/dev/null || true
81
+ if git rev-parse --verify "$BRANCH" &>/dev/null 2>&1 || git ls-remote --exit-code origin "$BRANCH" &>/dev/null; then
82
+ archive_branch_safe "workspace repo" "$REPO_ROOT" "$BRANCH" || true
83
+ else
84
+ warn "Branch '$BRANCH' not found in workspace repo — skipping archive."
85
+ fi
86
+
87
+ # ── Archive each code repo branch ────────────────────────────────────────────
88
+
89
+ while IFS= read -r repo_url; do
90
+ REPO_NAME=$(get_repo_name "$repo_url")
91
+ REPO_DIR="$(repo_clone_dir "$PROJECT_ID" "$REPO_NAME")"
92
+ if [[ ! -e "$REPO_DIR/.git" ]]; then
93
+ warn "Repo $REPO_NAME not cloned locally — archiving via remote only."
94
+ TMP_DIR=$(mktemp -d)
95
+ if git clone --branch "$BRANCH" --single-branch "$repo_url" "$TMP_DIR" 2>/dev/null; then
96
+ archive_branch_safe "$REPO_NAME" "$TMP_DIR" "$BRANCH" || true
97
+ elif git ls-remote --exit-code --tags "$repo_url" "archive/$BRANCH" &>/dev/null; then
98
+ info "Archive tag 'archive/$BRANCH' already exists in $REPO_NAME — skipping (idempotent)."
99
+ else
100
+ warn "Could not archive branch '$BRANCH' in $repo_url — recorded; continuing."
101
+ ARCHIVE_FAILURES+=("$REPO_NAME")
102
+ fi
103
+ rm -rf "$TMP_DIR"
104
+ continue
105
+ fi
106
+ git -C "$REPO_DIR" fetch origin "$BRANCH" 2>/dev/null || true
107
+ if git -C "$REPO_DIR" rev-parse --verify "$BRANCH" &>/dev/null 2>&1 \
108
+ || git -C "$REPO_DIR" rev-parse --verify "refs/tags/archive/$BRANCH" &>/dev/null \
109
+ || git -C "$REPO_DIR" ls-remote --exit-code --tags origin "archive/$BRANCH" &>/dev/null; then
110
+ archive_branch_safe "$REPO_NAME" "$REPO_DIR" "$BRANCH" || true
111
+ else
112
+ warn "Branch '$BRANCH' not found in $REPO_NAME — skipping archive."
113
+ fi
114
+ done < <(get_project_repos "$PROJECT_YAML")
115
+
116
+ # ── Gate the status flip on a clean archive ───────────────────────────────────
117
+ # H5: only flip status to 'cancelled' if every repo we attempted archived
118
+ # successfully. If any failed, stop BEFORE mutating status so the project stays
119
+ # active/paused and the operator can re-run — the re-run is idempotent (already-
120
+ # archived repos are skipped) and will complete the remaining repos.
121
+ if [[ ${#ARCHIVE_FAILURES[@]} -gt 0 ]]; then
122
+ hard_stop "Archive incomplete for: ${ARCHIVE_FAILURES[*]}. Status NOT changed — re-run cancel after resolving (already-archived repos will be skipped)."
123
+ fi
124
+
125
+ # ── Record cancellation ──────────────────────────────────────────────────────
126
+ # project.yaml status is recorded on the project branch (preserved in the
127
+ # archive tag). The registry index entry is flipped to 'cancelled' on
128
+ # $DEFAULT_BRANCH, where it lives (authored at seed).
129
+
130
+ TODAY=$(today)
131
+ cd "$REPO_ROOT"
132
+ if [[ -f "$PROJECT_YAML" ]]; then
133
+ yaml_set "$PROJECT_YAML" "status" "cancelled"
134
+ yaml_set "$PROJECT_YAML" "cancelled_at" "$TODAY"
135
+ yaml_set "$PROJECT_YAML" "cancellation_reason" "$CANCELLATION_REASON"
136
+ git add "projects/$PROJECT_ID/project.yaml"
137
+ if ! git diff --cached --quiet; then
138
+ git commit -m "cancel: $PROJECT_ID — $CANCELLATION_REASON"
139
+ git push origin "$BRANCH" 2>/dev/null || true
140
+ fi
141
+ fi
142
+
143
+ registry_set_status_on_main "$PROJECT_ID" "cancelled"
144
+ project_readme_mirror "$PROJECT_ID" "$(yaml_get "$PROJECT_YAML" github_project 2>/dev/null)" "cancelled" \
145
+ "$(yaml_get "$PROJECT_YAML" assigned_to 2>/dev/null)" "$(yaml_get "$PROJECT_YAML" seeded_by 2>/dev/null)" "$BRANCH" || true
146
+
147
+ # Close the GitHub Project board so a cancelled project stops reading as active (#56 Facet A).
148
+ close_project_board "$GH_PROJECT"
149
+
150
+ echo ""
151
+ echo "=== Project cancelled."
152
+ echo " Status: cancelled"
153
+ echo " cancelled_at: $TODAY"
154
+ echo " cancellation_reason: $CANCELLATION_REASON"
155
+ echo ""
156
+ echo " All code changes are preserved in archive tags (archive/$BRANCH)."
157
+ echo " No knowledge close was run."
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env bash
2
+ # Script: close-knowledge
3
+ # Purpose: Synthesizes project knowledge into org knowledge proposals using LLM+RAG.
4
+ # Raises PR for domain owner review.
5
+ # Usage: bash close-knowledge.sh <project_id>
6
+ # Triggered by: close-project automatically after successful project close.
7
+ # Compliance: C02 (POL-097 to POL-106)
8
+ #
9
+ # LLM synthesis note:
10
+ # This script prepares the branch and context, then invokes the agent (Claude Code)
11
+ # to perform synthesis. The agent reads all project knowledge, queries relevant org
12
+ # knowledge, and proposes changes via the knowledge-close branch.
13
+ # If the agent is not available, the script falls back to creating the PR with raw
14
+ # project knowledge attached for manual review.
15
+
16
+ set -euo pipefail
17
+ source "$(dirname "$0")/lib.sh"
18
+ load_config
19
+
20
+ # ── Inputs ────────────────────────────────────────────────────────────────────
21
+
22
+ PROJECT_ID="${1:-}"
23
+ [[ -n "$PROJECT_ID" ]] || hard_stop "Usage: $0 <project_id>"
24
+
25
+ echo "=== close-knowledge: $PROJECT_ID"
26
+ echo ""
27
+
28
+ PROJECT_YAML=$(get_project_yaml "$PROJECT_ID")
29
+ PROJECT_DIR=$(get_project_dir "$PROJECT_ID")
30
+ check_project_exists "$PROJECT_ID"
31
+
32
+ # ── Pre-conditions ────────────────────────────────────────────────────────────
33
+
34
+ require_project_status "$PROJECT_YAML" "completed"
35
+
36
+ # The person closing knowledge must be authorized on the project — assigned_to
37
+ # individual or a member of the assigned_to team (per-task/team model, POL-047).
38
+ # Mirrors create-task.sh; closes the inconsistent-authz gap (#62/H9).
39
+ CURRENT_USER=$(git config user.email 2>/dev/null || echo "")
40
+ ASSIGNED_TO=$(yaml_get "$PROJECT_YAML" "assigned_to") # display/audit cache
41
+ GH_PROJECT=$(yaml_get "$PROJECT_YAML" "github_project")
42
+ is_authorized_for_project "$GH_PROJECT" "$ASSIGNED_TO" \
43
+ || hard_stop "You ($CURRENT_USER) are not authorized on this project — you need write access to its GitHub Project ($GH_PROJECT)."
44
+
45
+ KNOWLEDGE_DIR="$PROJECT_DIR/knowledge"
46
+ if [[ ! -d "$KNOWLEDGE_DIR" ]] || [[ -z "$(find "$KNOWLEDGE_DIR" -type f 2>/dev/null)" ]]; then
47
+ hard_stop "projects/$PROJECT_ID/knowledge/ is empty — nothing to synthesize."
48
+ fi
49
+
50
+ BRANCH=$(project_branch_for_id "$PROJECT_ID")
51
+ KNOWLEDGE_BRANCH="${BRANCH}-knowledge"
52
+ TODAY=$(today)
53
+
54
+ # ── Failure cleanup (#64) ─────────────────────────────────────────────────────
55
+ # On any failure, undo the branch/temp-file this run created so a failed run
56
+ # leaves no orphan branch or context file and is re-runnable. State flags are
57
+ # flipped as each resource is created; _CK_DONE disarms the trap on success.
58
+ KNOWLEDGE_SUMMARY_FILE="$REPO_ROOT/.close-knowledge-context-$PROJECT_ID.md"
59
+ _CK_BRANCH_CREATED=false
60
+ _CK_BRANCH_PUSHED=false
61
+ _CK_DONE=false
62
+
63
+ cleanup_on_failure() {
64
+ local rc=$?
65
+ $_CK_DONE && return 0
66
+ [[ $rc -eq 0 ]] && return 0
67
+ warn "close-knowledge failed (exit $rc) — cleaning up so the run is re-runnable."
68
+ rm -f "$KNOWLEDGE_SUMMARY_FILE" 2>/dev/null || true
69
+ if $_CK_BRANCH_CREATED; then
70
+ git -C "$REPO_ROOT" checkout "$DEFAULT_BRANCH" &>/dev/null || true
71
+ local scope="local"
72
+ if $_CK_BRANCH_PUSHED; then
73
+ git -C "$REPO_ROOT" push origin --delete "$KNOWLEDGE_BRANCH" &>/dev/null || true
74
+ scope="local + remote"
75
+ fi
76
+ git -C "$REPO_ROOT" branch -D "$KNOWLEDGE_BRANCH" &>/dev/null || true
77
+ info "Removed knowledge branch '$KNOWLEDGE_BRANCH' ($scope)."
78
+ fi
79
+ }
80
+ trap cleanup_on_failure EXIT
81
+
82
+ # ── Create knowledge branch ───────────────────────────────────────────────────
83
+
84
+ echo "Creating knowledge branch '$KNOWLEDGE_BRANCH'..."
85
+ cd "$REPO_ROOT"
86
+ git fetch origin "$DEFAULT_BRANCH"
87
+ git checkout "$DEFAULT_BRANCH"
88
+ git pull origin "$DEFAULT_BRANCH"
89
+
90
+ if git rev-parse --verify "$KNOWLEDGE_BRANCH" &>/dev/null; then
91
+ hard_stop "Branch '$KNOWLEDGE_BRANCH' already exists — investigate before proceeding."
92
+ fi
93
+ git checkout -b "$KNOWLEDGE_BRANCH"
94
+ _CK_BRANCH_CREATED=true
95
+
96
+ # ── Collect project knowledge ─────────────────────────────────────────────────
97
+
98
+ echo "Collecting project knowledge from $KNOWLEDGE_DIR..."
99
+
100
+ {
101
+ echo "# Knowledge Close Context: $PROJECT_ID"
102
+ echo ""
103
+ echo "**Project:** $PROJECT_ID"
104
+ echo "**Closed:** $TODAY"
105
+ echo "**Knowledge dir:** projects/$PROJECT_ID/knowledge/"
106
+ echo ""
107
+ echo "---"
108
+ echo ""
109
+ echo "## Project Knowledge Files"
110
+ echo ""
111
+ find "$KNOWLEDGE_DIR" -type f | sort | while IFS= read -r f; do
112
+ rel="${f#$REPO_ROOT/}"
113
+ echo "### $rel"
114
+ echo ""
115
+ cat "$f"
116
+ echo ""
117
+ echo "---"
118
+ echo ""
119
+ done
120
+ } > "$KNOWLEDGE_SUMMARY_FILE"
121
+
122
+ info "Context written to: $KNOWLEDGE_SUMMARY_FILE"
123
+
124
+ # ── LLM synthesis step ────────────────────────────────────────────────────────
125
+ #
126
+ # The agent (Claude Code) running this script MUST follow the Knowledge Harvest
127
+ # Protocol — knowledge/development/procedures/knowledge-harvest.md (POL-413, C01):
128
+ # 1. Reconstruct from EVIDENCE (git log -p across project repos, merged issues,
129
+ # todo.md, all projects/$PROJECT_ID/knowledge/ docs) — not from memory.
130
+ # 2. Enumerate → classify (graduate/local/discard) every durable artifact; mine
131
+ # the non-obvious (gotchas, failures-and-fixes); journey review;
132
+ # completeness-critic pass.
133
+ # 3. Write the manifest projects/$PROJECT_ID/knowledge/knowledge-close.md
134
+ # (template in the protocol). close-project's gate checks it is present +
135
+ # structurally complete (no TBD).
136
+ # 4. Apply proposed org-knowledge changes to knowledge/ on this branch.
137
+ # 5. Call: bash close-knowledge.sh <project_id> --finalize <pr_description_file>
138
+ #
139
+ # If the agent is not available, we fall back to attaching raw knowledge.
140
+
141
+ if [[ "${2:-}" == "--finalize" ]]; then
142
+ # Phase 2: agent has done synthesis and calls us back to create the PR
143
+ PR_DESC_FILE="${3:-}"
144
+ [[ -n "$PR_DESC_FILE" && -f "$PR_DESC_FILE" ]] \
145
+ || hard_stop "--finalize requires a PR description file as argument 3."
146
+ PR_BODY=$(cat "$PR_DESC_FILE")
147
+ _finalize_mode=true
148
+ else
149
+ # Phase 1: no agent synthesis — fall back to attaching raw knowledge for manual review
150
+ warn "LLM synthesis not performed — attaching raw project knowledge for manual review."
151
+ PR_BODY=$(cat <<MD
152
+ ## Knowledge Close: $PROJECT_ID
153
+
154
+ **Automated synthesis was not performed.** This PR attaches the raw project knowledge
155
+ for manual review by domain owners.
156
+
157
+ ### Project Knowledge
158
+
159
+ See \`projects/$PROJECT_ID/knowledge/\` in this branch for all captured learnings.
160
+
161
+ ### Review Instructions
162
+
163
+ Domain owners: please review the project knowledge and manually apply relevant
164
+ learnings to the appropriate \`knowledge/\` subfolders in this PR.
165
+
166
+ *Generated by close-knowledge.sh — fallback mode (no LLM synthesis)*
167
+ MD
168
+ )
169
+ _finalize_mode=false
170
+ fi
171
+
172
+ # ── Commit knowledge changes to branch ───────────────────────────────────────
173
+
174
+ # Remove temp context file
175
+ rm -f "$KNOWLEDGE_SUMMARY_FILE"
176
+
177
+ # Stage any knowledge/ changes the agent may have made
178
+ git add "framework/knowledge/" 2>/dev/null || true
179
+
180
+ # If no changes were staged (fallback mode), there is nothing new to commit;
181
+ # the PR will just carry the branch with existing org knowledge as baseline.
182
+ if git diff --cached --quiet; then
183
+ info "No knowledge/ changes staged — PR will describe manual review needed."
184
+ # Create a placeholder note so the branch has at least one commit
185
+ mkdir -p "$REPO_ROOT/framework/knowledge/accumulated"
186
+ cat >> "$REPO_ROOT/framework/knowledge/accumulated/README.md" <<NOTE
187
+
188
+ <!-- close-knowledge: $PROJECT_ID — manual review needed ($TODAY) -->
189
+ NOTE
190
+ git add "framework/knowledge/accumulated/README.md"
191
+ fi
192
+
193
+ git commit -m "close-knowledge: $PROJECT_ID" --allow-empty
194
+ git push -u origin "$KNOWLEDGE_BRANCH"
195
+ _CK_BRANCH_PUSHED=true
196
+
197
+ # ── Raise PR ──────────────────────────────────────────────────────────────────
198
+
199
+ echo "Raising PR: $KNOWLEDGE_BRANCH → $DEFAULT_BRANCH..."
200
+
201
+ PR_URL=$(gh pr create \
202
+ --base "$DEFAULT_BRANCH" \
203
+ --head "$KNOWLEDGE_BRANCH" \
204
+ --title "[Knowledge Close] $PROJECT_ID" \
205
+ --body "$PR_BODY" \
206
+ 2>/dev/null) \
207
+ || {
208
+ warn "PR creation failed — retrying..."
209
+ PR_URL=$(gh pr create \
210
+ --base "$DEFAULT_BRANCH" \
211
+ --head "$KNOWLEDGE_BRANCH" \
212
+ --title "[Knowledge Close] $PROJECT_ID" \
213
+ --body "$PR_BODY")
214
+ }
215
+
216
+ info "PR created: $PR_URL"
217
+
218
+ # Point of no return: the branch is now referenced by a PR, so deleting it on a
219
+ # later failure would orphan the PR. Disarm the branch/temp-file cleanup but
220
+ # still drop the temp context file (which is removed earlier anyway).
221
+ _CK_DONE=true
222
+ rm -f "$KNOWLEDGE_SUMMARY_FILE" 2>/dev/null || true
223
+
224
+ # ── Update project.yaml ───────────────────────────────────────────────────────
225
+
226
+ cd "$REPO_ROOT"
227
+ git checkout "$DEFAULT_BRANCH"
228
+ git pull origin "$DEFAULT_BRANCH"
229
+
230
+ yaml_set "$PROJECT_YAML" "knowledge_status" "pending_review"
231
+ yaml_set "$PROJECT_YAML" "knowledge_pr" "$PR_URL"
232
+
233
+ git add "projects/$PROJECT_ID/project.yaml"
234
+ git commit -m "close-knowledge: update knowledge_status for $PROJECT_ID"
235
+
236
+ # Pre-push validation gate (rolls back commit if validators fail)
237
+ validate_or_revert
238
+
239
+ git push origin "$DEFAULT_BRANCH"
240
+
241
+ echo ""
242
+ echo "=== Knowledge close initiated."
243
+ echo " Branch: $KNOWLEDGE_BRANCH"
244
+ echo " PR: $PR_URL"
245
+ echo " knowledge_status: pending_review"
246
+ echo ""
247
+ echo " CODEOWNERS will auto-assign domain reviewers."
248
+ echo " Outcome updates:"
249
+ echo " Merged → archive tag + delete branch, knowledge_status: merged"
250
+ echo " Rejected → owner closes PR, knowledge_status: rejected"