@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
|
@@ -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"
|