@selesai/code 0.5.19 → 0.5.21
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/dist/extensions/handoff-new.test.ts +8 -9
- package/dist/extensions/handoff-new.ts +6 -8
- package/dist/extensions/pi-intercom/broker/spawn.test.ts +31 -0
- package/dist/extensions/pi-intercom/broker/spawn.ts +5 -3
- package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
- package/dist/extensions/pi-subagents/README.md +602 -220
- package/dist/extensions/pi-subagents/banner.png +0 -0
- package/dist/extensions/pi-subagents/index.ts +1 -0
- package/dist/extensions/pi-subagents/install.mjs +3 -3
- package/dist/extensions/pi-subagents/package-lock.json +3405 -0
- package/dist/extensions/pi-subagents/package.json +26 -14
- package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
- package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
- package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
- package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
- package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
- package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
- package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
- package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
- package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
- package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
- package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
- package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
- package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
- package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
- package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
- package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
- package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
- package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
- package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
- package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
- package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
- package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
- package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
- package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
- package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
- package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
- package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
- package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
- package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
- package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
- package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
- package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
- package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
- package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
- package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
- package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
- package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
- package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
- package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
- package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
- package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
- package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
- package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
- package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
- package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
- package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
- package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
- package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
- package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
- package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
- package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
- package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
- package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
- package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
- package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
- package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
- package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
- package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
- package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
- package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
- package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
- package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
- package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
- package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
- package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
- package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
- package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
- package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
- package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
- package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
- package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
- package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
- package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
- package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
- package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
- package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
- package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
- package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
- package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
- package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
- package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
- package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
- package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
- package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
- package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
- package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
- package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
- package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
- package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
- package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
- package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
- package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
- package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
- package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
- package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
- package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
- package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
- package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
- package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
- package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
- package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
- package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
- package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
- package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
- package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
- package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
- package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
- package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
- package/dist/extensions/question/batch.ts +39 -0
- package/dist/extensions/question/dialog-adapter.ts +23 -1
- package/dist/extensions/question/index.ts +237 -7
- package/dist/extensions/question/schemas.ts +28 -15
- package/dist/extensions/question/tests/batch.test.ts +26 -0
- package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
- package/dist/extensions/question/tui-adapter.ts +2 -3
- package/dist/extensions/question/types.ts +37 -0
- package/dist/extensions/question/ui-protocol.ts +3 -3
- package/dist/skills/batch-grill-me/SKILL.md +3 -1
- package/package.json +2 -1
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
6
7
|
import {
|
|
7
8
|
acceptanceFailureMessage,
|
|
8
9
|
aggregateAcceptanceReport,
|
|
@@ -12,7 +13,9 @@ import {
|
|
|
12
13
|
resolveEffectiveAcceptance,
|
|
13
14
|
stripAcceptanceReport,
|
|
14
15
|
validateAcceptanceInput,
|
|
16
|
+
validateExecutionAcceptance,
|
|
15
17
|
} from "../../src/runs/shared/acceptance.ts";
|
|
18
|
+
import { extractChildWrittenOutput } from "../../src/runs/shared/single-output.ts";
|
|
16
19
|
|
|
17
20
|
function reportData(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
18
21
|
return {
|
|
@@ -44,16 +47,125 @@ function tempRepo(): string {
|
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
describe("acceptance gates", () => {
|
|
47
|
-
it("infers
|
|
48
|
-
assert.equal(resolveEffectiveAcceptance({ agentName: "
|
|
49
|
-
assert.equal(resolveEffectiveAcceptance({ agentName: "
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
it("infers evidence levels and review requirements independently", () => {
|
|
51
|
+
assert.equal(resolveEffectiveAcceptance({ agentName: "commentator", task: "Review-only. Do not edit.", mode: "single" }).level, "attested");
|
|
52
|
+
assert.equal(resolveEffectiveAcceptance({ agentName: "builder", task: "Implement the fix", mode: "single" }).level, "checked");
|
|
53
|
+
for (const resolved of [
|
|
54
|
+
resolveEffectiveAcceptance({ agentName: "builder", task: "Implement the fix", mode: "single", async: true }),
|
|
55
|
+
resolveEffectiveAcceptance({ agentName: "builder", task: "Fix each item", mode: "chain", dynamic: true }),
|
|
56
|
+
]) {
|
|
57
|
+
assert.equal(resolved.level, "checked");
|
|
58
|
+
assert.equal(resolved.review && resolved.review !== false ? resolved.review.required : undefined, true);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("keeps async commentator review tasks on read-only acceptance despite implementation vocabulary", () => {
|
|
63
|
+
const resolved = resolveEffectiveAcceptance({
|
|
64
|
+
agentName: "commentator",
|
|
65
|
+
task: "Review prep findings and determine what to implement with playbooks instead of before.",
|
|
66
|
+
mode: "single",
|
|
67
|
+
async: true,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
assert.equal(resolved.level, "attested");
|
|
71
|
+
assert.deepEqual(resolved.inferredReason, ["read-only/reviewer-style agent"]);
|
|
72
|
+
assert.deepEqual(resolved.criteria.map((criterion) => criterion.must), ["Return concrete findings with file paths and severity when applicable"]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("uses explicit agent roles for ambiguous tasks while preserving task-intent precedence", () => {
|
|
76
|
+
assert.equal(resolveEffectiveAcceptance({
|
|
77
|
+
agentName: "explorer",
|
|
78
|
+
acceptanceRole: "read-only",
|
|
79
|
+
task: "Explore the authentication flow",
|
|
80
|
+
mode: "single",
|
|
81
|
+
}).level, "attested");
|
|
82
|
+
assert.equal(resolveEffectiveAcceptance({
|
|
83
|
+
agentName: "commentator",
|
|
84
|
+
acceptanceRole: "writer",
|
|
85
|
+
task: "Handle the authentication flow",
|
|
86
|
+
mode: "single",
|
|
87
|
+
}).level, "checked");
|
|
88
|
+
for (const task of ["Implement the authentication fix", "Create a fixture", "Add coverage", "Replace the dependency", "Patch src/auth.ts"]) {
|
|
89
|
+
assert.equal(resolveEffectiveAcceptance({
|
|
90
|
+
agentName: "builder",
|
|
91
|
+
acceptanceRole: "read-only",
|
|
92
|
+
task,
|
|
93
|
+
mode: "single",
|
|
94
|
+
}).level, "checked", task);
|
|
95
|
+
}
|
|
96
|
+
assert.equal(resolveEffectiveAcceptance({
|
|
97
|
+
agentName: "builder",
|
|
98
|
+
acceptanceRole: "read-only",
|
|
99
|
+
task: "Patch src/auth.ts",
|
|
100
|
+
mode: "single",
|
|
101
|
+
async: true,
|
|
102
|
+
}).level, "checked");
|
|
103
|
+
assert.equal(resolveEffectiveAcceptance({
|
|
104
|
+
agentName: "builder",
|
|
105
|
+
acceptanceRole: "read-only",
|
|
106
|
+
task: "Create a report",
|
|
107
|
+
mode: "single",
|
|
108
|
+
}).level, "attested");
|
|
109
|
+
assert.equal(resolveEffectiveAcceptance({
|
|
110
|
+
agentName: "builder",
|
|
111
|
+
acceptanceRole: "writer",
|
|
112
|
+
task: "Review only; do not edit files",
|
|
113
|
+
mode: "single",
|
|
114
|
+
}).level, "attested");
|
|
115
|
+
assert.equal(resolveEffectiveAcceptance({
|
|
116
|
+
agentName: "commentator",
|
|
117
|
+
acceptanceRole: "writer",
|
|
118
|
+
task: "Handle the authentication flow",
|
|
119
|
+
mode: "single",
|
|
120
|
+
async: true,
|
|
121
|
+
}).level, "checked");
|
|
122
|
+
assert.equal(resolveEffectiveAcceptance({
|
|
123
|
+
agentName: "builder",
|
|
124
|
+
acceptanceRole: "read-only",
|
|
125
|
+
task: "Explore the authentication flow",
|
|
126
|
+
mode: "single",
|
|
127
|
+
}).level, "attested");
|
|
128
|
+
assert.equal(resolveEffectiveAcceptance({
|
|
129
|
+
agentName: "explorer",
|
|
130
|
+
acceptanceRole: "read-only",
|
|
131
|
+
task: "Audit the security posture",
|
|
132
|
+
mode: "single",
|
|
133
|
+
}).level, "attested");
|
|
134
|
+
assert.equal(resolveEffectiveAcceptance({
|
|
135
|
+
agentName: "explorer",
|
|
136
|
+
acceptanceRole: "read-only",
|
|
137
|
+
task: "Explore each target",
|
|
138
|
+
mode: "chain",
|
|
139
|
+
dynamic: true,
|
|
140
|
+
}).level, "attested");
|
|
141
|
+
assert.equal(resolveEffectiveAcceptance({
|
|
142
|
+
agentName: "builder",
|
|
143
|
+
acceptanceRole: "writer",
|
|
144
|
+
task: "Review only; do not edit files",
|
|
145
|
+
mode: "chain",
|
|
146
|
+
dynamicGroup: true,
|
|
147
|
+
}).level, "attested");
|
|
148
|
+
const dynamicReviewer = resolveEffectiveAcceptance({
|
|
149
|
+
agentName: "commentator",
|
|
150
|
+
task: "Review each target",
|
|
151
|
+
mode: "chain",
|
|
152
|
+
dynamic: true,
|
|
153
|
+
});
|
|
154
|
+
assert.equal(dynamicReviewer.level, "checked");
|
|
155
|
+
assert.equal(dynamicReviewer.review && dynamicReviewer.review !== false ? dynamicReviewer.review.required : undefined, true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("preserves risky keyword review inference when acceptance role metadata is omitted", () => {
|
|
159
|
+
for (const task of ["Inspect the security posture", "Read-only security audit"]) {
|
|
160
|
+
const resolved = resolveEffectiveAcceptance({ agentName: "builder", task });
|
|
161
|
+
assert.equal(resolved.level, "checked", task);
|
|
162
|
+
assert.equal(resolved.review && resolved.review !== false ? resolved.review.required : undefined, true, task);
|
|
163
|
+
}
|
|
52
164
|
});
|
|
53
165
|
|
|
54
166
|
it("explicit acceptance can strengthen inferred policy", () => {
|
|
55
167
|
const resolved = resolveEffectiveAcceptance({
|
|
56
|
-
agentName: "
|
|
168
|
+
agentName: "commentator",
|
|
57
169
|
task: "Review-only.",
|
|
58
170
|
explicit: { level: "verified", verify: [{ id: "ok", command: "node --version" }] },
|
|
59
171
|
});
|
|
@@ -62,9 +174,55 @@ describe("acceptance gates", () => {
|
|
|
62
174
|
assert.equal(resolved.verify[0]?.id, "ok");
|
|
63
175
|
});
|
|
64
176
|
|
|
177
|
+
it("agent contract v1 disables inferred acceptance without changing current defaults", () => {
|
|
178
|
+
const current = resolveEffectiveAcceptance({ agentName: "builder", acceptanceRole: "writer", task: "Implement the fix", mode: "single", async: true });
|
|
179
|
+
assert.equal(current.level, "checked");
|
|
180
|
+
assert.equal(current.review && current.review !== false ? current.review.required : undefined, true);
|
|
181
|
+
assert.deepEqual(current.inferredReason, ["async write-capable or risky run"]);
|
|
182
|
+
|
|
183
|
+
for (const explicit of [undefined, "auto" as const, false] as const) {
|
|
184
|
+
const resolved = resolveEffectiveAcceptance({ agentName: "builder", acceptanceRole: "writer", task: "Implement the fix", mode: "single", async: true, explicit, agentContract: { version: 1 } });
|
|
185
|
+
assert.equal(resolved.level, "none");
|
|
186
|
+
assert.deepEqual(resolved.inferredReason, []);
|
|
187
|
+
assert.equal(resolved.explicit, explicit !== undefined);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("agent contract v1 keeps explicit acceptance report-optional and verify-only", async () => {
|
|
192
|
+
const checked = resolveEffectiveAcceptance({
|
|
193
|
+
agentName: "builder",
|
|
194
|
+
task: "Implement the fix",
|
|
195
|
+
explicit: "checked",
|
|
196
|
+
agentContract: { version: 1 },
|
|
197
|
+
});
|
|
198
|
+
assert.equal(formatAcceptancePrompt(checked, { reportOptional: true }), "");
|
|
199
|
+
const checkedLedger = await evaluateAcceptance({ acceptance: checked, output: "done", cwd: process.cwd(), reportOptional: true });
|
|
200
|
+
assert.equal(checkedLedger.status, "checked");
|
|
201
|
+
assert.equal(acceptanceFailureMessage(checkedLedger), undefined);
|
|
202
|
+
|
|
203
|
+
const verified = resolveEffectiveAcceptance({
|
|
204
|
+
agentName: "builder",
|
|
205
|
+
task: "Implement the fix",
|
|
206
|
+
explicit: { level: "verified", verify: [{ id: "ok", command: `${process.execPath} -e "process.exit(0)"` }] },
|
|
207
|
+
agentContract: { version: 1 },
|
|
208
|
+
});
|
|
209
|
+
assert.equal(formatAcceptancePrompt(verified, { reportOptional: true }), "");
|
|
210
|
+
const verifiedLedger = await evaluateAcceptance({ acceptance: verified, output: "done", cwd: process.cwd(), reportOptional: true });
|
|
211
|
+
assert.equal(verifiedLedger.status, "verified");
|
|
212
|
+
assert.equal(verifiedLedger.verifyRuns[0]?.status, "passed");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("current/default acceptance still rejects missing required reports", async () => {
|
|
216
|
+
const current = resolveEffectiveAcceptance({ agentName: "builder", task: "Implement the fix", explicit: "checked" });
|
|
217
|
+
const ledger = await evaluateAcceptance({ acceptance: current, output: "done", cwd: process.cwd() });
|
|
218
|
+
|
|
219
|
+
assert.equal(ledger.status, "rejected");
|
|
220
|
+
assert.match(acceptanceFailureMessage(ledger) ?? "", /Structured acceptance report not found/);
|
|
221
|
+
});
|
|
222
|
+
|
|
65
223
|
it("formats a standardized child prompt section", () => {
|
|
66
224
|
const resolved = resolveEffectiveAcceptance({
|
|
67
|
-
agentName: "
|
|
225
|
+
agentName: "builder",
|
|
68
226
|
task: "Implement a fix",
|
|
69
227
|
explicit: { level: "checked", criteria: ["Patch the bug"], stopRules: ["Do not stop after analysis"] },
|
|
70
228
|
});
|
|
@@ -75,9 +233,37 @@ describe("acceptance gates", () => {
|
|
|
75
233
|
assert.match(prompt, /Patch the bug/);
|
|
76
234
|
assert.match(prompt, /```acceptance-report/);
|
|
77
235
|
assert.match(prompt, /array fields contain strings/);
|
|
236
|
+
assert.match(prompt, /criteriaSatisfied\[\]\.status.*satisfied, not-satisfied, not-applicable/);
|
|
237
|
+
assert.match(prompt, /commandsRun\[\]\.result.*passed, failed, not-run/);
|
|
238
|
+
assert.match(prompt, /manualNotes.*optional strings.*empty string.*does not satisfy.*manual-notes/);
|
|
78
239
|
assert.match(prompt, /"reviewFindings": \[\n "blocker:/);
|
|
79
240
|
});
|
|
80
241
|
|
|
242
|
+
it("includes every required resolved criterion in report examples", () => {
|
|
243
|
+
const inferred = resolveEffectiveAcceptance({ agentName: "builder", task: "Implement the fix", mode: "single", async: true });
|
|
244
|
+
const inferredExample = formatAcceptancePrompt(inferred).match(/```acceptance-report\n([\s\S]*?)\n```/);
|
|
245
|
+
assert.ok(inferredExample?.[1]);
|
|
246
|
+
assert.deepEqual(
|
|
247
|
+
(JSON.parse(inferredExample[1]!) as { criteriaSatisfied: Array<{ id: string }> }).criteriaSatisfied.map((criterion) => criterion.id),
|
|
248
|
+
["criterion-1", "criterion-2"],
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const custom = resolveEffectiveAcceptance({
|
|
252
|
+
agentName: "builder",
|
|
253
|
+
task: "Implement the fix",
|
|
254
|
+
explicit: { level: "checked", criteria: [
|
|
255
|
+
{ id: "required-check", must: "Required", severity: "required" },
|
|
256
|
+
{ id: "recommended-check", must: "Recommended", severity: "recommended" },
|
|
257
|
+
] },
|
|
258
|
+
});
|
|
259
|
+
const customExample = formatAcceptancePrompt(custom).match(/```acceptance-report\n([\s\S]*?)\n```/);
|
|
260
|
+
assert.ok(customExample?.[1]);
|
|
261
|
+
assert.deepEqual(
|
|
262
|
+
(JSON.parse(customExample[1]!) as { criteriaSatisfied: Array<{ id: string }> }).criteriaSatisfied.map((criterion) => criterion.id),
|
|
263
|
+
["required-check"],
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
81
267
|
it("parses acceptance-report fences and ignores unrelated json fences", () => {
|
|
82
268
|
const parsed = parseAcceptanceReport(report());
|
|
83
269
|
|
|
@@ -97,12 +283,18 @@ describe("acceptance gates", () => {
|
|
|
97
283
|
assert.equal(criteriaOnlyJson.report, undefined);
|
|
98
284
|
assert.match(criteriaOnlyJson.error ?? "", /Structured acceptance report not found/);
|
|
99
285
|
|
|
286
|
+
const criteriaWithUnknownJson = parseAcceptanceReport(`done\n\
|
|
287
|
+
\
|
|
288
|
+
\`\`\`json\n{\"criteriaSatisfied\":[],\"unexpected\":true}\n\`\`\``);
|
|
289
|
+
assert.equal(criteriaWithUnknownJson.report, undefined);
|
|
290
|
+
assert.match(criteriaWithUnknownJson.error ?? "", /unexpected: unsupported acceptance report field/);
|
|
291
|
+
|
|
100
292
|
const invalidSignalJson = `done\n\
|
|
101
293
|
\
|
|
102
294
|
\`\`\`json\n{\"criteriaSatisfied\":[{\"id\":\"criterion-1\",\"status\":\"satisfied\",\"evidence\":\"example\"}],\"changedFiles\":false}\n\`\`\``;
|
|
103
295
|
const genericJsonWithInvalidSignal = parseAcceptanceReport(invalidSignalJson);
|
|
104
296
|
assert.equal(genericJsonWithInvalidSignal.report, undefined);
|
|
105
|
-
assert.match(genericJsonWithInvalidSignal.error ?? "", /
|
|
297
|
+
assert.match(genericJsonWithInvalidSignal.error ?? "", /changedFiles: expected string\[\]; got boolean false/);
|
|
106
298
|
assert.equal(stripAcceptanceReport(invalidSignalJson), invalidSignalJson);
|
|
107
299
|
|
|
108
300
|
const partialWrapperJson = `done\n\
|
|
@@ -121,9 +313,17 @@ describe("acceptance gates", () => {
|
|
|
121
313
|
assert.match(genericReportShapedJson.error ?? "", /Structured acceptance report not found/);
|
|
122
314
|
assert.equal(stripAcceptanceReport(reportShapedJson), reportShapedJson);
|
|
123
315
|
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
316
|
+
for (const malformedOutput of [
|
|
317
|
+
"```acceptance-report\n{bad-json\n```",
|
|
318
|
+
"```acceptance-report\n```",
|
|
319
|
+
"```acceptance-report\n{\"criteriaSatisfied\": []}",
|
|
320
|
+
"ACCEPTANCE_REPORT: { not json",
|
|
321
|
+
"ACCEPTANCE_REPORT: no object",
|
|
322
|
+
]) {
|
|
323
|
+
const malformed = parseAcceptanceReport(malformedOutput);
|
|
324
|
+
assert.equal(malformed.report, undefined);
|
|
325
|
+
assert.match(malformed.error ?? "", /Failed to parse acceptance-report/, malformedOutput);
|
|
326
|
+
}
|
|
127
327
|
});
|
|
128
328
|
|
|
129
329
|
it("parses acceptance reports from json-family fences", () => {
|
|
@@ -161,46 +361,162 @@ describe("acceptance gates", () => {
|
|
|
161
361
|
].join("\n"));
|
|
162
362
|
});
|
|
163
363
|
|
|
164
|
-
it("unwraps
|
|
364
|
+
it("unwraps every accepted report wrapper", () => {
|
|
365
|
+
for (const wrapper of ["acceptance", "acceptance-report", "acceptance_report", "acceptanceReport"]) {
|
|
366
|
+
const output = [
|
|
367
|
+
"done",
|
|
368
|
+
"```json",
|
|
369
|
+
JSON.stringify({ [wrapper]: reportData() }),
|
|
370
|
+
"```",
|
|
371
|
+
].join("\n");
|
|
372
|
+
const parsed = parseAcceptanceReport(output);
|
|
373
|
+
|
|
374
|
+
assert.ok(parsed.report, wrapper);
|
|
375
|
+
assert.deepEqual(parsed.report.testsAddedOrUpdated, ["test/file.test.ts"]);
|
|
376
|
+
assert.equal(stripAcceptanceReport(output), "done");
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("normalizes known report wire variants to the canonical shape", () => {
|
|
165
381
|
const output = [
|
|
166
382
|
"done",
|
|
167
|
-
"```
|
|
168
|
-
JSON.stringify({
|
|
383
|
+
"```acceptance_report",
|
|
384
|
+
JSON.stringify({ acceptance_report: {
|
|
385
|
+
criteria_satisfied: { id: "Criterion_1", status: "DONE", evidence: "verified" },
|
|
386
|
+
changed_files: "src/file.ts",
|
|
387
|
+
tests_added_or_updated: "test/file.test.ts",
|
|
388
|
+
commands_run: { command: "npm test", result: "success", summary: "passed" },
|
|
389
|
+
validation_output: "tests passed",
|
|
390
|
+
residual_risks: "none",
|
|
391
|
+
no_staged_files: " TRUE ",
|
|
392
|
+
diff_summary: "patched",
|
|
393
|
+
review_findings: "no blockers",
|
|
394
|
+
manual_notes: "complete",
|
|
395
|
+
} }),
|
|
169
396
|
"```",
|
|
170
397
|
].join("\n");
|
|
171
398
|
const parsed = parseAcceptanceReport(output);
|
|
172
399
|
|
|
173
|
-
assert.
|
|
174
|
-
assert.deepEqual(parsed.report
|
|
400
|
+
assert.equal(parsed.error, undefined);
|
|
401
|
+
assert.deepEqual(parsed.report, {
|
|
402
|
+
criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "verified" }],
|
|
403
|
+
changedFiles: ["src/file.ts"],
|
|
404
|
+
testsAddedOrUpdated: ["test/file.test.ts"],
|
|
405
|
+
commandsRun: [{ command: "npm test", result: "passed", summary: "passed" }],
|
|
406
|
+
validationOutput: ["tests passed"],
|
|
407
|
+
residualRisks: ["none"],
|
|
408
|
+
noStagedFiles: true,
|
|
409
|
+
diffSummary: "patched",
|
|
410
|
+
reviewFindings: ["no blockers"],
|
|
411
|
+
manualNotes: "complete",
|
|
412
|
+
});
|
|
175
413
|
assert.equal(stripAcceptanceReport(output), "done");
|
|
176
414
|
});
|
|
177
415
|
|
|
416
|
+
it("normalizes only known enum synonyms and criterion id separators", () => {
|
|
417
|
+
for (const [value, expected] of [
|
|
418
|
+
["met", "satisfied"],
|
|
419
|
+
["not met", "not-satisfied"],
|
|
420
|
+
["not_applicable", "not-applicable"],
|
|
421
|
+
] as const) {
|
|
422
|
+
const parsed = parseAcceptanceReport(report({ criteriaSatisfied: [{ id: "Criterion 1", status: value, evidence: "proof" }] }));
|
|
423
|
+
assert.equal(parsed.report?.criteriaSatisfied?.[0]?.id, "criterion-1");
|
|
424
|
+
assert.equal(parsed.report?.criteriaSatisfied?.[0]?.status, expected);
|
|
425
|
+
}
|
|
426
|
+
for (const [value, expected] of [
|
|
427
|
+
["ok", "passed"],
|
|
428
|
+
["failure", "failed"],
|
|
429
|
+
["not run", "not-run"],
|
|
430
|
+
] as const) {
|
|
431
|
+
const parsed = parseAcceptanceReport(report({ commandsRun: [{ command: "check", result: value, summary: "complete" }] }));
|
|
432
|
+
assert.equal(parsed.report?.commandsRun?.[0]?.result, expected);
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("rejects unknown and ambiguous report fields with exact diagnostics", () => {
|
|
437
|
+
for (const [value, expected] of [
|
|
438
|
+
[{ ...reportData(), unexpected: true }, /unexpected: unsupported acceptance report field/],
|
|
439
|
+
[{ ...reportData(), changed_files: ["src/other.ts"] }, /changed_files: duplicates normalized field 'changedFiles'/],
|
|
440
|
+
[{ acceptance: reportData(), changedFiles: ["src/file.ts"] }, /changedFiles: unsupported alongside acceptance report wrapper 'acceptance'/],
|
|
441
|
+
[{ acceptance: reportData(), acceptance_report: reportData() }, /multiple acceptance report wrappers are ambiguous/],
|
|
442
|
+
] as const) {
|
|
443
|
+
const parsed = parseAcceptanceReport(`\`\`\`acceptance-report\n${JSON.stringify(value)}\n\`\`\``);
|
|
444
|
+
assert.equal(parsed.report, undefined);
|
|
445
|
+
assert.match(parsed.error ?? "", expected);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("rejects unknown enums, duplicate criterion ids, and blank evidence", () => {
|
|
450
|
+
for (const [overrides, expected] of [
|
|
451
|
+
[{ commandsRun: [{ command: "npm test", result: "maybe", summary: "passed" }] }, /commandsRun\[0\]\.result.*got "maybe"/],
|
|
452
|
+
[{ commandsRun: [{ command: "npm test", result: "passed", summary: "", exitCode: 0 }] }, /commandsRun\[0\]\.exitCode: unsupported acceptance command field/],
|
|
453
|
+
[{ commandsRun: [{ command: "npm test", result: "passed", summary: "" }] }, /commandsRun\[0\]\.summary: expected non-empty string; got ""/],
|
|
454
|
+
[{ criteriaSatisfied: [{ id: "criterion-1", status: "maybe", evidence: "proof" }] }, /criteriaSatisfied\[0\]\.status.*got "maybe"/],
|
|
455
|
+
[{ criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "proof", confidence: 1 }] }, /criteriaSatisfied\[0\]\.confidence: unsupported acceptance criterion field/],
|
|
456
|
+
[{ criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "proof" }, { id: "Criterion_1", status: "satisfied", evidence: "proof" }] }, /duplicate normalized criterion id 'criterion-1'/],
|
|
457
|
+
[{ reviewFindings: [""] }, /reviewFindings\[0\]: expected non-empty string; got ""/],
|
|
458
|
+
[{ noStagedFiles: "yes" }, /noStagedFiles: expected boolean; got "yes"/],
|
|
459
|
+
] as const) {
|
|
460
|
+
const parsed = parseAcceptanceReport(report(overrides));
|
|
461
|
+
assert.equal(parsed.report, undefined);
|
|
462
|
+
assert.match(parsed.error ?? "", expected);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("accepts empty optional note strings without treating them as evidence", async () => {
|
|
467
|
+
for (const field of ["manualNotes", "notes"] as const) {
|
|
468
|
+
const parsed = parseAcceptanceReport(report({ notes: undefined, [field]: "" }));
|
|
469
|
+
assert.equal(parsed.error, undefined);
|
|
470
|
+
assert.equal(parsed.report?.[field], "");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const cwd = tempRepo();
|
|
474
|
+
try {
|
|
475
|
+
const acceptance = resolveEffectiveAcceptance({
|
|
476
|
+
agentName: "builder",
|
|
477
|
+
task: "Return a concise result",
|
|
478
|
+
explicit: { level: "attested", evidence: ["manual-notes"] },
|
|
479
|
+
});
|
|
480
|
+
const ledger = await evaluateAcceptance({
|
|
481
|
+
acceptance,
|
|
482
|
+
output: report({ notes: undefined, manualNotes: "" }),
|
|
483
|
+
cwd,
|
|
484
|
+
});
|
|
485
|
+
assert.equal(ledger.childReportParseError, undefined);
|
|
486
|
+
assert.equal(ledger.childReport?.manualNotes, "");
|
|
487
|
+
assert.equal(ledger.status, "rejected");
|
|
488
|
+
assert.equal(ledger.runtimeChecks.find((check) => check.id === "evidence:manual-notes")?.status, "failed");
|
|
489
|
+
} finally {
|
|
490
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
178
494
|
it("reports field-level validation errors for malformed acceptance-report fields", () => {
|
|
179
495
|
const invalidReviewerReport = parseAcceptanceReport(report({
|
|
180
496
|
reviewFindings: [{ id: "B-1", severity: "blocker", finding: "Missing evidence" }],
|
|
181
497
|
}));
|
|
182
498
|
assert.equal(invalidReviewerReport.report, undefined);
|
|
183
|
-
assert.match(invalidReviewerReport.error ?? "", /reviewFindings\[0\]: expected string; got object/);
|
|
499
|
+
assert.match(invalidReviewerReport.error ?? "", /reviewFindings\[0\]: expected non-empty string; got object/);
|
|
184
500
|
|
|
185
501
|
const invalidCommandReport = parseAcceptanceReport(report({
|
|
186
502
|
commandsRun: [{ command: "npm test", exitCode: 0 }],
|
|
187
503
|
}));
|
|
188
504
|
assert.equal(invalidCommandReport.report, undefined);
|
|
189
505
|
assert.match(invalidCommandReport.error ?? "", /commandsRun\[0\]\.result: expected one of "passed", "failed", "not-run"; got missing/);
|
|
190
|
-
assert.match(invalidCommandReport.error ?? "", /commandsRun\[0\]\.summary: expected string; got missing/);
|
|
506
|
+
assert.match(invalidCommandReport.error ?? "", /commandsRun\[0\]\.summary: expected non-empty string; got missing/);
|
|
191
507
|
|
|
192
508
|
const invalidCriteriaReport = parseAcceptanceReport(report({
|
|
193
|
-
criteriaSatisfied: [{ id: 7, status: "
|
|
509
|
+
criteriaSatisfied: [{ id: 7, status: "maybe", evidence: "" }],
|
|
194
510
|
}));
|
|
195
511
|
assert.equal(invalidCriteriaReport.report, undefined);
|
|
196
512
|
assert.match(invalidCriteriaReport.error ?? "", /criteriaSatisfied\[0\]\.id: expected string; got number 7/);
|
|
197
|
-
assert.match(invalidCriteriaReport.error ?? "", /criteriaSatisfied\[0\]\.status: expected one of "satisfied", "not-satisfied", "not-applicable"; got "
|
|
513
|
+
assert.match(invalidCriteriaReport.error ?? "", /criteriaSatisfied\[0\]\.status: expected one of "satisfied", "not-satisfied", "not-applicable"; got "maybe"/);
|
|
198
514
|
assert.match(invalidCriteriaReport.error ?? "", /criteriaSatisfied\[0\]\.evidence: expected non-empty string; got ""/);
|
|
199
515
|
});
|
|
200
516
|
|
|
201
517
|
it("explicit none disables inferred gates when a reason is present", () => {
|
|
202
518
|
const acceptance = resolveEffectiveAcceptance({
|
|
203
|
-
agentName: "
|
|
519
|
+
agentName: "builder",
|
|
204
520
|
task: "Implement a fix",
|
|
205
521
|
explicit: { level: "none", reason: "parent is doing manual acceptance" },
|
|
206
522
|
});
|
|
@@ -209,22 +525,53 @@ describe("acceptance gates", () => {
|
|
|
209
525
|
assert.deepEqual(acceptance.evidence, []);
|
|
210
526
|
});
|
|
211
527
|
|
|
212
|
-
it("checked mode
|
|
528
|
+
it("checked mode accepts explicit empty changed and test arrays as not applicable", async () => {
|
|
213
529
|
const cwd = tempRepo();
|
|
214
530
|
try {
|
|
215
531
|
const acceptance = resolveEffectiveAcceptance({
|
|
216
|
-
agentName: "
|
|
532
|
+
agentName: "builder",
|
|
217
533
|
task: "Implement a fix",
|
|
218
534
|
explicit: { level: "checked" },
|
|
219
535
|
});
|
|
220
536
|
const ledger = await evaluateAcceptance({
|
|
221
537
|
acceptance,
|
|
222
|
-
output: report({ testsAddedOrUpdated: [] }),
|
|
538
|
+
output: report({ changedFiles: [], testsAddedOrUpdated: [] }),
|
|
223
539
|
cwd,
|
|
224
540
|
});
|
|
225
541
|
|
|
226
|
-
assert.equal(ledger.status, "
|
|
227
|
-
assert.
|
|
542
|
+
assert.equal(ledger.status, "checked");
|
|
543
|
+
assert.equal(ledger.runtimeChecks.find((check) => check.id === "evidence:changed-files")?.status, "not-applicable");
|
|
544
|
+
assert.equal(ledger.runtimeChecks.find((check) => check.id === "evidence:tests-added")?.status, "not-applicable");
|
|
545
|
+
} finally {
|
|
546
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it("still rejects missing changed and test evidence and empty required commands", async () => {
|
|
551
|
+
const cwd = tempRepo();
|
|
552
|
+
try {
|
|
553
|
+
const acceptance = resolveEffectiveAcceptance({ agentName: "builder", task: "Implement a fix", explicit: { level: "checked" } });
|
|
554
|
+
const missing = await evaluateAcceptance({ acceptance, output: report({
|
|
555
|
+
changedFiles: undefined,
|
|
556
|
+
testsAddedOrUpdated: undefined,
|
|
557
|
+
}), cwd });
|
|
558
|
+
assert.equal(missing.status, "rejected");
|
|
559
|
+
assert.match(acceptanceFailureMessage(missing) ?? "", /changed-files evidence missing/);
|
|
560
|
+
|
|
561
|
+
const missingTests = await evaluateAcceptance({ acceptance, output: report({
|
|
562
|
+
changedFiles: [],
|
|
563
|
+
testsAddedOrUpdated: undefined,
|
|
564
|
+
}), cwd });
|
|
565
|
+
assert.equal(missingTests.status, "rejected");
|
|
566
|
+
assert.match(acceptanceFailureMessage(missingTests) ?? "", /tests-added evidence missing/);
|
|
567
|
+
|
|
568
|
+
const emptyCommands = await evaluateAcceptance({
|
|
569
|
+
acceptance,
|
|
570
|
+
output: report({ changedFiles: [], testsAddedOrUpdated: [], commandsRun: [] }),
|
|
571
|
+
cwd,
|
|
572
|
+
});
|
|
573
|
+
assert.equal(emptyCommands.status, "rejected");
|
|
574
|
+
assert.match(acceptanceFailureMessage(emptyCommands) ?? "", /commands-run evidence missing/);
|
|
228
575
|
} finally {
|
|
229
576
|
fs.rmSync(cwd, { recursive: true, force: true });
|
|
230
577
|
}
|
|
@@ -234,7 +581,7 @@ describe("acceptance gates", () => {
|
|
|
234
581
|
const cwd = tempRepo();
|
|
235
582
|
try {
|
|
236
583
|
const acceptance = resolveEffectiveAcceptance({
|
|
237
|
-
agentName: "
|
|
584
|
+
agentName: "commentator",
|
|
238
585
|
task: "Review-only. Do not edit.",
|
|
239
586
|
explicit: { level: "attested", evidence: ["review-findings"] },
|
|
240
587
|
});
|
|
@@ -246,7 +593,40 @@ describe("acceptance gates", () => {
|
|
|
246
593
|
|
|
247
594
|
assert.equal(ledger.status, "rejected");
|
|
248
595
|
assert.match(acceptanceFailureMessage(ledger) ?? "", /Failed to parse acceptance-report/);
|
|
249
|
-
assert.match(acceptanceFailureMessage(ledger) ?? "", /reviewFindings\[0\]: expected string; got object/);
|
|
596
|
+
assert.match(acceptanceFailureMessage(ledger) ?? "", /reviewFindings\[0\]: expected non-empty string; got object/);
|
|
597
|
+
} finally {
|
|
598
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it("normalizes configured criterion ids and validates direct report objects", async () => {
|
|
603
|
+
const cwd = tempRepo();
|
|
604
|
+
try {
|
|
605
|
+
const acceptance = resolveEffectiveAcceptance({
|
|
606
|
+
agentName: "builder",
|
|
607
|
+
task: "Implement a fix",
|
|
608
|
+
explicit: { level: "checked", criteria: [{ id: "Release_Check", must: "Release check passes" }] },
|
|
609
|
+
});
|
|
610
|
+
const normalized = await evaluateAcceptance({
|
|
611
|
+
acceptance,
|
|
612
|
+
output: report({
|
|
613
|
+
criteriaSatisfied: [{ id: "release check", status: "met", evidence: "verified" }],
|
|
614
|
+
changedFiles: [],
|
|
615
|
+
testsAddedOrUpdated: [],
|
|
616
|
+
}),
|
|
617
|
+
cwd,
|
|
618
|
+
});
|
|
619
|
+
assert.equal(normalized.status, "checked");
|
|
620
|
+
assert.equal(normalized.childReport?.criteriaSatisfied?.[0]?.id, "release-check");
|
|
621
|
+
|
|
622
|
+
const malformedDirect = await evaluateAcceptance({
|
|
623
|
+
acceptance,
|
|
624
|
+
output: "",
|
|
625
|
+
report: { ...reportData(), unexpected: true } as never,
|
|
626
|
+
cwd,
|
|
627
|
+
});
|
|
628
|
+
assert.equal(malformedDirect.status, "rejected");
|
|
629
|
+
assert.match(malformedDirect.childReportParseError ?? "", /unexpected: unsupported acceptance report field/);
|
|
250
630
|
} finally {
|
|
251
631
|
fs.rmSync(cwd, { recursive: true, force: true });
|
|
252
632
|
}
|
|
@@ -256,7 +636,7 @@ describe("acceptance gates", () => {
|
|
|
256
636
|
const cwd = tempRepo();
|
|
257
637
|
try {
|
|
258
638
|
const acceptance = resolveEffectiveAcceptance({
|
|
259
|
-
agentName: "
|
|
639
|
+
agentName: "builder",
|
|
260
640
|
task: "Implement a fix",
|
|
261
641
|
explicit: { level: "checked", criteria: [{ id: "regression", must: "Regression is covered" }] },
|
|
262
642
|
});
|
|
@@ -277,7 +657,7 @@ describe("acceptance gates", () => {
|
|
|
277
657
|
const cwd = tempRepo();
|
|
278
658
|
try {
|
|
279
659
|
const passing = resolveEffectiveAcceptance({
|
|
280
|
-
agentName: "
|
|
660
|
+
agentName: "builder",
|
|
281
661
|
task: "Implement a fix",
|
|
282
662
|
explicit: { level: "verified", verify: [{ id: "pass", command: "node -e \"process.exit(0)\"", timeoutMs: 10_000 }] },
|
|
283
663
|
});
|
|
@@ -286,7 +666,7 @@ describe("acceptance gates", () => {
|
|
|
286
666
|
assert.equal(passLedger.verifyRuns[0]?.status, "passed");
|
|
287
667
|
|
|
288
668
|
const failing = resolveEffectiveAcceptance({
|
|
289
|
-
agentName: "
|
|
669
|
+
agentName: "builder",
|
|
290
670
|
task: "Implement a fix",
|
|
291
671
|
explicit: { level: "verified", verify: [{ id: "fail", command: "node -e \"process.exit(7)\"", timeoutMs: 10_000 }] },
|
|
292
672
|
});
|
|
@@ -299,22 +679,23 @@ describe("acceptance gates", () => {
|
|
|
299
679
|
}
|
|
300
680
|
});
|
|
301
681
|
|
|
302
|
-
it("
|
|
682
|
+
it("records achieved, blocked, and pending independent review separately from evidence", async () => {
|
|
303
683
|
const cwd = tempRepo();
|
|
304
684
|
try {
|
|
305
685
|
const acceptance = resolveEffectiveAcceptance({
|
|
306
|
-
agentName: "
|
|
686
|
+
agentName: "builder",
|
|
307
687
|
task: "Implement a risky fix",
|
|
308
|
-
explicit: { level: "
|
|
688
|
+
explicit: { level: "checked", review: { agent: "commentator", required: true } },
|
|
309
689
|
});
|
|
310
|
-
const
|
|
690
|
+
const reviewed = await evaluateAcceptance({
|
|
311
691
|
acceptance,
|
|
312
692
|
output: report(),
|
|
313
693
|
cwd,
|
|
314
|
-
reviewResult: { status: "
|
|
694
|
+
reviewResult: { status: "reviewed", findings: [] },
|
|
315
695
|
});
|
|
316
|
-
assert.equal(
|
|
317
|
-
assert.equal(
|
|
696
|
+
assert.equal(reviewed.status, "reviewed");
|
|
697
|
+
assert.equal(reviewed.evidenceStatus, "checked");
|
|
698
|
+
assert.equal(reviewed.reviewResult?.status, "reviewed");
|
|
318
699
|
|
|
319
700
|
const blockers = await evaluateAcceptance({
|
|
320
701
|
acceptance,
|
|
@@ -326,91 +707,344 @@ describe("acceptance gates", () => {
|
|
|
326
707
|
},
|
|
327
708
|
});
|
|
328
709
|
assert.equal(blockers.status, "rejected");
|
|
710
|
+
assert.equal(blockers.evidenceStatus, "checked");
|
|
329
711
|
assert.equal(blockers.reviewResult?.status, "blockers");
|
|
330
712
|
|
|
331
|
-
const
|
|
332
|
-
assert.equal(
|
|
333
|
-
assert.equal(
|
|
713
|
+
const pending = await evaluateAcceptance({ acceptance, output: report(), cwd });
|
|
714
|
+
assert.equal(pending.status, "review-required");
|
|
715
|
+
assert.equal(pending.evidenceStatus, "checked");
|
|
716
|
+
assert.equal(pending.reviewResult?.status, "review-required");
|
|
717
|
+
} finally {
|
|
718
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it("keeps inferred review required when callers explicitly select checked or auto evidence", async () => {
|
|
723
|
+
const cwd = tempRepo();
|
|
724
|
+
try {
|
|
725
|
+
for (const explicit of [{ level: "checked" }, "auto", { level: "auto" }] as const) {
|
|
726
|
+
const acceptance = resolveEffectiveAcceptance({
|
|
727
|
+
agentName: "builder",
|
|
728
|
+
task: "Implement each dynamic item",
|
|
729
|
+
dynamic: true,
|
|
730
|
+
explicit,
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
assert.equal(acceptance.level, "checked");
|
|
734
|
+
assert.equal(acceptance.review && acceptance.review !== false ? acceptance.review.required : undefined, true);
|
|
735
|
+
const ledger = await evaluateAcceptance({ acceptance, output: report({ criteriaSatisfied: [
|
|
736
|
+
{ id: "criterion-1", status: "satisfied", evidence: "implemented" },
|
|
737
|
+
{ id: "criterion-2", status: "satisfied", evidence: "evidence returned" },
|
|
738
|
+
] }), cwd });
|
|
739
|
+
assert.equal(ledger.status, "review-required");
|
|
740
|
+
assert.equal(ledger.evidenceStatus, "checked");
|
|
741
|
+
assert.equal(ledger.reviewResult?.status, "review-required");
|
|
742
|
+
}
|
|
743
|
+
} finally {
|
|
744
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it("allows an explicit review opt-out without claiming reviewed status", async () => {
|
|
749
|
+
const cwd = tempRepo();
|
|
750
|
+
try {
|
|
751
|
+
for (const review of [false, { agent: "commentator", required: false }] as const) {
|
|
752
|
+
const acceptance = resolveEffectiveAcceptance({
|
|
753
|
+
agentName: "builder",
|
|
754
|
+
task: "Implement an async fix",
|
|
755
|
+
async: true,
|
|
756
|
+
explicit: { level: "checked", review },
|
|
757
|
+
});
|
|
758
|
+
const ledger = await evaluateAcceptance({
|
|
759
|
+
acceptance,
|
|
760
|
+
output: report({ criteriaSatisfied: [
|
|
761
|
+
{ id: "criterion-1", status: "satisfied", evidence: "implemented" },
|
|
762
|
+
{ id: "criterion-2", status: "satisfied", evidence: "evidence returned" },
|
|
763
|
+
] }),
|
|
764
|
+
cwd,
|
|
765
|
+
reviewResult: { status: "review-required", findings: [] },
|
|
766
|
+
});
|
|
767
|
+
assert.equal(ledger.status, "checked");
|
|
768
|
+
assert.equal(ledger.evidenceStatus, "checked");
|
|
769
|
+
assert.equal(ledger.reviewResult, undefined);
|
|
770
|
+
}
|
|
771
|
+
} finally {
|
|
772
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
it("zero-child aggregate reports do not fabricate required evidence", async () => {
|
|
777
|
+
const cwd = tempRepo();
|
|
778
|
+
try {
|
|
779
|
+
const acceptance = resolveEffectiveAcceptance({
|
|
780
|
+
agentName: "builder",
|
|
781
|
+
task: "Implement dynamic fanout fixes",
|
|
782
|
+
explicit: { level: "checked" },
|
|
783
|
+
});
|
|
784
|
+
const ledger = await evaluateAcceptance({
|
|
785
|
+
acceptance,
|
|
786
|
+
output: "",
|
|
787
|
+
report: aggregateAcceptanceReport({ results: [] }),
|
|
788
|
+
cwd,
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
assert.equal(ledger.status, "rejected");
|
|
792
|
+
assert.match(acceptanceFailureMessage(ledger) ?? "", /criterion|changed-files|tests-added|commands-run|validation-output|no-staged-files/);
|
|
334
793
|
} finally {
|
|
335
794
|
fs.rmSync(cwd, { recursive: true, force: true });
|
|
336
795
|
}
|
|
337
796
|
});
|
|
338
797
|
|
|
339
|
-
it("
|
|
798
|
+
it("recovers the acceptance report from child-written configured output", async () => {
|
|
340
799
|
const cwd = tempRepo();
|
|
341
800
|
try {
|
|
342
801
|
const acceptance = resolveEffectiveAcceptance({
|
|
343
|
-
agentName: "
|
|
344
|
-
task: "Implement
|
|
345
|
-
dynamic: true,
|
|
802
|
+
agentName: "builder",
|
|
803
|
+
task: "Implement a fix",
|
|
346
804
|
explicit: { level: "checked" },
|
|
347
805
|
});
|
|
806
|
+
const receipt = "Report written to the configured output file.";
|
|
807
|
+
|
|
808
|
+
const withoutFile = await evaluateAcceptance({ acceptance, output: receipt, cwd });
|
|
809
|
+
assert.equal(withoutFile.status, "rejected");
|
|
348
810
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
{
|
|
353
|
-
|
|
354
|
-
|
|
811
|
+
const ledger = await evaluateAcceptance({
|
|
812
|
+
acceptance,
|
|
813
|
+
output: receipt,
|
|
814
|
+
fileOutput: { content: report(), path: "/tmp/report.md", authoritative: true },
|
|
815
|
+
cwd,
|
|
816
|
+
});
|
|
355
817
|
assert.equal(ledger.status, "checked");
|
|
356
|
-
assert.
|
|
818
|
+
assert.ok(ledger.childReport);
|
|
357
819
|
} finally {
|
|
358
820
|
fs.rmSync(cwd, { recursive: true, force: true });
|
|
359
821
|
}
|
|
360
822
|
});
|
|
361
823
|
|
|
362
|
-
it("
|
|
824
|
+
it("report source order follows output authority", async () => {
|
|
363
825
|
const cwd = tempRepo();
|
|
364
826
|
try {
|
|
365
827
|
const acceptance = resolveEffectiveAcceptance({
|
|
366
|
-
agentName: "
|
|
828
|
+
agentName: "builder",
|
|
367
829
|
task: "Implement a fix",
|
|
368
|
-
explicit: {
|
|
369
|
-
level: "reviewed",
|
|
370
|
-
review: false,
|
|
371
|
-
},
|
|
830
|
+
explicit: { level: "checked" },
|
|
372
831
|
});
|
|
373
|
-
|
|
832
|
+
const inline = await evaluateAcceptance({
|
|
833
|
+
acceptance,
|
|
834
|
+
output: report({ notes: "from text" }),
|
|
835
|
+
fileOutput: { content: report({ notes: "from file" }), path: "/tmp/report.md" },
|
|
836
|
+
cwd,
|
|
837
|
+
});
|
|
838
|
+
assert.equal(inline.childReport?.notes, "from text");
|
|
374
839
|
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
840
|
+
const fileOnly = await evaluateAcceptance({
|
|
841
|
+
acceptance,
|
|
842
|
+
output: report({ notes: "from text" }),
|
|
843
|
+
fileOutput: { content: report({ notes: "from file" }), path: "/tmp/report.md", authoritative: true },
|
|
844
|
+
cwd,
|
|
845
|
+
});
|
|
846
|
+
assert.equal(fileOnly.childReport?.notes, "from file");
|
|
379
847
|
} finally {
|
|
380
848
|
fs.rmSync(cwd, { recursive: true, force: true });
|
|
381
849
|
}
|
|
382
850
|
});
|
|
383
851
|
|
|
384
|
-
it("
|
|
852
|
+
it("sibling overwrites of a shared output path cannot pollute this child's acceptance", async () => {
|
|
385
853
|
const cwd = tempRepo();
|
|
854
|
+
const sharedPath = path.join(cwd, "context.md");
|
|
386
855
|
try {
|
|
856
|
+
// Sibling child B wrote the shared path last (issue #420); the disk
|
|
857
|
+
// content is B's. Child A's acceptance input comes from A's own
|
|
858
|
+
// successful write-tool call, so B's report must not be attributed to A.
|
|
859
|
+
fs.writeFileSync(sharedPath, report({ notes: "sibling B report" }), "utf-8");
|
|
860
|
+
const childAMessages: Message[] = [
|
|
861
|
+
{
|
|
862
|
+
role: "assistant",
|
|
863
|
+
content: [{ type: "toolCall", id: "w1", name: "write", arguments: { path: sharedPath, content: report({ notes: "child A report" }) } }],
|
|
864
|
+
api: "test",
|
|
865
|
+
provider: "test",
|
|
866
|
+
model: "mock/test-model",
|
|
867
|
+
usage: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0, totalTokens: 2, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
|
868
|
+
stopReason: "toolUse",
|
|
869
|
+
timestamp: 0,
|
|
870
|
+
},
|
|
871
|
+
{ role: "toolResult", toolCallId: "w1", toolName: "write", content: [{ type: "text", text: "ok" }], isError: false, timestamp: 0 },
|
|
872
|
+
];
|
|
873
|
+
const childAContent = extractChildWrittenOutput(childAMessages, sharedPath, cwd);
|
|
874
|
+
assert.equal(typeof childAContent, "string");
|
|
875
|
+
|
|
387
876
|
const acceptance = resolveEffectiveAcceptance({
|
|
388
|
-
agentName: "
|
|
389
|
-
task: "Implement
|
|
877
|
+
agentName: "builder",
|
|
878
|
+
task: "Implement a fix",
|
|
390
879
|
explicit: { level: "checked" },
|
|
391
880
|
});
|
|
392
881
|
const ledger = await evaluateAcceptance({
|
|
393
882
|
acceptance,
|
|
394
|
-
output: "",
|
|
395
|
-
|
|
883
|
+
output: "Report written to the configured output file.",
|
|
884
|
+
fileOutput: { content: childAContent!, path: sharedPath, authoritative: true },
|
|
885
|
+
cwd,
|
|
886
|
+
});
|
|
887
|
+
assert.equal(ledger.childReport?.notes, "child A report");
|
|
888
|
+
} finally {
|
|
889
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
it("rejects a malformed primary report instead of falling back to the secondary source", async () => {
|
|
894
|
+
const cwd = tempRepo();
|
|
895
|
+
try {
|
|
896
|
+
const acceptance = resolveEffectiveAcceptance({
|
|
897
|
+
agentName: "builder",
|
|
898
|
+
task: "Implement a fix",
|
|
899
|
+
explicit: { level: "checked" },
|
|
900
|
+
});
|
|
901
|
+
const malformedReports = [
|
|
902
|
+
"```acceptance-report\n{ not json\n```",
|
|
903
|
+
"```acceptance-report\n```",
|
|
904
|
+
"```acceptance-report\n{\"criteriaSatisfied\": []}",
|
|
905
|
+
"ACCEPTANCE_REPORT: { not json",
|
|
906
|
+
];
|
|
907
|
+
|
|
908
|
+
for (const malformed of malformedReports) {
|
|
909
|
+
const fileOnly = await evaluateAcceptance({
|
|
910
|
+
acceptance,
|
|
911
|
+
output: report({ notes: "valid text report" }),
|
|
912
|
+
fileOutput: { content: malformed, path: "/tmp/report.md", authoritative: true },
|
|
913
|
+
cwd,
|
|
914
|
+
});
|
|
915
|
+
assert.equal(fileOnly.status, "rejected", malformed);
|
|
916
|
+
assert.match(fileOnly.childReportParseError ?? "", /configured output/, malformed);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const inline = await evaluateAcceptance({
|
|
920
|
+
acceptance,
|
|
921
|
+
output: malformedReports[0]!,
|
|
922
|
+
fileOutput: { content: report({ notes: "valid file report" }), path: "/tmp/report.md" },
|
|
396
923
|
cwd,
|
|
397
924
|
});
|
|
925
|
+
assert.equal(inline.status, "rejected");
|
|
926
|
+
assert.match(inline.childReportParseError ?? "", /Failed to parse acceptance-report/);
|
|
927
|
+
} finally {
|
|
928
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
929
|
+
}
|
|
930
|
+
});
|
|
398
931
|
|
|
932
|
+
it("surfaces field-level errors from an invalid configured-output report", async () => {
|
|
933
|
+
const cwd = tempRepo();
|
|
934
|
+
try {
|
|
935
|
+
const acceptance = resolveEffectiveAcceptance({
|
|
936
|
+
agentName: "builder",
|
|
937
|
+
task: "Implement a fix",
|
|
938
|
+
explicit: { level: "checked" },
|
|
939
|
+
});
|
|
940
|
+
const ledger = await evaluateAcceptance({
|
|
941
|
+
acceptance,
|
|
942
|
+
output: "wrote the report to the file",
|
|
943
|
+
fileOutput: {
|
|
944
|
+
content: report({ criteriaSatisfied: [{ id: "criterion-1", status: "partially_done", evidence: "x" }] }),
|
|
945
|
+
path: "/tmp/report.md",
|
|
946
|
+
},
|
|
947
|
+
cwd,
|
|
948
|
+
});
|
|
399
949
|
assert.equal(ledger.status, "rejected");
|
|
400
|
-
assert.match(acceptanceFailureMessage(ledger) ?? "", /
|
|
950
|
+
assert.match(acceptanceFailureMessage(ledger) ?? "", /partially_done/);
|
|
951
|
+
assert.match(acceptanceFailureMessage(ledger) ?? "", /configured output/);
|
|
401
952
|
} finally {
|
|
402
953
|
fs.rmSync(cwd, { recursive: true, force: true });
|
|
403
954
|
}
|
|
404
955
|
});
|
|
405
956
|
|
|
957
|
+
it("validates explicit reviewed acceptance at every execution nesting level", () => {
|
|
958
|
+
const errors = validateExecutionAcceptance({
|
|
959
|
+
acceptance: "reviewed",
|
|
960
|
+
tasks: [{ acceptance: { level: "reviewed" } }],
|
|
961
|
+
chain: [
|
|
962
|
+
{ acceptance: "reviewed" },
|
|
963
|
+
{ parallel: [{ acceptance: { level: "reviewed" } }] },
|
|
964
|
+
{ parallel: { acceptance: "reviewed" } },
|
|
965
|
+
],
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
assert.equal(errors.length, 5);
|
|
969
|
+
assert.match(errors[0] ?? "", /^acceptance is an achieved status/);
|
|
970
|
+
assert.match(errors[1] ?? "", /^tasks\[0\]\.acceptance\.level is an achieved status/);
|
|
971
|
+
assert.match(errors[2] ?? "", /^chain\[0\]\.acceptance is an achieved status/);
|
|
972
|
+
assert.match(errors[3] ?? "", /^chain\[1\]\.parallel\[0\]\.acceptance\.level is an achieved status/);
|
|
973
|
+
assert.match(errors[4] ?? "", /^chain\[2\]\.parallel\.acceptance is an achieved status/);
|
|
974
|
+
assert.match(errors.join("\n"), /acceptance\.review\.required/);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
it("blanket read-only wording is not inferred as a risky write task", () => {
|
|
978
|
+
const readOnlyWorker = resolveEffectiveAcceptance({
|
|
979
|
+
agentName: "builder",
|
|
980
|
+
task: "Report on the extraction pipeline. Do not modify project/source files.",
|
|
981
|
+
async: true,
|
|
982
|
+
});
|
|
983
|
+
assert.equal(readOnlyWorker.level, "attested");
|
|
984
|
+
for (const task of [
|
|
985
|
+
"Inspect the extraction pipeline",
|
|
986
|
+
"Summarize the extraction pipeline",
|
|
987
|
+
"Review only: return findings",
|
|
988
|
+
"Analyze the extraction pipeline without edits",
|
|
989
|
+
]) {
|
|
990
|
+
assert.equal(resolveEffectiveAcceptance({ agentName: "builder", task, async: true }).level, "attested", task);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
assert.equal(resolveEffectiveAcceptance({ agentName: "builder", task: "Inspect the failure and implement the fix" }).level, "checked");
|
|
994
|
+
assert.equal(resolveEffectiveAcceptance({ agentName: "builder", task: "Inspect the failure and implement the fix", async: true }).level, "checked");
|
|
995
|
+
assert.equal(resolveEffectiveAcceptance({ agentName: "builder", task: "Do not modify tests; implement the fix", async: true }).level, "checked");
|
|
996
|
+
assert.equal(resolveEffectiveAcceptance({ agentName: "builder", task: "Do not modify tests but implement the fix", async: true }).level, "checked");
|
|
997
|
+
assert.equal(resolveEffectiveAcceptance({ agentName: "builder", task: "Do not modify tests and implement the fix", async: true }).level, "checked");
|
|
998
|
+
for (const task of [
|
|
999
|
+
"Do not modify tests - implement the fix",
|
|
1000
|
+
"Do not modify tests – implement the fix",
|
|
1001
|
+
"Do not modify tests — implement the fix",
|
|
1002
|
+
]) {
|
|
1003
|
+
assert.equal(resolveEffectiveAcceptance({ agentName: "builder", task }).level, "checked", task);
|
|
1004
|
+
assert.equal(resolveEffectiveAcceptance({ agentName: "builder", task, async: true }).level, "checked", task);
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
it("bare write verbs keep their review requirement for async tasks on any agent", () => {
|
|
1009
|
+
const tasks = [
|
|
1010
|
+
"Write the code",
|
|
1011
|
+
"Commit the changes",
|
|
1012
|
+
"Delete temporary data",
|
|
1013
|
+
"Remove obsolete assets",
|
|
1014
|
+
"Update dependencies",
|
|
1015
|
+
];
|
|
1016
|
+
for (const task of tasks) {
|
|
1017
|
+
const resolved = resolveEffectiveAcceptance({ agentName: "builder", task, async: true });
|
|
1018
|
+
assert.equal(resolved.level, "checked", task);
|
|
1019
|
+
assert.equal(resolved.review && resolved.review !== false ? resolved.review.required : undefined, true, task);
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
it("explicit levels are honored over a read-only inference without silent escalation", () => {
|
|
1024
|
+
const resolved = resolveEffectiveAcceptance({
|
|
1025
|
+
agentName: "researcher",
|
|
1026
|
+
task: "Research PDF backends. Do not modify project/source files.",
|
|
1027
|
+
explicit: "checked",
|
|
1028
|
+
async: true,
|
|
1029
|
+
});
|
|
1030
|
+
assert.equal(resolved.level, "checked");
|
|
1031
|
+
});
|
|
1032
|
+
|
|
406
1033
|
it("validates invalid disable and verify shapes", () => {
|
|
407
1034
|
assert.deepEqual(validateAcceptanceInput({ level: "none" }), ["acceptance.reason is required when level is none."]);
|
|
1035
|
+
assert.deepEqual(validateAcceptanceInput("none"), ["acceptance level \"none\" requires a reason; use { level: \"none\", reason: \"...\" }."]);
|
|
408
1036
|
assert.deepEqual(validateAcceptanceInput({ verify: [{ id: "missing-command" }] }), ["acceptance.verify[0].command is required."]);
|
|
409
1037
|
assert.deepEqual(validateAcceptanceInput({ verify: [{ id: "fractional", command: "npm test", timeoutMs: 1.5 }] }), ["acceptance.verify[0].timeoutMs must be an integer >= 1."]);
|
|
410
1038
|
assert.deepEqual(validateAcceptanceInput(false), []);
|
|
411
1039
|
assert.deepEqual(validateAcceptanceInput("checked"), []);
|
|
1040
|
+
assert.match(validateAcceptanceInput("reviewed").join("\n"), /achieved status.*omit acceptance.*acceptance\.review\.required/i);
|
|
1041
|
+
assert.match(validateAcceptanceInput({ level: "reviewed" }).join("\n"), /achieved status.*omit acceptance.*acceptance\.review\.required/i);
|
|
412
1042
|
assert.deepEqual(validateAcceptanceInput({ criteria: ["ship the fix"], review: false, stopRules: ["stay scoped"] }), []);
|
|
413
1043
|
assert.match(validateAcceptanceInput({ criteria: [{ id: "missing-must" }] }).join("\n"), /acceptance\.criteria\[0\]\.must is required/);
|
|
1044
|
+
assert.match(validateAcceptanceInput({ criteria: [
|
|
1045
|
+
{ id: "Release_Check", must: "first" },
|
|
1046
|
+
{ id: "release check", must: "second" },
|
|
1047
|
+
] }).join("\n"), /acceptance\.criteria\[1\]\.id duplicates normalized criterion id 'release-check'/);
|
|
414
1048
|
assert.match(validateAcceptanceInput({ criteria: [123] }).join("\n"), /acceptance\.criteria\[0\] must be a string or an object/);
|
|
415
1049
|
assert.match(validateAcceptanceInput({ evidence: ["bogus"] }).join("\n"), /acceptance\.evidence\[0\] is not a supported evidence kind/);
|
|
416
1050
|
assert.match(validateAcceptanceInput({ review: true }).join("\n"), /acceptance\.review must be false or an object/);
|