panopticon-cli 0.4.12 → 0.4.14

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.
@@ -17,7 +17,7 @@
17
17
  }
18
18
  })();
19
19
  </script>
20
- <script type="module" crossorigin src="/assets/index-Bwu9d0w1.js"></script>
20
+ <script type="module" crossorigin src="/assets/index-CE4Bu8aP.js"></script>
21
21
  <link rel="stylesheet" crossorigin href="/assets/index-Bp7DyPLO.css">
22
22
  </head>
23
23
  <body class="bg-surface text-content transition-colors duration-150">
@@ -51275,7 +51275,7 @@ var rally_exports = {};
51275
51275
  __export(rally_exports, {
51276
51276
  RallyTracker: () => RallyTracker
51277
51277
  });
51278
- var STATE_MAP, PRIORITY_MAP, REVERSE_PRIORITY_MAP, RallyTracker;
51278
+ var STATE_MAP, QUERYABLE_TYPES, FETCH_FIELDS, PRIORITY_MAP, REVERSE_PRIORITY_MAP, RallyTracker;
51279
51279
  var init_rally = __esm({
51280
51280
  "../../lib/tracker/rally.ts"() {
51281
51281
  "use strict";
@@ -51287,6 +51287,27 @@ var init_rally = __esm({
51287
51287
  Completed: "closed",
51288
51288
  Accepted: "closed"
51289
51289
  };
51290
+ QUERYABLE_TYPES = [
51291
+ { type: "hierarchicalrequirement", stateField: "ScheduleState", closedStates: ["Completed", "Accepted"] },
51292
+ { type: "defect", stateField: "State", closedStates: ["Closed"] },
51293
+ { type: "task", stateField: "State", closedStates: ["Completed"] }
51294
+ ];
51295
+ FETCH_FIELDS = [
51296
+ "ObjectID",
51297
+ "FormattedID",
51298
+ "Name",
51299
+ "Description",
51300
+ "ScheduleState",
51301
+ "State",
51302
+ "Tags",
51303
+ "Owner",
51304
+ "Priority",
51305
+ "DueDate",
51306
+ "CreationDate",
51307
+ "LastUpdateDate",
51308
+ "Parent",
51309
+ "_type"
51310
+ ];
51290
51311
  PRIORITY_MAP = {
51291
51312
  "Resolve Immediately": 0,
51292
51313
  High: 1,
@@ -51324,50 +51345,55 @@ var init_rally = __esm({
51324
51345
  this.workspace = config2.workspace;
51325
51346
  this.project = config2.project;
51326
51347
  }
51348
+ /**
51349
+ * List issues by querying each artifact type separately and merging results.
51350
+ *
51351
+ * Rally WSAPI cannot apply ScheduleState filters across the generic Artifact
51352
+ * endpoint because not all subtypes have that field. We query each type with
51353
+ * its own state field, then merge and sort. (PAN-168)
51354
+ */
51327
51355
  async listIssues(filters) {
51328
- const queryString = this.buildQueryString(filters);
51329
51356
  if (process.env.DEBUG?.includes("rally")) {
51330
51357
  console.debug("[Rally] Query filters:", JSON.stringify(filters));
51331
- console.debug("[Rally] Generated query:", queryString);
51332
- }
51333
- const query = {
51334
- type: "artifact",
51335
- // Query all artifact types
51336
- fetch: [
51337
- "FormattedID",
51338
- "Name",
51339
- "Description",
51340
- "ScheduleState",
51341
- "State",
51342
- // For Defects
51343
- "Tags",
51344
- "Owner",
51345
- "Priority",
51346
- "DueDate",
51347
- "CreationDate",
51348
- "LastUpdateDate",
51349
- "Parent",
51350
- "_type"
51351
- ],
51352
- limit: filters?.limit ?? 50,
51353
- query: queryString
51354
- };
51355
- if (this.workspace) {
51356
- query.workspace = this.workspace;
51357
- }
51358
- if (this.project) {
51359
- query.project = this.project;
51360
- query.projectScopeDown = true;
51361
51358
  }
51362
- try {
51363
- const result = await this.queryRally(query);
51364
- return result.Results.map((artifact) => this.normalizeIssue(artifact));
51365
- } catch (error) {
51366
- if (error.message?.includes("Unauthorized") || error.message?.includes("401")) {
51367
- throw new TrackerAuthError("rally", "Invalid API key or insufficient permissions");
51359
+ const limit = filters?.limit ?? 50;
51360
+ const queries = QUERYABLE_TYPES.map(async (artifactType) => {
51361
+ const queryString = this.buildQueryStringForType(filters, artifactType);
51362
+ if (process.env.DEBUG?.includes("rally")) {
51363
+ console.debug(`[Rally] ${artifactType.type} query:`, queryString);
51368
51364
  }
51369
- throw error;
51370
- }
51365
+ const query = {
51366
+ type: artifactType.type,
51367
+ fetch: FETCH_FIELDS,
51368
+ limit,
51369
+ query: queryString
51370
+ };
51371
+ if (this.workspace) {
51372
+ query.workspace = this.workspace;
51373
+ }
51374
+ if (this.project) {
51375
+ query.project = this.project;
51376
+ query.projectScopeDown = true;
51377
+ }
51378
+ try {
51379
+ const result = await this.queryRally(query);
51380
+ return result.Results.map((artifact) => this.normalizeIssue(artifact));
51381
+ } catch (error) {
51382
+ if (error.message?.includes("Unauthorized") || error.message?.includes("401")) {
51383
+ throw new TrackerAuthError("rally", "Invalid API key or insufficient permissions");
51384
+ }
51385
+ if (process.env.DEBUG?.includes("rally")) {
51386
+ console.debug(`[Rally] Failed to query ${artifactType.type}:`, error.message);
51387
+ }
51388
+ return [];
51389
+ }
51390
+ });
51391
+ const results = await Promise.all(queries);
51392
+ const allIssues = results.flat();
51393
+ allIssues.sort(
51394
+ (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
51395
+ );
51396
+ return allIssues.slice(0, limit);
51371
51397
  }
51372
51398
  async getIssue(id) {
51373
51399
  try {
@@ -51552,24 +51578,31 @@ var init_rally = __esm({
51552
51578
  }
51553
51579
  // Private helper methods
51554
51580
  /**
51555
- * Build a Rally WSAPI query string from issue filters.
51581
+ * Build a Rally WSAPI query string for a specific artifact type.
51556
51582
  *
51557
- * Rally WSAPI v2.0 requires the entire compound query expression to be wrapped
51558
- * in outer parentheses when multiple conditions are joined with AND/OR.
51559
- * Without the outer parens, the WSAPI parser fails with:
51560
- * "Could not parse: Error parsing expression -- expected ")" but saw "AND" instead."
51583
+ * Each artifact type has its own state field:
51584
+ * - HierarchicalRequirement: ScheduleState (Defined, In-Progress, Completed, Accepted)
51585
+ * - Defect: State (Submitted, Open, Fixed, Closed)
51586
+ * - Task: State (Defined, In-Progress, Completed)
51561
51587
  *
51562
- * Valid: (((ScheduleState != "Completed") AND (State != "Closed")) AND (Owner.Name contains "John"))
51563
- * Invalid: ((ScheduleState != "Completed") AND (State != "Closed")) AND (Owner.Name contains "John")
51588
+ * Rally WSAPI v2.0 requires binary-nested AND/OR with outer parentheses.
51589
+ * (PAN-166, PAN-168)
51564
51590
  */
51565
- buildQueryString(filters) {
51591
+ buildQueryStringForType(filters, artifactType) {
51566
51592
  const conditions = [];
51567
51593
  if (filters?.state && !filters.includeClosed) {
51568
51594
  const rallyState = this.reverseMapState(filters.state);
51569
- conditions.push(`((ScheduleState = "${rallyState}") OR (State = "${rallyState}"))`);
51595
+ conditions.push(`(${artifactType.stateField} = "${rallyState}")`);
51570
51596
  }
51571
51597
  if (!filters?.includeClosed) {
51572
- conditions.push('(((ScheduleState != "Completed") AND (ScheduleState != "Accepted")) AND (State != "Closed"))');
51598
+ const closedConditions = artifactType.closedStates.map(
51599
+ (state) => `(${artifactType.stateField} != "${state}")`
51600
+ );
51601
+ const closedExpr = closedConditions.reduce(
51602
+ (acc, cond) => acc ? `(${acc} AND ${cond})` : cond,
51603
+ ""
51604
+ );
51605
+ conditions.push(closedExpr);
51573
51606
  }
51574
51607
  if (filters?.assignee) {
51575
51608
  conditions.push(`(Owner.Name contains "${filters.assignee}")`);
@@ -51591,13 +51624,21 @@ var init_rally = __esm({
51591
51624
  const state = this.mapState(stateValue);
51592
51625
  const labels = [];
51593
51626
  if (rallyArtifact.Tags && rallyArtifact.Tags._tagsNameArray) {
51594
- labels.push(...rallyArtifact.Tags._tagsNameArray);
51627
+ for (const tag of rallyArtifact.Tags._tagsNameArray) {
51628
+ if (typeof tag === "string") {
51629
+ labels.push(tag);
51630
+ } else if (tag?.Name) {
51631
+ labels.push(tag.Name);
51632
+ }
51633
+ }
51595
51634
  }
51596
51635
  const priority = rallyArtifact.Priority ? PRIORITY_MAP[rallyArtifact.Priority] ?? 2 : void 0;
51636
+ const objectId = rallyArtifact.ObjectID || rallyArtifact.FormattedID;
51637
+ const artifactType = rallyArtifact._type || "artifact";
51597
51638
  const baseUrl = this.restApi.server.replace("/slm/webservice/", "");
51598
- const url = `${baseUrl}/#/detail/${rallyArtifact._type.toLowerCase()}/${rallyArtifact.ObjectID}`;
51639
+ const url = `${baseUrl}/#/detail/${artifactType.toLowerCase()}/${objectId}`;
51599
51640
  return {
51600
- id: rallyArtifact.ObjectID,
51641
+ id: String(objectId),
51601
51642
  ref: rallyArtifact.FormattedID,
51602
51643
  title: rallyArtifact.Name || "",
51603
51644
  description: rallyArtifact.Description || "",
@@ -67224,16 +67265,38 @@ async function postMergeCleanup(issueId, projectPath) {
67224
67265
  }
67225
67266
  const isGitHub = issueId.toUpperCase().startsWith("PAN-");
67226
67267
  if (isGitHub) {
67268
+ const issueNum = issueId.replace(/^PAN-/i, "");
67269
+ try {
67270
+ const branchName = `feature/${issueId.toLowerCase()}`;
67271
+ const { stdout: prListRaw } = await execAsync10(
67272
+ `gh pr list --repo eltmon/panopticon-cli --head "${branchName}" --state open --json number --jq '.[0].number'`,
67273
+ { cwd: projectPath, encoding: "utf-8" }
67274
+ );
67275
+ const prNumber = prListRaw.trim();
67276
+ if (prNumber) {
67277
+ await execAsync10(
67278
+ `gh pr close ${prNumber} --repo eltmon/panopticon-cli --comment "Merged to main via Panopticon merge-agent"`,
67279
+ { cwd: projectPath, encoding: "utf-8" }
67280
+ );
67281
+ console.log(`[merge-agent] \u2713 Closed PR #${prNumber} for ${issueId}`);
67282
+ logActivity("pr_closed", `Closed PR #${prNumber} for ${issueId}`);
67283
+ }
67284
+ } catch (err) {
67285
+ console.warn(`[merge-agent] Could not close PR: ${err}`);
67286
+ }
67227
67287
  try {
67228
- const issueNum = issueId.replace(/^PAN-/i, "");
67229
67288
  await execAsync10(`gh issue edit ${issueNum} --remove-label "in-progress" --add-label "done" 2>/dev/null || true`, {
67230
67289
  cwd: projectPath,
67231
67290
  encoding: "utf-8"
67232
67291
  });
67233
- console.log(`[merge-agent] \u2713 Updated GitHub issue ${issueId} labels`);
67234
- logActivity("issue_updated", `Updated ${issueId} labels: removed in-progress, added done`);
67292
+ await execAsync10(`gh issue close ${issueNum} --repo eltmon/panopticon-cli --comment "Merged to main" 2>/dev/null || true`, {
67293
+ cwd: projectPath,
67294
+ encoding: "utf-8"
67295
+ });
67296
+ console.log(`[merge-agent] \u2713 Updated and closed GitHub issue #${issueNum}`);
67297
+ logActivity("issue_closed", `Closed GitHub issue #${issueNum} after merge`);
67235
67298
  } catch (err) {
67236
- console.warn(`[merge-agent] Could not update GitHub issue labels: ${err}`);
67299
+ console.warn(`[merge-agent] Could not close GitHub issue: ${err}`);
67237
67300
  }
67238
67301
  } else {
67239
67302
  try {
@@ -67246,6 +67309,18 @@ async function postMergeCleanup(issueId, projectPath) {
67246
67309
  console.warn(`[merge-agent] Could not update Linear issue: ${err}`);
67247
67310
  }
67248
67311
  }
67312
+ try {
67313
+ const apiPort = process.env.API_PORT || process.env.PORT || "3011";
67314
+ const apiUrl = process.env.DASHBOARD_URL || `http://localhost:${apiPort}`;
67315
+ await fetch(`${apiUrl}/api/specialists/done`, {
67316
+ method: "POST",
67317
+ headers: { "Content-Type": "application/json" },
67318
+ body: JSON.stringify({ specialist: "merge", issueId, status: "passed", notes: "Merge and validation completed" })
67319
+ });
67320
+ console.log(`[merge-agent] \u2713 Reported merge success to dashboard API`);
67321
+ } catch (err) {
67322
+ console.warn(`[merge-agent] Could not report to dashboard API: ${err}`);
67323
+ }
67249
67324
  await conditionalBeadsCompaction(projectPath);
67250
67325
  console.log(`[merge-agent] Post-merge cleanup completed for ${issueId}`);
67251
67326
  }
@@ -67703,12 +67778,7 @@ Report any issues or conflicts you encountered.`;
67703
67778
  });
67704
67779
  const currentHead = currentHeadRaw.trim();
67705
67780
  if (currentHead !== headBefore) {
67706
- const { stdout: commitMessageRaw } = await execAsync10("git log -1 --pretty=%s", {
67707
- cwd: projectPath,
67708
- encoding: "utf-8"
67709
- });
67710
- const commitMessage = commitMessageRaw.trim().toLowerCase();
67711
- if (commitMessage.includes("merge") || commitMessage.includes(sourceBranch.toLowerCase())) {
67781
+ {
67712
67782
  try {
67713
67783
  const { stdout: remoteHeadRaw } = await execAsync10(`git rev-parse origin/${targetBranch}`, {
67714
67784
  cwd: projectPath,
@@ -85012,6 +85082,8 @@ function mapGitHubStateToCanonical(state, labels) {
85012
85082
  return "todo";
85013
85083
  }
85014
85084
  function mapRallyStateToCanonical(scheduleState) {
85085
+ if (!scheduleState)
85086
+ return "todo";
85015
85087
  const stateLower = scheduleState.toLowerCase();
85016
85088
  if (stateLower === "defined")
85017
85089
  return "todo";
@@ -85588,10 +85660,11 @@ var IssueDataService = class {
85588
85660
  });
85589
85661
  const formatted = issues.map((issue) => {
85590
85662
  const canonicalStatus = mapRallyStateToCanonical(issue.state);
85663
+ const identifier = issue.ref || issue.id || "unknown";
85591
85664
  return {
85592
- id: `rally-${issue.id}`,
85593
- identifier: issue.ref,
85594
- title: issue.title,
85665
+ id: `rally-${issue.id || identifier}`,
85666
+ identifier,
85667
+ title: issue.title || "",
85595
85668
  description: issue.description || "",
85596
85669
  status: canonicalStatus === "todo" ? "Todo" : canonicalStatus === "in_progress" ? "In Progress" : canonicalStatus === "done" ? "Done" : "Todo",
85597
85670
  priority: issue.priority ?? 3,
@@ -85599,8 +85672,8 @@ var IssueDataService = class {
85599
85672
  name: issue.assignee,
85600
85673
  email: `${issue.assignee.replace(/\s+/g, ".").toLowerCase()}@rally`
85601
85674
  } : void 0,
85602
- labels: issue.labels || [],
85603
- url: issue.url,
85675
+ labels: Array.isArray(issue.labels) ? issue.labels.filter((l) => typeof l === "string") : [],
85676
+ url: issue.url || "",
85604
85677
  createdAt: issue.createdAt,
85605
85678
  updatedAt: issue.updatedAt,
85606
85679
  project: {
@@ -94469,6 +94542,14 @@ app.post("/api/specialists/done", async (req, res) => {
94469
94542
  console.log(`[specialists/done] Cleared ${normalizedIssueId} from ${specialist}-agent queue`);
94470
94543
  }
94471
94544
  }
94545
+ if (specialist === "merge" && status === "passed") {
94546
+ try {
94547
+ await closeIssueAfterMerge(normalizedIssueId);
94548
+ console.log(`[specialists/done] Closed issue/PR for ${normalizedIssueId} after merge`);
94549
+ } catch (err) {
94550
+ console.warn(`[specialists/done] Failed to close issue after merge: ${err}`);
94551
+ }
94552
+ }
94472
94553
  res.json({
94473
94554
  success: true,
94474
94555
  specialist,
@@ -94878,6 +94959,25 @@ app.post("/api/workspaces/:issueId/merge", async (req, res) => {
94878
94959
  });
94879
94960
  app.post("/api/workspaces/:issueId/approve", async (req, res) => {
94880
94961
  const { issueId } = req.params;
94962
+ const existingStatus = getReviewStatus(issueId);
94963
+ if (existingStatus?.readyForMerge && existingStatus.reviewStatus === "passed" && existingStatus.testStatus === "passed") {
94964
+ console.log(`[approve] Review+test already passed for ${issueId}, forwarding to merge endpoint...`);
94965
+ try {
94966
+ const apiPort = process.env.API_PORT || process.env.PORT || "3011";
94967
+ const mergeRes = await fetch(`http://localhost:${apiPort}/api/workspaces/${issueId}/merge`, {
94968
+ method: "POST",
94969
+ headers: { "Content-Type": "application/json" }
94970
+ });
94971
+ const mergeData = await mergeRes.json();
94972
+ if (mergeRes.ok) {
94973
+ return res.json(mergeData);
94974
+ } else {
94975
+ return res.status(mergeRes.status).json(mergeData);
94976
+ }
94977
+ } catch (err) {
94978
+ return res.status(500).json({ error: `Failed to forward to merge: ${err.message}` });
94979
+ }
94980
+ }
94881
94981
  const issuePrefix = issueId.split("-")[0];
94882
94982
  const projectPath = getProjectPath(void 0, issuePrefix);
94883
94983
  const issueLower = issueId.toLowerCase();
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  planSync,
30
30
  restoreBackup,
31
31
  syncHooks
32
- } from "./chunk-WLL3UEYI.js";
32
+ } from "./chunk-YCX7RVYW.js";
33
33
  import {
34
34
  PROVIDERS,
35
35
  getAgentCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panopticon-cli",
3
- "version": "0.4.12",
3
+ "version": "0.4.14",
4
4
  "description": "Multi-agent orchestration for AI coding assistants (Claude Code, Codex, Cursor, Gemini CLI)",
5
5
  "keywords": [
6
6
  "ai-agents",