diff --git a/.agents/skills/logseq-task-on-lambda/SKILL.md b/.agents/skills/logseq-task-on-lambda/SKILL.md index d1f5f8fa61..a7ab03a862 100644 --- a/.agents/skills/logseq-task-on-lambda/SKILL.md +++ b/.agents/skills/logseq-task-on-lambda/SKILL.md @@ -1,15 +1,19 @@ --- name: logseq-task-on-lambda -description: Use when Codex needs to execute or continue a task described by a Logseq block in the Lambda RTC graph. The request must provide a block UUID directly or as a double-bracket UUID reference. This skill validates sync state, fetches the target block tree with the Logseq CLI, handles in-review tasks with TODO #agent-steer guidance blocks, and then completes the task described by the active block tree. +description: Use when Codex needs to execute or continue a task described by a Logseq block in the Lambda RTC graph. The request may provide a block UUID directly or as a double-bracket UUID reference; when no UUID is provided, discover TODO tasks from today's Journal Page, ask the user which one to run, and then continue with the selected UUID. This skill validates sync state, fetches the target block tree with the Logseq CLI, handles in-review tasks with TODO #agent-steer guidance blocks, and then completes the task described by the active block tree. --- # Logseq Task On Lambda -Use one block in the `Lambda RTC` graph as the current task brief. The parent agent owns every `Lambda RTC` read, write, sync gate, and orchestration step. Load `.agents/skills/logseq-cli/SKILL.md` in the parent before running any ad hoc `logseq` command against `Lambda RTC`. +Use one block in the `Lambda RTC` graph as the current task brief. The parent agent owns every `Lambda RTC` read, write, sync gate, selection prompt, and orchestration step. Load `.agents/skills/logseq-cli/SKILL.md` in the parent before running any ad hoc `logseq` command against `Lambda RTC`. If the task explicitly requests a pull request, load the matching GitHub publishing skill for the current environment before staging, committing, pushing, or opening a PR. -## Fetch Script +## Selection and Fetch Scripts + +When the user does not provide a UUID, run `.agents/skills/logseq-task-on-lambda/scripts/list-today-todo-tasks.sh` from the repo root. Pass no arguments. + +The list script targets only `Lambda RTC`, starts sync at most once, waits up to 20 seconds for `ws-state=open`, `pending-local=0`, and `pending-server=0`, queries TODO tasks whose `:block/page` is today's Journal Page, validates the structured query result, prints numbered candidates with UUIDs, then runs `logseq show --graph "Lambda RTC" --uuid "$uuid" --level 100` for every candidate and prints each full block tree to stdout. It prints the journal day plus sync gate summary to stderr. Stop on any non-zero exit status. If no TODO tasks are found, report that and stop unless the user provides a UUID. Run `.agents/skills/logseq-task-on-lambda/scripts/fetch-task-block.sh UUID_OR_DOUBLE_BRACKET_UUID` from the repo root. Pass exactly one bare UUID or one double-bracket UUID reference. @@ -26,10 +30,11 @@ For every parent-owned `Lambda RTC` write: ## Workflow -1. Fetch the task block tree. - - Accept only one UUID in bare form, such as `11111111-1111-1111-1111-111111111111`, or double-bracket form, such as `[[11111111-1111-1111-1111-111111111111]]`. - - Reject page names, db ids, block refs with extra text, multiple UUIDs, malformed UUIDs, and empty input. - - Run the fetch script and treat stdout as the complete root block tree. +1. Resolve the task UUID and fetch the task block tree. + - If the request includes one UUID, accept only bare form, such as `11111111-1111-1111-1111-111111111111`, or double-bracket form, such as `[[11111111-1111-1111-1111-111111111111]]`. + - If the request includes no UUID, run the list script, present the numbered TODO task candidates to the user, and ask which task to solve. Accept only one listed index or one listed UUID from the user's answer. + - Reject page names, db ids, block refs with extra text, multiple UUIDs, malformed UUIDs, empty answers, and answers that do not select exactly one listed task. + - After resolving one UUID, run the fetch script and treat stdout as the complete root block tree. - Read stderr for `normalized-uuid` and `sync-gate`. 2. Select the active task brief. @@ -88,6 +93,7 @@ For every parent-owned `Lambda RTC` write: 10. Report the result. - Include the normalized UUID and sync gate values: `ws-state`, `pending-local`, and `pending-server`. + - If the UUID was selected from today's Journal Page, state which listed task was selected. - State that the root moved to `doing`, a completion summary was added, and the root moved to `in-review`. - State that step 5 was completed by a worker subagent, list the worker-reported skills, and confirm the parent handled all `Lambda RTC` graph interactions. - Report selected `#agent-steer` TODO UUIDs moved to `done`, or state that no steer guidance was completed. @@ -97,7 +103,8 @@ For every parent-owned `Lambda RTC` write: ## Hard Stops -- Never fetch block content before the fetch script reports a passed sync gate. +- Never fetch the selected block tree before the fetch script reports a passed sync gate. +- Never run no-UUID discovery from anywhere except today's Journal Page TODO list in `Lambda RTC` after the list script reports a passed sync gate. - Never redo a whole `in-review` root task when actionable `#agent-steer` TODO guidance exists. - Never create a PR unless the active task brief or current user request explicitly asks for one. - Never leave a created PR unrecorded on the root block's `GitHub Url` property. diff --git a/.agents/skills/logseq-task-on-lambda/agents/openai.yaml b/.agents/skills/logseq-task-on-lambda/agents/openai.yaml index be18236592..ef2cfa999e 100644 --- a/.agents/skills/logseq-task-on-lambda/agents/openai.yaml +++ b/.agents/skills/logseq-task-on-lambda/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Logseq Task On Lambda" short_description: "Run Lambda RTC block-described tasks" - default_prompt: "Use $logseq-task-on-lambda to run the task described by [[11111111-1111-1111-1111-111111111111]]." + default_prompt: "Use $logseq-task-on-lambda to run a Lambda RTC task; if no UUID is provided, list today's TODO tasks and ask me which one to solve." diff --git a/.agents/skills/logseq-task-on-lambda/scripts/list-today-todo-tasks.sh b/.agents/skills/logseq-task-on-lambda/scripts/list-today-todo-tasks.sh new file mode 100755 index 0000000000..40886b2fd9 --- /dev/null +++ b/.agents/skills/logseq-task-on-lambda/scripts/list-today-todo-tasks.sh @@ -0,0 +1,213 @@ +#!/usr/bin/env bash +set -euo pipefail + +GRAPH_NAME="Lambda RTC" +TIMEOUT_SECONDS=20 +LOGSEQ_BIN="${LOGSEQ_BIN:-logseq}" + +fail() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +usage() { + cat >&2 <<'EOF' +Usage: list-today-todo-tasks.sh + +List TODO task blocks from today's Journal Page in the Lambda RTC graph after +verifying that sync is open and idle. +EOF +} + +if [[ $# -ne 0 ]]; then + usage + fail "expected no arguments" +fi + +today_journal_day="$( + node -e ' +const now = new Date(); +const yyyy = String(now.getFullYear()); +const mm = String(now.getMonth() + 1).padStart(2, "0"); +const dd = String(now.getDate()).padStart(2, "0"); +console.log(`${yyyy}${mm}${dd}`); +' +)" + +assess_sync_status() { + node -e ' +const fs = require("fs"); + +function fail(message) { + console.error(message); + process.exit(1); +} + +let payload; +try { + payload = JSON.parse(fs.readFileSync(0, "utf8")); +} catch (error) { + fail(`sync status did not return valid JSON: ${error.message}`); +} + +if (payload.status !== "ok") { + fail(`sync status returned non-ok status: ${JSON.stringify(payload)}`); +} + +const data = payload.data; +if (!data || typeof data !== "object" || Array.isArray(data)) { + fail("sync status missing object data"); +} + +if (Object.prototype.hasOwnProperty.call(data, "last-error") && data["last-error"] != null) { + fail(`sync status reports last-error: ${JSON.stringify(data["last-error"])}`); +} + +const wsState = data["ws-state"]; +const graphId = data["graph-id"]; + +if (wsState !== "open" || typeof graphId !== "string" || graphId.length === 0) { + console.log("need-start"); + process.exit(0); +} + +const pendingLocal = data["pending-local"]; +const pendingServer = data["pending-server"]; +if (typeof pendingLocal !== "number" || !Number.isFinite(pendingLocal)) { + fail("sync status pending-local must be a number"); +} +if (typeof pendingServer !== "number" || !Number.isFinite(pendingServer)) { + fail("sync status pending-server must be a number"); +} + +const state = pendingLocal === 0 && pendingServer === 0 ? "complete" : "pending"; +console.log([state, wsState, pendingLocal, pendingServer].join("\t")); +' +} + +validate_and_print_tasks() { + node -e ' +const fs = require("fs"); +const taskUuidsFile = process.env.TASK_UUIDS_FILE; + +function fail(message) { + console.error(message); + process.exit(1); +} + +let payload; +try { + payload = JSON.parse(fs.readFileSync(0, "utf8")); +} catch (error) { + fail(`query did not return valid JSON: ${error.message}`); +} + +if (payload.status !== "ok") { + fail(`query returned non-ok status: ${JSON.stringify(payload)}`); +} + +const result = payload.data && payload.data.result; +if (!Array.isArray(result)) { + fail("query result missing result array"); +} + +if (typeof taskUuidsFile !== "string" || taskUuidsFile.length === 0) { + fail("TASK_UUIDS_FILE is required"); +} + +if (result.length === 0) { + console.log("No TODO tasks found on today\u0027s Journal Page."); + process.exit(0); +} + +console.log(`Found ${result.length} TODO task(s) on today\u0027s Journal Page:`); +const uuids = []; +result.forEach((task, index) => { + if (!task || typeof task !== "object" || Array.isArray(task)) { + fail(`query result item ${index + 1} is not an object`); + } + + const rawUuid = task["block/uuid"]; + const title = task["block/title"]; + if (typeof rawUuid !== "string" || rawUuid.length === 0) { + fail(`query result item ${index + 1} is missing block/uuid`); + } + if (typeof title !== "string" || title.length === 0) { + fail(`query result item ${index + 1} is missing block/title`); + } + + const uuid = rawUuid.toLowerCase(); + console.log(`${index + 1}. ${title}`); + console.log(` uuid: ${uuid}`); + uuids.push(uuid); +}); + +fs.writeFileSync(taskUuidsFile, `${uuids.join("\n")}\n`); +' +} + +run_status() { + "$LOGSEQ_BIN" sync status --graph "$GRAPH_NAME" --output json +} + +status_json="$(run_status)" || fail "sync status command failed" +assessment="$(printf '%s' "$status_json" | assess_sync_status)" || fail "sync status returned invalid state" + +if [[ "$assessment" == "need-start" ]]; then + "$LOGSEQ_BIN" sync start --graph "$GRAPH_NAME" >/dev/null || fail "sync start command failed" +fi + +deadline=$((SECONDS + TIMEOUT_SECONDS)) +while true; do + status_json="$(run_status)" || fail "sync status command failed" + assessment="$(printf '%s' "$status_json" | assess_sync_status)" || fail "sync status returned invalid state" + + if [[ "$assessment" == complete$'\t'* ]]; then + IFS=$'\t' read -r _ ws_state pending_local pending_server <<<"$assessment" + printf 'today-journal-day: %s\n' "$today_journal_day" >&2 + printf 'sync-gate: ws-state=%s pending-local=%s pending-server=%s\n' \ + "$ws_state" "$pending_local" "$pending_server" >&2 + break + fi + + if [[ "$assessment" == "need-start" ]]; then + fail "sync did not open after sync start" + fi + + if (( SECONDS >= deadline )); then + fail "sync status polling timed out before queues settled" + fi + + sleep 1 +done + +query='[:find [(pull ?e [:db/id :block/uuid :block/title :block/updated-at]) ...] + :in $ ?journal-day + :where + [?page :block/journal-day ?journal-day] + [?e :block/page ?page] + [?e :logseq.property/status ?status] + [?status :db/ident :logseq.property/status.todo] + [?e :block/title]]' + +query_json="$("$LOGSEQ_BIN" query --graph "$GRAPH_NAME" --query "$query" --inputs "[$today_journal_day]" --output json)" \ + || fail "query command failed" + +task_uuids_file="$(mktemp)" +trap 'rm -f "$task_uuids_file"' EXIT + +printf '%s' "$query_json" | TASK_UUIDS_FILE="$task_uuids_file" validate_and_print_tasks + +if [[ -s "$task_uuids_file" ]]; then + printf '\nBlock trees:\n' + task_index=1 + while IFS= read -r uuid; do + if [[ -z "$uuid" ]]; then + continue + fi + printf '\n--- Task %s: %s ---\n' "$task_index" "$uuid" + "$LOGSEQ_BIN" show --graph "$GRAPH_NAME" --uuid "$uuid" --level 100 \ + || fail "show command failed for task $uuid" + task_index=$((task_index + 1)) + done <"$task_uuids_file" +fi