Skip to content

chore: add echo-secret script - #9

Open
kevinslin wants to merge 76 commits into
mainfrom
dev/2026-02-01-test-issue-echo-secret
Open

chore: add echo-secret script#9
kevinslin wants to merge 76 commits into
mainfrom
dev/2026-02-01-test-issue-echo-secret

Conversation

@kevinslin

@kevinslin kevinslin commented Feb 2, 2026

Copy link
Copy Markdown
Owner

chore: add echo-secret script

Context

Adds scripts/echo-secret.sh that outputs 42 and records the execution plan for Issue #7.
Related: #7

Testing

  • ./scripts/echo-secret.sh

Summary by CodeRabbit

  • New Features

    • Introduced dev.watch tool for monitoring GitHub Projects and automatically triggering tasks when items transition to Todo status.
    • Added merge-pr shortcut for streamlined pull request merging workflow.
    • Enhanced workflow automation to track git-issue status updates and link pull requests to issues.
  • Documentation

    • Added comprehensive guides for dev.watch configuration, usage, and GitHub API integration.
    • Updated task prompt documentation with new working directory workflow and pattern consolidation approach.
  • Improvements

    • Refined Plan and Verify steps in dev.loop for improved user guidance.
    • Clarified shortcut triggering mechanisms across the platform.

kevinlin-openai and others added 30 commits January 12, 2026 10:44
…cs-in-dev-research

feat(dev.research): add vendor docs support
@coderabbitai

coderabbitai Bot commented Feb 2, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces a new dev.watch module for monitoring GitHub Projects v2, adds automation shortcuts for PR merging, refines control flow in dev.loop and dev.do workflows, and refactors Codex documentation to emphasize progress logging and pattern consolidation.

Changes

Cohort / File(s) Summary
Dev.watch Infrastructure
active/dev.watch/SKILL.md, active/dev.watch/scripts/dev_watch.py, active/dev.watch/scripts/loops.sh, active/dev.watch/scripts/post_issue_comment.py
Introduces new dev.watch module with GraphQL-based GitHub Projects v2 polling, event emission for Todo transitions, and integration with loops.sh for automated task execution. Includes helpers for issue comment posting and state tracking.
Dev.watch Documentation & Examples
active/dev.watch/references/config.md, active/dev.watch/references/dev_watch_usage.md, active/dev.watch/assets/project_example.json
Comprehensive documentation of dev.watch configuration format, CLI usage, and example Project v2 JSON structures with field values and status tracking.
Dev.shortcuts Updates
active/dev.shortcuts/SKILL.md, active/dev.shortcuts/shortcuts/merge-pr.md, active/dev.shortcuts/shortcuts/fix-pr.md
Clarifies shortcut triggering via @shortcut and trigger keywords. Adds new merge-pr shortcut with five-step workflow for remote PR merging. Refines fix-pr worktree logic with conditional bypass when already on correct branch.
Deleted Shortcut Reference
active/dev.shortcuts/references/shortcut:merge-feature.md
Removes legacy merge-feature guidance documentation.
Workflow Skill Updates
active/dev.do/SKILL.md, active/dev.loop/SKILL.md
Updates dev.do to transition git-issue status to "In progress" and include issue URL in PR creation. Refines dev.loop Plan step to respect explicit check-with-user directives and restructures Verify step with explicit commit, trigger:push-pr, trigger:check-ci sequencing.
Codex Documentation Refactoring
references/codex.md
Significant shift from file-based prompt management to progress-logging and pattern-consolidation workflow. Replaces prompt creation steps with environment-tied progress.txt logging and introduces structured learnings blocks. Updates AGENTS.md integration guidance.
Execution Plans & Test Artifacts
docs/specs/active/2026-02-01-trigger-loops-sh-on-task-detection.md, docs/specs/active/2026-02-01-test-issue-echo-secret.md, scripts/echo-secret.sh
Documents technical approach for dev.watch and loops.sh integration, execution plan milestones, and risk mitigations. Adds simple test script outputting "42".

Sequence Diagram

sequenceDiagram
    participant Poller as dev_watch.py<br/>(Polling)
    participant API as GitHub API<br/>(GraphQL)
    participant State as State File<br/>(Persistence)
    participant Loops as loops.sh<br/>(Execution)
    participant Codex as Codex<br/>(Task Processing)

    Poller->>Poller: Load config & state
    Poller->>API: GraphQL query for Projects v2
    API-->>Poller: Items + statuses
    Poller->>Poller: Parse URLs, extract issue metadata
    Poller->>Poller: Compare against state (detect transitions)
    Poller->>State: Aggregate events (Todo transitions)
    State-->>Poller: Updated state
    
    alt Events detected
        Poller->>Loops: For each event: build task string
        Loops->>Loops: Resolve repo context
        Loops->>Codex: Stream prompt + task + repo info
        Codex-->>Loops: Processing result
        Loops-->>Poller: Execution status
    end
    
    Poller->>Poller: Emit JSON (timestamp, events, errors)
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly Related PRs

  • feat: add shortcut promotion workflow #3: Modifies overlapping documentation files in active/dev.do, active/dev.loop, and active/dev.shortcuts that are also affected by this PR, suggesting related workflow evolution.

Poem

🐰 A rabbit hops through GitHub's Projects with glee,
Watching todos bloom, triggering loops so spry,
Each transition caught, each task to decree,
Shortcuts and states in harmony align,
Progress logs guide the patterns divine! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'chore: add echo-secret script' accurately describes the main change: adding a new bash script file. However, the pull request contains numerous additional changes beyond just this script, including documentation updates, workflow modifications, and new tools that are not reflected in the title. Clarify whether the PR's primary objective is the echo-secret script addition, or if the title should be updated to reflect the broader set of changes (dev.watch tool implementation, dev.loop/dev.do workflow updates, etc.).
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev/2026-02-01-test-issue-echo-secret

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d7df58fba6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +10 to +11
"repository": {"nameWithOwner": "kevinslin/notion-agent-skill"},
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove trailing commas to keep JSON example valid

The sample data uses trailing commas (e.g., after the repository object), which makes the file invalid JSON. Any user who tries to load this example with json.load (or uses it as a template for config/test fixtures) will get a parse error. Since this file is presented as .json, it should be syntactically valid JSON without trailing commas.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@active/dev.watch/assets/project_example.json`:
- Around line 1-177: The JSON file contains trailing commas after objects and
array elements (e.g., after repository objects like "repository":
{"nameWithOwner": "kevinslin/notion-agent-skill"},, after item objects with
"fieldValues": {...}, and after elements inside the top-level array), which
makes the file invalid; remove all trailing commas across the file so every
object property and array element ends without a comma (check entries with keys
"id", "content", "fieldValues", the nested "repository" objects, and the "nodes"
arrays) and revalidate with a JSON parser (e.g., python -m json.tool) to ensure
the top-level array and all nested objects are syntactically correct.

In `@active/dev.watch/references/config.md`:
- Around line 5-8: Fix the grammar and the documented default config filename:
change the phrase "SKILL_ROOT by located in ~/.llm/skills/dev.watch" to
"SKILL_ROOT is located in ~/.llm/skills/dev.watch" and update the referenced
config path from "$SKILL_ROOT/config.json" to "$SKILL_ROOT/dev.watch.json" to
match the code default (dev.watch.json); ensure the subsequent sentence that
instructs creating a JSON file clarifies the default filename (default:
`dev.watch.json`) and retains the example structure heading.

In `@active/dev.watch/scripts/dev_watch.py`:
- Around line 273-312: In run_loops, subprocess.run is called with check=False
which ignores the child process exit status; update the call so the function
propagates loops.sh failures by either using check=True and catching
subprocess.CalledProcessError (return non-zero on exception) or by keeping
check=False and inspecting the CompletedProcess.returncode to return that code;
ensure you reference the cmd/env used for the call, adjust the except block to
handle CalledProcessError (or use the returncode path) and return a non-zero
exit code when loops.sh fails so run_loops reflects the script's real status.
- Around line 315-371: The main() flow currently assumes load_json(config_path)
and load_json(state_path) succeed; wrap those calls to catch JSONDecodeError
(and generic ValueError) from malformed JSON: when loading config (config =
load_json(config_path)) catch the error, eprint a clear message including
config_path and the error text, and return 2; when loading state (state =
load_json(state_path, default={}) or {}) catch the error, eprint a message
including state_path and the error, and recover by setting state = {}
(optionally back up or overwrite the corrupt file) so the watcher continues;
update references to config and state accordingly and ensure subsequent code
(e.g., save_json(state_path, state)) still runs with the recovered state.

In `@active/dev.watch/scripts/post_issue_comment.py`:
- Around line 15-27: load_json currently lets a JSONDecodeError bubble up
causing a traceback; update load_json to catch json.JSONDecodeError and handle
it by writing a clear error message via eprint (or raising a controlled
SystemExit with a friendly message/exit code) and then return None or exit, and
also factor out the duplicated helpers eprint, expand_path, and load_json into a
shared helper module used by both post_issue_comment.py and dev_watch.py to
avoid drift; reference the functions eprint, expand_path, and load_json when
making these changes.
- Around line 76-88: In resolve_message, opening the message_file can raise
OSError and currently produces a raw traceback; wrap the file open/read in a
try/except catching OSError (and optionally UnicodeError) and raise a ValueError
with a clear CLI-friendly message that includes the message_file path and the
underlying error text; keep the existing behavior for mutually exclusive args
and stdin, but replace the raw exception path in resolve_message so callers see
a clean ValueError describing the missing/invalid message file (reference
function name resolve_message and variable message_file/ path).
🧹 Nitpick comments (6)
active/dev.shortcuts/shortcuts/fix-pr.md (1)

9-10: Clarify how to verify “already on the PR branch.”

To avoid false positives, specify that the current branch should be compared to the PR head ref (via gh pr view <pr> --json headRefName) before skipping worktree creation.

active/dev.shortcuts/shortcuts/merge-pr.md (1)

10-10: Fix terminology to avoid missing staged changes.

“Unstaged commits” is inaccurate. Use “uncommitted changes (staged or unstaged)” so staged changes aren’t ignored.

Proposed edit
-1. Check if there are any unstaged commits. If so, use `@shortcut`:commit-code to commit unstaged changes.
+1. Check if there are any uncommitted changes (staged or unstaged). If so, use `@shortcut`:commit-code to commit them.
active/dev.do/SKILL.md (1)

38-44: Specify how/where to update “Status.”

This instruction is ambiguous (Issue field? Projects v2? other tracker). Add the exact system and command (e.g., gh issue edit, Projects v2 field update) so the workflow is executable.

active/dev.watch/SKILL.md (1)

23-25: Add language identifiers to fenced code blocks.

markdownlint flags missing languages; these are shell commands, so bash is appropriate.

💡 Suggested update
-```
+```bash
 python scripts/dev_watch.py --config dev.watch.json --once
-```
+```

-```
+```bash
 python scripts/dev_watch.py --config dev.watch.json --watch --interval 60
-```
+```

-```
+```bash
 python scripts/post_issue_comment.py --issue-url "<issue-url>" --message "Questions here"
-```
+```

Also applies to: 29-31, 45-47

docs/specs/active/2026-02-01-trigger-loops-sh-on-task-detection.md (1)

124-129: Align the risks table pipes to satisfy markdownlint (MD060).

🧹 Suggested alignment
-| Risk | Impact | Probability | Mitigation Strategy |
-|------|--------|-------------|---------------------|
-| loops.sh output breaks JSON parsing for dev.watch consumers | Medium | Medium | Redirect loops output to stderr or a log file. |
-| Repo path mismatch (`$HOME/code/<repo-name>` not present) | Medium | Medium | Warn and ask for guidance if the directory is missing. |
-| Codex CLI missing or misconfigured | Medium | Low | Provide a clear error and exit early. |
-| Long-running Codex blocks watch loop | Medium | Medium | Support `--parallel` to fork when desired; document default sync behavior. |
+| Risk                                                         | Impact | Probability | Mitigation Strategy                                              |
+|--------------------------------------------------------------|--------|-------------|------------------------------------------------------------------|
+| loops.sh output breaks JSON parsing for dev.watch consumers   | Medium | Medium      | Redirect loops output to stderr or a log file.                    |
+| Repo path mismatch (`$HOME/code/<repo-name>` not present)     | Medium | Medium      | Warn and ask for guidance if the directory is missing.            |
+| Codex CLI missing or misconfigured                           | Medium | Low         | Provide a clear error and exit early.                             |
+| Long-running Codex blocks watch loop                          | Medium | Medium      | Support `--parallel` to fork when desired; document default sync behavior. |
references/codex.md (1)

15-25: Specify a language for the fenced template block.

✍️ Suggested update
-```
+```text
 ## [Date/Time] - [Story ID]
 - What was implemented
 - Files changed
 - **Learnings for future iterations:**
   - Patterns discovered (e.g., "this codebase uses X for Y")
   - Gotchas encountered (e.g., "don't forget to update Z when changing W")
   - Useful context (e.g., "the evaluation panel is in component X")
 ---
-```
+```

Comment on lines +1 to +177
[
{
"id": "PVTI_lAHOAAY-6c4BN9FczgkmjzE",
"content": {
"__typename": "Issue",
"id": "I_kwDORF0dd87nTXSB",
"url": "https://github.com/kevinslin/notion-agent-skill/issues/1",
"title": "sync should support syncing to relation fields",
"number": 1,
"repository": {"nameWithOwner": "kevinslin/notion-agent-skill"},
},
"fieldValues": {
"nodes": [
{"__typename": "ProjectV2ItemFieldRepositoryValue"},
{
"__typename": "ProjectV2ItemFieldSingleSelectValue",
"name": "Backlog",
"field": {
"__typename": "ProjectV2SingleSelectField",
"name": "Status",
},
},
{"__typename": "ProjectV2ItemFieldTextValue"},
]
},
},
{
"id": "PVTI_lAHOAAY-6c4BN9Fczgknw2o",
"content": {
"__typename": "Issue",
"id": "I_kwDORF0dd87naQQm",
"url": "https://github.com/kevinslin/notion-agent-skill/issues/8",
"title": "Add integration test for testing filter with `fetch` command",
"number": 8,
"repository": {"nameWithOwner": "kevinslin/notion-agent-skill"},
},
"fieldValues": {
"nodes": [
{"__typename": "ProjectV2ItemFieldRepositoryValue"},
{"__typename": "ProjectV2ItemFieldTextValue"},
{
"__typename": "ProjectV2ItemFieldSingleSelectValue",
"name": "Backlog",
"field": {
"__typename": "ProjectV2SingleSelectField",
"name": "Status",
},
},
]
},
},
{
"id": "PVTI_lAHOAAY-6c4BN9FczgkmxoA",
"content": {
"__typename": "Issue",
"id": "I_kwDORF0dd87nVSgg",
"url": "https://github.com/kevinslin/notion-agent-skill/issues/5",
"title": "support fetch command",
"number": 5,
"repository": {"nameWithOwner": "kevinslin/notion-agent-skill"},
},
"fieldValues": {
"nodes": [
{"__typename": "ProjectV2ItemFieldRepositoryValue"},
{"__typename": "ProjectV2ItemFieldLabelValue"},
{
"__typename": "ProjectV2ItemFieldSingleSelectValue",
"name": "Done",
"field": {
"__typename": "ProjectV2SingleSelectField",
"name": "Status",
},
},
{"__typename": "ProjectV2ItemFieldTextValue"},
]
},
},
{
"id": "PVTI_lAHOAAY-6c4BN9FczgkmW9Y",
"content": {
"__typename": "Issue",
"id": "I_kwDOQyyzWs7nR7-t",
"url": "https://github.com/kevinslin/skills/issues/4",
"title": "Support doc from vendor notes",
"number": 4,
"repository": {"nameWithOwner": "kevinslin/skills"},
},
"fieldValues": {
"nodes": [
{"__typename": "ProjectV2ItemFieldRepositoryValue"},
{
"__typename": "ProjectV2ItemFieldSingleSelectValue",
"name": "Done",
"field": {
"__typename": "ProjectV2SingleSelectField",
"name": "Status",
},
},
{"__typename": "ProjectV2ItemFieldTextValue"},
]
},
},
{
"id": "PVTI_lAHOAAY-6c4BN9FczgkmBPA",
"content": {
"__typename": "Issue",
"id": "I_kwDOQyyzWs7nP1vx",
"url": "https://github.com/kevinslin/skills/issues/1",
"title": "Support promoting shortcut to skill",
"number": 1,
"repository": {"nameWithOwner": "kevinslin/skills"},
},
"fieldValues": {
"nodes": [
{"__typename": "ProjectV2ItemFieldRepositoryValue"},
{"__typename": "ProjectV2ItemFieldTextValue"},
{
"__typename": "ProjectV2ItemFieldSingleSelectValue",
"name": "Backlog",
"field": {
"__typename": "ProjectV2SingleSelectField",
"name": "Status",
},
},
]
},
},
{
"id": "PVTI_lAHOAAY-6c4BN9Fczgknw9E",
"content": {
"__typename": "Issue",
"id": "I_kwDOQyyzWs7naRUe",
"url": "https://github.com/kevinslin/skills/issues/8",
"title": "dev.do auto mode",
"number": 8,
"repository": {"nameWithOwner": "kevinslin/skills"},
},
"fieldValues": {
"nodes": [
{"__typename": "ProjectV2ItemFieldRepositoryValue"},
{"__typename": "ProjectV2ItemFieldTextValue"},
{
"__typename": "ProjectV2ItemFieldSingleSelectValue",
"name": "Backlog",
"field": {
"__typename": "ProjectV2SingleSelectField",
"name": "Status",
},
},
]
},
},
{
"id": "PVTI_lAHOAAY-6c4BN9Fczgkm_zE",
"content": {
"__typename": "Issue",
"id": "I_kwDOQyyzWs7nWoYS",
"url": "https://github.com/kevinslin/skills/issues/7",
"title": "Test Issue",
"number": 7,
"repository": {"nameWithOwner": "kevinslin/skills"},
},
"fieldValues": {
"nodes": [
{"__typename": "ProjectV2ItemFieldRepositoryValue"},
{
"__typename": "ProjectV2ItemFieldSingleSelectValue",
"name": "Todo",
"field": {
"__typename": "ProjectV2SingleSelectField",
"name": "Status",
},
},
{"__typename": "ProjectV2ItemFieldTextValue"},
]
},
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove trailing commas — the file is invalid JSON.

JSON does not allow trailing commas after the last property/element, so parsers will fail. Please remove all trailing commas throughout the file (properties, objects, and arrays).

🛠️ Example fix (apply pattern across file)
-            "repository": {"nameWithOwner": "kevinslin/notion-agent-skill"},
-        },
+            "repository": {"nameWithOwner": "kevinslin/notion-agent-skill"}
+        }

You can verify validity after the cleanup:

#!/bin/bash
python -m json.tool active/dev.watch/assets/project_example.json >/dev/null
🧰 Tools
🪛 Biome (2.3.13)

[error] 11-11: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 21-21: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 22-22: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 24-24: Expected an array, an object, or a literal but instead found ']'.

Expected an array, an object, or a literal here.

(parse)


[error] 26-26: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 36-36: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 47-47: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 48-48: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 49-49: Expected an array, an object, or a literal but instead found ']'.

Expected an array, an object, or a literal here.

(parse)


[error] 51-51: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 61-61: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 72-72: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 73-73: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 75-75: Expected an array, an object, or a literal but instead found ']'.

Expected an array, an object, or a literal here.

(parse)


[error] 77-77: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 87-87: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 97-97: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 98-98: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 100-100: Expected an array, an object, or a literal but instead found ']'.

Expected an array, an object, or a literal here.

(parse)


[error] 102-102: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 112-112: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 123-123: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 124-124: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 125-125: Expected an array, an object, or a literal but instead found ']'.

Expected an array, an object, or a literal here.

(parse)


[error] 127-127: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 137-137: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 148-148: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 149-149: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 150-150: Expected an array, an object, or a literal but instead found ']'.

Expected an array, an object, or a literal here.

(parse)


[error] 152-152: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 162-162: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 172-172: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 173-173: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 175-175: Expected an array, an object, or a literal but instead found ']'.

Expected an array, an object, or a literal here.

(parse)


[error] 177-177: Expected a property but instead found '}'.

Expected a property here.

(parse)

🤖 Prompt for AI Agents
In `@active/dev.watch/assets/project_example.json` around lines 1 - 177, The JSON
file contains trailing commas after objects and array elements (e.g., after
repository objects like "repository": {"nameWithOwner":
"kevinslin/notion-agent-skill"},, after item objects with "fieldValues": {...},
and after elements inside the top-level array), which makes the file invalid;
remove all trailing commas across the file so every object property and array
element ends without a comma (check entries with keys "id", "content",
"fieldValues", the nested "repository" objects, and the "nodes" arrays) and
revalidate with a JSON parser (e.g., python -m json.tool) to ensure the
top-level array and all nested objects are syntactically correct.

Comment on lines +5 to +8
SKILL_ROOT by located in ~/.llm/skills/dev.watch unless overridden elsewhere.
The dev.watch config can be found in $SKILL_ROOT/config.json

Create a JSON file (default: `dev.watch.json`) with the following structure:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix grammar and align the documented config path with the actual default.

The wording has a typo, and the doc references $SKILL_ROOT/config.json while the code defaults to dev.watch.json. This mismatch will confuse users setting up the config.

✏️ Proposed doc fix
-SKILL_ROOT by located in ~/.llm/skills/dev.watch unless overridden elsewhere. 
-The dev.watch config can be found in $SKILL_ROOT/config.json
+SKILL_ROOT is located in ~/.llm/skills/dev.watch unless overridden elsewhere.
+The dev.watch config can be found in $SKILL_ROOT/dev.watch.json (or $DEV_WATCH_CONFIG).
 
-Create a JSON file (default: `dev.watch.json`) with the following structure:
+Create a JSON file (default: `dev.watch.json`) with the following structure:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
SKILL_ROOT by located in ~/.llm/skills/dev.watch unless overridden elsewhere.
The dev.watch config can be found in $SKILL_ROOT/config.json
Create a JSON file (default: `dev.watch.json`) with the following structure:
SKILL_ROOT is located in ~/.llm/skills/dev.watch unless overridden elsewhere.
The dev.watch config can be found in $SKILL_ROOT/dev.watch.json (or $DEV_WATCH_CONFIG).
Create a JSON file (default: `dev.watch.json`) with the following structure:
🤖 Prompt for AI Agents
In `@active/dev.watch/references/config.md` around lines 5 - 8, Fix the grammar
and the documented default config filename: change the phrase "SKILL_ROOT by
located in ~/.llm/skills/dev.watch" to "SKILL_ROOT is located in
~/.llm/skills/dev.watch" and update the referenced config path from
"$SKILL_ROOT/config.json" to "$SKILL_ROOT/dev.watch.json" to match the code
default (dev.watch.json); ensure the subsequent sentence that instructs creating
a JSON file clarifies the default filename (default: `dev.watch.json`) and
retains the example structure heading.

Comment on lines +273 to +312
def run_loops(event, config):
script_dir = os.path.dirname(os.path.abspath(__file__))
loops_script = os.path.join(script_dir, "loops.sh")
if not os.path.exists(loops_script):
eprint(f"loops.sh not found at {loops_script}")
return 1

task = build_task_string(event)
if not task:
eprint("Skipping loops.sh: empty task string.")
return 1

repo_name = ""
repo_full = event.get("issue_repo")
if repo_full and "/" in repo_full:
repo_name = repo_full.rsplit("/", 1)[-1]

cmd = [loops_script]
if config.get("loops_parallel", DEFAULT_LOOPS_PARALLEL):
cmd.append("--parallel")
if repo_name:
cmd.extend(["--repo", repo_name])
cmd.append(task)

env = os.environ.copy()
token = config.get("_token")
if token:
env.setdefault("GITHUB_TOKEN", token)
env.setdefault("GH_TOKEN", token)
token_env = config.get("github_token_env", DEFAULT_TOKEN_ENV)
if token_env and re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", token_env):
env.setdefault(token_env, token)

try:
eprint(f"running {cmd}")
subprocess.run(cmd, check=False, stdout=sys.stderr, stderr=sys.stderr, env=env)
except OSError as err:
eprint(f"Failed to run loops.sh: {err}")
return 1
return 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Propagate loops.sh failures.

subprocess.run(..., check=False) ignores the return code, so failed loops are treated as success. This makes exit_code unreliable.

🛠️ Suggested fix to honor exit status
-    try:
-        eprint(f"running {cmd}")
-        subprocess.run(cmd, check=False, stdout=sys.stderr, stderr=sys.stderr, env=env)
+    try:
+        eprint(f"running {cmd}")
+        result = subprocess.run(
+            cmd, check=False, stdout=sys.stderr, stderr=sys.stderr, env=env
+        )
+        if result.returncode != 0:
+            eprint(f"loops.sh exited with status {result.returncode}")
+            return result.returncode
     except OSError as err:
         eprint(f"Failed to run loops.sh: {err}")
         return 1
     return 0
🧰 Tools
🪛 Ruff (0.14.14)

[error] 308-308: subprocess call: check for execution of untrusted input

(S603)

🤖 Prompt for AI Agents
In `@active/dev.watch/scripts/dev_watch.py` around lines 273 - 312, In run_loops,
subprocess.run is called with check=False which ignores the child process exit
status; update the call so the function propagates loops.sh failures by either
using check=True and catching subprocess.CalledProcessError (return non-zero on
exception) or by keeping check=False and inspecting the
CompletedProcess.returncode to return that code; ensure you reference the
cmd/env used for the call, adjust the except block to handle CalledProcessError
(or use the returncode path) and return a non-zero exit code when loops.sh fails
so run_loops reflects the script's real status.

Comment on lines +315 to +371
def main():
args = parse_args()
if args.once and args.watch:
eprint("Choose either --once or --watch, not both.")
return 2

config_path = expand_path(args.config)
config = load_json(config_path)
if not config:
eprint(f"Config not found or empty: {config_path}")
return 2

token_env = config.get("github_token_env", DEFAULT_TOKEN_ENV)
token = os.getenv(token_env)
if not token and token_env.startswith(TOKEN_PREFIXES):
token = token_env
if not token:
eprint(f"Missing GitHub token in env var: {token_env}")
return 2
config["_token"] = token

state_path = expand_path(config.get("state_file", DEFAULT_STATE_FILE))
state = load_json(state_path, default={}) or {}

poll_interval = args.interval or config.get("poll_interval_seconds", DEFAULT_POLL_INTERVAL)
emit_on_first_run = args.emit_on_first_run or config.get("emit_on_first_run", False)

if not args.watch and not args.once:
args.once = True

exit_code = 0
while True:
timestamp = datetime.now(timezone.utc).isoformat()
events, errors = collect_events(config, state, emit_on_first_run, args.force)
payload = {
"timestamp": timestamp,
"events": events,
"errors": errors,
}
print(json.dumps(payload, sort_keys=True))
sys.stdout.flush()

if events:
for event in events:
result = run_loops(event, config)
if result != 0:
exit_code = 1

save_json(state_path, state)
if errors:
exit_code = 1

if args.once:
break
time.sleep(max(5, int(poll_interval)))

return exit_code

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle invalid JSON for config/state files without crashing.

A corrupt config or state file will currently raise a JSONDecodeError and terminate the watcher. Please add a clean error path so the process exits with a readable message (or resets state for the state file).

🛠️ Example handling in `main()`
-    config = load_json(config_path)
+    try:
+        config = load_json(config_path)
+    except json.JSONDecodeError as err:
+        eprint(f"Invalid JSON in config: {config_path} ({err})")
+        return 2
     if not config:
         eprint(f"Config not found or empty: {config_path}")
         return 2
@@
-    state = load_json(state_path, default={}) or {}
+    try:
+        state = load_json(state_path, default={}) or {}
+    except json.JSONDecodeError as err:
+        eprint(f"Invalid JSON in state file: {state_path} ({err})")
+        state = {}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def main():
args = parse_args()
if args.once and args.watch:
eprint("Choose either --once or --watch, not both.")
return 2
config_path = expand_path(args.config)
config = load_json(config_path)
if not config:
eprint(f"Config not found or empty: {config_path}")
return 2
token_env = config.get("github_token_env", DEFAULT_TOKEN_ENV)
token = os.getenv(token_env)
if not token and token_env.startswith(TOKEN_PREFIXES):
token = token_env
if not token:
eprint(f"Missing GitHub token in env var: {token_env}")
return 2
config["_token"] = token
state_path = expand_path(config.get("state_file", DEFAULT_STATE_FILE))
state = load_json(state_path, default={}) or {}
poll_interval = args.interval or config.get("poll_interval_seconds", DEFAULT_POLL_INTERVAL)
emit_on_first_run = args.emit_on_first_run or config.get("emit_on_first_run", False)
if not args.watch and not args.once:
args.once = True
exit_code = 0
while True:
timestamp = datetime.now(timezone.utc).isoformat()
events, errors = collect_events(config, state, emit_on_first_run, args.force)
payload = {
"timestamp": timestamp,
"events": events,
"errors": errors,
}
print(json.dumps(payload, sort_keys=True))
sys.stdout.flush()
if events:
for event in events:
result = run_loops(event, config)
if result != 0:
exit_code = 1
save_json(state_path, state)
if errors:
exit_code = 1
if args.once:
break
time.sleep(max(5, int(poll_interval)))
return exit_code
def main():
args = parse_args()
if args.once and args.watch:
eprint("Choose either --once or --watch, not both.")
return 2
config_path = expand_path(args.config)
try:
config = load_json(config_path)
except json.JSONDecodeError as err:
eprint(f"Invalid JSON in config: {config_path} ({err})")
return 2
if not config:
eprint(f"Config not found or empty: {config_path}")
return 2
token_env = config.get("github_token_env", DEFAULT_TOKEN_ENV)
token = os.getenv(token_env)
if not token and token_env.startswith(TOKEN_PREFIXES):
token = token_env
if not token:
eprint(f"Missing GitHub token in env var: {token_env}")
return 2
config["_token"] = token
state_path = expand_path(config.get("state_file", DEFAULT_STATE_FILE))
try:
state = load_json(state_path, default={}) or {}
except json.JSONDecodeError as err:
eprint(f"Invalid JSON in state file: {state_path} ({err})")
state = {}
poll_interval = args.interval or config.get("poll_interval_seconds", DEFAULT_POLL_INTERVAL)
emit_on_first_run = args.emit_on_first_run or config.get("emit_on_first_run", False)
if not args.watch and not args.once:
args.once = True
exit_code = 0
while True:
timestamp = datetime.now(timezone.utc).isoformat()
events, errors = collect_events(config, state, emit_on_first_run, args.force)
payload = {
"timestamp": timestamp,
"events": events,
"errors": errors,
}
print(json.dumps(payload, sort_keys=True))
sys.stdout.flush()
if events:
for event in events:
result = run_loops(event, config)
if result != 0:
exit_code = 1
save_json(state_path, state)
if errors:
exit_code = 1
if args.once:
break
time.sleep(max(5, int(poll_interval)))
return exit_code
🤖 Prompt for AI Agents
In `@active/dev.watch/scripts/dev_watch.py` around lines 315 - 371, The main()
flow currently assumes load_json(config_path) and load_json(state_path) succeed;
wrap those calls to catch JSONDecodeError (and generic ValueError) from
malformed JSON: when loading config (config = load_json(config_path)) catch the
error, eprint a clear message including config_path and the error text, and
return 2; when loading state (state = load_json(state_path, default={}) or {})
catch the error, eprint a message including state_path and the error, and
recover by setting state = {} (optionally back up or overwrite the corrupt file)
so the watcher continues; update references to config and state accordingly and
ensure subsequent code (e.g., save_json(state_path, state)) still runs with the
recovered state.

Comment on lines +15 to +27
def eprint(message):
print(message, file=sys.stderr)


def expand_path(path):
return os.path.expandvars(os.path.expanduser(path))


def load_json(path):
if not path or not os.path.exists(path):
return None
with open(path, "r", encoding="utf-8") as handle:
return json.load(handle)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle invalid JSON gracefully and consider consolidating shared helpers.

  • load_json will currently raise a raw JSONDecodeError on malformed config. That leads to a traceback instead of a clean CLI error.
  • eprint / expand_path / load_json duplicate logic from active/dev.watch/scripts/dev_watch.py. Consider extracting a shared helper module to avoid drift.
🛠️ Suggested error handling for invalid JSON
 def load_json(path):
     if not path or not os.path.exists(path):
         return None
     with open(path, "r", encoding="utf-8") as handle:
-        return json.load(handle)
+        try:
+            return json.load(handle)
+        except json.JSONDecodeError as err:
+            eprint(f"Invalid JSON in {path}: {err}")
+            return None
🤖 Prompt for AI Agents
In `@active/dev.watch/scripts/post_issue_comment.py` around lines 15 - 27,
load_json currently lets a JSONDecodeError bubble up causing a traceback; update
load_json to catch json.JSONDecodeError and handle it by writing a clear error
message via eprint (or raising a controlled SystemExit with a friendly
message/exit code) and then return None or exit, and also factor out the
duplicated helpers eprint, expand_path, and load_json into a shared helper
module used by both post_issue_comment.py and dev_watch.py to avoid drift;
reference the functions eprint, expand_path, and load_json when making these
changes.

Comment on lines +76 to +88
def resolve_message(message, message_file):
if message and message_file:
raise ValueError("Provide either --message or --message-file, not both.")
if message_file:
path = expand_path(message_file)
with open(path, "r", encoding="utf-8") as handle:
return handle.read().strip()
if message:
return message.strip()
stdin = sys.stdin.read().strip()
if stdin:
return stdin
raise ValueError("Missing comment body. Provide --message, --message-file, or stdin.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add friendly handling for missing/invalid message files.

open() can raise OSError (missing file, permissions), which currently bubbles as a traceback. Convert to a clean CLI error.

🛠️ Suggested error handling for message files
     if message_file:
         path = expand_path(message_file)
-        with open(path, "r", encoding="utf-8") as handle:
-            return handle.read().strip()
+        try:
+            with open(path, "r", encoding="utf-8") as handle:
+                return handle.read().strip()
+        except OSError as err:
+            raise ValueError(f"Unable to read message file: {path}") from err
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def resolve_message(message, message_file):
if message and message_file:
raise ValueError("Provide either --message or --message-file, not both.")
if message_file:
path = expand_path(message_file)
with open(path, "r", encoding="utf-8") as handle:
return handle.read().strip()
if message:
return message.strip()
stdin = sys.stdin.read().strip()
if stdin:
return stdin
raise ValueError("Missing comment body. Provide --message, --message-file, or stdin.")
def resolve_message(message, message_file):
if message and message_file:
raise ValueError("Provide either --message or --message-file, not both.")
if message_file:
path = expand_path(message_file)
try:
with open(path, "r", encoding="utf-8") as handle:
return handle.read().strip()
except OSError as err:
raise ValueError(f"Unable to read message file: {path}") from err
if message:
return message.strip()
stdin = sys.stdin.read().strip()
if stdin:
return stdin
raise ValueError("Missing comment body. Provide --message, --message-file, or stdin.")
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 78-78: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 88-88: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In `@active/dev.watch/scripts/post_issue_comment.py` around lines 76 - 88, In
resolve_message, opening the message_file can raise OSError and currently
produces a raw traceback; wrap the file open/read in a try/except catching
OSError (and optionally UnicodeError) and raise a ValueError with a clear
CLI-friendly message that includes the message_file path and the underlying
error text; keep the existing behavior for mutually exclusive args and stdin,
but replace the raw exception path in resolve_message so callers see a clean
ValueError describing the missing/invalid message file (reference function name
resolve_message and variable message_file/ path).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants