@nathapp/nax-agent 0.1.0
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.
- package/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/coding-tools/coding-tool-bash.d.ts +46 -0
- package/dist/coding-tools/coding-tool-bash.js +44 -0
- package/dist/coding-tools/coding-tool-extras.d.ts +35 -0
- package/dist/coding-tools/coding-tool-extras.js +56 -0
- package/dist/coding-tools/coding-tool-sandbox.d.ts +74 -0
- package/dist/coding-tools/coding-tool-sandbox.js +215 -0
- package/dist/coding-tools/coding-tool-support.d.ts +132 -0
- package/dist/coding-tools/coding-tool-support.js +159 -0
- package/dist/coding-tools/universal-coding-tools.d.ts +11 -0
- package/dist/coding-tools/universal-coding-tools.js +5 -0
- package/dist/command-interceptor/index.d.ts +55 -0
- package/dist/command-interceptor/index.js +60 -0
- package/dist/command-interceptor/shell.d.ts +36 -0
- package/dist/command-interceptor/shell.js +137 -0
- package/dist/command-safety/build.d.ts +19 -0
- package/dist/command-safety/build.js +47 -0
- package/dist/command-safety/guard.d.ts +32 -0
- package/dist/command-safety/guard.js +179 -0
- package/dist/command-safety/identifiers.d.ts +24 -0
- package/dist/command-safety/identifiers.js +20 -0
- package/dist/command-safety/index.d.ts +17 -0
- package/dist/command-safety/index.js +11 -0
- package/dist/command-safety/questions.d.ts +19 -0
- package/dist/command-safety/questions.js +60 -0
- package/dist/command-safety/row.d.ts +2 -0
- package/dist/command-safety/row.js +14 -0
- package/dist/command-safety/rule-scorer.d.ts +9 -0
- package/dist/command-safety/rule-scorer.js +391 -0
- package/dist/command-safety/shadow.d.ts +33 -0
- package/dist/command-safety/shadow.js +188 -0
- package/dist/command-safety/systemone-client.d.ts +16 -0
- package/dist/command-safety/systemone-client.js +92 -0
- package/dist/command-safety/tap.d.ts +37 -0
- package/dist/command-safety/tap.js +77 -0
- package/dist/command-safety/temp-only.d.ts +6 -0
- package/dist/command-safety/temp-only.js +215 -0
- package/dist/command-safety/tmp-write.d.ts +2 -0
- package/dist/command-safety/tmp-write.js +112 -0
- package/dist/command-safety/types.d.ts +185 -0
- package/dist/command-safety/types.js +21 -0
- package/dist/config/bash-approval.d.ts +33 -0
- package/dist/config/bash-approval.js +30 -0
- package/dist/config/catalog-overrides.d.ts +94 -0
- package/dist/config/catalog-overrides.js +7 -0
- package/dist/config/native-agent/index.d.ts +8 -0
- package/dist/config/native-agent/index.js +8 -0
- package/dist/config/schemas-sandbox.d.ts +31 -0
- package/dist/config/schemas-sandbox.js +45 -0
- package/dist/cost/core/index.d.ts +8 -0
- package/dist/cost/core/index.js +7 -0
- package/dist/cost/estimate.d.ts +34 -0
- package/dist/cost/estimate.js +65 -0
- package/dist/cost/model-spec.d.ts +28 -0
- package/dist/cost/model-spec.js +29 -0
- package/dist/cost/standard-types.d.ts +9 -0
- package/dist/cost/standard-types.js +1 -0
- package/dist/cost/usage-math.d.ts +18 -0
- package/dist/cost/usage-math.js +46 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +21 -0
- package/dist/infra/agent-logger.d.ts +22 -0
- package/dist/infra/agent-logger.js +19 -0
- package/dist/infra/credentials-config.d.ts +20 -0
- package/dist/infra/credentials-config.js +15 -0
- package/dist/infra/errors.d.ts +9 -0
- package/dist/infra/errors.js +11 -0
- package/dist/infra/index.d.ts +7 -0
- package/dist/infra/index.js +7 -0
- package/dist/infra/nax-error.d.ts +10 -0
- package/dist/infra/nax-error.js +16 -0
- package/dist/infra/spin-breaker/hash.d.ts +2 -0
- package/dist/infra/spin-breaker/hash.js +5 -0
- package/dist/infra/spin-breaker/index.d.ts +120 -0
- package/dist/infra/spin-breaker/index.js +340 -0
- package/dist/internal/agent-output-env.d.ts +44 -0
- package/dist/internal/agent-output-env.js +57 -0
- package/dist/internal/argv-exec.d.ts +58 -0
- package/dist/internal/argv-exec.js +224 -0
- package/dist/internal/bounded-io.d.ts +24 -0
- package/dist/internal/bounded-io.js +51 -0
- package/dist/internal/command-spec/index.d.ts +29 -0
- package/dist/internal/command-spec/index.js +36 -0
- package/dist/internal/describe-value-type.d.ts +8 -0
- package/dist/internal/describe-value-type.js +14 -0
- package/dist/internal/exec-framing.d.ts +18 -0
- package/dist/internal/exec-framing.js +22 -0
- package/dist/internal/file-lock.d.ts +91 -0
- package/dist/internal/file-lock.js +212 -0
- package/dist/internal/file-size.d.ts +2 -0
- package/dist/internal/file-size.js +12 -0
- package/dist/internal/git-add.d.ts +43 -0
- package/dist/internal/git-add.js +59 -0
- package/dist/internal/git-env.d.ts +59 -0
- package/dist/internal/git-env.js +93 -0
- package/dist/internal/git-exec.d.ts +58 -0
- package/dist/internal/git-exec.js +115 -0
- package/dist/internal/path-file-lock.d.ts +23 -0
- package/dist/internal/path-file-lock.js +25 -0
- package/dist/internal/process-alive.d.ts +42 -0
- package/dist/internal/process-alive.js +56 -0
- package/dist/internal/process-kill.d.ts +27 -0
- package/dist/internal/process-kill.js +54 -0
- package/dist/internal/realpath.d.ts +29 -0
- package/dist/internal/realpath.js +52 -0
- package/dist/internal/redact.d.ts +40 -0
- package/dist/internal/redact.js +161 -0
- package/dist/internal/shell-quote.d.ts +2 -0
- package/dist/internal/shell-quote.js +4 -0
- package/dist/internal/sort.d.ts +20 -0
- package/dist/internal/sort.js +20 -0
- package/dist/internal/strip-control-chars.d.ts +19 -0
- package/dist/internal/strip-control-chars.js +38 -0
- package/dist/internal/thenable.d.ts +10 -0
- package/dist/internal/thenable.js +15 -0
- package/dist/internal.d.ts +91 -0
- package/dist/internal.js +91 -0
- package/dist/native/adapter-deps.d.ts +46 -0
- package/dist/native/adapter-deps.js +43 -0
- package/dist/native/auth-types.d.ts +70 -0
- package/dist/native/auth-types.js +13 -0
- package/dist/native/auth.d.ts +133 -0
- package/dist/native/auth.js +362 -0
- package/dist/native/client.d.ts +50 -0
- package/dist/native/client.js +174 -0
- package/dist/native/complete.d.ts +33 -0
- package/dist/native/complete.js +88 -0
- package/dist/native/credentials/chained-store.d.ts +30 -0
- package/dist/native/credentials/chained-store.js +68 -0
- package/dist/native/credentials/change-guard.d.ts +51 -0
- package/dist/native/credentials/change-guard.js +136 -0
- package/dist/native/credentials/exec-source.d.ts +50 -0
- package/dist/native/credentials/exec-source.js +222 -0
- package/dist/native/credentials/fingerprint.d.ts +26 -0
- package/dist/native/credentials/fingerprint.js +174 -0
- package/dist/native/credentials/helper-process.d.ts +79 -0
- package/dist/native/credentials/helper-process.js +259 -0
- package/dist/native/credentials/helper-protocol.d.ts +36 -0
- package/dist/native/credentials/helper-protocol.js +79 -0
- package/dist/native/credentials/index.d.ts +60 -0
- package/dist/native/credentials/index.js +184 -0
- package/dist/native/errors.d.ts +34 -0
- package/dist/native/errors.js +109 -0
- package/dist/native/index.d.ts +25 -0
- package/dist/native/index.js +23 -0
- package/dist/native/model-resolver.d.ts +57 -0
- package/dist/native/model-resolver.js +73 -0
- package/dist/native/models.d.ts +116 -0
- package/dist/native/models.js +183 -0
- package/dist/native/session/ask-human.d.ts +15 -0
- package/dist/native/session/ask-human.js +22 -0
- package/dist/native/session/compaction.d.ts +94 -0
- package/dist/native/session/compaction.js +170 -0
- package/dist/native/session/handle-invalid-tool-call.d.ts +111 -0
- package/dist/native/session/handle-invalid-tool-call.js +159 -0
- package/dist/native/session/loop-events/cache-boundary.d.ts +11 -0
- package/dist/native/session/loop-events/cache-boundary.js +29 -0
- package/dist/native/session/loop-events/external-handler.d.ts +38 -0
- package/dist/native/session/loop-events/external-handler.js +193 -0
- package/dist/native/session/loop-events/index.d.ts +16 -0
- package/dist/native/session/loop-events/index.js +15 -0
- package/dist/native/session/loop-events/payload-guard.d.ts +9 -0
- package/dist/native/session/loop-events/payload-guard.js +31 -0
- package/dist/native/session/loop-events/registry.d.ts +84 -0
- package/dist/native/session/loop-events/registry.js +305 -0
- package/dist/native/session/loop-events/types.d.ts +251 -0
- package/dist/native/session/loop-events/types.js +14 -0
- package/dist/native/session/loop-handlers.d.ts +73 -0
- package/dist/native/session/loop-handlers.js +137 -0
- package/dist/native/session/nudge.d.ts +29 -0
- package/dist/native/session/nudge.js +35 -0
- package/dist/native/session/rate-provenance.d.ts +17 -0
- package/dist/native/session/rate-provenance.js +39 -0
- package/dist/native/session/session.d.ts +139 -0
- package/dist/native/session/session.js +208 -0
- package/dist/native/session/tool-input-exemplar.d.ts +2 -0
- package/dist/native/session/tool-input-exemplar.js +66 -0
- package/dist/native/session/tool-input-validation.d.ts +15 -0
- package/dist/native/session/tool-input-validation.js +146 -0
- package/dist/native/session/tool-mapping.d.ts +18 -0
- package/dist/native/session/tool-mapping.js +27 -0
- package/dist/native/session/tool-result.d.ts +37 -0
- package/dist/native/session/tool-result.js +25 -0
- package/dist/native/session/transcript-store.d.ts +77 -0
- package/dist/native/session/transcript-store.js +200 -0
- package/dist/native/session/truncation-handler.d.ts +47 -0
- package/dist/native/session/truncation-handler.js +83 -0
- package/dist/native/session/turn-accumulator.d.ts +45 -0
- package/dist/native/session/turn-accumulator.js +67 -0
- package/dist/native/session/turn-ask-human.d.ts +23 -0
- package/dist/native/session/turn-ask-human.js +41 -0
- package/dist/native/session/turn-compaction-step.d.ts +74 -0
- package/dist/native/session/turn-compaction-step.js +128 -0
- package/dist/native/session/turn-complete-step.d.ts +77 -0
- package/dist/native/session/turn-complete-step.js +159 -0
- package/dist/native/session/turn-end-event.d.ts +3 -0
- package/dist/native/session/turn-end-event.js +17 -0
- package/dist/native/session/turn-events.d.ts +63 -0
- package/dist/native/session/turn-events.js +48 -0
- package/dist/native/session/turn-loop-round-trip.d.ts +116 -0
- package/dist/native/session/turn-loop-round-trip.js +318 -0
- package/dist/native/session/turn-loop.d.ts +23 -0
- package/dist/native/session/turn-loop.js +250 -0
- package/dist/native/session/turn-result.d.ts +39 -0
- package/dist/native/session/turn-result.js +64 -0
- package/dist/native/session/turn-retry.d.ts +120 -0
- package/dist/native/session/turn-retry.js +182 -0
- package/dist/native/session/turn-tool-batch.d.ts +61 -0
- package/dist/native/session/turn-tool-batch.js +259 -0
- package/dist/native/session/turn-types.d.ts +164 -0
- package/dist/native/session/turn-types.js +36 -0
- package/dist/native/session-adapter.d.ts +56 -0
- package/dist/native/session-adapter.js +369 -0
- package/dist/native/session-affinity.d.ts +36 -0
- package/dist/native/session-affinity.js +43 -0
- package/dist/native/tier-providers.d.ts +18 -0
- package/dist/native/tier-providers.js +20 -0
- package/dist/permissions/approval-audit.d.ts +10 -0
- package/dist/permissions/approval-audit.js +22 -0
- package/dist/permissions/approvals-link.d.ts +20 -0
- package/dist/permissions/approvals-link.js +62 -0
- package/dist/permissions/approvals-store.d.ts +168 -0
- package/dist/permissions/approvals-store.js +259 -0
- package/dist/permissions/approvals-taint.d.ts +33 -0
- package/dist/permissions/approvals-taint.js +138 -0
- package/dist/permissions/ask-chain.d.ts +69 -0
- package/dist/permissions/ask-chain.js +32 -0
- package/dist/permissions/ask.d.ts +23 -0
- package/dist/permissions/ask.js +25 -0
- package/dist/permissions/bash-lex.d.ts +59 -0
- package/dist/permissions/bash-lex.js +279 -0
- package/dist/permissions/grammar.d.ts +12 -0
- package/dist/permissions/grammar.js +24 -0
- package/dist/permissions/index.d.ts +11 -0
- package/dist/permissions/index.js +9 -0
- package/dist/permissions/secret-spans.d.ts +19 -0
- package/dist/permissions/secret-spans.js +88 -0
- package/dist/permissions/types.d.ts +85 -0
- package/dist/permissions/types.js +1 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/node-glob.d.ts +3 -0
- package/dist/runtime/node-glob.js +83 -0
- package/dist/runtime/node-runtime.d.ts +3 -0
- package/dist/runtime/node-runtime.js +126 -0
- package/dist/runtime/slot.d.ts +11 -0
- package/dist/runtime/slot.js +17 -0
- package/dist/runtime/types.d.ts +51 -0
- package/dist/runtime/types.js +1 -0
- package/dist/runtime/which.d.ts +8 -0
- package/dist/runtime/which.js +32 -0
- package/dist/sandbox/argv-quote.d.ts +11 -0
- package/dist/sandbox/argv-quote.js +13 -0
- package/dist/sandbox/defaults.d.ts +27 -0
- package/dist/sandbox/defaults.js +47 -0
- package/dist/sandbox/git-guards.d.ts +64 -0
- package/dist/sandbox/git-guards.js +161 -0
- package/dist/sandbox/index.d.ts +11 -0
- package/dist/sandbox/index.js +10 -0
- package/dist/sandbox/launcher.d.ts +37 -0
- package/dist/sandbox/launcher.js +160 -0
- package/dist/sandbox/messages.d.ts +17 -0
- package/dist/sandbox/messages.js +46 -0
- package/dist/sandbox/policy-builder.d.ts +44 -0
- package/dist/sandbox/policy-builder.js +149 -0
- package/dist/sandbox/policy-inputs.d.ts +53 -0
- package/dist/sandbox/policy-inputs.js +76 -0
- package/dist/sandbox/probe.d.ts +8 -0
- package/dist/sandbox/probe.js +66 -0
- package/dist/sandbox/registry.d.ts +17 -0
- package/dist/sandbox/registry.js +57 -0
- package/dist/sandbox/session-tmp.d.ts +20 -0
- package/dist/sandbox/session-tmp.js +95 -0
- package/dist/sandbox/srt-backend.d.ts +21 -0
- package/dist/sandbox/srt-backend.js +99 -0
- package/dist/sandbox/types.d.ts +102 -0
- package/dist/sandbox/types.js +1 -0
- package/dist/session/adapter-failure.d.ts +60 -0
- package/dist/session/adapter-failure.js +11 -0
- package/dist/session/agent-stream-events.d.ts +103 -0
- package/dist/session/agent-stream-events.js +6 -0
- package/dist/session/interaction-handler.d.ts +53 -0
- package/dist/session/interaction-handler.js +2 -0
- package/dist/session/no-op-interaction-handler.d.ts +9 -0
- package/dist/session/no-op-interaction-handler.js +11 -0
- package/dist/session/protocol-types.d.ts +12 -0
- package/dist/session/protocol-types.js +1 -0
- package/dist/session/session-types.d.ts +456 -0
- package/dist/session/session-types.js +64 -0
- package/dist/session/tool-descriptor.d.ts +23 -0
- package/dist/session/tool-descriptor.js +5 -0
- package/dist/session/turn-deadline.d.ts +17 -0
- package/dist/session/turn-deadline.js +23 -0
- package/dist/tools/ask-request.d.ts +35 -0
- package/dist/tools/ask-request.js +57 -0
- package/dist/tools/bash-cwd.d.ts +82 -0
- package/dist/tools/bash-cwd.js +55 -0
- package/dist/tools/bash.d.ts +69 -0
- package/dist/tools/bash.js +283 -0
- package/dist/tools/delete.d.ts +57 -0
- package/dist/tools/delete.js +154 -0
- package/dist/tools/denial-redirect.d.ts +42 -0
- package/dist/tools/denial-redirect.js +266 -0
- package/dist/tools/deny-paths.d.ts +35 -0
- package/dist/tools/deny-paths.js +76 -0
- package/dist/tools/edit-region.d.ts +50 -0
- package/dist/tools/edit-region.js +90 -0
- package/dist/tools/edit.d.ts +25 -0
- package/dist/tools/edit.js +114 -0
- package/dist/tools/exec-allowlist-text.d.ts +18 -0
- package/dist/tools/exec-allowlist-text.js +24 -0
- package/dist/tools/exec-guard.d.ts +69 -0
- package/dist/tools/exec-guard.js +151 -0
- package/dist/tools/git-commit.d.ts +59 -0
- package/dist/tools/git-commit.js +220 -0
- package/dist/tools/git-flags/index.d.ts +10 -0
- package/dist/tools/git-flags/index.js +10 -0
- package/dist/tools/git.d.ts +88 -0
- package/dist/tools/git.js +397 -0
- package/dist/tools/glob.d.ts +29 -0
- package/dist/tools/glob.js +153 -0
- package/dist/tools/grep.d.ts +21 -0
- package/dist/tools/grep.js +162 -0
- package/dist/tools/index.d.ts +36 -0
- package/dist/tools/index.js +28 -0
- package/dist/tools/narrow-grants.d.ts +19 -0
- package/dist/tools/narrow-grants.js +54 -0
- package/dist/tools/nax-owned-writes.d.ts +135 -0
- package/dist/tools/nax-owned-writes.js +259 -0
- package/dist/tools/package-managers-table.d.ts +146 -0
- package/dist/tools/package-managers-table.js +325 -0
- package/dist/tools/package-managers-types.d.ts +36 -0
- package/dist/tools/package-managers-types.js +6 -0
- package/dist/tools/package-managers.d.ts +85 -0
- package/dist/tools/package-managers.js +391 -0
- package/dist/tools/path-list.d.ts +38 -0
- package/dist/tools/path-list.js +62 -0
- package/dist/tools/policy-bash-raw.d.ts +19 -0
- package/dist/tools/policy-bash-raw.js +210 -0
- package/dist/tools/policy-bash.d.ts +64 -0
- package/dist/tools/policy-bash.js +232 -0
- package/dist/tools/policy-command-branch.d.ts +32 -0
- package/dist/tools/policy-command-branch.js +63 -0
- package/dist/tools/policy-input.d.ts +2 -0
- package/dist/tools/policy-input.js +10 -0
- package/dist/tools/policy-match.d.ts +75 -0
- package/dist/tools/policy-match.js +117 -0
- package/dist/tools/policy-paths-branch.d.ts +97 -0
- package/dist/tools/policy-paths-branch.js +314 -0
- package/dist/tools/policy.d.ts +65 -0
- package/dist/tools/policy.js +232 -0
- package/dist/tools/protected-paths.d.ts +26 -0
- package/dist/tools/protected-paths.js +8 -0
- package/dist/tools/provider-adapt.d.ts +4 -0
- package/dist/tools/provider-adapt.js +32 -0
- package/dist/tools/provider-advertise.d.ts +39 -0
- package/dist/tools/provider-advertise.js +50 -0
- package/dist/tools/provider-grants.d.ts +36 -0
- package/dist/tools/provider-grants.js +72 -0
- package/dist/tools/provider-sanitize.d.ts +19 -0
- package/dist/tools/provider-sanitize.js +53 -0
- package/dist/tools/provider-types.d.ts +41 -0
- package/dist/tools/provider-types.js +26 -0
- package/dist/tools/read-continuation.d.ts +147 -0
- package/dist/tools/read-continuation.js +176 -0
- package/dist/tools/read-file.d.ts +32 -0
- package/dist/tools/read-file.js +85 -0
- package/dist/tools/read.d.ts +25 -0
- package/dist/tools/read.js +182 -0
- package/dist/tools/registry.d.ts +125 -0
- package/dist/tools/registry.js +67 -0
- package/dist/tools/request-capability.d.ts +13 -0
- package/dist/tools/request-capability.js +23 -0
- package/dist/tools/run-command-exec.d.ts +14 -0
- package/dist/tools/run-command-exec.js +161 -0
- package/dist/tools/run-command.d.ts +107 -0
- package/dist/tools/run-command.js +398 -0
- package/dist/tools/runtime-calltool.d.ts +146 -0
- package/dist/tools/runtime-calltool.js +223 -0
- package/dist/tools/runtime.d.ts +149 -0
- package/dist/tools/runtime.js +345 -0
- package/dist/tools/scratchpad.d.ts +20 -0
- package/dist/tools/scratchpad.js +197 -0
- package/dist/tools/spill.d.ts +81 -0
- package/dist/tools/spill.js +251 -0
- package/dist/tools/tool-audit.d.ts +171 -0
- package/dist/tools/tool-audit.js +183 -0
- package/dist/tools/truncate.d.ts +98 -0
- package/dist/tools/truncate.js +297 -0
- package/dist/tools/types.d.ts +145 -0
- package/dist/tools/types.js +24 -0
- package/dist/tools/verb-denial-argv-hint.d.ts +35 -0
- package/dist/tools/verb-denial-argv-hint.js +40 -0
- package/dist/tools/write.d.ts +9 -0
- package/dist/tools/write.js +45 -0
- package/package.json +54 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repetition breaker for an agent turn.
|
|
3
|
+
*
|
|
4
|
+
* A verifier session ran 36 minutes re-running the same PASSING scoped test
|
|
5
|
+
* 622 times, emitted no verdict, and was ended by an operator (nax#2013). Every
|
|
6
|
+
* call was fast and exited 0, so there was no timeout, no error rate and no
|
|
7
|
+
* failing gate to trip; what distinguished it was repetition without progress
|
|
8
|
+
* — 622 calls across 2 distinct argv shapes and no file writes.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately NOT a round-trip cap. That was removed twice on purpose
|
|
11
|
+
* (#1823/#1827/#1830, then #1819/#1820) and a session making 600 VARIED calls
|
|
12
|
+
* is working, not spinning. This counts only calls whose normalised shape was
|
|
13
|
+
* already seen.
|
|
14
|
+
*
|
|
15
|
+
* Transport-neutral and config-free by construction: it takes resolved
|
|
16
|
+
* settings, so `src/agents/native/` can consult it without reading NaxConfig
|
|
17
|
+
* (check:adapter-no-config-import).
|
|
18
|
+
*/
|
|
19
|
+
import { getSafeLogger } from "#src/infra/index";
|
|
20
|
+
import { byCodePoint } from "#src/internal/sort";
|
|
21
|
+
import { stripControlChars } from "#src/internal/strip-control-chars";
|
|
22
|
+
import { digest64 } from "./hash.js";
|
|
23
|
+
export const DEFAULT_SPIN_BREAKER_SETTINGS = Object.freeze({
|
|
24
|
+
enabled: true,
|
|
25
|
+
nudgeAfterRepeats: 25,
|
|
26
|
+
maxNudges: 3,
|
|
27
|
+
stopAfterRepeats: 50,
|
|
28
|
+
recentKeyWindow: 64,
|
|
29
|
+
stopAfterSameKeyRepeats: 12,
|
|
30
|
+
stopAfterNoProgressSeconds: 900,
|
|
31
|
+
});
|
|
32
|
+
/** Beyond this, the key is hashed — one large input must not grow the key set without bound. */
|
|
33
|
+
const MAX_KEY_BYTES = 512;
|
|
34
|
+
/**
|
|
35
|
+
* nax#2017: repeats of one call that must accumulate before the time axis may
|
|
36
|
+
* end the turn. A deliberate module constant, not a setting — a run of two or
|
|
37
|
+
* three calls is not a spin, however long each one took.
|
|
38
|
+
*/
|
|
39
|
+
const NO_PROGRESS_TIME_MIN_REPEATS = 5;
|
|
40
|
+
/** Deterministic regardless of property order, so a reordered input is the same call. */
|
|
41
|
+
function stableStringify(value) {
|
|
42
|
+
if (value === null || typeof value !== "object")
|
|
43
|
+
return JSON.stringify(value) ?? "null";
|
|
44
|
+
if (Array.isArray(value))
|
|
45
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
46
|
+
const entries = Object.entries(value).sort((a, b) => byCodePoint(a[0], b[0]));
|
|
47
|
+
return `{${entries.map(([key, val]) => `${JSON.stringify(key)}:${stableStringify(val)}`).join(",")}}`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Strips the tokens that differ between two byte-identical runs: elapsed
|
|
51
|
+
* times and clock timestamps (ANSI escapes are removed by
|
|
52
|
+
* `stripControlChars` first). Deliberately surgical rather than blanking
|
|
53
|
+
* every digit — "2 failed" -> "1 failed" is real progress and must survive
|
|
54
|
+
* normalisation, while "in 1.20s" -> "in 1.23s" must not.
|
|
55
|
+
*
|
|
56
|
+
* The single-letter `m`/`s` units are ambiguous with ordinary tokens
|
|
57
|
+
* (`file-2m.ts`, "expected 5 m"), so they are stripped only when the number
|
|
58
|
+
* is decimal (`1.2s`) or sits in a time context (`in`/`took`/`time`/
|
|
59
|
+
* `elapsed`/`duration`/`=`/`(`/`,`). `ms` and `min` are unambiguous and stay
|
|
60
|
+
* unconditional. The `ms|s|m|min` alternation is ordered so backtracking
|
|
61
|
+
* still reaches `min`.
|
|
62
|
+
*/
|
|
63
|
+
const DURATION_OR_TIMESTAMP = /\b\d+\.\d+\s?(?:ms|s|m|min)\b|(?:\b(?:in|took|time|elapsed|duration)\s?|[=(,]\s?)\d+\s?[ms]\b|\b\d+(?:\.\d+)?\s?(?:ms|min)\b|\b\d{2}:\d{2}:\d{2}(?:\.\d+)?\b|\d{4}-\d{2}-\d{2}T[\d:.]+Z?/g;
|
|
64
|
+
function resultDigest(text) {
|
|
65
|
+
const normalised = stripControlChars(text).replace(DURATION_OR_TIMESTAMP, "").replace(/\s+/g, " ").trim();
|
|
66
|
+
return digest64(normalised);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Tool names are fixed identifiers (e.g. "Read", "RunCommand") that never
|
|
70
|
+
* contain a space, so concatenating on one never lets `toolName="a:b"` +
|
|
71
|
+
* `body="c"` collide with `toolName="a"` + `body="b:c"` the way a colon would.
|
|
72
|
+
*/
|
|
73
|
+
const KEY_SEPARATOR = " ";
|
|
74
|
+
function callKey(toolName, input) {
|
|
75
|
+
const body = stableStringify(input);
|
|
76
|
+
const clipped = body.length > MAX_KEY_BYTES ? digest64(body) : body;
|
|
77
|
+
return `${toolName}${KEY_SEPARATOR}${clipped}`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Nudge points are derived from the three knobs rather than configured
|
|
81
|
+
* separately: with 25/50/3 they fall at 25, 33 and 42, leaving the stop at 50.
|
|
82
|
+
*/
|
|
83
|
+
function nudgePoints(settings) {
|
|
84
|
+
const span = settings.stopAfterRepeats - settings.nudgeAfterRepeats;
|
|
85
|
+
return Array.from({ length: settings.maxNudges }, (_unused, index) => Math.round(settings.nudgeAfterRepeats + (index * span) / settings.maxNudges));
|
|
86
|
+
}
|
|
87
|
+
const NUDGE_ESCALATION = [
|
|
88
|
+
"You have made {repeats} tool calls without issuing a new, distinct call. You are repeating work already done and the results are not changing. Stop re-running and produce your final answer now, in the exact format your instructions require.",
|
|
89
|
+
"You are still repeating the same calls ({repeats} with no new work). If you cannot conclude, say so explicitly in your final answer and stop — an explicit inability to conclude is a valid answer; repeating is not.",
|
|
90
|
+
"Final warning: {repeats} repeated calls with no progress. The next repeated call ends this session with no answer recorded. Produce your final answer now.",
|
|
91
|
+
];
|
|
92
|
+
/**
|
|
93
|
+
* Answered to every outstanding call when the breaker first decides to end a
|
|
94
|
+
* turn (nax#2120). The stop used to break with the call unexecuted and
|
|
95
|
+
* unanswered, so the turn was classified `fail-spin` and retried on the
|
|
96
|
+
* timeout lane — a FRESH session at a reduced budget, discarding everything
|
|
97
|
+
* the turn had accumulated. One terminal round trip lets the model close out
|
|
98
|
+
* instead. NUDGE_ESCALATION's final warning is one step ahead of the hard
|
|
99
|
+
* end, not literally true: the first stop only warns (this notice) and only a
|
|
100
|
+
* further stop verdict ends the turn.
|
|
101
|
+
*/
|
|
102
|
+
export const SPIN_TERMINAL_NOTICE = "[nax] This turn is ending: you repeated the same call with no change in its result. " +
|
|
103
|
+
"This call was not executed. Produce your final answer now, in the exact format your " +
|
|
104
|
+
"instructions require. Any further repeated call ends the turn with no answer recorded.";
|
|
105
|
+
const SPIN_RAW_BACKSTOP_TERMINAL_NOTICE = "[nax] This turn is ending: the same call reached the safety call limit even though its results changed. " +
|
|
106
|
+
"This call was not executed. Produce your final answer now, in the exact format your " +
|
|
107
|
+
"instructions require. Any further repeated call ends the turn with no answer recorded.";
|
|
108
|
+
/**
|
|
109
|
+
* nax#2017: the time axis fires on repetition alone — the ladder is spent on
|
|
110
|
+
* the repeat path, and AC1's own sequence never calls `noteResult` — so the
|
|
111
|
+
* generic notice would assert an unchanged result the breaker never observed.
|
|
112
|
+
* The same reasoning as the raw backstop's copy above: a stop message that
|
|
113
|
+
* claims the wrong diagnostic is worse than a blunter one.
|
|
114
|
+
*/
|
|
115
|
+
const SPIN_NO_PROGRESS_TIME_TERMINAL_NOTICE = "[nax] This turn is ending: you have repeated calls for too long with no new, distinct call. " +
|
|
116
|
+
"This call was not executed. Produce your final answer now, in the exact format your " +
|
|
117
|
+
"instructions require. Any further repeated call ends the turn with no answer recorded.";
|
|
118
|
+
/**
|
|
119
|
+
* Exhaustive, so a future stop reason cannot silently inherit copy written for
|
|
120
|
+
* a different diagnostic.
|
|
121
|
+
*/
|
|
122
|
+
const SPIN_TERMINAL_NOTICES = {
|
|
123
|
+
"repeat-run": SPIN_TERMINAL_NOTICE,
|
|
124
|
+
"same-key-cumulative": SPIN_TERMINAL_NOTICE,
|
|
125
|
+
"same-key-backstop": SPIN_RAW_BACKSTOP_TERMINAL_NOTICE,
|
|
126
|
+
"no-progress-time": SPIN_NO_PROGRESS_TIME_TERMINAL_NOTICE,
|
|
127
|
+
};
|
|
128
|
+
export function spinTerminalNotice(reason) {
|
|
129
|
+
return SPIN_TERMINAL_NOTICES[reason];
|
|
130
|
+
}
|
|
131
|
+
function nudgeText(nudgeNumber, repeats) {
|
|
132
|
+
const template = NUDGE_ESCALATION[Math.min(nudgeNumber, NUDGE_ESCALATION.length) - 1] ?? NUDGE_ESCALATION[0];
|
|
133
|
+
return (template ?? "").replace("{repeats}", String(repeats));
|
|
134
|
+
}
|
|
135
|
+
export function createSpinBreaker(settings, deps) {
|
|
136
|
+
const points = nudgePoints(settings);
|
|
137
|
+
/**
|
|
138
|
+
* nax#2017: injectable clock (milliseconds). The time axis needs a clock a
|
|
139
|
+
* test can drive; production takes the default (`Date.now`).
|
|
140
|
+
*/
|
|
141
|
+
const now = deps?.now ?? Date.now;
|
|
142
|
+
// Insertion-ordered and capped: a Map's iteration order gives the eviction
|
|
143
|
+
// order for free, so the window needs no second structure. The value carries
|
|
144
|
+
// the cumulative count and the same-result run, so a re-issued key reads as
|
|
145
|
+
// a continuation of its prior state — but only for keys still in the window:
|
|
146
|
+
// an evicted key is gone, and the next sighting will start fresh at 1. That
|
|
147
|
+
// is the same windowing tradeoff `newKeyEvents` already documents, just on
|
|
148
|
+
// the count axis.
|
|
149
|
+
const recent = new Map();
|
|
150
|
+
let repeatsSinceProgress = 0;
|
|
151
|
+
let totalCalls = 0;
|
|
152
|
+
let newKeyEvents = 0;
|
|
153
|
+
let maxRepeatRun = 0;
|
|
154
|
+
let maxSameKeyRepeats = 0;
|
|
155
|
+
/** Session-cumulative; what `summary()` reports. Telemetry meaning unchanged. */
|
|
156
|
+
let nudges = 0;
|
|
157
|
+
/**
|
|
158
|
+
* nax#2017: the budget the ladder actually spends, restored by sustained new
|
|
159
|
+
* work rather than by any single new key. Sessions are long-lived (nax#2047),
|
|
160
|
+
* so a session-lifetime budget left a turn that had already spent its three
|
|
161
|
+
* nudges with no warning left at all.
|
|
162
|
+
*/
|
|
163
|
+
let episodeNudges = 0;
|
|
164
|
+
/** `newKeyEvents` when the last nudge was built — the replenishment anchor. */
|
|
165
|
+
let newKeyEventsAtLastNudge = 0;
|
|
166
|
+
/** nax#2017: when a new call key was last seen; the time axis' origin. */
|
|
167
|
+
let lastProgressAt = now();
|
|
168
|
+
function remember(key) {
|
|
169
|
+
const record = { count: 1, sameResultRun: 0 };
|
|
170
|
+
recent.set(key, record);
|
|
171
|
+
newKeyEvents += 1;
|
|
172
|
+
if (recent.size > settings.recentKeyWindow) {
|
|
173
|
+
const oldest = recent.keys().next();
|
|
174
|
+
if (!oldest.done)
|
|
175
|
+
recent.delete(oldest.value);
|
|
176
|
+
}
|
|
177
|
+
return record;
|
|
178
|
+
}
|
|
179
|
+
function buildNudge(toolName, repeats) {
|
|
180
|
+
nudges += 1;
|
|
181
|
+
episodeNudges += 1;
|
|
182
|
+
newKeyEventsAtLastNudge = newKeyEvents;
|
|
183
|
+
getSafeLogger()?.warn("spin-breaker", "Repeated calls with no progress — nudging", {
|
|
184
|
+
tool: toolName,
|
|
185
|
+
repeats,
|
|
186
|
+
nudgeNumber: episodeNudges,
|
|
187
|
+
newKeyEvents,
|
|
188
|
+
});
|
|
189
|
+
return { action: "nudge", nudgeNumber: episodeNudges, repeats, text: nudgeText(episodeNudges, repeats) };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Reason-specific stop messages: the raw backstop fires precisely when the
|
|
193
|
+
* results ARE changing, so reusing the no-progress copy there would invert
|
|
194
|
+
* the one diagnostic that matters.
|
|
195
|
+
*/
|
|
196
|
+
const stopMessage = {
|
|
197
|
+
"repeat-run": "Ending the turn — repeated calls with no progress",
|
|
198
|
+
"same-key-cumulative": "Ending the turn — the same call returned the same result too many times",
|
|
199
|
+
"same-key-backstop": "Ending the turn — the same call repeated too many times (raw backstop; results were changing)",
|
|
200
|
+
"no-progress-time": "Ending the turn — repeated calls with no new call for too long",
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* The raw per-key backstop: a cap on the cumulative count, and that count
|
|
204
|
+
* includes the call being judged — `remember` seeds a new key at 1. It is
|
|
205
|
+
* therefore checked on the key's FIRST occurrence too, in both branches,
|
|
206
|
+
* rather than only when the key is seen again. For every supported cap
|
|
207
|
+
* (`stopAfterRepeats` has a schema minimum of 3) the first occurrence sits
|
|
208
|
+
* below it and nothing changes; a degenerate cap of 1 stops on the first
|
|
209
|
+
* call rather than the second, and that is the only case where the two
|
|
210
|
+
* readings differ.
|
|
211
|
+
*/
|
|
212
|
+
function backstopVerdict(record, toolName) {
|
|
213
|
+
if (settings.stopAfterSameKeyRepeats <= 0 || record.count < settings.stopAfterRepeats)
|
|
214
|
+
return undefined;
|
|
215
|
+
return stopOrNudge(toolName, record.count, "same-key-backstop", () => {
|
|
216
|
+
record.count = 0;
|
|
217
|
+
record.sameResultRun = 0;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* nax#2120: the escalation ladder must always render before a kill. The
|
|
222
|
+
* nudge points are derived from `repeatsSinceProgress`, but the cumulative
|
|
223
|
+
* per-key check runs on a different counter, so for a loop over 1-2
|
|
224
|
+
* already-seen keys the first nudge point was unreachable by arithmetic
|
|
225
|
+
* and the turn was killed cold with `nudges: 0`.
|
|
226
|
+
*
|
|
227
|
+
* Deliberately NOT a config refinement: reachability depends on the loop's
|
|
228
|
+
* CYCLE LENGTH, not on the knobs, so no cross-field inequality can express
|
|
229
|
+
* it. Spending a nudge instead costs at most `maxNudges` extra calls before
|
|
230
|
+
* a genuine nax#2047 kill, and makes `nudges: 0` on a stop unreachable for
|
|
231
|
+
* every configuration.
|
|
232
|
+
*
|
|
233
|
+
* `onStop` runs only when the verdict is a real stop — the Task 1 evidence
|
|
234
|
+
* reset must not fire on a downgrade, or the threshold could never be
|
|
235
|
+
* reached twice and the loop would nudge forever.
|
|
236
|
+
*
|
|
237
|
+
* A real stop also consumes `repeatsSinceProgress`, symmetric with the
|
|
238
|
+
* per-key reset. The breaker is session-scoped by design (nax#2047), and
|
|
239
|
+
* the first stop is now reprieved by the turn loop (nax#2120 Task 4): if
|
|
240
|
+
* the run counter stayed latched at the threshold, the next turn would stop
|
|
241
|
+
* cold on its first or second repeated call. It must re-accumulate in full.
|
|
242
|
+
*/
|
|
243
|
+
function stopOrNudge(toolName, repeats, reason, onStop) {
|
|
244
|
+
if (episodeNudges < settings.maxNudges)
|
|
245
|
+
return buildNudge(toolName, repeats);
|
|
246
|
+
onStop?.();
|
|
247
|
+
repeatsSinceProgress = 0;
|
|
248
|
+
// nax#2017: a real stop hands the next run a full ladder. The breaker is
|
|
249
|
+
// session-scoped (nax#2047), so a budget spent in one turn would otherwise
|
|
250
|
+
// deny every later turn its warnings — the shape this story exists to fix.
|
|
251
|
+
episodeNudges = 0;
|
|
252
|
+
getSafeLogger()?.error("spin-breaker", stopMessage[reason], {
|
|
253
|
+
tool: toolName,
|
|
254
|
+
repeats,
|
|
255
|
+
reason,
|
|
256
|
+
newKeyEvents,
|
|
257
|
+
totalCalls,
|
|
258
|
+
nudges,
|
|
259
|
+
});
|
|
260
|
+
return { action: "stop", repeats, reason };
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
observe(toolName, input) {
|
|
264
|
+
if (!settings.enabled)
|
|
265
|
+
return { action: "allow" };
|
|
266
|
+
totalCalls += 1;
|
|
267
|
+
const key = callKey(toolName, input);
|
|
268
|
+
const existing = recent.get(key);
|
|
269
|
+
if (existing === undefined) {
|
|
270
|
+
const created = remember(key);
|
|
271
|
+
repeatsSinceProgress = 0;
|
|
272
|
+
// nax#2017: a new call key is progress on the time axis...
|
|
273
|
+
lastProgressAt = now();
|
|
274
|
+
// ...and sustained new work replenishes the episode's nudge budget.
|
|
275
|
+
// Deliberately NOT every new key: a laundering loop that interleaves
|
|
276
|
+
// one fresh key between repeats of a single call (nax#2047) would then
|
|
277
|
+
// restore its own budget forever and never stop.
|
|
278
|
+
if (newKeyEvents - newKeyEventsAtLastNudge >= settings.nudgeAfterRepeats)
|
|
279
|
+
episodeNudges = 0;
|
|
280
|
+
// Judged on its own first occurrence — see `backstopVerdict`.
|
|
281
|
+
const firstBackstop = backstopVerdict(created, toolName);
|
|
282
|
+
return firstBackstop ?? { action: "allow" };
|
|
283
|
+
}
|
|
284
|
+
const record = existing;
|
|
285
|
+
record.count += 1;
|
|
286
|
+
if (record.count > maxSameKeyRepeats)
|
|
287
|
+
maxSameKeyRepeats = record.count;
|
|
288
|
+
// nax#2120: the cumulative threshold runs on the RESULT axis. Same call
|
|
289
|
+
// + same result N times is a spin whatever interleaved; same call with
|
|
290
|
+
// a changing result is an edit -> test loop, which is work. The raw
|
|
291
|
+
// count is kept as a backstop for a call whose result is unique every
|
|
292
|
+
// time, or which never reaches `noteResult` (a throw; a DENIAL does
|
|
293
|
+
// reach it) — `stopAfterRepeats` bounds it without adding a knob. The
|
|
294
|
+
// backstop fires while results are changing, hence its own reason.
|
|
295
|
+
if (settings.stopAfterSameKeyRepeats > 0 && record.sameResultRun >= settings.stopAfterSameKeyRepeats) {
|
|
296
|
+
return stopOrNudge(toolName, record.sameResultRun, "same-key-cumulative", () => {
|
|
297
|
+
record.count = 0;
|
|
298
|
+
record.sameResultRun = 0;
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
const backstop = backstopVerdict(record, toolName);
|
|
302
|
+
if (backstop !== undefined)
|
|
303
|
+
return backstop;
|
|
304
|
+
repeatsSinceProgress += 1;
|
|
305
|
+
if (repeatsSinceProgress > maxRepeatRun)
|
|
306
|
+
maxRepeatRun = repeatsSinceProgress;
|
|
307
|
+
// nax#2017: the time axis. A repeated call that takes ~40 s each reaches
|
|
308
|
+
// the tool-call-only idle watchdog before the repeat thresholds, and the
|
|
309
|
+
// watchdog's cancel is classified `fail-stale` — a retry on the timeout
|
|
310
|
+
// lane, discarding the turn. Ending it here instead makes it `fail-spin`.
|
|
311
|
+
// Checked before the repeat-run threshold so a slow spin ends on time
|
|
312
|
+
// rather than on a count it may never reach.
|
|
313
|
+
if (settings.stopAfterNoProgressSeconds > 0 &&
|
|
314
|
+
repeatsSinceProgress >= NO_PROGRESS_TIME_MIN_REPEATS &&
|
|
315
|
+
now() - lastProgressAt >= settings.stopAfterNoProgressSeconds * 1000) {
|
|
316
|
+
return stopOrNudge(toolName, repeatsSinceProgress, "no-progress-time");
|
|
317
|
+
}
|
|
318
|
+
if (repeatsSinceProgress >= settings.stopAfterRepeats) {
|
|
319
|
+
return stopOrNudge(toolName, repeatsSinceProgress, "repeat-run");
|
|
320
|
+
}
|
|
321
|
+
const isNudgePoint = points.includes(repeatsSinceProgress);
|
|
322
|
+
if (isNudgePoint && episodeNudges < settings.maxNudges)
|
|
323
|
+
return buildNudge(toolName, repeatsSinceProgress);
|
|
324
|
+
return { action: "allow" };
|
|
325
|
+
},
|
|
326
|
+
noteResult(toolName, input, resultText) {
|
|
327
|
+
if (!settings.enabled)
|
|
328
|
+
return;
|
|
329
|
+
const record = recent.get(callKey(toolName, input));
|
|
330
|
+
if (record === undefined)
|
|
331
|
+
return;
|
|
332
|
+
const digest = resultDigest(resultText);
|
|
333
|
+
record.sameResultRun = record.digest === digest ? record.sameResultRun + 1 : 1;
|
|
334
|
+
record.digest = digest;
|
|
335
|
+
},
|
|
336
|
+
summary() {
|
|
337
|
+
return { totalCalls, newKeyEvents, maxRepeatRun, maxSameKeyRepeats, nudges };
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-friendly output env for nax's own agent-facing spawn sites.
|
|
3
|
+
*
|
|
4
|
+
* `AGENT_OUTPUT_MARKERS` / `withAgentOutputEnv` are the rule the quality and
|
|
5
|
+
* verification runners already share; this module is where they live so the
|
|
6
|
+
* other spawn sites (Bash, RunCommand's Exec branch) can apply the identical
|
|
7
|
+
* rule instead of a second copy of it. `src/verification/executor.ts`
|
|
8
|
+
* re-exports both, so `src/quality/runner.ts` keeps importing them unchanged.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Env vars that tell an agent-aware test runner to emit failures-only output.
|
|
12
|
+
* `bun test` honours all three (https://bun.com/docs/test): failures keep their
|
|
13
|
+
* full code frame, diff and stack, and the summary line is preserved — only the
|
|
14
|
+
* per-test pass/skip/todo roll call is dropped.
|
|
15
|
+
*
|
|
16
|
+
* Exported so the quality runner applies the identical rule at its own spawn
|
|
17
|
+
* site; the two env paths must not drift.
|
|
18
|
+
*/
|
|
19
|
+
export declare const AGENT_OUTPUT_MARKERS: readonly ["CLAUDECODE", "REPL_ID", "AGENT"];
|
|
20
|
+
/**
|
|
21
|
+
* Add `AGENT=1` unless a marker is already present or the caller stripped it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function withAgentOutputEnv(env: Record<string, string | undefined>, strippedVars?: readonly string[]): Record<string, string | undefined>;
|
|
24
|
+
/** Where marker presence is read from; tests replace it. */
|
|
25
|
+
export declare const _agentOutputEnvDeps: {
|
|
26
|
+
processEnv: () => Record<string, string | undefined>;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* `{ AGENT: "1" }`, or undefined when a marker is already inherited or AGENT
|
|
30
|
+
* is stripped.
|
|
31
|
+
*
|
|
32
|
+
* The spawn sites here overlay a small record onto the child's inherited
|
|
33
|
+
* environment rather than normalizing a whole env, so "no overlay" has to be
|
|
34
|
+
* distinguishable from "an empty overlay": undefined means the caller passes
|
|
35
|
+
* no `env` key at all and the child inherits `process.env` untouched.
|
|
36
|
+
*
|
|
37
|
+
* Marker presence is read from the nax process environment — the SAME source
|
|
38
|
+
* `withAgentOutputEnv` reads from — so an inherited CLAUDECODE/REPL_ID speaks
|
|
39
|
+
* for itself, and a stripped AGENT stays stripped. Reading from the child's
|
|
40
|
+
* already-built env would not work: by the time the spawn site runs, the
|
|
41
|
+
* inherited env is what nax itself was launched under, and there is no second
|
|
42
|
+
* "real" source of truth to consult.
|
|
43
|
+
*/
|
|
44
|
+
export declare function agentOutputOverlay(strippedVars: readonly string[]): Readonly<Record<string, string>> | undefined;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-friendly output env for nax's own agent-facing spawn sites.
|
|
3
|
+
*
|
|
4
|
+
* `AGENT_OUTPUT_MARKERS` / `withAgentOutputEnv` are the rule the quality and
|
|
5
|
+
* verification runners already share; this module is where they live so the
|
|
6
|
+
* other spawn sites (Bash, RunCommand's Exec branch) can apply the identical
|
|
7
|
+
* rule instead of a second copy of it. `src/verification/executor.ts`
|
|
8
|
+
* re-exports both, so `src/quality/runner.ts` keeps importing them unchanged.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Env vars that tell an agent-aware test runner to emit failures-only output.
|
|
12
|
+
* `bun test` honours all three (https://bun.com/docs/test): failures keep their
|
|
13
|
+
* full code frame, diff and stack, and the summary line is preserved — only the
|
|
14
|
+
* per-test pass/skip/todo roll call is dropped.
|
|
15
|
+
*
|
|
16
|
+
* Exported so the quality runner applies the identical rule at its own spawn
|
|
17
|
+
* site; the two env paths must not drift.
|
|
18
|
+
*/
|
|
19
|
+
export const AGENT_OUTPUT_MARKERS = ["CLAUDECODE", "REPL_ID", "AGENT"];
|
|
20
|
+
/**
|
|
21
|
+
* Add `AGENT=1` unless a marker is already present or the caller stripped it.
|
|
22
|
+
*/
|
|
23
|
+
export function withAgentOutputEnv(env, strippedVars = []) {
|
|
24
|
+
if (strippedVars.includes("AGENT"))
|
|
25
|
+
return env;
|
|
26
|
+
if (AGENT_OUTPUT_MARKERS.some((marker) => env[marker] !== undefined))
|
|
27
|
+
return env;
|
|
28
|
+
return { ...env, AGENT: "1" };
|
|
29
|
+
}
|
|
30
|
+
/** Where marker presence is read from; tests replace it. */
|
|
31
|
+
export const _agentOutputEnvDeps = {
|
|
32
|
+
processEnv: () => process.env,
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* `{ AGENT: "1" }`, or undefined when a marker is already inherited or AGENT
|
|
36
|
+
* is stripped.
|
|
37
|
+
*
|
|
38
|
+
* The spawn sites here overlay a small record onto the child's inherited
|
|
39
|
+
* environment rather than normalizing a whole env, so "no overlay" has to be
|
|
40
|
+
* distinguishable from "an empty overlay": undefined means the caller passes
|
|
41
|
+
* no `env` key at all and the child inherits `process.env` untouched.
|
|
42
|
+
*
|
|
43
|
+
* Marker presence is read from the nax process environment — the SAME source
|
|
44
|
+
* `withAgentOutputEnv` reads from — so an inherited CLAUDECODE/REPL_ID speaks
|
|
45
|
+
* for itself, and a stripped AGENT stays stripped. Reading from the child's
|
|
46
|
+
* already-built env would not work: by the time the spawn site runs, the
|
|
47
|
+
* inherited env is what nax itself was launched under, and there is no second
|
|
48
|
+
* "real" source of truth to consult.
|
|
49
|
+
*/
|
|
50
|
+
export function agentOutputOverlay(strippedVars) {
|
|
51
|
+
if (strippedVars.includes("AGENT"))
|
|
52
|
+
return undefined;
|
|
53
|
+
const env = _agentOutputEnvDeps.processEnv();
|
|
54
|
+
if (AGENT_OUTPUT_MARKERS.some((marker) => env[marker] !== undefined))
|
|
55
|
+
return undefined;
|
|
56
|
+
return { AGENT: "1" };
|
|
57
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { killProcessGroup } from "./process-kill.ts";
|
|
2
|
+
export interface RunArgvOptions {
|
|
3
|
+
readonly argv: readonly string[];
|
|
4
|
+
readonly cwd: string;
|
|
5
|
+
readonly timeoutMs: number;
|
|
6
|
+
readonly stripEnvVars?: readonly string[];
|
|
7
|
+
/**
|
|
8
|
+
* Overlay applied on top of process.env, after stripping.
|
|
9
|
+
*
|
|
10
|
+
* Yarn 2+ has no --ignore-scripts option and honours the enableScripts
|
|
11
|
+
* setting instead, overridable per invocation by YARN_ENABLE_SCRIPTS. So the
|
|
12
|
+
* no-scripts mechanism is a flag for some managers and an environment
|
|
13
|
+
* variable for others, and both must be nax-supplied.
|
|
14
|
+
*/
|
|
15
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
16
|
+
/**
|
|
17
|
+
* When aborted, the process group is SIGKILLed, both readers are cancelled
|
|
18
|
+
* and the result resolves with `aborted: true`. US-001.
|
|
19
|
+
*/
|
|
20
|
+
readonly signal?: AbortSignal;
|
|
21
|
+
}
|
|
22
|
+
export interface ArgvExecResult {
|
|
23
|
+
readonly exitCode: number;
|
|
24
|
+
readonly stdout: string;
|
|
25
|
+
readonly stderr: string;
|
|
26
|
+
readonly timedOut: boolean;
|
|
27
|
+
/** True when an abort killed the process group. Always set by runArgv. */
|
|
28
|
+
readonly aborted?: boolean;
|
|
29
|
+
/** True when background processes holding a stream were SIGKILLed on drain. Always set by runArgv. */
|
|
30
|
+
readonly orphansKilled?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Time, in milliseconds, the readers are allowed to drain after `exited`
|
|
34
|
+
* before runArgv SIGKILLs the whole group and reports `orphansKilled`. US-001.
|
|
35
|
+
* Injectable for tests; not exposed as user configuration.
|
|
36
|
+
*/
|
|
37
|
+
export declare const DRAIN_GRACE_MS = 500;
|
|
38
|
+
/** Injectable seam, mirroring _worktreeDependencyDeps. */
|
|
39
|
+
export declare const _argvExecDeps: {
|
|
40
|
+
spawn: (cmd: readonly string[], opts: import("#src/runtime/index").AgentSpawnOptions) => import("#src/runtime/index").AgentSpawnResult;
|
|
41
|
+
killProcessGroup: typeof killProcessGroup;
|
|
42
|
+
/**
|
|
43
|
+
* The post-exit drain grace (US-001). A real test can shrink it to keep the
|
|
44
|
+
* "background process holding the pipe" case under a second; production
|
|
45
|
+
* stays at the DRAIN_GRACE_MS constant.
|
|
46
|
+
*/
|
|
47
|
+
drainGraceMs: number;
|
|
48
|
+
/**
|
|
49
|
+
* Injectable timer pair — lets the BUG-13 "hung install" test drive the
|
|
50
|
+
* timeout off a virtual clock instead of waiting the full 1s schema minimum.
|
|
51
|
+
* Mirrors `_heartbeatDeps` / `_idleWatchdogDeps` / `_authDeps`.
|
|
52
|
+
*
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
setTimeout: (fn: () => void, ms: number) => unknown;
|
|
56
|
+
clearTimeout: (id: unknown) => void;
|
|
57
|
+
};
|
|
58
|
+
export declare function runArgv(options: RunArgvOptions): Promise<ArgvExecResult>;
|