diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0d2f1c5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,45 @@ +# CLAUDE.md + +Working notes for AI agents in this repo (the macOS desktop Scout app). + +## Fixtures must be anonymized — this repo and its two siblings are public + +Scout runs against a real person's vault, so anything lifted from it into a +fixture or an inline test string **must be scrubbed before it lands**. All three +Scout repos — this one, `scout-iOS-app`, and `scout-plugin` — are public. + +- **No real identifiers.** Strip company/product names, real coworker names, real + Linear IDs, GitHub repos, and Slack workspaces/channels. Use the shared + stand-ins so fixtures stay internally consistent: + - People: `Alex` / `Priya` / `Sam`; comment/proposal author `alex` / `Alex`. + - Linear: `PROJ-1234` (use neutral team prefixes like `OPS-`, `DESK-`, `TEAM-` + when you need variety) — never the real team prefixes (`AI-`, `KAI-`, `ST-`, …). + - GitHub: `example-org/`. + - Slack: `acme-co.slack.com/archives/C0123456789/p1700000000000000`. + - Vendors/products: a generic noun ("the demo", "the tracing job"), not the brand. +- **Anonymize content, not structure.** Keep the load-bearing tokens the parser is + tested on — the synthetic `[#TAG]` short-prefixes (`MIRO`, `AI3026`, `RSM`, + `5864M`…), `**bold**`, `_(italic)_`, `[[wikilinks]]`, ` — ` separators, + `` `code` ``. Only swap the words around them. +- **Preserve legitimate attribution** — these are NOT leaks, leave them: the + `pyproject`/`marketplace.json` owner, `LICENSE`, and the project's own + `github.com//…` URLs. + +### `parser-corpus.json` is ONE byte-identical file living in three repos + +`ScoutTests/Fixtures/parser-corpus.json` is byte-identical to the **canonical** +copy in `scout-plugin` and the copy in `scout-iOS-app`, and is checksum-guarded on +both the Swift and Python sides — so you cannot edit just one copy. On any change +(anonymizing counts): + +1. Edit the corpus; keep every `expected` field consistent with the parser rules + (`ParserContractTests` is the judge). +2. Copy it byte-for-byte into the sibling checkouts (cloned alongside this repo): + - `../scout-plugin/engine/tests/fixtures/contract/parser-corpus.json` (canonical) + - `../scout-ios/ScoutMobileTests/Fixtures/parser-corpus.json` +3. Update BOTH checksum guards to the new `shasum -a 256` of the file: + - `canonicalSHA256` in `ScoutTests/ActionItems/ParserContractTests.swift` + - `EXPECTED_SHA256` in `../scout-plugin/engine/tests/unit/test_parser_corpus_checksum.py` +4. Verify all three: this repo's `ParserContractTests` (on `platform=macOS`), + scout-iOS `ParserContractTests`, and plugin + `pytest tests/unit/test_parser_contract.py tests/unit/test_parser_corpus_checksum.py`. diff --git a/README.md b/README.md index e416fbe..ec2b76c 100644 --- a/README.md +++ b/README.md @@ -133,4 +133,4 @@ Scout is local-first and collects no data of its own — the macOS app only read - **Terms of Use** — https://raven-scout.github.io/scout-plugin/terms.html - **[Security Policy](https://github.com/Raven-Scout/.github/blob/main/SECURITY.md)** · **[Code of Conduct](https://github.com/Raven-Scout/.github/blob/main/CODE_OF_CONDUCT.md)** -Scout is an independent project, not affiliated with Anthropic, Microsoft, Keboola, or any other company. +Scout is an independent project, not affiliated with Anthropic, Microsoft, or any other company. diff --git a/Scout/ActionItems/ActionItemsParser.swift b/Scout/ActionItems/ActionItemsParser.swift index 3d6fc4c..0899b26 100644 --- a/Scout/ActionItems/ActionItemsParser.swift +++ b/Scout/ActionItems/ActionItemsParser.swift @@ -439,7 +439,7 @@ extension ActionItemsParser { // Sub-bullet comment line attached to the last task: scoutctl // writes ` - : `. Distinct from the blockquote // form above. Match must run BEFORE the bare-bullet `bulletRe` - // path so ` - jordan: hello` becomes a comment rather than a + // path so ` - alex: hello` becomes a comment rather than a // sub-task body. if inSection, let last = currentTasks.last, diff --git a/Scout/ActionItems/Views/SnoozePopoverView.swift b/Scout/ActionItems/Views/SnoozePopoverView.swift index 3e96710..bfd1b27 100644 --- a/Scout/ActionItems/Views/SnoozePopoverView.swift +++ b/Scout/ActionItems/Views/SnoozePopoverView.swift @@ -121,7 +121,7 @@ struct SnoozePopoverView: View { .buttonStyle(.plainHit) .foregroundStyle(.secondary) // Matches the unstyled "Send" button in - // CommentComposerView — Adam's UI pass (eb88094) targeted + // CommentComposerView — Alex's UI pass (eb88094) targeted // toggles/segmented controls, leaving primary-action buttons // on the system style. Stay consistent with that. Button("Snooze") { commitCustom() } diff --git a/Scout/Proposals/Models/ProposalStatus.swift b/Scout/Proposals/Models/ProposalStatus.swift index 94c2958..6219fed 100644 --- a/Scout/Proposals/Models/ProposalStatus.swift +++ b/Scout/Proposals/Models/ProposalStatus.swift @@ -9,7 +9,7 @@ import Foundation /// `Applied`. The app only ever writes `Approved` / `Rejected` — applying the /// underlying SKILL.md change stays with the dreaming run. nonisolated enum ProposalStatus: Equatable, Sendable { - /// `Proposed (awaiting Adam approval)` — needs an explicit decision. + /// `Proposed (awaiting Alex approval)` — needs an explicit decision. case proposed /// `Pending (auto-apply after )` — opt-out; auto-applies unless /// rejected. `autoApplyDate` is the ISO date when present. diff --git a/Scout/Services/SessionLogService.swift b/Scout/Services/SessionLogService.swift index 0775d71..9ef5073 100644 --- a/Scout/Services/SessionLogService.swift +++ b/Scout/Services/SessionLogService.swift @@ -593,7 +593,7 @@ extension RunType { /// 12h dreaming) were way too loose and produced multi-hour false /// "running" badges in the Now strip and Sessions list. CC-1/CC-5. /// - /// Observed P95s (May 2026 on Jordan's box): + /// Observed P95s (May 2026 on Alex's box): /// briefing: ~6 min · consolidation: ~3 min /// dreaming: ~25 min · research: ~40 min /// Cutoffs leave ~3× headroom so a legitimately long run still resolves diff --git a/ScoutTests/ActionItems/ActionItemsParserTests.swift b/ScoutTests/ActionItems/ActionItemsParserTests.swift index cec024d..5d3e636 100644 --- a/ScoutTests/ActionItems/ActionItemsParserTests.swift +++ b/ScoutTests/ActionItems/ActionItemsParserTests.swift @@ -173,7 +173,7 @@ struct ActionItemsParserTests { let withComment = Self.stableDoc.replacingOccurrences( of: "- [ ] **First task** — needs attention", - with: "- [ ] **First task** — needs attention\n - jordan: looking into it" + with: "- [ ] **First task** — needs attention\n - alex: looking into it" ) let after = try ActionItemsParser.parse( text: withComment, sourceURL: Self.stableURL, sourceBytes: withComment.utf8.count) diff --git a/ScoutTests/ActionItems/ActionItemsWriterTests.swift b/ScoutTests/ActionItems/ActionItemsWriterTests.swift index d13df79..ce04cac 100644 --- a/ScoutTests/ActionItems/ActionItemsWriterTests.swift +++ b/ScoutTests/ActionItems/ActionItemsWriterTests.swift @@ -20,7 +20,7 @@ struct ActionItemsWriterTests { subject: "Engage on PROJ-123", shortPrefix: nil, text: "Paging reviewer.", - author: "jordan" + author: "alex" ), displayedDate: date) let call = try #require(await recorder.calls.first) @@ -29,7 +29,7 @@ struct ActionItemsWriterTests { "action-items", "add-comment", "/tmp/Scout/action-items/action-items-2026-04-20.md", "--subject", "Engage on PROJ-123", - "--comment", "jordan: Paging reviewer." + "--comment", "alex: Paging reviewer." ]) } @@ -256,13 +256,13 @@ struct ActionItemsWriterTests { } @Test func withShortPrefixReplacesPrefixPreservingPayload() { - let op = WriteOp.addComment(subject: "Subj", shortPrefix: nil, text: "hi", author: "jordan") + let op = WriteOp.addComment(subject: "Subj", shortPrefix: nil, text: "hi", author: "alex") let promoted = op.withShortPrefix("AB12") #expect(promoted.shortPrefix == "AB12") #expect(promoted.subject == "Subj") if case .addComment(_, _, let text, let author) = promoted { #expect(text == "hi") - #expect(author == "jordan") + #expect(author == "alex") } else { Issue.record("case changed unexpectedly") } diff --git a/ScoutTests/ActionItems/ClaudeLauncherPromptTests.swift b/ScoutTests/ActionItems/ClaudeLauncherPromptTests.swift index 22bb708..0fee04c 100644 --- a/ScoutTests/ActionItems/ClaudeLauncherPromptTests.swift +++ b/ScoutTests/ActionItems/ClaudeLauncherPromptTests.swift @@ -27,7 +27,7 @@ struct ClaudeLauncherPromptTests { let task = makeTask( plainSubject: "Investigate pager storm", comments: [ - TaskComment(author: "jordan", timestamp: "2026-04-20 10:00 AM ET", + TaskComment(author: "alex", timestamp: "2026-04-20 10:00 AM ET", text: "Saw three alerts in ten minutes."), TaskComment(author: "priya", timestamp: "", text: "Probably related to the queue drain we shipped."), @@ -35,7 +35,7 @@ struct ClaudeLauncherPromptTests { ) let out = ClaudeLauncher.prompt(for: task) #expect(out.contains("Prior comments:")) - #expect(out.contains("- jordan (2026-04-20 10:00 AM ET): Saw three alerts in ten minutes.")) + #expect(out.contains("- alex (2026-04-20 10:00 AM ET): Saw three alerts in ten minutes.")) #expect(out.contains("- priya: Probably related to the queue drain we shipped.")) } diff --git a/ScoutTests/ActionItems/DeepLinkDetectionTests.swift b/ScoutTests/ActionItems/DeepLinkDetectionTests.swift index 664cdff..d37818b 100644 --- a/ScoutTests/ActionItems/DeepLinkDetectionTests.swift +++ b/ScoutTests/ActionItems/DeepLinkDetectionTests.swift @@ -5,17 +5,17 @@ import Foundation @Suite("Deep link detection") struct DeepLinkDetectionTests { @Test func detectsLinearIDsAcrossAllPrefixes() { - let text = "Blocked by [[AI-2879]] and ST-3853; see SUPPORT-15915, LDRS-321, KAI-12, DATA-42." + let text = "Blocked by [[PROJ-2879]] and OPS-3853; see DESK-15915, TEAM-321, PLAT-12, META-42." let links = ActionItemsParser.detectDeepLinks(in: text) let linearIDs = links.compactMap { if case .linear(let id) = $0 { return id } else { return nil } } - #expect(linearIDs == ["AI-2879", "ST-3853", "SUPPORT-15915", "LDRS-321", "KAI-12", "DATA-42"]) + #expect(linearIDs == ["PROJ-2879", "OPS-3853", "DESK-15915", "TEAM-321", "PLAT-12", "META-42"]) } @Test func dedupesRepeatedLinearID() { - let text = "[[AI-2619]] comment; [[AI-2619]] again; AI-2619 third mention." + let text = "[[PROJ-2619]] comment; [[PROJ-2619]] again; PROJ-2619 third mention." let links = ActionItemsParser.detectDeepLinks(in: text) #expect(links.count == 1) - if case .linear(let id) = links.first! { #expect(id == "AI-2619") } else { Issue.record("expected linear") } + if case .linear(let id) = links.first! { #expect(id == "PROJ-2619") } else { Issue.record("expected linear") } } @Test func detectsGitHubPR() { @@ -39,14 +39,14 @@ struct DeepLinkDetectionTests { @Test func detectsSchemelessSlackThread() { // Sessions sometimes write the source as a bare host with no scheme, e.g. - // "Slack attachment `keboolaglobal.slack.com/archives/C094NK1JPJB/p1779784363387259`". - let text = "Slack attachment keboolaglobal.slack.com/archives/C094NK1JPJB/p1779784363387259" + // "Slack attachment `acme-co.slack.com/archives/C01234ABCDE/p1700000000123456`". + let text = "Slack attachment acme-co.slack.com/archives/C01234ABCDE/p1700000000123456" let links = ActionItemsParser.detectDeepLinks(in: text) #expect(links.count == 1) if case .slackThread(let url) = links.first! { // The scheme must be normalized to https so the link is openable. #expect(url.scheme == "https") - #expect(url.absoluteString == "https://keboolaglobal.slack.com/archives/C094NK1JPJB/p1779784363387259") + #expect(url.absoluteString == "https://acme-co.slack.com/archives/C01234ABCDE/p1700000000123456") } else { Issue.record("expected slackThread") } @@ -67,11 +67,11 @@ struct DeepLinkDetectionTests { } @Test func preservesDetectionOrder() { - let text = "[[AI-2879]] then https://github.com/acme-co/api-kit/pull/68 then AI-3007." + let text = "[[PROJ-2879]] then https://github.com/acme-co/api-kit/pull/68 then PROJ-3007." let links = ActionItemsParser.detectDeepLinks(in: text) #expect(links.count == 3) - if case .linear(let a) = links[0] { #expect(a == "AI-2879") } else { Issue.record() } + if case .linear(let a) = links[0] { #expect(a == "PROJ-2879") } else { Issue.record() } if case .githubPR = links[1] {} else { Issue.record() } - if case .linear(let b) = links[2] { #expect(b == "AI-3007") } else { Issue.record() } + if case .linear(let b) = links[2] { #expect(b == "PROJ-3007") } else { Issue.record() } } } diff --git a/ScoutTests/ActionItems/GitHubRefLinkifierTests.swift b/ScoutTests/ActionItems/GitHubRefLinkifierTests.swift index 1890bcb..a8b68b3 100644 --- a/ScoutTests/ActionItems/GitHubRefLinkifierTests.swift +++ b/ScoutTests/ActionItems/GitHubRefLinkifierTests.swift @@ -5,18 +5,18 @@ import Foundation @Suite("GitHub ref linkifier") struct GitHubRefLinkifierTests { @Test func linkifiesQualifiedRef() { - let out = GitHubRefLinkifier.linkify("See keboola/mcp-server#553 for details.") - #expect(out == "See [keboola/mcp-server#553](https://github.com/keboola/mcp-server/issues/553) for details.") + let out = GitHubRefLinkifier.linkify("See example-org/mcp-server#553 for details.") + #expect(out == "See [example-org/mcp-server#553](https://github.com/example-org/mcp-server/issues/553) for details.") } @Test func linkifiesBareRefsWhenSingleRepoInferable() { // The issue #17 example: bare #NNN refs plus a single repo slug mention. let out = GitHubRefLinkifier.linkify( - "Triage mcp-server review requests — #555 (bump GH Actions), #498, #553 in keboola/mcp-server" + "Triage mcp-server review requests — #555 (bump GH Actions), #498, #553 in example-org/mcp-server" ) - #expect(out.contains("[#555](https://github.com/keboola/mcp-server/issues/555)")) - #expect(out.contains("[#498](https://github.com/keboola/mcp-server/issues/498)")) - #expect(out.contains("[#553](https://github.com/keboola/mcp-server/issues/553)")) + #expect(out.contains("[#555](https://github.com/example-org/mcp-server/issues/555)")) + #expect(out.contains("[#498](https://github.com/example-org/mcp-server/issues/498)")) + #expect(out.contains("[#553](https://github.com/example-org/mcp-server/issues/553)")) } @Test func leavesBareRefsPlainWhenNoRepo() { @@ -69,10 +69,10 @@ struct GitHubRefLinkifierTests { } @Test func doesNotTouchWikilink() { - let input = "Context [[issue-tracker]] for keboola/mcp-server#1." + let input = "Context [[issue-tracker]] for example-org/mcp-server#1." let out = GitHubRefLinkifier.linkify(input) #expect(out.contains("[[issue-tracker]]")) - #expect(out.contains("[keboola/mcp-server#1](https://github.com/keboola/mcp-server/issues/1)")) + #expect(out.contains("[example-org/mcp-server#1](https://github.com/example-org/mcp-server/issues/1)")) } @Test func doesNotTouchInlineCode() { diff --git a/ScoutTests/ActionItems/MatchableSubjectTests.swift b/ScoutTests/ActionItems/MatchableSubjectTests.swift index d8e62f4..9d688cd 100644 --- a/ScoutTests/ActionItems/MatchableSubjectTests.swift +++ b/ScoutTests/ActionItems/MatchableSubjectTests.swift @@ -10,10 +10,10 @@ struct MatchableSubjectTests { // italic body and scoutctl fails to substring-match it. Bold-only // matches reliably. let task = make( - subject: "**🔥 🆕 Update kai-pricing-calculator-app with per-client conversion levers + margin maximizer** _(net-new from Kai's pricing meeting 6-7 AM ET; Jordan already iterating during meeting)_", - plainSubject: "🔥 🆕 Update kai-pricing-calculator-app with per-client conversion levers + margin maximizer _(net-new from Kai's pricing meeting 6-7 AM ET; Jordan already iterating during meeting)_" + subject: "**🔥 🆕 Update the pricing calculator app with per-client conversion levers + margin maximizer** _(net-new from the pricing review 6-7 AM ET; Alex already iterating during meeting)_", + plainSubject: "🔥 🆕 Update the pricing calculator app with per-client conversion levers + margin maximizer _(net-new from the pricing review 6-7 AM ET; Alex already iterating during meeting)_" ) - #expect(task.matchableSubject == "🔥 🆕 Update kai-pricing-calculator-app with per-client conversion levers + margin maximizer") + #expect(task.matchableSubject == "🔥 🆕 Update the pricing calculator app with per-client conversion levers + margin maximizer") } @Test func preservesInnerMarkdownInBoldPortion() { @@ -33,10 +33,10 @@ struct MatchableSubjectTests { // so we have to keep it too — stripping it would yield a substring // that doesn't exist in the title. let task = make( - subject: "**🔥 🆕 Close [PR #5526 (AI-3079 sandboxId metadata)](https://github.com/keboola/ui/pull/5526) with re-implement-on-OTel note** _(promoted 7:04 AM ET 5/20…)_", - plainSubject: "🔥 🆕 Close PR #5526 (AI-3079 sandboxId metadata) with re-implement-on-OTel note _(promoted 7:04 AM ET 5/20…)_" + subject: "**🔥 🆕 Close [PR #5526 (PROJ-3079 sandboxId metadata)](https://github.com/example-org/ui/pull/5526) with re-implement-on-OTel note** _(promoted 7:04 AM ET 5/20…)_", + plainSubject: "🔥 🆕 Close PR #5526 (PROJ-3079 sandboxId metadata) with re-implement-on-OTel note _(promoted 7:04 AM ET 5/20…)_" ) - #expect(task.matchableSubject == "🔥 🆕 Close [PR #5526 (AI-3079 sandboxId metadata)](https://github.com/keboola/ui/pull/5526) with re-implement-on-OTel note") + #expect(task.matchableSubject == "🔥 🆕 Close [PR #5526 (PROJ-3079 sandboxId metadata)](https://github.com/example-org/ui/pull/5526) with re-implement-on-OTel note") } @Test func stripsPriorityEmojiInsideBold() { @@ -47,10 +47,10 @@ struct MatchableSubjectTests { // urgent task with a markdown link in the title. Mirror scoutctl's // cleanup here. let task = make( - subject: "[#OIDC-MERGE] **🔴 Merge [mcp-server PR #546](https://github.com/keboola/mcp-server/pull/546) (AI-3295)** _(APPROVED…)_", - plainSubject: "🔴 Merge mcp-server PR #546 (AI-3295) _(APPROVED…)_" + subject: "[#OIDC-MERGE] **🔴 Merge [mcp-server PR #546](https://github.com/example-org/mcp-server/pull/546) (PROJ-3295)** _(APPROVED…)_", + plainSubject: "🔴 Merge mcp-server PR #546 (PROJ-3295) _(APPROVED…)_" ) - #expect(task.matchableSubject == "Merge [mcp-server PR #546](https://github.com/keboola/mcp-server/pull/546) (AI-3295)") + #expect(task.matchableSubject == "Merge [mcp-server PR #546](https://github.com/example-org/mcp-server/pull/546) (PROJ-3295)") } @Test func stripsLeadingStatusEmoji() { @@ -58,8 +58,8 @@ struct MatchableSubjectTests { // cleaned title. If Scout extracts a bold portion that begins with // one, drop it so the substring matches. let task = make( - subject: "**✅ DONE 12:01 PM ET** — Jordan merged himself.", - plainSubject: "✅ DONE 12:01 PM ET — Jordan merged himself." + subject: "**✅ DONE 12:01 PM ET** — Alex merged himself.", + plainSubject: "✅ DONE 12:01 PM ET — Alex merged himself." ) #expect(task.matchableSubject == "DONE 12:01 PM ET") } @@ -67,8 +67,8 @@ struct MatchableSubjectTests { @Test func unwrapsStrikethrough() { // STRIKETHROUGH `~~foo~~` is reduced to `foo` in scoutctl's title. let task = make( - subject: "**~~Send report~~** _(superseded by Jordan)_", - plainSubject: "Send report _(superseded by Jordan)_" + subject: "**~~Send report~~** _(superseded by Alex)_", + plainSubject: "Send report _(superseded by Alex)_" ) #expect(task.matchableSubject == "Send report") } diff --git a/ScoutTests/ActionItems/ParserContractTests.swift b/ScoutTests/ActionItems/ParserContractTests.swift index a1ac5f0..04dca77 100644 --- a/ScoutTests/ActionItems/ParserContractTests.swift +++ b/ScoutTests/ActionItems/ParserContractTests.swift @@ -13,7 +13,7 @@ struct ParserContractTests { /// On an intentional corpus change: re-copy the canonical corpus into both /// repos, then update this digest to the output of /// `shasum -a 256 ScoutTests/Fixtures/parser-corpus.json`. - static let canonicalSHA256 = "4ebe8ae34a5b945bb5165ebd6bb6b818986c2cafec0ad30910bfd3fcb66e21a1" + static let canonicalSHA256 = "745dc8f886c52cd3a2273a2f5fd76934782492b159a6f63ab0d9e6978114511f" struct Corpus: Decodable { let entries: [Entry] diff --git a/ScoutTests/ActionItems/ShortPrefixTests.swift b/ScoutTests/ActionItems/ShortPrefixTests.swift index e3db860..154000a 100644 --- a/ScoutTests/ActionItems/ShortPrefixTests.swift +++ b/ScoutTests/ActionItems/ShortPrefixTests.swift @@ -100,7 +100,7 @@ struct ActionItemsParseWithPrefixTests { ## 🔴 Urgent - [ ] [#ABCD] **A task** body - > jordan: a blockquote comment + > alex: a blockquote comment """ let doc = try ActionItemsParser.parse( text: md, diff --git a/ScoutTests/ActionItems/TaskBodyParserTests.swift b/ScoutTests/ActionItems/TaskBodyParserTests.swift index 2b01a32..5e96ecb 100644 --- a/ScoutTests/ActionItems/TaskBodyParserTests.swift +++ b/ScoutTests/ActionItems/TaskBodyParserTests.swift @@ -56,11 +56,11 @@ struct TaskBodyParserTests { } @Test func pullsTrailingWikilinkCluster() { - let body = "The point. Tooling: [[some-skill]] skill. [[ai-costs]] [[kai-backend]] [[people/martin-vasko]]" + let body = "The point. Tooling: [[some-skill]] skill. [[cost-tracking]] [[backend-service]] [[people/sam-rivera]]" let blocks = TaskBodyParser.blocks(from: body) #expect(blocks == [ .paragraph(label: nil, text: "The point. Tooling: [[some-skill]] skill."), - .links(["ai-costs", "kai-backend", "people/martin-vasko"]), + .links(["cost-tracking", "backend-service", "people/sam-rivera"]), ]) } @@ -71,16 +71,16 @@ struct TaskBodyParserTests { } @Test func realisticBodyDecomposesIntoBlocks() { - let body = "**Overnight progress:** ten sessions audited the pipelines. **Why:** [[AI-2619]] ruled out drift, leaving a residual. **5-step checklist:** (1) split LS traces; (2) redo per-stack; (3) verify child runs; (4) grep prod Helm; (5) carve out non-Kai. **Caveat:** OTel never emits cost. [[ai-costs]] [[kai-backend]] [[evals]]" + let body = "**Overnight progress:** ten sessions audited the pipelines. **Why:** [[PROJ-2619]] ruled out drift, leaving a residual. **5-step checklist:** (1) split trace spans; (2) redo per-stack; (3) verify child runs; (4) grep prod Helm; (5) carve out non-core. **Caveat:** OTel never emits cost. [[cost-tracking]] [[backend-service]] [[evals]]" let blocks = TaskBodyParser.blocks(from: body) #expect(blocks.count == 5) guard case .paragraph(label: "Overnight progress", _) = blocks[0] else { Issue.record("0"); return } guard case .paragraph(label: "Why", _) = blocks[1] else { Issue.record("1"); return } guard case .steps(label: "5-step checklist", let items) = blocks[2] else { Issue.record("2"); return } #expect(items.count == 5) - #expect(items[0] == "split LS traces") + #expect(items[0] == "split trace spans") guard case .paragraph(label: "Caveat", _) = blocks[3] else { Issue.record("3"); return } guard case .links(let targets) = blocks[4] else { Issue.record("4"); return } - #expect(targets == ["ai-costs", "kai-backend", "evals"]) + #expect(targets == ["cost-tracking", "backend-service", "evals"]) } } diff --git a/ScoutTests/ActionItems/TaskChipTests.swift b/ScoutTests/ActionItems/TaskChipTests.swift index bc7fe75..1d22464 100644 --- a/ScoutTests/ActionItems/TaskChipTests.swift +++ b/ScoutTests/ActionItems/TaskChipTests.swift @@ -26,8 +26,8 @@ struct TaskChipTests { } @Test func surfacesRepoOnlyWhenSingleRepo() { - let same = TaskChip.chips(for: task(links: [pr("keboola/mcp-server", 1), pr("keboola/mcp-server", 2)])) - #expect(same.contains { $0.label == "keboola/mcp-server" }) + let same = TaskChip.chips(for: task(links: [pr("example-org/mcp-server", 1), pr("example-org/mcp-server", 2)])) + #expect(same.contains { $0.label == "example-org/mcp-server" }) let mixed = TaskChip.chips(for: task(links: [pr("a/b", 1), pr("c/d", 2)])) #expect(!mixed.contains { $0.glyph == .github && $0.label.contains("/") }) @@ -35,7 +35,7 @@ struct TaskChipTests { @Test func linearAndSlackChips() { let chips = TaskChip.chips(for: task(links: [ - .linear(id: "AI-1"), + .linear(id: "PROJ-1"), .slackThread(URL(string: "https://x.slack.com/archives/C/p1")!), ])) #expect(chips.contains { $0.glyph == .linear && $0.label == "Linear" }) @@ -51,7 +51,7 @@ struct TaskChipTests { let chips = TaskChip.chips( for: task(links: [ .slackThread(URL(string: "https://x.slack.com/archives/C/p1")!), - .linear(id: "AI-1"), + .linear(id: "PROJ-1"), pr("a/b", 1), ]), carriedLabel: "Jun 2" @@ -62,15 +62,15 @@ struct TaskChipTests { } @Test func prChipCarriesPRUrls() { - let chips = TaskChip.chips(for: task(links: [pr("keboola/crm", 925)])) + let chips = TaskChip.chips(for: task(links: [pr("example-org/crm", 925)])) let prChip = chips.first { $0.label == "1 PR" } - #expect(prChip?.links.map(\.url) == [URL(string: "https://github.com/keboola/crm/pull/925")!]) + #expect(prChip?.links.map(\.url) == [URL(string: "https://github.com/example-org/crm/pull/925")!]) } @Test func repoChipOpensRepoHomepage() { - let chips = TaskChip.chips(for: task(links: [pr("keboola/crm", 925)])) - let repoChip = chips.first { $0.label == "keboola/crm" } - #expect(repoChip?.links.map(\.url) == [URL(string: "https://github.com/keboola/crm")!]) + let chips = TaskChip.chips(for: task(links: [pr("example-org/crm", 925)])) + let repoChip = chips.first { $0.label == "example-org/crm" } + #expect(repoChip?.links.map(\.url) == [URL(string: "https://github.com/example-org/crm")!]) } @Test func multiPRChipListsEachPR() { diff --git a/ScoutTests/Fixtures/parser-corpus.json b/ScoutTests/Fixtures/parser-corpus.json index f9ee19c..ed81883 100644 --- a/ScoutTests/Fixtures/parser-corpus.json +++ b/ScoutTests/Fixtures/parser-corpus.json @@ -3,22 +3,22 @@ "entries": [ { "name": "prefixed-emoji-bold", - "line": "- [ ] [#AB30] **🔴 Validate kai-agent LangSmith tracing — [[AI-3026]]** _(carries 6/2→6/4)_ — overnight progress", + "line": "- [ ] [#AB30] **🔴 Validate the tracing pipeline — [[PROJ-3026]]** _(carries 6/2→6/4)_ — overnight progress", "expected": { "short_prefix": "AB30", - "subject": "**🔴 Validate kai-agent LangSmith tracing — [[AI-3026]]** _(carries 6/2→6/4)_", - "plain_subject": "🔴 Validate kai-agent LangSmith tracing — AI-3026 _(carries 6/2→6/4)_", + "subject": "**🔴 Validate the tracing pipeline — [[PROJ-3026]]** _(carries 6/2→6/4)_", + "plain_subject": "🔴 Validate the tracing pipeline — PROJ-3026 _(carries 6/2→6/4)_", "body": "overnight progress" }, - "_note": "Prefix changed from draft [#AI30] to [#AB30]: 'AI30' contains 'I', which is NOT in the Crockford alphabet (I/L/O/U excluded), so it could never parse as a short_prefix. subject retains '**' bold markers per the Swift reference contract; the inner ' — ' before [[AI-3026]] is inside the bold span so the split lands at the LAST top-level ' — ' (before 'overnight progress'); the italic _(carries ...)_ is not a separator so it stays in subject. Python render.py xfails this case: it does not strip the [#AB30] prefix from subject/plain_subject (no short_prefix extraction in render.py) — see test _note." + "_note": "Prefix changed from draft [#AI30] to [#AB30]: 'AI30' contains 'I', which is NOT in the Crockford alphabet (I/L/O/U excluded), so it could never parse as a short_prefix. subject retains '**' bold markers per the Swift reference contract; the inner ' — ' before [[PROJ-3026]] is inside the bold span so the split lands at the LAST top-level ' — ' (before 'overnight progress'); the italic _(carries ...)_ is not a separator so it stays in subject. Python render.py xfails this case: it does not strip the [#AB30] prefix from subject/plain_subject (no short_prefix extraction in render.py) — see test _note." }, { "name": "unprefixed-the-issue10-line", - "line": "- [ ] **🔥 🆕 Update kai-pricing-calculator-app with per-client conversion levers** _(net-new from Kai's pricing meeting)_", + "line": "- [ ] **🔥 🆕 Update the pricing calculator app with per-tenant conversion levers** _(net-new from the pricing review)_", "expected": { "short_prefix": null, - "subject": "**🔥 🆕 Update kai-pricing-calculator-app with per-client conversion levers** _(net-new from Kai's pricing meeting)_", - "plain_subject": "🔥 🆕 Update kai-pricing-calculator-app with per-client conversion levers _(net-new from Kai's pricing meeting)_", + "subject": "**🔥 🆕 Update the pricing calculator app with per-tenant conversion levers** _(net-new from the pricing review)_", + "plain_subject": "🔥 🆕 Update the pricing calculator app with per-tenant conversion levers _(net-new from the pricing review)_", "body": "" }, "_note": "No separator outside tokens, so body is empty and the whole line (bold span + trailing italic) is the subject. subject retains '**'; plain_subject strips it. Draft omitted the bold markers from subject (corrected, case a)." @@ -30,85 +30,85 @@ }, { "name": "done-with-github-pr-link", - "line": "- [x] [#PR99] **Merge the fix** — see https://github.com/keboola/keboola_com/pull/301", + "line": "- [x] [#PR99] **Merge the fix** — see https://github.com/example-org/example-repo/pull/301", "expected": { "short_prefix": "PR99", "subject": "**Merge the fix**", "plain_subject": "Merge the fix", - "body": "see https://github.com/keboola/keboola_com/pull/301" + "body": "see https://github.com/example-org/example-repo/pull/301" } }, { "name": "snooze-suffix", - "line": "- [ ] [#SNZ1] **Ping Devin** — nudge — 🛌 Snoozed until 2026-06-10", + "line": "- [ ] [#SNZ1] **Ping Sam** — nudge — 🛌 Snoozed until 2026-06-10", "expected": { "short_prefix": "SNZ1", - "subject": "**Ping Devin**", - "plain_subject": "Ping Devin", + "subject": "**Ping Sam**", + "plain_subject": "Ping Sam", "body": "nudge — 🛌 Snoozed until 2026-06-10" } }, { "name": "carry-in-was-kind", - "line": "- [ ] [#CRY2] **Reschedule Groupon** _(carried in from 2026-06-03, was urgent)_", + "line": "- [ ] [#CRY2] **Reschedule the demo** _(carried in from 2026-06-03, was urgent)_", "expected": { "short_prefix": "CRY2", - "subject": "**Reschedule Groupon** _(carried in from 2026-06-03, was urgent)_", - "plain_subject": "Reschedule Groupon _(carried in from 2026-06-03, was urgent)_", + "subject": "**Reschedule the demo** _(carried in from 2026-06-03, was urgent)_", + "plain_subject": "Reschedule the demo _(carried in from 2026-06-03, was urgent)_", "body": "" }, "_note": "No separator outside tokens (the italic _(...)_ is not a separator), so body is empty and the parenthetical stays in subject. Draft put the parenthetical in body (corrected, case a)." }, { "name": "wikilink-alias-and-code", - "line": "- [ ] [#WK01] **Check [[AI-2619|the cost ruling]]** — run `scoutctl status`", + "line": "- [ ] [#WK01] **Check [[PROJ-2619|the cost ruling]]** — run `scoutctl status`", "expected": { "short_prefix": "WK01", - "subject": "**Check [[AI-2619|the cost ruling]]**", - "plain_subject": "Check AI-2619", + "subject": "**Check [[PROJ-2619|the cost ruling]]**", + "plain_subject": "Check PROJ-2619", "body": "run `scoutctl status`" } }, { "name": "semantic-tag-4char-non-crockford", - "line": "- [ ] [#MIRO] **Miro 1:1 follow-through** — 3 quick sends", + "line": "- [ ] [#MIRO] **Team 1:1 follow-through** — 3 quick sends", "expected": { "short_prefix": "MIRO", - "subject": "**Miro 1:1 follow-through**", - "plain_subject": "Miro 1:1 follow-through", + "subject": "**Team 1:1 follow-through**", + "plain_subject": "Team 1:1 follow-through", "body": "3 quick sends" }, "_note": "4-char tag containing I and O (NOT Crockford). Exercises the widened recognition grammar (#117). subject/plain_subject xfail on Python (render.py prefix-strip #114)." }, { "name": "semantic-tag-6char", - "line": "- [ ] [#AI3026] **Validate kai-agent tracing**", + "line": "- [ ] [#AI3026] **Validate the tracing job**", "expected": { "short_prefix": "AI3026", - "subject": "**Validate kai-agent tracing**", - "plain_subject": "Validate kai-agent tracing", + "subject": "**Validate the tracing job**", + "plain_subject": "Validate the tracing job", "body": "" }, "_note": "6-char tag containing I. Length > the old 4-char limit." }, { "name": "semantic-tag-3char", - "line": "- [ ] [#RSM] Rossum SL tester", + "line": "- [ ] [#RSM] Review SL tester", "expected": { "short_prefix": "RSM", - "subject": "Rossum SL tester", - "plain_subject": "Rossum SL tester", + "subject": "Review SL tester", + "plain_subject": "Review SL tester", "body": "" }, "_note": "3-char tag, no bold, no separator." }, { "name": "semantic-tag-digit-led", - "line": "- [ ] [#5864M] **Merge ui PR** — APPROVED", + "line": "- [ ] [#5864M] **Merge the web PR** — APPROVED", "expected": { "short_prefix": "5864M", - "subject": "**Merge ui PR**", - "plain_subject": "Merge ui PR", + "subject": "**Merge the web PR**", + "plain_subject": "Merge the web PR", "body": "APPROVED" }, "_note": "Digit-led 5-char tag with one trailing letter — accepted (has a letter); pure-digit would be rejected as a GitHub ref." diff --git a/ScoutTests/PerFile/PerFileItemParserTests.swift b/ScoutTests/PerFile/PerFileItemParserTests.swift index d3a5e9d..3d0b8c0 100644 --- a/ScoutTests/PerFile/PerFileItemParserTests.swift +++ b/ScoutTests/PerFile/PerFileItemParserTests.swift @@ -14,7 +14,7 @@ struct PerFileItemParserTests { status: in-progress priority: high date: 2026-06-12 - source: "Jordan Slack DM" + source: "Alex Slack DM" --- # Upgrade the graph system @@ -26,7 +26,7 @@ struct PerFileItemParserTests { #expect(item.status == .inProgress) #expect(item.priority == .high) #expect(item.date == "2026-06-12") - #expect(item.source == "Jordan Slack DM") + #expect(item.source == "Alex Slack DM") #expect(item.area == nil) #expect(item.bodyMarkdown == "Evaluate TinkerPop + Gremlin.") // H1 stripped } diff --git a/ScoutTests/PerFile/PerFileItemWriterTests.swift b/ScoutTests/PerFile/PerFileItemWriterTests.swift index 595b671..627866f 100644 --- a/ScoutTests/PerFile/PerFileItemWriterTests.swift +++ b/ScoutTests/PerFile/PerFileItemWriterTests.swift @@ -14,18 +14,18 @@ struct PerFileItemWriterPureTests { } @Test func renderEmitsQuotedFrontmatterAndStrippableBody() throws { let text = PerFileItemWriter.renderItemFile(title: "Build a config: store", status: .open, - priority: .high, date: "2026-06-19", source: "Jordan DM", area: nil, body: "The body.") + priority: .high, date: "2026-06-19", source: "Alex DM", area: nil, body: "The body.") #expect(text.hasPrefix("---\n")) #expect(text.contains("title: \"Build a config: store\"")) // colon -> quoted #expect(text.contains("status: open")) #expect(text.contains("priority: high")) #expect(text.contains("date: 2026-06-19")) - #expect(text.contains("source: \"Jordan DM\"")) + #expect(text.contains("source: \"Alex DM\"")) #expect(!text.contains("area:")) #expect(text.contains("\n# Build a config: store\n")) // round-trips through the parser let item = try #require(PerFileItemParser.parseFile(contents: text, fileURL: URL(fileURLWithPath: "/tmp/x.md"))) - #expect(item.title == "Build a config: store" && item.status == .open && item.priority == .high && item.source == "Jordan DM") + #expect(item.title == "Build a config: store" && item.status == .open && item.priority == .high && item.source == "Alex DM") } @Test func renderResearchAreaNoSource() { let text = PerFileItemWriter.renderItemFile(title: "T", status: .open, priority: .urgent, @@ -85,10 +85,10 @@ struct PerFileItemWriterE2ETests { let runner = okRunner() let writer = PerFileItemWriter(scoutDirectory: vault, gitService: GitService(repoURL: vault, runner: runner), now: { Self.fixedDate() }) let url = try await writer.addItem(title: "Alpha thing", priority: .high, body: "do alpha", - source: "Jordan DM", area: nil, in: dir, noun: "wishlist item") + source: "Alex DM", area: nil, in: dir, noun: "wishlist item") #expect(url.lastPathComponent == "2026-06-19-alpha-thing.md") let written = try String(contentsOf: url, encoding: .utf8) - #expect(written.contains("status: open") && written.contains("priority: high") && written.contains("source: \"Jordan DM\"")) + #expect(written.contains("status: open") && written.contains("priority: high") && written.contains("source: \"Alex DM\"")) let commit = try #require(runner.calls.last) #expect(commit.arguments.contains("commit")) #expect(commit.arguments.contains("app: add wishlist item Alpha thing")) diff --git a/ScoutTests/Proposals/ProposalStatusTests.swift b/ScoutTests/Proposals/ProposalStatusTests.swift index 89b66e9..bd3fac0 100644 --- a/ScoutTests/Proposals/ProposalStatusTests.swift +++ b/ScoutTests/Proposals/ProposalStatusTests.swift @@ -6,7 +6,7 @@ import Foundation struct ProposalStatusTests { @Test func classifiesLeadingWordCaseInsensitively() { - #expect(ProposalStatus.parse("Proposed (awaiting Adam approval)") == .proposed) + #expect(ProposalStatus.parse("Proposed (awaiting Alex approval)") == .proposed) #expect(ProposalStatus.parse("approved") == .approved) #expect(ProposalStatus.parse("APPROVED (2026-06-14, via Scout app)") == .approved) #expect(ProposalStatus.parse("Rejected") == .rejected) diff --git a/ScoutTests/Proposals/ProposalsParserTests.swift b/ScoutTests/Proposals/ProposalsParserTests.swift index ab9a1ee..e5a0698 100644 --- a/ScoutTests/Proposals/ProposalsParserTests.swift +++ b/ScoutTests/Proposals/ProposalsParserTests.swift @@ -8,7 +8,7 @@ private let proposalFixture = """ --- date: 2026-06-13 title: Add a risk-scoped PR re-resolution step -status: Proposed (awaiting Adam approval) +status: Proposed (awaiting Alex approval) target: SKILL.md parent: [[dreaming-proposals]] --- @@ -49,7 +49,7 @@ struct ProposalsParserTests { let text = "---\nstatus: \(raw)\ntitle: t\n---\nbody" return ProposalsParser.parseFile(contents: text, fileURL: url("2026-01-01-x.md"))!.status } - #expect(status("Proposed (awaiting Adam approval)") == .proposed) + #expect(status("Proposed (awaiting Alex approval)") == .proposed) #expect(status("Pending (auto-apply after 2026-06-18)") == .pending(autoApplyDate: "2026-06-18")) #expect(status("Approved — 2026-06-02") == .approved) #expect(status("Rejected") == .rejected) diff --git a/ScoutTests/Proposals/ProposalsWriterTests.swift b/ScoutTests/Proposals/ProposalsWriterTests.swift index c0adb82..0987275 100644 --- a/ScoutTests/Proposals/ProposalsWriterTests.swift +++ b/ScoutTests/Proposals/ProposalsWriterTests.swift @@ -7,7 +7,7 @@ private let writerFixture = """ --- date: 2026-06-13 title: Add a risk-scoped PR re-resolution step -status: Proposed (awaiting Adam approval) +status: Proposed (awaiting Alex approval) target: SKILL.md parent: [[dreaming-proposals]] --- @@ -31,7 +31,7 @@ struct ProposalsWriterRewriteTests { file: "p.md" ) #expect(out.contains("status: Approved (2026-06-14, via Scout app)")) - #expect(!out.contains("status: Proposed (awaiting Adam approval)")) + #expect(!out.contains("status: Proposed (awaiting Alex approval)")) // Other frontmatter fields untouched. #expect(out.contains("title: Add a risk-scoped PR re-resolution step")) #expect(out.contains("target: SKILL.md"))