guardian-framework 0.1.70 → 0.1.72

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/dist/cli.js CHANGED
@@ -3197,7 +3197,7 @@ __export(exports_package, {
3197
3197
  bin: () => bin,
3198
3198
  author: () => author
3199
3199
  });
3200
- var name = "guardian-framework", version = "0.1.70", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
3200
+ var name = "guardian-framework", version = "0.1.72", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
3201
3201
  var init_package = __esm(() => {
3202
3202
  exports = {
3203
3203
  ".": {
package/dist/exports.js CHANGED
@@ -1982,7 +1982,7 @@ __export(exports_package, {
1982
1982
  bin: () => bin,
1983
1983
  author: () => author
1984
1984
  });
1985
- var name = "guardian-framework", version = "0.1.70", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
1985
+ var name = "guardian-framework", version = "0.1.72", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
1986
1986
  var init_package = __esm(() => {
1987
1987
  exports = {
1988
1988
  ".": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardian-framework",
3
- "version": "0.1.70",
3
+ "version": "0.1.72",
4
4
  "description": "Token-optimized agentic framework scaffolder with pi-first architecture",
5
5
  "type": "module",
6
6
  "main": "dist/exports.js",
@@ -7,11 +7,16 @@ export function generateIssueMarkdown(
7
7
  slice: ArchitectureSlice,
8
8
  issueIndex: number,
9
9
  totalIssues: number,
10
+ tdd?: boolean,
10
11
  ): string {
11
12
  const moduleId = slice.module.replace(/^module-/, "");
12
13
  const componentName = component.name.toLowerCase().replace(/\s+/g, "-");
13
14
  const issueId = `ISSUE-${moduleId.toUpperCase()}-${issueIndex + 1}`;
14
15
 
16
+ const testLine = tdd
17
+ ? ` - "update: tests/unit/${moduleId}/${componentName}/ (failing tests already generated — make them pass)"`
18
+ : ` - "create: tests/unit/${moduleId}/${componentName}/"`;
19
+
15
20
  return `---
16
21
  guardian_issue:
17
22
  id: "${issueId}"
@@ -70,7 +75,7 @@ ${component.dependencies.map((d) => ` - "${d}"`).join("\n")}
70
75
  - "modify: src/${moduleId}/application/"
71
76
  - "modify: src/${moduleId}/infrastructure/"
72
77
  - "modify: src/${moduleId}/interfaces/"
73
- - "create: tests/unit/${moduleId}/${componentName}/"
78
+ - ${testLine}
74
79
  - "create: tests/integration/${moduleId}/${componentName}/"
75
80
  ---
76
81
 
@@ -161,8 +161,16 @@ export function ensureRemoteRepo(
161
161
  `glab repo create "${repository}" --private --description "Epic: ${epicName}" 2>&1`,
162
162
  );
163
163
  if (createResult.exitCode !== 0) {
164
- console.warn(`Failed to create GitLab project: ${createResult.stdout}`);
165
- return "";
164
+ // Repo may already exist — verify via API before giving up
165
+ const viewResult = runScript(
166
+ cwd,
167
+ `glab api "projects/${repository}" --method GET 2>/dev/null | jq -r '.id // empty'`,
168
+ );
169
+ if (viewResult.exitCode !== 0 || !viewResult.stdout.trim()) {
170
+ console.warn(`Failed to create or find GitLab project: ${createResult.stdout}`);
171
+ return "";
172
+ }
173
+ console.warn(`GitLab project already exists, connecting to it.`);
166
174
  }
167
175
  runScript(cwd, "git remote remove origin 2>/dev/null");
168
176
  const httpsUrl = `${glabBaseUrl}/${repository}.git`;
@@ -147,6 +147,14 @@ class EpicManager {
147
147
 
148
148
  ctx.ui.setStatus("architect", `Planning epic: ${name}`);
149
149
 
150
+ // Ensure git repo is initialized before any remote operations
151
+ const gitCheck = runScript(this.cwd, "git rev-parse --git-dir 2>/dev/null");
152
+ if (gitCheck.exitCode !== 0) {
153
+ runScript(this.cwd, "git init");
154
+ runScript(this.cwd, "git add .");
155
+ runScript(this.cwd, 'git commit -m "Initial Guardian scaffold"');
156
+ }
157
+
150
158
  const repoTool = readRepoTool(this.cwd);
151
159
  const repository = readRepository(this.cwd);
152
160
  const targetRepo = repository || slice.module;
@@ -159,18 +167,11 @@ class EpicManager {
159
167
  repoTool === "glab" ? "glab auth status 2>/dev/null" : "gh auth status 2>/dev/null",
160
168
  );
161
169
  if (authCheck.exitCode === 0) {
162
- // Verify project-level access, not just global auth
163
- const projectAccessCheck = repository ? runScript(
164
- this.cwd,
165
- repoTool === "glab"
166
- ? `glab api "projects/${repository}" --method GET 2>/dev/null | jq -r '.id // empty'`
167
- : `gh api "repos/${repository}" --method GET 2>/dev/null | jq -r '.id // empty'`,
168
- ) : { exitCode: 0, stdout: "" };
169
-
170
- if (projectAccessCheck.exitCode === 0 && projectAccessCheck.stdout.trim().length > 0) {
171
- remoteRepo = ensureRemoteRepo(this.cwd, targetRepo, name, repoTool);
172
- hasRemote = remoteRepo.length > 0;
173
- }
170
+ // Try to ensure remote repo — ensureRemoteRepo handles both:
171
+ // - existing repos: configures local origin
172
+ // - nonexistent repos: creates remote repo via gh/glab CLI
173
+ remoteRepo = ensureRemoteRepo(this.cwd, targetRepo, name, repoTool);
174
+ hasRemote = remoteRepo.length > 0;
174
175
  }
175
176
  }
176
177
 
@@ -250,7 +251,7 @@ class EpicManager {
250
251
  status: "planned" as string,
251
252
  remoteIssueId: null as string | null,
252
253
  };
253
- const md = generateIssueMarkdown(comp, slice, i, slice.nextLogicalSlice.length);
254
+ const md = generateIssueMarkdown(comp, slice, i, slice.nextLogicalSlice.length, tdd);
254
255
  writeFileSync(join(issuesDir, `${id}.md`), md);
255
256
  if (hasRemote && remoteRepo) {
256
257
  const result = createRemoteIssue(this.cwd, entry.title, join(issuesDir, `${id}.md`), "epic,implementation", remoteRepo);
@@ -757,15 +758,7 @@ export default function (pi: ExtensionAPI) {
757
758
  return;
758
759
  }
759
760
 
760
- // Initialize git if needed
761
- try {
762
- const gitCheck = runScript(ctx.cwd, "git rev-parse --git-dir 2>/dev/null");
763
- if (gitCheck.exitCode !== 0) {
764
- runScript(ctx.cwd, "git init");
765
- runScript(ctx.cwd, "git add .");
766
- runScript(ctx.cwd, 'git commit -m "Initial Guardian scaffold"');
767
- }
768
- } catch { /* ignore */ }
761
+
769
762
 
770
763
  // Remove stale pipeline state so the new one takes effect
771
764
  try {
@@ -20,7 +20,7 @@
20
20
  * /pipeline abort Kill pipeline
21
21
  */
22
22
 
23
- import { execSync } from "node:child_process";
23
+ import { execFileSync, execSync } from "node:child_process";
24
24
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
25
25
  import { dirname, join } from "node:path";
26
26
  import {
@@ -863,14 +863,71 @@ export default function (pi: ExtensionAPI) {
863
863
  const prevItemIndex = state.currentItemIndex;
864
864
  const prevStepIndex = state.currentStepIndex;
865
865
  const stepName = (params.stepName as string) || state.steps[prevStepIndex]?.name;
866
+
867
+ // Verify current step before advancing (unless user explicitly provides the step name,
868
+ // which counts as manual confirmation)
869
+ if (!params.stepName) {
870
+ const verification = manager.verifyCurrentStep();
871
+ if (!verification.verified) {
872
+ return {
873
+ content: [{ type: "text" as const, text: `Cannot advance: ${verification.reason}` }],
874
+ };
875
+ }
876
+ }
877
+
866
878
  manager.markStepPassed(stepName);
867
879
  manager.advanceStep();
868
880
 
869
881
  // Re-read state after advance
870
882
  const updatedState = manager.getState()!;
871
883
 
884
+ // Non-blocking: sync progress to tracking issue if one exists
885
+ try {
886
+ const trackingStatePath = join(ctx.cwd, ".pi/.guardian-epic-state.json");
887
+ if (existsSync(trackingStatePath)) {
888
+ const trackingState = JSON.parse(readFileSync(trackingStatePath, "utf-8")) as {
889
+ trackingIssueId?: string | null;
890
+ issues?: { id: string; title: string; status: string }[];
891
+ };
892
+ if (trackingState.trackingIssueId && !trackingState.trackingIssueId.startsWith("local:")) {
893
+ const stepNote = stepName
894
+ ? `✓ Step "${stepName}" complete for item "${updatedState.items[prevItemIndex]}"`
895
+ : `✓ Advanced to item ${updatedState.currentItemIndex + 1}/${updatedState.items.length}: "${updatedState.items[updatedState.currentItemIndex]}"`;
896
+ const updateScript = join(ctx.cwd, ".pi/scripts/git/update-tracking-issue.sh");
897
+ if (existsSync(updateScript)) {
898
+ execFileSync("bash", [updateScript, "--id", trackingState.trackingIssueId, "--comment", stepNote], {
899
+ cwd: ctx.cwd,
900
+ timeout: 30_000,
901
+ encoding: "utf-8",
902
+ stdio: "ignore",
903
+ });
904
+ }
905
+ }
906
+ }
907
+ } catch { /* non-blocking — don't fail the pipeline */ }
908
+
872
909
  // Pipeline complete
873
910
  if (updatedState.currentItemIndex >= updatedState.items.length) {
911
+ // Non-blocking: close tracking issue and epic on pipeline completion
912
+ try {
913
+ const epicStatePath = join(ctx.cwd, ".pi/.guardian-epic-state.json");
914
+ if (existsSync(epicStatePath)) {
915
+ const epicState = JSON.parse(readFileSync(epicStatePath, "utf-8")) as {
916
+ trackingIssueId?: string | null;
917
+ epicId?: string | null;
918
+ };
919
+ if (epicState.trackingIssueId && !epicState.trackingIssueId.startsWith("local:")) {
920
+ const closeEpicScript = join(ctx.cwd, ".pi/scripts/git/close-epic.sh");
921
+ if (existsSync(closeEpicScript)) {
922
+ const args: string[] = [closeEpicScript, "--tracking-id", epicState.trackingIssueId];
923
+ if (epicState.epicId) args.push("--epic-id", epicState.epicId);
924
+ execFileSync("bash", args, {
925
+ cwd: ctx.cwd, timeout: 30_000, encoding: "utf-8", stdio: "ignore",
926
+ });
927
+ }
928
+ }
929
+ }
930
+ } catch { /* non-blocking */ }
874
931
  return {
875
932
  content: [{
876
933
  type: "text" as const,
@@ -884,6 +941,30 @@ export default function (pi: ExtensionAPI) {
884
941
  const movedToNextItem = updatedState.currentItemIndex !== prevItemIndex;
885
942
 
886
943
  // If we moved to a new item (completed all steps of previous item),
944
+ // close its remote issue
945
+ if (movedToNextItem) {
946
+ // Close remote issue for the completed item
947
+ try {
948
+ const epicStatePath = join(ctx.cwd, ".pi/.guardian-epic-state.json");
949
+ if (existsSync(epicStatePath)) {
950
+ const epicState = JSON.parse(readFileSync(epicStatePath, "utf-8")) as {
951
+ issues?: { id: string; remoteIssueId?: string | null }[];
952
+ };
953
+ const prevItemId = updatedState.items[prevItemIndex];
954
+ const prevIssue = epicState.issues?.find((i) => i.id === prevItemId);
955
+ if (prevIssue?.remoteIssueId) {
956
+ const closeScript = join(ctx.cwd, ".pi/scripts/git/close-issue.sh");
957
+ if (existsSync(closeScript)) {
958
+ execFileSync("bash", [closeScript, "--id", prevIssue.remoteIssueId], {
959
+ cwd: ctx.cwd, timeout: 30_000, encoding: "utf-8", stdio: "ignore",
960
+ });
961
+ }
962
+ }
963
+ }
964
+ } catch { /* non-blocking */ }
965
+ }
966
+
967
+ // If we moved to a new item and the next step is implement,
887
968
  // inject the full next-task prompt with issue context
888
969
  if (movedToNextItem && currentStep?.name === "implement") {
889
970
  // Find the remote issue ID from epic state
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-07-13T11:07:34Z",
2
+ "timestamp": "2026-07-13T11:41:49Z",
3
3
  "mode": "all",
4
4
  "stages_run": [],
5
5
  "summary": {
@@ -56,7 +56,7 @@ if [[ "$PLATFORM" == "none" ]]; then
56
56
  fi
57
57
 
58
58
  # Determine branch name from issue ID
59
- BRANCH_NAME="feature/${ISSUE_ID}"
59
+ BRANCH_NAME="feat/${ISSUE_ID}"
60
60
 
61
61
  # Ensure we're on the main branch before creating feature branch
62
62
  git checkout -b "$BRANCH_NAME" 2>/dev/null || git checkout "$BRANCH_NAME" 2>/dev/null || true
@@ -6,7 +6,8 @@
6
6
  # Usage: bash .pi/scripts/git/close-epic.sh \
7
7
  # --epic-id 101 \
8
8
  # --tracking-id 100 \
9
- # --comment "Epic complete. All 5 issues done."
9
+ # --comment "Epic complete. All 5 issues done." \
10
+ # [--repo "group/project"]
10
11
 
11
12
  set -euo pipefail
12
13
 
@@ -22,44 +23,63 @@ detect_platform() {
22
23
  else echo "none"; fi
23
24
  }
24
25
 
26
+ read_repository() {
27
+ if [ -f "guardian-manifest.json" ]; then
28
+ jq -r '.repository // (.templateContext.repository // "")' guardian-manifest.json 2>/dev/null || echo ""
29
+ else
30
+ echo ""
31
+ fi
32
+ }
33
+
25
34
  EPIC_ID=""
26
35
  TRACKING_ID=""
27
36
  COMMENT="Epic complete."
37
+ REPO=""
28
38
 
29
39
  while [[ $# -gt 0 ]]; do
30
40
  case $1 in
31
41
  --epic-id) EPIC_ID="$2"; shift 2 ;;
32
42
  --tracking-id) TRACKING_ID="$2"; shift 2 ;;
33
43
  --comment) COMMENT="$2"; shift 2 ;;
44
+ --repo) REPO="$2"; shift 2 ;;
34
45
  *) shift ;;
35
46
  esac
36
47
  done
37
48
 
38
49
  PLATFORM=$(detect_platform)
50
+ [[ -z "$REPO" ]] && REPO=$(read_repository)
39
51
 
40
52
  if [[ -z "$EPIC_ID" ]]; then
41
- echo "Usage: $0 --epic-id <epic_number> [--tracking-id <tracking_id>] [--comment <comment>]"
53
+ echo "Usage: $0 --epic-id <epic_number> [--tracking-id <tracking_id>] [--comment <comment>] [--repo <repo>]"
42
54
  exit 1
43
55
  fi
44
56
 
45
57
  # Close tracking issue first
46
58
  if [[ -n "$TRACKING_ID" ]]; then
47
- bash "$(dirname "$0")/close-issue.sh" --id "$TRACKING_ID"
59
+ bash "$(dirname "$0")/close-issue.sh" --id "$TRACKING_ID" ${REPO:+--repo "$REPO"}
48
60
  fi
49
61
 
50
62
  # Close the epic
51
63
  case "$PLATFORM" in
52
64
  github)
53
- gh issue close "$EPIC_ID" 2>/dev/null
65
+ gh issue close "$EPIC_ID" ${REPO:+--repo "$REPO"} 2>/dev/null
54
66
  if [[ -n "$COMMENT" ]]; then
55
- gh issue comment "$EPIC_ID" --body "$COMMENT" 2>/dev/null
67
+ gh issue comment "$EPIC_ID" ${REPO:+--repo "$REPO"} --body "$COMMENT" 2>/dev/null
56
68
  fi
57
69
  echo "Closed GitHub epic #$EPIC_ID"
58
70
  ;;
59
71
  gitlab)
60
- glab issue update "$EPIC_ID" --state-event close 2>/dev/null
72
+ # If EPIC_ID looks numeric, use the GitLab Epic API
73
+ if [[ "$EPIC_ID" =~ ^[0-9]+$ ]]; then
74
+ # Try epic close first, fallback to issue close
75
+ glab api --method PUT "epics/$EPIC_ID" --body '{"state_event":"close"}' 2>/dev/null && \
76
+ echo "Closed GitLab epic #$EPIC_ID" || \
77
+ glab issue update "$EPIC_ID" --state-event close ${REPO:+--repo "$REPO"} 2>/dev/null
78
+ else
79
+ glab issue update "$EPIC_ID" --state-event close ${REPO:+--repo "$REPO"} 2>/dev/null
80
+ fi
61
81
  if [[ -n "$COMMENT" ]]; then
62
- glab issue note "$EPIC_ID" --message "$COMMENT" 2>/dev/null
82
+ glab issue note "$EPIC_ID" ${REPO:+--repo "$REPO"} --message "$COMMENT" 2>/dev/null || true
63
83
  fi
64
84
  echo "Closed GitLab epic #$EPIC_ID"
65
85
  ;;
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # Git Wrapper: Close Issue
3
3
  #
4
- # Usage: bash .pi/scripts/git/close-issue.sh --id 101
4
+ # Usage: bash .pi/scripts/git/close-issue.sh --id 101 [--repo "group/project"]
5
5
 
6
6
  set -euo pipefail
7
7
 
@@ -17,19 +17,31 @@ detect_platform() {
17
17
  else echo "none"; fi
18
18
  }
19
19
 
20
+ read_repository() {
21
+ if [ -f "guardian-manifest.json" ]; then
22
+ jq -r '.repository // (.templateContext.repository // "")' guardian-manifest.json 2>/dev/null || echo ""
23
+ else
24
+ echo ""
25
+ fi
26
+ }
27
+
20
28
  ISSUE_ID=""
29
+ REPO=""
30
+
21
31
  while [[ $# -gt 0 ]]; do
22
32
  case $1 in
23
33
  --id) ISSUE_ID="$2"; shift 2 ;;
34
+ --repo) REPO="$2"; shift 2 ;;
24
35
  *) shift ;;
25
36
  esac
26
37
  done
27
38
 
28
- [[ -z "$ISSUE_ID" ]] && { echo "Usage: $0 --id <issue_number>"; exit 1; }
39
+ [[ -z "$ISSUE_ID" ]] && { echo "Usage: $0 --id <issue_number> [--repo <repo>]"; exit 1; }
29
40
  PLATFORM=$(detect_platform)
41
+ [[ -z "$REPO" ]] && REPO=$(read_repository)
30
42
 
31
43
  case "$PLATFORM" in
32
- github) gh issue close "$ISSUE_ID" 2>/dev/null && echo "Closed GitHub issue #$ISSUE_ID" ;;
33
- gitlab) glab issue update "$ISSUE_ID" --state-event close 2>/dev/null && echo "Closed GitLab issue #$ISSUE_ID" ;;
44
+ github) gh issue close "$ISSUE_ID" ${REPO:+--repo "$REPO"} 2>/dev/null && echo "Closed GitHub issue #$ISSUE_ID" ;;
45
+ gitlab) glab issue update "$ISSUE_ID" --state-event close ${REPO:+--repo "$REPO"} 2>/dev/null && echo "Closed GitLab issue #$ISSUE_ID" ;;
34
46
  *) echo "Local issue #$ISSUE_ID marked as closed (no platform)" ;;
35
47
  esac
@@ -3,7 +3,8 @@
3
3
  #
4
4
  # Usage: bash .pi/scripts/git/link-issue-to-epic.sh \
5
5
  # --issue-id 102 \
6
- # --epic-id 101
6
+ # --epic-id 101 \
7
+ # [--repo "group/project"]
7
8
 
8
9
  set -euo pipefail
9
10
 
@@ -19,46 +20,53 @@ detect_platform() {
19
20
  else echo "none"; fi
20
21
  }
21
22
 
23
+ read_repository() {
24
+ if [ -f "guardian-manifest.json" ]; then
25
+ jq -r '.repository // (.templateContext.repository // "")' guardian-manifest.json 2>/dev/null || echo ""
26
+ else
27
+ echo ""
28
+ fi
29
+ }
30
+
22
31
  ISSUE_ID=""
23
32
  EPIC_ID=""
33
+ REPO=""
24
34
 
25
35
  while [[ $# -gt 0 ]]; do
26
36
  case $1 in
27
37
  --issue-id) ISSUE_ID="$2"; shift 2 ;;
28
38
  --epic-id) EPIC_ID="$2"; shift 2 ;;
39
+ --repo) REPO="$2"; shift 2 ;;
29
40
  *) shift ;;
30
41
  esac
31
42
  done
32
43
 
33
- [[ -z "$ISSUE_ID" || -z "$EPIC_ID" ]] && { echo "Usage: $0 --issue-id <issue> --epic-id <epic>"; exit 1; }
44
+ [[ -z "$ISSUE_ID" || -z "$EPIC_ID" ]] && { echo "Usage: $0 --issue-id <issue> --epic-id <epic> [--repo <repo>]"; exit 1; }
34
45
  PLATFORM=$(detect_platform)
46
+ [[ -z "$REPO" ]] && REPO=$(read_repository)
35
47
 
36
48
  case "$PLATFORM" in
37
49
  github)
38
- # GitHub uses "closes #N" or "fixes #N" in issue body to link
39
- gh issue edit "$ISSUE_ID" --body "$(gh issue view "$ISSUE_ID" --json body -q .body)
50
+ gh issue edit "$ISSUE_ID" ${REPO:+--repo "$REPO"} --body "$(gh issue view "$ISSUE_ID" --json body -q .body 2>/dev/null || true)
40
51
 
41
52
  Relates to #$EPIC_ID" 2>/dev/null
42
53
  echo "Linked GitHub issue #$ISSUE_ID to #$EPIC_ID"
43
54
  ;;
44
55
  gitlab)
45
- # Use GitLab API to link issue to epic (--milestone is semantically wrong).
56
+ # GitLab: link issue to epic via API
46
57
  # API: POST /groups/:group/epics/:epic_iid/issues/:issue_iid
47
- # We derive the group from the project path.
48
- if command -v jq &>/dev/null && [[ -n "${CI_PROJECT_ID:-}" || -n "${GITLAB_PROJECT_ID:-}" ]]; then
49
- local pid="${CI_PROJECT_ID:-${GITLAB_PROJECT_ID:-}}"
50
- glab api "projects/$pid" --method GET 2>/dev/null | jq -r '.namespace.full_path // empty' | read -r group_path
58
+ # Derive group from repository path: "group/subgroup/project" -> "group/subgroup"
59
+ if [[ -n "$REPO" ]]; then
60
+ local group_path=$(echo "$REPO" | rev | cut -d'/' -f2- | rev)
51
61
  if [[ -n "$group_path" ]]; then
52
- glab api --method POST "groups/$group_path/epics/$EPIC_ID/issues/$ISSUE_ID" 2>/dev/null || {
53
- # Fallback: add a note referencing the epic
54
- glab issue note "$ISSUE_ID" --message "Part of epic #$EPIC_ID" 2>/dev/null || true
62
+ glab api --method POST "groups/$group_path/epics/$EPIC_ID/issues/$ISSUE_ID" 2>/dev/null && {
63
+ echo "Linked GitLab issue #$ISSUE_ID to epic #$EPIC_ID"
64
+ exit 0
55
65
  }
56
- else
57
- glab issue note "$ISSUE_ID" --message "Part of epic #$EPIC_ID" 2>/dev/null || true
58
66
  fi
59
- else
60
- glab issue note "$ISSUE_ID" --message "Part of epic #$EPIC_ID" 2>/dev/null || true
61
67
  fi
68
+ # Fallback: add a note referencing the epic
69
+ glab issue note "$ISSUE_ID" ${REPO:+--repo "$REPO"} --message "Part of epic #$EPIC_ID" 2>/dev/null || true
62
70
  echo "Linked GitLab issue #$ISSUE_ID to epic #$EPIC_ID"
63
71
  ;;
64
72
  *)
@@ -6,6 +6,7 @@
6
6
  # Usage: bash .pi/scripts/git/update-tracking-issue.sh \
7
7
  # --id 100 \
8
8
  # --comment "✓ Issue #102 complete (CI green, tests pass)"
9
+ # [--repo "group/project"]
9
10
 
10
11
  set -euo pipefail
11
12
 
@@ -21,21 +22,34 @@ detect_platform() {
21
22
  else echo "none"; fi
22
23
  }
23
24
 
25
+ read_repository() {
26
+ if [ -f "guardian-manifest.json" ]; then
27
+ jq -r '.repository // (.templateContext.repository // "")' guardian-manifest.json 2>/dev/null || echo ""
28
+ else
29
+ echo ""
30
+ fi
31
+ }
32
+
24
33
  ISSUE_ID=""
25
34
  COMMENT=""
35
+ REPO=""
26
36
 
27
37
  while [[ $# -gt 0 ]]; do
28
38
  case $1 in
29
39
  --id) ISSUE_ID="$2"; shift 2 ;;
30
40
  --comment) COMMENT="$2"; shift 2 ;;
41
+ --repo) REPO="$2"; shift 2 ;;
31
42
  *) shift ;;
32
43
  esac
33
44
  done
34
45
 
35
46
  PLATFORM=$(detect_platform)
47
+ if [[ -z "$REPO" ]]; then
48
+ REPO=$(read_repository)
49
+ fi
36
50
 
37
51
  if [[ -z "$ISSUE_ID" || -z "$COMMENT" ]]; then
38
- echo "Usage: $0 --id <issue_number> --comment <comment_text>"
52
+ echo "Usage: $0 --id <issue_number> --comment <comment_text> [--repo <repo>]"
39
53
  exit 1
40
54
  fi
41
55
 
@@ -53,10 +67,10 @@ fi
53
67
 
54
68
  case "$PLATFORM" in
55
69
  github)
56
- gh issue comment "$ISSUE_ID" --body "$COMMENT" 2>/dev/null && echo "Comment posted to GitHub issue #$ISSUE_ID"
70
+ gh issue comment "$ISSUE_ID" ${REPO:+--repo "$REPO"} --body "$COMMENT" 2>/dev/null && echo "Comment posted to GitHub issue #$ISSUE_ID"
57
71
  ;;
58
72
  gitlab)
59
- glab issue note "$ISSUE_ID" --message "$COMMENT" 2>/dev/null && echo "Comment posted to GitLab issue #$ISSUE_ID"
73
+ glab issue note "$ISSUE_ID" ${REPO:+--repo "$REPO"} --message "$COMMENT" 2>/dev/null && echo "Comment posted to GitLab issue #$ISSUE_ID"
60
74
  ;;
61
75
  none)
62
76
  echo "No git platform detected. Comment not posted."
@@ -36,7 +36,7 @@ while [[ $# -gt 0 ]]; do
36
36
  done
37
37
 
38
38
  PLATFORM=$(detect_platform)
39
- BRANCH_NAME="feature/${ISSUE_ID}"
39
+ BRANCH_NAME="feat/${ISSUE_ID}"
40
40
 
41
41
  if [[ "$PLATFORM" == "none" ]]; then
42
42
  echo "No git platform detected. Cannot merge without gh/glab CLI."