diff --git a/.githooks/pre-push b/.githooks/pre-push index a298e381..0079d83f 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -19,6 +19,22 @@ if [ "$WORKLOG_SKIP_PRE_PUSH" = "1" ]; then exit 0 fi +# Skip when running inside a temp worktree created by withTempWorktree +# for internal sync operations. These worktrees don't have worklog +# initialized and the push is already done with --no-verify as defense. +case "$PWD" in + *tmp-worktree-*) + exit 0 + ;; +esac + +# Skip when inside a git worktree (not the main checkout). +# Worktrees are for feature development and typically don't have worklog +# initialized. The sync should run from the main checkout. +if [ "$(git rev-parse --git-dir 2>/dev/null)" != "$(git rev-parse --git-common-dir 2>/dev/null)" ]; then + exit 0 +fi + # Read stdin to check which refs are being pushed. # If pushing to refs/worklog/data, skip the sync to avoid infinite loops. skip=0 @@ -44,5 +60,8 @@ fi # Force the data branch to refs/worklog/data regardless of config. # This prevents the sync from accidentally pushing to a standard branch # if the user has overridden syncBranch in their worklog config. -"$WL" sync --git-branch refs/worklog/data +"$WL" sync --git-branch refs/worklog/data || { + echo "worklog: pre-push sync failed (pushing anyway)" >&2 + exit 0 +} exit 0 diff --git a/.github/workflows/install-and-smoke-test.yml b/.github/workflows/install-and-smoke-test.yml index 23a0a83a..4f5c0e32 100644 --- a/.github/workflows/install-and-smoke-test.yml +++ b/.github/workflows/install-and-smoke-test.yml @@ -19,7 +19,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "22" cache: "npm" - name: Install dependencies @@ -31,6 +31,11 @@ jobs: - name: Verify package builds run: npm pack + - name: Initialize Worklog + run: | + node ./dist/cli.js init --project-name "ContextHub" --prefix WL --agents-template skip --json > /dev/null + echo "✓ Worklog initialized" + - name: Run headless smoke test run: | # Verify the wl command is available and works @@ -39,30 +44,36 @@ jobs: # Verify the TUI module loads without errors (using tsx for TypeScript imports) npx tsx -e " - const { ChatPane } = await import('./packages/tui/extensions/chatPane.js'); - const { ActionPalette } = await import('./packages/tui/extensions/actionPalette.js'); - const { runWl } = await import('./packages/tui/extensions/wl-integration.js'); + (async () => { + const { ChatPane } = await import('./packages/tui/extensions/Worklog/chatPane.ts'); + const { ActionPalette } = await import('./packages/tui/extensions/Worklog/actionPalette.ts'); + const { runWl } = await import('./packages/tui/extensions/wl-integration.ts'); console.log('✓ TUI modules load successfully'); + })(); " # Verify chat pane can send messages npx tsx -e " - const { ChatPane } = await import('./packages/tui/extensions/chatPane.js'); + (async () => { + const { ChatPane } = await import('./packages/tui/extensions/Worklog/chatPane.ts'); const pane = new ChatPane(); pane.clear(); console.log('✓ ChatPane instantiated'); + })(); " # Verify action palette has default actions npx tsx -e " - const { ChatPane } = await import('./packages/tui/extensions/chatPane.js'); - const { ActionPalette } = await import('./packages/tui/extensions/actionPalette.js'); + (async () => { + const { ChatPane } = await import('./packages/tui/extensions/Worklog/chatPane.ts'); + const { ActionPalette } = await import('./packages/tui/extensions/Worklog/actionPalette.ts'); const chat = new ChatPane(); const palette = new ActionPalette(chat); palette.open(); const actions = palette.getFilteredActions(); if (actions.length < 5) throw new Error('Expected at least 5 default actions, got ' + actions.length); console.log('✓ ActionPalette has ' + actions.length + ' default actions'); + })(); " # Verify pi-audit module works diff --git a/.github/workflows/tui-tests.yml b/.github/workflows/tui-tests.yml index 470e373e..7267f9fe 100644 --- a/.github/workflows/tui-tests.yml +++ b/.github/workflows/tui-tests.yml @@ -13,7 +13,6 @@ jobs: outputs: cli: ${{ steps.filter.outputs.cli }} shared: ${{ steps.filter.outputs.shared }} - tui: ${{ steps.filter.outputs.tui }} steps: - name: Checkout uses: actions/checkout@v4 @@ -38,27 +37,16 @@ jobs: - 'src/github*.ts' - 'src/types.ts' - 'src/index.ts' - - 'tests/cli.test.ts' - 'tests/**/*.test.ts' - 'test/**/*.test.ts' - - 'tests/e2e/**' - tui: - - 'src/commands/tui.ts' - - 'src/tui/**' - - 'tests/tui/**/*.test.ts' - - 'test/tui-*.test.ts' - - 'tests/tui-ci-run.sh' - - 'vitest.tui.config.ts' - - 'Dockerfile.tui-tests' - - 'test-tui.sh' docs-only: runs-on: ubuntu-latest needs: changes - if: ${{ needs.changes.outputs.cli != 'true' && needs.changes.outputs.tui != 'true' && needs.changes.outputs.shared != 'true' }} + if: ${{ needs.changes.outputs.cli != 'true' && needs.changes.outputs.shared != 'true' }} steps: - name: No test changes detected - run: echo "Docs-only change; skipping CLI/TUI test jobs." + run: echo "Docs-only change; skipping CLI test jobs." cli-tests: runs-on: ubuntu-latest @@ -71,7 +59,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "22" cache: "npm" - name: Install dependencies @@ -80,25 +68,10 @@ jobs: - name: Build CLI run: npm run build + - name: Initialize Worklog + run: | + node ./dist/cli.js init --project-name "ContextHub" --prefix WL --agents-template skip --json > /dev/null + echo "✓ Worklog initialized" + - name: Run CLI tests run: npm test - - tui-tests: - runs-on: ubuntu-latest - needs: changes - if: ${{ needs.changes.outputs.tui == 'true' || needs.changes.outputs.shared == 'true' }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "npm" - - - name: Install dependencies - run: npm ci - - - name: Run headless TUI tests - run: bash ./tests/tui-ci-run.sh diff --git a/.implement_state.json b/.implement_state.json new file mode 100644 index 00000000..dd025f8a --- /dev/null +++ b/.implement_state.json @@ -0,0 +1,8 @@ +{ + "work_item_id": "WL-0MRSGMTYU009OZCF", + "worktree_path": "/home/rgardler/projects/ContextHub/.worklog/worktrees/wl-WL-0MRSGMTYU009OZCF-add-audit-file-to-wl-audit-set-for-file", + "repo_root": "/home/rgardler/projects/ContextHub", + "parent_branch": "dev", + "commit_msg": "", + "started_at": "2026-07-21T18:30:22Z" +} \ No newline at end of file diff --git a/.pi/settings.json b/.pi/settings.json index 12b6c405..2e130bb0 100644 --- a/.pi/settings.json +++ b/.pi/settings.json @@ -5,6 +5,7 @@ "context-hub": { "showActivityIndicator": true, "showHelpText": true, - "browseItemCount": 15 + "browseItemCount": 15, + "schedules": [] } } diff --git a/.pi/skills/heartbeat/SKILL.md b/.pi/skills/heartbeat/SKILL.md new file mode 100644 index 00000000..49632952 --- /dev/null +++ b/.pi/skills/heartbeat/SKILL.md @@ -0,0 +1,144 @@ +--- +name: heartbeat +description: "Automated work item monitoring — inspects the completed/in_review queue and either flags the next item for producer review (when sparse) or runs an audit on the first unverified item (when full). Trigger on queries like: '/skill:heartbeat'" +--- + +# Heartbeat Skill + +## Completion-Detection Gate + +The purpose of this gate is to prevent the heartbeat from interrupting work +in progress. However, the gate logic differs depending on **who** is invoking +the skill. + +### User invocation (`/skill:heartbeat`) + +When the skill is invoked by a **user** via `/skill:heartbeat`, **proceed +immediately**. The user is taking explicit control and does not need the gate. + +This is equivalent to passing `--force` to the script — no conversation-history +check is performed. + +### Agent-initiated invocation + +When the agent invokes the heartbeat **autonomously** (e.g., in an idle loop, +cron-like context, or without an explicit user request), check whether the +previous work has completed to avoid interrupting mid-process tasks: + +1. Review the conversation history up to (but not including) this + invocation. Find the **last assistant message** + (the most recent message from the Pi agent before this turn). +2. If that message **clearly states that a process has completed** + (e.g., "Work committed to dev", "Task complete", "All tests pass", + a work-item summary with "Work committed to dev", or any message that + unambiguously signals the end of a task), **proceed** to the heartbeat + logic below. +3. If the last assistant message **does not clearly indicate completion** + (e.g., it asks a question, reports an error, or is mid-process), + print the following and exit **without** taking any heartbeat action: + + ``` + No completed process detected — heartbeat taking no action. + ``` + + Do NOT run `./scripts/heartbeat.py` in this case. + +4. **Direct script invocation bypass:** When the script is run directly + (outside Pi, e.g., from CI or a terminal), there is no agent to perform + the gate check. In that case, pass `--force` to the script to bypass: + + ```bash + python3 skill/heartbeat/scripts/heartbeat.py --force + ``` + + The `--force` flag has no effect inside the script (the gate is an + agent-level concern); it is a documentation/handshake flag indicating + that the caller takes responsibility for the gate check. + +## Overview + +The heartbeat skill automates work item queue monitoring. It inspects the +completed/in_review queue and takes one of three actions depending on queue +state: + +- **Sparse queue (< 10 items):** Finds the next ready work item via `wl next` + and flags it for producer review (`needsProducerReview: true`). +- **Full queue (>= 10 items):** Finds the first item (by `sortIndex`) that + does **not** have a valid audit result ("Ready to close: Yes") and runs + `/skill:audit ` via the Pi framework on it. +- **All items ready:** Reports "Project is ready for producer review prior to + a new release". + +Only one audit is triggered per heartbeat invocation. + +## Invocation + +``` +/skill:heartbeat +``` + +Invoke via the Pi chat interface. Output is displayed directly in the chat. + +## Behavior + +After passing the completion-detection gate above: + +1. Run `python3 ./scripts/heartbeat.py` (or `python3 [--force]` for + standalone/automated use). +2. The script queries `wl list --status completed --stage in_review --json` + to count items. +3. **If count < 10 (sparse queue):** + - Call `wl next --json` to find the next ready work item. + - If found, set `needsProducerReview: true` via `wl update --needs-producer-review true`. + - Report which item was flagged. + - If no item returned by `wl next`, report gracefully. +4. **If count >= 10 (full queue):** + - Sort items by `sortIndex` ascending. + - For each item, check for a valid audit result via `wl audit-show --json`. + - An item is considered "ready" if its `rawOutput` starts with "Ready to close: Yes". + - Find the first non-ready item and run `/skill:audit ` via the Pi framework (`pi -p --mode json --no-session /skill:audit `). + - Only one item is audited per invocation. +5. **If all items have "Ready to close: Yes":** + - Report "Project is ready for producer review prior to a new release". + +## Inputs + +None (when invoked via `/skill:heartbeat`). The skill operates on the current +state of the Worklog database. + +The underlying script accepts: + +- `--force` — Bypass the completion-detection gate. Use when running the + script standalone (e.g., from CI, cron, or a scheduler that already + performs its own idle/completion checks). + +## Outputs + +Human-readable text printed to stdout (displayed in the Pi chat interface). + +## Exit Codes + +- 0 — Success (action taken or all items ready) +- 1 — Error (wl command failure, JSON parse error) + +## Dependencies + +- `wl` CLI (Worklog) — must be installed and in PATH +- `pi` CLI — must be installed and in PATH (for invoking `/skill:audit`) +- Audit skill — located at `~/.pi/agent/skills/audit/` +- Python 3 — for running the heartbeat script + +## Key Files + +- `skill/heartbeat/SKILL.md` — This file +- `skill/heartbeat/scripts/heartbeat.py` — Core implementation script +- `tests/skill/heartbeat/test_heartbeat.py` — Unit tests + +## Related Work Items + +- WL-0MLQ0ZHQE0JBX8Y6 — TUI filter shortcut for needsProducerReview +- WL-0MLGTWT4S1X4HDD9 — `--needs-producer-review` filter for `wl list` +- WL-0MR6XG7RX008AF2W — Closing question in audit skill output +- WL-0MLYTKTI20V31KYW — Structured audit report format +- WL-0MM347F9D1EGKLSQ — sortIndex selection with batch mode +- WL-0MRJATQJ900832IT — Completion-detection gate diff --git a/.pi/skills/heartbeat/__init__.py b/.pi/skills/heartbeat/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/.pi/skills/heartbeat/scripts/heartbeat.py b/.pi/skills/heartbeat/scripts/heartbeat.py new file mode 100644 index 00000000..d79ec5ea --- /dev/null +++ b/.pi/skills/heartbeat/scripts/heartbeat.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +""" +Heartbeat skill for automated work item monitoring. + +Invocation: /skill:heartbeat + +The Pi-agent-level gate check is documented in SKILL.md. This script is the +backend that runs after the gate passes. For standalone/automated use: + + python3 scripts/heartbeat.py [--force] + + --force Bypass the Pi-agent completion-detection gate. + Use when running from CI, cron, or a scheduler that already + performs its own idle checks. + +Behavior: +1. Get count of completed/in_review work items +2. If count < 10: call wl next, flag next item for producer review +3. If count >= 10: audit first non-ready item (by sortIndex) +4. If all items have "Ready to close: Yes", report ready +""" + +import argparse +import json +import subprocess +import sys + + +def run_wl(args): + """Run a wl command and return parsed JSON output. + + Args: + args: List of command arguments (e.g., ['list', '--status', 'completed', '--json']) + + Returns: + Parsed JSON dict from wl stdout. + + Raises: + RuntimeError: If the wl command fails (non-zero exit code). + json.JSONDecodeError: If wl output is not valid JSON. + """ + cmd = ['wl'] + args + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + raise RuntimeError( + f"wl command failed: {' '.join(cmd)}\n" + f"stderr: {result.stderr}" + ) + return json.loads(result.stdout) + + +def get_audit_result(item_id): + """Get the audit result text for a work item. + + Checks rawOutput first, then falls back to summary (in case rawOutput + was stored as null but summary contains the audit content). + + Args: + item_id: The work item ID. + + Returns: + The audit text (rawOutput or summary), or None if no audit exists + or the audit-show call fails. + """ + try: + data = run_wl(['audit-show', item_id, '--json']) + audit = data.get('audit') + if audit: + raw = audit.get('rawOutput') + if raw: + return raw + # Fallback: some audits store content in summary instead + summary = audit.get('summary') + if summary: + return summary + except (RuntimeError, json.JSONDecodeError): + pass + return None + + +def is_ready_to_close(raw_output): + """Check whether an audit result indicates the item is ready to close. + + Args: + raw_output: The rawOutput string from wl audit-show, or None. + + Returns: + True if raw_output starts with 'Ready to close: Yes' (ignoring + leading/trailing whitespace), False otherwise. + """ + if not raw_output: + return False + stripped = raw_output.strip() + return stripped.startswith('Ready to close: Yes') + + +def check_queue(): + """Main heartbeat decision logic. + + Returns: + A human-readable string summarizing the action taken (or error). + + This function does NOT raise exceptions on wl failures — it catches + RuntimeError and returns an error string. + """ + try: + # Step 1: Get count of completed/in_review items + data = run_wl(['list', '--status', 'completed', '--stage', 'in_review', '--json']) + items = data.get('workItems', []) + count = len(items) + + if count == 0: + return "No items in completed/in_review queue." + + if count < 10: + # Sparse queue — flag next item for producer review + next_data = run_wl(['next', '--json']) + next_item = next_data.get('workItem') + if next_item: + item_id = next_item['id'] + item_title = next_item.get('title', item_id) + run_wl(['update', item_id, '--needs-producer-review', 'true', '--json']) + return ( + f"Flagged {item_title} ({item_id}) for producer review.\n" + f"Queue has {count} completed/in_review items (below threshold of 10)." + ) + else: + return "No next item found to flag for review." + else: + # Full queue — find first non-ready item by sortIndex + items_sorted = sorted(items, key=lambda x: x.get('sortIndex', 0) or 0) + + for item in items_sorted: + item_id = item['id'] + item_title = item.get('title', item_id) + raw_output = get_audit_result(item_id) + if not is_ready_to_close(raw_output): + # Run audit via Pi framework's /skill:audit command + pi_cmd = [ + 'pi', + '-p', + '--mode', 'json', + '--no-session', + f'/skill:audit {item_id}', + ] + try: + pi_result = subprocess.run( + pi_cmd, capture_output=True, text=True, timeout=600 + ) + if pi_result.returncode != 0: + return ( + f"Audit failed for {item_title} ({item_id}):\n" + f"{pi_result.stderr}" + ) + except subprocess.TimeoutExpired: + return ( + f"Audit timed out for {item_title} ({item_id})." + ) + return ( + f"Running audit on {item_title} ({item_id})...\n" + f"Audit completed for {item_id}." + ) + + # All items ready + return "Project is ready for producer review prior to a new release." + + except RuntimeError as e: + return f"Heartbeat error: {e}" + except json.JSONDecodeError as e: + return f"Heartbeat error: Invalid JSON from wl command: {e}" + + +def parse_args(): + """Parse command-line arguments. + + Returns: + Parsed arguments namespace. + """ + parser = argparse.ArgumentParser( + description='Heartbeat skill for automated work item monitoring', + ) + parser.add_argument( + '--force', + action='store_true', + help='Bypass the Pi-agent completion-detection gate. Use when running ' + 'standalone (e.g., from CI, cron, or a scheduler that already ' + 'performs its own idle checks).', + ) + return parser.parse_args() + + +def main(): + """Entry point for command-line invocation.""" + args = parse_args() + result = check_queue() + print(result) + if result.startswith('Heartbeat error'): + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/CHANGELOG.md b/CHANGELOG.md index 6af3b124..072679a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,59 @@ # Changelog +## v1.0.4 (2026-07-26) +### Features +- Add periodic request scheduling to pi worklog plugin (WL-0MRHYQU1S009DJ9B) +- wl doctor upgrade must refresh .githooks/ committed hooks when core.hooksPath is set (WL-0MRH2HP8O001C52X) +- Allow reopening closed work items via sync when local update is newer (WL-0MRH2EE02008LRHU) +- Create a heartbeat skill for automated work item monitoring (WL-0MRIHB4Q100946QD) +- Change in_review icons and reuse audit column for requiresProducerReview flag (WL-0MRJ3TUPP002TSGI) +- Related Items is too broad (WL-0MRSGXYRI007R9L0) +- help line for chord commands too verbose (WL-0MRMG8P2X004V6YC) +- Integrate process lifecycle into withTempWorktree (WL-0MRTSP4BV002SJY7) +- CLI command wl cleanup-worktree (WL-0MRTSPCNZ001XWRN) +- CLI and exec auto-registration of spawned processes (WL-0MRTSOTF1002I8WZ) +- Process lifecycle utility module (WL-0MRTSOK8C009CDLA) +- Add completion-detection gate to heartbeat skill (WL-0MRJATQJ900832IT) +- Support arbitrary-depth chords in the Pi selection list (WL-0MRMC3SPH005YE6R) +- Add getChordByPrefix() to ShortcutRegistry (WL-0MRMCO4V00012ZQM) +- Arbitrary-depth chords in list view (WL-0MRMCO4VA009VQUX) +- Arbitrary-depth chords in detail view (WL-0MRMCO4WX0073W30) +- Show model status in pi quickly (WL-0MRO7LWIX001516P) +- Encode always-persist + always-verify as invariant in audit_runner.py (WL-0MRSGMTG300680YA) +- Add --audit-file to wl audit-set for file-based audit input (WL-0MRSGMTYU009OZCF) +- Show distinct icon when audit is stale but readyToClose is true (WL-0MS1OJZL7005CR7A) +### Bug Fixes +- wl sync corrupts dev branch when run from git worktree (WL-0MRIEPH3J006RAPL) +- RCA: wl sync pre-push hook destroyed Tableau-Card-Engine repository (WL-0MRCTZZ82000X7TM) +- Flaky test: github-assign-issue and github-label-events fail intermittently in full suite (WL-0MRSGHLIQ004QG6O) +- wl audit-set should not fail silently (WL-0MRMBRK750042QC9) +- Regression: Blocked items not auto-unblocked when all blockers are completed (WL-0MRNM8EBD000C2P1) +- [test-failure] tests/skill-path-conventions.test.ts - Skill: speak - should have no legacy skill/ path references — failing test (WL-0MRJB5YK3007XJ8B) +- [test-failure] Skill: audit > should have no legacy skill/ path references — failing test (WL-0MRJDBUG20086CE5) +- [test-failure] tests/cli/status.test.ts - should output human-readable format by default — failing test (WL-0MRJB6EVH005JJ3T) +- [test-failure] tests/extensions/worklog-browse-extension.test.ts - Stage filtering via /wl - getArgumentCompletions returns sorted stage values including shorthands, canonical names, and settings — failing test (WL-0MRJF09W3001DH0V) +- [test-failure] tests/extensions/worklog-browse-extension.test.ts - getArgumentCompletions returns sorted stage values — failing test (WL-0MRJB6OKZ004H056) +- Fix orphaned ContextHub node processes that never exit after worktree cleanup (WL-0MRP1SLZB005ZNYO) +- Shortcuts with multiple tokens only replace the first occurrence (WL-0MRNLF5UO0088ID2) +- [test-failure] shortcut-config.test.ts:loadShortcutConfig > loads valid entries from shortcuts.json — failing test (WL-0MRNW3CZY006K0LD) +- [test-failure] shortcut-config.test.ts:loadShortcutConfig > loads chord entries from shortcuts.json — failing test (WL-0MRNW3D11002Y35T) +- Reset resolved model on session_start to avoid stale display across sessions (WL-0MRZ1EIY5008VBE6) +- Activity indicator tests fail when run in full suite due to test interaction / state leaking (WL-0MRVZ4GM9005SAA9) +### Other +- Add --no-verify to temp worktree push in gitPushDataFileToBranch (WL-0MRIFV2OW002DDAN) +- Make pre-push hook safe in worktree and temp worktree contexts (WL-0MRIFWG8O005CLI0) +- Add integration tests for heartbeat skill (WL-0MRWRKVWM003YVAY) +- Add heartbeat skill reference to project README (WL-0MRWRL35R0039RS5) +- Comprehensive test: every --json command returns valid JSON with no preamble (WL-0MRJ2R8LJ003LA8V) +- Documentation for process lifecycle management (WL-0MRTSPLAX005Y8QY) +- Fix test suite collection: add conftest.py for PYTHONPATH (WL-0MRWRLJNV000T1E8) +- Tests: getChordByPrefix and arbitrary-depth dispatch (WL-0MRMCO4VB009HIJL) +- Depth-3 shortcuts and documentation (WL-0MRMCO4WL003MLMR) +- wl audit-set should fail hard when audit text is not persisted (WL-0MRSGMTYY0062WFJ) + ## v1.0.3 (2026-07-11) ### Features +- Add periodic request scheduling with cron expressions to Worklog pi extension (WL-0MRHYQU1S009DJ9B) - Extend doctor upgrade to refresh installed hooks from .githooks (WL-0MRDEM7OO005UB1H) - Session Health Extension for Pi Footer (WL-0MRDRZ32L00404D0) - proactively release leases (WL-0MRE6JDT3004OSTF) diff --git a/CLI.md b/CLI.md index 096ef106..61b6521e 100644 --- a/CLI.md +++ b/CLI.md @@ -255,6 +255,7 @@ Options: - `--ready-to-close ` — Whether the work item is ready to close (required). - `--summary ` — Human-readable summary of the audit. - `--raw-output ` — Machine-readable raw output from the audit tool. +- `--audit-file ` — Read audit raw output from a file (takes precedence over `--raw-output`). - `--author ` — Author of the audit (defaults to current user). - `--prefix ` — Override default ID prefix (optional). - `--json` — Output in JSON format. @@ -265,6 +266,7 @@ Examples: wl audit-set WL-ABC123 --ready-to-close yes --summary "All criteria met" wl audit-set WL-ABC123 --ready-to-close no --summary "Outstanding work items" --json wl audit-set WL-ABC123 --ready-to-close yes --author "bot" --raw-output "..." +wl audit-set WL-ABC123 --ready-to-close yes --audit-file report.md --summary "From file" ``` ### `delete` [options] @@ -940,6 +942,35 @@ Notes: - If the lock file is corrupted (unparseable metadata), `--force` is required to remove it. - If the lock is held by a still-running process, the command warns but still allows removal with confirmation or `--force`. +### `cleanup-worktree` [path] [options] + +Kill tracked processes for a worktree path. Used to clean up orphaned processes +that were spawned during worktree operations. + +Arguments: + +- `path` — Path to the worktree to clean up (required unless `--all` is used). + +Options: + +- `--all` — Kill tracked processes for all worktrees. +- `--force` — Use `SIGKILL` instead of `SIGTERM`. +- `--json` — Output machine-readable JSON. + +Examples: + +```sh +wl cleanup-worktree /path/to/worktree +wl cleanup-worktree --all +wl cleanup-worktree /path/to/worktree --force +wl --json cleanup-worktree /path/to/worktree +``` + +Notes: + +- Safe to run when no processes are tracked (no-op, exit 0). +- If neither a path nor `--all` is provided, the command prints an error and exits non-zero. + --- ## Plugins diff --git a/README.md b/README.md index 536df828..3ed543fa 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ A lightweight, Git-friendly issue tracker designed for AI agents and development - **Hierarchical Work Items**: Parent-child relationships for organizing epics, features, and tasks - **Plugin System**: Extend the CLI with custom commands (see [Plugin Guide](PLUGIN_GUIDE.md)) - **AI Agent Integration**: Built-in Pi agent with real-time streaming, interactive agent chat pane, and agent-driven action palette. +- **Heartbeat Skill**: Automated work item monitoring via the Pi agent — run `/skill:heartbeat` to flag items needing producer review or to audit completed items for closure readiness (see [skill/heartbeat/SKILL.md](skill/heartbeat/SKILL.md)). - **Multi-Project Support**: Custom prefixes for issue IDs per project ## Installation @@ -115,6 +116,7 @@ You can get a lot of value from using Worklog as a memory for your agents. But y | Document | Description | |----------|-------------| +| [Heartbeat Skill](skill/heartbeat/SKILL.md) | Automated work item monitoring and audit orchestration for the Pi agent | | [TUI.md](TUI.md) | Interactive terminal UI controls and features | | [PLUGIN_GUIDE.md](PLUGIN_GUIDE.md) | Plugin development guide and API reference | | [LOCAL_LLM.md](LOCAL_LLM.md) | Configure local LLM providers (Ollama, Foundry) | diff --git a/docs/AUDIT_STATUS.md b/docs/AUDIT_STATUS.md index ebd5eb3f..ab5a93fe 100644 --- a/docs/AUDIT_STATUS.md +++ b/docs/AUDIT_STATUS.md @@ -148,4 +148,13 @@ Fields: - Config: `auditWriteEnabled` controls whether audit writes are allowed. - Storage: audit data is stored in the `audit_results` table with foreign key constraints and CASCADE DELETE semantics. - Migration: Use `wl doctor upgrade --confirm` to apply schema migrations on existing databases. -- Tests: Unit and integration tests cover valid first-line parsing, invalid first-line errors, redaction, whitespace handling, CRUD operations on the `audit_results` table, migration backfill, and legacy column removal. \ No newline at end of file +- Tests: Unit and integration tests cover valid first-line parsing, invalid first-line errors, redaction, whitespace handling, CRUD operations on the `audit_results` table, migration backfill, and legacy column removal. + +### Error Behavior + +Both `wl audit-set` and `wl update --audit-text/--audit-file` now detect write failures (e.g., permissions issues, disk errors, database corruption) and return an error rather than silently succeeding: + +- In **JSON mode** (`--json`): outputs `{ "success": false, "error": "" }` with a non-zero exit code. +- In **human mode**: prints an error message to stderr and exits with code 1. + +Previously, these commands always returned `success: true` regardless of whether the data was actually persisted. \ No newline at end of file diff --git a/docs/PROCESS_LIFECYCLE.md b/docs/PROCESS_LIFECYCLE.md new file mode 100644 index 00000000..7a47754b --- /dev/null +++ b/docs/PROCESS_LIFECYCLE.md @@ -0,0 +1,172 @@ +# Process Lifecycle Management + +## Overview + +ContextHub's worktree operations (via `withTempWorktree`) spawn child processes (git commands, +CLI invocations) that can become orphaned — surviving beyond the lifecycle of the parent +worktree. The process lifecycle module (`src/process-lifecycle.ts`) provides PID tracking, +timeout enforcement, and cleanup utilities to prevent resource leaks. + +## Architecture + +The process lifecycle module is a **singleton** (module-level state) that manages: + +- A **registry** mapping worktree paths to sets of PIDs +- A **metadata store** mapping PIDs to their worktree path and registration timestamp +- A **watchdog timer** that periodically checks for stale PIDs and kills them + +### Core Data Flow + +``` +[Child Process Spawned] + │ + ▼ + registerProcess(pid, worktreePath) + │ + ▼ + [Registry: worktreePath → Set] + [Metadata: pid → {worktreePath, registeredAt}] + │ + ├─► killProcessesForWorktree(path, signal) + │ └─► process.kill(-pid, signal) [process group] + │ └─► process.kill(pid, signal) [fallback] + │ + ├─► killAllTracked(signal) + │ └─► kills all tracked processes + │ + └─► Watchdog (every 60s, default timeout 10 min) + └─► kills PIDs exceeding timeout threshold +``` + +## Key Concepts + +### Process Group Killing + +On POSIX systems (Linux, macOS), killing with a negative PID (`process.kill(-pid)`) targets +the entire process group. This ensures that not just the parent process, but all its children +are terminated. If the process group kill fails (EPERM — no permission, or ESRCH — no process +group), the module falls back to individual `process.kill(pid)`. + +### Worktree Context + +The `withinWorktreeContext()` / `contextExec()` API provides a stack-based context for +transparent PID registration. When a worktree context is active, any command executed via +`contextExec()` automatically registers its child PID against that worktree. + +### Watchdog Timer + +The watchdog timer runs on a configurable interval (default: 60 seconds) and checks all +tracked PIDs against a configurable timeout threshold (default: 10 minutes). PIDs that +exceed the threshold are killed automatically. The timer is `unref()`'d so it does not +prevent the Node.js process from exiting. + +## Exported Functions + +### Registration + +| Function | Description | +|----------|-------------| +| `registerProcess(pid, worktreePath)` | Register a PID against a worktree path | +| `registerCurrentProcess(worktreePath)` | Register the current Node.js PID | +| `createTrackedExec(worktreePath)` | Return a promisified `exec()` that auto-registers child PIDs | +| `detectWorktreeFromCwd(cwd?)` | Detect if a directory is inside a ContextHub worktree | + +### Cleanup + +| Function | Description | +|----------|-------------| +| `killProcessesForWorktree(worktreePath, signal?)` | Kill all tracked PIDs for a worktree | +| `killAllTracked(signal?)` | Kill all tracked PIDs across all worktrees | + +### Context Management + +| Function | Description | +|----------|-------------| +| `withinWorktreeContext(worktreePath)` | Set a worktree context (returns restore function) | +| `contextExec(command, options?)` | Execute a command in the current worktree context | + +### Watchdog + +| Function | Description | +|----------|-------------| +| `startWatchdog(checkIntervalMs?, timeoutMs?)` | Start/restart the watchdog timer | +| `shutdown()` | Stop the watchdog and clear all tracking state | +| `getWatchdogInterval()` | Get the current watchdog check interval | +| `getWatchdogTimeout()` | Get the current watchdog timeout threshold | +| `isWatchdogRunning()` | Check if the watchdog is active | + +### Introspection + +| Function | Description | +|----------|-------------| +| `getTrackedProcesses()` | Get a snapshot of all tracked processes by worktree | +| `getProcessMeta(pid)` | Get metadata (worktree, timestamp) for a specific PID | + +## CLI Integration + +### `wl cleanup-worktree` + +Management command for process cleanup: + +```bash +# Kill processes for a specific worktree +wl cleanup-worktree .worklog/worktrees/wl-ABC123 + +# Kill all tracked processes +wl cleanup-worktree --all + +# Use SIGKILL instead of SIGTERM +wl cleanup-worktree .worklog/worktrees/wl-ABC123 --force +wl cleanup-worktree --all --force + +# JSON output +wl --json cleanup-worktree .worklog/worktrees/wl-ABC123 +``` + +### Auto-registration + +When the `wl` CLI starts, it automatically registers its own PID if it detects it is +running inside a worktree directory (path contains `.worklog/worktrees/`). This ensures +the CLI process can be cleaned up when the worktree is removed. + +The `execAsync` functions in `src/sync.ts` and `src/commands/sync.ts` use the context-aware +`contextExec()` function from the process lifecycle module. When called inside a worktree +context (set by `withinWorktreeContext()`), child PIDs are automatically registered. + +## Integration with `withTempWorktree` + +The `withTempWorktree()` function in `src/sync.ts` has been enhanced: + +1. **Registration**: Before calling `run(worktreePath)`, the function sets the worktree + context via `withinWorktreeContext(worktreePath)`. Any child processes spawned inside + `run()` are automatically registered. + +2. **Cleanup**: In the `finally` block, `killProcessesForWorktree(worktreePath)` is called + BEFORE `git worktree remove --force`. This kills all tracked processes for the worktree + before the directory is removed. + +3. **Preservation**: Existing behavior is preserved when no processes are registered + (no-op, no errors). + +## Error Handling + +All cleanup functions handle edge cases gracefully: + +| Error | Handling | +|-------|----------| +| `ESRCH` (process already dead) | Silently ignored | +| `EPERM` (no permission to kill) | Silently ignored (process group → individual fallback) | +| Unknown worktree path | No-op | +| No tracked processes | No-op (registry stays empty) | +| Concurrent modification | Iterates over PID snapshots | + +## Testing + +Tests are in: +- `tests/process-lifecycle.test.ts` — Core module unit tests (32 tests) +- `tests/process-lifecycle-auto-register.test.ts` — Auto-registration tests (16 tests) +- `tests/sync-worktree-lifecycle.test.ts` — withTempWorktree integration tests (7 tests) +- `tests/cleanup-worktree.test.ts` — CLI command tests (9 tests) + +All tests mock `process.kill` to verify the module's behavior without spawning +real child processes. diff --git a/docs/dependency-reconciliation.md b/docs/dependency-reconciliation.md index e1e49adf..f935058b 100644 --- a/docs/dependency-reconciliation.md +++ b/docs/dependency-reconciliation.md @@ -8,29 +8,31 @@ When a work item's status or stage changes, the database layer automatically rec ## Key Functions -All reconciliation logic lives in `src/database.ts`: +All reconciliation logic lives in `packages/shared/src/database.ts`: | Function | Line | Purpose | |---|---|---| -| `reconcileDependentsForTarget(targetId)` | ~1811 | Entry point: finds all dependents of `targetId` and reconciles each one | -| `reconcileDependentStatus(dependentId)` | ~1772 | Determines whether a dependent should be blocked or unblocked | -| `reconcileBlockedStatus(itemId)` | ~1749 | Sets or clears `blocked` status based on active blockers | -| `isDependencyActive(item)` | ~1701 | Returns `true` if an item is an active blocker (not completed, not deleted, not in `in_review` or `done` stage) | -| `hasActiveBlockers(itemId)` | ~1738 | Returns `true` if any inbound dependency edges point to active items | -| `getInboundDependents(targetId)` | ~1726 | Returns IDs of items that depend on `targetId` | -| `listDependencyEdgesTo(targetId)` | ~1696 | Returns all dependency edges where `targetId` is the prerequisite | +| `reconcileDependentsForTarget(targetId)` | ~2587 | Entry point: finds all dependents of `targetId` and reconciles each one | +| `reconcileDependentStatus(dependentId)` | ~2544 | Determines whether a dependent should be blocked or unblocked | +| `reconcileBlockedStatus(itemId)` | ~2522 | Sets or clears `blocked` status based on active blockers | +| `isDependencyActive(item)` | ~2455 | Returns `true` if an item is an active blocker (not completed, not deleted, not in `in_review` or `done` stage) | +| `hasActiveBlockers(itemId)` | ~2511 | Returns `true` if any inbound dependency edges point to active items | +| `getInboundDependents(targetId)` | ~2499 | Returns IDs of items that depend on `targetId` | +| `listDependencyEdgesTo(targetId)` | ~2451 | Returns all dependency edges where `targetId` is the prerequisite | ## How It Works -1. **Trigger**: `db.update()` (line ~655) and `db.delete()` (line ~688) check whether the status or stage changed. If so, they call `reconcileDependentsForTarget(itemId)`. +1. **Trigger**: `db.update()` (line ~1076) and `db.deleteSingle()` (line ~1143) check whether the status or stage changed. If so, they call `reconcileDependentStatus(id)` to reconcile the item itself, then `reconcileDependentsForTarget(itemId)` to reconcile the item's dependents. -2. **Fan-out**: `reconcileDependentsForTarget()` finds all items that depend on the changed item using `getInboundDependents()`. +2. **Self-reconciliation**: `reconcileDependentStatus(id)` checks if the item itself should be blocked or unblocked based on its own dependency edges. For example, when a completed item is reopened and its prerequisite is still active, it gets re-blocked. -3. **Per-dependent check**: For each dependent, `reconcileDependentStatus()` calls `hasActiveBlockers()` to determine if any remaining blockers are still active. +3. **Fan-out**: `reconcileDependentsForTarget()` finds all items that depend on the changed item using `getInboundDependents()`. -4. **Status update**: If no active blockers remain and the dependent is currently `blocked`, its status is set to `open`. If active blockers exist and the dependent is not already `blocked`, its status is set to `blocked`. +4. **Per-dependent check**: For each dependent, `reconcileDependentStatus()` calls `hasActiveBlockers()` to determine if any remaining blockers are still active. -5. **Cascade**: The status update on the dependent itself triggers another round of reconciliation, so chain dependencies (A blocks B blocks C) resolve transitively. +5. **Status update**: If no active blockers remain and the dependent is currently `blocked`, its status is set to `open`. If active blockers exist and the dependent is not already `blocked`, its status is set to `blocked`. + +6. **Cascade**: The status update on the dependent itself triggers another round of reconciliation via `saveWorkItem()`, so chain dependencies (A blocks B blocks C) resolve transitively. ## Behaviour Summary diff --git a/docs/icons-design.md b/docs/icons-design.md index b0ff2bd7..f21c92df 100644 --- a/docs/icons-design.md +++ b/docs/icons-design.md @@ -56,6 +56,40 @@ across the CLI (chalk) and TUI rendering paths. It covers: | no | `❌` | `[NO]` | "Audit: Failed" | | unknown | `❓` | `[UNKN]` | "Audit: Not run" | +## 3a. Stale Audit Result Icons + +| State | Icon | Text Fallback | Accessible Label | +|-------------------------------|--------|-----------------|-------------------------------| +| Audit passed (stale) | `🟩` | `[YES_STALE]` | "Audit: Passed (stale)" | + +When an audit result is `readyToClose: true` but the audit timestamp is stale +(more than 60 seconds before `updatedAt`), the stale-passed icon is displayed +in column 2 instead of the stage icon. This preserves the information that +audit passed even after subsequent minor updates made the audit appear stale. + +The stale-passed icon only applies to `in_review` items. The regular audit +icons (✅ / ❌ / ❔) are used for fresh audits, and the stage icon (🔍) is +used when no audit exists or when the audit is stale with `readyToClose: false`. + +The stale-passed icon was chosen to be visually distinct from: +- ✅ (fresh audit passed, green check mark) +- ❌ (fresh audit failed, red cross) +- 🔍 (stage icon, when no audit or stale without pass) +- ❔ (unknown/not run) + +🟩 (green square button, U+1F7E9) has a distinct shape from all of these, +and its green colour still conveys a positive (passed) result even when the +check mark is not shown. + +## 3b. Producer Review Flag Icons + +| State | Icon | Text Fallback | Accessible Label | +|--------------------------|--------|---------------------|-------------------------------| +| Needs producer review | `❌` | `[NEEDS_PRODUCER]` | "Needs producer review" | +| Producer review complete | `✅` | `[PRODUCER_OK]` | "Producer review complete" | + +The producer review flag is always shown in the third icon column of the TUI selection list, replacing the audit result icon for all stages. + ## 4. Epic Icons | Type | Icon | Text Fallback | Accessible Label | Visual Meaning | @@ -259,23 +293,57 @@ export function iconsEnabled(opts?: { noIcons?: boolean }): boolean; ### 13.1 Pi TUI List Rendering (`packages/tui/extensions/index.ts`) -The Pi TUI browse selection list renders status, stage, and audit result icons -before the title in each row. For epic items (`issueType === 'epic'`), an epic -icon and child count are also displayed: +The Pi TUI browse selection list renders status, stage (or audit-aware icon +for `in_review`), and producer review flag icons before the title in each row. +The third column (previously audit result) now shows the producer review flag +for all stages. The layout is: + +- **Column 1**: Status icon (🔓 open, 🔄 in-progress, ✔️ completed, etc.) +- **Column 2**: Stage icon (💡 idea, 📥 intake, 📋 plan, 🛠️ progress, 🏁 done) + For `in_review` stage, this column becomes audit-aware: + - 🟩 (stale-passed icon) — if the audit is stale but readyToClose === true + (auditedAt <= updatedAt - 60 seconds, but auditResult === true) + - 🔍 (stage icon) — if no audit exists, or the audit is stale + with readyToClose !== true + - ✅ — if a fresh audit exists and readyToClose === true + - ❌ — if a fresh audit exists and readyToClose === false +- **Column 3**: Producer review flag (❌ needs review, ✅ review complete) + Replaces the audit result icon for all stages. +- **Column 4 (optional)**: Epic icon + child count for epic items + +Examples: ``` 🔄 🛠️ ✅ 🏰(5) Epic feature name ← when icons enabled -[INPR][PROG][YES][EPIC](5) Epic feature name ← when fallback +[INPR][PROG][PRODUCER_OK][EPIC](5) Epic feature name ← when fallback + +🔄 🔍 ✅ 🏰 Epic feature name ← in_review, no audit +[INPR][REVIEW][PRODUCER_OK][EPIC] Epic feature name ← when fallback + +🔄 🟩 ✅ 🏰 Epic feature name ← in_review, stale audit but passed +[INPR][YES_STALE][PRODUCER_OK][EPIC] Epic feature name ← when fallback + +🔄 ✅ ❌ Regular task ← in_review, fresh audit pass, needs producer review +[INPR][YES][NEEDS_PRODUCER] Regular task ← when fallback ``` -When the child count is 0 or undefined, the epic icon is shown without a count: +### Audit Staleness + +The staleness check uses a 60-second buffer to prevent the audit's own +timestamp from falsely appearing as "fresh": ``` -🔄 🛠️ ✅ 🏰 Epic feature name ← epic with no children +audit is fresh when: auditedAt > updatedAt - 60000 (milliseconds) +audit is stale when: auditedAt <= updatedAt - 60000 ``` +When no audit exists or the audit is stale without a pass result +(`auditResult !== true`), column 2 shows the normal `in_review` stage icon +(🔍 / `[REVIEW]`). When the audit is stale but `readyToClose === true`, +column 2 shows the stale-passed icon (🟩 / `[YES_STALE]`). + The `formatBrowseOption` function prepends the icons before the title. -The icon prefix (status + stage + audit + optional epic icon/child count) +The icon prefix (status + stage/producer + optional epic icon/child count) is padded to a fixed visible width via per-list dynamic padding so that titles start at the same column position across all rows. The padding is computed as the maximum icon prefix width across all items in the current diff --git a/package-lock.json b/package-lock.json index 6eecfa45..56093467 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,16 @@ { "name": "worklog", - "version": "1.0.2", + "version": "1.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "worklog", - "version": "1.0.2", + "version": "1.0.4", "license": "MIT", + "workspaces": [ + "packages/shared" + ], "dependencies": { "@worklog/shared": "file:./packages/shared", "better-sqlite3": "^12.6.2", @@ -3117,12 +3120,8 @@ } }, "node_modules/@worklog/shared": { - "version": "1.0.0", - "resolved": "file:packages/shared", - "license": "MIT", - "dependencies": { - "better-sqlite3": "^12.6.2" - } + "resolved": "packages/shared", + "link": true }, "node_modules/accepts": { "version": "1.3.8", @@ -5215,6 +5214,18 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" + }, + "packages/shared": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "better-sqlite3": "^12.6.2" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/node": "^20.10.5", + "typescript": "^5.3.3" + } } } } diff --git a/package.json b/package.json index 25a48587..32299a2b 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "worklog", - "version": "1.0.3", + "version": "1.0.4", "description": "A simple experimental issue tracker for AI agents", "main": "dist/index.js", "type": "module", + "workspaces": ["packages/shared"], "bin": { "worklog": "./dist/cli.js", "wl": "./dist/cli.js" diff --git a/packages/shared/src/database.ts b/packages/shared/src/database.ts index 628e5f02..fd9cd9d5 100644 --- a/packages/shared/src/database.ts +++ b/packages/shared/src/database.ts @@ -1074,6 +1074,11 @@ export class WorklogDatabase { this.triggerAutoSync(); if (previousStatus !== updated.status || previousStage !== updated.stage) { + // Reconcile the item itself (e.g., re-block when reopened while + // active blockers still exist, or unblock when all blockers completed + // if the item was previously manually blocked without blockers). + this.reconcileDependentStatus(id); + // Reconcile all items that depend on this item if (this.listDependencyEdgesTo(id).length > 0) { this.reconcileDependentsForTarget(id); } diff --git a/packages/shared/src/persistent-store.ts b/packages/shared/src/persistent-store.ts index c1e3b47e..beeb43d5 100644 --- a/packages/shared/src/persistent-store.ts +++ b/packages/shared/src/persistent-store.ts @@ -1075,7 +1075,10 @@ export class SqlitePersistentStore { audit.author ?? null, ]; const normalized = normalizeSqliteBindings(values); - stmt.run(...normalized); + const result = stmt.run(...normalized); + if (result.changes === 0) { + throw new Error(`Audit result could not be persisted for work item ${audit.workItemId}`); + } } /** @@ -1146,11 +1149,18 @@ export class SqlitePersistentStore { ]; return normalizeSqliteBindings(values); }); + const failed: string[] = []; this.db.transaction(() => { for (const values of normalized) { - stmt.run(...values); + const result = stmt.run(...values); + if (result.changes === 0) { + failed.push(values[0] as string); + } } })(); + if (failed.length > 0) { + throw new Error(`Audit results could not be persisted for work items: ${failed.join(', ')}`); + } } // ── FTS5 Full-Text Search ────────────────────────────────────────── diff --git a/packages/tui/extensions/README.md b/packages/tui/extensions/README.md index 66f85dfb..7dccc410 100644 --- a/packages/tui/extensions/README.md +++ b/packages/tui/extensions/README.md @@ -196,7 +196,7 @@ The footer uses a **three-section layout**: **left** (status + elapsed time sinc | **Total session time** | Total wall-clock session duration (e.g., `Total: 5m 42s`) — shown in the center section | | **Token usage** | Input/output token counts (e.g., `↑1.2k ↓4.5k`) | | **Context usage** | Percentage of context window (e.g., `76.8%/128k`) | -| **Model ID** | Currently active model (e.g., `gpt-4`) | +| **Model ID** | Currently active model (e.g., `gpt-4`). While a model alias is selected but no resolved provider/model has been received yet, shows `{alias} → (resolving)` (e.g., `code → (resolving)`). | ### Colour Coding @@ -210,13 +210,15 @@ The response age indicator uses colour coding to provide at-a-glance health: ### Layout -The footer spans two lines. The first line shows extension status entries -(e.g., resolved provider/model, activity indicator). The second line shows -session health metrics in a **three-section layout**: +The footer spans three lines. The first line shows extension status entries +(e.g., activity indicator). The second line shows session health metrics in +a **three-section layout**. The third line shows the model/provider info and +(optionally) an initial prompt preview: ``` -openai/gpt-4 ⏵ /wl ← Extension statuses -● Streaming 45s #5 (3s ago) Total: 5m 42s ↑1.2k ↓4.5k 39.1%/128k ← Session health +⏵ /wl ← Extension statuses +● Streaming 45s #5 Total: 5m 42s ↑1.2k ↓4.5k 39.1%/128k ← Session health +code → openai/gpt-4 │ Fix the bug ← Model + prompt │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └────────────── Context usage │ │ │ │ │ │ │ └────────────────────── Output tokens @@ -228,6 +230,21 @@ openai/gpt-4 ⏵ /wl └────────────────────────────────────────────────────────────────────────── Status marker ``` +### Model/Provider Display (Line 3) + +The third footer line shows the model alias and resolved provider/model in +dimmed text. The display varies depending on available state: + +| State | Example | +|-------|---------| +| Model alias selected, resolved provider/model received | `code → openai/gpt-4` | +| Model alias selected, waiting for resolution | `code → (resolving)` | +| No model alias, resolved provider/model available | `openai/gpt-4` | +| No model info at all | `—` | + +When an initial prompt preview is available, it is shown after the model +info separated by a vertical bar (e.g., `code → openai/gpt-4 │ Fix the bug`). + During **idle** or **tool execution**, the last-chunk timer is not shown. ### Event Tracking @@ -447,6 +464,7 @@ The `/wl` slash command browses work items recommended by the `wl next` algorith ``` /wl # Show unfiltered work items (count from settings) /wl settings # Open the settings overlay +/wl schedule # Manage periodic request schedules (see Periodic Request Scheduler) /wl idea # Show items in idea stage /wl intake # Show items in intake_complete stage /wl plan # Show items in plan_complete stage @@ -473,13 +491,15 @@ Typing an unrecognised stage value produces an error notification and falls back ### Autocomplete -The `/wl` command registers `getArgumentCompletions`, so Pi's editor shows autocomplete suggestions for valid stage values (both shorthand and canonical) when typing arguments. +The `/wl` command registers `getArgumentCompletions`, so Pi's editor shows autocomplete suggestions for valid stage values (both shorthand and canonical) and the `settings`/`schedule` commands when typing arguments. ### Example - `/wl progress` — filters to items in `in_progress` stage - `/wl in_review` — filters to items in `in_review` stage - `/wl settings` — opens the settings overlay +- `/wl schedule list` — lists all configured periodic request schedules +- `/wl schedule add "0 1 * * *" "Daily audit"` — adds a daily audit schedule - `/wl` — shows the default unfiltered items (count from settings) - `/wl ` — whitespace-only arguments are treated as "no arguments" and show unfiltered items @@ -728,4 +748,111 @@ releases the previous session's model lease when a new Pi session is created - Results are cached per-extension-lifecycle to avoid repeated filesystem reads. - Registered in `Worklog/index.ts` via `registerLeaseRelease(pi)`. - Tests are in `Worklog/lease-release.test.ts`. + +## Periodic Request Scheduler + +The extension includes a **periodic request scheduler** that automatically submits pi requests on a recurring schedule using cron expressions. This is useful for automating recurring tasks such as hourly audits, daily intake checks, or nightly cleanup while the pi session is running. + +Requests are submitted through pi's normal message flow via `pi.sendUserMessage()`, making both the request and its response fully visible in the session history. + +### How It Works + +1. **Background ticker**: The scheduler runs a background interval (every 30 seconds by default) that checks all configured schedules. +2. **Cron matching**: For each enabled schedule, the current time is checked against the cron expression. If the expression matches the current minute, the request is a candidate for submission. +3. **Idle check**: Before submitting, the scheduler verifies that the pi agent is idle (no active streaming or tool execution) to avoid interrupting active work. If the agent is busy, the scheduled run is skipped for that minute. +4. **Duplicate prevention**: Once a schedule fires, it will not fire again within the same minute, preventing duplicate submissions. +5. **Visibility**: The request is submitted via `pi.sendUserMessage()`, so it appears as a user message in the session log alongside any pi responses. + +### The `/wl schedule` Command + +Manage configured schedules via the `/wl schedule` command. Autocomplete for `schedule` is available when typing `/wl ` in Pi's editor. + +#### Subcommands + +| Subcommand | Description | +|------------|-------------| +| `list` | List all configured schedules with their ID, cron expression, request text, label, and enabled status | +| `add ` | Add a new schedule with a cron expression and pi request text. Optionally add `--label