Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4862415
ENG-2319 Add DG foundations section to landing page (#1519)
jsmorabito Oct 9, 2026
13088f8
ENG-2377 Add MIRA kickoff article to website news section (#1534)
jsmorabito Oct 9, 2026
c9b446b
ENG-2363 Documentation for schema import and export (#1531)
trangdoan982 Oct 9, 2026
66ab2ba
ENG-2376 Grant the relation type and triple when the Obsidian sync pa…
maparent Oct 9, 2026
fdb983e
ENG-2364 Investigate beta build having wrong versioning (#1530)
trangdoan982 Oct 9, 2026
dd71246
FEE-86 Roam/use-cases/Synthesize insights from the literature (#1458)
KarolaKirsanow Oct 9, 2026
ddd88d5
ENG-2378 Correct MIRA kickoff article date in website news section (#…
jsmorabito Oct 9, 2026
2d6b01d
FEE-86 add user guide on creating a personal knowledge base in Roam (…
KarolaKirsanow Oct 9, 2026
ecc78e6
FEE-86 - add Roam user guide to share your ideas and research (#1425)
KarolaKirsanow Oct 9, 2026
c51f64e
ENG-1967 Keep Share Data open in page-reference discourse context (#1…
mdroidian Oct 13, 2026
bbfb862
ENG-2380 Extract shared section header and align Foundations section …
jsmorabito Oct 13, 2026
ef71db9
ENG-2316 Replace current About page and Tool choice section with new …
jsmorabito Oct 13, 2026
e375088
ENG-2396 Prototype an inline Roam relations browser (#1542)
mdroidian Oct 13, 2026
7523cc8
ENG-2406 Refine the inline Roam relations browser (#1548)
mdroidian Oct 14, 2026
24bad6f
ENG-2312 Release mobile-supported DG obsidian (#1533)
trangdoan982 Oct 16, 2026
0b098cd
ENG-2323 Warn about unsupported placeholders in Obsidian node formats…
trangdoan982 Oct 16, 2026
8f3ae64
ENG-2366 Show candidate nodes as results in Roam advanced node search…
trangdoan982 Oct 16, 2026
64d1bac
Link MATSU Lab and Matt Akamatsu in the Sharing card (#1546)
jsmorabito Oct 16, 2026
e6404a3
Add graph background simulation and brand color utilities (#1543)
jsmorabito Oct 16, 2026
1b903db
ENG-2400 Add animated graph background to the landing page hero (#1544)
jsmorabito Oct 16, 2026
4ef138e
Use a light theme for the contact section and footer (#1545)
jsmorabito Oct 16, 2026
549245a
ENG-2143 Add Roam and Obsidian round-trip tests (#1529)
sid597 Oct 16, 2026
11c8b34
DIS-617 Sunset Assorted task projects in favor of labels and negative…
jsmorabito Oct 16, 2026
93fdd16
ENG-2397 Default Obsidian Source node format to @{content} (#1550)
trangdoan982 Oct 16, 2026
ee382a4
ENG-2417 Use existing YAML tooling in Obsidian import tests (#1555)
mdroidian Oct 16, 2026
9db726a
ENG-2367 Scroll to a candidate result in context of its page in the R…
trangdoan982 Oct 17, 2026
0b5aec5
ENG-2228 Add link support to tldraw text shapes in Obsidian (#1442)
trangdoan982 Oct 17, 2026
67e93a0
ENG-2361 Fix DG schema (#1514)
maparent Oct 20, 2026
1f123aa
Restrict my_accounts
maparent Oct 9, 2026
87982f5
eng-1837-add-public-status-in-db
maparent Jun 5, 2026
acd30a4
corrections
maparent Oct 8, 2026
3cc4099
allow access to storage
maparent Oct 9, 2026
b6608c0
Do not give account info to anonymous user
maparent Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .agents/skills/dg-create-engineering-ticket/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: dg-create-engineering-ticket
description: Create concise Discourse Graphs Engineering tickets in Linear with the canonical template, correct readiness status, an active existing project, and existing labels. Use when the user asks to create or draft an Engineering ticket; do not use for Feedback tickets.
description: Create concise Discourse Graphs Engineering tickets in Linear with the canonical template, correct readiness status, an active existing project (none for unprojected Obsidian work), and existing labels (including `Obsidian plugin` for Obsidian work). Use when the user asks to create or draft an Engineering ticket; do not use for Feedback tickets.
---

# DG Create Engineering Ticket
Expand Down Expand Up @@ -57,28 +57,28 @@ Always use the Engineering team. This skill never creates or redirects to Feedba

## Project selection

Every ticket must use an existing project that is not completed, canceled, archived, or trashed. Resolve the project in this order:
Resolve the project in this order:

1. Use the project explicitly named by the user.
2. Use a clearly relevant active project from the current work context.
3. For Roam work, use `Roam Discourse Graph plugin assorted tasks`.
4. For Obsidian work, use `Obsidian Discourse Graphs plugin assorted tasks`.
3. For Obsidian work with no relevant project, leave the project empty and apply the existing `Obsidian plugin` label. Do not use `Obsidian Discourse Graphs plugin assorted tasks`; the Obsidian queue picks up unprojected issues by label (see DIS-617).
4. For Roam work, use `Roam Discourse Graph plugin assorted tasks`.
5. Otherwise, use `Engineering Ops assorted tasks`.

Confirm the selected project still exists, is active, and includes the Engineering team. Never create a project. If a user-specified project is missing, ambiguous, or inactive, ask the user for another project instead of silently substituting one.
Any project you select must exist, not be completed, canceled, archived, or trashed, and include the Engineering team. Never create a project. If a user-specified project is missing, ambiguous, or inactive, ask the user for another project instead of silently substituting one. Obsidian tickets that do belong to a real project still use that project, plus the `Obsidian plugin` label.

## Labels and optional fields

- Inspect existing issue labels and apply every clearly relevant label.
- Inspect existing issue labels and apply every clearly relevant label. Always apply `Obsidian plugin` to Obsidian work, since it is how unprojected Obsidian tickets reach the queue.
- Never create a label or guess an uncertain label.
- Missing or uncertain optional labels do not block creation. If `Ticket Draft` or the required provenance label is missing or ambiguous, return the prepared draft and report the configuration gap instead of creating an unmarked ticket or substituting a status.
- Missing or uncertain optional labels do not block creation. If `Ticket Draft`, the required provenance label, or `Obsidian plugin` for Obsidian work is missing or ambiguous, return the prepared draft and report the configuration gap instead of creating an unmarked ticket or substituting a status.
- Set priority only when the user or context makes it clear and useful.
- Do not set a cycle or release. CI/CD handles releases.

## Create or draft

Draft without changing Linear when the user asks for a draft, review, or rewrite. Create the ticket without an additional confirmation when the user explicitly asks to create it.

Before creation, resolve the current Engineering statuses, selected project, and existing labels. Create the issue once with its title, completed description (including skill attribution), `Ticket Draft` status, project, provenance and relevant labels, and justified priority. If the result is ambiguous, search for the issue before retrying so a transient failure does not create a duplicate.
Before creation, resolve the current Engineering statuses, selected project, and existing labels. Create the issue once with its title, completed description (including skill attribution), `Ticket Draft` status, project (omitted for unprojected Obsidian work), provenance and relevant labels, and justified priority. If the result is ambiguous, search for the issue before retrying so a transient failure does not create a duplicate.

Read the created issue back and verify its status, labels, and attribution. Report any mismatch instead of claiming successful routing. Return the issue identifier, title, status, project, and link, and say that human review is pending. Include the generated branch name when Linear returns one. Do not restate the full description unless the user asks.
Read the created issue back and verify its status, labels, and attribution. For unprojected Obsidian work, confirm the project is empty and `Obsidian plugin` is applied. Report any mismatch instead of claiming successful routing. Return the issue identifier, title, status, project, and link, and say that human review is pending. Include the generated branch name when Linear returns one. Do not restate the full description unless the user asks.
16 changes: 11 additions & 5 deletions .github/workflows/obsidian-main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,20 @@ jobs:
# lookup needs an explicit --repo and a token that can read that repo.
GH_TOKEN: ${{ env.OBSIDIAN_PLUGIN_REPO_TOKEN }}
run: |
# manifest.json is the plugin's canonical version: it is what Obsidian
# and the community store read. package.json is private and its version
# is incidental.
STABLE_VERSION=$(node -p "require('./apps/obsidian/manifest.json').version")
LATEST_N=$(gh release list --repo "$PUBLISH_REPO" --limit 100 --json tagName --jq '.[].tagName' \
# The publish repo's manifest.json is the live store version; CI can't push the
# bump to this repo's main, so the local manifest.json goes stale after a release.
STABLE_VERSION=$(gh api "repos/$PUBLISH_REPO/contents/manifest.json" --jq '.content' | base64 -d | jq -r '.version')
if ! [[ "$STABLE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Unexpected stable version in $PUBLISH_REPO manifest.json: '$STABLE_VERSION'"
exit 1
fi
# Separate command so bash -e (no pipefail) fails here instead of restarting at beta.1.
TAGS=$(gh release list --repo "$PUBLISH_REPO" --limit 100 --json tagName --jq '.[].tagName')
LATEST_N=$(echo "$TAGS" \
| grep "^${STABLE_VERSION}-beta\." \
| sed "s/^${STABLE_VERSION}-beta\.//" \
| sort -n | tail -1)
# Deliberately below the stable version in semver, so automatic updates never pick a beta.
NEXT_BETA="${STABLE_VERSION}-beta.$(( ${LATEST_N:-0} + 1 ))"
echo "Stable version: $STABLE_VERSION, next beta: $NEXT_BETA"
echo "version=$NEXT_BETA" >> "$GITHUB_OUTPUT"
Expand Down
2 changes: 2 additions & 0 deletions apps/obsidian/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ for (let leaf of app.workspace.getActiveLeavesOfType(MY_VIEW_TYPE)) {
- Node.js and Electron APIs (`fs`, `crypto`, `os`) are unavailable on mobile
- If the plugin targets mobile, use web API equivalents: `SubtleCrypto` instead of `crypto`, `navigator.clipboard` for clipboard access
- Regex lookbehind assertions are not supported on some mobile — avoid them if possible
- The plugin ships with `isDesktopOnly: false`, so it must load on mobile
- Node builtins and `electron` are deliberately not in the esbuild `external` array in `scripts/compile.ts`. Marking them external lets a dependency's `require("fs")` reach the bundle and throw on mobile; leaving them out fails the build instead. Replace the dependency rather than re-adding the external

### Code quality

Expand Down
2 changes: 1 addition & 1 deletion apps/obsidian/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"description": "Add semantic structure to your notes with the Discourse Graph protocol.",
"author": "Discourse Graphs",
"authorUrl": "https://discoursegraphs.com",
"isDesktopOnly": true
"isDesktopOnly": false
}
6 changes: 2 additions & 4 deletions apps/obsidian/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,14 @@
"@octokit/core": "^6.1.2",
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/mime-types": "3.0.1",
"@types/node": "catalog:",
"@types/react": "catalog:obsidian",
"@types/react-dom": "catalog:obsidian",
"autoprefixer": "^10.4.21",
"builtin-modules": "3.3.0",
"dotenv": "^16.4.5",
"esbuild": "0.17.3",
"eslint": "catalog:",
"gray-matter": "^4.0.3",
"obsidian": "^1.7.2",
"postcss": "^8.5.3",
"tailwindcss": "^3.4.17",
Expand All @@ -39,6 +38,7 @@
"typescript": "5.5.4",
"uuidv7": "1.1.0",
"vitest": "catalog:",
"yaml": "^2.8.1",
"zod": "^3.24.1"
},
"dependencies": {
Expand All @@ -48,8 +48,6 @@
"@repo/utils": "workspace:*",
"@supabase/supabase-js": "catalog:",
"date-fns": "^4.1.0",
"gray-matter": "^4.0.3",
"mime-types": "^3.0.1",
"nanoid": "^4.0.2",
"react": "catalog:obsidian",
"react-dom": "catalog:obsidian",
Expand Down
7 changes: 4 additions & 3 deletions apps/obsidian/scripts/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import esbuild from "esbuild";
import fs from "fs";
import path from "path";
import { z } from "zod";
import builtins from "builtin-modules";
import dotenv from "dotenv";
import postcss from "postcss";
import tailwindcss from "tailwindcss";
Expand Down Expand Up @@ -43,9 +42,11 @@ export const args = {
format: "cjs",
root: ".",
mirror: process.env.OBSIDIAN_PLUGIN_PATH,
// Node builtins and "electron" are deliberately NOT external. Marking them external lets a
// dependency's `require("fs")` survive into the bundle, which throws at runtime on Obsidian
// mobile; leaving them out fails the build instead. Replace the dependency, do not re-add these.
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
Expand All @@ -58,7 +59,6 @@ export const args = {
"@lezer/highlight",
"@lezer/lr",
"tslib=window.TSLib",
...builtins,
],
} as CliOpts;

Expand Down Expand Up @@ -115,6 +115,7 @@ export const compile = ({
outdir,
bundle: true,
format,
platform: "browser",
sourcemap: isProd ? undefined : "inline",
minify: isProd,
entryNames: out,
Expand Down
228 changes: 228 additions & 0 deletions apps/obsidian/src/components/canvas/TextLinkDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import { useCallback, useEffect, useRef, useState } from "react";
import {
TLBaseShape,
TLShape,
TldrawUiButton,
TldrawUiButtonLabel,
TldrawUiDialogBody,
TldrawUiDialogCloseButton,
TldrawUiDialogFooter,
TldrawUiDialogHeader,
TldrawUiDialogTitle,
TldrawUiInput,
track,
useEditor,
} from "tldraw";
import {
getTextShapeLinkUrl,
isAllowedTextLinkUrl,
isObsidianUrl,
} from "~/components/canvas/utils/textShapeLink";

type ShapeWithUrl = TLBaseShape<string, { url: string }>;

// Text shapes keep their link in `meta` so the stock props schema is untouched;
// every other shape type uses tldraw's own `props.url`.
const isTextShape = (shape: TLShape): boolean => shape.type === "text";

const linkPatch = (shape: TLShape, url: string) =>
isTextShape(shape) ? { meta: { url } } : { props: { url } };

const readLinkUrl = (shape: TLShape): string =>
isTextShape(shape)
? getTextShapeLinkUrl(shape)
: (((shape.props as { url?: unknown }).url as string | undefined) ?? "");

// Declared as a function property rather than reusing TLUiDialogProps, whose
// method-shorthand onClose trips @typescript-eslint/unbound-method.
type TextLinkDialogProps = { onClose: () => void };

type UrlValidity = { isValid: boolean; hasProtocol: boolean };

// Bookmarks link their title with a raw <a href>, not HyperlinkButton, so a
// page link there would reach the OS handler and skip the vault check.
const acceptsPageLinks = (shapeType: string): boolean =>
shapeType !== "bookmark";

// Every shape type uses tldraw's linkUrl, patched to accept obsidian://open links.
const validateUrl = (url: string, shapeType: string): UrlValidity => {
// Kept away from the https:// fallback, which would accept `https://obsidian://…`.
if (isObsidianUrl(url)) {
const isValid = acceptsPageLinks(shapeType) && isAllowedTextLinkUrl(url);
return { isValid, hasProtocol: isValid };
}
if (isAllowedTextLinkUrl(url)) return { isValid: true, hasProtocol: true };
if (isAllowedTextLinkUrl(`https://${url}`))
return { isValid: true, hasProtocol: false };
return { isValid: false, hasProtocol: false };
};

// A fork of tldraw's EditLinkDialog, which is not exported. Copy differs: the
// original's msg() keys are inlined, as this app has no i18n layer.
export const TextLinkDialog = track(({ onClose }: TextLinkDialogProps) => {
const editor = useEditor();
const selectedShape = editor.getOnlySelectedShape();

const canEditLink =
selectedShape &&
(isTextShape(selectedShape) ||
("url" in selectedShape.props &&
typeof selectedShape.props.url === "string"));

if (!canEditLink) return null;

return (
<TextLinkDialogInner
key={selectedShape.id}
onClose={onClose}
selectedShape={selectedShape as ShapeWithUrl}
/>
);
});

const TextLinkDialogInner = track(
({
onClose,
selectedShape,
}: TextLinkDialogProps & { selectedShape: ShapeWithUrl }) => {
const editor = useEditor();
const rInput = useRef<HTMLInputElement | null>(null);
const shapeType = selectedShape.type;

useEffect(() => {
editor.timers.requestAnimationFrame(() => rInput.current?.focus());
}, [editor]);

const rInitialValue = useRef(readLinkUrl(selectedShape));

const [urlInputState, setUrlInputState] = useState(() => {
const initialUrl = readLinkUrl(selectedShape);
const result = validateUrl(initialUrl, shapeType);
const initialValue = result.isValid
? result.hasProtocol
? initialUrl
: `https://${initialUrl}`
: "https://";
return { actual: initialValue, safe: initialValue, valid: true };
});

const handleChange = useCallback(
(rawValue: string) => {
// Drop the prefilled https:// when a full URL is pasted after it.
const fixedRawValue = rawValue.replace(
/https?:\/\/((?:https?|obsidian):\/\/)/i,
(_match, arg1: string) => arg1,
);
const result = validateUrl(fixedRawValue, shapeType);
const safeValue = result.isValid
? result.hasProtocol
? fixedRawValue
: `https://${fixedRawValue}`
: "https://";

setUrlInputState({
actual: fixedRawValue,
safe: safeValue,
valid: result.isValid,
});
},
[shapeType],
);

const handleClear = useCallback(() => {
const onlySelectedShape = editor.getOnlySelectedShape();
if (!onlySelectedShape) return;
editor.updateShapes([
{
id: onlySelectedShape.id,
type: onlySelectedShape.type,
...linkPatch(onlySelectedShape, ""),
},
]);
onClose();
}, [editor, onClose]);

const handleComplete = useCallback(() => {
// Enter reaches this even while Save is disabled, and the fallback
// "https://" fails both validators.
if (!urlInputState.valid) return;
const onlySelectedShape = editor.getOnlySelectedShape();
if (!onlySelectedShape) return;
if (readLinkUrl(onlySelectedShape) !== urlInputState.safe) {
editor.updateShapes([
{
id: onlySelectedShape.id,
type: onlySelectedShape.type,
...linkPatch(onlySelectedShape, urlInputState.safe),
},
]);
}
onClose();
}, [editor, onClose, urlInputState]);

const handleCancel = useCallback(() => {
onClose();
}, [onClose]);

const isRemoving = rInitialValue.current && !urlInputState.valid;

return (
<>
<TldrawUiDialogHeader>
<TldrawUiDialogTitle>Edit link</TldrawUiDialogTitle>
<TldrawUiDialogCloseButton />
</TldrawUiDialogHeader>
<TldrawUiDialogBody>
<div className="tlui-edit-link-dialog">
<TldrawUiInput
ref={rInput}
className="tlui-edit-link-dialog__input"
label="edit-link-dialog.url"
autoFocus
autoSelect
placeholder="https://example.com"
value={urlInputState.actual}
onValueChange={handleChange}
onComplete={handleComplete}
onCancel={handleCancel}
/>
<div>
{!urlInputState.valid
? "Invalid URL"
: acceptsPageLinks(shapeType)
? "Enter a URL, or an obsidian:// link to a page."
: "Enter a URL."}
</div>
</div>
</TldrawUiDialogBody>
<TldrawUiDialogFooter className="tlui-dialog__footer__actions">
<TldrawUiButton
type="normal"
onClick={handleCancel}
onTouchEnd={handleCancel}
>
<TldrawUiButtonLabel>Cancel</TldrawUiButtonLabel>
</TldrawUiButton>
{isRemoving ? (
<TldrawUiButton
type="danger"
onClick={handleClear}
onTouchEnd={handleClear}
>
<TldrawUiButtonLabel>Clear</TldrawUiButtonLabel>
</TldrawUiButton>
) : (
<TldrawUiButton
type="primary"
disabled={!urlInputState.valid}
onClick={handleComplete}
onTouchEnd={handleComplete}
>
<TldrawUiButtonLabel>Save</TldrawUiButtonLabel>
</TldrawUiButton>
)}
</TldrawUiDialogFooter>
</>
);
},
);
Loading