Repository navigation
Add scrum-master scenario: Scrum Master autopilot using Node.js SDK #334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d33aecc
feb98d9
1c5a4af
251de83
ca578fa
aa27069
ff11662
2335201
5e8a957
0c5b4a7
812c25d
94afea8
69bc367
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| # OpenAI Configuration | ||
| # Use EITHER standard OpenAI OR Azure OpenAI (not both) | ||
|
|
||
| # Option 1: Standard OpenAI API | ||
| OPENAI_API_KEY= | ||
| OPENAI_MODEL=gpt-4o | ||
|
|
||
| # Option 2: Azure OpenAI (takes precedence if AZURE_OPENAI_API_KEY is set) | ||
| AZURE_OPENAI_API_KEY= | ||
| AZURE_OPENAI_ENDPOINT= | ||
| AZURE_OPENAI_DEPLOYMENT= | ||
| AZURE_OPENAI_API_VERSION=2024-10-21 | ||
|
|
||
| # MCP Tooling Configuration | ||
| BEARER_TOKEN= | ||
|
|
||
| # Enable to use observability exporter, default is false which means using console exporter. | ||
| # MUST be true in any deployed/dev-tunnel environment or MAC Activity stays empty. | ||
| ENABLE_A365_OBSERVABILITY_EXPORTER=false | ||
| # Use by the sample to demo using custom token resolver and token cache when it is true, otherwise use the built-in AgenticTokenCache | ||
| Use_Custom_Resolver=true | ||
| # optional - set to enable observability logs, value can be 'info', 'warn', or 'error', default to 'none' if not set | ||
| A365_OBSERVABILITY_LOG_LEVEL= | ||
| # Uncomment together with A365_OBSERVABILITY_LOG_LEVEL to debug export failures. | ||
| # OTEL_LOG_LEVEL=INFO | ||
|
|
||
| # --- A365 observability identity --- | ||
| # Runtime agent identity (the AGENTIC INSTANCE, not the blueprint). Normally | ||
| # populated automatically from activity.recipient.agenticAppId. Set explicitly | ||
| # only if you invoke the observability helpers outside the standard A365 | ||
| # hosting flow (e.g. a stand-alone script). See src/observability.ts. | ||
| agent365Observability__agentInstanceId= | ||
|
|
||
| # Environment Settings | ||
| NODE_ENV=development # Retrieve mcp servers from ToolingManifest | ||
| HOST=127.0.0.1 # Agents Playground connects to 127.0.0.1; set this so the server binds to the same address | ||
|
|
||
| # Telemetry and Tracing Configuration | ||
| DEBUG=agents:* | ||
|
|
||
| # --- Scenario-level logging (this sample only) ------------------------------- | ||
| # Level-based logger in src/util/logger.ts. | ||
| # LOG_LEVEL: error | warn | info | debug | trace (default=info) | ||
| # LOG_HTTP: when 'true', traces instrumented axios requests (including Jira) | ||
| # with status + latency. Not Graph/fetch or MCP transport tracing. | ||
| # Credentials are redacted. Keep false in production. | ||
| LOG_LEVEL=info | ||
| LOG_HTTP=false | ||
|
|
||
| # Use Agentic Authentication rather than OBO | ||
| USE_AGENTIC_AUTH=false | ||
|
|
||
| # Service Connection Settings | ||
| connections__service_connection__settings__clientId= | ||
| connections__service_connection__settings__clientSecret= | ||
| connections__service_connection__settings__tenantId= | ||
|
|
||
| # Set service connection as default | ||
| connectionsMap__0__serviceUrl=* | ||
| connectionsMap__0__connection=service_connection | ||
|
|
||
| # AgenticAuthentication Options | ||
| agentic_type=agentic | ||
| agentic_altBlueprintConnectionName=service_connection | ||
| agentic_scopes=ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default # Prod Agentic scope | ||
|
|
||
| # ============================================================================= | ||
| # Scrum Master Assistant POC configuration (added on top of the base sample) | ||
| # ============================================================================= | ||
|
|
||
| # --- Jira (Atlassian Cloud) --- | ||
| # Set JIRA_MODE=mock to run against the built-in mock sprint (no Atlassian creds needed). | ||
| JIRA_MODE=mock # live | mock | ||
| JIRA_BASE_URL=https://your-org.atlassian.net | ||
| JIRA_EMAIL= | ||
| JIRA_API_TOKEN= | ||
| JIRA_PROJECT_KEY=SCRUM # mock board key; replace for live Jira | ||
| JIRA_BOARD_ID=1 | ||
|
|
||
| # --- SharePoint (Microsoft Graph, delegated) --- | ||
| # Site URL of the site that will host the SMA_* lists + SprintReports library. | ||
| SHAREPOINT_SITE_URL= | ||
| SHAREPOINT_LISTS_PREFIX=SMA_ | ||
| # App registration used by scripts/setup-sharepoint.ts (device-code) and runtime Graph calls. | ||
| GRAPH_TENANT_ID=common | ||
| # Blank uses Microsoft Graph Command Line Tools public client | ||
| # (14d82eec-204b-4c2f-b7e8-296a70dab67e). Tenant policy/admin consent still applies. | ||
| # For production, register your own public client and set its app id here. | ||
| GRAPH_CLIENT_ID= | ||
|
|
||
| # --- Scrum ceremony scheduling --- | ||
| STANDUP_CRON=0 30 3 * * 1-5 # UTC = 09:00 Asia/Kolkata on weekdays | ||
| NIGHTLY_CRON=0 0 19 * * * # UTC = 00:30 Asia/Kolkata daily | ||
|
Comment on lines
+92
to
+93
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should fix: Cron defaults are documented as UTC, but node-cron evaluates them in TIMEZONE, so the standup fires at 03:30 IST The template describes these as UTC expressions ( With
Evidence
Scenario: create With the Function App also deployed:
Suggested fix: Pick one convention and make the template, config defaults and README agree:
In either case, drop 'Display only' for TIMEZONE in the README.
Comment on lines
+92
to
+93
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should fix: Cron defaults are documented as UTC, but node-cron evaluates them in TIMEZONE, so the standup fires at 03:30 IST The template describes these as UTC expressions ( With
Evidence
Scenario: create With the Function App also deployed:
Suggested fix: Pick one convention and make the template, config defaults and README agree:
In either case, drop 'Display only' for TIMEZONE in the README. |
||
| STANDUP_CUTOFF_HOURS=4 | ||
| TIMEZONE=Asia/Kolkata | ||
| # Both schedulers evaluate the expressions in UTC. TIMEZONE controls local | ||
| # date-based gates, card labels and meeting-slot searches, not cron evaluation. | ||
| # Leave WEBSITE_TIME_ZONE unset on the Functions app. | ||
|
|
||
| # --- Warn thresholds (Sprint risk detection) --- | ||
| WARN_TODO_PCT=0.40 # >40% of committed points still in To Do | ||
| WARN_SPRINT_PROGRESS_PCT=0.50 # ... once >=50% of sprint duration has elapsed | ||
|
|
||
| # --- Runtime toggles --- | ||
| LOCAL_CRON=true # run the in-process scheduler in dev; false in prod | ||
| AGENT_CALLBACK_URL= # Functions app setting: reachable agent base URL (no /api/messages suffix) | ||
| INTERNAL_TRIGGER_TOKEN= # required nonempty unless NODE_ENV is exactly development | ||
| # Cloud agent overrides: NODE_ENV=production, HOST=0.0.0.0, LOCAL_CRON=false | ||
| # when Functions timers are deployed. Do not copy the development values above. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # dependencies + build | ||
| node_modules/ | ||
| dist/ | ||
| *.tsbuildinfo | ||
|
|
||
| # TS compile artefacts inside src (only source .ts should be tracked) | ||
| src/**/*.js | ||
| src/**/*.d.ts | ||
| src/**/*.js.map | ||
| azure-functions/src/**/*.js | ||
| azure-functions/src/**/*.d.ts | ||
| azure-functions/src/**/*.js.map | ||
|
|
||
| # env & secrets — never commit | ||
| .env | ||
| .env.local | ||
| .env.*.local | ||
| .mstoken-cache.json | ||
|
|
||
| # generated at first `npx a365` run — contains tenant/agent IDs | ||
| a365.generated.config.json | ||
| a365.config.json | ||
| .a365-workspace-detection.local.json | ||
|
|
||
| # dev logs | ||
| dev-out.txt | ||
| seed-out.txt | ||
| setup-out.txt | ||
| *.log | ||
|
|
||
| # agent skills installed via `gh skill add microsoft/agent365-skills` — workspace-local | ||
| .agents/ | ||
| .claude/ | ||
| .github/copilot-instructions.md | ||
| .github/copilot-instructions.md.bak |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| # Scrum Master Agent Code Walkthrough | ||
|
|
||
| This walkthrough describes the scenario implementation, not the smaller base | ||
| OpenAI sample. For configuration and commands, see [`README.md`](README.md); | ||
| for flow diagrams, see [`docs/design.md`](docs/design.md). | ||
|
|
||
| ## 1. Entry point and request authentication | ||
|
|
||
| [`src/index.ts`](src/index.ts) loads `.env`, installs scenario logging, | ||
| prints the startup configuration summary and creates the Express server. | ||
|
|
||
| - `GET /api/health` is public. | ||
| - `/api/internal/*` routes run **before** JWT middleware. They check the raw | ||
| `x-internal-token` header against `INTERNAL_TRIGGER_TOKEN`; this is a shared | ||
| secret, not an Agent 365 token exchange. Blank is allowed for local | ||
| development only when `NODE_ENV` is exactly `development`. Every other | ||
| value, including unset or `test`, requires a nonempty token at startup. | ||
| - `POST /api/messages` sits **after** `authorizeJWT(authConfig)`. Only | ||
| `NODE_ENV=development` uses the empty development auth configuration. | ||
| Other environments load the SDK connection settings. | ||
| - `CloudAdapter.process` converts the request into a turn and calls | ||
| `agentApplication.run(context)`. Request JWT validation and the agent's | ||
| downstream token acquisition are separate stages. | ||
|
|
||
| The listener uses `PORT` (default `3978`) and `HOST`. Cloud hosting must set | ||
| `NODE_ENV=production` and `HOST=0.0.0.0`; do not deploy the template's | ||
| loopback/development settings. | ||
|
|
||
| ## 2. Agent application and dispatch | ||
|
|
||
| [`src/agent.ts`](src/agent.ts) defines `MyAgent`, an | ||
| `AgentApplication<TurnState>` using `MemoryStorage`. Its configured | ||
| authorization handler is `agentic`. Message and notification handlers declare | ||
| that handler; installation-update events have their own route. | ||
|
|
||
| For a message, the application: | ||
|
|
||
| 1. Attempts observability token warm-up; failures are logged and do not block | ||
| the scenario turn. | ||
| 2. Wraps dispatch in `runWithTurnBaggage`. | ||
| 3. Best-effort captures the sender's conversation reference for proactive | ||
| delivery. Live mode stores it through the roster service; mock mode keeps | ||
| its roster in-memory. | ||
| 4. Routes `activity.value.action` prefixes `standup.`, `reconcile.`, | ||
| `blocker.` and `meeting.` to the corresponding card handlers. | ||
| 5. Routes recognized slash commands through `handlers/commands.ts`. | ||
| 6. Sends nonempty free text to `handlers/answer.ts` by default. | ||
|
|
||
| The routing is important: ordinary sprint questions do **not** go through | ||
| `getClient()` and its generic MCP assistant. | ||
|
|
||
| ## 3. Deterministic scenario handlers | ||
|
|
||
| | Handler | Responsibility | | ||
| |---|---| | ||
| | `handlers/standup.ts` | Group Jira issues by roster assignee, send cards, collect updates and summarize | | ||
| | `handlers/reconcile.ts` | Classify update phrases and apply/approve Jira transitions | | ||
| | `handlers/chase.ts` | Resolve blockers, propose slots and book a meeting after card actions | | ||
| | `handlers/warn.ts` | Evaluate sprint progress and remaining-point thresholds | | ||
| | `handlers/sprint-summary.ts` | Classify issue risk and post a mid-sprint RAG table | | ||
| | `handlers/report.ts` | Generate deterministic sprint-close markdown with a per-sprint delivery guard | | ||
| | `handlers/config.ts` | Capture the destination channel conversation reference | | ||
|
|
||
| Reconciliation approval cards submit only the standup id and approval | ||
| toggles. The handler requires the Scrum Master's AAD object id and uses a | ||
| server-side pending proposal with a 24-hour lifetime; caller-supplied issue | ||
| transitions are not authoritative. Apply/skip consumes the proposal. | ||
| Expired, replayed or post-restart submissions fail closed and need a new | ||
| proposal. The deterministic classifier refuses completion inference when | ||
| negation is present. | ||
|
|
||
| `/standup` and `/help` do not require LLM inference. In mock mode the first | ||
| personal Playground caller becomes the mock Alex (SM) persona and receives | ||
| Alex's issue card; other mock assignees are separate identities, not silently | ||
| merged into the caller. Live delivery requires real roster mappings and prior | ||
| conversations. | ||
|
|
||
| ## 4. Grounded Jira Q&A | ||
|
|
||
| [`handlers/answer.ts`](src/handlers/answer.ts) creates a scenario-specific | ||
| `Agent` named `Scrum Master — Answer` using `JIRA_TOOLS` from | ||
| [`services/jira-tool.ts`](src/services/jira-tool.ts). | ||
|
|
||
| The model selects Jira tools for issue status, sprint issues and comments. | ||
| Friendly `Task-N` labels are translated at the tool boundary. `JIRA_MODE` | ||
| selects live REST or the mutable mock board; even with mocked Jira, inference | ||
| requires the configured OpenAI/Azure OpenAI service. | ||
|
|
||
| The handler retains at most eight user/assistant history items per user in a | ||
| process-local map. A restart loses this context. Empty model output receives | ||
| a fallback message; exceptions produce an error reply, not invented Jira data. | ||
|
|
||
| ## 5. Generic MCP assistant and notifications | ||
|
|
||
| Setting `SMA_ANSWER_DISABLED=true` switches ordinary free text to the generic | ||
| client in [`src/client.ts`](src/client.ts). This path acknowledges the message, | ||
| sends typing activities and calls `getClient(authorization, authHandlerName, | ||
| turnContext, displayName)`. | ||
|
|
||
| `getClient()` creates an OpenAI agent with the configured model and registers | ||
| MCP servers through `McpToolRegistrationService`. Registration receives the | ||
| authorization object, handler name, turn context and optional `BEARER_TOKEN`. | ||
| It does not read `MCP_AUTH_TOKEN` or `AGENTIC_USER_ID`. If registration fails, | ||
| the warning is logged and the agent can continue without those tools; that is | ||
| not proof that an external operation succeeded. | ||
|
|
||
| The notification route handles email notifications by retrieving content, | ||
| then asking the generic client to summarize/draft a response while treating | ||
| the email body as untrusted data. Other notification types receive a generic | ||
| notification-type response. Installation updates send welcome/farewell text. | ||
|
|
||
| ## 6. Calendar tools, fallback and connection lifecycle | ||
|
|
||
| [`services/calendar.ts`](src/services/calendar.ts) creates a separate | ||
| `Scrum Master — Calendar` agent with Zod-structured output and the same MCP | ||
| registration service pattern. It attempts calendar availability lookup and | ||
| event creation through the configured Calendar server. | ||
|
|
||
| - In mock mode the meeting-booking handler returns an offline notice rather | ||
| than attempting Graph-backed booking; it does not create a real event. | ||
| - If slot lookup or structured output is unusable, it returns synthetic | ||
| candidate slots. These are **not verified free/busy availability**. | ||
| - Booking failures are errors; suggested slots alone do not prove an event | ||
| was created. Successful booking requires the returned event details. | ||
| - The booking handler conditionally claims the blocker `open → booking` | ||
| before attempting Calendar and records `booked` afterwards. Uncertain | ||
| results retain a duplicate-prevention claim; inspect Outlook before | ||
| manually recovering it, rather than automatically retrying invitations. | ||
| - `withServers()` connects available transports and retries once on | ||
| `Session not found` / `-32001`. | ||
| - Neither the calendar helper nor `OpenAIClient.invokeAgent()` closes servers | ||
| after each call. The module-level tooling service reuses Streamable-HTTP | ||
| sessions; adding `finally { closeServers(); }` would break later turns. | ||
|
|
||
| ## 7. Observability and logging | ||
|
|
||
| [`src/client.ts`](src/client.ts) configures the service name | ||
| `Scrum Master Sample Agent`, initializes `OpenAIAgentsTraceInstrumentor`, | ||
| and selects the custom token resolver or the built-in agentic token cache. | ||
| [`src/observability.ts`](src/observability.ts) supplies runtime identity, | ||
| baggage and invoke/tool scope helpers. Missing identity/token information may | ||
| skip observability without blocking the user flow. | ||
|
|
||
| The generic client wraps inference in `InferenceScope`, records input/output | ||
| messages and finish reasons, and disposes the scope in `finally`. It does | ||
| **not** record fabricated fixed token counts. Exporter configuration is not | ||
| evidence of successful Defender/MAC ingestion. | ||
|
|
||
| `LOG_LEVEL` centrally filters console output; SDK-specific logging may also | ||
| have separate controls. `LOG_HTTP=true` traces instrumented axios clients including Jira, | ||
| not Graph's fetch client or MCP transports. | ||
|
|
||
| ## 8. Scheduling and persistence boundaries | ||
|
|
||
| [`cron/local-scheduler.ts`](src/cron/local-scheduler.ts) and the sibling | ||
| [`azure-functions/`](azure-functions) app drive standup and nightly ceremonies. | ||
| Both evaluate cron expressions in UTC; nightly checks cover Warn, mid-sprint | ||
| summary and sprint close with their eligibility gates. Deploy only one | ||
| scheduler (`LOCAL_CRON=false` when Functions is active). | ||
|
|
||
| Live mode uses delegated Graph device-code authentication for SharePoint, | ||
| separate from inbound request authentication and MCP authorization. The | ||
| file-backed MSAL cache is unencrypted and intended for local development. | ||
|
|
||
| Standup response fan-in, cutoff timers, completed daily guard and Q&A history | ||
| are process-local. SharePoint records do not make those maps a distributed | ||
| lock or reconstruct them after restart. Sprint-close delivery has a separate | ||
| durable per-sprint claim in live mode; `force` bypasses timing, not that guard. | ||
| Mock services and their changes are in-memory and reset with the process. | ||
| Live standup startup checks existing SharePoint session records before | ||
| fanout, but that check is not atomic across processes. Ambiguous failed | ||
| finalization remains `summarizing` for operator review, not automatic retry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should fix: Cron defaults are documented as UTC, but node-cron evaluates them in TIMEZONE, so the standup fires at 03:30 IST
The template describes these as UTC expressions (
0 30 3 * * 1-5 # UTC = 09:00 Asia/Kolkata), and the README calls them 'UTC cron'. But the local scheduler passes{ timezone: cfg.timezone }to node-cron, which matches the expression against wall-clock time in that zone.With
TIMEZONE=Asia/Kolkata:config.tsuses the same UTC strings as defaults, so leaving the vars unset has the same problem. The local scheduler and the Azure Function timers (which run these strings as UTC NCRONTAB) also fire 5.5 hours apart, not 'simultaneously'. README.md:221 callsTIMEZONE'Display only', but it drives scheduling, the T-2 gate in sprint-summary and the meeting-slot search.Evidence
.env.template:88-89:STANDUP_CRON=0 30 3 * * 1-5 # UTC = 09:00 Asia/Kolkata on weekdaysandNIGHTLY_CRON=0 0 19 * * * # UTC = 00:30 Asia/Kolkata daily..env.template:91:TIMEZONE=Asia/Kolkata..env.template:98:LOCAL_CRON=true.src/cron/local-scheduler.ts:37-44:cron.schedule(cfg.standupCron, async () => {...}, { timezone: cfg.timezone });(same at :46-52 for nightly).src/config.ts:106-109defaults:'0 30 3 * * 1-5','0 0 19 * * *',timezone: optional('TIMEZONE', 'Asia/Kolkata').timezoneoption compares against the time in that zone, so0 30 3 * * 1-5matches at 03:30 Asia/Kolkata.azure-functions/src/index.ts:31-34:// 09:00 Asia/Kolkata Mon–Fri = 03:30 UTC,schedule: '0 30 3 * * 1-5', as NCRONTAB in UTC.getScheduleConfig().timezoneis used in local-scheduler.ts:44/52, sprint-summary.ts:253-263 and chase.ts:332.Scenario: create
.envfrom the template and runnpm run devon Monday. Standup cards are DM'd at 03:30 IST and the cutoff summary posts at about 07:30 IST.With the Function App also deployed:
101#<Mon>).Suggested fix: Pick one convention and make the template, config defaults and README agree:
{ timezone: 'Etc/UTC' }in local-scheduler.ts). The documented values and the Function App timers then line up.0 0 9 * * 1-5and0 30 0 * * *, described as 'evaluated in TIMEZONE'.In either case, drop 'Display only' for TIMEZONE in the README.