Implement changes to "il plan" and "il start" to support pasting in urls to Github issue + PR, linear, JIRA urls#1010
Conversation
Enhancement AnalysisOpen Questions (autonomous-execution assumptions filled in)
Problem Summary User Impact Enhancement Goal Next Steps
Complete Context and Details (click to expand)User Stories
Supported URL Shapes
All shapes must tolerate: trailing slashes, query strings ( Acceptance Criteria
Edge Cases
Out of Scope
AuthorReporter: @acreeger. |
Issue #1009 Analysis — URL-paste support for
|
| Question | Answer |
|---|---|
Does the spec's "provider mismatch errors loudly" rule extend to GitHub-PR URLs pasted in a Linear/Jira-configured project? Today il start already routes any numeric input to GitHubService for PR detection regardless of the configured tracker (src/commands/start.ts:537-557), so a pasted GitHub PR URL on a Linear project is the spec's "ambiguous case." |
The PR URL form already has a privileged path through GitHubService. Recommend treating GitHub PR URLs as always valid for il start regardless of configured tracker (no provider-mismatch error), and only enforcing provider-mismatch on GitHub issue URLs vs. Linear/Jira-configured projects. Otherwise the new rule contradicts existing PR-detection behaviour for Linear users. |
Should the URL parser run in cli.ts's argument parser (so it normalizes before reaching commands), or only inside start.ts / plan.ts? |
Recommend keeping it inside the commands so each command can apply its own policy (il plan accepts cross-repo, il start rejects, il plan rejects PR URLs). A shared utility is fine; the policy is per-command. |
il plan currently calls issueTracker.detectInputType(prompt) and fetchIssue(detection.identifier) with no repo argument (plan.ts:224, :228). The spec requires il plan to accept cross-repo GitHub URLs. Is threading repo through these two calls (plus the MCP provider construction at plan.ts:236/:253 and the auto-swarm StartCommand.execute() at :715/:743) the intended design? |
Yes — that's the only way the URL's owner/repo reaches the GitHub API. The MCP provider construction also needs the repo (otherwise GitHub MCP calls fall back to the cwd remote and silently fetch the wrong issue). |
HIGH/CRITICAL Risks
- Identifier case-sensitivity (CLAUDE.md rule): URL paths on
linear.appare lowercase-tolerant.LinearService.normalizeIdentifier()(src/lib/LinearService.ts:221) andJiraIssueTracker.normalizeIdentifier()(src/lib/providers/jira/JiraIssueTracker.ts:90) both uppercase. Any URL-extracted Linear/Jira key MUST be normalized before lookup, comparison, or worktree resolution — otherwise an existing loom registered asWEB-2423won't be found from a pastedweb-2423URL. - Cross-repo
repoplumbing inil plan: A URL pointing atiloom-ai/other-repowhilecwdisiloom-ai/iloom-clirequires the parsedrepoto flow into four call sites:detectInputType,fetchIssue,IssueManagementProviderFactory.create()/mcpProvider.getIssue(), and the auto-swarmStartCommand.execute(). Missing any silently falls back to the cwd remote and corrupts the MCP/decomposition context. - Provider mismatch carve-out for GitHub PR URLs on Linear/Jira projects:
il start's existing PR fallback (start.ts:537-557) routes numeric PRs to GitHubService regardless of configured tracker. A blanket "URL provider must match configured tracker" rule would regress this. The spec needs an explicit carve-out, or the implementation has to treat GitHub PR URLs as a special case independent of tracker.
Impact Summary
- 3 files for primary modification:
src/commands/start.ts(~6 places:parseInput, validation paths, telemetry, repo plumbing)src/commands/plan.ts(~5 places: identifier detection,repothreading to detectInputType/fetchIssue/MCP/StartCommand)src/utils/IdentifierParser.ts(addparseTrackerUrl()alongsidematchIssueIdentifier())
- 2 test files to extend:
src/utils/IdentifierParser.test.ts,src/commands/start.test.ts(input parsing block at line 203), and a newsrc/commands/plan.test.tsblock. - 1 type file to extend:
src/types/telemetry.tsaddsidentifier_source: 'url' | 'identifier'toLoomCreatedProperties. - 1 doc file:
docs/iloom-commands.mdper CLAUDE.md documentation requirement.
📋 Complete Technical Reference (click to expand for implementation details)
Problem Space Research
Problem Understanding
Users frequently start work from a browser tab, Slack message, or email and currently must hand-extract the identifier (especially Linear TEAM-NUM or Jira KEY-NUM) before invoking il start / il plan. Tracker URLs come in four shapes (GitHub issue, GitHub PR, Linear, Jira self-hosted + cloud) plus chat-client and shell artifacts (angle-bracket-wrapped, trailing punctuation, query/fragment). The spec assumes URL parsing is additive — existing numeric/project-key inputs must continue to work unchanged.
Architectural Context
The codebase already has a strict separation between shape detection (regex on a string, matchIssueIdentifier()) and existence validation (provider-specific IssueTracker.detectInputType()). URL parsing slots in as a third detection layer before shape detection, normalizing a URL into the same { identifier, repo? } tuple that the existing flow consumes. The IssueTracker interface (src/lib/IssueTracker.ts:16) already exposes normalizeIdentifier() (line 48) — the key contract URL-extracted identifiers must flow through.
Cross-repo support in il plan was added in commit 8873e1a (docs(plan): add cross-repo epic guidance to plan command prompt) — but that commit is prompt-only: it added "One Epic Per Repo for Cross-Repo Work" guidance to templates/prompts/plan-prompt.txt:154-164. The il plan CLI command itself has no --repo flag (src/cli.ts:1622-1647) and threads no repo into provider calls today. Cross-repo URL support in il plan is therefore the first code-level cross-repo affordance, not just a docs change.
Edge Cases Identified
- Wrapped URLs: chat clients output
<https://...>and trailing-comma artifacts; spec requires defensive trimming. - Auth-credentialed URLs:
https://user:[email protected]/...—URL.parse()strips auth into.username/.password; treat as parse-only and never log the raw URL (telemetry / debug logger). - Mixed case:
Issuesvsissuesin path segments; tracker keywords are case-insensitive but the extracted identifier must be normalized viaIssueTracker.normalizeIdentifier()(uppercase for Linear/Jira). - Slug suffixes: Linear's "copy as link" produces
linear.app/<workspace>/issue/TEAM-123/<title-slug>— slug must be discarded. - Query / fragment:
?focusedCommentId=...,#issuecomment-12345,#comment-...— strip wholesale. - Trailing slash:
.../issues/123/should resolve identically. - Self-hosted Jira host validation: detection by path shape (
/browse/<KEY>-<NUM>), not host. Configured Jira host (settings.issueManagement.jira.host) must match URL host or error loudly with both hosts named. - Empty / partial URLs:
https://github.com/owner/repo/issues/(missing number) should produce a clear error naming what was missing. - GitHub Enterprise (
github.<company>.com): out of scope per spec — error like an unknown host.
Codebase Research Findings
Affected Area 1: Shared identifier matcher
Entry Point: src/utils/IdentifierParser.ts:30 (matchIssueIdentifier())
Used By:
src/commands/start.ts:487— sole gate beforeIssueTracker.detectInputType()foril startsrc/commands/plan.ts:204— sole gate forlooksLikeIssueIdentifierinil plansrc/utils/IdentifierParser.ts:81—IdentifierParserclass for cleanup/finish (worktree-pattern matching, NOT URL parsing — different concern; do not extend it)
Note: The class IdentifierParser at line 81 operates on existing worktrees and matches branch-name patterns. It is unrelated to URL parsing and should not be extended for this feature. The new helper should be a standalone exported function in the same file (alongside matchIssueIdentifier) or a sibling file.
Affected Area 2: il start parseInput
Entry Point: src/commands/start.ts:450 (parseInput)
Flow:
- Line 453-471: empty/description short-circuits
- Line 474-482: explicit PR format (
pr/123,PR-123) - Line 487: calls
matchIssueIdentifier()— URL parser slots in BEFORE this line - Line 489-508: project-key path →
issueTracker.detectInputType(input, repo) - Line 511-558: numeric path →
issueTracker.detectInputType()if supports PRs, else GitHubService fallback for PR + tracker for issue - Line 561-565: branch-name fallback
Cross-repo policy enforcement for il start lives in validateInput() at src/commands/start.ts:571 — the URL-derived repo must be compared against the configured-remote repo (already loaded at line 161 via getConfiguredRepoFromSettings()) and rejected if different (per spec).
repo parameter is already plumbed through parseInput → detectInputType (line 491, 519, 540). Implementation just needs to populate it from the URL parse instead of leaving it undefined.
Affected Area 3: il plan decomposition path
Entry Point: src/commands/plan.ts:204 (identifierMatch = prompt ? matchIssueIdentifier(prompt) : ...)
Flow:
- Line 204-205:
matchIssueIdentifier()shape-only check — URL parser slots in BEFORE this - Line 219:
IssueTrackerFactory.create(settings) - Line 224:
issueTracker.detectInputType(prompt)— NO repo argument today - Line 228:
issueTracker.fetchIssue(detection.identifier)— NO repo argument today - Line 236:
IssueManagementProviderFactory.create(provider, settings)— provider-only, no repo - Line 253:
mcpProvider.getIssue({ number, includeComments: true })— relies on cwd remote - Line 304-313:
mcpProvider.getChildIssues(),mcpProvider.getDependencies()— same fallback - Line 715, 743: auto-swarm calls
StartCommand.execute({ identifier: String(epicIssueNumber), options })—identifieris the bare number; cross-repo would need the URL form preserved orrepoplumbed into options
Cross-repo plumbing gap: cross-repo GitHub URL support requires repo (owner/repo string) reaching all of (3), (4), (5)/(6)/(7), and the auto-swarm child loom creation. The current code does NOT support this; this is the largest cross-cutting impact of the spec.
Affected Area 4: Provider-specific identifier detection
Files:
src/lib/GitHubService.ts:54-80—detectInputType(): matches^#?(\d+)$onlysrc/lib/LinearService.ts:61-92—detectInputType(): matches/^([A-Z]{2,}-\d+)$/isrc/lib/providers/jira/JiraIssueTracker.ts:98-121—detectInputType(): matches/^([A-Z][A-Z0-9]+)-(\d+)$/i
None of these accept URL strings. The URL parser must extract the identifier before these are called. They do not need modification.
Affected Area 5: IssueTracker.normalizeIdentifier()
Files:
src/lib/GitHubService.ts:392—String(identifier)(numeric)src/lib/LinearService.ts:221—.toUpperCase()src/lib/providers/jira/JiraIssueTracker.ts:90—.toUpperCase()
URL-extracted identifiers from Linear/Jira URLs must be passed through normalizeIdentifier() before any downstream comparison/lookup (CLAUDE.md identifier-comparisons rule).
Affected Area 6: Repo URL precedent
Reference: src/commands/contribute.ts:73-108 (parseGitHubRepoUrl()) and src/utils/remote.ts:60-89 (extractOwnerRepoFromUrl())
Both already handle GitHub HTTPS / SSH / shorthand for repo URLs (not issue/PR URLs). The new tracker-URL parser should not subsume these but can borrow their hostname-pattern style.
Similar Patterns Found
src/utils/jira.ts:20,:69— Jira issue URL construction ({host}/browse/{key}); use as the inverse pattern for parsing.src/utils/linear.ts:51— Linear issue URL construction (linear.app/issue/{identifier}); same.src/mcp/JiraIssueManagementProvider.ts:264, 281, 359, 398, 409, 464— Jira browse URL construction sites.src/lib/LoomManager.ts:676— synthesizes a GitHub issue URL from a PR URL by replacing/pull/Nwith/issues/M— confirms the existing in-tree assumption that GitHub PR URL ↔ GitHub issue URL share the/owner/repo/<type>/Nshape.
Historical Context (regression scope)
Issue #1009 is a feature-add, not a regression. Recent commit 8873e1a (Apr 28 2026) added cross-repo guidance to plan-prompt.txt but no code-level cross-repo affordance — so cross-repo URL support in il plan is net-new code, not a fix for a regression.
Architectural Flow Analysis
Data Flow: parsed URL → resolved identifier (il start)
User input: "https://github.com/iloom-ai/iloom-cli/issues/1009"
→ src/commands/start.ts:172 StartCommand.parseInput(input.identifier, repo, options)
→ [NEW] parseTrackerUrl(input.identifier)
returns { provider: 'github', kind: 'issue', identifier: '1009', repo: 'iloom-ai/iloom-cli' }
→ cross-repo validation against configured repo (start.ts:161)
→ IssueTracker.normalizeIdentifier('1009') ← MANDATORY pass-through
→ existing parseInput flow with identifier='1009', repo='iloom-ai/iloom-cli'
→ IssueTracker.detectInputType(identifier, repo) ← repo already plumbed
→ IssueTracker.fetchIssue(identifier, repo) ← repo already plumbed
→ src/commands/start.ts:357 loomManager.createIloom({ ... })
→ src/commands/start.ts:399 TelemetryService.track('loom.created', {
..., identifier_source: 'url', tracker_type: 'github' ← NEW props
})
Data Flow: parsed URL → resolved identifier (il plan)
User input: "https://github.com/iloom-ai/other-repo/issues/42" (cross-repo)
→ src/commands/plan.ts:204 identifierMatch
→ [NEW] parseTrackerUrl(prompt)
returns { provider: 'github', kind: 'issue', identifier: '42', repo: 'iloom-ai/other-repo' }
→ reject if kind === 'pr' (per spec: PR URL invalid for plan)
→ provider-mismatch check vs IssueTrackerFactory.getProviderName(settings)
→ cross-repo: no rejection (per spec, plan accepts cross-repo)
→ IssueTracker.normalizeIdentifier(identifier)
→ src/commands/plan.ts:224 issueTracker.detectInputType(identifier, repo) ← repo NEW
→ src/commands/plan.ts:228 issueTracker.fetchIssue(detection.identifier, repo) ← repo NEW
→ src/commands/plan.ts:236 IssueManagementProviderFactory.create(provider, settings, repo?) ← repo NEW (gap)
→ src/commands/plan.ts:253 mcpProvider.getIssue({ number, includeComments: true, repo? }) ← repo NEW (gap)
→ src/commands/plan.ts:304 mcpProvider.getChildIssues({ number, repo? }) ← repo NEW (gap)
→ src/commands/plan.ts:312 mcpProvider.getDependencies({ number, direction, repo? }) ← repo NEW (gap)
→ [auto-swarm path] src/commands/plan.ts:715/743 StartCommand.execute({ identifier, options: { ..., repo? } }) ← repo NEW
Critical Implementation Note: Cross-repo URL support in il plan is a cross-cutting change touching at least 5 method signatures (detectInputType, fetchIssue, IssueManagementProviderFactory.create, IssueProvider.getIssue/getChildIssues/getDependencies, StartCommand.execute). The MCP IssueProvider interface (src/mcp/types.ts) and the GitHub MCP provider (src/mcp/GitHubIssueManagementProvider.ts) may not currently accept a repo argument on getIssue/getChildIssues/getDependencies — implementation needs to verify and potentially extend those signatures (out-of-scope to confirm during analysis but flagged here).
Affected Files
src/utils/IdentifierParser.ts:30—matchIssueIdentifier()is the existing shape detector. Add a new exportedparseTrackerUrl()here (or a siblingsrc/utils/UrlParser.ts).src/commands/start.ts:450-566—parseInput()— insert URL detection before line 487 (matchIssueIdentifier()); enforceil startcross-repo rejection invalidateInput()at:571.src/commands/start.ts:172— call site receives URL-input viainput.identifier.src/commands/start.ts:399-409— telemetry block needs newidentifier_sourceproperty.src/commands/plan.ts:204—identifierMatchblock — insert URL detection beforematchIssueIdentifier(); reject PR URLs (per spec).src/commands/plan.ts:224, 228, 236, 253, 304, 312— repo plumbing for cross-repo URL support.src/commands/plan.ts:715, 743— auto-swarmStartCommand.execute()calls; cross-repo identifier should preserve URL or passrepoin options.src/types/telemetry.ts:25-32—LoomCreatedPropertiesaddidentifier_source: 'url' | 'identifier'(note: existingtrackerfield already covers spec'stracker_typeask — flag this overlap).src/utils/IdentifierParser.test.ts— addparseTrackerUrl()unit tests covering all shapes, edge cases.src/commands/start.test.ts:203(input parsing block) — extend with URL-paste cases for each provider, malformed URLs, cross-repo rejection.src/commands/plan.test.ts— add a newdescribe('URL paste support')block covering cross-repo accept and PR-URL rejection.docs/iloom-commands.md— examples per project documentation requirement (CLAUDE.md).
Integration Points
IssueTracker.normalizeIdentifier()— mandatory pass-through after URL extraction; case-sensitivity rule (CLAUDE.md).IssueTrackerFactory.getProviderName(settings)(src/lib/IssueTrackerFactory.ts:73) — used to detect provider mismatch.getConfiguredRepoFromSettings(settings)(src/utils/remote.ts:115) — used instart.ts:161to load the configured repo for cross-repo comparison.IssueManagementProviderFactory(src/mcp/IssueManagementProviderFactory.ts) andIssueProvider(src/mcp/types.ts) — MCP provider seam; may needrepoparameter extension foril plancross-repo.
Medium Severity Risks
- Auth credential URL log leakage:
https://user:[email protected]/...URLs must never appear inlogger.debug()/logger.info()output. UseURLAPI to stripusername/passwordbefore any log call. The codebase's existing logger does not auto-redact URL credentials. - MCP provider signatures:
IssueProvider.getIssue/getChildIssues/getDependenciesmay not currently acceptrepo. If extending these is out of scope, cross-repoil planwill read MCP context from cwd remote and silently mismatch — better to either extend or explicitly skip MCP calls whenrepo !== cwd-repo(with a logged warning). - Slug containing dashes that look like project keys:
linear.app/team/issue/WEB-2423/fix-bug-in-PROJ-99— the URL parser must extractWEB-2423from the path segment immediately after/issue/, NOT regex-search the whole URL forKEY-NUMpatterns (would match the slug). - Telemetry overlap: spec asks for
tracker_typeenum but existingLoomCreatedProperties.tracker(src/types/telemetry.ts:27) already provides this. Implementation should NOT duplicate; just addidentifier_source. il planwith noplan.startedevent today: URL-vs-identifier telemetry forplanhas no existing event to extend. Either extendepic.planned(plan.ts:772) — but only fires for decomposition mode — or add a newplan.startedevent. Spec says "the equivalent plan event" but none exists.
Related Context
- React Contexts: N/A (CLI, not web).
- Test config (
vitest.config.ts):mockReset: true,clearMocks: true,restoreMocks: true— do not add redundantafterEachcleanup in new tests (per project CLAUDE.md). - Identifier-case rule: project CLAUDE.md "Identifier comparisons are case-sensitive risks" — non-negotiable for this feature.
Implementation Plan for Issue #1009SummaryAdd a new Questions and Key Decisions
High-Level Execution Phases
Quick Stats
Potential Risks (HIGH/CRITICAL only)
Complete Implementation Guide (click to expand for step-by-step details)Shared Contract (defined in Step 1, consumed by Steps 2/3)// src/utils/TrackerUrlParser.ts
export type TrackerUrlProvider = 'github' | 'linear' | 'jira'
export type TrackerUrlKind = 'issue' | 'pr'
export interface TrackerUrlParseResult {
provider: TrackerUrlProvider
kind: TrackerUrlKind
/** Raw extracted identifier (Linear/Jira pre-normalization, GitHub numeric) */
identifier: string
/** owner/repo for GitHub URLs only */
repo?: string
/** URL host — used for Jira host-mismatch validation */
host?: string
}
/**
* Returns null if input is not a tracker URL.
* Throws TrackerUrlError ONLY for "looks like a URL but malformed" cases
* (e.g., known host with missing identifier segment).
*/
export function parseTrackerUrl(input: string): TrackerUrlParseResult | null
export class TrackerUrlError extends Error {
constructor(message: string, public readonly host?: string) { super(message) }
}Behavior contract consumed by Steps 2 and 3:
Files to Modify1.
|
Implementation CompleteSummary
Changes Made
Validation Results
Detailed Changes by File (click to expand)src/utils/TrackerUrlParser.ts (new)Public contract: src/utils/TrackerUrlParser.test.ts (new)46 Vitest tests across all four URL shapes plus the slug-disambiguation case (Linear slug containing src/types/telemetry.ts
src/commands/start.ts
src/commands/start.test.tsNew src/commands/plan.ts
src/commands/plan.test.tsNew src/lib/LinearService.ts, src/lib/providers/jira/JiraIssueTracker.tsAccept and ignore the new docs/iloom-commands.md, src/commands/CLAUDE.mdDocumentation for supported URL shapes, policy distinctions between |
Adds a TrackerUrlParser helper that converts pasted GitHub issue/PR,
Linear, and Jira URLs (cloud + self-hosted) into the canonical
identifier expected by il start and il plan. Identifiers are
normalized through IssueTracker.normalizeIdentifier() to keep
case-sensitive comparisons safe.
Policy:
- il start: rejects cross-repo URLs and provider mismatches, with a
carve-out for GitHub PR URLs (matches the existing PR fallback);
validates Jira host against the configured one.
- il plan: rejects PR URLs and provider mismatches; accepts cross-repo
GitHub URLs but skips MCP getIssue/getChildIssues/getDependencies
with a logger.warn (full cross-repo MCP plumbing is deferred).
Telemetry adds identifier_source ('url' | 'identifier') and
url_provider ('github' | 'linear' | 'jira') as enums only — URLs are
never logged. Includes unit tests for the parser, validator, and
both commands plus docs in docs/iloom-commands.md.
2705b9e to
5878f48
Compare
Fixes #1009
Implement changes to "il plan" and "il start" to support pasting in urls to Github issue + PR, linear, JIRA urls
This PR was created automatically by iloom.