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.
- {noah_code-0.6.0 → noah_code-0.7.0}/PKG-INFO +17 -1
- {noah_code-0.6.0 → noah_code-0.7.0}/README.md +16 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/interactive-reference.md +29 -5
- noah_code-0.7.0/docs/releases/v0.7.0.md +51 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/reliability.md +22 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/pyproject.toml +1 -1
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/__init__.py +1 -1
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/agent.py +4 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/cli.py +60 -38
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/host.py +41 -15
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/process_tools.py +37 -15
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/task_tools.py +30 -11
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/workspace_tools.py +37 -10
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/ui/textual_app.py +123 -108
- noah_code-0.7.0/src/noah_code/verification.py +312 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_cli.py +76 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_host.py +48 -0
- noah_code-0.7.0/tests/test_run_exit.py +191 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_task_tools.py +37 -1
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_textual_tui.py +155 -13
- noah_code-0.7.0/tests/test_tui_editing.py +135 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_tui_review_receipt.py +67 -28
- noah_code-0.7.0/tests/test_verification.py +262 -0
- noah_code-0.7.0/tests/test_verification_tools.py +119 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/uv.lock +1 -1
- noah_code-0.6.0/tests/test_run_exit.py +0 -113
- {noah_code-0.6.0 → noah_code-0.7.0}/.github/workflows/ci.yml +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/.github/workflows/release.yml +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/.gitignore +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/.pre-commit-config.yaml +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/assets/noah-in-action.svg +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/assets/noah-logo.svg +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/configuration.md +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/development.md +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/extensions.md +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.1.0.md +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.1.1.md +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.2.0.md +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.2.1.md +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.2.2.md +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.2.3.md +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.2.4.md +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.3.0.md +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.4.0.md +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.4.1.md +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.5.0.md +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.5.1.md +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/releases/v0.6.0.md +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/docs/security.md +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/install.sh +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/__main__.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/agents.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/approvals.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/budget.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/checkpoints.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/commands.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/composer.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/config.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/credentials.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/custom_commands.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/event_bridge.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/events.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/github.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/hooks.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/llm.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/llm_replies.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/macos_sandbox.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/mcp_setup.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/nooa_compat.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/permissions.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/predict.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/project_notes.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/providers.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/redaction.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/runtime_state.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/secure_files.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/sessions.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/skills_setup.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/snapshots.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/steer.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/summarization.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/telemetry.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/themes.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tool_output.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/__init__.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/diff_tools.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/git_tools.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/github_tools.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/lsp_tools.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/media_tools.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/memory_tools.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/plan_tools.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/question_tools.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/tools/web_tools.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/ui/__init__.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/ui/console.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/ui/protocol.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/ui/textual.css +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/updates.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/usage.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/workspace.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/src/noah_code/worktree.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/conftest.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_agent_security.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_agents.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_approvals.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_budget.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_cache_context.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_checkpoints.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_commands.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_compaction_eviction.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_composer.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_config.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_console_ui.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_credentials.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_custom_commands.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_diff_tools.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_efficiency.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_event_bridge.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_event_bridge_and_shell.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_git_tools.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_github.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_hooks.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_installer.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_lean_reasoning.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_llm.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_llm_replies.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_lsp_tools.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_mcp_setup.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_nooa_migration.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_permissions.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_plan_memory_tools.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_process_tools.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_project_notes.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_prompt_scorecard.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_providers.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_question_tools.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_redaction.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_runtime_state.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_sessions.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_skills_setup.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_snapshots.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_steer.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_summarization.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_telemetry.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_tool_output.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_tui_composer_queue.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_tui_history_dialogs.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_tui_layout.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_updates.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_wave1_e2e.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_web_tools.py +0 -0
- {noah_code-0.6.0 → noah_code-0.7.0}/tests/test_workspace_tools.py +0 -0
- {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.
|
|
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` |
|
|
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` |
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
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,
|
|
@@ -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
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
205
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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:
|