LSA-9069 — Jira Software + ServiceNow opt-in outbound create¶
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Story / ticket: LSA-9069 — Ticketing — Jira Software / ServiceNow opt-in outbound create (not bidirectional v1)
Estimated PR size: large (~2000–2500 LOC)
Owner: @floh/server connectors + itsm-sync; operator docs; admin ticket detail
Spec / design doc: clone docs/plans/2026-08-28-lsa-9894-plane-outbound-create.md + ADR-014
Parent epic: LSA-9065
Blocked by: LSA-9894 (link table, {{ticket.*}}, Plane persist) — present on this branch
Product: same as Plane — catalog create_ticket plus an explicit connector createWorkItem step. Jira Software issues (POST /rest/api/3/issue), not JSM request APIs. ServiceNow v1 is incident. Local e2e: Jira Cloud free site + ServiceNow PDI (option A), not Docker. One PR ships both vendors.
1. Goal¶
Operators can add a jira and/or servicenow connector instance and bind createWorkItem on catalogs that should open engineering/IT work in that system. Success writes ticket_external_link (external_system jira or servicenow). Admin ticket detail lists every stored link (Plane + Jira + ServiceNow). Email inbound and POST /api/tickets still do not create remote items. No inbound webhooks, no status PATCH, no comments.
1a. Why Jira Software issues (not JSM requests)¶
Plane v1 opens a work item in an engineering project, not a second intake form. Floh is already the customer-facing ticket of record. The Jira peer matches that role: POST /rest/api/3/issue. JSM /rest/servicedeskapi/request is a second service desk and is deferred (jira_sm CHECK value reserved). ServiceNow v1 stays incident, not sc_req_item.
2. Scope contract¶
Hard rule: the implementation MUST NOT modify files outside this list.
In scope (will modify)¶
| File | Reason |
|---|---|
docs/plans/2026-09-02-lsa-9069-jira-servicenow-outbound-create.md |
This plan |
docs/architecture/decisions.md |
ADR-014 one-paragraph update |
docs/connectors/jira-connector.md |
Operator setup |
docs/connectors/servicenow-connector.md |
Operator setup |
docs/connectors/README.md |
Index rows |
packages/server/src/db/migrations/20260908120000_ticket_external_link_jira_system.ts |
Widen CHECK to include jira |
packages/server/src/modules/itsm-sync/constants.ts |
EXTERNAL_SYSTEM_JIRA |
packages/server/src/modules/itsm-sync/itsm-create-handler.ts |
Dispatch table |
packages/server/src/modules/itsm-sync/persist-itsm-link.ts |
Generic persist |
packages/server/src/modules/itsm-sync/persist-plane-link.ts |
Plane wrapper |
packages/server/src/modules/itsm-sync/plane-create-work-item-step.ts |
Shared prepare |
packages/server/src/modules/itsm-sync/jira-url.ts |
Browse URL |
packages/server/src/modules/itsm-sync/servicenow-url.ts |
Incident URL |
packages/server/src/modules/itsm-sync/resolve-plane-connector.ts |
Fallback URLs for jira/SN |
packages/server/src/modules/itsm-sync/README.md |
Module doc |
packages/server/src/modules/workflows/step-executor.ts |
Dispatch table instead of plane-only if |
packages/server/src/modules/connectors/handlers/jira.ts |
defineConnector |
packages/server/src/modules/connectors/handlers/jira-support/** |
Client, URL validator, config |
packages/server/src/modules/connectors/handlers/servicenow.ts |
defineConnector |
packages/server/src/modules/connectors/handlers/servicenow-support/** |
Client, URL validator, config |
packages/server/src/modules/connectors/handlers/index.ts |
Register both |
packages/web/src/app/features/tickets/ticket-detail.component.ts |
List all external links |
packages/web/src/app/features/tickets/ticket-detail.component.spec.ts |
Multi-link spec |
Unit tests under packages/server/test/unit/connectors/ and …/itsm-sync/ |
Mirror Plane |
packages/server/test/unit/step-executor-connector.test.ts |
Persist mock path |
Out of scope (will NOT modify in this PR)¶
| File / area | Why deferred | Tracking ticket |
|---|---|---|
| Jira Service Management request types / service desk API | Product chose Software issues | Future (reserved jira_sm) |
| OAuth 2.0 | Clone Plane: t.secret API token / password |
Defer |
| Inbound HMAC, CSRF-exempt routes, status PATCH, comments | Same park as 9895/9896 | LSA-9895 / LSA-9896 analogues later |
| Auto-create on every Floh ticket | Opt-in step only | — |
| Portal My Tickets URLs | Admin externalLinks already |
— |
| Configurable ServiceNow table name | v1 = incident only |
— |
| Assignee / attachment sync | Not asked | — |
| In-repo Docker Compose for Jira or ServiceNow | SN cannot run locally; Jira DC eval self-serve ended 2026-03-30 | Manual e2e only |
Adjacent code that would benefit from a sweep but is explicitly NOT included¶
- Plane inbound HMAC / status PATCH (LSA-9895).
- Renaming
resolve-plane-connector.ts(keep filename; extend fallbacks).
3. Invariants matrix¶
| # | Invariant | Enforcement layer | Covering test |
|---|---|---|---|
| 1 | Outbound HTTP uses connectorHttpRequest only (auth: basic for Jira and SN). |
Clients + no-raw-fetch-in-modules |
jira-client.test.ts, servicenow-client.test.ts |
| 2 | Jira apiToken and SN password are t.secret; never logged; Basic auth only. |
defineConnector + errorMap |
handler secret-flag + redaction |
| 3 | baseUrl is origin; shared URL validator; private net needs allowPrivateNetworkEndpoint; strip trailing /rest/api/3 (Jira) and /api/now (SN). |
Config resolvers | SSRF / suffix tests |
| 4 | Create only from explicit jira/servicenow createWorkItem step; catalog/email/POST /api/tickets do not call them. |
No new hooks | existing hook-site specs stay green |
| 5 | Reserved {{ticket.*}} bind: engine overwrites externalId / ticketNumber; missing snapshot fails before HTTP. |
Shared prepare | create-step tests |
| 6 | Local adopt: existing (ticket_id, system) link completes with stored ids, no HTTP. |
Prepare | create-step tests |
| 7 | Remote idempotency: Jira lookup by label floh-{ticketUuid} then create; SN correlation_id = ticket UUID then create. |
Clients | client lookup+create tests |
| 8 | Persist unique-violation OK only when stored external_id matches. |
Persist helper | persist tests |
| 9 | Jira create id (numeric issue id) and key required; SN sys_id + number required. |
Parse guards | malformed-body tests |
| 10 | Logs: ids/keys/status only — never summary, description, token, password. | Clients + errorMap | redaction assertions |
| 11 | Disabled connector fails before HTTP (same as Plane). | isConnectorDisabled |
existing step-executor |
| 12 | Jira projectKey allowlisted (/^[A-Z][A-Z0-9_]+$/); SN table hardcoded incident. |
Runtime guard | unsafe-slug tests |
| 13 | Manager GET still omits externalLinks without ticket:manage; stored web_url preferred. |
Existing route | existing + URL-builder tests |
4. Persistence- and runtime-boundary matrix¶
| Boundary | Reads / writes | Validator | Test |
|---|---|---|---|
| Migration CHECK | ticket_external_link.external_system |
SQL CHECK | migration up/down |
| Connector execute | existing authenticated routes | existing | handler tests |
step-executor persist after createWorkItem |
writes link row | payload parse + unique match | persist + step-executor |
| No new public route prefix | — | routes not registered | §4b |
4a. Platform contract checklist¶
Applies: persistence (CHECK migration) + connector secrets + operator docs.
| # | Topic | Yes / No / N/A | Where enforced / documented |
|---|---|---|---|
| 1 | env vars | N/A | Connector row fields, not env |
| 2 | secrets | Yes | Jira apiToken, SN password via t.secret |
| 3 | bootstrap | N/A | loadBuiltInConnectors already |
| 4 | shutdown | N/A | No new worker |
| 5 | test-only hooks | N/A | No new hooks |
| 6 | redaction | Yes | Invariant 10; client + handler tests |
| 7 | Docker | N/A | No Compose peers (option A) |
| 8 | public-export surface | Yes | handlers/index.ts built-in seed only |
| 9 | operator runbook | Yes | docs/connectors/jira-connector.md, servicenow-connector.md |
| 10 | CI two-step | N/A | Existing vitest; no new required check |
| 11 | rollback | Yes | down() restores CHECK; disable instance / remove step |
| 12 | PII | Yes | Never log description/summary; externalLinks manage-only |
4b. Negative space¶
| Behavior the PR does NOT support | How accidental support is prevented | Follow-up ticket |
|---|---|---|
JSM /request API |
No servicedeskapi client; jira_sm unused |
Future JSM story |
| OAuth | Fields not declared | Defer |
Inbound / CSRF /api/jira-ticketing or /api/servicenow-ticketing |
Routes not registered | Later |
updateWorkItem / comments |
Commands absent | Later |
SN tables other than incident |
Hardcoded path | Defer |
| Auto-create | No ticket-route enqueue | — |
5. File map¶
docs/plans/2026-09-02-lsa-9069-jira-servicenow-outbound-create.md [create]
docs/architecture/decisions.md [modify]
docs/connectors/jira-connector.md [create]
docs/connectors/servicenow-connector.md [create]
docs/connectors/README.md [modify]
packages/server/src/db/migrations/20260908120000_ticket_external_link_jira_system.ts [create]
packages/server/src/modules/itsm-sync/ [modify + create]
packages/server/src/modules/workflows/step-executor.ts [modify]
packages/server/src/modules/connectors/handlers/jira.ts [create]
packages/server/src/modules/connectors/handlers/jira-support/ [create]
packages/server/src/modules/connectors/handlers/servicenow.ts [create]
packages/server/src/modules/connectors/handlers/servicenow-support/ [create]
packages/server/src/modules/connectors/handlers/index.ts [modify]
packages/web/src/app/features/tickets/ticket-detail.component.ts [modify]
packages/server/test/unit/connectors/jira-*.test.ts [create]
packages/server/test/unit/connectors/servicenow-*.test.ts [create]
packages/server/test/unit/itsm-sync/*.test.ts [modify + create]
packages/server/test/unit/step-executor-connector.test.ts [modify]
6. Implementation steps¶
Step 1 — Plan + CHECK + dispatch¶
- Write this plan; rewrite LSA-9069 Jira AC.
- Widen CHECK; add
jiratoEXTERNAL_SYSTEMS. - Extract
ITSM_CREATE_HANDLERS+ generic prepare/persist; wirestep-executor.
Verification:
- [ ] Plane unit tests still pass.
- [ ]
preparePlaneCreateWorkItemStep/ persist-plane wrappers remain.
Step 2 — Jira connector¶
jirahandler:test+createWorkItem; Basic auth; label lookup; persistjira.
Verification:
- [ ]
pnpm --filter @floh/server run test:unit -- jirapasses.
Step 3 — ServiceNow connector¶
servicenowhandler:test+createWorkItem;correlation_idlookup; persistservicenow.
Verification:
- [ ]
pnpm --filter @floh/server run test:unit -- servicenowpasses.
Step 4 — Admin UI + docs¶
- List all
externalLinks; operator docs; ADR-014.
Verification:
- [ ] Ticket-detail spec covers Plane + Jira links.
7. Testing strategy¶
- Unit tests: URL/SSRF, Basic auth not logged, create/lookup/adopt, malformed id, unique conflict mismatch, prepare without snapshot, persist failure after HTTP success.
- Architectural tests: existing
no-raw-fetch-in-modulesandconnector-url-validators-userinfocover newvalidate-*.tsfiles. - Integration tests: not required — CI stays mocked HTTP. Manual e2e is Jira Cloud free + ServiceNow PDI.
7.5 Malformed-input matrix¶
| # | Input source | Trust boundary | Malformed shape | Expected behavior | Spec file / it(...) |
§3 inv. |
|---|---|---|---|---|---|---|
| 1 | Jira baseUrl |
validateJiraBaseUrl |
userinfo / metadata IP | fail closed, no HTTP | validate-jira-base-url.test.ts |
3 |
| 2 | Jira projectKey |
resolveJiraConfig |
/ or .. |
fail closed | jira-handler.test.ts unsafe projectKey |
12 |
| 3 | Jira create body | parseJiraIssue |
missing id or key |
step fail, no link | jira-client.test.ts missing id/key |
9 |
| 4 | SN create body | parseIncident |
missing sys_id/number |
step fail | servicenow-client.test.ts |
9 |
| 5 | Step externalId interpolated |
prepareItsmCreateWorkItemStep |
other ticket UUID | overwritten from snapshot | create-step tests | 5 |
| 6 | JQL/SN query | prepare already failed | non-UUID ticket id | never interpolated | prepare tests | 5 |
8. Stopping criteria for the autonomous loop¶
- [ ] Zero unresolved review comments tagged
CRITICALorMAJOR. - [ ] Every invariant in §3 has a passing test.
- [ ] All CI checks (typecheck, lint, unit, integration, architectural) are green.
- [ ] Every modified file is listed in §2 (scope contract).
- [ ] The PR description references this plan doc.
Reassess rule — newly-raised MAJOR findings¶
If two consecutive review rounds raise NEW MAJOR findings, halt and reassess against §4b (do not add bidirectional sync, JSM, or OAuth).
9. Acceptance gates (user-visible)¶
- [ ] Catalog with
jira/createWorkItemaftercreate_ticketcreates a Jira issue; admin sees key + browse URL. - [ ] Same for ServiceNow incident number + URL.
- [ ] Second step run adopts; no duplicate remote item.
- [ ] Catalog without those steps: Floh ticket only.
- [ ] Forged/unauthenticated “webhook” has no route.
10. Risks and unknowns¶
- Large PR. User chose one PR; keep commits sequential (dispatch → Jira → SN) so review can be chunked.
- Jira ADF. Minimal one-paragraph ADF; do not invent a full wiki converter.
- Jira labels.
floh-{uuid}must be unique enough; do not use summary-equality for adopt. - Epic vs story drift. Epic LSA-9065 still says bidirectional + OAuth + 60s auto-create. Do not implement the epic AC.
- Local e2e is cloud, not Docker. Locked to option A. CI stays mocked HTTP.
10a. Local development (option A — locked)¶
| Peer | Instance | Connector baseUrl |
Auth |
|---|---|---|---|
| Jira Software | Jira Cloud free (https://<site>.atlassian.net) |
that origin | email + API token (Basic) |
| ServiceNow | PDI (https://devXXXXX.service-now.com) |
that origin | PDI admin Basic |
11. Out-of-band cleanups deferred from this PR¶
- Parked Plane sync: LSA-9895 / LSA-9896 unchanged.
- JSM request-type connector: new story when product asks (uses reserved
jira_sm). - Comment on LSA-9065 AC vs shipped opt-in model.
12. Sign-off¶
- Author — agent: clone of 9894 for Jira Software + SN incident, one PR.
- Architectural reviewer (human) — confirmed via Cursor plan
lsa-9069_jira_servicenow_95d36879before Agent implementation. - Platform reviewer (human) — required (§4a/§4b): CHECK migration + secrets + operator docs. Recorded with the Cursor plan approval that started this implementation.