noah-code 0.6.0__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. {noah_code-0.6.0 → noah_code-0.7.0}/PKG-INFO +17 -1
  2. {noah_code-0.6.0 → noah_code-0.7.0}/README.md +16 -0
  3. {noah_code-0.6.0 → noah_code-0.7.0}/docs/interactive-reference.md +29 -5
  4. noah_code-0.7.0/docs/releases/v0.7.0.md +51 -0
  5. {noah_code-0.6.0 → noah_code-0.7.0}/docs/reliability.md +22 -0
  6. {noah_code-0.6.0 → noah_code-0.7.0}/pyproject.toml +1 -1
  7. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/__init__.py +1 -1
  8. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/agent.py +4 -0
  9. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/cli.py +60 -38
  10. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/host.py +41 -15
  11. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/process_tools.py +37 -15
  12. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/task_tools.py +30 -11
  13. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/workspace_tools.py +37 -10
  14. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/ui/textual_app.py +123 -108
  15. noah_code-0.7.0/src/noah_code/verification.py +312 -0
  16. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_cli.py +76 -0
  17. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_host.py +48 -0
  18. noah_code-0.7.0/tests/test_run_exit.py +191 -0
  19. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_task_tools.py +37 -1
  20. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_textual_tui.py +155 -13
  21. noah_code-0.7.0/tests/test_tui_editing.py +135 -0
  22. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_tui_review_receipt.py +67 -28
  23. noah_code-0.7.0/tests/test_verification.py +262 -0
  24. noah_code-0.7.0/tests/test_verification_tools.py +119 -0
  25. {noah_code-0.6.0 → noah_code-0.7.0}/uv.lock +1 -1
  26. noah_code-0.6.0/tests/test_run_exit.py +0 -113
  27. {noah_code-0.6.0 → noah_code-0.7.0}/.github/workflows/ci.yml +0 -0
  28. {noah_code-0.6.0 → noah_code-0.7.0}/.github/workflows/release.yml +0 -0
  29. {noah_code-0.6.0 → noah_code-0.7.0}/.gitignore +0 -0
  30. {noah_code-0.6.0 → noah_code-0.7.0}/.pre-commit-config.yaml +0 -0
  31. {noah_code-0.6.0 → noah_code-0.7.0}/docs/assets/noah-in-action.svg +0 -0
  32. {noah_code-0.6.0 → noah_code-0.7.0}/docs/assets/noah-logo.svg +0 -0
  33. {noah_code-0.6.0 → noah_code-0.7.0}/docs/configuration.md +0 -0
  34. {noah_code-0.6.0 → noah_code-0.7.0}/docs/development.md +0 -0
  35. {noah_code-0.6.0 → noah_code-0.7.0}/docs/extensions.md +0 -0
  36. {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.1.0.md +0 -0
  37. {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.1.1.md +0 -0
  38. {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.2.0.md +0 -0
  39. {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.2.1.md +0 -0
  40. {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.2.2.md +0 -0
  41. {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.2.3.md +0 -0
  42. {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.2.4.md +0 -0
  43. {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.3.0.md +0 -0
  44. {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.4.0.md +0 -0
  45. {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.4.1.md +0 -0
  46. {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.5.0.md +0 -0
  47. {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.5.1.md +0 -0
  48. {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.6.0.md +0 -0
  49. {noah_code-0.6.0 → noah_code-0.7.0}/docs/security.md +0 -0
  50. {noah_code-0.6.0 → noah_code-0.7.0}/install.sh +0 -0
  51. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/__main__.py +0 -0
  52. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/agents.py +0 -0
  53. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/approvals.py +0 -0
  54. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/budget.py +0 -0
  55. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/checkpoints.py +0 -0
  56. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/commands.py +0 -0
  57. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/composer.py +0 -0
  58. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/config.py +0 -0
  59. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/credentials.py +0 -0
  60. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/custom_commands.py +0 -0
  61. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/event_bridge.py +0 -0
  62. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/events.py +0 -0
  63. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/github.py +0 -0
  64. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/hooks.py +0 -0
  65. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/llm.py +0 -0
  66. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/llm_replies.py +0 -0
  67. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/macos_sandbox.py +0 -0
  68. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/mcp_setup.py +0 -0
  69. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/nooa_compat.py +0 -0
  70. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/permissions.py +0 -0
  71. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/predict.py +0 -0
  72. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/project_notes.py +0 -0
  73. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/providers.py +0 -0
  74. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/redaction.py +0 -0
  75. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/runtime_state.py +0 -0
  76. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/secure_files.py +0 -0
  77. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/sessions.py +0 -0
  78. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/skills_setup.py +0 -0
  79. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/snapshots.py +0 -0
  80. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/steer.py +0 -0
  81. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/summarization.py +0 -0
  82. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/telemetry.py +0 -0
  83. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/themes.py +0 -0
  84. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tool_output.py +0 -0
  85. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/__init__.py +0 -0
  86. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/diff_tools.py +0 -0
  87. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/git_tools.py +0 -0
  88. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/github_tools.py +0 -0
  89. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/lsp_tools.py +0 -0
  90. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/media_tools.py +0 -0
  91. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/memory_tools.py +0 -0
  92. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/plan_tools.py +0 -0
  93. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/question_tools.py +0 -0
  94. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/web_tools.py +0 -0
  95. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/ui/__init__.py +0 -0
  96. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/ui/console.py +0 -0
  97. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/ui/protocol.py +0 -0
  98. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/ui/textual.css +0 -0
  99. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/updates.py +0 -0
  100. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/usage.py +0 -0
  101. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/workspace.py +0 -0
  102. {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/worktree.py +0 -0
  103. {noah_code-0.6.0 → noah_code-0.7.0}/tests/conftest.py +0 -0
  104. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_agent_security.py +0 -0
  105. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_agents.py +0 -0
  106. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_approvals.py +0 -0
  107. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_budget.py +0 -0
  108. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_cache_context.py +0 -0
  109. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_checkpoints.py +0 -0
  110. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_commands.py +0 -0
  111. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_compaction_eviction.py +0 -0
  112. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_composer.py +0 -0
  113. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_config.py +0 -0
  114. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_console_ui.py +0 -0
  115. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_credentials.py +0 -0
  116. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_custom_commands.py +0 -0
  117. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_diff_tools.py +0 -0
  118. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_efficiency.py +0 -0
  119. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_event_bridge.py +0 -0
  120. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_event_bridge_and_shell.py +0 -0
  121. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_git_tools.py +0 -0
  122. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_github.py +0 -0
  123. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_hooks.py +0 -0
  124. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_installer.py +0 -0
  125. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_lean_reasoning.py +0 -0
  126. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_llm.py +0 -0
  127. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_llm_replies.py +0 -0
  128. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_lsp_tools.py +0 -0
  129. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_mcp_setup.py +0 -0
  130. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_nooa_migration.py +0 -0
  131. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_permissions.py +0 -0
  132. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_plan_memory_tools.py +0 -0
  133. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_process_tools.py +0 -0
  134. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_project_notes.py +0 -0
  135. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_prompt_scorecard.py +0 -0
  136. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_providers.py +0 -0
  137. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_question_tools.py +0 -0
  138. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_redaction.py +0 -0
  139. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_runtime_state.py +0 -0
  140. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_sessions.py +0 -0
  141. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_skills_setup.py +0 -0
  142. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_snapshots.py +0 -0
  143. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_steer.py +0 -0
  144. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_summarization.py +0 -0
  145. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_telemetry.py +0 -0
  146. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_tool_output.py +0 -0
  147. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_tui_composer_queue.py +0 -0
  148. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_tui_history_dialogs.py +0 -0
  149. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_tui_layout.py +0 -0
  150. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_updates.py +0 -0
  151. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_wave1_e2e.py +0 -0
  152. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_web_tools.py +0 -0
  153. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_workspace_tools.py +0 -0
  154. {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_worktree.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: noah-code
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Noah Code terminal coding agent, built on NVIDIA OO Agents (NOOA)
5
5
  Project-URL: Homepage, https://github.com/skundu42/noah-code
6
6
  Project-URL: Documentation, https://github.com/skundu42/noah-code#readme
@@ -137,6 +137,9 @@ noah --mode plan .
137
137
  # Run one task and exit
138
138
  noah run "Explain how authentication is wired" .
139
139
 
140
+ # Emit one structured result on stdout (progress goes to stderr)
141
+ noah run --json "Fix the failing unit test" .
142
+
140
143
  # Allow actions that would normally ask; explicit deny rules still apply
141
144
  noah run --auto "Fix the failing unit test" .
142
145
 
@@ -148,6 +151,13 @@ noah --session SESSION_ID .
148
151
  noah --console .
149
152
  ```
150
153
 
154
+ `run --json` reports `status` (`completed`, `needs_input`, `failed`, or `cancelled`),
155
+ `exit_code`, `explanation`, `session_id`, `run_id`, session `usage`, and recorded `checks`.
156
+ Check `status` when automating: a request for input keeps the existing exit code 0,
157
+ but its status is `needs_input`. Check results are separate from task completion;
158
+ an empty list means no checks were recorded. Errors keep nonzero exit codes and
159
+ Ctrl+C returns 130. Delegated agents also retain `needs_input` in the work ledger.
160
+
151
161
  Check the installation and resolved configuration with:
152
162
 
153
163
  ```bash
@@ -170,9 +180,14 @@ Type `/` to search the full command and configuration reference. The most common
170
180
  | `Shift+Enter` / `Ctrl+J` | Insert a newline |
171
181
  | `Alt+Enter` | Expand or collapse the composer |
172
182
  | `Alt+Z` | Restore a displaced draft |
183
+ | Click / arrows in the prompt | Position the editing cursor |
184
+ | `Ctrl+A` / `Ctrl+X` / `Ctrl+V` | Select all / cut / paste prompt text |
185
+ | `Ctrl+Z` / `Ctrl+Y` | Undo / redo prompt edits |
186
+ | `Ctrl+Home` / `Ctrl+End` | Jump to the beginning / end of the prompt; Shift selects |
173
187
  | Drag, then `Cmd+C` / `Ctrl+Shift+C` | Select and copy TUI text |
174
188
  | `Ctrl+Shift+C` with no selection | Copy the latest Noah reply |
175
189
  | `Ctrl+G` | Open the searchable skills picker |
190
+ | `Ctrl+P` | Search and run slash commands |
176
191
  | `Ctrl+L` | Open the model picker |
177
192
  | `Ctrl+R` | Search and recall a prior prompt |
178
193
  | `Ctrl+T` | Expand or collapse live tool output |
@@ -180,6 +195,7 @@ Type `/` to search the full command and configuration reference. The most common
180
195
  | `Alt+Up` | Recall the newest queued prompt for editing |
181
196
  | `Ctrl+]` | Return to live transcript output |
182
197
  | `Tab` / `Shift+Tab` | Navigate focus; Tab completes suggestions |
198
+ | `Enter` or click a command | Run the selected slash command or open its picker; required arguments stay editable |
183
199
  | `Ctrl+B` | Switch between build and plan mode |
184
200
  | `Ctrl+D` | Review changes while working or idle |
185
201
  | `Ctrl+C` | Stop the current run and pause queued follow-ups |
@@ -91,6 +91,9 @@ noah --mode plan .
91
91
  # Run one task and exit
92
92
  noah run "Explain how authentication is wired" .
93
93
 
94
+ # Emit one structured result on stdout (progress goes to stderr)
95
+ noah run --json "Fix the failing unit test" .
96
+
94
97
  # Allow actions that would normally ask; explicit deny rules still apply
95
98
  noah run --auto "Fix the failing unit test" .
96
99
 
@@ -102,6 +105,13 @@ noah --session SESSION_ID .
102
105
  noah --console .
103
106
  ```
104
107
 
108
+ `run --json` reports `status` (`completed`, `needs_input`, `failed`, or `cancelled`),
109
+ `exit_code`, `explanation`, `session_id`, `run_id`, session `usage`, and recorded `checks`.
110
+ Check `status` when automating: a request for input keeps the existing exit code 0,
111
+ but its status is `needs_input`. Check results are separate from task completion;
112
+ an empty list means no checks were recorded. Errors keep nonzero exit codes and
113
+ Ctrl+C returns 130. Delegated agents also retain `needs_input` in the work ledger.
114
+
105
115
  Check the installation and resolved configuration with:
106
116
 
107
117
  ```bash
@@ -124,9 +134,14 @@ Type `/` to search the full command and configuration reference. The most common
124
134
  | `Shift+Enter` / `Ctrl+J` | Insert a newline |
125
135
  | `Alt+Enter` | Expand or collapse the composer |
126
136
  | `Alt+Z` | Restore a displaced draft |
137
+ | Click / arrows in the prompt | Position the editing cursor |
138
+ | `Ctrl+A` / `Ctrl+X` / `Ctrl+V` | Select all / cut / paste prompt text |
139
+ | `Ctrl+Z` / `Ctrl+Y` | Undo / redo prompt edits |
140
+ | `Ctrl+Home` / `Ctrl+End` | Jump to the beginning / end of the prompt; Shift selects |
127
141
  | Drag, then `Cmd+C` / `Ctrl+Shift+C` | Select and copy TUI text |
128
142
  | `Ctrl+Shift+C` with no selection | Copy the latest Noah reply |
129
143
  | `Ctrl+G` | Open the searchable skills picker |
144
+ | `Ctrl+P` | Search and run slash commands |
130
145
  | `Ctrl+L` | Open the model picker |
131
146
  | `Ctrl+R` | Search and recall a prior prompt |
132
147
  | `Ctrl+T` | Expand or collapse live tool output |
@@ -134,6 +149,7 @@ Type `/` to search the full command and configuration reference. The most common
134
149
  | `Alt+Up` | Recall the newest queued prompt for editing |
135
150
  | `Ctrl+]` | Return to live transcript output |
136
151
  | `Tab` / `Shift+Tab` | Navigate focus; Tab completes suggestions |
152
+ | `Enter` or click a command | Run the selected slash command or open its picker; required arguments stay editable |
137
153
  | `Ctrl+B` | Switch between build and plan mode |
138
154
  | `Ctrl+D` | Review changes while working or idle |
139
155
  | `Ctrl+C` | Stop the current run and pause queued follow-ups |
@@ -4,13 +4,13 @@
4
4
 
5
5
  | Key | Action |
6
6
  |-----|--------|
7
- | `Enter` | Send the current message, or queue a follow-up while a turn is running |
7
+ | `Enter` | Run the highlighted command, send the current message, or queue a follow-up while a turn is running |
8
8
  | `Shift+Enter` or `Ctrl+J` | Insert a newline without sending |
9
9
  | `Alt+Enter` | Expand or collapse the composer |
10
10
  | `Alt+Z` | Restore the draft saved before a palette or history selection |
11
11
  | `Tab` / `Shift+Tab` | Move focus forward/back; Tab completes an open suggestion |
12
12
  | `Ctrl+B` | Switch between build and plan mode |
13
- | `Ctrl+P` | Open the command palette |
13
+ | `Ctrl+P` | Search and run slash commands |
14
14
  | `Ctrl+G` | Open the searchable skills picker |
15
15
  | `Ctrl+L` | Open the model picker |
16
16
  | `Ctrl+O` | Open the session picker |
@@ -57,9 +57,11 @@ finishes, explicitly marking omitted middle output when the capture limit is rea
57
57
  duplicate status lines; the banner returns between tools.
58
58
 
59
59
  Type `/` in the composer to open the inline command list; the list remains visible and filters
60
- continuously. Use `Up`/`Down` to highlight a command, `Enter` or `Tab` to complete it, and `Esc` to
61
- close the list. Press `Enter` again to run the completed command. Typing `/config` expands the list
62
- to every resolved configuration path and its current redacted value.
60
+ continuously. Use `Up`/`Down` to highlight a command, then press `Enter` or click once to run it
61
+ or open its picker. `Tab` completes the command for editing, and `Esc` closes the list. Commands
62
+ requiring arguments and file mentions remain editable. Typing `/config` expands the list to every
63
+ resolved configuration path and its current redacted value. `Ctrl+P` offers the same single-action
64
+ selection in a searchable command picker.
63
65
 
64
66
  Press `Ctrl+R` to search prompts already shown in the current session. Choosing one loads it into
65
67
  the composer without sending it and saves the displaced draft for `Alt+Z`. Palette choices
@@ -88,6 +90,28 @@ Inside the composer, `Cmd+C`/`Ctrl+C` copies the composer's
88
90
  own selection; `Ctrl+C` with no composer selection keeps its cancel-turn behavior. Selection uses a
89
91
  high-contrast highlight in every Noah theme.
90
92
 
93
+ The prompt input supports mouse placement, drag selection, and normal text editing:
94
+
95
+ | Prompt control | Action |
96
+ | --- | --- |
97
+ | Click / arrow keys | Place or move the cursor within the prompt |
98
+ | `Shift` + arrow keys | Extend a selection |
99
+ | `Ctrl+A` / `Cmd+A` | Select the entire prompt |
100
+ | `Ctrl+X` / `Cmd+X` | Cut selected text; with no selection, cut the current line |
101
+ | `Ctrl+C` / `Cmd+C` | Copy selected prompt text |
102
+ | `Ctrl+V` / `Cmd+V` | Paste, replacing the selected text |
103
+ | `Ctrl+Z` / `Ctrl+Y` | Undo / redo prompt edits |
104
+ | `Home` / `End` | Move to the beginning / end of the current line |
105
+ | `Ctrl+Home` / `Ctrl+End` | Move to the beginning / end of the entire prompt |
106
+ | `Ctrl+Shift+Home` / `Ctrl+Shift+End` | Select to the beginning / end of the entire prompt |
107
+ | `Ctrl+Left` / `Ctrl+Right` | Move by word; hold Shift to select |
108
+ | `Alt+Enter` | Expand the prompt for longer edits |
109
+
110
+ `Ctrl+A` now selects all; use `Home` for the former line-start behavior. Prompt undo is
111
+ separate from `/undo`, which reverts workspace edits. A paste waits for an in-progress
112
+ cut/copy to reach the clipboard; if the draft, selection, focus, or session changes while
113
+ the clipboard is being read, that paste is cancelled to protect the newer input.
114
+
91
115
  ### Mid-turn follow-ups
92
116
 
93
117
  While Noah is working, the composer stays open. `Enter` queues the current text instead of starting
@@ -0,0 +1,51 @@
1
+ # Noah Code v0.7.0
2
+
3
+ Slash commands now run with one Enter or click. Check results are shared across agents and
4
+ background jobs, and headless runs expose structured outcomes for automation.
5
+
6
+ ## Commands and editing
7
+
8
+ - Enter or a single click runs the highlighted slash command or opens its picker. Tab completes
9
+ the command for editing; required arguments and file mentions remain editable.
10
+ - Ctrl+P opens Noah's command picker and runs the selected command immediately. Displaced drafts
11
+ remain available through Alt+Z, and typed command arguments are preserved.
12
+ - Prompt editing and clipboard handling are more reliable, including selection and paste behavior.
13
+
14
+ ## Shared verification
15
+
16
+ - Foreground checks, streamed commands, subagents, background jobs, and terminal commands use a
17
+ shared session ledger. Records include the command, source, exit code, execution directory when
18
+ known, and workspace fingerprints before and after execution.
19
+ - Receipts mark results stale when workspace files change during or after a check. Interrupted
20
+ checks remain incomplete, and unavailable workspace fingerprints are reported as unknown.
21
+ - Agents can inspect the same evidence with `await self.ws.checks()`. Records survive session
22
+ reopening and are included in JSON outcomes. Headless outcomes refresh after process cleanup.
23
+ - Workspace fingerprints use file metadata. Ignored untracked artifacts, external dependencies,
24
+ and metadata-preserving changes are outside their scope. Compound shell commands are not
25
+ credited as individual checks.
26
+
27
+ ## Task outcomes
28
+
29
+ - Subagents requesting input retain a distinct `needs_input` state. The interface reports
30
+ "Input needed" instead of showing a completed turn.
31
+ - `noah run --json` emits one result on stdout and sends progress to stderr. The result includes
32
+ `status`, `exit_code`, `explanation`, `session_id`, `run_id`, session `usage`, and `checks`.
33
+ - Status distinguishes `completed`, `needs_input`, `failed`, and `cancelled`. Existing exit codes
34
+ remain compatible: both completion and waiting for input return zero, so automation should
35
+ inspect `status`. Task completion is separate from passing checks.
36
+ - Headless follow-ups using `--session` continue the waiting run. Invalid agent stop reasons
37
+ and persistence failures are reported as failures.
38
+
39
+ ## Upgrade
40
+
41
+ ```bash
42
+ noah update
43
+ ```
44
+
45
+ Example structured run:
46
+
47
+ ```bash
48
+ noah run --json "Fix the failing unit test" .
49
+ ```
50
+
51
+ **Full changelog:** https://github.com/skundu42/noah-code/compare/v0.6.0...v0.7.0
@@ -142,6 +142,28 @@ unbounded growth. Delete sessions that are no longer needed with `noah sessions
142
142
 
143
143
  ## Observability
144
144
 
145
+ ### Verification evidence
146
+
147
+ Foreground and streamed check commands, subagents, background jobs, and persistent-terminal
148
+ commands share a bounded verification ledger. The agent can inspect it with
149
+ `await self.ws.checks()`. Each record includes the observed exit code, command, source, execution
150
+ directory when known, and workspace fingerprints before and after execution. Records persist with
151
+ the session and are included in `noah run --json` results.
152
+
153
+ Turn receipts recompute check status against the current workspace. A check becomes `stale` if
154
+ files changed during or after its execution; missing fingerprints produce `unknown`, and an
155
+ interrupted check is `incomplete`. A successful rerun supersedes the earlier attempt in the receipt.
156
+ Completion and verification are separate: `completed` means the agent finished its turn, while
157
+ the check records show the evidence available for its changes.
158
+
159
+ Fingerprints use file metadata and symlink targets without reading file contents. In Git checkouts
160
+ they cover tracked files and non-ignored untracked files; outside Git, common cache, build, and
161
+ vendor directories are excluded. Ignored artifacts, external dependencies, and metadata-preserving
162
+ edits are outside this check. Commands containing shell chaining, redirection, or expansion are
163
+ not credited as individual checks because their exit code cannot establish each check's result.
164
+
165
+ ### Runtime status
166
+
145
167
  Use `/health` during idle or active work. It reports runtime database and artifact sizes, pending
146
168
  inbox items and interactions, live jobs, retained runtime events, and the current non-terminal run.
147
169
  Use `/status` for workspace/session state, `/tokens` for model and cache usage, `F2` for activity,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "noah-code"
3
- version = "0.6.0"
3
+ version = "0.7.0"
4
4
  description = "Noah Code terminal coding agent, built on NVIDIA OO Agents (NOOA)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<3.14"
@@ -1,3 +1,3 @@
1
1
  """Noah Code: terminal coding agent on NVIDIA OO Agents."""
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "0.7.0"
@@ -123,6 +123,7 @@ class _PermissionSandboxedExecutor(SandboxedExecutor):
123
123
  ("workspace_root",),
124
124
  ("ws", "edit"),
125
125
  ("ws", "inspect"),
126
+ ("ws", "checks"),
126
127
  ("ws", "apply_patch"),
127
128
  ("ws", "apply_unified_diff"),
128
129
  ("ws", "list"),
@@ -484,6 +485,8 @@ call is async: always `await` it before iterating or accessing the result.
484
485
  returncode/stdout/stderr. `read_only=True` skips approval only for commands
485
486
  the engine recognizes as read-only (Git inspection, search, listing, text
486
487
  filters). It is rejected for pytest, uv, Python, builds, and mutations.
488
+ - Inspect `await self.ws.checks()` for shared verification results. Rerun stale
489
+ checks after edits; a completed command alone does not prove the task is done.
487
490
  - Use `self.processes.start/logs/status/input/stop` for long jobs and consume
488
491
  logs by cursor. Persistent shells use `open_terminal`, `terminal_run`,
489
492
  `terminal_status`, and `close_terminal`; raw terminal input is blocked.
@@ -829,6 +832,7 @@ class CodingAgent(InteractiveAgent):
829
832
  lsp=self.lsp,
830
833
  runtime=runtime,
831
834
  coordinator=self._coordinator,
835
+ verification_source=f"subagent:{self.agent_id}" if nested else "main",
832
836
  output_store_root=runtime.artifact_dir if runtime is not None else None,
833
837
  output_store_max_bytes=config.reliability.artifact_max_bytes,
834
838
  )
@@ -3,8 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import contextlib
7
+ import json
6
8
  import os
7
9
  import sys
10
+ from dataclasses import asdict, replace
8
11
  from pathlib import Path
9
12
  from typing import Any, Literal
10
13
 
@@ -20,7 +23,8 @@ from noah_code.config import (
20
23
  save_user_reasoning_effort,
21
24
  user_default_model,
22
25
  )
23
- from noah_code.host import AgentHost
26
+ from noah_code.host import AgentHost, HostResult
27
+ from noah_code.redaction import safe_error_message
24
28
  from noah_code.sessions import SessionError, SessionStore
25
29
  from noah_code.ui.console import ConsoleUI
26
30
  from noah_code.updates import (
@@ -192,6 +196,7 @@ def cli_group() -> None:
192
196
  @click.argument("path", required=False, type=click.Path())
193
197
  @_common_options
194
198
  @click.option("--session", "session_id", default=None)
199
+ @click.option("--json", "json_output", is_flag=True, help="Print one JSON outcome; send progress to stderr")
195
200
  def run_cmd(
196
201
  prompt: str,
197
202
  path: str | None,
@@ -203,6 +208,7 @@ def run_cmd(
203
208
  max_iterations: int | None,
204
209
  session_id: str | None,
205
210
  unsafe_inprocess_code_execution: bool,
211
+ json_output: bool,
206
212
  ) -> None:
207
213
  """Run one coding task without opening the interactive interface."""
208
214
  code = _run_async(
@@ -217,6 +223,7 @@ def run_cmd(
217
223
  max_iterations=max_iterations,
218
224
  session_id=session_id,
219
225
  unsafe_inprocess_code_execution=unsafe_inprocess_code_execution,
226
+ json_output=json_output,
220
227
  )
221
228
  )
222
229
  raise SystemExit(code)
@@ -995,46 +1002,61 @@ async def _run_session(
995
1002
  max_iterations: int | None,
996
1003
  session_id: str | None,
997
1004
  unsafe_inprocess_code_execution: bool,
1005
+ json_output: bool = False,
998
1006
  ) -> int:
999
1007
  """Run one task through the normal host without automation-only adapters."""
1000
1008
 
1001
- prepared, code = await _prepare(
1002
- path=path,
1003
- model=model,
1004
- reasoning_effort=reasoning_effort,
1005
- auto=auto,
1006
- yolo=yolo,
1007
- mode=mode,
1008
- max_iterations=max_iterations,
1009
- session_id=session_id,
1010
- frontend="console",
1011
- unsafe_inprocess_code_execution=unsafe_inprocess_code_execution,
1012
- allow_auto_install=False,
1013
- )
1014
- if prepared is None:
1015
- return code
1016
- workspace, config, store, meta = prepared
1017
- host = AgentHost(
1018
- workspace,
1019
- config,
1020
- session_meta=meta,
1021
- store=store,
1022
- ui=ConsoleUI(markdown=config.ui.markdown),
1023
- )
1024
- try:
1025
- result = await host.run_once(prompt)
1026
- return result.exit_code
1027
- except KeyboardInterrupt:
1028
- host.cancel_active_turn()
1029
- return EXIT_SIGINT
1030
- except Exception as exc: # noqa: BLE001 - keep one-shot failures concise
1031
- from noah_code.redaction import safe_error_message
1032
-
1033
- click.echo(f"error: {safe_error_message(exc)}", err=True)
1034
- return EXIT_AGENT
1035
- finally:
1036
- # Startup can fail before run_once reaches its own cleanup block.
1037
- await host.close()
1009
+ host: AgentHost | None = None
1010
+ # Some provider libraries print directly; stdout stays machine-readable.
1011
+ with contextlib.redirect_stdout(sys.stderr) if json_output else contextlib.nullcontext():
1012
+ try:
1013
+ prepared, code = await _prepare(
1014
+ path=path,
1015
+ model=model,
1016
+ reasoning_effort=reasoning_effort,
1017
+ auto=auto,
1018
+ yolo=yolo,
1019
+ mode=mode,
1020
+ max_iterations=max_iterations,
1021
+ session_id=session_id,
1022
+ frontend="console",
1023
+ unsafe_inprocess_code_execution=unsafe_inprocess_code_execution,
1024
+ allow_auto_install=False,
1025
+ )
1026
+ if prepared is None:
1027
+ result = HostResult(
1028
+ code, "Could not prepare the task; see stderr for details.",
1029
+ session_id, status="failed",
1030
+ )
1031
+ else:
1032
+ workspace, config, store, meta = prepared
1033
+ host = AgentHost(
1034
+ workspace, config, session_meta=meta, store=store,
1035
+ ui=ConsoleUI(markdown=config.ui.markdown),
1036
+ )
1037
+ try:
1038
+ result = await host.run_once(prompt)
1039
+ finally:
1040
+ # Startup can fail before run_once reaches its cleanup block.
1041
+ await host.close()
1042
+ except (Exception, asyncio.CancelledError, KeyboardInterrupt) as exc:
1043
+ cancelled = isinstance(exc, (asyncio.CancelledError, KeyboardInterrupt))
1044
+ code = EXIT_SIGINT if cancelled else EXIT_CONFIG if isinstance(exc, ConfigError) else EXIT_AGENT
1045
+ explanation = "cancelled" if cancelled else safe_error_message(exc)
1046
+ previous = getattr(host, "last_result", None)
1047
+ result = replace(
1048
+ previous if isinstance(previous, HostResult) else HostResult(code, session_id=session_id),
1049
+ exit_code=code, explanation=explanation,
1050
+ status="cancelled" if cancelled else "failed",
1051
+ )
1052
+ if cancelled:
1053
+ if host is not None:
1054
+ host.cancel_active_turn()
1055
+ else:
1056
+ click.echo(f"error: {explanation}", err=True)
1057
+ if json_output:
1058
+ click.echo(json.dumps(asdict(result), ensure_ascii=False))
1059
+ return result.exit_code
1038
1060
 
1039
1061
 
1040
1062
  def main(argv: list[str] | None = None) -> None:
@@ -9,7 +9,7 @@ import logging
9
9
  import re
10
10
  import shlex
11
11
  from collections.abc import Coroutine
12
- from dataclasses import dataclass
12
+ from dataclasses import dataclass, field
13
13
  from pathlib import Path
14
14
  from typing import TYPE_CHECKING, Any, Literal
15
15
 
@@ -197,6 +197,10 @@ class HostResult:
197
197
  exit_code: int
198
198
  explanation: str = ""
199
199
  session_id: str | None = None
200
+ status: Literal["completed", "needs_input", "failed", "cancelled"] = "completed"
201
+ run_id: str | None = None
202
+ usage: dict[str, int | float] | None = None
203
+ checks: list[dict[str, Any]] = field(default_factory=list)
200
204
 
201
205
 
202
206
  @dataclass(frozen=True)
@@ -238,6 +242,7 @@ class AgentHost:
238
242
  self._hooks: Any = None
239
243
  self._checkpoints: Any = None
240
244
  self.last_checkpoint: dict[str, Any] | None = None
245
+ self.last_result: HostResult | None = None
241
246
  self._post_hook_tasks: list[asyncio.Task[Any]] = []
242
247
  self._trace_info = "session jsonl"
243
248
  self._telemetry = AgentTelemetry()
@@ -750,7 +755,7 @@ class AgentHost:
750
755
  prompt = " ".join(str(activity.get("prompt", "")).split())[:80]
751
756
  duration = float(activity.get("duration", 0.0) or 0.0)
752
757
  detail = f" · {prompt}" if prompt else ""
753
- elapsed = f" · {duration:.1f}s" if state in {"completed", "failed", "cancelled"} else ""
758
+ elapsed = f" · {duration:.1f}s" if state in {"completed", "needs_input", "failed", "cancelled"} else ""
754
759
  self.ui.render(
755
760
  HostEvent(
756
761
  HostEventKind.STATUS,
@@ -2493,6 +2498,9 @@ class AgentHost:
2493
2498
  explanation = ""
2494
2499
  run_state: Any = "running"
2495
2500
  telemetry_error_type = ""
2501
+ turn_result = HostResult(
2502
+ exit_code=0, session_id=self.meta.session_id if self.meta else None, run_id=run_id,
2503
+ )
2496
2504
  try:
2497
2505
  agent.journal.begin_turn()
2498
2506
  if recovery:
@@ -2508,11 +2516,7 @@ class AgentHost:
2508
2516
  exit_code = 1
2509
2517
  run_state = "failed"
2510
2518
  explanation = "No waiting prompt could be expanded"
2511
- return HostResult(
2512
- exit_code=exit_code,
2513
- explanation=explanation,
2514
- session_id=self.meta.session_id if self.meta else None,
2515
- )
2519
+ return turn_result
2516
2520
  else:
2517
2521
  self._deliver_expanded(agent, self._expand_user_text(text, attach_paths))
2518
2522
 
@@ -2590,9 +2594,11 @@ class AgentHost:
2590
2594
  "and consume any relevant logs, then continue the task.",
2591
2595
  )
2592
2596
  continue
2593
- else:
2597
+ elif kind == RespondReason.DONE:
2594
2598
  exit_code = 0
2595
2599
  run_state = "completed"
2600
+ else:
2601
+ raise RuntimeError(f"Agent returned an invalid stop reason: {kind!r}")
2596
2602
  except PermissionError as exc:
2597
2603
  exit_code = 3
2598
2604
  run_state = "failed"
@@ -2665,6 +2671,8 @@ class AgentHost:
2665
2671
  )
2666
2672
  except Exception as persist_error:
2667
2673
  run_state = "failed"
2674
+ exit_code = 1
2675
+ explanation = safe_error_message(persist_error)
2668
2676
  telemetry_error_type = type(persist_error).__name__
2669
2677
  if runtime is not None and run_id is not None:
2670
2678
  with contextlib.suppress(Exception):
@@ -2680,12 +2688,21 @@ class AgentHost:
2680
2688
  outcome=outcome,
2681
2689
  error_type=telemetry_error_type,
2682
2690
  )
2691
+ turn_result.exit_code = exit_code or (1 if outcome == "failed" else 0)
2692
+ turn_result.explanation = explanation
2693
+ turn_result.status = (
2694
+ "needs_input" if outcome == "waiting_user" else
2695
+ "completed" if outcome == "completed" else
2696
+ "cancelled" if outcome == "cancelled" else "failed"
2697
+ )
2698
+ turn_result.usage = self.usage_snapshot().to_dict()
2699
+ self.last_result = turn_result
2683
2700
  self._current_run_id = None
2684
- return HostResult(
2685
- exit_code=exit_code,
2686
- explanation=explanation,
2687
- session_id=self.meta.session_id if self.meta else None,
2688
- )
2701
+ ledger = getattr(agent.ws, "_verification", None)
2702
+ if ledger is not None:
2703
+ with contextlib.suppress(Exception):
2704
+ turn_result.checks = await ledger.snapshot(since=0.0)
2705
+ return turn_result
2689
2706
 
2690
2707
  def _background_task_origin(self, agent: Any) -> _BackgroundTaskOrigin:
2691
2708
  if self.meta is None:
@@ -2909,6 +2926,7 @@ class AgentHost:
2909
2926
 
2910
2927
  async def run_once(self, prompt: str) -> HostResult:
2911
2928
  await self.start()
2929
+ ledger = getattr(self.agent.ws, "_verification", None)
2912
2930
  try:
2913
2931
  if self.config.auto_approve:
2914
2932
 
@@ -2926,7 +2944,15 @@ class AgentHost:
2926
2944
  self.agent.approvals.set_handler(_reject)
2927
2945
 
2928
2946
  await self.resume_interrupted_run()
2929
- result = await self._run_user_turn(prompt)
2947
+ continuable = self._runtime.latest_incomplete_run() if self._runtime else None
2948
+ run_id = continuable.run_id if continuable and continuable.state == "waiting_user" else None
2949
+ result = await self._run_user_turn(prompt, run_id=run_id)
2930
2950
  return result
2931
2951
  finally:
2932
- await self.close()
2952
+ try:
2953
+ await self.close()
2954
+ finally:
2955
+ # Teardown may terminate a check that was still running at DONE.
2956
+ if self.last_result is not None and ledger is not None:
2957
+ with contextlib.suppress(Exception):
2958
+ self.last_result.checks = await ledger.snapshot(since=0.0)
@@ -21,6 +21,7 @@ from typing import TYPE_CHECKING, Annotated, Any
21
21
  from nooa import Skill, hidden, spec
22
22
 
23
23
  from noah_code.tools.workspace_tools import WorkspaceTools
24
+ from noah_code.verification import CheckRecord
24
25
 
25
26
  if TYPE_CHECKING:
26
27
  from noah_code.runtime_state import RuntimeStateStore
@@ -56,6 +57,7 @@ class BackgroundJob:
56
57
  tasks: list[asyncio.Task[Any]] = field(default_factory=list)
57
58
  command_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
58
59
  output_event: asyncio.Event = field(default_factory=asyncio.Event)
60
+ check: CheckRecord | None = None
59
61
 
60
62
  @property
61
63
  def elapsed(self) -> float:
@@ -201,13 +203,20 @@ class ProcessTools(Skill):
201
203
  payload = f"{command}\r\necho {marker}:%errorlevel%\r\n"
202
204
  else:
203
205
  payload = f"{command}\nprintf '\\n{marker}:%s\\n' \"$?\"\n"
204
- await self._write_input(job, payload)
205
- output, returncode = await self._wait_for_terminal_marker(
206
- job,
207
- cursor=cursor,
208
- marker=marker,
209
- timeout=wait_timeout,
206
+ check = await self._ws._verification.begin(
207
+ command, source=f"{self._ws._verification_source}:terminal:{job.name}"
210
208
  )
209
+ returncode = None
210
+ try:
211
+ await self._write_input(job, payload)
212
+ output, returncode = await self._wait_for_terminal_marker(
213
+ job,
214
+ cursor=cursor,
215
+ marker=marker,
216
+ timeout=wait_timeout,
217
+ )
218
+ finally:
219
+ await self._ws._verification.finish(check, returncode)
211
220
  bounded = output.strip()
212
221
  if len(bounded) > 32_000:
213
222
  bounded = bounded[:16_000] + "\n… terminal output bounded …\n" + bounded[-16_000:]
@@ -253,16 +262,24 @@ class ProcessTools(Skill):
253
262
  for job in self._jobs.values()
254
263
  ):
255
264
  raise ValueError(f"terminal name already exists: {name}")
256
- process = await asyncio.create_subprocess_exec(
257
- *argv,
265
+ check = await self._ws._verification.begin(
266
+ command, source=f"{self._ws._verification_source}:job:{name}",
258
267
  cwd=self._ws._workspace.root,
259
- stdin=asyncio.subprocess.PIPE,
260
- stdout=asyncio.subprocess.PIPE,
261
- stderr=(
262
- asyncio.subprocess.STDOUT if kind == "terminal" else asyncio.subprocess.PIPE
263
- ),
264
- start_new_session=os.name != "nt",
265
- )
268
+ ) if kind == "process" else None
269
+ try:
270
+ process = await asyncio.create_subprocess_exec(
271
+ *argv,
272
+ cwd=self._ws._workspace.root,
273
+ stdin=asyncio.subprocess.PIPE,
274
+ stdout=asyncio.subprocess.PIPE,
275
+ stderr=(
276
+ asyncio.subprocess.STDOUT if kind == "terminal" else asyncio.subprocess.PIPE
277
+ ),
278
+ start_new_session=os.name != "nt",
279
+ )
280
+ except BaseException:
281
+ await self._ws._verification.finish(check, None)
282
+ raise
266
283
  job_id = uuid.uuid4().hex[:8]
267
284
  log_path = self._runtime.process_log_dir / f"{job_id}.jsonl" if self._runtime else None
268
285
  job = BackgroundJob(
@@ -272,6 +289,7 @@ class ProcessTools(Skill):
272
289
  process=process,
273
290
  log_path=log_path,
274
291
  kind=kind,
292
+ check=check,
275
293
  )
276
294
  self._jobs[job_id] = job
277
295
  try:
@@ -287,6 +305,7 @@ class ProcessTools(Skill):
287
305
  log_path=log_path or self._runtime.process_log_dir / f"{job_id}.jsonl",
288
306
  )
289
307
  except Exception:
308
+ await self._ws._verification.finish(job.check, None)
290
309
  with contextlib.suppress(Exception):
291
310
  await self._terminate(job)
292
311
  self._close_transport(process)
@@ -671,6 +690,9 @@ class ProcessTools(Skill):
671
690
  elif job.state == "stopping":
672
691
  job.state = "stopped"
673
692
  job.returncode = returncode
693
+ await self._ws._verification.finish(
694
+ job.check, returncode if job.state in {"completed", "failed"} else None
695
+ )
674
696
  job.finished_at = time.monotonic()
675
697
  job.output_event.set()
676
698
  if self._runtime is not None: