guardian-framework 0.1.73 → 0.1.75

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.73", 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.75", 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.73", 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.75", 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.73",
3
+ "version": "0.1.75",
4
4
  "description": "Token-optimized agentic framework scaffolder with pi-first architecture",
5
5
  "type": "module",
6
6
  "main": "dist/exports.js",
@@ -72,6 +72,15 @@ export function readLanguage(cwd: string): string {
72
72
  return "typescript";
73
73
  }
74
74
 
75
+ /**
76
+ * URL-encode a GitLab project/group path for API calls.
77
+ * GitLab API requires nested paths to be URL-encoded:
78
+ * "group/subgroup/project" -> "group%2Fsubgroup%2Fproject"
79
+ */
80
+ export function glabEncodePath(rawPath: string): string {
81
+ return encodeURIComponent(rawPath);
82
+ }
83
+
75
84
  /**
76
85
  * Get forge base URL. For GitLab, detects self-hosted instances via glab config.
77
86
  */
@@ -142,10 +151,11 @@ export function verifyForgeAccess(cwd: string): { authenticated: boolean; projec
142
151
 
143
152
  // Verify project-level access
144
153
  if (repository) {
154
+ const encodedRepo = repoTool === "glab" ? glabEncodePath(repository) : repository;
145
155
  const projectCheck = runScript(
146
156
  cwd,
147
157
  repoTool === "glab"
148
- ? `glab api "projects/${repository}" --method GET 2>/dev/null | jq -r '.id // empty'`
158
+ ? `glab api "projects/${encodedRepo}" --method GET 2>/dev/null | jq -r '.id // empty'`
149
159
  : `gh api "repos/${repository}" --method GET 2>/dev/null | jq -r '.id // empty'`,
150
160
  );
151
161
  if (projectCheck.exitCode === 0 && projectCheck.stdout.trim().length > 0) {
@@ -10,6 +10,7 @@ import {
10
10
  commandExists as adapterCommandExists,
11
11
  runScript as adapterRunScript,
12
12
  verifyForgeAccess,
13
+ glabEncodePath,
13
14
  } from "./forge-adapter.ts";
14
15
  // ── Constants ──
15
16
 
@@ -105,6 +106,48 @@ export function createRemoteIssue(
105
106
  return { success: false, issueNumber: null, error: "Could not parse issue number" };
106
107
  }
107
108
 
109
+ // Create a real GitLab Epic via the glab CLI.
110
+ // Returns the epic IID (string) on success, null on failure.
111
+ export function createGitlabEpic(
112
+ cwd: string,
113
+ repository: string,
114
+ title: string,
115
+ description: string,
116
+ ): string | null {
117
+ // Derive group from repository path: "group/subgroup/project" -> "group/subgroup"
118
+ const parts = repository.split("/");
119
+ if (parts.length < 2) return null;
120
+ const group = parts.slice(0, -1).join("/");
121
+
122
+ // Build description from markdown content (strip YAML frontmatter)
123
+ const desc = description.replace(/^---[\s\S]*?---\n*/, "").trim();
124
+
125
+ // Create epic via glab CLI
126
+ const result = runScript(
127
+ cwd,
128
+ `glab epic create --group "${group}" --title "${title}" --description "${desc.slice(0, 5000)}" 2>&1`,
129
+ );
130
+
131
+ if (result.exitCode !== 0) {
132
+ // Fallback: try the API directly (URL-encode nested group paths)
133
+ const encodedGroup = glabEncodePath(group);
134
+ const body = JSON.stringify({ title, description: desc.slice(0, 5000) });
135
+ const apiResult = runScript(
136
+ cwd,
137
+ `glab api --method POST "groups/${encodedGroup}/epics" --body '${body}' 2>/dev/null | jq -r '.iid // empty'`,
138
+ );
139
+ if (apiResult.exitCode !== 0 || !apiResult.stdout.trim()) {
140
+ console.warn(`Failed to create GitLab epic: ${result.stdout || apiResult.stdout}`);
141
+ return null;
142
+ }
143
+ return apiResult.stdout.trim();
144
+ }
145
+
146
+ // Parse epic IID from output: "https://gitlab.com/groups/.../-/epics/42"
147
+ const epicMatch = result.stdout.match(/epics\/(\d+)/);
148
+ return epicMatch ? epicMatch[1] : null;
149
+ }
150
+
108
151
  // Ensure the GitHub/GitLab repository exists and local git remote is configured.
109
152
  // Returns the repository slug if remote is ready, empty string if not available.
110
153
  export function ensureRemoteRepo(
@@ -156,15 +199,28 @@ export function ensureRemoteRepo(
156
199
 
157
200
  // GitLab path — detect self-hosted base URL from glab config
158
201
  const glabBaseUrl = getGitBaseUrl("glab");
159
- const createResult = runScript(
160
- cwd,
161
- `glab repo create "${repository}" --private --description "Epic: ${epicName}" 2>&1`,
162
- );
202
+
203
+ // Split repository path into group + project name for glab CLI
204
+ const parts = repository.split("/");
205
+ const projectName = parts.pop()!;
206
+ const group = parts.join("/");
207
+
208
+ const createResult = group
209
+ ? runScript(
210
+ cwd,
211
+ `glab repo create "${projectName}" --group "${group}" --private --description "Epic: ${epicName}" 2>&1`,
212
+ )
213
+ : runScript(
214
+ cwd,
215
+ `glab repo create "${repository}" --private --description "Epic: ${epicName}" 2>&1`,
216
+ );
217
+
163
218
  if (createResult.exitCode !== 0) {
164
219
  // Repo may already exist — verify via API before giving up
220
+ const encodedPath = glabEncodePath(repository);
165
221
  const viewResult = runScript(
166
222
  cwd,
167
- `glab api "projects/${repository}" --method GET 2>/dev/null | jq -r '.id // empty'`,
223
+ `glab api "projects/${encodedPath}" --method GET 2>/dev/null | jq -r '.id // empty'`,
168
224
  );
169
225
  if (viewResult.exitCode !== 0 || !viewResult.stdout.trim()) {
170
226
  console.warn(`Failed to create or find GitLab project: ${createResult.stdout}`);
@@ -28,6 +28,7 @@ import {
28
28
  readRepoTool,
29
29
  runScript,
30
30
  saveRoadmapState,
31
+ createGitlabEpic,
31
32
  } from "./architect-lib/helpers";
32
33
  import {
33
34
  generateArchitectureReadinessMarkdown,
@@ -175,6 +176,16 @@ class EpicManager {
175
176
  }
176
177
  }
177
178
 
179
+ // Loud warning if GitLab remote setup failed
180
+ if (repoTool === "glab" && !hasRemote) {
181
+ console.warn(
182
+ "⚠️ GitLab remote repo setup failed. Pipeline will run LOCAL-ONLY."
183
+ + " Issues will be created as local .md files, not on GitLab."
184
+ + " Check that the repository path in guardian-manifest.json exists"
185
+ + " and your GitLab token has 'write_repository' scope.",
186
+ );
187
+ }
188
+
178
189
  const issues: { id: string; title: string; status: string; remoteIssueId?: string | null }[] = [];
179
190
  const issuesDir = join(this.cwd, ".pi/issues");
180
191
  if (!existsSync(issuesDir)) mkdirSync(issuesDir, { recursive: true });
@@ -302,29 +313,52 @@ class EpicManager {
302
313
  issues.push(readinessEntry);
303
314
 
304
315
  // ── TDD test generation ──
305
- let tddTestFiles: string[] = [];
316
+ const tddTestFiles: string[] = [];
306
317
  if (tdd) {
307
318
  const language = readLanguage(this.cwd);
308
319
  if (isTddSupported(language)) {
309
- tddTestFiles = generateEpicTestFiles({
320
+ tddTestFiles.push(...generateEpicTestFiles({
310
321
  components: slice.nextLogicalSlice,
311
322
  moduleId: slice.module,
312
323
  cwd: this.cwd,
313
324
  language,
314
- });
325
+ }));
326
+ }
327
+ }
328
+
329
+ // ── GitLab Epic creation (only for GitLab repos with remote access) ──
330
+ let effectiveEpicId: string | null = null;
331
+ if (repoTool === "glab" && hasRemote && remoteRepo) {
332
+ const epicTitle = `Epic: ${name} — ${slice.module}`;
333
+ const epicDesc = [
334
+ `# ${epicTitle}`,
335
+ "",
336
+ `**Module:** ${slice.module}`,
337
+ `**Components:** ${slice.nextLogicalSlice.length}`,
338
+ "",
339
+ "## Issues",
340
+ ...issues.map((i) => `- ${i.title}${i.remoteIssueId ? ` (#${i.remoteIssueId})` : ""}`),
341
+ "",
342
+ "---",
343
+ "Auto-generated by Guardian Architect",
344
+ ].join("\n");
345
+ effectiveEpicId = createGitlabEpic(this.cwd, remoteRepo, epicTitle, epicDesc);
346
+ if (effectiveEpicId) {
347
+ console.warn(`Created GitLab epic #${effectiveEpicId}`);
315
348
  }
316
349
  }
317
350
 
318
351
  const state: EpicState = {
319
352
  name,
320
353
  trackingIssueId: effectiveTrackingId,
321
- epicId: null,
354
+ epicId: effectiveEpicId,
322
355
  slices: [slice],
323
356
  issues,
324
357
  status: "planning",
325
358
  currentIssueIndex: 0,
326
359
  createdAt: new Date().toISOString(),
327
360
  tdd,
361
+ tddTestFiles,
328
362
  };
329
363
 
330
364
  this.state = state;
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-07-13T11:44:15Z",
2
+ "timestamp": "2026-07-13T12:43:57Z",
3
3
  "mode": "all",
4
4
  "stages_run": [],
5
5
  "summary": {
@@ -163,7 +163,7 @@ case "$PLATFORM" in
163
163
 
164
164
  # Try to add labels separately (non-fatal)
165
165
  if [[ -n "$LABELS" && -n "$ISSUE_NUMBER" ]]; then
166
- glab issue update "$ISSUE_NUMBER" --label "$LABELS" 2>/dev/null || true
166
+ glab issue update "$ISSUE_NUMBER" --label "$LABELS" ${REPO:+--repo "$REPO"} 2>/dev/null || true
167
167
  fi
168
168
 
169
169
  echo "TRACKING_ID=$ISSUE_NUMBER"
@@ -57,7 +57,7 @@ Relates to #$EPIC_ID" 2>/dev/null
57
57
  # API: POST /groups/:group/epics/:epic_iid/issues/:issue_iid
58
58
  # Derive group from repository path: "group/subgroup/project" -> "group/subgroup"
59
59
  if [[ -n "$REPO" ]]; then
60
- local group_path=$(echo "$REPO" | rev | cut -d'/' -f2- | rev)
60
+ group_path=$(echo "$REPO" | rev | cut -d'/' -f2- | rev)
61
61
  if [[ -n "$group_path" ]]; then
62
62
  glab api --method POST "groups/$group_path/epics/$EPIC_ID/issues/$ISSUE_ID" 2>/dev/null && {
63
63
  echo "Linked GitLab issue #$ISSUE_ID to epic #$EPIC_ID"