tq-triage

star 13

Inventory and organize open tasks - review status, propose cleanup, execute

MH4GF By MH4GF schedule Updated 6/8/2026

name: tq-triage description: Inventory and organize open tasks - review status, propose cleanup, execute argument-hint: "[project_name]" allowed-tools: Bash(tq *), Bash(gh pr view *), Bash(find *), Read, AskUserQuestion, Skill(tq:create-action)

tq triage

Inventory and organize open tasks.

Steps

1. Collect (lightweight)

The raw tq task list --status open payload often exceeds 1MB. Fetch with --jq to extract only the fields needed for triage.

MUST execute the following jq query verbatim — do not modify, simplify, or substitute custom field selections. latest_triage_note is required by Step 3's skip rule; dropping it silently breaks the skip evaluation and forces re-decisions on already-triaged tasks.

tq task list --status open --jq '
  .[] | {
    id, project_id, title, updated_at,
    metadata_url: (.metadata // "{}" | try fromjson.url // null),
    latest_triage_note,
    latest: (
      .actions | sort_by(.created_at) | last
      | if . then {id, title, status, completed_at,
                    result_head: ((.result // "")[0:300])} else null end
    )
  }
'

latest_triage_note is the most recent kind=triage_keep note on the task, or null. When present it has {reason, at, snooze_until?}. It surfaces the previous "leave open" judgment so Step 3 can skip tasks whose situation has not changed.

Filter by --project <id> if $ARGUMENTS is given.

Pre-flight declaration (MUST output before Step 2): After running the query, count the result and emit an assistant message in this exact form:

Found N open tasks. M have prior latest_triage_note — Step 3 skip rule (including PR-merge override) will be evaluated for each.

Where N is the total task count and M is the count of tasks with latest_triage_note != null. Both numbers MUST be derived from the Step 1 query output. Do NOT proceed to Step 2 without emitting this declaration.

2. Project consistency check (before phase detection)

Detect tasks that landed in the wrong project due to auto-creation (e.g. gh-ops:watch). Run tq project list to get the project list with metadata, then infer the expected project from each task's metadata_url (pre-extracted in Step 1), title, and latest.result_head, and compare against the current project_id.

Capture dispatch_enabled per project from the same tq project list output. Build a project_id → dispatch_enabled map; it is reused in Steps 3, 5, and 6 to reason about focus. A project with dispatch_enabled == false ("unfocus") will NOT auto-dispatch its pending actions — they sit indefinitely unless manually dispatched or dispatch_enabled is flipped to true.

Present mismatches and fix: If mismatches are found, present them in the table below and confirm one at a time via AskUserQuestion (choices: move / skip). On each approval, run tq task update <ID> --project <new_id>. Never batch-approve.

ID Title Current Expected Evidence
420 Respond to PR #55 works (example) metadata.url: github.com/example/app/pull/55

Project moves are resolved here; subsequent steps use the updated project_id.

3. Phase detection

PR-state pre-fetch (runs first; the cache is read by the Phase criteria below, the skip rule, Step 4, and Step 6): For every task with a PR URL, run gh pr view <url> --json state,mergedAt,mergeable,reviewDecision calls in parallel (single message, multiple Bash calls). Cache the JSON by task_id. Tasks with no extractable PR URL skip this fetch — the PR-merge override below simply does not apply to them.

PR URL precedence per task:

  1. Prefer metadata_url (extracted in Step 1). It is authoritative when set — the task was created with this specific PR in mind.
  2. Otherwise, take the first match in latest.result_head from the regex https://github\.com/[\w.-]+/[\w.-]+/pull/\d+. The [\w.-]+ form anchors org/repo segments and stops the URL cleanly at the PR number — no trailing /files, #issuecomment-…, or punctuation.

Now classify each task. Inspect latest.status, keywords in latest.result_head, and the cached PR state when available.

Phase criteria:

  • Not started: latest == null.
  • In progress: latest.status ∈ {running, pending}.
  • Awaiting review: latest.status == done and result contains push complete / review / PR opened.
  • Awaiting deploy: latest done-action is a review/self-review, merge/deploy remains.
  • Stalled: Persistent failures (status failed multiple times, or stale: ... in result) or updated_at older than 14 days.
  • Blocked: Stalled with a result that explicitly states a blocker (permission error, external dependency, etc.) that cannot be resolved independently.
  • Likely complete: state == MERGED from the PR-state pre-fetch (above), or latest.status == done with merged / done / complete in the result.

Focus qualifier: When latest.status ∈ {running, pending} AND the task's project is unfocus (dispatch_enabled == false), append (unfocus: manual dispatch required) to the phase label. This surfaces the fact that a pending action in an unfocus project will not progress on its own.

Triage skip rule (after the phase is assigned): If latest_triage_note != null, evaluate whether the prior keep judgment still holds. The task is skipped from Step 6 (and shown in Step 5 as triaged Nd ago: <reason>) when all of the following are true:

  • (a) now - latest_triage_note.at < 7 days (cooldown window).
  • (b) No new action has completed (any completed_at > latest_triage_note.at) and no task.status_changed event since latest_triage_note.at for this task. Inspect the action list and tq event list --entity task --id <id> if needed.
  • (c) latest_triage_note.snooze_until is unset, OR now < latest_triage_note.snooze_until.
  • (d) The task either has no cached PR state, or pr_state.mergedAt is null, or pr_state.mergedAt <= latest_triage_note.at (see the timestamp normalization note below before comparing).

PR-merge override: A PR merged after the prior triage note (pr_state.mergedAt > latest_triage_note.at) breaks (d) and forces re-evaluation even when (a) cooldown or (c) snooze would otherwise hold. The rationale: a merged PR means the task should almost certainly be marked done, so neither the 7-day wait nor an explicit snooze is load-bearing once the PR has landed. This catches the case where a human merges a PR manually while a self-review action sits pending — without (d), the note keeps the task buried until the cooldown lapses even though the underlying work is finished.

Out of scope for this override: PR state transitions other than merge (CLOSED-without-merge, OPEN → APPROVED, mergeable conflict, etc.). They are still handled by clauses (a)/(b) if/when an action records them in tq, and by the Likely-complete phase rules in Step 6 once the next action runs. The override deliberately covers only the silent merge case.

Timestamp normalization (for clause (d)): pr_state.mergedAt from gh pr view is ISO 8601 with T separator and Z suffix (e.g. 2026-06-02T19:00:00Z). latest_triage_note.at is SQLite TEXT with a space separator (e.g. 2026-06-02 19:00:00). Direct string comparison is unsafe — T (0x54) sorts after ' ' (0x20), so a same-day note can wrongly compare less-than. Before evaluating clause (d), replace the space in at with T and append Z (or parse both as datetimes); only then compare.

If (c) is set and now < snooze_until, skip even if (a) or (b) would re-evaluate — explicit snooze wins. The PR-merge override (d) is the one exception: a post-note merge re-evaluates the task even with an active snooze. Otherwise, failing any of (a)/(b)/(c)/(d) means the task is re-evaluated normally in Step 6 and the prior reason is shown in option description for context.

Recurring-task exclusion rule (independent of the triage skip rule; applies even when latest_triage_note == null). Fetch the schedule map once, reuse in Steps 5/6:

tq schedule list --jq '.[] | {id, task_id, enabled}'

A task is recurring when it has a schedule-map entry OR any of its actions' metadata contains schedule_id (check tq action list --task <id> when the map is inconclusive). The map is authoritative for enabled.

Exclude every recurring task from Step 6 unconditionally — the task is Leave open by default, the next scheduled run is the recovery mechanism, and AskUserQuestion MUST NOT fire for it. This holds regardless of latest.status (including failed, pending, done). The only exception is when the schedule is disabled or missing (see Override below). Canonical cases the user has called out as never needing an AskUserQuestion: weekly review, Gmail Inbox Zero, work-log recording, MF classification, turso-query-watch. These recurring jobs self-heal on the next tick.

A task whose latest.status == running is also kept out of Step 6 by the existing "In progress" rule — recurring or not, do not interrupt a live action.

Persistent failures on a recurring task surface through other channels (/tq:investigate-incidents, the next schedule run, or a human-initiated schedule update). They are deliberately outside the triage AskUserQuestion path — re-including them would re-introduce the noise this rule eliminates.

Override — triage normally even when the task is recurring: the backing schedule is absent from the map OR enabled == false (disabled/deleted — the recurring task may be orphaned and needs a human decision).

Excluded tasks are reported under the recurring category in Step 6's pre-report, not the triage-note list. If both rules exclude a task, the recurring category wins.

Deep-dive condition: If latest.status == done AND len(result_head) == 300 (truncated) AND none of the keywords push complete, review, merged, stale, blocked, failed, done appear in result_head (case-insensitive), fetch the latest action's full result:

tq task get <ID> --jq '.actions | sort_by(.created_at) | last | {status, result}'

If multiple tasks need deep-dive, issue the tq task get calls in parallel (single message, multiple Bash calls). Skip deep-dive when latest.status ∈ {running, pending} or when the truncated head already contains a decisive keyword.

Session-log fallback: When the deep-dive still leaves result thin (failed action with a 1-line error, running action with empty result, or len(result) < 100) AND metadata.claude_session_id is non-empty, read the Claude Code session log to recover the missing detail:

SID=$(tq action get <id> --jq '.metadata | fromjson.claude_session_id // empty')
[ -n "$SID" ] && find ~/.claude/projects -name "$SID.jsonl" -print -quit

Use Read on the resolved path (last ~200 lines — the file may be large) and quote the latest few type:"assistant" entries (each embeds the response text plus any tool_use blocks in message.content[]) and any type:"user" entries carrying tool_result blocks, into 6-a Diagnosis. This does not replace the tq task get deep-dive — it runs in addition.

4. PR-state finalization

The Step 3 PR-state pre-fetch has already cached gh pr view JSON for every task with a PR URL — do not re-fetch here. Tasks without an extractable PR URL have no cache entry and skip this finalization (Step 3's keyword-based fallback in the Phase criteria already covers them).

Finalize classification using the cached state: state == MERGEDLikely complete. The cache remains available for Step 6.

5. Summary

Present tasks by project in a table. Mark each project's section header with its focus state (focus / unfocus) from the Step 2 map.

ID Title Age Phase Latest action Latest triage
157 Implement feature A 3d Awaiting review #815 implement done — implementation complete, pushed
302 Refactor parser 2d In progress (unfocus: manual dispatch required) #900 implement pending — queued, will not auto-dispatch
55 Fix bug B 5d Not started 3d ago: awaiting PR review
88 Weekly rows-read check 1d Likely complete (recurring) #940 run done — no regression, schedule #12 enabled

The Latest triage column shows Nd ago: <reason> when latest_triage_note is present, otherwise . Tasks skipped by the Step 3 triage skip rule still appear in this table but are excluded from Step 6. Tasks excluded by the Step 3 recurring-task exclusion rule also appear here, with (recurring) appended to the Phase column (e.g. Likely complete (recurring)), and are likewise excluded from Step 6.

Use the post-move project_id (tasks moved in Step 2 appear under their new project).

6. Proposals — per-task sequential triage (Rumelt's kernel of strategy)

Pre-Step-6 skipped-task report (MUST output before any AskUserQuestion): Before starting the first task's 6-a Diagnosis, emit an assistant message listing every task excluded by the Step 3 triage skip rule, with the prior triage reason and timestamp. Use this exact form (one example with cooldown, one with snooze — pick the gating clause that matches each task):

Skipping N tasks with valid prior triage notes:

  • Task # () — <code><reason></code> (triaged Nd ago at YYYY-MM-DD; cooldown active)</li> <li>Task #<id> (<title>) — <code><reason></code> (triaged Nd ago at YYYY-MM-DD; snooze_until: YYYY-MM-DD)</li> </ul> </blockquote> <p>Each line MUST include the task <code>id</code>, <code>title</code>, the <code>latest_triage_note.reason</code> quoted verbatim in backticks, days since <code>latest_triage_note.at</code>, and the gating clause: <code>snooze_until: YYYY-MM-DD</code> when <code>latest_triage_note.snooze_until</code> is set, otherwise <code>cooldown active</code>. If no tasks are skipped, still emit <code>Skipping 0 tasks — no valid prior triage notes.</code> so the user can confirm the skip rule was evaluated. Do NOT issue the first <code>AskUserQuestion</code> without this report.</p> <p><strong>Recurring-task exclusions (separate category, MUST also be emitted)</strong>: list every task excluded by the Step 3 recurring-task exclusion rule. This is distinct from the triage-note list above — a task appears in only one. Use this exact form:</p> <blockquote> <p>Skipping M recurring tasks (Leave open by default, next scheduled run is the recovery path):</p> <ul> <li>Task #<id> (<title>) — schedule #<sid> (enabled), latest action #<aid> <status></li> </ul> </blockquote> <p>If none, emit <code>Skipping 0 recurring tasks.</code>.</p> <p>After the Step 5 summary and the skipped-task report, triage open tasks <strong>one at a time, in order</strong>. For each task, walk the three sub-steps below — Diagnosis, Guiding Policy, Coherent Actions — modeled on Richard Rumelt's <em>kernel of strategy</em> (<em>Good Strategy, Bad Strategy</em>): name the situation, choose a direction, then act coherently.</p> <p><strong>Skip from this step</strong>: tasks excluded by the <strong>Step 3 triage skip rule</strong> or the <strong>Step 3 recurring-task exclusion rule</strong> (both already enumerated in the report above), and tasks in the <strong>In progress</strong> phase (running — do not interrupt; they appear in Step 5 only). Project moves are already resolved in Step 2.</p> <p><strong>MUST NOT</strong> batch <code>AskUserQuestion</code> across tasks — task IDs and one-line summaries are not enough for a human to judge several cases in parallel. Complete 6-a → 6-b → 6-c → Step 7 execution for one task before starting the next task's 6-a.</p> <h4>6-a. Diagnosis</h4> <p>In the <strong>assistant message body</strong> (not <code>AskUserQuestion</code>), present:</p> <ul> <li>Task ID, title, age (days since <code>updated_at</code>).</li> <li>Latest substantive action: ID, status, and a 1-3 line <code>result</code> quote (use a blockquote for the decisive lines).</li> <li>Phase classification from Step 3 + the specific evidence that decided it (which keyword in <code>result_head</code>, which <code>latest.status</code>, which PR <code>state</code> from the Step 3 cache).</li> <li>Phase-specific concern:<ul> <li><strong>Awaiting review / Awaiting deploy</strong>: blockers, PR state from the Step 3 PR-state cache.</li> <li><strong>Stalled / Blocked</strong>: stall duration, unresolved obstacle.</li> <li><strong>Likely complete</strong>: completion evidence (PR merged, etc.).</li> <li><strong>Not started</strong>: probable reason for non-start.</li> </ul> </li> </ul> <h4>6-b. Guiding Policy</h4> <p>Continuing in the message body, state the recommended direction:</p> <ul> <li>Recommended action (<code>Mark done</code> / <code>Archive</code> / continue / <code>Create ... action</code> / <code>Manually dispatch</code> / <code>Enable dispatch</code>) and <strong>why</strong>.</li> <li>Counter-options ruled out and the reason.</li> <li>For unfocus-project tasks with stalled <code>pending</code> actions, justify whether to keep, manually dispatch, or enable dispatch — the user must know <code>pending</code> actions will not auto-dispatch.</li> </ul> <p>Pick the 6-c options (2-4 per task) from this template:</p> <table> <thead> <tr> <th>Phase</th> <th>Options</th> </tr> </thead> <tbody><tr> <td>Awaiting review</td> <td><code>Create review-request action</code> / <code>Create merge action</code> / <code>Mark done (already merged)</code> / <code>Leave open</code></td> </tr> <tr> <td>Awaiting deploy</td> <td><code>Create deploy action</code> / <code>Mark done</code> / <code>Leave open</code></td> </tr> <tr> <td>Stalled</td> <td><code>Create investigate-root-cause action</code> / <code>Change approach (new action)</code> / <code>Archive</code> / <code>Leave open</code></td> </tr> <tr> <td>Blocked</td> <td><code>Create unblock action</code> / <code>Archive</code> / <code>Leave open</code></td> </tr> <tr> <td>Likely complete</td> <td><code>Mark done</code> / <code>Create merge action</code> / <code>Leave open</code> (see PR-state rule below)</td> </tr> <tr> <td>Not started</td> <td><code>Create first action</code> / <code>Archive</code> / <code>Leave open</code></td> </tr> </tbody></table> <p><strong>Likely complete — PR-state rule</strong> (uses the Step 3 PR-state cache):</p> <ul> <li><code>state == MERGED</code> → <code>Mark done</code> first, label <code>Mark done (Recommended)</code>.</li> <li><code>state == OPEN</code> → <code>Create merge action</code> first, label <code>Create merge action (Recommended)</code>.</li> </ul> <p><strong>Universal "leave open" options</strong>: every phase may add <code>Leave open with note (keep)</code> and <code>Snooze N days</code> so the next triage run can skip the task. Plain <code>Leave open</code> remains for "no reason worth recording".</p> <p><strong>Forward-motion default</strong>: every phase except In progress MUST include at least one concrete forward-motion option (create next action, mark done, archive). Tasks that reach Step 6's <code>AskUserQuestion</code> must not sit as <code>Leave open</code> by default. (Recurring tasks are exempt — they never reach this question; see the Step 3 recurring-task exclusion rule.)</p> <p><strong>Unfocus-aware options</strong>: when the task's project is unfocus AND it has a <code>pending</code> action (or the proposal would otherwise be <code>Leave open</code>), the option set MUST include both, with the unfocus state stated in the option <code>description</code>:</p> <ul> <li><code>Manually dispatch pending action</code> → runs <code>tq action dispatch <action_id></code> for the waiting action.</li> <li><code>Enable dispatch and batch-run</code> → runs <code>tq project update <project_id> --dispatch-enabled true</code> so all pending actions in that project drain automatically.</li> </ul> <p><strong>Likely complete with pending follow-ups</strong>: when proposing <code>Mark done</code> while <code>pending</code> actions remain, distinguish in the option <code>description</code>:</p> <ul> <li>Pending in an <strong>unfocus</strong> project → likely stale (queued but unreachable). Recommend <code>Mark done</code> and cancel/rework the leftovers separately.</li> <li>Pending in a <strong>focus</strong> project → genuine follow-up in flight. Prefer <code>Leave open</code> until they complete, or cancel them explicitly before <code>Mark done</code>.</li> </ul> <h4>6-c. Coherent Actions</h4> <p>Issue <strong>one</strong> <code>AskUserQuestion</code> for <strong>this task only</strong>:</p> <ul> <li>2-4 options drawn from the 6-b template, shaped by the 6-a / 6-b context.</li> <li>Place the recommended option first; append <code>(Recommended)</code> to its label.</li> <li>Per-option <code>description</code> MUST contain the decision material:<ul> <li>1-2 line summary of the latest <code>result</code>.</li> <li>For PR-related tasks: PR number + state from the Step 3 PR-state cache.</li> <li>Days since last update (<code>updated_at</code> vs today).</li> </ul> </li> </ul> <p><strong>Action instruction quality</strong>: when the user picks <code>Create ... action</code>, invoke <code>/tq:create-action</code> with a <code>task_id</code> and an instruction that quotes the specific next step from the previous <code>result</code> (e.g. "Request review on PR #XXX" — not a vague "request review"). Include relevant URLs, IDs, and any <code>next action</code> note from the prior result. Do not call <code>tq action create</code> directly.</p> <p>After the user approves, immediately run the corresponding Step 7 command for the chosen option. Only once that command has completed (or <code>Leave open</code> etc. has been recorded) do you move on to the <strong>next task's 6-a</strong>. Do not present another task's diagnosis before the current task's Step 7 reflection finishes.</p> <h3>7. Execute</h3> <p>Execute each approved option immediately:</p> <ul> <li><code>Mark done</code> → <code>tq task update <ID> --status done --note '<1-line why it is complete, cite the result evidence>'</code> (<code>--note</code> is required alongside <code>--status</code>).</li> <li><code>Archive</code> → <code>tq task update <ID> --status archived --note '<1-line reason, e.g. "stalled 30d, no path forward">'</code>.</li> <li><code>Create ... action</code> → invoke <code>/tq:create-action</code> (task_id + instruction). Do not call <code>tq action create</code> directly.</li> <li><code>Manually dispatch pending action</code> → <code>tq action dispatch <action_id></code> for the pending action identified in Step 3.</li> <li><code>Enable dispatch and batch-run</code> → <code>tq project update <project_id> --dispatch-enabled true</code>. Report the project name to the user so they know which project now auto-dispatches.</li> <li><code>Leave open with note (keep)</code> → ask the user for a one-line reason, then <code>tq task note <ID> --kind triage_keep --reason '<reason>'</code>.</li> <li><code>Snooze N days</code> → ask the user for the snooze duration (number of days, or an explicit <code>YYYY-MM-DD</code> target date). Compute <code>snooze_until</code> and run <code>tq task note <ID> --kind triage_keep --reason '<reason>' --metadata '{"snooze_until":"YYYY-MM-DD"}'</code>.</li> <li><code>Leave open</code> → no-op.</li> </ul> <p>If an execute fails, record the error, report it to the user, and continue with the remaining batch. After all rounds of all phases complete, triage ends.</p> </article> </div> <!-- Right: Metadata & Command Sidebar --> <div class="w-full lg:w-80 shrink-0 flex flex-col gap-6" data-astro-cid-7zzsworf> <!-- Install Card --> <div class="p-6 rounded-xl bg-surface-container border border-border/80 flex flex-col gap-4 shadow-sm" data-astro-cid-7zzsworf> <span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant/60 font-mono" data-astro-cid-7zzsworf>Install via CLI</span> <div class="flex flex-col gap-2" data-astro-cid-7zzsworf> <div id="detail-install-cmd" class="font-mono text-[11px] p-3 rounded-lg bg-black/40 border border-border select-all break-all text-primary font-bold leading-relaxed" data-astro-cid-7zzsworf> npx skills add https://github.com/MH4GF/tq --skill tq-triage </div> <button id="detail-copy-btn" class="w-full py-2.5 rounded-lg bg-primary hover:bg-primary-hover text-on-primary font-sans font-bold text-sm shadow transition-all active:scale-95 flex items-center justify-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px]" data-astro-cid-7zzsworf>content_copy</span> <span data-astro-cid-7zzsworf>Copy Command</span> </button> </div> </div> <!-- Details & Stats Card --> <div class="p-6 rounded-xl bg-surface-container border border-border/80 flex flex-col gap-4 shadow-sm text-on-surface" data-astro-cid-7zzsworf> <span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant/60 font-sans" data-astro-cid-7zzsworf>Repository Details</span> <div class="flex flex-col gap-3.5" data-astro-cid-7zzsworf> <div class="flex justify-between items-center text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>star</span> Stars </span> <span class="font-mono font-bold text-on-surface" data-astro-cid-7zzsworf>13</span> </div> <div class="flex justify-between items-center text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>call_split</span> Forks </span> <span class="font-mono font-bold text-on-surface" data-astro-cid-7zzsworf>0</span> </div> <div class="flex justify-between items-center text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>navigation</span> Branch </span> <span class="font-mono bg-surface border border-border px-2 py-0.5 rounded text-[11px] text-on-surface-variant" data-astro-cid-7zzsworf>main</span> </div> <div class="flex justify-between items-start text-sm" data-astro-cid-7zzsworf> <span class="text-on-surface-variant/70 flex items-center gap-1.5 mt-0.5" data-astro-cid-7zzsworf> <span class="material-symbols-outlined text-[16px] text-on-surface-variant/60" data-astro-cid-7zzsworf>article</span> Path </span> <span class="font-mono bg-surface border border-border px-2 py-0.5 rounded text-[11px] text-on-surface-variant truncate max-w-[150px]" title="SKILL.md" data-astro-cid-7zzsworf>SKILL.md</span> </div> </div> </div> <!-- Occupations Tag Card --> <!-- Related Creators Card --> <div class="p-6 rounded-xl bg-surface-container border border-border/80 flex flex-col gap-3 shadow-sm" data-astro-cid-7zzsworf> <span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant/60 font-sans" data-astro-cid-7zzsworf>More from Creator</span> <div class="flex items-center gap-2" data-astro-cid-7zzsworf> <img class="w-8 h-8 rounded-full border border-border" src="https://avatars.githubusercontent.com/u/31152321?u=e2c383c466a36a552f5d984fa3b6ed99275267f9&v=4" alt="MH4GF" onerror="this.src='https://avatars.githubusercontent.com/u/9919?v=4'" data-astro-cid-7zzsworf> <div class="flex flex-col min-w-0" data-astro-cid-7zzsworf> <span class="font-bold text-sm truncate text-on-surface" data-astro-cid-7zzsworf>MH4GF</span> <a href="/?creator=MH4GF" class="text-xs text-primary hover:underline font-semibold transition-all" data-astro-cid-7zzsworf>Explore all skills →</a> </div> </div> </div> </div> </div> </div> </div> <script> const copyBtn = document.getElementById("detail-copy-btn"); const installCmd = document.getElementById("detail-install-cmd"); if (copyBtn && installCmd) { copyBtn.addEventListener("click", () => { const cmd = installCmd.textContent.trim(); navigator.clipboard.writeText(cmd).then(() => { const originalText = copyBtn.innerHTML; copyBtn.innerHTML = ` <span class="material-symbols-outlined text-[16px]">check</span> <span>Copied!</span> `; copyBtn.style.background = "#10b981"; copyBtn.style.borderColor = "#10b981"; setTimeout(() => { copyBtn.innerHTML = originalText; copyBtn.style.background = ""; copyBtn.style.borderColor = ""; }, 1500); }); }); } </script> </div> <!-- Footer --> <footer class="border-t border-border bg-surface-container-low text-on-surface-variant py-8 px-gutter mt-16 rounded-xl"> <div class="max-w-container-max mx-auto flex flex-col md:flex-row justify-between items-center gap-6"> <div class="flex items-center gap-2"> <div class="w-6 h-6 rounded bg-primary bg-opacity-20 flex items-center justify-center"> <span class="material-symbols-outlined text-primary text-sm">code_blocks</span> </div> <span class="font-bold text-on-surface text-sm">SkillMD</span> </div> <div class="flex flex-wrap justify-center gap-6 text-sm"> <a href="/about" class="hover:text-primary transition-colors">About Us</a> <a href="/contact" class="hover:text-primary transition-colors">Contact Us</a> <a href="/privacy" class="hover:text-primary transition-colors">Privacy Policy</a> <a href="/terms" class="hover:text-primary transition-colors">Terms of Service</a> <a href="/support" class="hover:text-primary transition-colors">Support</a> </div> <div class="text-xs text-on-surface-variant/80"> © 2026 SkillMD. All rights reserved. </div> </div> </footer> </main> <!-- Script for Theme Toggle, Mobile Menu, and Sidebar Filter Redirection --> <script> // Theme setup const savedTheme = localStorage.getItem("theme") || "dark"; function applyTheme(theme) { document.documentElement.classList.remove("dark", "green", "dracula", "nord"); if (theme === "dark") { document.documentElement.classList.add("dark"); } else if (theme === "green") { document.documentElement.classList.add("dark", "green"); } else if (theme === "dracula") { document.documentElement.classList.add("dark", "dracula"); } else if (theme === "nord") { document.documentElement.classList.add("dark", "nord"); } document.documentElement.setAttribute("data-theme", theme); const themeMoon = document.getElementById("theme-moon"); const themeSun = document.getElementById("theme-sun"); const themeLeaf = document.getElementById("theme-leaf"); const themeDracula = document.getElementById("theme-dracula"); const themeNord = document.getElementById("theme-nord"); if (themeMoon && themeSun && themeLeaf && themeDracula && themeNord) { themeMoon.style.display = theme === "dark" ? "inline" : "none"; themeSun.style.display = theme === "light" ? "inline" : "none"; themeLeaf.style.display = theme === "green" ? "inline" : "none"; themeDracula.style.display = theme === "dracula" ? "inline" : "none"; themeNord.style.display = theme === "nord" ? "inline" : "none"; } } applyTheme(savedTheme); const themeToggleBtn = document.getElementById("theme-toggle-btn"); if (themeToggleBtn) { themeToggleBtn.addEventListener("click", () => { const currentTheme = document.documentElement.getAttribute("data-theme") || "dark"; let newTheme = "dark"; if (currentTheme === "dark") { newTheme = "light"; } else if (currentTheme === "light") { newTheme = "green"; } else if (currentTheme === "green") { newTheme = "dracula"; } else if (currentTheme === "dracula") { newTheme = "nord"; } else { newTheme = "dark"; } applyTheme(newTheme); localStorage.setItem("theme", newTheme); }); } // Mobile menu toggle and sidebar logic const mobileMenuToggle = document.getElementById("mobile-menu-toggle"); const sidebarMenu = document.getElementById("sidebar-menu"); const sidebarOverlay = document.getElementById("sidebar-overlay"); function isMobile() { return window.innerWidth < 768; // 768px is the 'md' breakpoint in Tailwind } function openSidebar() { if (sidebarMenu) { sidebarMenu.classList.remove("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.remove("hidden"); } } function closeSidebar() { if (sidebarMenu && isMobile()) { sidebarMenu.classList.add("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.add("hidden"); } } if (mobileMenuToggle && sidebarMenu) { mobileMenuToggle.addEventListener("click", (e) => { e.stopPropagation(); if (isMobile()) { const isClosed = sidebarMenu.classList.contains("-translate-x-full"); if (isClosed) { openSidebar(); } else { closeSidebar(); } } }); document.addEventListener("click", (e) => { if (isMobile()) { if (!sidebarMenu.contains(e.target) && !mobileMenuToggle.contains(e.target)) { closeSidebar(); } } }); if (sidebarOverlay) { sidebarOverlay.addEventListener("click", () => { if (isMobile()) { closeSidebar(); } }); } // Collapse sidebar when clicking a filter button, creator button, or nav item inside it sidebarMenu.addEventListener("click", (e) => { if (isMobile()) { const clickTarget = e.target.closest("button, a"); if (clickTarget) { closeSidebar(); } } }); // Sync sidebar state on window resize window.addEventListener("resize", () => { if (!isMobile()) { // Desktop: sidebar should be visible, no overlay if (sidebarMenu) { sidebarMenu.classList.remove("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.add("hidden"); } } else { // Mobile: start collapsed if (sidebarMenu) { sidebarMenu.classList.add("-translate-x-full"); } if (sidebarOverlay) { sidebarOverlay.classList.add("hidden"); } } }); } // If not on homepage, redirect on sidebar filter click const isHomepage = window.location.pathname === "/"; document.querySelectorAll("#occupation-filters .filter-btn").forEach(btn => { btn.addEventListener("click", (e) => { const occ = e.currentTarget.getAttribute("data-occupation"); if (!isHomepage) { window.location.href = occ ? `/?occupation=${encodeURIComponent(occ)}` : "/"; } }); }); document.querySelectorAll("#creator-filters .creator-btn").forEach(btn => { btn.addEventListener("click", (e) => { const creator = e.currentTarget.getAttribute("data-creator"); if (!isHomepage) { window.location.href = `/?creator=${encodeURIComponent(creator)}`; } }); }); </script> </body> </html>