@task-boards/mcp-server 0.15.0 → 0.16.0

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.
Files changed (3) hide show
  1. package/build/cli.js +1735 -1085
  2. package/build/cli.js.map +4 -4
  3. package/package.json +1 -1
package/build/cli.js CHANGED
@@ -25,6 +25,25 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
  mod
26
26
  ));
27
27
 
28
+ // ../api/build/agent-runs/agent-runs.api.js
29
+ var require_agent_runs_api = __commonJS({
30
+ "../api/build/agent-runs/agent-runs.api.js"(exports) {
31
+ "use strict";
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.AGENT_RUNS_ROUTES = exports.AGENT_RUNS_PREFIX = void 0;
34
+ exports.AGENT_RUNS_PREFIX = "/agent-runs";
35
+ exports.AGENT_RUNS_ROUTES = {
36
+ listAgentRuns: () => exports.AGENT_RUNS_PREFIX,
37
+ claimAgentRuns: () => `${exports.AGENT_RUNS_PREFIX}/claim`,
38
+ listAgentRunsByWorkItem: (workItemId) => `/work-items/${workItemId}/agent-runs`,
39
+ retryAgentRun: (id) => `${exports.AGENT_RUNS_PREFIX}/${id}/retry`,
40
+ acknowledgeAgentRun: (id) => `${exports.AGENT_RUNS_PREFIX}/${id}/acknowledge`,
41
+ setAgentRunAttention: (id) => `${exports.AGENT_RUNS_PREFIX}/${id}/attention`,
42
+ completeAgentRun: (id) => `${exports.AGENT_RUNS_PREFIX}/${id}/complete`
43
+ };
44
+ }
45
+ });
46
+
28
47
  // ../api/build/api-prefix.js
29
48
  var require_api_prefix = __commonJS({
30
49
  "../api/build/api-prefix.js"(exports) {
@@ -102,6 +121,7 @@ var require_agent_run_outcome_enum = __commonJS({
102
121
  exports.AGENT_RUN_OUTCOME = {
103
122
  DEFAULT: "DEFAULT",
104
123
  SKIP_DESIGN: "SKIP_DESIGN",
124
+ SKIP_DEV: "SKIP_DEV",
105
125
  HAS_BUGS: "HAS_BUGS",
106
126
  NEEDS_CLARIFICATION: "NEEDS_CLARIFICATION",
107
127
  FAILED: "FAILED"
@@ -915,25 +935,6 @@ var require_notifications = __commonJS({
915
935
  }
916
936
  });
917
937
 
918
- // ../api/build/agent-runs/agent-runs.api.js
919
- var require_agent_runs_api = __commonJS({
920
- "../api/build/agent-runs/agent-runs.api.js"(exports) {
921
- "use strict";
922
- Object.defineProperty(exports, "__esModule", { value: true });
923
- exports.AGENT_RUNS_ROUTES = exports.AGENT_RUNS_PREFIX = void 0;
924
- exports.AGENT_RUNS_PREFIX = "/agent-runs";
925
- exports.AGENT_RUNS_ROUTES = {
926
- listAgentRuns: () => exports.AGENT_RUNS_PREFIX,
927
- claimAgentRuns: () => `${exports.AGENT_RUNS_PREFIX}/claim`,
928
- listAgentRunsByWorkItem: (workItemId) => `/work-items/${workItemId}/agent-runs`,
929
- retryAgentRun: (id) => `${exports.AGENT_RUNS_PREFIX}/${id}/retry`,
930
- acknowledgeAgentRun: (id) => `${exports.AGENT_RUNS_PREFIX}/${id}/acknowledge`,
931
- setAgentRunAttention: (id) => `${exports.AGENT_RUNS_PREFIX}/${id}/attention`,
932
- completeAgentRun: (id) => `${exports.AGENT_RUNS_PREFIX}/${id}/complete`
933
- };
934
- }
935
- });
936
-
937
938
  // ../api/build/agent-runs/agent-runs.types.js
938
939
  var require_agent_runs_types = __commonJS({
939
940
  "../api/build/agent-runs/agent-runs.types.js"(exports) {
@@ -1682,7 +1683,10 @@ var require_exception_codes = __commonJS({
1682
1683
  ADMIN_NOTIFICATION_SETTINGS_NOT_FOUND: "ADMIN_NOTIFICATION_SETTINGS_NOT_FOUND",
1683
1684
  ADMIN_NOTIFICATION_CHANNEL_INVALID: "ADMIN_NOTIFICATION_CHANNEL_INVALID",
1684
1685
  ADMIN_NOTIFICATION_EMAIL_RECIPIENT_REQUIRED: "ADMIN_NOTIFICATION_EMAIL_RECIPIENT_REQUIRED",
1685
- ADMIN_NOTIFICATION_TELEGRAM_CONFIG_INCOMPLETE: "ADMIN_NOTIFICATION_TELEGRAM_CONFIG_INCOMPLETE"
1686
+ ADMIN_NOTIFICATION_TELEGRAM_CONFIG_INCOMPLETE: "ADMIN_NOTIFICATION_TELEGRAM_CONFIG_INCOMPLETE",
1687
+ PAYMENTS_DISABLED: "PAYMENTS_DISABLED",
1688
+ PAYMENTS_NOT_CONFIGURED: "PAYMENTS_NOT_CONFIGURED",
1689
+ PAYMENTS_PROBE_FAILED: "PAYMENTS_PROBE_FAILED"
1686
1690
  };
1687
1691
  }
1688
1692
  });
@@ -1695,7 +1699,8 @@ var require_health_api = __commonJS({
1695
1699
  exports.HEALTH_ROUTES = exports.HEALTH_PREFIX = void 0;
1696
1700
  exports.HEALTH_PREFIX = "/health";
1697
1701
  exports.HEALTH_ROUTES = {
1698
- getHealth: () => exports.HEALTH_PREFIX
1702
+ getHealth: () => exports.HEALTH_PREFIX,
1703
+ getReadiness: () => `${exports.HEALTH_PREFIX}/ready`
1699
1704
  };
1700
1705
  }
1701
1706
  });
@@ -1705,10 +1710,18 @@ var require_health_types = __commonJS({
1705
1710
  "../shared/build/internal/health/health.types.js"(exports) {
1706
1711
  "use strict";
1707
1712
  Object.defineProperty(exports, "__esModule", { value: true });
1708
- exports.HEALTH_STATUS = void 0;
1713
+ exports.READINESS_CHECK_STATUS = exports.READINESS_STATUS = exports.HEALTH_STATUS = void 0;
1709
1714
  exports.HEALTH_STATUS = {
1710
1715
  OK: "ok"
1711
1716
  };
1717
+ exports.READINESS_STATUS = {
1718
+ OK: "ok",
1719
+ ERROR: "error"
1720
+ };
1721
+ exports.READINESS_CHECK_STATUS = {
1722
+ UP: "up",
1723
+ DOWN: "down"
1724
+ };
1712
1725
  }
1713
1726
  });
1714
1727
 
@@ -1873,6 +1886,36 @@ var require_admin = __commonJS({
1873
1886
  }
1874
1887
  });
1875
1888
 
1889
+ // ../shared/build/internal/payments/admin-payments.api.js
1890
+ var require_admin_payments_api = __commonJS({
1891
+ "../shared/build/internal/payments/admin-payments.api.js"(exports) {
1892
+ "use strict";
1893
+ Object.defineProperty(exports, "__esModule", { value: true });
1894
+ exports.ADMIN_PAYMENTS_ROUTES = exports.ADMIN_PAYMENTS_PREFIX = void 0;
1895
+ var admin_api_1 = require_admin_api();
1896
+ exports.ADMIN_PAYMENTS_PREFIX = `${admin_api_1.ADMIN_PREFIX}/payments`;
1897
+ exports.ADMIN_PAYMENTS_ROUTES = {
1898
+ probePaymentsConnectivity: () => `${exports.ADMIN_PAYMENTS_PREFIX}/probe`
1899
+ };
1900
+ }
1901
+ });
1902
+
1903
+ // ../shared/build/internal/payments/index.js
1904
+ var require_payments = __commonJS({
1905
+ "../shared/build/internal/payments/index.js"(exports) {
1906
+ "use strict";
1907
+ Object.defineProperty(exports, "__esModule", { value: true });
1908
+ exports.ADMIN_PAYMENTS_ROUTES = exports.ADMIN_PAYMENTS_PREFIX = void 0;
1909
+ var admin_payments_api_1 = require_admin_payments_api();
1910
+ Object.defineProperty(exports, "ADMIN_PAYMENTS_PREFIX", { enumerable: true, get: function() {
1911
+ return admin_payments_api_1.ADMIN_PAYMENTS_PREFIX;
1912
+ } });
1913
+ Object.defineProperty(exports, "ADMIN_PAYMENTS_ROUTES", { enumerable: true, get: function() {
1914
+ return admin_payments_api_1.ADMIN_PAYMENTS_ROUTES;
1915
+ } });
1916
+ }
1917
+ });
1918
+
1876
1919
  // ../shared/build/internal/subscriptions/subscription-source.enum.js
1877
1920
  var require_subscription_source_enum = __commonJS({
1878
1921
  "../shared/build/internal/subscriptions/subscription-source.enum.js"(exports) {
@@ -2090,6 +2133,7 @@ var require_internal = __commonJS({
2090
2133
  __exportStar(require_exception_codes(), exports);
2091
2134
  __exportStar(require_health(), exports);
2092
2135
  __exportStar(require_admin(), exports);
2136
+ __exportStar(require_payments(), exports);
2093
2137
  __exportStar(require_subscriptions2(), exports);
2094
2138
  }
2095
2139
  });
@@ -2428,7 +2472,8 @@ var require_agentic_sdlc_workflow = __commonJS({
2428
2472
  "in-analysis": {
2429
2473
  defaultNextSlug: "in-development",
2430
2474
  outcomes: {
2431
- [agent_run_outcome_enum_1.AGENT_RUN_OUTCOME.SKIP_DESIGN]: "in-development"
2475
+ [agent_run_outcome_enum_1.AGENT_RUN_OUTCOME.SKIP_DESIGN]: "in-development",
2476
+ [agent_run_outcome_enum_1.AGENT_RUN_OUTCOME.SKIP_DEV]: "released"
2432
2477
  }
2433
2478
  },
2434
2479
  "in-development": {
@@ -2470,7 +2515,7 @@ var require_agentic_sdlc_workflow = __commonJS({
2470
2515
  nextAgentRole
2471
2516
  };
2472
2517
  }
2473
- function resolveInAnalysisTransition(outcome, workflowPhase) {
2518
+ function resolveInAnalysisTransition(outcome, workflowPhase, context) {
2474
2519
  if (outcome === agent_run_outcome_enum_1.AGENT_RUN_OUTCOME.NEEDS_CLARIFICATION) {
2475
2520
  return moveTransition(exports.IN_AWAITING_COLUMN_SLUG, workflowPhase, null);
2476
2521
  }
@@ -2491,6 +2536,12 @@ var require_agentic_sdlc_workflow = __commonJS({
2491
2536
  if (outcome === agent_run_outcome_enum_1.AGENT_RUN_OUTCOME.SKIP_DESIGN) {
2492
2537
  return moveTransition("in-development", workflow_phase_enum_1.WORKFLOW_PHASE.DEVELOPMENT, null);
2493
2538
  }
2539
+ if (outcome === agent_run_outcome_enum_1.AGENT_RUN_OUTCOME.SKIP_DEV) {
2540
+ if (context.codeChangesRequired !== false) {
2541
+ return null;
2542
+ }
2543
+ return moveTransition("released", null, null);
2544
+ }
2494
2545
  return null;
2495
2546
  }
2496
2547
  if (workflowPhase === workflow_phase_enum_1.WORKFLOW_PHASE.ARCHITECT) {
@@ -2566,7 +2617,7 @@ var require_agentic_sdlc_workflow = __commonJS({
2566
2617
  }
2567
2618
  if (currentColumnSlug === "in-analysis") {
2568
2619
  const phase = context.workflowPhase ?? (0, workflow_phase_subagent_role_map_1.subagentRoleToWorkflowPhase)(context.agentRole);
2569
- return resolveInAnalysisTransition(outcome, phase);
2620
+ return resolveInAnalysisTransition(outcome, phase, context);
2570
2621
  }
2571
2622
  if (currentColumnSlug === "in-development") {
2572
2623
  return resolveInDevelopmentTransition(outcome, context);
@@ -3326,7 +3377,8 @@ var require_mcp_sensitive_data_util = __commonJS({
3326
3377
  exports.MCP_REDACTED_HOST = "[REDACTED_HOST]";
3327
3378
  exports.MCP_REDACTED_PORT = "[REDACTED_PORT]";
3328
3379
  exports.MCP_WORKSPACE_PLACEHOLDER = "[workspace]";
3329
- var SENSITIVE_OBJECT_KEY_PATTERN = /^(password|passwd|secret|clientsecret|client_secret|apikey|api_key|token|authorization|smtp_password|tokenpepper|token_pepper|privatekey|private_key)$/i;
3380
+ var SENSITIVE_OBJECT_KEY_PATTERN = /^(password|passwd|secret|clientsecret|client_secret|apikey|api_key|apisecret|api_secret|token|authorization|smtp_password|tokenpepper|token_pepper|privatekey|private_key|pan|cardnumber|card_number)$/i;
3381
+ var PAN_PATTERN = /\b(?:\d[ -]*?){13,19}\b/g;
3330
3382
  var BLOCKED_ATTACHMENT_FILE_NAME_PATTERN = /^(?:\.env(?:\..*)?|secrets\.ya?ml|config\.ya?ml|.*\.pem|.*\.p12|.*\.key|id_rsa(?:\..*)?|credentials\.json|\.npmrc|docker-compose\.ya?ml|compose\.ya?ml)$/i;
3331
3383
  var JWT_PATTERN = /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
3332
3384
  var BEARER_PATTERN = /Bearer\s+[A-Za-z0-9._~+/=-]+/gi;
@@ -3373,6 +3425,7 @@ var require_mcp_sensitive_data_util = __commonJS({
3373
3425
  result = result.replace(HTTPS_REMOTE_URL_PATTERN, `${exports.MCP_REDACTED_HOST}/...`);
3374
3426
  result = result.replace(ENV_ASSIGNMENT_PATTERN, `${exports.MCP_REDACTED}_ENV`);
3375
3427
  result = result.replace(NPM_TOKEN_PATTERN, `NpmToken.${exports.MCP_REDACTED}`);
3428
+ result = result.replace(PAN_PATTERN, exports.MCP_REDACTED);
3376
3429
  return result;
3377
3430
  }
3378
3431
  function redactSensitiveValueDeep(value, workspaceRoot) {
@@ -3551,43 +3604,44 @@ var require_build2 = __commonJS({
3551
3604
  }
3552
3605
  });
3553
3606
 
3554
- // src/index.ts
3555
- import { createRequire } from "node:module";
3556
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3557
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3607
+ // src/orchestrator/orchestrator-transport.ts
3608
+ var import_agent_runs2 = __toESM(require_agent_runs_api(), 1);
3609
+ var import_shared5 = __toESM(require_build2(), 1);
3558
3610
 
3559
- // src/config.ts
3560
- var import_api = __toESM(require_build(), 1);
3561
- function resolveSanitizeResponses() {
3562
- const raw = process.env.TASK_BOARDS_MCP_SANITIZE;
3563
- if (raw === void 0 || raw.trim() === "") {
3564
- return true;
3565
- }
3566
- const normalized = raw.trim().toLowerCase();
3567
- return normalized !== "false" && normalized !== "0" && normalized !== "no";
3611
+ // src/tools/attachments.ts
3612
+ var import_attachments = __toESM(require_attachments_api(), 1);
3613
+ var import_shared3 = __toESM(require_build2(), 1);
3614
+ import { existsSync as existsSync3 } from "node:fs";
3615
+ import { z } from "zod";
3616
+
3617
+ // src/attachments/detect-mime-type.ts
3618
+ import { basename } from "node:path";
3619
+ import { lookup as lookupMimeType } from "mime-types";
3620
+ function detectMimeType(filePath) {
3621
+ const mimeType = lookupMimeType(basename(filePath));
3622
+ return typeof mimeType === "string" ? mimeType : "application/octet-stream";
3568
3623
  }
3569
- function resolveBlockSensitiveAttachments() {
3570
- const raw = process.env.TASK_BOARDS_MCP_BLOCK_SENSITIVE_ATTACHMENTS;
3571
- if (raw === void 0 || raw.trim() === "") {
3572
- return true;
3573
- }
3574
- const normalized = raw.trim().toLowerCase();
3575
- return normalized !== "false" && normalized !== "0" && normalized !== "no";
3624
+
3625
+ // src/attachments/staging.util.ts
3626
+ import { mkdir, writeFile } from "node:fs/promises";
3627
+ import { dirname, resolve } from "node:path";
3628
+ var MAX_FILE_NAME_LENGTH = 200;
3629
+ function sanitizeFileName(name) {
3630
+ const baseName = name.split(/[/\\]/).pop() ?? name;
3631
+ const sanitized = baseName.replace(/[^a-zA-Z0-9._-]/g, "_");
3632
+ return sanitized.length > MAX_FILE_NAME_LENGTH ? sanitized.slice(0, MAX_FILE_NAME_LENGTH) : sanitized;
3576
3633
  }
3577
- function loadConfig() {
3578
- const apiUrl = process.env.TASK_BOARDS_API_URL ?? "http://localhost:3000";
3579
- const apiToken = process.env.TASK_BOARDS_API_TOKEN;
3580
- const workspaceRoot = process.env.WORKSPACE_ROOT;
3581
- if (process.env.NODE_ENV === "production" && (apiToken === void 0 || apiToken.trim() === "")) {
3582
- console.error("WARNING: TASK_BOARDS_API_TOKEN is not set. MCP server cannot authenticate to the Task Boards API.");
3583
- }
3584
- return {
3585
- apiUrl: `${apiUrl.replace(/\/$/, "")}${import_api.API_V1_PREFIX}`,
3586
- apiToken,
3587
- workspaceRoot: workspaceRoot !== void 0 && workspaceRoot.trim() !== "" ? workspaceRoot : void 0,
3588
- sanitizeResponses: resolveSanitizeResponses(),
3589
- blockSensitiveAttachments: resolveBlockSensitiveAttachments()
3590
- };
3634
+ function buildStagingPath(workspaceRoot, workItemId, attachmentId, fileName) {
3635
+ const sanitizedFileName = sanitizeFileName(fileName);
3636
+ return resolve(workspaceRoot, ".task-boards", "attachments", workItemId, `${attachmentId}_${sanitizedFileName}`);
3637
+ }
3638
+ function buildStagingDir(workspaceRoot, workItemId) {
3639
+ return resolve(workspaceRoot, ".task-boards", "attachments", workItemId);
3640
+ }
3641
+ async function writeStagingFile(path, data) {
3642
+ const absolutePath = resolve(path);
3643
+ await mkdir(dirname(absolutePath), { recursive: true });
3644
+ await writeFile(absolutePath, data);
3591
3645
  }
3592
3646
 
3593
3647
  // src/rest-client.ts
@@ -3694,6 +3748,121 @@ var RestClient = class {
3694
3748
  }
3695
3749
  };
3696
3750
 
3751
+ // src/workspace/confine-file-path.ts
3752
+ import { existsSync, readFileSync, statSync } from "node:fs";
3753
+ import { basename as basename2, isAbsolute, relative, resolve as resolve2 } from "node:path";
3754
+
3755
+ // src/workspace/workspace-error.ts
3756
+ var WorkspaceError = class extends Error {
3757
+ constructor(code, message) {
3758
+ super(message);
3759
+ this.code = code;
3760
+ this.name = "WorkspaceError";
3761
+ }
3762
+ };
3763
+
3764
+ // src/workspace/confine-file-path.ts
3765
+ function confineFilePath(workspaceRoot, filePath) {
3766
+ const resolvedRoot = resolve2(workspaceRoot);
3767
+ const resolvedPath = isAbsolute(filePath) ? resolve2(filePath) : resolve2(resolvedRoot, filePath);
3768
+ const relativePath = relative(resolvedRoot, resolvedPath);
3769
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
3770
+ throw new WorkspaceError(
3771
+ "FILE_OUT_OF_BOUNDS",
3772
+ `filePath is outside workspaceRoot: ${basename2(filePath) || filePath}`
3773
+ );
3774
+ }
3775
+ if (!existsSync(resolvedPath)) {
3776
+ throw new WorkspaceError("FILE_NOT_FOUND", `filePath does not exist: ${basename2(filePath) || filePath}`);
3777
+ }
3778
+ const stats = statSync(resolvedPath);
3779
+ if (!stats.isFile()) {
3780
+ throw new WorkspaceError("FILE_NOT_FOUND", `filePath is not a file: ${basename2(filePath) || filePath}`);
3781
+ }
3782
+ return resolvedPath;
3783
+ }
3784
+ function readConfinedWorkspaceFile(workspaceRoot, filePath) {
3785
+ const absolutePath = confineFilePath(workspaceRoot, filePath);
3786
+ const stats = statSync(absolutePath);
3787
+ const buffer = readFileSync(absolutePath);
3788
+ return {
3789
+ absolutePath,
3790
+ fileName: basename2(absolutePath),
3791
+ sizeBytes: stats.size,
3792
+ buffer
3793
+ };
3794
+ }
3795
+
3796
+ // src/workspace/resolve-workspace-root.ts
3797
+ import { existsSync as existsSync2, statSync as statSync2 } from "node:fs";
3798
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join, relative as relative2, resolve as resolve3 } from "node:path";
3799
+ var TASK_BOARDS_FILE = ".task-boards.yaml";
3800
+ var MAX_UPWARD_LEVELS = 20;
3801
+ function assertDirectory(path, source) {
3802
+ const absolutePath = resolve3(path);
3803
+ if (!existsSync2(absolutePath)) {
3804
+ throw new WorkspaceError("WORKSPACE_NOT_FOUND", `${source} does not exist: ${absolutePath}`);
3805
+ }
3806
+ const stats = statSync2(absolutePath);
3807
+ if (!stats.isDirectory()) {
3808
+ throw new WorkspaceError("WORKSPACE_NOT_FOUND", `${source} is not a directory: ${absolutePath}`);
3809
+ }
3810
+ return absolutePath;
3811
+ }
3812
+ function assertWithinAllowedRoot(target, allowedRoot) {
3813
+ const relativePath = relative2(allowedRoot, target);
3814
+ const isNested = relativePath !== "" && !relativePath.startsWith("..") && !isAbsolute2(relativePath);
3815
+ if (target !== allowedRoot && !isNested) {
3816
+ throw new WorkspaceError(
3817
+ "WORKSPACE_OUT_OF_BOUNDS",
3818
+ `workspaceRoot is outside the allowed WORKSPACE_ROOT: ${target}`
3819
+ );
3820
+ }
3821
+ }
3822
+ function assertGitRepository(absolutePath) {
3823
+ if (!existsSync2(join(absolutePath, ".git"))) {
3824
+ throw new WorkspaceError("WORKSPACE_NOT_GIT_REPO", `workspaceRoot is not a git repository: ${absolutePath}`);
3825
+ }
3826
+ }
3827
+ function findWorkspaceRootUpward(startDir) {
3828
+ let current = resolve3(startDir);
3829
+ for (let level = 0; level <= MAX_UPWARD_LEVELS; level += 1) {
3830
+ const markerPath = join(current, TASK_BOARDS_FILE);
3831
+ if (existsSync2(markerPath)) {
3832
+ return current;
3833
+ }
3834
+ const parent = dirname2(current);
3835
+ if (parent === current) {
3836
+ break;
3837
+ }
3838
+ current = parent;
3839
+ }
3840
+ return null;
3841
+ }
3842
+ function resolveWorkspaceRoot(explicitOverride, envWorkspaceRoot) {
3843
+ const allowedRoot = envWorkspaceRoot !== void 0 && envWorkspaceRoot.trim() !== "" ? assertDirectory(envWorkspaceRoot, "WORKSPACE_ROOT") : void 0;
3844
+ if (explicitOverride !== void 0 && explicitOverride.trim() !== "") {
3845
+ const resolved = assertDirectory(explicitOverride, "workspaceRoot");
3846
+ if (allowedRoot !== void 0) {
3847
+ assertWithinAllowedRoot(resolved, allowedRoot);
3848
+ } else {
3849
+ assertGitRepository(resolved);
3850
+ }
3851
+ return resolved;
3852
+ }
3853
+ if (allowedRoot !== void 0) {
3854
+ return allowedRoot;
3855
+ }
3856
+ const fromMarker = findWorkspaceRootUpward(process.cwd());
3857
+ if (fromMarker !== null) {
3858
+ return fromMarker;
3859
+ }
3860
+ return resolve3(process.cwd());
3861
+ }
3862
+
3863
+ // src/tools/tool-utils.ts
3864
+ var import_shared2 = __toESM(require_build2(), 1);
3865
+
3697
3866
  // src/tool-runtime.ts
3698
3867
  var serverConfig = null;
3699
3868
  function initToolRuntime(config) {
@@ -3706,24 +3875,577 @@ function shouldSanitizeResponses() {
3706
3875
  return serverConfig.sanitizeResponses !== false;
3707
3876
  }
3708
3877
 
3709
- // src/tools/agent-runs.ts
3710
- var import_agent_runs2 = __toESM(require_agent_runs_api(), 1);
3711
- var import_agent_run_outcome = __toESM(require_agent_run_outcome_enum(), 1);
3712
- var import_agent_run_status4 = __toESM(require_agent_run_status_enum(), 1);
3713
- import { z } from "zod";
3714
-
3715
- // src/tools/orchestrator-hints.ts
3716
- var import_agent_run_status2 = __toESM(require_agent_run_status_enum(), 1);
3717
- var import_subagent_role = __toESM(require_subagent_role_enum(), 1);
3878
+ // src/tools/tool-utils.ts
3879
+ function jsonResult(data) {
3880
+ return {
3881
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
3882
+ };
3883
+ }
3884
+ function toolError(code, message) {
3885
+ const sanitizedMessage = shouldSanitizeResponses() ? (0, import_shared2.sanitizeMcpErrorMessage)(message) : message;
3886
+ return {
3887
+ isError: true,
3888
+ content: [{ type: "text", text: JSON.stringify({ code, message: sanitizedMessage }) }]
3889
+ };
3890
+ }
3891
+ function toToolError(error) {
3892
+ if (error instanceof RestClientError || error instanceof WorkspaceError) {
3893
+ return toolError(error.code, error.message);
3894
+ }
3895
+ const message = error instanceof Error ? error.message : "Unknown error";
3896
+ return toolError("INTERNAL_ERROR", message);
3897
+ }
3898
+ async function runTool(handler, options) {
3899
+ try {
3900
+ let data = await handler();
3901
+ if (shouldSanitizeResponses()) {
3902
+ data = (0, import_shared2.sanitizeMcpToolResult)(data, { workspaceRoot: options?.workspaceRoot });
3903
+ }
3904
+ return jsonResult(data);
3905
+ } catch (error) {
3906
+ return toToolError(error);
3907
+ }
3908
+ }
3718
3909
 
3719
- // src/tools/orchestrator-attention.util.ts
3720
- var import_agent_run_status = __toESM(require_agent_run_status_enum(), 1);
3721
- var ATTENTION_MESSAGE_CATEGORY = {
3722
- SHELL: "shell",
3723
- GIT: "git",
3724
- NETWORK: "network",
3725
- SMART_MODE: "smart-mode",
3726
- MCP: "mcp",
3910
+ // src/tools/attachments.ts
3911
+ var MAX_ATTACHMENT_COUNT = 20;
3912
+ var MAX_TOTAL_BYTES = 200 * 1024 * 1024;
3913
+ async function fetchAttachments(client, workItemId) {
3914
+ return client.get(import_attachments.ATTACHMENTS_ROUTES.listAttachments(workItemId));
3915
+ }
3916
+ function filterAttachments(items, attachmentIds) {
3917
+ if (attachmentIds === void 0 || attachmentIds.length === 0) {
3918
+ return items;
3919
+ }
3920
+ const idSet = new Set(attachmentIds);
3921
+ return items.filter((item) => idSet.has(item.id));
3922
+ }
3923
+ function assertDownloadLimits(attachments) {
3924
+ if (attachments.length > MAX_ATTACHMENT_COUNT) {
3925
+ throw new RestClientError(
3926
+ "ATTACHMENT_LIMIT_EXCEEDED",
3927
+ `Cannot download more than ${MAX_ATTACHMENT_COUNT} attachments at once (requested ${attachments.length})`
3928
+ );
3929
+ }
3930
+ const totalBytes = attachments.reduce((sum, attachment) => sum + attachment.sizeBytes, 0);
3931
+ if (totalBytes > MAX_TOTAL_BYTES) {
3932
+ throw new RestClientError(
3933
+ "ATTACHMENT_SIZE_LIMIT_EXCEEDED",
3934
+ `Total attachment size ${totalBytes} bytes exceeds limit of ${MAX_TOTAL_BYTES} bytes`
3935
+ );
3936
+ }
3937
+ }
3938
+ async function downloadWorkItemAttachments(client, params) {
3939
+ const { workItemId, workspaceRoot, attachmentIds, overwrite = false, blockSensitiveAttachments = true } = params;
3940
+ const listResponse = await fetchAttachments(client, workItemId);
3941
+ const selected = filterAttachments(listResponse.items, attachmentIds);
3942
+ assertDownloadLimits(selected);
3943
+ const stagingDir = buildStagingDir(workspaceRoot, workItemId);
3944
+ const downloaded = [];
3945
+ const skipped = [];
3946
+ if (attachmentIds !== void 0 && attachmentIds.length > 0) {
3947
+ const selectedIds = new Set(selected.map((item) => item.id));
3948
+ for (const attachmentId of attachmentIds) {
3949
+ if (!selectedIds.has(attachmentId)) {
3950
+ skipped.push({ attachmentId, reason: "not found on work item" });
3951
+ }
3952
+ }
3953
+ }
3954
+ for (const attachment of selected) {
3955
+ if (blockSensitiveAttachments && (0, import_shared3.isBlockedMcpAttachmentFileName)(attachment.fileName)) {
3956
+ skipped.push({ attachmentId: attachment.id, reason: "blocked_sensitive_filename" });
3957
+ continue;
3958
+ }
3959
+ const stagingPath = buildStagingPath(workspaceRoot, workItemId, attachment.id, attachment.fileName);
3960
+ if (!overwrite && existsSync3(stagingPath)) {
3961
+ skipped.push({ attachmentId: attachment.id, reason: "file already exists (set overwrite=true to replace)" });
3962
+ continue;
3963
+ }
3964
+ const { data } = await client.downloadBinary(import_attachments.ATTACHMENTS_ROUTES.downloadAttachment(workItemId, attachment.id));
3965
+ await writeStagingFile(stagingPath, data);
3966
+ downloaded.push({
3967
+ attachmentId: attachment.id,
3968
+ fileName: attachment.fileName,
3969
+ path: stagingPath,
3970
+ sizeBytes: data.length
3971
+ });
3972
+ }
3973
+ return {
3974
+ workItemId,
3975
+ downloaded,
3976
+ skipped,
3977
+ stagingDir
3978
+ };
3979
+ }
3980
+ async function uploadWorkItemAttachment(client, params) {
3981
+ const { workItemId, workspaceRoot, filePath, blockSensitiveAttachments = true } = params;
3982
+ const confinedFile = readConfinedWorkspaceFile(workspaceRoot, filePath);
3983
+ if (blockSensitiveAttachments && (0, import_shared3.isBlockedMcpAttachmentFileName)(confinedFile.fileName)) {
3984
+ throw new RestClientError(
3985
+ "BLOCKED_SENSITIVE_FILENAME",
3986
+ `Attachment filename is blocked for MCP upload: ${confinedFile.fileName}`
3987
+ );
3988
+ }
3989
+ const mimeType = detectMimeType(confinedFile.absolutePath);
3990
+ const attachment = await client.uploadMultipart(
3991
+ import_attachments.ATTACHMENTS_ROUTES.uploadAttachment(workItemId),
3992
+ "file",
3993
+ {
3994
+ buffer: confinedFile.buffer,
3995
+ fileName: confinedFile.fileName,
3996
+ mimeType
3997
+ }
3998
+ );
3999
+ return {
4000
+ workItemId,
4001
+ attachment,
4002
+ sourcePath: confinedFile.absolutePath
4003
+ };
4004
+ }
4005
+ function registerAttachmentTools(server, client, config) {
4006
+ server.registerTool(
4007
+ "list_work_item_attachments",
4008
+ {
4009
+ description: "List file attachments for a work item.",
4010
+ inputSchema: {
4011
+ workItemId: z.string().uuid().describe("Work item UUID")
4012
+ }
4013
+ },
4014
+ async ({ workItemId }) => runTool(async () => fetchAttachments(client, workItemId))
4015
+ );
4016
+ server.registerTool(
4017
+ "download_work_item_attachments",
4018
+ {
4019
+ description: "Download work item attachments into the local workspace staging directory (.task-boards/attachments).",
4020
+ inputSchema: {
4021
+ workItemId: z.string().uuid().describe("Work item UUID"),
4022
+ attachmentIds: z.array(z.string().uuid()).optional().describe("Optional subset of attachment UUIDs; downloads all when omitted"),
4023
+ overwrite: z.boolean().optional().default(false).describe("Replace existing staged files when true"),
4024
+ workspaceRoot: z.string().optional().describe("Optional absolute path to git repo root; defaults to WORKSPACE_ROOT or upward search from cwd")
4025
+ }
4026
+ },
4027
+ async ({ workItemId, attachmentIds, overwrite, workspaceRoot }) => {
4028
+ const resolvedWorkspaceRoot = resolveWorkspaceRoot(workspaceRoot, config.workspaceRoot);
4029
+ return runTool(
4030
+ async () => downloadWorkItemAttachments(client, {
4031
+ workItemId,
4032
+ workspaceRoot: resolvedWorkspaceRoot,
4033
+ attachmentIds,
4034
+ overwrite,
4035
+ blockSensitiveAttachments: config.blockSensitiveAttachments
4036
+ }),
4037
+ { workspaceRoot: resolvedWorkspaceRoot }
4038
+ );
4039
+ }
4040
+ );
4041
+ server.registerTool(
4042
+ "upload_work_item_attachment",
4043
+ {
4044
+ description: "Upload a local workspace file as a work item attachment (multipart field file).",
4045
+ inputSchema: {
4046
+ workItemId: z.string().uuid().describe("Work item UUID"),
4047
+ filePath: z.string().describe("Absolute or workspace-relative path to the file; must stay within workspaceRoot"),
4048
+ workspaceRoot: z.string().optional().describe("Optional absolute path to git repo root; defaults to WORKSPACE_ROOT or upward search from cwd")
4049
+ }
4050
+ },
4051
+ async ({ workItemId, filePath, workspaceRoot }) => {
4052
+ const resolvedWorkspaceRoot = resolveWorkspaceRoot(workspaceRoot, config.workspaceRoot);
4053
+ return runTool(
4054
+ async () => uploadWorkItemAttachment(client, {
4055
+ workItemId,
4056
+ workspaceRoot: resolvedWorkspaceRoot,
4057
+ filePath,
4058
+ blockSensitiveAttachments: config.blockSensitiveAttachments
4059
+ }),
4060
+ { workspaceRoot: resolvedWorkspaceRoot }
4061
+ );
4062
+ }
4063
+ );
4064
+ }
4065
+
4066
+ // src/tools/comments.ts
4067
+ var import_comments = __toESM(require_comments_api(), 1);
4068
+ import { z as z2 } from "zod";
4069
+ async function fetchComments(client, workItemId) {
4070
+ return client.get(import_comments.COMMENTS_ROUTES.listComments(workItemId));
4071
+ }
4072
+ async function postComment(client, workItemId, body) {
4073
+ const request = { body };
4074
+ return client.post(import_comments.COMMENTS_ROUTES.createComment(workItemId), request);
4075
+ }
4076
+ function registerCommentTools(server, client) {
4077
+ server.registerTool(
4078
+ "list_comments",
4079
+ {
4080
+ description: "List comments for a work item (newest last).",
4081
+ inputSchema: {
4082
+ workItemId: z2.string().uuid().describe("Work item UUID")
4083
+ }
4084
+ },
4085
+ async ({ workItemId }) => runTool(async () => fetchComments(client, workItemId))
4086
+ );
4087
+ server.registerTool(
4088
+ "create_comment",
4089
+ {
4090
+ description: "Create a comment on a work item. Use for orchestrator NEEDS_CLARIFICATION questions before complete_agent_run.",
4091
+ inputSchema: {
4092
+ workItemId: z2.string().uuid().describe("Work item UUID"),
4093
+ body: z2.string().min(1).describe("Comment body (markdown supported)")
4094
+ }
4095
+ },
4096
+ async ({ workItemId, body }) => runTool(async () => postComment(client, workItemId, body))
4097
+ );
4098
+ }
4099
+
4100
+ // src/subagents/sync-project-subagents.ts
4101
+ var import_shared4 = __toESM(require_build2(), 1);
4102
+ import { mkdirSync, writeFileSync } from "node:fs";
4103
+ import { join as join2 } from "node:path";
4104
+ var SAFE_SUBAGENT_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
4105
+ var MAX_SUBAGENT_SLUG_LENGTH = 64;
4106
+ function isSyncableSlug(slug) {
4107
+ const trimmed = slug.trim();
4108
+ return trimmed.length > 0 && trimmed.length <= MAX_SUBAGENT_SLUG_LENGTH && SAFE_SUBAGENT_SLUG_PATTERN.test(trimmed);
4109
+ }
4110
+ function buildFileContent(subagent) {
4111
+ return (0, import_shared4.buildSubagentFileContentFromBody)({
4112
+ slug: subagent.slug,
4113
+ description: subagent.description,
4114
+ bodyMarkdown: subagent.bodyMarkdown
4115
+ });
4116
+ }
4117
+ async function syncProjectSubagents(client, params) {
4118
+ const { projectId, workspaceRoot } = params;
4119
+ const listResponse = await client.get(import_shared4.PROJECT_SUBAGENTS_ROUTES.listSubagents(projectId));
4120
+ const directory = join2(workspaceRoot, ".cursor", "agents");
4121
+ mkdirSync(directory, { recursive: true });
4122
+ const synced = [];
4123
+ const skipped = [];
4124
+ for (const subagent of listResponse.items) {
4125
+ if (!isSyncableSlug(subagent.slug)) {
4126
+ skipped.push(subagent.slug);
4127
+ continue;
4128
+ }
4129
+ const filePath = join2(directory, `${subagent.slug}.md`);
4130
+ writeFileSync(filePath, buildFileContent(subagent), "utf8");
4131
+ synced.push(subagent.slug);
4132
+ }
4133
+ return { synced, skipped, directory };
4134
+ }
4135
+
4136
+ // src/tools/wait-for-agent-runs.ts
4137
+ var import_agent_runs = __toESM(require_agent_runs_api(), 1);
4138
+ var import_agent_run_status = __toESM(require_agent_run_status_enum(), 1);
4139
+ var DEFAULT_RETRY_BASE_MS = 500;
4140
+ var DEFAULT_RETRY_MAX_MS = 3e4;
4141
+ var DEFAULT_INFLIGHT_LIMIT = 10;
4142
+ function sleep(ms) {
4143
+ return new Promise((resolve4) => {
4144
+ setTimeout(resolve4, ms);
4145
+ });
4146
+ }
4147
+ function isTransientClaimError(error) {
4148
+ if (!(error instanceof RestClientError)) {
4149
+ return false;
4150
+ }
4151
+ if (error.status === void 0) {
4152
+ return error.code === "HTTP_ERROR";
4153
+ }
4154
+ return error.status >= 500;
4155
+ }
4156
+ function computeBackoffMs(consecutiveFailures, baseMs, maxMs) {
4157
+ const exponential = baseMs * 2 ** (consecutiveFailures - 1);
4158
+ return Math.min(exponential, maxMs);
4159
+ }
4160
+ function compareInflightRuns(a, b) {
4161
+ if (a.requiresAttention !== b.requiresAttention) {
4162
+ return a.requiresAttention ? -1 : 1;
4163
+ }
4164
+ const aDispatched = a.dispatchedAt ?? "";
4165
+ const bDispatched = b.dispatchedAt ?? "";
4166
+ return aDispatched.localeCompare(bDispatched);
4167
+ }
4168
+ async function fetchInflightAgentRuns(client, options) {
4169
+ const limit = options.limit ?? DEFAULT_INFLIGHT_LIMIT;
4170
+ const page = await client.get(import_agent_runs.AGENT_RUNS_ROUTES.listAgentRuns(), {
4171
+ projectId: options.projectId,
4172
+ activeOnly: "true",
4173
+ limit: String(limit)
4174
+ });
4175
+ const filtered = page.items.filter(
4176
+ (run) => run.status === import_agent_run_status.AGENT_RUN_STATUS.DISPATCHED || run.status === import_agent_run_status.AGENT_RUN_STATUS.ACKNOWLEDGED
4177
+ );
4178
+ filtered.sort(compareInflightRuns);
4179
+ return filtered.slice(0, limit);
4180
+ }
4181
+ async function pollAgentRuns(client, options, deps) {
4182
+ const { projectId, timeoutMs, pollIntervalMs, limit } = options;
4183
+ if (pollIntervalMs > timeoutMs) {
4184
+ throw new WorkspaceError("VALIDATION_ERROR", "pollInterval must not exceed timeout");
4185
+ }
4186
+ const sleepFn = deps?.sleep ?? sleep;
4187
+ const deadline = Date.now() + timeoutMs;
4188
+ const claimLimit = limit ?? 1;
4189
+ const retryBaseMs = options.retryBaseMs ?? DEFAULT_RETRY_BASE_MS;
4190
+ const retryMaxMs = options.retryMaxMs ?? DEFAULT_RETRY_MAX_MS;
4191
+ let consecutiveFailures = 0;
4192
+ while (true) {
4193
+ let response;
4194
+ try {
4195
+ response = await client.post(import_agent_runs.AGENT_RUNS_ROUTES.claimAgentRuns(), {
4196
+ projectId,
4197
+ limit: claimLimit
4198
+ });
4199
+ consecutiveFailures = 0;
4200
+ } catch (error) {
4201
+ if (!isTransientClaimError(error)) {
4202
+ throw error;
4203
+ }
4204
+ if (Date.now() >= deadline) {
4205
+ return { items: [], inflightRuns: [], timedOut: true };
4206
+ }
4207
+ consecutiveFailures += 1;
4208
+ const backoffMs = computeBackoffMs(consecutiveFailures, retryBaseMs, retryMaxMs);
4209
+ const remainingMs2 = deadline - Date.now();
4210
+ await sleepFn(Math.min(backoffMs, remainingMs2));
4211
+ continue;
4212
+ }
4213
+ if (response.items.length > 0) {
4214
+ return { items: response.items, inflightRuns: [], timedOut: false };
4215
+ }
4216
+ const inflightRuns = await fetchInflightAgentRuns(client, { projectId, limit: claimLimit });
4217
+ if (inflightRuns.length > 0) {
4218
+ return { items: [], inflightRuns, timedOut: false };
4219
+ }
4220
+ if (Date.now() >= deadline) {
4221
+ return { items: [], inflightRuns: [], timedOut: true };
4222
+ }
4223
+ const remainingMs = deadline - Date.now();
4224
+ const waitMs = Math.min(pollIntervalMs, remainingMs);
4225
+ await sleepFn(waitMs);
4226
+ }
4227
+ }
4228
+
4229
+ // src/orchestrator/orchestrator-transport.ts
4230
+ var OrchestratorTransport = class {
4231
+ constructor(client, options = {}) {
4232
+ this.client = client;
4233
+ this.options = options;
4234
+ }
4235
+ async claimAndPoll(options, deps) {
4236
+ return pollAgentRuns(this.client, options, deps);
4237
+ }
4238
+ async fetchInflightRuns(projectId, limit) {
4239
+ return fetchInflightAgentRuns(this.client, { projectId, limit });
4240
+ }
4241
+ async ackRun(agentRunId, note) {
4242
+ const body = note !== void 0 ? { note } : void 0;
4243
+ return this.client.patch(import_agent_runs2.AGENT_RUNS_ROUTES.acknowledgeAgentRun(agentRunId), body);
4244
+ }
4245
+ async setAttention(agentRunId, requiresAttention, message) {
4246
+ const body = requiresAttention ? { requiresAttention: true, attentionMessage: message ?? "" } : { requiresAttention: false };
4247
+ return this.client.patch(import_agent_runs2.AGENT_RUNS_ROUTES.setAgentRunAttention(agentRunId), body);
4248
+ }
4249
+ async completeRun(agentRunId, body) {
4250
+ return this.client.post(import_agent_runs2.AGENT_RUNS_ROUTES.completeAgentRun(agentRunId), body);
4251
+ }
4252
+ async listAttachments(workItemId) {
4253
+ return fetchAttachments(this.client, workItemId);
4254
+ }
4255
+ async downloadAttachments(workItemId, workspaceRoot, attachmentIds) {
4256
+ return downloadWorkItemAttachments(this.client, {
4257
+ workItemId,
4258
+ workspaceRoot,
4259
+ attachmentIds,
4260
+ blockSensitiveAttachments: this.options.blockSensitiveAttachments ?? true
4261
+ });
4262
+ }
4263
+ async syncSubagents(projectId, workspaceRoot) {
4264
+ return syncProjectSubagents(this.client, { projectId, workspaceRoot });
4265
+ }
4266
+ async createComment(workItemId, body) {
4267
+ return postComment(this.client, workItemId, body);
4268
+ }
4269
+ };
4270
+ function createOrchestratorTransport(client, config) {
4271
+ return new OrchestratorTransport(client, {
4272
+ workspaceRoot: config?.workspaceRoot,
4273
+ blockSensitiveAttachments: config?.blockSensitiveAttachments
4274
+ });
4275
+ }
4276
+
4277
+ // src/orchestrator/active-run-state.ts
4278
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
4279
+ import { join as join3 } from "node:path";
4280
+ var ACTIVE_RUN_STATE_RELATIVE_PATH = ".cursor/orchestrator-active-run.json";
4281
+ var ATTENTION_PENDING_RELATIVE_PATH = ".cursor/orchestrator-attention-pending.json";
4282
+ var ACTIVE_RUN_LEASE_MINUTES = 30;
4283
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
4284
+ function isRecord(value) {
4285
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4286
+ }
4287
+ function isUuid(value) {
4288
+ return UUID_PATTERN.test(value);
4289
+ }
4290
+ function isIsoDateTimeString(value) {
4291
+ return !Number.isNaN(Date.parse(value));
4292
+ }
4293
+ function getActiveRunStatePath(workspaceRoot) {
4294
+ return join3(workspaceRoot, ACTIVE_RUN_STATE_RELATIVE_PATH);
4295
+ }
4296
+ function getAttentionPendingPath(workspaceRoot) {
4297
+ return join3(workspaceRoot, ATTENTION_PENDING_RELATIVE_PATH);
4298
+ }
4299
+ function computeExpiresAt(acknowledgedAt, leaseMinutes = ACTIVE_RUN_LEASE_MINUTES) {
4300
+ const expiresMs = Date.parse(acknowledgedAt) + leaseMinutes * 60 * 1e3;
4301
+ return new Date(expiresMs).toISOString();
4302
+ }
4303
+ function ensureCursorDir(workspaceRoot) {
4304
+ const cursorDir = join3(workspaceRoot, ".cursor");
4305
+ if (!existsSync4(cursorDir)) {
4306
+ mkdirSync2(cursorDir, { recursive: true });
4307
+ }
4308
+ }
4309
+ function parseActiveRunState(raw) {
4310
+ if (!isRecord(raw)) {
4311
+ return null;
4312
+ }
4313
+ const version = raw.version;
4314
+ const agentRunId = raw.agentRunId;
4315
+ const workItemId = raw.workItemId;
4316
+ const projectId = raw.projectId;
4317
+ const acknowledgedAt = raw.acknowledgedAt;
4318
+ const expiresAt = raw.expiresAt;
4319
+ if (version !== 1 || typeof agentRunId !== "string" || typeof workItemId !== "string" || typeof projectId !== "string" || typeof acknowledgedAt !== "string" || typeof expiresAt !== "string" || !isUuid(agentRunId) || !isUuid(workItemId) || !isUuid(projectId) || !isIsoDateTimeString(acknowledgedAt) || !isIsoDateTimeString(expiresAt)) {
4320
+ return null;
4321
+ }
4322
+ return {
4323
+ version: 1,
4324
+ agentRunId,
4325
+ workItemId,
4326
+ projectId,
4327
+ acknowledgedAt,
4328
+ expiresAt
4329
+ };
4330
+ }
4331
+ function parseAttentionPendingState(raw) {
4332
+ if (!isRecord(raw)) {
4333
+ return null;
4334
+ }
4335
+ const agentRunId = raw.agentRunId;
4336
+ const reportedAt = raw.reportedAt;
4337
+ if (typeof agentRunId !== "string" || typeof reportedAt !== "string" || !isUuid(agentRunId) || !isIsoDateTimeString(reportedAt)) {
4338
+ return null;
4339
+ }
4340
+ return { agentRunId, reportedAt };
4341
+ }
4342
+ function isActiveRunStateUsable(state, nowMs = Date.now()) {
4343
+ if (state === null) {
4344
+ return false;
4345
+ }
4346
+ const expiresMs = Date.parse(state.expiresAt);
4347
+ if (Number.isNaN(expiresMs)) {
4348
+ return false;
4349
+ }
4350
+ return nowMs < expiresMs;
4351
+ }
4352
+ function readActiveRunState(workspaceRoot) {
4353
+ const filePath = getActiveRunStatePath(workspaceRoot);
4354
+ if (!existsSync4(filePath)) {
4355
+ return null;
4356
+ }
4357
+ try {
4358
+ const content = readFileSync2(filePath, "utf8");
4359
+ const parsed = JSON.parse(content);
4360
+ return parseActiveRunState(parsed);
4361
+ } catch {
4362
+ return null;
4363
+ }
4364
+ }
4365
+ function writeActiveRunState(workspaceRoot, input) {
4366
+ ensureCursorDir(workspaceRoot);
4367
+ const state = {
4368
+ version: 1,
4369
+ agentRunId: input.agentRunId,
4370
+ workItemId: input.workItemId,
4371
+ projectId: input.projectId,
4372
+ acknowledgedAt: input.acknowledgedAt,
4373
+ expiresAt: computeExpiresAt(input.acknowledgedAt)
4374
+ };
4375
+ writeFileSync2(getActiveRunStatePath(workspaceRoot), `${JSON.stringify(state, null, 2)}
4376
+ `, "utf8");
4377
+ return state;
4378
+ }
4379
+ function clearActiveRunState(workspaceRoot) {
4380
+ const filePath = getActiveRunStatePath(workspaceRoot);
4381
+ if (existsSync4(filePath)) {
4382
+ rmSync(filePath, { force: true });
4383
+ }
4384
+ }
4385
+ function readAttentionPendingState(workspaceRoot) {
4386
+ const filePath = getAttentionPendingPath(workspaceRoot);
4387
+ if (!existsSync4(filePath)) {
4388
+ return null;
4389
+ }
4390
+ try {
4391
+ const content = readFileSync2(filePath, "utf8");
4392
+ const parsed = JSON.parse(content);
4393
+ return parseAttentionPendingState(parsed);
4394
+ } catch {
4395
+ return null;
4396
+ }
4397
+ }
4398
+ function writeAttentionPendingState(workspaceRoot, agentRunId) {
4399
+ ensureCursorDir(workspaceRoot);
4400
+ const pending = {
4401
+ agentRunId,
4402
+ reportedAt: (/* @__PURE__ */ new Date()).toISOString()
4403
+ };
4404
+ writeFileSync2(getAttentionPendingPath(workspaceRoot), `${JSON.stringify(pending, null, 2)}
4405
+ `, "utf8");
4406
+ return pending;
4407
+ }
4408
+ function clearAttentionPendingState(workspaceRoot) {
4409
+ const filePath = getAttentionPendingPath(workspaceRoot);
4410
+ if (existsSync4(filePath)) {
4411
+ rmSync(filePath, { force: true });
4412
+ }
4413
+ }
4414
+ function clearOrchestratorBridgeState(workspaceRoot) {
4415
+ clearActiveRunState(workspaceRoot);
4416
+ clearAttentionPendingState(workspaceRoot);
4417
+ }
4418
+
4419
+ // src/workspace/parse-task-boards-yaml.ts
4420
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
4421
+ import { join as join4 } from "node:path";
4422
+ var TASK_BOARDS_FILE2 = ".task-boards.yaml";
4423
+ var VERSION_PATTERN = /^\s*version\s*:\s*1\s*$/m;
4424
+ var PROJECT_ID_PATTERN = /^\s*projectId\s*:\s*["']?([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})["']?\s*$/im;
4425
+ function parseTaskBoardsYaml(workspaceRoot) {
4426
+ const filePath = join4(workspaceRoot, TASK_BOARDS_FILE2);
4427
+ if (!existsSync5(filePath)) {
4428
+ return null;
4429
+ }
4430
+ const content = readFileSync3(filePath, "utf8");
4431
+ if (!VERSION_PATTERN.test(content)) {
4432
+ return null;
4433
+ }
4434
+ const match = PROJECT_ID_PATTERN.exec(content);
4435
+ if (match === null) {
4436
+ return null;
4437
+ }
4438
+ return match[1] ?? null;
4439
+ }
4440
+
4441
+ // src/tools/orchestrator-attention.util.ts
4442
+ var import_agent_run_status2 = __toESM(require_agent_run_status_enum(), 1);
4443
+ var ATTENTION_MESSAGE_CATEGORY = {
4444
+ SHELL: "shell",
4445
+ GIT: "git",
4446
+ NETWORK: "network",
4447
+ SMART_MODE: "smart-mode",
4448
+ MCP: "mcp",
3727
4449
  FILE: "file",
3728
4450
  OTHER: "other"
3729
4451
  };
@@ -3780,10 +4502,10 @@ function deriveOrchestratorRunPhase(run) {
3780
4502
  if (run.requiresAttention) {
3781
4503
  return "awaiting_ide_approval";
3782
4504
  }
3783
- if (run.status === import_agent_run_status.AGENT_RUN_STATUS.DISPATCHED) {
4505
+ if (run.status === import_agent_run_status2.AGENT_RUN_STATUS.DISPATCHED) {
3784
4506
  return "dispatched";
3785
4507
  }
3786
- if (run.status === import_agent_run_status.AGENT_RUN_STATUS.ACKNOWLEDGED) {
4508
+ if (run.status === import_agent_run_status2.AGENT_RUN_STATUS.ACKNOWLEDGED) {
3787
4509
  return "acknowledged";
3788
4510
  }
3789
4511
  if (run.acknowledgedAt !== null) {
@@ -3802,1114 +4524,1067 @@ function parseAttentionMessageCategory(message) {
3802
4524
  }
3803
4525
  return ATTENTION_MESSAGE_CATEGORY.OTHER;
3804
4526
  }
4527
+ function formatAttentionMessage(category, detail) {
4528
+ return `[${category}] ${detail.trim()}`;
4529
+ }
3805
4530
  function buildAttentionGuidance(run) {
3806
4531
  const phase = deriveOrchestratorRunPhase(run);
3807
4532
  const detectedCategory = run.attentionMessage !== null ? parseAttentionMessageCategory(run.attentionMessage) : null;
3808
- const reportStep = phase === "awaiting_ide_approval" ? null : `report_agent_attention(${run.id}, message) \u2014 use formatAttentionMessage(category, detail); replaces prior message on same ACK run`;
3809
- const clearStep = run.requiresAttention ? `clear_agent_attention(${run.id}) after resolving: ${run.attentionMessage ?? "attention flag"}` : null;
3810
- return {
3811
- phase,
3812
- reportStep,
3813
- clearStep,
3814
- detectedCategory,
3815
- templateExamples: ALL_TEMPLATE_CATEGORIES
3816
- };
3817
- }
3818
-
3819
- // src/tools/orchestrator-hints.ts
3820
- var SLEEPER_POLICY = {
3821
- singleBlockingPoll: true,
3822
- noParallelPoll: true,
3823
- noDuplicateWake: true,
3824
- guidance: "Run exactly one blocking poll (run_orchestrator_once or wait_for_agent_runs) per workspace at a time; do not overlap polls or stack duplicate automation wakes."
3825
- };
3826
- var IDLE_INFLIGHT_CHECK_STEP = "Checked inflight runs (DISPATCHED/ACKNOWLEDGED); none found \u2014 true idle.";
3827
- var IDLE_NO_DUPLICATE_WAKE_STEP = "Schedule at most one follow-up automation wake; do not stack duplicate sleepers while a poll is in flight.";
3828
- var PROCESSING_NO_PARALLEL_POLL_STEP = "Do not start another poll until this batch finishes.";
3829
- var INFLIGHT_DISPATCHED_RECOVERY_STEP = "Orphan DISPATCHED \u2014 resume Task+ack; avoid duplicate Task if subagent already running in this session";
3830
- var IDLE_TIMEOUT_DO_NOT_CLEAR_ATTENTION = "Do not clear_agent_attention on timeout; badge remains until human resolves in IDE";
3831
- var IDLE_TIMEOUT_NEXT_STEPS = [
3832
- "Call sync_git_releases(projectId) to sync work-item commits",
3833
- IDLE_TIMEOUT_DO_NOT_CLEAR_ATTENTION,
3834
- "Call wait_for_agent_runs again (or run_orchestrator_once) to continue monitoring"
3835
- ];
3836
- var IDE_ATTENTION_NOT_IN_AWAITING = "IDE attention is not in-awaiting; do not move column; replace attention message on re-report";
3837
- var PROCESSING_NEXT_STEPS = [
3838
- "Call sync_project_subagents(projectId) before Task dispatch when the project uses custom subagent slugs",
3839
- "Agent runs only for STORY items assigned to \u0410\u0433\u0435\u043D\u0442 \u0418\u0418 (SERVICE user); assign via update_work_item before moving to agent columns",
3840
- "When a subagent is blocked, call report_agent_attention(agentRunId, message); clear with clear_agent_attention after resolution",
3841
- "After processing all runs: local git commit with work-item:{uuid} tag (always; no push on master/main)",
3842
- "On feature branch (not master/main): git push origin HEAD then sync_git_releases(projectId)",
3843
- "Parallel Task runs OK for same workItemId when agentRole differs (e.g. FRONTEND_DEVELOPER + DEVELOPER)",
3844
- "Claim priority: in-development > in-qa > in-analysis; in-analysis blocked while SERVICE-assigned stories exist in dev/qa",
3845
- "Call run_orchestrator_once or wait_for_agent_runs to continue monitoring"
3846
- ];
3847
- var ATTENTION_PROCESSING_NEXT_STEPS = [
3848
- "Resolve requiresAttention runs before dispatching new Task subagents",
3849
- IDE_ATTENTION_NOT_IN_AWAITING,
3850
- "Call clear_agent_attention(agentRunId) after each blocker is resolved"
3851
- ];
3852
- var RUN_LIFECYCLE_DOC = {
3853
- phases: ["dispatched", "acknowledged", "awaiting_ide_approval", "ready_to_complete"],
3854
- terminalPhase: "complete",
3855
- ideAttentionVsWorkflowAwaiting: "IDE attention (report_agent_attention on ACKNOWLEDGED runs) is not in-awaiting \u2014 do not move the story column for shell/git/network/MCP/Smart Mode approvals. Use complete_agent_run(NEEDS_CLARIFICATION) and in-awaiting only for workflow clarifications."
3856
- };
3857
- function buildTaskStep(run) {
3858
- const subagent = run.payload.suggestedSubagentType ?? "subagent";
3859
- return `Task(subagent_type=${subagent}, prompt=payload.contextSummary)`;
3860
- }
3861
- function buildCompleteStep(run) {
3862
- if (run.agentRole === import_subagent_role.SUBAGENT_ROLE.ARCHITECT) {
3863
- return "complete_agent_run after work; architect may set workItemPatch.designerRequired (boolean, default false)";
3864
- }
3865
- return "complete_agent_run after work";
3866
- }
3867
- function buildReportAttentionStep(run) {
3868
- return `report_agent_attention(${run.id}, message) if blocked awaiting human input \u2014 ${ATTENTION_MESSAGE_TEMPLATES.shell.messagePattern.replace("{detail}", "<detail>")}; replaces prior message on re-report`;
3869
- }
3870
- function buildProcessRunAttentionHint(run) {
3871
- const guidance = buildAttentionGuidance(run);
3872
- return {
3873
- phase: guidance.phase,
3874
- requiresAttention: run.requiresAttention,
3875
- message: run.attentionMessage,
3876
- detectedCategory: guidance.detectedCategory,
3877
- reportGuidance: `report_agent_attention(${run.id}, message) using formatAttentionMessage(category, detail); replaces prior message on same ACK run`,
3878
- clearGuidance: guidance.clearStep,
3879
- templateCategories: guidance.templateExamples
3880
- };
3881
- }
3882
- function buildDispatchedProcessRunSteps(run) {
3883
- return [
3884
- "sync_project_subagents(projectId) when payload.suggestedSubagentType is a project custom slug",
3885
- `list_work_item_attachments(${run.workItemId})`,
3886
- "download_work_item_attachments if needed",
3887
- buildTaskStep(run),
3888
- `ack_agent_run(${run.id})`,
3889
- buildReportAttentionStep(run),
3890
- `clear_agent_attention(${run.id}) after human resolves in IDE`,
3891
- buildCompleteStep(run),
3892
- "git commit with work-item tag (local; push only on feature branch)"
3893
- ];
3894
- }
3895
- function buildAwaitingIdeApprovalProcessRunSteps(run) {
3896
- return [
3897
- `clear_agent_attention(${run.id}) after resolving: ${run.attentionMessage ?? "attention flag"}`,
3898
- buildCompleteStep(run),
3899
- "git commit with work-item tag (local; push only on feature branch)"
3900
- ];
3901
- }
3902
- function buildAcknowledgedProcessRunSteps(run) {
3903
- return [
3904
- "Resume subagent work (Task may already be running in this session)",
3905
- buildReportAttentionStep(run),
3906
- `clear_agent_attention(${run.id}) after human resolves in IDE`,
3907
- buildCompleteStep(run),
3908
- "git commit with work-item tag (local; push only on feature branch)"
3909
- ];
3910
- }
3911
- function buildProcessRunHint(run, source) {
3912
- const phase = deriveOrchestratorRunPhase(run);
3913
- if (phase === "awaiting_ide_approval") {
3914
- return {
3915
- agentRunId: run.id,
3916
- workItemId: run.workItemId,
3917
- phase,
3918
- source,
3919
- steps: buildAwaitingIdeApprovalProcessRunSteps(run),
3920
- attention: buildProcessRunAttentionHint(run)
3921
- };
3922
- }
3923
- if (phase === "acknowledged") {
3924
- return {
3925
- agentRunId: run.id,
3926
- workItemId: run.workItemId,
3927
- phase,
3928
- source,
3929
- steps: buildAcknowledgedProcessRunSteps(run)
3930
- };
3931
- }
3932
- const steps = buildDispatchedProcessRunSteps(run);
3933
- if (source === "inflight") {
3934
- return {
3935
- agentRunId: run.id,
3936
- workItemId: run.workItemId,
3937
- phase,
3938
- source,
3939
- steps: [INFLIGHT_DISPATCHED_RECOVERY_STEP, ...steps]
3940
- };
3941
- }
3942
- return {
3943
- agentRunId: run.id,
3944
- workItemId: run.workItemId,
3945
- phase,
3946
- source,
3947
- steps
3948
- };
3949
- }
3950
- function buildProcessRuns(items, inflightRuns) {
3951
- const hints = [];
3952
- for (const run of items) {
3953
- if (run.requiresAttention) {
3954
- hints.push(buildProcessRunHint(run, "claimed"));
3955
- }
3956
- }
3957
- for (const run of inflightRuns) {
3958
- if (run.requiresAttention) {
3959
- hints.push(buildProcessRunHint(run, "inflight"));
3960
- }
3961
- }
3962
- for (const run of inflightRuns) {
3963
- if (!run.requiresAttention) {
3964
- hints.push(buildProcessRunHint(run, "inflight"));
3965
- }
3966
- }
3967
- for (const run of items) {
3968
- if (!run.requiresAttention) {
3969
- hints.push(buildProcessRunHint(run, "claimed"));
3970
- }
3971
- }
3972
- return hints;
3973
- }
3974
- function buildAttentionSummary(runs) {
3975
- const blockedRuns = runs.filter((run) => run.requiresAttention);
3976
- return {
3977
- blockedRunCount: blockedRuns.length,
3978
- blockedAgentRunIds: blockedRuns.map((run) => run.id),
3979
- resolveBeforeNewDispatch: blockedRuns.length > 0
3980
- };
3981
- }
3982
- function buildInflightSummary(inflightRuns) {
3983
- if (inflightRuns.length === 0) {
3984
- return void 0;
3985
- }
3986
- return {
3987
- dispatchedCount: inflightRuns.filter((run) => run.status === import_agent_run_status2.AGENT_RUN_STATUS.DISPATCHED).length,
3988
- acknowledgedCount: inflightRuns.filter((run) => run.status === import_agent_run_status2.AGENT_RUN_STATUS.ACKNOWLEDGED).length,
3989
- requiresAttentionCount: inflightRuns.filter((run) => run.requiresAttention).length,
3990
- agentRunIds: inflightRuns.map((run) => run.id)
3991
- };
3992
- }
3993
- function derivePollResultSource(items, inflightRuns) {
3994
- if (items.length > 0) {
3995
- return "claimed";
3996
- }
3997
- if (inflightRuns.length > 0) {
3998
- return "inflight";
3999
- }
4000
- return "idle";
4001
- }
4002
- function buildOrchestratorHints(items, inflightRuns, timedOut) {
4003
- const pollResultSource = derivePollResultSource(items, inflightRuns);
4004
- const allRuns = [...items, ...inflightRuns];
4005
- if (timedOut) {
4006
- return {
4007
- phase: "idle",
4008
- pollResultSource,
4009
- sleeperPolicy: SLEEPER_POLICY,
4010
- nextSteps: [IDLE_INFLIGHT_CHECK_STEP, ...IDLE_TIMEOUT_NEXT_STEPS, IDLE_NO_DUPLICATE_WAKE_STEP]
4011
- };
4012
- }
4013
- if (allRuns.length === 0) {
4014
- return {
4015
- phase: "idle",
4016
- pollResultSource,
4017
- sleeperPolicy: SLEEPER_POLICY,
4018
- nextSteps: [IDLE_INFLIGHT_CHECK_STEP, ...IDLE_TIMEOUT_NEXT_STEPS, IDLE_NO_DUPLICATE_WAKE_STEP]
4019
- };
4020
- }
4021
- const hasAttentionRuns = allRuns.some((run) => run.requiresAttention);
4022
- return {
4023
- phase: "processing",
4024
- pollResultSource,
4025
- sleeperPolicy: SLEEPER_POLICY,
4026
- inflightSummary: buildInflightSummary(inflightRuns),
4027
- processRuns: buildProcessRuns(items, inflightRuns),
4028
- attentionSummary: buildAttentionSummary(allRuns),
4029
- runLifecycle: RUN_LIFECYCLE_DOC,
4030
- nextSteps: hasAttentionRuns ? [PROCESSING_NO_PARALLEL_POLL_STEP, ...ATTENTION_PROCESSING_NEXT_STEPS, ...PROCESSING_NEXT_STEPS] : [PROCESSING_NO_PARALLEL_POLL_STEP, ...PROCESSING_NEXT_STEPS]
4031
- };
4032
- }
4033
- function enrichPollResult(items, inflightRuns, timedOut) {
4533
+ const reportStep = phase === "awaiting_ide_approval" ? null : `report_agent_attention(${run.id}, message) \u2014 use formatAttentionMessage(category, detail); replaces prior message on same ACK run`;
4534
+ const clearStep = run.requiresAttention ? `clear_agent_attention(${run.id}) after resolving: ${run.attentionMessage ?? "attention flag"}` : null;
4034
4535
  return {
4035
- items,
4036
- inflightRuns,
4037
- timedOut,
4038
- orchestrator: buildOrchestratorHints(items, inflightRuns, timedOut)
4536
+ phase,
4537
+ reportStep,
4538
+ clearStep,
4539
+ detectedCategory,
4540
+ templateExamples: ALL_TEMPLATE_CATEGORIES
4039
4541
  };
4040
4542
  }
4041
4543
 
4042
- // src/tools/tool-utils.ts
4043
- var import_shared2 = __toESM(require_build2(), 1);
4044
-
4045
- // src/workspace/workspace-error.ts
4046
- var WorkspaceError = class extends Error {
4047
- constructor(code, message) {
4048
- super(message);
4049
- this.code = code;
4050
- this.name = "WorkspaceError";
4544
+ // src/attention/shell-category.util.ts
4545
+ var READONLY_SHELL_PATTERN = /^\s*(?:ls(?:\s|$)|cat\s|pwd(?:\s|$)|echo\s|git\s+status(?:\s|$)|git\s+diff(?:\s|$)|git\s+log(?:\s|$))/i;
4546
+ var GIT_DESTRUCTIVE_PATTERN = /\bgit\b.*\b(?:push|commit|checkout|reset|rebase|merge|tag|stash\s+pop|clean|fetch|pull|clone|remote|submodule)\b|\b(?:push|commit|checkout|reset|rebase|merge|tag|stash\s+pop|clean|fetch|pull|clone|remote|submodule)\b.*\bgit\b/i;
4547
+ var NETWORK_PATTERN = /\b(?:curl|wget|npm\s+(?:install|ci|publish|login)|pnpm\s+(?:install|add|i)|yarn\s+(?:install|add)|pip3?\s+install|docker\s+(?:pull|push|run|build|compose)|gh\s+api|npx\s+-y|nc\s|telnet\s|ssh\s)\b/i;
4548
+ function truncateCommand(command, maxLength) {
4549
+ const trimmed = command.trim();
4550
+ if (trimmed.length <= maxLength) {
4551
+ return trimmed;
4051
4552
  }
4052
- };
4553
+ return `${trimmed.slice(0, maxLength - 1)}\u2026`;
4554
+ }
4555
+ function classifyShellCommand(command) {
4556
+ const trimmed = command.trim();
4557
+ if (trimmed === "") {
4558
+ return null;
4559
+ }
4560
+ if (READONLY_SHELL_PATTERN.test(trimmed)) {
4561
+ return null;
4562
+ }
4563
+ if (GIT_DESTRUCTIVE_PATTERN.test(trimmed)) {
4564
+ return "git";
4565
+ }
4566
+ if (NETWORK_PATTERN.test(trimmed)) {
4567
+ return "network";
4568
+ }
4569
+ return "shell";
4570
+ }
4053
4571
 
4054
- // src/tools/tool-utils.ts
4055
- function jsonResult(data) {
4056
- return {
4057
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
4058
- };
4572
+ // src/attention/attention-message.util.ts
4573
+ function buildShellAttentionMessage(category, command) {
4574
+ const truncated = truncateCommand(command, 200);
4575
+ switch (category) {
4576
+ case "git":
4577
+ return formatAttentionMessage(ATTENTION_MESSAGE_CATEGORY.GIT, `Approve git operation: ${truncated}`);
4578
+ case "network":
4579
+ return formatAttentionMessage(ATTENTION_MESSAGE_CATEGORY.NETWORK, `Approve network access: ${truncated}`);
4580
+ case "shell":
4581
+ return formatAttentionMessage(ATTENTION_MESSAGE_CATEGORY.SHELL, `Approve shell command: ${truncated}`);
4582
+ }
4059
4583
  }
4060
- function toolError(code, message) {
4061
- const sanitizedMessage = shouldSanitizeResponses() ? (0, import_shared2.sanitizeMcpErrorMessage)(message) : message;
4062
- return {
4063
- isError: true,
4064
- content: [{ type: "text", text: JSON.stringify({ code, message: sanitizedMessage }) }]
4065
- };
4584
+ function buildMcpAttentionMessage(server, toolName) {
4585
+ const detail = `${server}/${toolName}`.trim();
4586
+ return formatAttentionMessage(ATTENTION_MESSAGE_CATEGORY.MCP, `Approve MCP / API action: ${detail}`);
4066
4587
  }
4067
- function toToolError(error) {
4068
- if (error instanceof RestClientError || error instanceof WorkspaceError) {
4069
- return toolError(error.code, error.message);
4588
+ function buildSmartModeAttentionMessage(toolName, reason) {
4589
+ const detailReason = reason !== void 0 && reason !== null && reason.trim() !== "" ? reason.trim() : "elevated permissions required";
4590
+ return formatAttentionMessage(
4591
+ ATTENTION_MESSAGE_CATEGORY.SMART_MODE,
4592
+ `Approve Smart Mode: ${toolName} \u2014 ${detailReason}`
4593
+ );
4594
+ }
4595
+
4596
+ // src/attention/hook-payload.util.ts
4597
+ function isRecord2(value) {
4598
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4599
+ }
4600
+ function readString(record, key) {
4601
+ const value = record[key];
4602
+ if (typeof value !== "string" || value.trim() === "") {
4603
+ return null;
4070
4604
  }
4071
- const message = error instanceof Error ? error.message : "Unknown error";
4072
- return toolError("INTERNAL_ERROR", message);
4605
+ return value;
4073
4606
  }
4074
- async function runTool(handler, options) {
4607
+ function parseHookPayloadJson(raw) {
4608
+ const trimmed = raw.trim();
4609
+ if (trimmed === "") {
4610
+ return null;
4611
+ }
4075
4612
  try {
4076
- let data = await handler();
4077
- if (shouldSanitizeResponses()) {
4078
- data = (0, import_shared2.sanitizeMcpToolResult)(data, { workspaceRoot: options?.workspaceRoot });
4613
+ const parsed = JSON.parse(trimmed);
4614
+ if (!isRecord2(parsed)) {
4615
+ return null;
4079
4616
  }
4080
- return jsonResult(data);
4081
- } catch (error) {
4082
- return toToolError(error);
4617
+ return parsed;
4618
+ } catch {
4619
+ return null;
4083
4620
  }
4084
4621
  }
4085
-
4086
- // src/tools/wait-for-agent-runs.ts
4087
- var import_agent_runs = __toESM(require_agent_runs_api(), 1);
4088
- var import_agent_run_status3 = __toESM(require_agent_run_status_enum(), 1);
4089
- var DEFAULT_RETRY_BASE_MS = 500;
4090
- var DEFAULT_RETRY_MAX_MS = 3e4;
4091
- var DEFAULT_INFLIGHT_LIMIT = 10;
4092
- function sleep(ms) {
4093
- return new Promise((resolve4) => {
4094
- setTimeout(resolve4, ms);
4095
- });
4622
+ function extractShellCommand(payload) {
4623
+ return readString(payload, "command");
4096
4624
  }
4097
- function isTransientClaimError(error) {
4098
- if (!(error instanceof RestClientError)) {
4099
- return false;
4625
+ function extractMcpExecution(payload) {
4626
+ const server = readString(payload, "server") ?? readString(payload, "serverName") ?? readString(payload, "mcpServer");
4627
+ const toolName = readString(payload, "toolName") ?? readString(payload, "tool") ?? readString(payload, "mcpToolName");
4628
+ if (server === null || toolName === null) {
4629
+ return null;
4100
4630
  }
4101
- if (error.status === void 0) {
4102
- return error.code === "HTTP_ERROR";
4631
+ return { server, toolName };
4632
+ }
4633
+ function extractPreToolUse(payload) {
4634
+ const toolName = readString(payload, "tool_name") ?? readString(payload, "toolName") ?? readString(payload, "tool") ?? readString(payload, "name");
4635
+ if (toolName === null) {
4636
+ return null;
4103
4637
  }
4104
- return error.status >= 500;
4638
+ const smartModeReason = readString(payload, "block_reason") ?? readString(payload, "smartModeBlockReason") ?? readString(payload, "smart_mode_block_reason") ?? readString(payload, "reason");
4639
+ const permission = readString(payload, "permission");
4640
+ const smartMode = payload.smart_mode ?? payload.smartMode;
4641
+ const hasSmartModeSignal = smartModeReason !== null || permission === "ask" || smartMode === true || smartMode === "blocked";
4642
+ if (!hasSmartModeSignal) {
4643
+ return null;
4644
+ }
4645
+ return { toolName, smartModeReason };
4105
4646
  }
4106
- function computeBackoffMs(consecutiveFailures, baseMs, maxMs) {
4107
- const exponential = baseMs * 2 ** (consecutiveFailures - 1);
4108
- return Math.min(exponential, maxMs);
4647
+ function isAfterHookEvent(event) {
4648
+ return event === "afterShellExecution" || event === "afterMCPExecution" || event === "postToolUse";
4109
4649
  }
4110
- function compareInflightRuns(a, b) {
4111
- if (a.requiresAttention !== b.requiresAttention) {
4112
- return a.requiresAttention ? -1 : 1;
4650
+
4651
+ // src/attention/attention-cli.ts
4652
+ function defaultReadStdin() {
4653
+ return new Promise((resolve4, reject) => {
4654
+ const chunks = [];
4655
+ process.stdin.setEncoding("utf8");
4656
+ process.stdin.on("data", (chunk) => {
4657
+ chunks.push(Buffer.from(chunk));
4658
+ });
4659
+ process.stdin.on("end", () => {
4660
+ resolve4(Buffer.concat(chunks).toString("utf8"));
4661
+ });
4662
+ process.stdin.on("error", reject);
4663
+ });
4664
+ }
4665
+ function resolveBridgeWorkspaceRoot(config) {
4666
+ try {
4667
+ return resolveWorkspaceRoot(void 0, config.workspaceRoot);
4668
+ } catch {
4669
+ return null;
4113
4670
  }
4114
- const aDispatched = a.dispatchedAt ?? "";
4115
- const bDispatched = b.dispatchedAt ?? "";
4116
- return aDispatched.localeCompare(bDispatched);
4117
4671
  }
4118
- async function fetchInflightAgentRuns(client, options) {
4119
- const limit = options.limit ?? DEFAULT_INFLIGHT_LIMIT;
4120
- const page = await client.get(import_agent_runs.AGENT_RUNS_ROUTES.listAgentRuns(), {
4121
- projectId: options.projectId,
4122
- activeOnly: "true",
4123
- limit: String(limit)
4124
- });
4125
- const filtered = page.items.filter(
4126
- (run) => run.status === import_agent_run_status3.AGENT_RUN_STATUS.DISPATCHED || run.status === import_agent_run_status3.AGENT_RUN_STATUS.ACKNOWLEDGED
4127
- );
4128
- filtered.sort(compareInflightRuns);
4129
- return filtered.slice(0, limit);
4672
+ function resolveProjectId(workspaceRoot) {
4673
+ return parseTaskBoardsYaml(workspaceRoot);
4130
4674
  }
4131
- async function pollAgentRuns(client, options, deps) {
4132
- const { projectId, timeoutMs, pollIntervalMs, limit } = options;
4133
- if (pollIntervalMs > timeoutMs) {
4134
- throw new WorkspaceError("VALIDATION_ERROR", "pollInterval must not exceed timeout");
4675
+ function writeActiveRunFromAck(config, run) {
4676
+ if (config.workspaceRoot === void 0) {
4677
+ return;
4135
4678
  }
4136
- const sleepFn = deps?.sleep ?? sleep;
4137
- const deadline = Date.now() + timeoutMs;
4138
- const claimLimit = limit ?? 1;
4139
- const retryBaseMs = options.retryBaseMs ?? DEFAULT_RETRY_BASE_MS;
4140
- const retryMaxMs = options.retryMaxMs ?? DEFAULT_RETRY_MAX_MS;
4141
- let consecutiveFailures = 0;
4142
- while (true) {
4143
- let response;
4144
- try {
4145
- response = await client.post(import_agent_runs.AGENT_RUNS_ROUTES.claimAgentRuns(), {
4146
- projectId,
4147
- limit: claimLimit
4148
- });
4149
- consecutiveFailures = 0;
4150
- } catch (error) {
4151
- if (!isTransientClaimError(error)) {
4152
- throw error;
4153
- }
4154
- if (Date.now() >= deadline) {
4155
- return { items: [], inflightRuns: [], timedOut: true };
4156
- }
4157
- consecutiveFailures += 1;
4158
- const backoffMs = computeBackoffMs(consecutiveFailures, retryBaseMs, retryMaxMs);
4159
- const remainingMs2 = deadline - Date.now();
4160
- await sleepFn(Math.min(backoffMs, remainingMs2));
4161
- continue;
4162
- }
4163
- if (response.items.length > 0) {
4164
- return { items: response.items, inflightRuns: [], timedOut: false };
4165
- }
4166
- const inflightRuns = await fetchInflightAgentRuns(client, { projectId, limit: claimLimit });
4167
- if (inflightRuns.length > 0) {
4168
- return { items: [], inflightRuns, timedOut: false };
4169
- }
4170
- if (Date.now() >= deadline) {
4171
- return { items: [], inflightRuns: [], timedOut: true };
4172
- }
4173
- const remainingMs = deadline - Date.now();
4174
- const waitMs = Math.min(pollIntervalMs, remainingMs);
4175
- await sleepFn(waitMs);
4679
+ const workspaceRoot = resolveBridgeWorkspaceRoot(config);
4680
+ if (workspaceRoot === null) {
4681
+ return;
4176
4682
  }
4683
+ const projectId = resolveProjectId(workspaceRoot);
4684
+ if (projectId === null) {
4685
+ return;
4686
+ }
4687
+ const acknowledgedAt = run.acknowledgedAt ?? (/* @__PURE__ */ new Date()).toISOString();
4688
+ writeActiveRunState(workspaceRoot, {
4689
+ agentRunId: run.id,
4690
+ workItemId: run.workItemId,
4691
+ projectId,
4692
+ acknowledgedAt
4693
+ });
4177
4694
  }
4178
-
4179
- // src/tools/agent-runs.ts
4180
- var agentRunStatusValues = Object.values(import_agent_run_status4.AGENT_RUN_STATUS);
4181
- var agentRunOutcomeValues = Object.values(import_agent_run_outcome.AGENT_RUN_OUTCOME);
4182
- var AGENTIC_SDLC_COLUMNS_SUMMARY = "AGENTIC_SDLC has 7 columns: backlog, in-analysis (PRODUCT\u2192ANALYST\u2192ARCHITECT handoffs), in-development (DESIGNER optional, then parallel FRONTEND_DEVELOPER+DEVELOPER), in-qa, in-awaiting, done, released.";
4183
- var COMPLETE_AGENT_RUN_OUTCOME_SUMMARY = "Prerequisite: STORY assignee must be \u0410\u0433\u0435\u043D\u0442 \u0418\u0418 (SERVICE user) \u2014 otherwise no agent_run is enqueued or claimed. in-analysis: PRODUCT/ANALYST DEFAULT \u2192 HANDOFF next phase (skip unbound roles); ANALYST SKIP_DESIGN \u2192 in-development; ARCHITECT DEFAULT \u2192 in-development (set workItemPatch.designerRequired). in-development: DESIGNER DEFAULT \u2192 DEVELOPMENT; dev role DEFAULT removes role from pendingDevRoles, moves to in-qa when empty; SKIP_DESIGN skips DESIGNER. in-qa: DEFAULT \u2192 done; HAS_BUGS \u2192 in-development (re-init pendingDevRoles). NEEDS_CLARIFICATION \u2192 in-awaiting. FAILED \u2192 no move. Claim priority: in-development > in-qa > in-analysis (SERVICE-assigned stories only block analysis).";
4184
- function registerAgentRunTools(server, client) {
4185
- server.registerTool(
4186
- "list_agent_runs",
4187
- {
4188
- description: "List agent runs for a project (default status PENDING). Prefer wait_for_agent_runs for orchestrator long-polling.",
4189
- inputSchema: {
4190
- projectId: z.string().uuid().describe("Project UUID (required)"),
4191
- status: z.enum(agentRunStatusValues).optional().describe("Filter by agent run status")
4192
- }
4193
- },
4194
- async ({ status, projectId }) => runTool(async () => {
4195
- const effectiveStatus = status ?? import_agent_run_status4.AGENT_RUN_STATUS.PENDING;
4196
- return client.get(import_agent_runs2.AGENT_RUNS_ROUTES.listAgentRuns(), {
4197
- status: effectiveStatus,
4198
- projectId
4199
- });
4200
- })
4201
- );
4202
- server.registerTool(
4203
- "wait_for_agent_runs",
4204
- {
4205
- description: "Long-poll agent runs for a project: atomically claims PENDING runs (POST /agent-runs/claim). On empty claim, immediately lists inflight DISPATCHED/ACKNOWLEDGED runs (GET activeOnly) for orphan recovery before sleeping. Returns items (newly claimed), inflightRuns (resume candidates), and orchestrator hints. First claim is immediate; subsequent polls sleep pollInterval seconds only when both claim and inflight are empty.",
4206
- inputSchema: {
4207
- projectId: z.string().uuid().describe("Project UUID"),
4208
- timeout: z.number().int().min(1).max(300).default(120).describe("Maximum wait in seconds (1\u2013300, default 120)"),
4209
- pollInterval: z.number().int().min(1).max(60).default(2).describe("Seconds between polls (1\u201360, default 2); must not exceed timeout"),
4210
- limit: z.number().int().min(1).max(10).default(1).describe("Max runs to claim per poll (1\u201310, default 1)")
4211
- }
4212
- },
4213
- async ({ projectId, timeout, pollInterval, limit }) => runTool(async () => {
4214
- const pollResult = await pollAgentRuns(client, {
4215
- projectId,
4216
- timeoutMs: timeout * 1e3,
4217
- pollIntervalMs: pollInterval * 1e3,
4218
- limit
4219
- });
4220
- return enrichPollResult(pollResult.items, pollResult.inflightRuns, pollResult.timedOut);
4221
- })
4222
- );
4223
- server.registerTool(
4224
- "ack_agent_run",
4225
- {
4226
- description: "Acknowledge agent run delivery after Task subagent has been dispatched. Requires status DISPATCHED (DISPATCHED \u2192 ACKNOWLEDGED).",
4227
- inputSchema: {
4228
- agentRunId: z.string().uuid().describe("Agent run UUID"),
4229
- note: z.string().nullable().optional().describe("Optional acknowledgment note")
4230
- }
4231
- },
4232
- async ({ agentRunId, note }) => runTool(async () => {
4233
- const body = note !== void 0 ? { note } : void 0;
4234
- return client.patch(import_agent_runs2.AGENT_RUNS_ROUTES.acknowledgeAgentRun(agentRunId), body);
4235
- })
4236
- );
4237
- server.registerTool(
4238
- "report_agent_attention",
4239
- {
4240
- description: "Signal that the in-flight subagent needs human attention while the run is ACKNOWLEDGED. Sets requiresAttention on the agent run for orchestrator/UI visibility.",
4241
- inputSchema: {
4242
- agentRunId: z.string().uuid().describe("Agent run UUID"),
4243
- message: z.string().min(1).describe("Why human attention is needed")
4244
- }
4245
- },
4246
- async ({ agentRunId, message }) => runTool(async () => {
4247
- const body = {
4248
- requiresAttention: true,
4249
- attentionMessage: message
4250
- };
4251
- return client.patch(import_agent_runs2.AGENT_RUNS_ROUTES.setAgentRunAttention(agentRunId), body);
4252
- })
4253
- );
4254
- server.registerTool(
4255
- "clear_agent_attention",
4256
- {
4257
- description: "Clear the human-attention flag after the blocker is resolved. Requires the run to still be ACKNOWLEDGED.",
4258
- inputSchema: {
4259
- agentRunId: z.string().uuid().describe("Agent run UUID")
4260
- }
4261
- },
4262
- async ({ agentRunId }) => runTool(async () => {
4263
- const body = {
4264
- requiresAttention: false
4265
- };
4266
- return client.patch(import_agent_runs2.AGENT_RUNS_ROUTES.setAgentRunAttention(agentRunId), body);
4267
- })
4268
- );
4269
- server.registerTool(
4270
- "complete_agent_run",
4271
- {
4272
- description: `Complete agent run: apply AGENTIC_SDLC workflow transition and optional work item patch. ${AGENTIC_SDLC_COLUMNS_SUMMARY} Outcomes: ${COMPLETE_AGENT_RUN_OUTCOME_SUMMARY}`,
4273
- inputSchema: {
4274
- agentRunId: z.string().uuid().describe("Agent run UUID"),
4275
- outcome: z.enum(agentRunOutcomeValues).optional().describe(
4276
- `Completion outcome (DEFAULT, SKIP_DESIGN, HAS_BUGS, NEEDS_CLARIFICATION, FAILED). ${COMPLETE_AGENT_RUN_OUTCOME_SUMMARY}`
4277
- ),
4278
- note: z.string().nullable().optional().describe("Optional completion note"),
4279
- workItemPatch: z.object({
4280
- description: z.string().nullable().optional().describe("Updated work item description"),
4281
- acceptanceCriteria: z.string().nullable().optional().describe("Updated acceptance criteria"),
4282
- designerRequired: z.boolean().optional().describe("Architect only: whether DESIGNER phase runs before development")
4283
- }).nullable().optional().describe("Optional work item field updates")
4284
- }
4285
- },
4286
- async ({ agentRunId, outcome, note, workItemPatch }) => runTool(async () => {
4287
- const body = {};
4288
- if (outcome !== void 0) {
4289
- body.outcome = outcome;
4290
- }
4291
- if (note !== void 0) {
4292
- body.note = note;
4293
- }
4294
- if (workItemPatch !== void 0 && workItemPatch !== null) {
4295
- body.workItemPatch = workItemPatch;
4296
- }
4297
- const hasBody = outcome !== void 0 || note !== void 0 || workItemPatch !== void 0 && workItemPatch !== null;
4298
- return client.post(
4299
- import_agent_runs2.AGENT_RUNS_ROUTES.completeAgentRun(agentRunId),
4300
- hasBody ? body : void 0
4301
- );
4302
- })
4303
- );
4304
- }
4305
-
4306
- // src/tools/orchestrator.ts
4307
- import { z as z2 } from "zod";
4308
-
4309
- // src/workspace/resolve-project.ts
4310
- var import_projects = __toESM(require_projects_api(), 1);
4311
- import { basename } from "node:path";
4312
-
4313
- // src/workspace/normalize-token.ts
4314
- function normalizeToken(value) {
4315
- return value.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
4695
+ function clearActiveRunAfterComplete(config) {
4696
+ if (config.workspaceRoot === void 0) {
4697
+ return;
4698
+ }
4699
+ const workspaceRoot = resolveBridgeWorkspaceRoot(config);
4700
+ if (workspaceRoot === null) {
4701
+ return;
4702
+ }
4703
+ clearActiveRunState(workspaceRoot);
4704
+ clearAttentionPendingState(workspaceRoot);
4316
4705
  }
4317
-
4318
- // src/workspace/auto-match-project.ts
4319
- function toCandidate(project, matchedBy) {
4320
- return {
4321
- projectId: project.id,
4322
- name: project.name,
4323
- key: project.key,
4324
- presetCode: project.presetCode,
4325
- matchedBy
4326
- };
4706
+ function logCliError(deps, message) {
4707
+ if (deps.logError !== void 0) {
4708
+ deps.logError(message);
4709
+ return;
4710
+ }
4711
+ console.error(message);
4327
4712
  }
4328
- function autoMatchProject(folderToken, projects) {
4329
- const normalizedFolder = normalizeToken(folderToken);
4330
- if (normalizedFolder.length === 0) {
4331
- return { kind: "none" };
4713
+ function readFlagValue(args, flag) {
4714
+ const index = args.indexOf(flag);
4715
+ if (index === -1 || index + 1 >= args.length) {
4716
+ return null;
4332
4717
  }
4333
- const keyMatches = [];
4334
- const nameMatches = [];
4335
- for (const project of projects) {
4336
- const input = {
4337
- id: project.id,
4338
- name: project.name,
4339
- key: project.key,
4340
- presetCode: project.presetCode
4341
- };
4342
- if (normalizeToken(project.key) === normalizedFolder) {
4343
- keyMatches.push(input);
4344
- }
4345
- if (normalizeToken(project.name) === normalizedFolder) {
4346
- nameMatches.push(input);
4718
+ return args[index + 1] ?? null;
4719
+ }
4720
+ function removeConsumedFlags(args) {
4721
+ const flagsWithValues = /* @__PURE__ */ new Set([
4722
+ "--category",
4723
+ "--detail",
4724
+ "--event",
4725
+ "--agent-run-id",
4726
+ "--work-item-id",
4727
+ "--project-id",
4728
+ "--acknowledged-at"
4729
+ ]);
4730
+ const result = [];
4731
+ for (let index = 0; index < args.length; index += 1) {
4732
+ const arg = args[index];
4733
+ if (flagsWithValues.has(arg)) {
4734
+ index += 1;
4735
+ continue;
4347
4736
  }
4737
+ result.push(arg);
4348
4738
  }
4349
- const uniqueById = /* @__PURE__ */ new Map();
4350
- for (const project of keyMatches) {
4351
- uniqueById.set(project.id, { project, matchedBy: "key" });
4739
+ return result;
4740
+ }
4741
+ async function reportAttention(deps, agentRunId, message, workspaceRoot) {
4742
+ const transport = createOrchestratorTransport(deps.client);
4743
+ await transport.setAttention(agentRunId, true, message);
4744
+ writeAttentionPendingState(workspaceRoot, agentRunId);
4745
+ }
4746
+ async function clearAttention(deps, workspaceRoot) {
4747
+ const pending = readAttentionPendingState(workspaceRoot);
4748
+ if (pending === null) {
4749
+ return;
4352
4750
  }
4353
- for (const project of nameMatches) {
4354
- if (!uniqueById.has(project.id)) {
4355
- uniqueById.set(project.id, { project, matchedBy: "name" });
4356
- }
4751
+ const active = readActiveRunState(workspaceRoot);
4752
+ if (!isActiveRunStateUsable(active) || active.agentRunId !== pending.agentRunId) {
4753
+ return;
4357
4754
  }
4358
- const matches = [...uniqueById.values()];
4359
- if (matches.length === 1) {
4360
- const match = matches[0];
4361
- if (match === void 0) {
4362
- return { kind: "none" };
4363
- }
4364
- return { kind: "single", project: match.project, matchedBy: match.matchedBy };
4755
+ const transport = createOrchestratorTransport(deps.client);
4756
+ try {
4757
+ await transport.setAttention(active.agentRunId, false);
4758
+ } catch (error) {
4759
+ const message = error instanceof Error ? error.message : "Failed to clear agent attention";
4760
+ logCliError(deps, message);
4761
+ } finally {
4762
+ clearAttentionPendingState(workspaceRoot);
4365
4763
  }
4366
- if (matches.length > 1) {
4367
- return {
4368
- kind: "candidates",
4369
- candidates: matches.map((match) => toCandidate(match.project, match.matchedBy))
4370
- };
4764
+ }
4765
+ async function handleAttentionReport(deps, args) {
4766
+ const category = readFlagValue(args, "--category");
4767
+ const detail = readFlagValue(args, "--detail");
4768
+ if (category === null || detail === null) {
4769
+ logCliError(deps, "attention report requires --category and --detail");
4770
+ return 0;
4771
+ }
4772
+ const workspaceRoot = resolveBridgeWorkspaceRoot(deps.config);
4773
+ if (workspaceRoot === null) {
4774
+ return 0;
4775
+ }
4776
+ const active = readActiveRunState(workspaceRoot);
4777
+ if (!isActiveRunStateUsable(active)) {
4778
+ return 0;
4779
+ }
4780
+ const message = `[${category}] ${detail.trim()}`;
4781
+ try {
4782
+ await reportAttention(deps, active.agentRunId, message, workspaceRoot);
4783
+ } catch (error) {
4784
+ const messageText = error instanceof Error ? error.message : "Failed to report agent attention";
4785
+ logCliError(deps, messageText);
4371
4786
  }
4372
- return { kind: "none" };
4787
+ return 0;
4373
4788
  }
4374
-
4375
- // src/workspace/parse-ide-rules.ts
4376
- import { existsSync, readdirSync, readFileSync } from "node:fs";
4377
- import { join } from "node:path";
4378
- var PROJECT_ID_LINE_PATTERN = /projectId\s*[:=]\s*["']?([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})/gi;
4379
- var TASK_BOARDS_UUID_PATTERN = /task-boards[^\n\r]*?([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})/gi;
4380
- function extractUuids(content) {
4381
- const ids = /* @__PURE__ */ new Set();
4382
- for (const pattern of [PROJECT_ID_LINE_PATTERN, TASK_BOARDS_UUID_PATTERN]) {
4383
- pattern.lastIndex = 0;
4384
- let match = pattern.exec(content);
4385
- while (match !== null) {
4386
- const id = match[1];
4387
- if (id !== void 0) {
4388
- ids.add(id.toLowerCase());
4389
- }
4390
- match = pattern.exec(content);
4789
+ async function handleAttentionReportFromHook(deps, args) {
4790
+ const eventRaw = readFlagValue(args, "--event");
4791
+ if (eventRaw === null) {
4792
+ logCliError(deps, "attention report-from-hook requires --event");
4793
+ return 0;
4794
+ }
4795
+ const event = eventRaw;
4796
+ if (isAfterHookEvent(event)) {
4797
+ return 0;
4798
+ }
4799
+ const readStdin = deps.readStdin ?? defaultReadStdin;
4800
+ const stdin = await readStdin();
4801
+ const payload = parseHookPayloadJson(stdin);
4802
+ if (payload === null) {
4803
+ return 0;
4804
+ }
4805
+ const workspaceRoot = resolveBridgeWorkspaceRoot(deps.config);
4806
+ if (workspaceRoot === null) {
4807
+ return 0;
4808
+ }
4809
+ const active = readActiveRunState(workspaceRoot);
4810
+ if (!isActiveRunStateUsable(active)) {
4811
+ return 0;
4812
+ }
4813
+ let message = null;
4814
+ if (event === "beforeShellExecution") {
4815
+ const command = extractShellCommand(payload);
4816
+ if (command === null) {
4817
+ return 0;
4391
4818
  }
4819
+ const category = classifyShellCommand(command);
4820
+ if (category === null) {
4821
+ return 0;
4822
+ }
4823
+ message = buildShellAttentionMessage(category, command);
4824
+ } else if (event === "beforeMCPExecution") {
4825
+ const mcp = extractMcpExecution(payload);
4826
+ if (mcp === null) {
4827
+ return 0;
4828
+ }
4829
+ message = buildMcpAttentionMessage(mcp.server, mcp.toolName);
4830
+ } else if (event === "preToolUse") {
4831
+ const preTool = extractPreToolUse(payload);
4832
+ if (preTool === null) {
4833
+ return 0;
4834
+ }
4835
+ message = buildSmartModeAttentionMessage(preTool.toolName, preTool.smartModeReason);
4392
4836
  }
4393
- return ids;
4837
+ if (message === null) {
4838
+ return 0;
4839
+ }
4840
+ try {
4841
+ await reportAttention(deps, active.agentRunId, message, workspaceRoot);
4842
+ } catch (error) {
4843
+ const messageText = error instanceof Error ? error.message : "Failed to report agent attention from hook";
4844
+ logCliError(deps, messageText);
4845
+ }
4846
+ return 0;
4394
4847
  }
4395
- function parseIdeRules(workspaceRoot) {
4396
- const rulesDir = join(workspaceRoot, ".cursor", "rules");
4397
- if (!existsSync(rulesDir)) {
4398
- return { status: "missing" };
4848
+ async function handleAttentionClear(deps) {
4849
+ const workspaceRoot = resolveBridgeWorkspaceRoot(deps.config);
4850
+ if (workspaceRoot === null) {
4851
+ return 0;
4399
4852
  }
4400
- const entries = readdirSync(rulesDir, { withFileTypes: true });
4401
- const allIds = /* @__PURE__ */ new Set();
4402
- for (const entry of entries) {
4403
- if (!entry.isFile()) {
4404
- continue;
4853
+ await clearAttention(deps, workspaceRoot);
4854
+ return 0;
4855
+ }
4856
+ function handleOrchestratorWriteActiveRun(deps, args) {
4857
+ const agentRunId = readFlagValue(args, "--agent-run-id");
4858
+ const workItemId = readFlagValue(args, "--work-item-id");
4859
+ const projectId = readFlagValue(args, "--project-id");
4860
+ const acknowledgedAt = readFlagValue(args, "--acknowledged-at") ?? (/* @__PURE__ */ new Date()).toISOString();
4861
+ if (agentRunId === null || workItemId === null || projectId === null) {
4862
+ logCliError(deps, "orchestrator write-active-run requires --agent-run-id, --work-item-id, --project-id");
4863
+ return 0;
4864
+ }
4865
+ const workspaceRoot = resolveBridgeWorkspaceRoot(deps.config);
4866
+ if (workspaceRoot === null) {
4867
+ return 0;
4868
+ }
4869
+ writeActiveRunState(workspaceRoot, {
4870
+ agentRunId,
4871
+ workItemId,
4872
+ projectId,
4873
+ acknowledgedAt
4874
+ });
4875
+ return 0;
4876
+ }
4877
+ function handleOrchestratorClearActiveRun(deps) {
4878
+ const workspaceRoot = resolveBridgeWorkspaceRoot(deps.config);
4879
+ if (workspaceRoot === null) {
4880
+ return 0;
4881
+ }
4882
+ clearOrchestratorBridgeState(workspaceRoot);
4883
+ return 0;
4884
+ }
4885
+ async function runAttentionCli(argv, deps) {
4886
+ const args = removeConsumedFlags(argv);
4887
+ const topLevel = args[0];
4888
+ if (topLevel === "attention") {
4889
+ const subcommand = args[1];
4890
+ if (subcommand === "report") {
4891
+ return handleAttentionReport(deps, argv);
4405
4892
  }
4406
- if (!entry.name.endsWith(".mdc") && !entry.name.endsWith(".md")) {
4407
- continue;
4893
+ if (subcommand === "report-from-hook") {
4894
+ return handleAttentionReportFromHook(deps, argv);
4408
4895
  }
4409
- const content = readFileSync(join(rulesDir, entry.name), "utf8");
4410
- for (const id of extractUuids(content)) {
4411
- allIds.add(id);
4896
+ if (subcommand === "clear") {
4897
+ return handleAttentionClear(deps);
4412
4898
  }
4899
+ logCliError(deps, `Unknown attention subcommand: ${subcommand ?? "(missing)"}`);
4900
+ return 0;
4413
4901
  }
4414
- if (allIds.size === 0) {
4415
- return { status: "missing" };
4416
- }
4417
- if (allIds.size === 1) {
4418
- const projectId = [...allIds][0];
4419
- if (projectId === void 0) {
4420
- return { status: "missing" };
4902
+ if (topLevel === "orchestrator") {
4903
+ const subcommand = args[1];
4904
+ if (subcommand === "write-active-run") {
4905
+ return handleOrchestratorWriteActiveRun(deps, argv);
4421
4906
  }
4422
- return { status: "found", projectId };
4907
+ if (subcommand === "clear-active-run") {
4908
+ return handleOrchestratorClearActiveRun(deps);
4909
+ }
4910
+ logCliError(deps, `Unknown orchestrator subcommand: ${subcommand ?? "(missing)"}`);
4911
+ return 0;
4423
4912
  }
4424
- return { status: "ambiguous" };
4913
+ return 1;
4914
+ }
4915
+ function isAttentionCliInvocation(argv) {
4916
+ const topLevel = argv[0];
4917
+ return topLevel === "attention" || topLevel === "orchestrator";
4425
4918
  }
4426
4919
 
4427
- // src/workspace/parse-task-boards-yaml.ts
4428
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
4429
- import { join as join2 } from "node:path";
4430
- var TASK_BOARDS_FILE = ".task-boards.yaml";
4431
- var VERSION_PATTERN = /^\s*version\s*:\s*1\s*$/m;
4432
- var PROJECT_ID_PATTERN = /^\s*projectId\s*:\s*["']?([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})["']?\s*$/im;
4433
- function parseTaskBoardsYaml(workspaceRoot) {
4434
- const filePath = join2(workspaceRoot, TASK_BOARDS_FILE);
4435
- if (!existsSync2(filePath)) {
4436
- return null;
4920
+ // src/config.ts
4921
+ var import_api = __toESM(require_build(), 1);
4922
+ function resolveSanitizeResponses() {
4923
+ const raw = process.env.TASK_BOARDS_MCP_SANITIZE;
4924
+ if (raw === void 0 || raw.trim() === "") {
4925
+ return true;
4437
4926
  }
4438
- const content = readFileSync2(filePath, "utf8");
4439
- if (!VERSION_PATTERN.test(content)) {
4440
- return null;
4927
+ const normalized = raw.trim().toLowerCase();
4928
+ return normalized !== "false" && normalized !== "0" && normalized !== "no";
4929
+ }
4930
+ function resolveBlockSensitiveAttachments() {
4931
+ const raw = process.env.TASK_BOARDS_MCP_BLOCK_SENSITIVE_ATTACHMENTS;
4932
+ if (raw === void 0 || raw.trim() === "") {
4933
+ return true;
4441
4934
  }
4442
- const match = PROJECT_ID_PATTERN.exec(content);
4443
- if (match === null) {
4444
- return null;
4935
+ const normalized = raw.trim().toLowerCase();
4936
+ return normalized !== "false" && normalized !== "0" && normalized !== "no";
4937
+ }
4938
+ function loadConfig() {
4939
+ const apiUrl = process.env.TASK_BOARDS_API_URL ?? "http://localhost:3000";
4940
+ const apiToken = process.env.TASK_BOARDS_API_TOKEN;
4941
+ const workspaceRoot = process.env.WORKSPACE_ROOT;
4942
+ if (process.env.NODE_ENV === "production" && (apiToken === void 0 || apiToken.trim() === "")) {
4943
+ console.error("WARNING: TASK_BOARDS_API_TOKEN is not set. MCP server cannot authenticate to the Task Boards API.");
4445
4944
  }
4446
- return match[1] ?? null;
4945
+ return {
4946
+ apiUrl: `${apiUrl.replace(/\/$/, "")}${import_api.API_V1_PREFIX}`,
4947
+ apiToken,
4948
+ workspaceRoot: workspaceRoot !== void 0 && workspaceRoot.trim() !== "" ? workspaceRoot : void 0,
4949
+ sanitizeResponses: resolveSanitizeResponses(),
4950
+ blockSensitiveAttachments: resolveBlockSensitiveAttachments()
4951
+ };
4952
+ }
4953
+
4954
+ // src/index.ts
4955
+ import { createRequire } from "node:module";
4956
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4957
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4958
+
4959
+ // src/tools/agent-runs.ts
4960
+ var import_agent_runs3 = __toESM(require_agent_runs_api(), 1);
4961
+ var import_agent_run_outcome = __toESM(require_agent_run_outcome_enum(), 1);
4962
+ var import_agent_run_status4 = __toESM(require_agent_run_status_enum(), 1);
4963
+ import { z as z3 } from "zod";
4964
+
4965
+ // src/tools/orchestrator-hints.ts
4966
+ var import_agent_run_status3 = __toESM(require_agent_run_status_enum(), 1);
4967
+ var import_subagent_role = __toESM(require_subagent_role_enum(), 1);
4968
+ var SLEEPER_POLICY = {
4969
+ singleBlockingPoll: true,
4970
+ noParallelPoll: true,
4971
+ noDuplicateWake: true,
4972
+ guidance: "Run exactly one blocking poll (run_orchestrator_once or wait_for_agent_runs) per workspace at a time; do not overlap polls or stack duplicate automation wakes."
4973
+ };
4974
+ var IDLE_INFLIGHT_CHECK_STEP = "Checked inflight runs (DISPATCHED/ACKNOWLEDGED); none found \u2014 true idle.";
4975
+ var IDLE_NO_DUPLICATE_WAKE_STEP = "Schedule at most one follow-up automation wake; do not stack duplicate sleepers while a poll is in flight.";
4976
+ var PROCESSING_NO_PARALLEL_POLL_STEP = "Do not start another poll until this batch finishes.";
4977
+ var INFLIGHT_DISPATCHED_RECOVERY_STEP = "Orphan DISPATCHED \u2014 resume Task+ack; avoid duplicate Task if subagent already running in this session";
4978
+ var IDLE_TIMEOUT_DO_NOT_CLEAR_ATTENTION = "Do not clear_agent_attention on timeout; badge remains until human resolves in IDE";
4979
+ var IDLE_TIMEOUT_NEXT_STEPS = [
4980
+ "Call sync_git_releases(projectId) to sync work-item commits",
4981
+ IDLE_TIMEOUT_DO_NOT_CLEAR_ATTENTION,
4982
+ "Call wait_for_agent_runs again (or run_orchestrator_once) to continue monitoring"
4983
+ ];
4984
+ var IDE_ATTENTION_NOT_IN_AWAITING = "IDE attention is not in-awaiting; do not move column; replace attention message on re-report";
4985
+ var PROCESSING_NEXT_STEPS = [
4986
+ "Call sync_project_subagents(projectId) before Task dispatch when the project uses custom subagent slugs",
4987
+ "Agent runs only for STORY items assigned to \u0410\u0433\u0435\u043D\u0442 \u0418\u0418 (SERVICE user); assign via update_work_item before moving to agent columns",
4988
+ "When a subagent is blocked, call report_agent_attention(agentRunId, message); clear with clear_agent_attention after resolution",
4989
+ "After processing all runs: local git commit with work-item:{uuid} tag (skip commit and sync_git_releases when any run completed with SKIP_DEV)",
4990
+ "On feature branch (not master/main): git push origin HEAD then sync_git_releases(projectId)",
4991
+ "Parallel Task runs OK for same workItemId when agentRole differs (e.g. FRONTEND_DEVELOPER + DEVELOPER)",
4992
+ "Claim priority: in-development > in-qa > in-analysis; in-analysis blocked while SERVICE-assigned stories exist in dev/qa",
4993
+ "Call run_orchestrator_once or wait_for_agent_runs to continue monitoring"
4994
+ ];
4995
+ var ATTENTION_PROCESSING_NEXT_STEPS = [
4996
+ "Resolve requiresAttention runs before dispatching new Task subagents",
4997
+ IDE_ATTENTION_NOT_IN_AWAITING,
4998
+ "Call clear_agent_attention(agentRunId) after each blocker is resolved"
4999
+ ];
5000
+ var RUN_LIFECYCLE_DOC = {
5001
+ phases: ["dispatched", "acknowledged", "awaiting_ide_approval", "ready_to_complete"],
5002
+ terminalPhase: "complete",
5003
+ ideAttentionVsWorkflowAwaiting: "IDE attention (report_agent_attention on ACKNOWLEDGED runs) is not in-awaiting \u2014 do not move the story column for shell/git/network/MCP/Smart Mode approvals. Use complete_agent_run(NEEDS_CLARIFICATION) and in-awaiting only for workflow clarifications."
5004
+ };
5005
+ function buildTaskStep(run) {
5006
+ const subagent = run.payload.suggestedSubagentType ?? "subagent";
5007
+ return `Task(subagent_type=${subagent}, prompt=payload.contextSummary)`;
5008
+ }
5009
+ function buildCompleteStep(run) {
5010
+ if (run.agentRole === import_subagent_role.SUBAGENT_ROLE.ARCHITECT) {
5011
+ return "complete_agent_run after work; architect may set workItemPatch.designerRequired (boolean, default false)";
5012
+ }
5013
+ if (run.agentRole === import_subagent_role.SUBAGENT_ROLE.ANALYST) {
5014
+ return "complete_agent_run after work; use outcome SKIP_DEV when codeChangesRequired=false (released without git commit)";
5015
+ }
5016
+ return "complete_agent_run after work";
5017
+ }
5018
+ function buildGitCommitStep(run) {
5019
+ if (run.agentRole === import_subagent_role.SUBAGENT_ROLE.ANALYST) {
5020
+ return "skip git commit and sync_git_releases when outcome SKIP_DEV; otherwise git commit with work-item tag (local; push only on feature branch)";
5021
+ }
5022
+ return "git commit with work-item tag (local; push only on feature branch)";
5023
+ }
5024
+ function buildReportAttentionStep(run) {
5025
+ return `report_agent_attention(${run.id}, message) if blocked awaiting human input \u2014 ${ATTENTION_MESSAGE_TEMPLATES.shell.messagePattern.replace("{detail}", "<detail>")}; replaces prior message on re-report`;
5026
+ }
5027
+ function buildProcessRunAttentionHint(run) {
5028
+ const guidance = buildAttentionGuidance(run);
5029
+ return {
5030
+ phase: guidance.phase,
5031
+ requiresAttention: run.requiresAttention,
5032
+ message: run.attentionMessage,
5033
+ detectedCategory: guidance.detectedCategory,
5034
+ reportGuidance: `report_agent_attention(${run.id}, message) using formatAttentionMessage(category, detail); replaces prior message on same ACK run`,
5035
+ clearGuidance: guidance.clearStep,
5036
+ templateCategories: guidance.templateExamples
5037
+ };
5038
+ }
5039
+ function buildDispatchedProcessRunSteps(run) {
5040
+ return [
5041
+ "sync_project_subagents(projectId) when payload.suggestedSubagentType is a project custom slug",
5042
+ `list_work_item_attachments(${run.workItemId})`,
5043
+ "download_work_item_attachments if needed",
5044
+ buildTaskStep(run),
5045
+ `ack_agent_run(${run.id})`,
5046
+ buildReportAttentionStep(run),
5047
+ `clear_agent_attention(${run.id}) after human resolves in IDE`,
5048
+ buildCompleteStep(run),
5049
+ buildGitCommitStep(run)
5050
+ ];
4447
5051
  }
4448
-
4449
- // src/workspace/resolve-workspace-root.ts
4450
- import { existsSync as existsSync3, statSync } from "node:fs";
4451
- import { dirname, isAbsolute, join as join3, relative, resolve } from "node:path";
4452
- var TASK_BOARDS_FILE2 = ".task-boards.yaml";
4453
- var MAX_UPWARD_LEVELS = 20;
4454
- function assertDirectory(path, source) {
4455
- const absolutePath = resolve(path);
4456
- if (!existsSync3(absolutePath)) {
4457
- throw new WorkspaceError("WORKSPACE_NOT_FOUND", `${source} does not exist: ${absolutePath}`);
4458
- }
4459
- const stats = statSync(absolutePath);
4460
- if (!stats.isDirectory()) {
4461
- throw new WorkspaceError("WORKSPACE_NOT_FOUND", `${source} is not a directory: ${absolutePath}`);
4462
- }
4463
- return absolutePath;
5052
+ function buildAwaitingIdeApprovalProcessRunSteps(run) {
5053
+ return [
5054
+ `clear_agent_attention(${run.id}) after resolving: ${run.attentionMessage ?? "attention flag"}`,
5055
+ buildCompleteStep(run),
5056
+ buildGitCommitStep(run)
5057
+ ];
4464
5058
  }
4465
- function assertWithinAllowedRoot(target, allowedRoot) {
4466
- const relativePath = relative(allowedRoot, target);
4467
- const isNested = relativePath !== "" && !relativePath.startsWith("..") && !isAbsolute(relativePath);
4468
- if (target !== allowedRoot && !isNested) {
4469
- throw new WorkspaceError(
4470
- "WORKSPACE_OUT_OF_BOUNDS",
4471
- `workspaceRoot is outside the allowed WORKSPACE_ROOT: ${target}`
4472
- );
4473
- }
5059
+ function buildAcknowledgedProcessRunSteps(run) {
5060
+ return [
5061
+ "Resume subagent work (Task may already be running in this session)",
5062
+ buildReportAttentionStep(run),
5063
+ `clear_agent_attention(${run.id}) after human resolves in IDE`,
5064
+ buildCompleteStep(run),
5065
+ buildGitCommitStep(run)
5066
+ ];
4474
5067
  }
4475
- function assertGitRepository(absolutePath) {
4476
- if (!existsSync3(join3(absolutePath, ".git"))) {
4477
- throw new WorkspaceError("WORKSPACE_NOT_GIT_REPO", `workspaceRoot is not a git repository: ${absolutePath}`);
5068
+ function buildProcessRunHint(run, source) {
5069
+ const phase = deriveOrchestratorRunPhase(run);
5070
+ if (phase === "awaiting_ide_approval") {
5071
+ return {
5072
+ agentRunId: run.id,
5073
+ workItemId: run.workItemId,
5074
+ phase,
5075
+ source,
5076
+ steps: buildAwaitingIdeApprovalProcessRunSteps(run),
5077
+ attention: buildProcessRunAttentionHint(run)
5078
+ };
5079
+ }
5080
+ if (phase === "acknowledged") {
5081
+ return {
5082
+ agentRunId: run.id,
5083
+ workItemId: run.workItemId,
5084
+ phase,
5085
+ source,
5086
+ steps: buildAcknowledgedProcessRunSteps(run)
5087
+ };
5088
+ }
5089
+ const steps = buildDispatchedProcessRunSteps(run);
5090
+ if (source === "inflight") {
5091
+ return {
5092
+ agentRunId: run.id,
5093
+ workItemId: run.workItemId,
5094
+ phase,
5095
+ source,
5096
+ steps: [INFLIGHT_DISPATCHED_RECOVERY_STEP, ...steps]
5097
+ };
4478
5098
  }
5099
+ return {
5100
+ agentRunId: run.id,
5101
+ workItemId: run.workItemId,
5102
+ phase,
5103
+ source,
5104
+ steps
5105
+ };
4479
5106
  }
4480
- function findWorkspaceRootUpward(startDir) {
4481
- let current = resolve(startDir);
4482
- for (let level = 0; level <= MAX_UPWARD_LEVELS; level += 1) {
4483
- const markerPath = join3(current, TASK_BOARDS_FILE2);
4484
- if (existsSync3(markerPath)) {
4485
- return current;
4486
- }
4487
- const parent = dirname(current);
4488
- if (parent === current) {
4489
- break;
5107
+ function buildProcessRuns(items, inflightRuns) {
5108
+ const hints = [];
5109
+ for (const run of items) {
5110
+ if (run.requiresAttention) {
5111
+ hints.push(buildProcessRunHint(run, "claimed"));
4490
5112
  }
4491
- current = parent;
4492
5113
  }
4493
- return null;
4494
- }
4495
- function resolveWorkspaceRoot(explicitOverride, envWorkspaceRoot) {
4496
- const allowedRoot = envWorkspaceRoot !== void 0 && envWorkspaceRoot.trim() !== "" ? assertDirectory(envWorkspaceRoot, "WORKSPACE_ROOT") : void 0;
4497
- if (explicitOverride !== void 0 && explicitOverride.trim() !== "") {
4498
- const resolved = assertDirectory(explicitOverride, "workspaceRoot");
4499
- if (allowedRoot !== void 0) {
4500
- assertWithinAllowedRoot(resolved, allowedRoot);
4501
- } else {
4502
- assertGitRepository(resolved);
5114
+ for (const run of inflightRuns) {
5115
+ if (run.requiresAttention) {
5116
+ hints.push(buildProcessRunHint(run, "inflight"));
4503
5117
  }
4504
- return resolved;
4505
5118
  }
4506
- if (allowedRoot !== void 0) {
4507
- return allowedRoot;
5119
+ for (const run of inflightRuns) {
5120
+ if (!run.requiresAttention) {
5121
+ hints.push(buildProcessRunHint(run, "inflight"));
5122
+ }
4508
5123
  }
4509
- const fromMarker = findWorkspaceRootUpward(process.cwd());
4510
- if (fromMarker !== null) {
4511
- return fromMarker;
5124
+ for (const run of items) {
5125
+ if (!run.requiresAttention) {
5126
+ hints.push(buildProcessRunHint(run, "claimed"));
5127
+ }
4512
5128
  }
4513
- return resolve(process.cwd());
5129
+ return hints;
4514
5130
  }
4515
-
4516
- // src/workspace/resolve-project.ts
4517
- function emptyResponse(workspaceRoot) {
5131
+ function buildAttentionSummary(runs) {
5132
+ const blockedRuns = runs.filter((run) => run.requiresAttention);
4518
5133
  return {
4519
- resolutionSource: "ambiguous",
4520
- workspaceRoot,
4521
- projectId: null,
4522
- name: null,
4523
- key: null,
4524
- presetCode: null,
4525
- hint: "No project binding found. Add .task-boards.yaml, an IDE rule (.cursor/rules) with projectId, or rename the folder to match a project key/name."
5134
+ blockedRunCount: blockedRuns.length,
5135
+ blockedAgentRunIds: blockedRuns.map((run) => run.id),
5136
+ resolveBeforeNewDispatch: blockedRuns.length > 0
4526
5137
  };
4527
5138
  }
4528
- function fromProject(workspaceRoot, resolutionSource, project) {
5139
+ function buildInflightSummary(inflightRuns) {
5140
+ if (inflightRuns.length === 0) {
5141
+ return void 0;
5142
+ }
4529
5143
  return {
4530
- resolutionSource,
4531
- workspaceRoot,
4532
- projectId: project.id,
4533
- name: project.name,
4534
- key: project.key,
4535
- presetCode: project.presetCode
5144
+ dispatchedCount: inflightRuns.filter((run) => run.status === import_agent_run_status3.AGENT_RUN_STATUS.DISPATCHED).length,
5145
+ acknowledgedCount: inflightRuns.filter((run) => run.status === import_agent_run_status3.AGENT_RUN_STATUS.ACKNOWLEDGED).length,
5146
+ requiresAttentionCount: inflightRuns.filter((run) => run.requiresAttention).length,
5147
+ agentRunIds: inflightRuns.map((run) => run.id)
4536
5148
  };
4537
5149
  }
4538
- async function enrichProject(client, workspaceRoot, resolutionSource, projectId) {
4539
- const project = await client.get(import_projects.PROJECTS_ROUTES.getProject(projectId));
4540
- return fromProject(workspaceRoot, resolutionSource, project);
4541
- }
4542
- async function resolveProject(client, options = {}) {
4543
- const workspaceRoot = resolveWorkspaceRoot(options.workspaceRootOverride, options.envWorkspaceRoot);
4544
- const yamlProjectId = parseTaskBoardsYaml(workspaceRoot);
4545
- if (yamlProjectId !== null) {
4546
- return enrichProject(client, workspaceRoot, "yaml", yamlProjectId);
5150
+ function derivePollResultSource(items, inflightRuns) {
5151
+ if (items.length > 0) {
5152
+ return "claimed";
4547
5153
  }
4548
- const rulesResult = parseIdeRules(workspaceRoot);
4549
- if (rulesResult.status === "ambiguous") {
5154
+ if (inflightRuns.length > 0) {
5155
+ return "inflight";
5156
+ }
5157
+ return "idle";
5158
+ }
5159
+ function buildOrchestratorHints(items, inflightRuns, timedOut) {
5160
+ const pollResultSource = derivePollResultSource(items, inflightRuns);
5161
+ const allRuns = [...items, ...inflightRuns];
5162
+ if (timedOut) {
4550
5163
  return {
4551
- ...emptyResponse(workspaceRoot),
4552
- hint: "Multiple distinct projectId values found in .cursor/rules. Use .task-boards.yaml or set WORKSPACE_ROOT to a single-project workspace."
5164
+ phase: "idle",
5165
+ pollResultSource,
5166
+ sleeperPolicy: SLEEPER_POLICY,
5167
+ nextSteps: [IDLE_INFLIGHT_CHECK_STEP, ...IDLE_TIMEOUT_NEXT_STEPS, IDLE_NO_DUPLICATE_WAKE_STEP]
4553
5168
  };
4554
5169
  }
4555
- if (rulesResult.status === "found") {
4556
- return enrichProject(client, workspaceRoot, "rule", rulesResult.projectId);
4557
- }
4558
- const folderToken = basename(workspaceRoot);
4559
- const listResponse = await client.get(import_projects.PROJECTS_ROUTES.listProjects());
4560
- const autoMatch = autoMatchProject(folderToken, listResponse.items);
4561
- if (autoMatch.kind === "single") {
4562
- return enrichProject(client, workspaceRoot, "auto_match", autoMatch.project.id);
4563
- }
4564
- if (autoMatch.kind === "candidates") {
5170
+ if (allRuns.length === 0) {
4565
5171
  return {
4566
- resolutionSource: "ambiguous",
4567
- workspaceRoot,
4568
- projectId: null,
4569
- name: null,
4570
- key: null,
4571
- presetCode: null,
4572
- candidates: autoMatch.candidates,
4573
- hint: `Multiple projects match folder name "${folderToken}". Pick one candidate or add .task-boards.yaml.`
5172
+ phase: "idle",
5173
+ pollResultSource,
5174
+ sleeperPolicy: SLEEPER_POLICY,
5175
+ nextSteps: [IDLE_INFLIGHT_CHECK_STEP, ...IDLE_TIMEOUT_NEXT_STEPS, IDLE_NO_DUPLICATE_WAKE_STEP]
4574
5176
  };
4575
5177
  }
4576
- return emptyResponse(workspaceRoot);
5178
+ const hasAttentionRuns = allRuns.some((run) => run.requiresAttention);
5179
+ return {
5180
+ phase: "processing",
5181
+ pollResultSource,
5182
+ sleeperPolicy: SLEEPER_POLICY,
5183
+ inflightSummary: buildInflightSummary(inflightRuns),
5184
+ processRuns: buildProcessRuns(items, inflightRuns),
5185
+ attentionSummary: buildAttentionSummary(allRuns),
5186
+ runLifecycle: RUN_LIFECYCLE_DOC,
5187
+ nextSteps: hasAttentionRuns ? [PROCESSING_NO_PARALLEL_POLL_STEP, ...ATTENTION_PROCESSING_NEXT_STEPS, ...PROCESSING_NEXT_STEPS] : [PROCESSING_NO_PARALLEL_POLL_STEP, ...PROCESSING_NEXT_STEPS]
5188
+ };
4577
5189
  }
4578
-
4579
- // src/tools/orchestrator.ts
4580
- async function runOrchestratorOnce(client, options) {
4581
- let projectId = options.projectId;
4582
- let resolutionSource = "explicit";
4583
- if (projectId === void 0) {
4584
- const resolved = await resolveProject(client, {
4585
- workspaceRootOverride: options.workspaceRootOverride,
4586
- envWorkspaceRoot: options.envWorkspaceRoot
4587
- });
4588
- if (resolved.projectId === null) {
4589
- const hint = resolved.hint ?? "Could not resolve project for workspace.";
4590
- throw new WorkspaceError("PROJECT_NOT_FOUND", hint);
4591
- }
4592
- projectId = resolved.projectId;
4593
- resolutionSource = resolved.resolutionSource;
4594
- }
4595
- const pollResult = await pollAgentRuns(client, {
4596
- projectId,
4597
- timeoutMs: options.timeoutMs,
4598
- pollIntervalMs: options.pollIntervalMs,
4599
- limit: options.limit
4600
- });
4601
- const enriched = enrichPollResult(pollResult.items, pollResult.inflightRuns, pollResult.timedOut);
5190
+ function enrichPollResult(items, inflightRuns, timedOut) {
4602
5191
  return {
4603
- projectId,
4604
- resolutionSource,
4605
- items: enriched.items,
4606
- inflightRuns: enriched.inflightRuns,
4607
- timedOut: enriched.timedOut,
4608
- orchestrator: enriched.orchestrator
5192
+ items,
5193
+ inflightRuns,
5194
+ timedOut,
5195
+ orchestrator: buildOrchestratorHints(items, inflightRuns, timedOut)
4609
5196
  };
4610
5197
  }
4611
- function registerOrchestratorTools(server, client, config) {
5198
+
5199
+ // src/tools/agent-runs.ts
5200
+ var agentRunStatusValues = Object.values(import_agent_run_status4.AGENT_RUN_STATUS);
5201
+ var agentRunOutcomeValues = Object.values(import_agent_run_outcome.AGENT_RUN_OUTCOME);
5202
+ var AGENTIC_SDLC_COLUMNS_SUMMARY = "AGENTIC_SDLC has 7 columns: backlog, in-analysis (PRODUCT\u2192ANALYST\u2192ARCHITECT handoffs), in-development (DESIGNER optional, then parallel FRONTEND_DEVELOPER+DEVELOPER), in-qa, in-awaiting, done, released.";
5203
+ var COMPLETE_AGENT_RUN_OUTCOME_SUMMARY = "Prerequisite: STORY assignee must be \u0410\u0433\u0435\u043D\u0442 \u0418\u0418 (SERVICE user) \u2014 otherwise no agent_run is enqueued or claimed. in-analysis: PRODUCT/ANALYST DEFAULT \u2192 HANDOFF next phase (skip unbound roles); ANALYST SKIP_DESIGN \u2192 in-development; ANALYST SKIP_DEV \u2192 released when codeChangesRequired=false (no git commit); ARCHITECT DEFAULT \u2192 in-development (set workItemPatch.designerRequired). in-development: DESIGNER DEFAULT \u2192 DEVELOPMENT; dev role DEFAULT removes role from pendingDevRoles, moves to in-qa when empty; SKIP_DESIGN skips DESIGNER. in-qa: DEFAULT \u2192 done; HAS_BUGS \u2192 in-development (re-init pendingDevRoles). NEEDS_CLARIFICATION \u2192 in-awaiting. FAILED \u2192 no move. Claim priority: in-development > in-qa > in-analysis (SERVICE-assigned stories only block analysis).";
5204
+ function registerAgentRunTools(server, client, config) {
5205
+ server.registerTool(
5206
+ "list_agent_runs",
5207
+ {
5208
+ description: "List agent runs for a project (default status PENDING). Prefer wait_for_agent_runs for orchestrator long-polling.",
5209
+ inputSchema: {
5210
+ projectId: z3.string().uuid().describe("Project UUID (required)"),
5211
+ status: z3.enum(agentRunStatusValues).optional().describe("Filter by agent run status")
5212
+ }
5213
+ },
5214
+ async ({ status, projectId }) => runTool(async () => {
5215
+ const effectiveStatus = status ?? import_agent_run_status4.AGENT_RUN_STATUS.PENDING;
5216
+ return client.get(import_agent_runs3.AGENT_RUNS_ROUTES.listAgentRuns(), {
5217
+ status: effectiveStatus,
5218
+ projectId
5219
+ });
5220
+ })
5221
+ );
4612
5222
  server.registerTool(
4613
- "run_orchestrator_once",
5223
+ "wait_for_agent_runs",
4614
5224
  {
4615
- description: "Resolve project (if needed), long-poll agent runs with atomic claim, and return orchestrator hints. Claims only STORY items assigned to \u0410\u0433\u0435\u043D\u0442 \u0418\u0418 (SERVICE user). Does not call sync_git_releases \u2014 the orchestrator agent decides when to sync.",
5225
+ description: "Long-poll agent runs for a project: atomically claims PENDING runs (POST /agent-runs/claim). On empty claim, immediately lists inflight DISPATCHED/ACKNOWLEDGED runs (GET activeOnly) for orphan recovery before sleeping. Returns items (newly claimed), inflightRuns (resume candidates), and orchestrator hints. First claim is immediate; subsequent polls sleep pollInterval seconds only when both claim and inflight are empty.",
4616
5226
  inputSchema: {
4617
- projectId: z2.string().uuid().optional().describe("Project UUID; when omitted, resolves via .task-boards.yaml / .cursor/rules / folder name"),
4618
- timeout: z2.number().int().min(1).max(300).default(120).describe("Maximum wait in seconds (1\u2013300, default 120)"),
4619
- pollInterval: z2.number().int().min(1).max(60).default(2).describe("Seconds between polls (1\u201360, default 2); must not exceed timeout"),
4620
- limit: z2.number().int().min(1).max(10).default(1).describe("Max runs to claim per poll (1\u201310, default 1)"),
4621
- workspaceRoot: z2.string().optional().describe("Optional absolute workspace root; defaults to WORKSPACE_ROOT or upward search from cwd")
5227
+ projectId: z3.string().uuid().describe("Project UUID"),
5228
+ timeout: z3.number().int().min(1).max(300).default(120).describe("Maximum wait in seconds (1\u2013300, default 120)"),
5229
+ pollInterval: z3.number().int().min(1).max(60).default(2).describe("Seconds between polls (1\u201360, default 2); must not exceed timeout"),
5230
+ limit: z3.number().int().min(1).max(10).default(1).describe("Max runs to claim per poll (1\u201310, default 1)")
4622
5231
  }
4623
5232
  },
4624
- async ({ projectId, timeout, pollInterval, limit, workspaceRoot }) => runTool(
4625
- async () => runOrchestratorOnce(client, {
5233
+ async ({ projectId, timeout, pollInterval, limit }) => runTool(async () => {
5234
+ const pollResult = await pollAgentRuns(client, {
4626
5235
  projectId,
4627
5236
  timeoutMs: timeout * 1e3,
4628
5237
  pollIntervalMs: pollInterval * 1e3,
4629
- limit,
4630
- workspaceRootOverride: workspaceRoot,
4631
- envWorkspaceRoot: config.workspaceRoot
4632
- })
4633
- )
5238
+ limit
5239
+ });
5240
+ return enrichPollResult(pollResult.items, pollResult.inflightRuns, pollResult.timedOut);
5241
+ })
5242
+ );
5243
+ server.registerTool(
5244
+ "ack_agent_run",
5245
+ {
5246
+ description: "Acknowledge agent run delivery after Task subagent has been dispatched. Requires status DISPATCHED (DISPATCHED \u2192 ACKNOWLEDGED).",
5247
+ inputSchema: {
5248
+ agentRunId: z3.string().uuid().describe("Agent run UUID"),
5249
+ note: z3.string().nullable().optional().describe("Optional acknowledgment note")
5250
+ }
5251
+ },
5252
+ async ({ agentRunId, note }) => runTool(async () => {
5253
+ const body = note !== void 0 ? { note } : void 0;
5254
+ const response = await client.patch(import_agent_runs3.AGENT_RUNS_ROUTES.acknowledgeAgentRun(agentRunId), body);
5255
+ writeActiveRunFromAck(config, response);
5256
+ return response;
5257
+ })
5258
+ );
5259
+ server.registerTool(
5260
+ "report_agent_attention",
5261
+ {
5262
+ description: "Signal that the in-flight subagent needs human attention while the run is ACKNOWLEDGED. Sets requiresAttention on the agent run for orchestrator/UI visibility.",
5263
+ inputSchema: {
5264
+ agentRunId: z3.string().uuid().describe("Agent run UUID"),
5265
+ message: z3.string().min(1).describe("Why human attention is needed")
5266
+ }
5267
+ },
5268
+ async ({ agentRunId, message }) => runTool(async () => {
5269
+ const body = {
5270
+ requiresAttention: true,
5271
+ attentionMessage: message
5272
+ };
5273
+ return client.patch(import_agent_runs3.AGENT_RUNS_ROUTES.setAgentRunAttention(agentRunId), body);
5274
+ })
5275
+ );
5276
+ server.registerTool(
5277
+ "clear_agent_attention",
5278
+ {
5279
+ description: "Clear the human-attention flag after the blocker is resolved. Requires the run to still be ACKNOWLEDGED.",
5280
+ inputSchema: {
5281
+ agentRunId: z3.string().uuid().describe("Agent run UUID")
5282
+ }
5283
+ },
5284
+ async ({ agentRunId }) => runTool(async () => {
5285
+ const body = {
5286
+ requiresAttention: false
5287
+ };
5288
+ return client.patch(import_agent_runs3.AGENT_RUNS_ROUTES.setAgentRunAttention(agentRunId), body);
5289
+ })
5290
+ );
5291
+ server.registerTool(
5292
+ "complete_agent_run",
5293
+ {
5294
+ description: `Complete agent run: apply AGENTIC_SDLC workflow transition and optional work item patch. ${AGENTIC_SDLC_COLUMNS_SUMMARY} Outcomes: ${COMPLETE_AGENT_RUN_OUTCOME_SUMMARY}`,
5295
+ inputSchema: {
5296
+ agentRunId: z3.string().uuid().describe("Agent run UUID"),
5297
+ outcome: z3.enum(agentRunOutcomeValues).optional().describe(
5298
+ `Completion outcome (DEFAULT, SKIP_DESIGN, SKIP_DEV, HAS_BUGS, NEEDS_CLARIFICATION, FAILED). ${COMPLETE_AGENT_RUN_OUTCOME_SUMMARY}`
5299
+ ),
5300
+ note: z3.string().nullable().optional().describe("Optional completion note"),
5301
+ workItemPatch: z3.object({
5302
+ description: z3.string().nullable().optional().describe("Updated work item description"),
5303
+ acceptanceCriteria: z3.string().nullable().optional().describe("Updated acceptance criteria"),
5304
+ designerRequired: z3.boolean().optional().describe("Architect only: whether DESIGNER phase runs before development")
5305
+ }).nullable().optional().describe("Optional work item field updates")
5306
+ }
5307
+ },
5308
+ async ({ agentRunId, outcome, note, workItemPatch }) => runTool(async () => {
5309
+ const body = {};
5310
+ if (outcome !== void 0) {
5311
+ body.outcome = outcome;
5312
+ }
5313
+ if (note !== void 0) {
5314
+ body.note = note;
5315
+ }
5316
+ if (workItemPatch !== void 0 && workItemPatch !== null) {
5317
+ body.workItemPatch = workItemPatch;
5318
+ }
5319
+ const hasBody = outcome !== void 0 || note !== void 0 || workItemPatch !== void 0 && workItemPatch !== null;
5320
+ const response = await client.post(
5321
+ import_agent_runs3.AGENT_RUNS_ROUTES.completeAgentRun(agentRunId),
5322
+ hasBody ? body : void 0
5323
+ );
5324
+ clearActiveRunAfterComplete(config);
5325
+ return response;
5326
+ })
4634
5327
  );
4635
5328
  }
4636
5329
 
4637
- // src/tools/attachments.ts
4638
- var import_attachments = __toESM(require_attachments_api(), 1);
4639
- var import_shared3 = __toESM(require_build2(), 1);
4640
- import { existsSync as existsSync5 } from "node:fs";
4641
- import { z as z3 } from "zod";
5330
+ // src/tools/orchestrator.ts
5331
+ import { z as z4 } from "zod";
4642
5332
 
4643
- // src/attachments/detect-mime-type.ts
4644
- import { basename as basename2 } from "node:path";
4645
- import { lookup as lookupMimeType } from "mime-types";
4646
- function detectMimeType(filePath) {
4647
- const mimeType = lookupMimeType(basename2(filePath));
4648
- return typeof mimeType === "string" ? mimeType : "application/octet-stream";
4649
- }
5333
+ // src/workspace/resolve-project.ts
5334
+ var import_projects = __toESM(require_projects_api(), 1);
5335
+ import { basename as basename3 } from "node:path";
4650
5336
 
4651
- // src/attachments/staging.util.ts
4652
- import { mkdir, writeFile } from "node:fs/promises";
4653
- import { dirname as dirname2, resolve as resolve2 } from "node:path";
4654
- var MAX_FILE_NAME_LENGTH = 200;
4655
- function sanitizeFileName(name) {
4656
- const baseName = name.split(/[/\\]/).pop() ?? name;
4657
- const sanitized = baseName.replace(/[^a-zA-Z0-9._-]/g, "_");
4658
- return sanitized.length > MAX_FILE_NAME_LENGTH ? sanitized.slice(0, MAX_FILE_NAME_LENGTH) : sanitized;
4659
- }
4660
- function buildStagingPath(workspaceRoot, workItemId, attachmentId, fileName) {
4661
- const sanitizedFileName = sanitizeFileName(fileName);
4662
- return resolve2(workspaceRoot, ".task-boards", "attachments", workItemId, `${attachmentId}_${sanitizedFileName}`);
4663
- }
4664
- function buildStagingDir(workspaceRoot, workItemId) {
4665
- return resolve2(workspaceRoot, ".task-boards", "attachments", workItemId);
4666
- }
4667
- async function writeStagingFile(path, data) {
4668
- const absolutePath = resolve2(path);
4669
- await mkdir(dirname2(absolutePath), { recursive: true });
4670
- await writeFile(absolutePath, data);
5337
+ // src/workspace/normalize-token.ts
5338
+ function normalizeToken(value) {
5339
+ return value.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "");
4671
5340
  }
4672
5341
 
4673
- // src/workspace/confine-file-path.ts
4674
- import { existsSync as existsSync4, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
4675
- import { basename as basename3, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3 } from "node:path";
4676
- function confineFilePath(workspaceRoot, filePath) {
4677
- const resolvedRoot = resolve3(workspaceRoot);
4678
- const resolvedPath = isAbsolute2(filePath) ? resolve3(filePath) : resolve3(resolvedRoot, filePath);
4679
- const relativePath = relative2(resolvedRoot, resolvedPath);
4680
- if (relativePath.startsWith("..") || isAbsolute2(relativePath)) {
4681
- throw new WorkspaceError(
4682
- "FILE_OUT_OF_BOUNDS",
4683
- `filePath is outside workspaceRoot: ${basename3(filePath) || filePath}`
4684
- );
4685
- }
4686
- if (!existsSync4(resolvedPath)) {
4687
- throw new WorkspaceError("FILE_NOT_FOUND", `filePath does not exist: ${basename3(filePath) || filePath}`);
4688
- }
4689
- const stats = statSync2(resolvedPath);
4690
- if (!stats.isFile()) {
4691
- throw new WorkspaceError("FILE_NOT_FOUND", `filePath is not a file: ${basename3(filePath) || filePath}`);
4692
- }
4693
- return resolvedPath;
4694
- }
4695
- function readConfinedWorkspaceFile(workspaceRoot, filePath) {
4696
- const absolutePath = confineFilePath(workspaceRoot, filePath);
4697
- const stats = statSync2(absolutePath);
4698
- const buffer = readFileSync3(absolutePath);
5342
+ // src/workspace/auto-match-project.ts
5343
+ function toCandidate(project, matchedBy) {
4699
5344
  return {
4700
- absolutePath,
4701
- fileName: basename3(absolutePath),
4702
- sizeBytes: stats.size,
4703
- buffer
5345
+ projectId: project.id,
5346
+ name: project.name,
5347
+ key: project.key,
5348
+ presetCode: project.presetCode,
5349
+ matchedBy
4704
5350
  };
4705
5351
  }
4706
-
4707
- // src/tools/attachments.ts
4708
- var MAX_ATTACHMENT_COUNT = 20;
4709
- var MAX_TOTAL_BYTES = 200 * 1024 * 1024;
4710
- async function fetchAttachments(client, workItemId) {
4711
- return client.get(import_attachments.ATTACHMENTS_ROUTES.listAttachments(workItemId));
4712
- }
4713
- function filterAttachments(items, attachmentIds) {
4714
- if (attachmentIds === void 0 || attachmentIds.length === 0) {
4715
- return items;
5352
+ function autoMatchProject(folderToken, projects) {
5353
+ const normalizedFolder = normalizeToken(folderToken);
5354
+ if (normalizedFolder.length === 0) {
5355
+ return { kind: "none" };
4716
5356
  }
4717
- const idSet = new Set(attachmentIds);
4718
- return items.filter((item) => idSet.has(item.id));
4719
- }
4720
- function assertDownloadLimits(attachments) {
4721
- if (attachments.length > MAX_ATTACHMENT_COUNT) {
4722
- throw new RestClientError(
4723
- "ATTACHMENT_LIMIT_EXCEEDED",
4724
- `Cannot download more than ${MAX_ATTACHMENT_COUNT} attachments at once (requested ${attachments.length})`
4725
- );
5357
+ const keyMatches = [];
5358
+ const nameMatches = [];
5359
+ for (const project of projects) {
5360
+ const input = {
5361
+ id: project.id,
5362
+ name: project.name,
5363
+ key: project.key,
5364
+ presetCode: project.presetCode
5365
+ };
5366
+ if (normalizeToken(project.key) === normalizedFolder) {
5367
+ keyMatches.push(input);
5368
+ }
5369
+ if (normalizeToken(project.name) === normalizedFolder) {
5370
+ nameMatches.push(input);
5371
+ }
4726
5372
  }
4727
- const totalBytes = attachments.reduce((sum, attachment) => sum + attachment.sizeBytes, 0);
4728
- if (totalBytes > MAX_TOTAL_BYTES) {
4729
- throw new RestClientError(
4730
- "ATTACHMENT_SIZE_LIMIT_EXCEEDED",
4731
- `Total attachment size ${totalBytes} bytes exceeds limit of ${MAX_TOTAL_BYTES} bytes`
4732
- );
5373
+ const uniqueById = /* @__PURE__ */ new Map();
5374
+ for (const project of keyMatches) {
5375
+ uniqueById.set(project.id, { project, matchedBy: "key" });
5376
+ }
5377
+ for (const project of nameMatches) {
5378
+ if (!uniqueById.has(project.id)) {
5379
+ uniqueById.set(project.id, { project, matchedBy: "name" });
5380
+ }
5381
+ }
5382
+ const matches = [...uniqueById.values()];
5383
+ if (matches.length === 1) {
5384
+ const match = matches[0];
5385
+ if (match === void 0) {
5386
+ return { kind: "none" };
5387
+ }
5388
+ return { kind: "single", project: match.project, matchedBy: match.matchedBy };
5389
+ }
5390
+ if (matches.length > 1) {
5391
+ return {
5392
+ kind: "candidates",
5393
+ candidates: matches.map((match) => toCandidate(match.project, match.matchedBy))
5394
+ };
4733
5395
  }
5396
+ return { kind: "none" };
4734
5397
  }
4735
- async function downloadWorkItemAttachments(client, params) {
4736
- const { workItemId, workspaceRoot, attachmentIds, overwrite = false, blockSensitiveAttachments = true } = params;
4737
- const listResponse = await fetchAttachments(client, workItemId);
4738
- const selected = filterAttachments(listResponse.items, attachmentIds);
4739
- assertDownloadLimits(selected);
4740
- const stagingDir = buildStagingDir(workspaceRoot, workItemId);
4741
- const downloaded = [];
4742
- const skipped = [];
4743
- if (attachmentIds !== void 0 && attachmentIds.length > 0) {
4744
- const selectedIds = new Set(selected.map((item) => item.id));
4745
- for (const attachmentId of attachmentIds) {
4746
- if (!selectedIds.has(attachmentId)) {
4747
- skipped.push({ attachmentId, reason: "not found on work item" });
5398
+
5399
+ // src/workspace/parse-ide-rules.ts
5400
+ import { existsSync as existsSync6, readdirSync, readFileSync as readFileSync4 } from "node:fs";
5401
+ import { join as join5 } from "node:path";
5402
+ var PROJECT_ID_LINE_PATTERN = /projectId\s*[:=]\s*["']?([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})/gi;
5403
+ var TASK_BOARDS_UUID_PATTERN = /task-boards[^\n\r]*?([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})/gi;
5404
+ function extractUuids(content) {
5405
+ const ids = /* @__PURE__ */ new Set();
5406
+ for (const pattern of [PROJECT_ID_LINE_PATTERN, TASK_BOARDS_UUID_PATTERN]) {
5407
+ pattern.lastIndex = 0;
5408
+ let match = pattern.exec(content);
5409
+ while (match !== null) {
5410
+ const id = match[1];
5411
+ if (id !== void 0) {
5412
+ ids.add(id.toLowerCase());
4748
5413
  }
5414
+ match = pattern.exec(content);
4749
5415
  }
4750
5416
  }
4751
- for (const attachment of selected) {
4752
- if (blockSensitiveAttachments && (0, import_shared3.isBlockedMcpAttachmentFileName)(attachment.fileName)) {
4753
- skipped.push({ attachmentId: attachment.id, reason: "blocked_sensitive_filename" });
5417
+ return ids;
5418
+ }
5419
+ function parseIdeRules(workspaceRoot) {
5420
+ const rulesDir = join5(workspaceRoot, ".cursor", "rules");
5421
+ if (!existsSync6(rulesDir)) {
5422
+ return { status: "missing" };
5423
+ }
5424
+ const entries = readdirSync(rulesDir, { withFileTypes: true });
5425
+ const allIds = /* @__PURE__ */ new Set();
5426
+ for (const entry of entries) {
5427
+ if (!entry.isFile()) {
4754
5428
  continue;
4755
5429
  }
4756
- const stagingPath = buildStagingPath(workspaceRoot, workItemId, attachment.id, attachment.fileName);
4757
- if (!overwrite && existsSync5(stagingPath)) {
4758
- skipped.push({ attachmentId: attachment.id, reason: "file already exists (set overwrite=true to replace)" });
5430
+ if (!entry.name.endsWith(".mdc") && !entry.name.endsWith(".md")) {
4759
5431
  continue;
4760
5432
  }
4761
- const { data } = await client.downloadBinary(import_attachments.ATTACHMENTS_ROUTES.downloadAttachment(workItemId, attachment.id));
4762
- await writeStagingFile(stagingPath, data);
4763
- downloaded.push({
4764
- attachmentId: attachment.id,
4765
- fileName: attachment.fileName,
4766
- path: stagingPath,
4767
- sizeBytes: data.length
4768
- });
5433
+ const content = readFileSync4(join5(rulesDir, entry.name), "utf8");
5434
+ for (const id of extractUuids(content)) {
5435
+ allIds.add(id);
5436
+ }
5437
+ }
5438
+ if (allIds.size === 0) {
5439
+ return { status: "missing" };
5440
+ }
5441
+ if (allIds.size === 1) {
5442
+ const projectId = [...allIds][0];
5443
+ if (projectId === void 0) {
5444
+ return { status: "missing" };
5445
+ }
5446
+ return { status: "found", projectId };
4769
5447
  }
5448
+ return { status: "ambiguous" };
5449
+ }
5450
+
5451
+ // src/workspace/resolve-project.ts
5452
+ function emptyResponse(workspaceRoot) {
4770
5453
  return {
4771
- workItemId,
4772
- downloaded,
4773
- skipped,
4774
- stagingDir
5454
+ resolutionSource: "ambiguous",
5455
+ workspaceRoot,
5456
+ projectId: null,
5457
+ name: null,
5458
+ key: null,
5459
+ presetCode: null,
5460
+ hint: "No project binding found. Add .task-boards.yaml, an IDE rule (.cursor/rules) with projectId, or rename the folder to match a project key/name."
4775
5461
  };
4776
5462
  }
4777
- async function uploadWorkItemAttachment(client, params) {
4778
- const { workItemId, workspaceRoot, filePath, blockSensitiveAttachments = true } = params;
4779
- const confinedFile = readConfinedWorkspaceFile(workspaceRoot, filePath);
4780
- if (blockSensitiveAttachments && (0, import_shared3.isBlockedMcpAttachmentFileName)(confinedFile.fileName)) {
4781
- throw new RestClientError(
4782
- "BLOCKED_SENSITIVE_FILENAME",
4783
- `Attachment filename is blocked for MCP upload: ${confinedFile.fileName}`
4784
- );
5463
+ function fromProject(workspaceRoot, resolutionSource, project) {
5464
+ return {
5465
+ resolutionSource,
5466
+ workspaceRoot,
5467
+ projectId: project.id,
5468
+ name: project.name,
5469
+ key: project.key,
5470
+ presetCode: project.presetCode
5471
+ };
5472
+ }
5473
+ async function enrichProject(client, workspaceRoot, resolutionSource, projectId) {
5474
+ const project = await client.get(import_projects.PROJECTS_ROUTES.getProject(projectId));
5475
+ return fromProject(workspaceRoot, resolutionSource, project);
5476
+ }
5477
+ async function resolveProject(client, options = {}) {
5478
+ const workspaceRoot = resolveWorkspaceRoot(options.workspaceRootOverride, options.envWorkspaceRoot);
5479
+ const yamlProjectId = parseTaskBoardsYaml(workspaceRoot);
5480
+ if (yamlProjectId !== null) {
5481
+ return enrichProject(client, workspaceRoot, "yaml", yamlProjectId);
4785
5482
  }
4786
- const mimeType = detectMimeType(confinedFile.absolutePath);
4787
- const attachment = await client.uploadMultipart(
4788
- import_attachments.ATTACHMENTS_ROUTES.uploadAttachment(workItemId),
4789
- "file",
4790
- {
4791
- buffer: confinedFile.buffer,
4792
- fileName: confinedFile.fileName,
4793
- mimeType
5483
+ const rulesResult = parseIdeRules(workspaceRoot);
5484
+ if (rulesResult.status === "ambiguous") {
5485
+ return {
5486
+ ...emptyResponse(workspaceRoot),
5487
+ hint: "Multiple distinct projectId values found in .cursor/rules. Use .task-boards.yaml or set WORKSPACE_ROOT to a single-project workspace."
5488
+ };
5489
+ }
5490
+ if (rulesResult.status === "found") {
5491
+ return enrichProject(client, workspaceRoot, "rule", rulesResult.projectId);
5492
+ }
5493
+ const folderToken = basename3(workspaceRoot);
5494
+ const listResponse = await client.get(import_projects.PROJECTS_ROUTES.listProjects());
5495
+ const autoMatch = autoMatchProject(folderToken, listResponse.items);
5496
+ if (autoMatch.kind === "single") {
5497
+ return enrichProject(client, workspaceRoot, "auto_match", autoMatch.project.id);
5498
+ }
5499
+ if (autoMatch.kind === "candidates") {
5500
+ return {
5501
+ resolutionSource: "ambiguous",
5502
+ workspaceRoot,
5503
+ projectId: null,
5504
+ name: null,
5505
+ key: null,
5506
+ presetCode: null,
5507
+ candidates: autoMatch.candidates,
5508
+ hint: `Multiple projects match folder name "${folderToken}". Pick one candidate or add .task-boards.yaml.`
5509
+ };
5510
+ }
5511
+ return emptyResponse(workspaceRoot);
5512
+ }
5513
+
5514
+ // src/tools/orchestrator.ts
5515
+ async function runOrchestratorOnce(client, options) {
5516
+ let projectId = options.projectId;
5517
+ let resolutionSource = "explicit";
5518
+ if (projectId === void 0) {
5519
+ const resolved = await resolveProject(client, {
5520
+ workspaceRootOverride: options.workspaceRootOverride,
5521
+ envWorkspaceRoot: options.envWorkspaceRoot
5522
+ });
5523
+ if (resolved.projectId === null) {
5524
+ const hint = resolved.hint ?? "Could not resolve project for workspace.";
5525
+ throw new WorkspaceError("PROJECT_NOT_FOUND", hint);
4794
5526
  }
4795
- );
5527
+ projectId = resolved.projectId;
5528
+ resolutionSource = resolved.resolutionSource;
5529
+ }
5530
+ const pollResult = await pollAgentRuns(client, {
5531
+ projectId,
5532
+ timeoutMs: options.timeoutMs,
5533
+ pollIntervalMs: options.pollIntervalMs,
5534
+ limit: options.limit
5535
+ });
5536
+ const enriched = enrichPollResult(pollResult.items, pollResult.inflightRuns, pollResult.timedOut);
4796
5537
  return {
4797
- workItemId,
4798
- attachment,
4799
- sourcePath: confinedFile.absolutePath
5538
+ projectId,
5539
+ resolutionSource,
5540
+ items: enriched.items,
5541
+ inflightRuns: enriched.inflightRuns,
5542
+ timedOut: enriched.timedOut,
5543
+ orchestrator: enriched.orchestrator
4800
5544
  };
4801
5545
  }
4802
- function registerAttachmentTools(server, client, config) {
4803
- server.registerTool(
4804
- "list_work_item_attachments",
4805
- {
4806
- description: "List file attachments for a work item.",
4807
- inputSchema: {
4808
- workItemId: z3.string().uuid().describe("Work item UUID")
4809
- }
4810
- },
4811
- async ({ workItemId }) => runTool(async () => fetchAttachments(client, workItemId))
4812
- );
4813
- server.registerTool(
4814
- "download_work_item_attachments",
4815
- {
4816
- description: "Download work item attachments into the local workspace staging directory (.task-boards/attachments).",
4817
- inputSchema: {
4818
- workItemId: z3.string().uuid().describe("Work item UUID"),
4819
- attachmentIds: z3.array(z3.string().uuid()).optional().describe("Optional subset of attachment UUIDs; downloads all when omitted"),
4820
- overwrite: z3.boolean().optional().default(false).describe("Replace existing staged files when true"),
4821
- workspaceRoot: z3.string().optional().describe("Optional absolute path to git repo root; defaults to WORKSPACE_ROOT or upward search from cwd")
4822
- }
4823
- },
4824
- async ({ workItemId, attachmentIds, overwrite, workspaceRoot }) => {
4825
- const resolvedWorkspaceRoot = resolveWorkspaceRoot(workspaceRoot, config.workspaceRoot);
4826
- return runTool(
4827
- async () => downloadWorkItemAttachments(client, {
4828
- workItemId,
4829
- workspaceRoot: resolvedWorkspaceRoot,
4830
- attachmentIds,
4831
- overwrite,
4832
- blockSensitiveAttachments: config.blockSensitiveAttachments
4833
- }),
4834
- { workspaceRoot: resolvedWorkspaceRoot }
4835
- );
4836
- }
4837
- );
5546
+ function registerOrchestratorTools(server, client, config) {
4838
5547
  server.registerTool(
4839
- "upload_work_item_attachment",
5548
+ "run_orchestrator_once",
4840
5549
  {
4841
- description: "Upload a local workspace file as a work item attachment (multipart field file).",
5550
+ description: "Resolve project (if needed), long-poll agent runs with atomic claim, and return orchestrator hints. Claims only STORY items assigned to \u0410\u0433\u0435\u043D\u0442 \u0418\u0418 (SERVICE user). Does not call sync_git_releases \u2014 the orchestrator agent decides when to sync.",
4842
5551
  inputSchema: {
4843
- workItemId: z3.string().uuid().describe("Work item UUID"),
4844
- filePath: z3.string().describe("Absolute or workspace-relative path to the file; must stay within workspaceRoot"),
4845
- workspaceRoot: z3.string().optional().describe("Optional absolute path to git repo root; defaults to WORKSPACE_ROOT or upward search from cwd")
5552
+ projectId: z4.string().uuid().optional().describe("Project UUID; when omitted, resolves via .task-boards.yaml / .cursor/rules / folder name"),
5553
+ timeout: z4.number().int().min(1).max(300).default(120).describe("Maximum wait in seconds (1\u2013300, default 120)"),
5554
+ pollInterval: z4.number().int().min(1).max(60).default(2).describe("Seconds between polls (1\u201360, default 2); must not exceed timeout"),
5555
+ limit: z4.number().int().min(1).max(10).default(1).describe("Max runs to claim per poll (1\u201310, default 1)"),
5556
+ workspaceRoot: z4.string().optional().describe("Optional absolute workspace root; defaults to WORKSPACE_ROOT or upward search from cwd")
4846
5557
  }
4847
5558
  },
4848
- async ({ workItemId, filePath, workspaceRoot }) => {
4849
- const resolvedWorkspaceRoot = resolveWorkspaceRoot(workspaceRoot, config.workspaceRoot);
4850
- return runTool(
4851
- async () => uploadWorkItemAttachment(client, {
4852
- workItemId,
4853
- workspaceRoot: resolvedWorkspaceRoot,
4854
- filePath,
4855
- blockSensitiveAttachments: config.blockSensitiveAttachments
4856
- }),
4857
- { workspaceRoot: resolvedWorkspaceRoot }
4858
- );
4859
- }
5559
+ async ({ projectId, timeout, pollInterval, limit, workspaceRoot }) => runTool(
5560
+ async () => runOrchestratorOnce(client, {
5561
+ projectId,
5562
+ timeoutMs: timeout * 1e3,
5563
+ pollIntervalMs: pollInterval * 1e3,
5564
+ limit,
5565
+ workspaceRootOverride: workspaceRoot,
5566
+ envWorkspaceRoot: config.workspaceRoot
5567
+ })
5568
+ )
4860
5569
  );
4861
5570
  }
4862
5571
 
4863
5572
  // src/tools/board.ts
4864
5573
  var import_boards = __toESM(require_boards_api(), 1);
4865
- import { z as z4 } from "zod";
5574
+ import { z as z5 } from "zod";
4866
5575
  function registerBoardTools(server, client) {
4867
5576
  server.registerTool(
4868
5577
  "get_board",
4869
5578
  {
4870
5579
  description: "Get board projection for a project (columns and on-board work items).",
4871
5580
  inputSchema: {
4872
- projectId: z4.string().uuid().describe("Project UUID")
5581
+ projectId: z5.string().uuid().describe("Project UUID")
4873
5582
  }
4874
5583
  },
4875
5584
  async ({ projectId }) => runTool(async () => client.get(import_boards.BOARDS_ROUTES.getBoard(projectId)))
4876
5585
  );
4877
5586
  }
4878
5587
 
4879
- // src/tools/comments.ts
4880
- var import_comments = __toESM(require_comments_api(), 1);
4881
- import { z as z5 } from "zod";
4882
- async function fetchComments(client, workItemId) {
4883
- return client.get(import_comments.COMMENTS_ROUTES.listComments(workItemId));
4884
- }
4885
- async function postComment(client, workItemId, body) {
4886
- const request = { body };
4887
- return client.post(import_comments.COMMENTS_ROUTES.createComment(workItemId), request);
4888
- }
4889
- function registerCommentTools(server, client) {
4890
- server.registerTool(
4891
- "list_comments",
4892
- {
4893
- description: "List comments for a work item (newest last).",
4894
- inputSchema: {
4895
- workItemId: z5.string().uuid().describe("Work item UUID")
4896
- }
4897
- },
4898
- async ({ workItemId }) => runTool(async () => fetchComments(client, workItemId))
4899
- );
4900
- server.registerTool(
4901
- "create_comment",
4902
- {
4903
- description: "Create a comment on a work item. Use for orchestrator NEEDS_CLARIFICATION questions before complete_agent_run.",
4904
- inputSchema: {
4905
- workItemId: z5.string().uuid().describe("Work item UUID"),
4906
- body: z5.string().min(1).describe("Comment body (markdown supported)")
4907
- }
4908
- },
4909
- async ({ workItemId, body }) => runTool(async () => postComment(client, workItemId, body))
4910
- );
4911
- }
4912
-
4913
5588
  // src/tools/labels.ts
4914
5589
  var import_label_color = __toESM(require_label_color_enum(), 1);
4915
5590
  var import_labels = __toESM(require_labels_api(), 1);
@@ -5306,44 +5981,6 @@ function registerSyncGitReleasesTools(server, client, config) {
5306
5981
 
5307
5982
  // src/tools/sync-project-subagents.ts
5308
5983
  import { z as z10 } from "zod";
5309
-
5310
- // src/subagents/sync-project-subagents.ts
5311
- var import_shared4 = __toESM(require_build2(), 1);
5312
- import { mkdirSync, writeFileSync } from "node:fs";
5313
- import { join as join4 } from "node:path";
5314
- var SAFE_SUBAGENT_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
5315
- var MAX_SUBAGENT_SLUG_LENGTH = 64;
5316
- function isSyncableSlug(slug) {
5317
- const trimmed = slug.trim();
5318
- return trimmed.length > 0 && trimmed.length <= MAX_SUBAGENT_SLUG_LENGTH && SAFE_SUBAGENT_SLUG_PATTERN.test(trimmed);
5319
- }
5320
- function buildFileContent(subagent) {
5321
- return (0, import_shared4.buildSubagentFileContentFromBody)({
5322
- slug: subagent.slug,
5323
- description: subagent.description,
5324
- bodyMarkdown: subagent.bodyMarkdown
5325
- });
5326
- }
5327
- async function syncProjectSubagents(client, params) {
5328
- const { projectId, workspaceRoot } = params;
5329
- const listResponse = await client.get(import_shared4.PROJECT_SUBAGENTS_ROUTES.listSubagents(projectId));
5330
- const directory = join4(workspaceRoot, ".cursor", "agents");
5331
- mkdirSync(directory, { recursive: true });
5332
- const synced = [];
5333
- const skipped = [];
5334
- for (const subagent of listResponse.items) {
5335
- if (!isSyncableSlug(subagent.slug)) {
5336
- skipped.push(subagent.slug);
5337
- continue;
5338
- }
5339
- const filePath = join4(directory, `${subagent.slug}.md`);
5340
- writeFileSync(filePath, buildFileContent(subagent), "utf8");
5341
- synced.push(subagent.slug);
5342
- }
5343
- return { synced, skipped, directory };
5344
- }
5345
-
5346
- // src/tools/sync-project-subagents.ts
5347
5984
  function registerSyncProjectSubagentsTools(server, client, config) {
5348
5985
  server.registerTool(
5349
5986
  "sync_project_subagents",
@@ -5499,6 +6136,7 @@ function registerWorkItemTools(server, client) {
5499
6136
  assigneeUserId: z11.string().uuid().nullable().optional().describe(
5500
6137
  "Project member user id assignee; set to \xAB\u0410\u0433\u0435\u043D\u0442 \u0418\u0418\xBB (SERVICE user) for AI orchestrator to process the STORY. Changing assignee from SERVICE to a human cancels pending agent runs for that work item."
5501
6138
  ),
6139
+ codeChangesRequired: z11.boolean().optional().describe("STORY only: when false, ANALYST may complete with SKIP_DEV to release without code/git"),
5502
6140
  labelIds: z11.array(z11.string().uuid()).max(10).optional().describe("Replace work item labels (up to 10)")
5503
6141
  }
5504
6142
  },
@@ -5513,6 +6151,7 @@ function registerWorkItemTools(server, client) {
5513
6151
  storyPoints,
5514
6152
  estimate,
5515
6153
  assigneeUserId,
6154
+ codeChangesRequired,
5516
6155
  labelIds
5517
6156
  }) => runTool(async () => {
5518
6157
  const current = await client.get(import_work_items.WORK_ITEMS_ROUTES.getWorkItem(workItemId));
@@ -5526,6 +6165,7 @@ function registerWorkItemTools(server, client) {
5526
6165
  storyPoints,
5527
6166
  estimate,
5528
6167
  assigneeUserId,
6168
+ codeChangesRequired,
5529
6169
  labelIds,
5530
6170
  version: current.version
5531
6171
  };
@@ -5584,7 +6224,7 @@ function registerTools(server, client, config) {
5584
6224
  registerLabelTools(server, client);
5585
6225
  registerAttachmentTools(server, client, config);
5586
6226
  registerBoardTools(server, client);
5587
- registerAgentRunTools(server, client);
6227
+ registerAgentRunTools(server, client, config);
5588
6228
  registerOrchestratorTools(server, client, config);
5589
6229
  registerSyncGitReleasesTools(server, client, config);
5590
6230
  registerSyncProjectSubagentsTools(server, client, config);
@@ -5607,7 +6247,17 @@ async function main() {
5607
6247
  }
5608
6248
 
5609
6249
  // src/cli.ts
5610
- main().catch((error) => {
6250
+ async function main2() {
6251
+ const argv = process.argv.slice(2);
6252
+ if (isAttentionCliInvocation(argv)) {
6253
+ const config = loadConfig();
6254
+ const client = new RestClient(config.apiUrl, config.apiToken);
6255
+ const exitCode = await runAttentionCli(argv, { config, client });
6256
+ process.exit(exitCode);
6257
+ }
6258
+ await main();
6259
+ }
6260
+ main2().catch((error) => {
5611
6261
  const message = error instanceof Error ? error.message : "Failed to start MCP server";
5612
6262
  console.error(message);
5613
6263
  process.exit(1);