@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
|
@@ -6,10 +6,13 @@ import type {
|
|
|
6
6
|
AcceptanceConfig,
|
|
7
7
|
AcceptanceEvidenceKind,
|
|
8
8
|
AcceptanceInput,
|
|
9
|
+
AgentContract,
|
|
9
10
|
AcceptanceLedger,
|
|
10
11
|
AcceptanceLevel,
|
|
11
12
|
AcceptanceReport,
|
|
13
|
+
AcceptanceRole,
|
|
12
14
|
AcceptanceRuntimeCheck,
|
|
15
|
+
AcceptanceRuntimeCheckStatus,
|
|
13
16
|
AcceptanceReviewResult,
|
|
14
17
|
AcceptanceVerifyCommand,
|
|
15
18
|
AcceptanceVerifyResult,
|
|
@@ -18,16 +21,17 @@ import type {
|
|
|
18
21
|
SingleResult,
|
|
19
22
|
SubagentRunMode,
|
|
20
23
|
} from "../../shared/types.ts";
|
|
24
|
+
import { isAgentContractV1 } from "./agent-contract.ts";
|
|
25
|
+
import { classifyTaskMutationIntent, taskMayMutate } from "./task-intent.ts";
|
|
21
26
|
|
|
22
27
|
const LEVEL_RANK: Record<Exclude<AcceptanceLevel, "auto">, number> = {
|
|
23
28
|
none: 0,
|
|
24
29
|
attested: 1,
|
|
25
30
|
checked: 2,
|
|
26
31
|
verified: 3,
|
|
27
|
-
reviewed: 4,
|
|
28
32
|
};
|
|
29
33
|
|
|
30
|
-
const VALID_LEVELS = new Set<AcceptanceLevel>(["auto", "none", "attested", "checked", "verified"
|
|
34
|
+
const VALID_LEVELS = new Set<AcceptanceLevel>(["auto", "none", "attested", "checked", "verified"]);
|
|
31
35
|
const VALID_EVIDENCE = new Set<AcceptanceEvidenceKind>([
|
|
32
36
|
"changed-files",
|
|
33
37
|
"tests-added",
|
|
@@ -43,6 +47,7 @@ const ACCEPTANCE_CONFIG_KEYS = new Set(["level", "criteria", "evidence", "verify
|
|
|
43
47
|
const ACCEPTANCE_GATE_KEYS = new Set(["id", "must", "evidence", "severity"]);
|
|
44
48
|
const ACCEPTANCE_VERIFY_KEYS = new Set(["id", "command", "timeoutMs", "cwd", "env", "allowFailure"]);
|
|
45
49
|
const ACCEPTANCE_REVIEW_KEYS = new Set(["agent", "focus", "required"]);
|
|
50
|
+
const EXPLICIT_REVIEWED_UNAVAILABLE = "is an achieved status, not a requestable acceptance level. For a read-only reviewer call, omit acceptance. To require independent review of a writer result, use acceptance.review.required and orchestrate the reviewer separately.";
|
|
46
51
|
|
|
47
52
|
function normalizeLevel(level: AcceptanceLevel | undefined): Exclude<AcceptanceLevel, "auto"> | "auto" {
|
|
48
53
|
return level ?? "auto";
|
|
@@ -61,13 +66,13 @@ function requiredEvidenceForLevel(level: Exclude<AcceptanceLevel, "auto">): Acce
|
|
|
61
66
|
case "checked":
|
|
62
67
|
return ["changed-files", "tests-added", "commands-run", "residual-risks", "no-staged-files"];
|
|
63
68
|
case "verified":
|
|
64
|
-
case "reviewed":
|
|
65
69
|
return ["changed-files", "tests-added", "commands-run", "validation-output", "residual-risks", "no-staged-files"];
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
function inferLevel(input: {
|
|
70
74
|
agentName: string;
|
|
75
|
+
acceptanceRole?: AcceptanceRole;
|
|
71
76
|
task?: string;
|
|
72
77
|
mode?: SubagentRunMode;
|
|
73
78
|
async?: boolean;
|
|
@@ -77,28 +82,41 @@ function inferLevel(input: {
|
|
|
77
82
|
const agent = input.agentName.toLowerCase();
|
|
78
83
|
const task = input.task?.toLowerCase() ?? "";
|
|
79
84
|
const reasons: string[] = [];
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|| /\
|
|
85
|
+
// Declared roles replace name heuristics, so use the full writer grammar to detect explicit mutation independently of the actual agent name.
|
|
86
|
+
const intent = classifyTaskMutationIntent(input.acceptanceRole ? "builder" : input.agentName, input.task ?? "");
|
|
87
|
+
const readOnlyTask = intent.kind === "read-only"
|
|
88
|
+
|| (intent.kind === "unknown" && /\b(?:read[- ]only|review[- ]only|no edits|without edits|inspect|summari[sz]e)\b/.test(task));
|
|
89
|
+
const rolePatchTask = input.acceptanceRole !== undefined
|
|
90
|
+
&& intent.kind !== "read-only"
|
|
91
|
+
&& !/\b(?:do not|don't|must not)\s+patch\b/.test(task)
|
|
92
|
+
&& /\bpatch\s+(?:(?:\.{0,2}[\\/])?(?:[\w.-]+[\\/])+[\w.-]+|[\w.-]+\.[a-z0-9]+\b|(?:the\s+)?parser\b)/.test(task);
|
|
93
|
+
const taskMayWrite = readOnlyTask ? false : taskMayMutate(input.task ?? "") || intent.kind === "implementation" || rolePatchTask;
|
|
94
|
+
const readOnlyAgent = input.acceptanceRole === "read-only"
|
|
95
|
+
|| (input.acceptanceRole === undefined && /\b(?:architect|commentator|explorer|recapper|researcher|analyst)\b/.test(agent));
|
|
96
|
+
const writeTask = taskMayWrite
|
|
97
|
+
|| (input.acceptanceRole === "writer" && !readOnlyTask)
|
|
98
|
+
|| (input.acceptanceRole === undefined && /\bbuilder\b/.test(agent) && !readOnlyTask);
|
|
99
|
+
const inferredReadOnly = readOnlyTask || (input.acceptanceRole === "read-only" && !taskMayWrite);
|
|
100
|
+
const roleResolvesReadOnly = input.acceptanceRole !== undefined && inferredReadOnly;
|
|
101
|
+
const keywordRiskReadOnly = input.acceptanceRole === undefined ? intent.kind === "read-only" : inferredReadOnly;
|
|
84
102
|
const risky = Boolean(input.async && writeTask)
|
|
85
|
-
|| Boolean(input.dynamic)
|
|
86
|
-
|| Boolean(input.dynamicGroup)
|
|
87
|
-
|| /\b(?:release|migration|migrate|security|data[- ]loss|destructive|post-review|fix pass)\b/.test(task);
|
|
103
|
+
|| (Boolean(input.dynamic) && !roleResolvesReadOnly)
|
|
104
|
+
|| (Boolean(input.dynamicGroup) && !roleResolvesReadOnly)
|
|
105
|
+
|| (!keywordRiskReadOnly && /\b(?:release|migration|migrate|security|data[- ]loss|destructive|post-review|fix pass)\b/.test(task));
|
|
88
106
|
|
|
89
107
|
if (risky) {
|
|
90
108
|
reasons.push(input.async ? "async write-capable or risky run" : "risky write-capable run");
|
|
91
109
|
if (input.dynamic || input.dynamicGroup) reasons.push("dynamic fanout context");
|
|
92
110
|
return {
|
|
93
|
-
level: "
|
|
111
|
+
level: "checked",
|
|
94
112
|
reasons,
|
|
95
113
|
criteria: ["Implement the requested change without widening scope", "Return evidence sufficient for an independent acceptance review"],
|
|
96
|
-
evidence: requiredEvidenceForLevel("
|
|
97
|
-
review: { agent: "
|
|
114
|
+
evidence: requiredEvidenceForLevel("checked"),
|
|
115
|
+
review: { agent: "commentator", required: true },
|
|
98
116
|
};
|
|
99
117
|
}
|
|
100
118
|
if (writeTask && !readOnlyTask) {
|
|
101
|
-
reasons.push("write-capable worker/task");
|
|
119
|
+
reasons.push(input.acceptanceRole === "writer" && !taskMayWrite ? "declared writer acceptance role" : "write-capable worker/task");
|
|
102
120
|
return {
|
|
103
121
|
level: "checked",
|
|
104
122
|
reasons,
|
|
@@ -107,7 +125,7 @@ function inferLevel(input: {
|
|
|
107
125
|
};
|
|
108
126
|
}
|
|
109
127
|
if (readOnlyAgent || readOnlyTask) {
|
|
110
|
-
reasons.push(readOnlyAgent ? "read-only/reviewer-style agent" : "read-only task wording");
|
|
128
|
+
reasons.push(input.acceptanceRole === "read-only" && !readOnlyTask ? "declared read-only acceptance role" : readOnlyAgent ? "read-only/reviewer-style agent" : "read-only task wording");
|
|
111
129
|
return {
|
|
112
130
|
level: "attested",
|
|
113
131
|
reasons,
|
|
@@ -140,7 +158,9 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
140
158
|
if (input === undefined) return errors;
|
|
141
159
|
if (input === false) return errors;
|
|
142
160
|
if (typeof input === "string") {
|
|
143
|
-
if (
|
|
161
|
+
if (input === "reviewed") errors.push(`${pathLabel} ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
|
|
162
|
+
else if (!VALID_LEVELS.has(input as AcceptanceLevel)) errors.push(`${pathLabel} has invalid level '${input}'.`);
|
|
163
|
+
else if (input === "none") errors.push(`${pathLabel} level "none" requires a reason; use { level: "none", reason: "..." }.`);
|
|
144
164
|
return errors;
|
|
145
165
|
}
|
|
146
166
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
@@ -151,8 +171,10 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
151
171
|
for (const key of Object.keys(value)) {
|
|
152
172
|
if (!ACCEPTANCE_CONFIG_KEYS.has(key)) errors.push(`${pathLabel}.${key} is not supported.`);
|
|
153
173
|
}
|
|
154
|
-
if (value.level
|
|
155
|
-
errors.push(`${pathLabel}.level
|
|
174
|
+
if (value.level === "reviewed") {
|
|
175
|
+
errors.push(`${pathLabel}.level ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
|
|
176
|
+
} else if (value.level !== undefined && (typeof value.level !== "string" || !VALID_LEVELS.has(value.level as AcceptanceLevel))) {
|
|
177
|
+
errors.push(`${pathLabel}.level must be one of auto, none, attested, checked, verified.`);
|
|
156
178
|
}
|
|
157
179
|
if (value.level === "none" && (typeof value.reason !== "string" || !value.reason.trim())) {
|
|
158
180
|
errors.push(`${pathLabel}.reason is required when level is none.`);
|
|
@@ -160,6 +182,7 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
160
182
|
if (value.reason !== undefined && typeof value.reason !== "string") errors.push(`${pathLabel}.reason must be a string.`);
|
|
161
183
|
if (value.criteria !== undefined && !Array.isArray(value.criteria)) errors.push(`${pathLabel}.criteria must be an array.`);
|
|
162
184
|
if (Array.isArray(value.criteria)) {
|
|
185
|
+
const criterionIds = new Set<string>();
|
|
163
186
|
for (const [index, criterion] of value.criteria.entries()) {
|
|
164
187
|
if (typeof criterion === "string") continue;
|
|
165
188
|
const criterionPath = `${pathLabel}.criteria[${index}]`;
|
|
@@ -171,7 +194,13 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
171
194
|
for (const key of Object.keys(gate)) {
|
|
172
195
|
if (!ACCEPTANCE_GATE_KEYS.has(key)) errors.push(`${criterionPath}.${key} is not supported.`);
|
|
173
196
|
}
|
|
174
|
-
if (typeof gate.id !== "string" || !gate.id.trim())
|
|
197
|
+
if (typeof gate.id !== "string" || !gate.id.trim()) {
|
|
198
|
+
errors.push(`${criterionPath}.id is required.`);
|
|
199
|
+
} else {
|
|
200
|
+
const normalizedId = normalizedToken(gate.id);
|
|
201
|
+
if (criterionIds.has(normalizedId)) errors.push(`${criterionPath}.id duplicates normalized criterion id '${normalizedId}'.`);
|
|
202
|
+
criterionIds.add(normalizedId);
|
|
203
|
+
}
|
|
175
204
|
if (typeof gate.must !== "string" || !gate.must.trim()) errors.push(`${criterionPath}.must is required.`);
|
|
176
205
|
if (gate.evidence !== undefined && !Array.isArray(gate.evidence)) errors.push(`${criterionPath}.evidence must be an array.`);
|
|
177
206
|
if (Array.isArray(gate.evidence)) {
|
|
@@ -248,6 +277,31 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
248
277
|
return errors;
|
|
249
278
|
}
|
|
250
279
|
|
|
280
|
+
export function validateExecutionAcceptance(input: {
|
|
281
|
+
acceptance?: unknown;
|
|
282
|
+
tasks?: Array<{ acceptance?: unknown }>;
|
|
283
|
+
chain?: Array<{
|
|
284
|
+
acceptance?: unknown;
|
|
285
|
+
parallel?: Array<{ acceptance?: unknown }> | { acceptance?: unknown };
|
|
286
|
+
}>;
|
|
287
|
+
}): string[] {
|
|
288
|
+
const errors = validateAcceptanceInput(input.acceptance, "acceptance");
|
|
289
|
+
for (const [index, task] of (input.tasks ?? []).entries()) {
|
|
290
|
+
errors.push(...validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`));
|
|
291
|
+
}
|
|
292
|
+
for (const [stepIndex, step] of (input.chain ?? []).entries()) {
|
|
293
|
+
errors.push(...validateAcceptanceInput(step.acceptance, `chain[${stepIndex}].acceptance`));
|
|
294
|
+
if (Array.isArray(step.parallel)) {
|
|
295
|
+
for (const [taskIndex, task] of step.parallel.entries()) {
|
|
296
|
+
errors.push(...validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`));
|
|
297
|
+
}
|
|
298
|
+
} else if (step.parallel) {
|
|
299
|
+
errors.push(...validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return errors;
|
|
303
|
+
}
|
|
304
|
+
|
|
251
305
|
function normalizeCriteria(criteria: Array<string | { id?: string; must?: string; evidence?: AcceptanceEvidenceKind[]; severity?: "required" | "recommended" }> | undefined, evidence: AcceptanceEvidenceKind[]): ResolvedAcceptanceGate[] {
|
|
252
306
|
return (criteria ?? []).map((criterion, index) => {
|
|
253
307
|
if (typeof criterion === "string") {
|
|
@@ -265,15 +319,38 @@ function normalizeCriteria(criteria: Array<string | { id?: string; must?: string
|
|
|
265
319
|
export function resolveEffectiveAcceptance(input: {
|
|
266
320
|
explicit?: AcceptanceInput;
|
|
267
321
|
agentName: string;
|
|
322
|
+
acceptanceRole?: AcceptanceRole;
|
|
268
323
|
task?: string;
|
|
269
324
|
mode?: SubagentRunMode;
|
|
270
325
|
async?: boolean;
|
|
271
326
|
dynamic?: boolean;
|
|
272
327
|
dynamicGroup?: boolean;
|
|
328
|
+
agentContract?: AgentContract;
|
|
273
329
|
}): ResolvedAcceptanceConfig {
|
|
274
330
|
const explicit = normalizeAcceptanceInput(input.explicit);
|
|
275
|
-
const inferred = inferLevel(input);
|
|
276
331
|
const explicitLevel = normalizeLevel(explicit.level);
|
|
332
|
+
if (isAgentContractV1(input.agentContract)) {
|
|
333
|
+
const level = explicitAcceptanceCanDisable(explicit) || explicitLevel === "auto"
|
|
334
|
+
? "none"
|
|
335
|
+
: explicitLevel;
|
|
336
|
+
const evidence = unique(explicit.evidence ?? []);
|
|
337
|
+
const criteria = normalizeCriteria(
|
|
338
|
+
explicit.criteria as Array<string | { id?: string; must?: string; evidence?: AcceptanceEvidenceKind[]; severity?: "required" | "recommended" }> | undefined,
|
|
339
|
+
evidence,
|
|
340
|
+
);
|
|
341
|
+
return {
|
|
342
|
+
level,
|
|
343
|
+
explicit: input.explicit !== undefined,
|
|
344
|
+
inferredReason: [],
|
|
345
|
+
criteria,
|
|
346
|
+
evidence,
|
|
347
|
+
verify: explicit.verify ?? [],
|
|
348
|
+
review: explicit.review,
|
|
349
|
+
stopRules: explicit.stopRules ?? [],
|
|
350
|
+
reason: explicit.reason,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const inferred = inferLevel(input);
|
|
277
354
|
const level = explicitAcceptanceCanDisable(explicit)
|
|
278
355
|
? "none"
|
|
279
356
|
: explicitLevel === "auto"
|
|
@@ -284,10 +361,7 @@ export function resolveEffectiveAcceptance(input: {
|
|
|
284
361
|
(explicit.criteria?.length ? explicit.criteria : inferred.criteria) as Array<string | { id?: string; must?: string; evidence?: AcceptanceEvidenceKind[]; severity?: "required" | "recommended" }>,
|
|
285
362
|
evidence,
|
|
286
363
|
);
|
|
287
|
-
|
|
288
|
-
if (level === "reviewed" && explicitLevel !== "auto" && explicitLevel !== "reviewed" && explicit.review === undefined && review && review !== false) {
|
|
289
|
-
review = { ...review, required: false };
|
|
290
|
-
}
|
|
364
|
+
const review = explicit.review !== undefined ? explicit.review : inferred.review;
|
|
291
365
|
return {
|
|
292
366
|
level,
|
|
293
367
|
explicit: input.explicit !== undefined,
|
|
@@ -301,8 +375,13 @@ export function resolveEffectiveAcceptance(input: {
|
|
|
301
375
|
};
|
|
302
376
|
}
|
|
303
377
|
|
|
304
|
-
|
|
378
|
+
function acceptanceRequiresChildReport(acceptance: ResolvedAcceptanceConfig): boolean {
|
|
379
|
+
return acceptance.criteria.length > 0 || acceptance.evidence.length > 0;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function formatAcceptancePrompt(acceptance: ResolvedAcceptanceConfig, options: { reportOptional?: boolean } = {}): string {
|
|
305
383
|
if (acceptance.level === "none") return "";
|
|
384
|
+
if (options.reportOptional && !acceptanceRequiresChildReport(acceptance)) return "";
|
|
306
385
|
const lines = [
|
|
307
386
|
"",
|
|
308
387
|
"## Acceptance Contract",
|
|
@@ -329,9 +408,14 @@ export function formatAcceptancePrompt(acceptance: ResolvedAcceptanceConfig): st
|
|
|
329
408
|
"",
|
|
330
409
|
"Finish with a fenced JSON block tagged `acceptance-report` in this shape:",
|
|
331
410
|
"Use empty arrays when no items apply; array fields contain strings unless object entries are shown.",
|
|
411
|
+
"`criteriaSatisfied[].status` must be exactly one of: satisfied, not-satisfied, not-applicable.",
|
|
412
|
+
"`commandsRun[].result` must be exactly one of: passed, failed, not-run.",
|
|
413
|
+
"`manualNotes` and `notes` are optional strings; an empty string means no note and does not satisfy `manual-notes` evidence.",
|
|
332
414
|
"```acceptance-report",
|
|
333
415
|
JSON.stringify({
|
|
334
|
-
criteriaSatisfied:
|
|
416
|
+
criteriaSatisfied: acceptance.criteria
|
|
417
|
+
.filter((criterion) => criterion.severity !== "recommended")
|
|
418
|
+
.map((criterion) => ({ id: criterion.id, status: "satisfied", evidence: "specific proof" })),
|
|
335
419
|
changedFiles: ["src/file.ts"],
|
|
336
420
|
testsAddedOrUpdated: ["test/file.test.ts"],
|
|
337
421
|
commandsRun: [{ command: "command", result: "passed", summary: "short result" }],
|
|
@@ -372,38 +456,166 @@ function extractBalancedJson(text: string, start: number): string | undefined {
|
|
|
372
456
|
return undefined;
|
|
373
457
|
}
|
|
374
458
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
459
|
+
const ACCEPTANCE_REPORT_WRAPPERS = new Set(["acceptance", "acceptance-report", "acceptance_report", "acceptanceReport"]);
|
|
460
|
+
|
|
461
|
+
const ACCEPTANCE_REPORT_FIELDS: Record<string, keyof AcceptanceReport> = {
|
|
462
|
+
criteriaSatisfied: "criteriaSatisfied",
|
|
463
|
+
criteria_satisfied: "criteriaSatisfied",
|
|
464
|
+
changedFiles: "changedFiles",
|
|
465
|
+
changed_files: "changedFiles",
|
|
466
|
+
testsAddedOrUpdated: "testsAddedOrUpdated",
|
|
467
|
+
tests_added_or_updated: "testsAddedOrUpdated",
|
|
468
|
+
commandsRun: "commandsRun",
|
|
469
|
+
commands_run: "commandsRun",
|
|
470
|
+
validationOutput: "validationOutput",
|
|
471
|
+
validation_output: "validationOutput",
|
|
472
|
+
residualRisks: "residualRisks",
|
|
473
|
+
residual_risks: "residualRisks",
|
|
474
|
+
noStagedFiles: "noStagedFiles",
|
|
475
|
+
no_staged_files: "noStagedFiles",
|
|
476
|
+
diffSummary: "diffSummary",
|
|
477
|
+
diff_summary: "diffSummary",
|
|
478
|
+
reviewFindings: "reviewFindings",
|
|
479
|
+
review_findings: "reviewFindings",
|
|
480
|
+
manualNotes: "manualNotes",
|
|
481
|
+
manual_notes: "manualNotes",
|
|
482
|
+
notes: "notes",
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const CRITERION_REPORT_FIELDS = new Set(["id", "status", "evidence"]);
|
|
486
|
+
const COMMAND_REPORT_FIELDS = new Set(["command", "result", "summary"]);
|
|
487
|
+
|
|
488
|
+
function normalizedToken(value: string): string {
|
|
489
|
+
return value.trim().toLowerCase().replace(/[\s_]+/g, "-").replace(/-+/g, "-");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function normalizeCriterionStatus(value: unknown): unknown {
|
|
493
|
+
if (typeof value !== "string") return value;
|
|
494
|
+
const token = normalizedToken(value);
|
|
495
|
+
if (["satisfied", "met", "complete", "completed", "done", "pass", "passed", "success", "succeeded"].includes(token)) return "satisfied";
|
|
496
|
+
if (["not-satisfied", "not-met", "unmet", "incomplete", "fail", "failed"].includes(token)) return "not-satisfied";
|
|
497
|
+
if (["not-applicable", "n-a", "na", "skip", "skipped"].includes(token)) return "not-applicable";
|
|
380
498
|
return value;
|
|
381
499
|
}
|
|
382
500
|
|
|
383
|
-
function
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
501
|
+
function normalizeCommandResult(value: unknown): unknown {
|
|
502
|
+
if (typeof value !== "string") return value;
|
|
503
|
+
const token = normalizedToken(value);
|
|
504
|
+
if (["passed", "pass", "success", "successful", "succeeded", "ok"].includes(token)) return "passed";
|
|
505
|
+
if (["failed", "fail", "failure", "error"].includes(token)) return "failed";
|
|
506
|
+
if (["not-run", "not-executed", "skip", "skipped"].includes(token)) return "not-run";
|
|
507
|
+
return value;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function normalizeCriterionReport(value: unknown, pathLabel: string, errors: string[]): unknown {
|
|
511
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
512
|
+
const normalized: Record<string, unknown> = {};
|
|
513
|
+
for (const [key, fieldValue] of Object.entries(value as Record<string, unknown>)) {
|
|
514
|
+
if (!CRITERION_REPORT_FIELDS.has(key)) {
|
|
515
|
+
errors.push(`${pathLabel}.${key}: unsupported acceptance criterion field`);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
normalized[key] = key === "id" && typeof fieldValue === "string"
|
|
519
|
+
? normalizedToken(fieldValue)
|
|
520
|
+
: key === "status"
|
|
521
|
+
? normalizeCriterionStatus(fieldValue)
|
|
522
|
+
: fieldValue;
|
|
523
|
+
}
|
|
524
|
+
return normalized;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function normalizeCommandReport(value: unknown, pathLabel: string, errors: string[]): unknown {
|
|
528
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
529
|
+
const normalized: Record<string, unknown> = {};
|
|
530
|
+
for (const [key, fieldValue] of Object.entries(value as Record<string, unknown>)) {
|
|
531
|
+
if (!COMMAND_REPORT_FIELDS.has(key)) {
|
|
532
|
+
errors.push(`${pathLabel}.${key}: unsupported acceptance command field`);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
normalized[key] = key === "result" ? normalizeCommandResult(fieldValue) : fieldValue;
|
|
536
|
+
}
|
|
537
|
+
return normalized;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function normalizeAcceptanceReportValue(value: unknown, pathLabel = ""): { value: unknown; pathLabel: string; errors: string[] } {
|
|
541
|
+
const errors: string[] = [];
|
|
542
|
+
let reportValue = value;
|
|
543
|
+
let reportPath = pathLabel;
|
|
544
|
+
if (reportValue && typeof reportValue === "object" && !Array.isArray(reportValue)) {
|
|
545
|
+
const record = reportValue as Record<string, unknown>;
|
|
546
|
+
const wrapperKeys = Object.keys(record).filter((key) => ACCEPTANCE_REPORT_WRAPPERS.has(key));
|
|
547
|
+
if (wrapperKeys.length > 0) {
|
|
548
|
+
const wrapperKey = wrapperKeys[0]!;
|
|
549
|
+
if (wrapperKeys.length > 1) errors.push(`${pathLabel || "acceptance-report"}: multiple acceptance report wrappers are ambiguous`);
|
|
550
|
+
for (const key of Object.keys(record)) {
|
|
551
|
+
if (key !== wrapperKey) errors.push(`${pathFor(pathLabel, key)}: unsupported alongside acceptance report wrapper '${wrapperKey}'`);
|
|
552
|
+
}
|
|
553
|
+
reportValue = record[wrapperKey];
|
|
554
|
+
reportPath = pathFor(pathLabel, wrapperKey);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (!reportValue || typeof reportValue !== "object" || Array.isArray(reportValue)) return { value: reportValue, pathLabel: reportPath, errors };
|
|
558
|
+
|
|
559
|
+
const normalized: Record<string, unknown> = {};
|
|
560
|
+
for (const [key, fieldValue] of Object.entries(reportValue as Record<string, unknown>)) {
|
|
561
|
+
const canonical = ACCEPTANCE_REPORT_FIELDS[key];
|
|
562
|
+
if (!canonical) {
|
|
563
|
+
errors.push(`${pathFor(reportPath, key)}: unsupported acceptance report field`);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (Object.hasOwn(normalized, canonical)) {
|
|
567
|
+
errors.push(`${pathFor(reportPath, key)}: duplicates normalized field '${canonical}'`);
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
const fieldPath = pathFor(reportPath, canonical);
|
|
571
|
+
switch (canonical) {
|
|
572
|
+
case "criteriaSatisfied": {
|
|
573
|
+
const items = Array.isArray(fieldValue) ? fieldValue : fieldValue && typeof fieldValue === "object" ? [fieldValue] : fieldValue;
|
|
574
|
+
normalized[canonical] = Array.isArray(items)
|
|
575
|
+
? items.map((item, index) => normalizeCriterionReport(item, `${fieldPath}[${index}]`, errors))
|
|
576
|
+
: items;
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
case "commandsRun": {
|
|
580
|
+
const items = Array.isArray(fieldValue) ? fieldValue : fieldValue && typeof fieldValue === "object" ? [fieldValue] : fieldValue;
|
|
581
|
+
normalized[canonical] = Array.isArray(items)
|
|
582
|
+
? items.map((item, index) => normalizeCommandReport(item, `${fieldPath}[${index}]`, errors))
|
|
583
|
+
: items;
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
case "changedFiles":
|
|
587
|
+
case "testsAddedOrUpdated":
|
|
588
|
+
case "validationOutput":
|
|
589
|
+
case "residualRisks":
|
|
590
|
+
case "reviewFindings":
|
|
591
|
+
normalized[canonical] = typeof fieldValue === "string" ? [fieldValue] : fieldValue;
|
|
592
|
+
break;
|
|
593
|
+
case "noStagedFiles": {
|
|
594
|
+
const token = typeof fieldValue === "string" ? fieldValue.trim().toLowerCase() : undefined;
|
|
595
|
+
normalized[canonical] = token === "true" ? true : token === "false" ? false : fieldValue;
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
default:
|
|
599
|
+
normalized[canonical] = fieldValue;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return { value: normalized, pathLabel: reportPath, errors };
|
|
391
603
|
}
|
|
392
604
|
|
|
393
605
|
function hasGenericAcceptanceReportSignal(value: unknown): boolean {
|
|
394
606
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
395
607
|
const record = value as Record<string, unknown>;
|
|
396
|
-
return "criteriaSatisfied" in record &&
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
);
|
|
608
|
+
return "criteriaSatisfied" in record && [
|
|
609
|
+
"changedFiles",
|
|
610
|
+
"testsAddedOrUpdated",
|
|
611
|
+
"commandsRun",
|
|
612
|
+
"validationOutput",
|
|
613
|
+
"residualRisks",
|
|
614
|
+
"noStagedFiles",
|
|
615
|
+
"diffSummary",
|
|
616
|
+
"reviewFindings",
|
|
617
|
+
"manualNotes",
|
|
618
|
+
].some((key) => key in record);
|
|
407
619
|
}
|
|
408
620
|
|
|
409
621
|
function parseReportJson(body: string): unknown {
|
|
@@ -426,30 +638,29 @@ function fencedBlocks(output: string, tag: string): string[] {
|
|
|
426
638
|
.filter((value): value is string => Boolean(value));
|
|
427
639
|
}
|
|
428
640
|
|
|
429
|
-
function validationPathLabelForWrapper(value: unknown): string {
|
|
430
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
|
|
431
|
-
const record = value as Record<string, unknown>;
|
|
432
|
-
if ("acceptance" in record) return "acceptance";
|
|
433
|
-
if ("acceptance-report" in record) return "acceptance-report";
|
|
434
|
-
return "";
|
|
435
|
-
}
|
|
436
|
-
|
|
437
641
|
function parseAcceptanceReportBody(body: string): { report?: AcceptanceReport; errors: string[] } {
|
|
438
|
-
|
|
439
|
-
const report = unwrapAcceptanceReport(parsed);
|
|
440
|
-
return validateAcceptanceReport(report, validationPathLabelForWrapper(parsed));
|
|
642
|
+
return validateAcceptanceReport(parseReportJson(body));
|
|
441
643
|
}
|
|
442
644
|
|
|
443
|
-
function parseGenericJsonAcceptanceReportBody(body: string): AcceptanceReport
|
|
645
|
+
function parseGenericJsonAcceptanceReportBody(body: string): { report?: AcceptanceReport; error?: string } {
|
|
444
646
|
const parsed = parseReportJson(body);
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
647
|
+
const normalized = normalizeAcceptanceReportValue(parsed);
|
|
648
|
+
const hasCriteriaMarker = normalized.value !== null
|
|
649
|
+
&& typeof normalized.value === "object"
|
|
650
|
+
&& !Array.isArray(normalized.value)
|
|
651
|
+
&& "criteriaSatisfied" in normalized.value;
|
|
652
|
+
if (!hasGenericAcceptanceReportSignal(normalized.value) && !(hasCriteriaMarker && normalized.errors.length > 0)) return {};
|
|
653
|
+
const validation = validateAcceptanceReport(parsed);
|
|
654
|
+
return validation.report
|
|
655
|
+
? { report: validation.report }
|
|
656
|
+
: { error: `Invalid acceptance-report: ${validation.errors.join("; ")}` };
|
|
449
657
|
}
|
|
450
658
|
|
|
659
|
+
export const ACCEPTANCE_REPORT_NOT_FOUND = "Structured acceptance report not found.";
|
|
660
|
+
|
|
451
661
|
export function parseAcceptanceReport(output: string): { report?: AcceptanceReport; error?: string } {
|
|
452
|
-
const
|
|
662
|
+
const explicitFencePresent = /```acceptance[-_]report\b/i.test(output);
|
|
663
|
+
const fenced = fencedBlocks(output, "acceptance[-_]report");
|
|
453
664
|
const parseErrors: string[] = [];
|
|
454
665
|
for (const body of fenced) {
|
|
455
666
|
try {
|
|
@@ -461,38 +672,65 @@ export function parseAcceptanceReport(output: string): { report?: AcceptanceRepo
|
|
|
461
672
|
}
|
|
462
673
|
}
|
|
463
674
|
if (parseErrors.length > 0) return { error: `Failed to parse acceptance-report: ${parseErrors.join("; ")}` };
|
|
675
|
+
if (explicitFencePresent) {
|
|
676
|
+
return { error: "Failed to parse acceptance-report: Empty or unterminated acceptance-report fence." };
|
|
677
|
+
}
|
|
464
678
|
for (const body of fencedBlocks(output, "(?:json|jsonc|json5)")) {
|
|
465
679
|
try {
|
|
466
|
-
const
|
|
467
|
-
if (report) return { report };
|
|
680
|
+
const parsed = parseGenericJsonAcceptanceReportBody(body);
|
|
681
|
+
if (parsed.report) return { report: parsed.report };
|
|
682
|
+
if (parsed.error) return { error: `Failed to parse acceptance-report: ${parsed.error}` };
|
|
468
683
|
} catch {
|
|
469
|
-
// Ignore unrelated
|
|
470
|
-
//
|
|
684
|
+
// Ignore unrelated malformed generic JSON. A recognizable report shape
|
|
685
|
+
// returns exact validation errors above instead of being mistaken for prose.
|
|
471
686
|
}
|
|
472
687
|
}
|
|
473
688
|
const markerIndex = output.search(/ACCEPTANCE_REPORT\s*:/i);
|
|
474
689
|
if (markerIndex !== -1) {
|
|
475
690
|
const jsonStart = output.indexOf("{", markerIndex);
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
const validation = validateAcceptanceReport(report, validationPathLabelForWrapper(parsed));
|
|
483
|
-
if (validation.report) return { report: validation.report };
|
|
484
|
-
return { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}` };
|
|
485
|
-
} catch (error) {
|
|
486
|
-
return { error: error instanceof Error ? error.message : String(error) };
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
691
|
+
if (jsonStart === -1) {
|
|
692
|
+
return { error: "Failed to parse acceptance-report: Expected a JSON object after ACCEPTANCE_REPORT:." };
|
|
693
|
+
}
|
|
694
|
+
const json = extractBalancedJson(output, jsonStart);
|
|
695
|
+
if (!json) {
|
|
696
|
+
return { error: "Failed to parse acceptance-report: Unterminated JSON object after ACCEPTANCE_REPORT:." };
|
|
490
697
|
}
|
|
491
|
-
|
|
698
|
+
try {
|
|
699
|
+
const parsed = JSON.parse(json) as unknown;
|
|
700
|
+
const validation = validateAcceptanceReport(parsed);
|
|
701
|
+
if (validation.report) return { report: validation.report };
|
|
702
|
+
return { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}` };
|
|
703
|
+
} catch (error) {
|
|
704
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
705
|
+
return { error: `Failed to parse acceptance-report: ${message}` };
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return { error: ACCEPTANCE_REPORT_NOT_FOUND };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function parseAcceptanceReportSources(
|
|
712
|
+
output: string,
|
|
713
|
+
fileOutput: { content: string; path: string; authoritative?: boolean } | undefined,
|
|
714
|
+
): { report?: AcceptanceReport; error?: string } {
|
|
715
|
+
const fromText = () => parseAcceptanceReport(output);
|
|
716
|
+
const fromFile = () => {
|
|
717
|
+
if (!fileOutput) return { error: ACCEPTANCE_REPORT_NOT_FOUND };
|
|
718
|
+
const parsed = parseAcceptanceReport(fileOutput.content);
|
|
719
|
+
return parsed.report || parsed.error === ACCEPTANCE_REPORT_NOT_FOUND
|
|
720
|
+
? parsed
|
|
721
|
+
: { error: `${parsed.error} (in configured output ${fileOutput.path})` };
|
|
722
|
+
};
|
|
723
|
+
const [primary, secondary] = fileOutput?.authoritative ? [fromFile, fromText] : [fromText, fromFile];
|
|
724
|
+
const first = primary();
|
|
725
|
+
// A malformed report in the primary source is a defect to surface, not a
|
|
726
|
+
// miss to paper over with the secondary source; only a genuinely absent
|
|
727
|
+
// report falls through.
|
|
728
|
+
if (first.report || first.error !== ACCEPTANCE_REPORT_NOT_FOUND) return first;
|
|
729
|
+
return secondary();
|
|
492
730
|
}
|
|
493
731
|
|
|
494
732
|
export function stripAcceptanceReport(output: string): string {
|
|
495
|
-
const trailingFencePattern = /\n?```(acceptance-report|json|jsonc|json5)\s*\n([\s\S]*?)```\s*/gi;
|
|
733
|
+
const trailingFencePattern = /\n?```(acceptance[-_]report|json|jsonc|json5)\s*\n([\s\S]*?)```\s*/gi;
|
|
496
734
|
let trailingFence: { index: number; tag: string; body: string } | undefined;
|
|
497
735
|
for (const match of output.matchAll(trailingFencePattern)) {
|
|
498
736
|
const end = (match.index ?? 0) + match[0].length;
|
|
@@ -501,15 +739,15 @@ export function stripAcceptanceReport(output: string): string {
|
|
|
501
739
|
}
|
|
502
740
|
}
|
|
503
741
|
if (trailingFence) {
|
|
504
|
-
if (trailingFence.tag === "acceptance-report") return output.slice(0, trailingFence.index).trimEnd();
|
|
742
|
+
if (trailingFence.tag === "acceptance-report" || trailingFence.tag === "acceptance_report") return output.slice(0, trailingFence.index).trimEnd();
|
|
505
743
|
try {
|
|
506
|
-
if (parseGenericJsonAcceptanceReportBody(trailingFence.body)) return output.slice(0, trailingFence.index).trimEnd();
|
|
744
|
+
if (parseGenericJsonAcceptanceReportBody(trailingFence.body).report) return output.slice(0, trailingFence.index).trimEnd();
|
|
507
745
|
} catch {
|
|
508
746
|
// Leave unrelated or malformed generic JSON fences visible.
|
|
509
747
|
}
|
|
510
748
|
}
|
|
511
749
|
return output
|
|
512
|
-
.replace(/\n?```acceptance-report\s*\n[\s\S]*?```\s*$/i, "")
|
|
750
|
+
.replace(/\n?```acceptance[-_]report\s*\n[\s\S]*?```\s*$/i, "")
|
|
513
751
|
.replace(/\n?ACCEPTANCE_REPORT\s*:\s*\{[\s\S]*\}\s*$/i, "")
|
|
514
752
|
.trimEnd();
|
|
515
753
|
}
|
|
@@ -544,12 +782,15 @@ function validateStringArrayField(errors: string[], value: unknown, pathLabel: s
|
|
|
544
782
|
return;
|
|
545
783
|
}
|
|
546
784
|
for (const [index, item] of value.entries()) {
|
|
547
|
-
if (typeof item !== "string") pushTypeError(errors, `${pathLabel}[${index}]`, "string", item);
|
|
785
|
+
if (typeof item !== "string" || !item.trim()) pushTypeError(errors, `${pathLabel}[${index}]`, "non-empty string", item);
|
|
548
786
|
}
|
|
549
787
|
}
|
|
550
788
|
|
|
551
789
|
function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: AcceptanceReport; errors: string[] } {
|
|
552
|
-
const
|
|
790
|
+
const normalized = normalizeAcceptanceReportValue(value, pathLabel);
|
|
791
|
+
value = normalized.value;
|
|
792
|
+
pathLabel = normalized.pathLabel;
|
|
793
|
+
const errors = normalized.errors;
|
|
553
794
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
554
795
|
pushTypeError(errors, pathLabel || "acceptance-report", "object", value);
|
|
555
796
|
return { errors };
|
|
@@ -559,6 +800,7 @@ function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: Ac
|
|
|
559
800
|
if (!Array.isArray(report.criteriaSatisfied)) {
|
|
560
801
|
pushTypeError(errors, pathFor(pathLabel, "criteriaSatisfied"), "array", report.criteriaSatisfied);
|
|
561
802
|
} else {
|
|
803
|
+
const criterionIds = new Set<string>();
|
|
562
804
|
for (const [index, item] of report.criteriaSatisfied.entries()) {
|
|
563
805
|
const itemPath = `${pathFor(pathLabel, "criteriaSatisfied")}[${index}]`;
|
|
564
806
|
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
@@ -566,7 +808,12 @@ function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: Ac
|
|
|
566
808
|
continue;
|
|
567
809
|
}
|
|
568
810
|
const criterion = item as { id?: unknown; status?: unknown; evidence?: unknown };
|
|
569
|
-
if (criterion.id !== undefined && typeof criterion.id !== "string")
|
|
811
|
+
if (criterion.id !== undefined && typeof criterion.id !== "string") {
|
|
812
|
+
pushTypeError(errors, `${itemPath}.id`, "string", criterion.id);
|
|
813
|
+
} else if (typeof criterion.id === "string" && criterion.id) {
|
|
814
|
+
if (criterionIds.has(criterion.id)) errors.push(`${itemPath}.id: duplicate normalized criterion id '${criterion.id}'`);
|
|
815
|
+
criterionIds.add(criterion.id);
|
|
816
|
+
}
|
|
570
817
|
if (criterion.status !== "satisfied" && criterion.status !== "not-satisfied" && criterion.status !== "not-applicable") {
|
|
571
818
|
pushTypeError(errors, `${itemPath}.status`, "one of \"satisfied\", \"not-satisfied\", \"not-applicable\"", criterion.status);
|
|
572
819
|
}
|
|
@@ -591,14 +838,14 @@ function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: Ac
|
|
|
591
838
|
if (command.result !== "passed" && command.result !== "failed" && command.result !== "not-run") {
|
|
592
839
|
pushTypeError(errors, `${itemPath}.result`, "one of \"passed\", \"failed\", \"not-run\"", command.result);
|
|
593
840
|
}
|
|
594
|
-
if (typeof command.summary !== "string") pushTypeError(errors, `${itemPath}.summary`, "string", command.summary);
|
|
841
|
+
if (typeof command.summary !== "string" || !command.summary.trim()) pushTypeError(errors, `${itemPath}.summary`, "non-empty string", command.summary);
|
|
595
842
|
}
|
|
596
843
|
}
|
|
597
844
|
}
|
|
598
845
|
if (report.validationOutput !== undefined) validateStringArrayField(errors, report.validationOutput, pathFor(pathLabel, "validationOutput"));
|
|
599
846
|
if (report.residualRisks !== undefined) validateStringArrayField(errors, report.residualRisks, pathFor(pathLabel, "residualRisks"));
|
|
600
847
|
if (report.noStagedFiles !== undefined && typeof report.noStagedFiles !== "boolean") pushTypeError(errors, pathFor(pathLabel, "noStagedFiles"), "boolean", report.noStagedFiles);
|
|
601
|
-
if (report.diffSummary !== undefined && typeof report.diffSummary !== "string") pushTypeError(errors, pathFor(pathLabel, "diffSummary"), "string", report.diffSummary);
|
|
848
|
+
if (report.diffSummary !== undefined && (typeof report.diffSummary !== "string" || !report.diffSummary.trim())) pushTypeError(errors, pathFor(pathLabel, "diffSummary"), "non-empty string", report.diffSummary);
|
|
602
849
|
if (report.reviewFindings !== undefined) validateStringArrayField(errors, report.reviewFindings, pathFor(pathLabel, "reviewFindings"));
|
|
603
850
|
if (report.manualNotes !== undefined && typeof report.manualNotes !== "string") pushTypeError(errors, pathFor(pathLabel, "manualNotes"), "string", report.manualNotes);
|
|
604
851
|
if (report.notes !== undefined && typeof report.notes !== "string") pushTypeError(errors, pathFor(pathLabel, "notes"), "string", report.notes);
|
|
@@ -620,26 +867,30 @@ function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: Ac
|
|
|
620
867
|
}
|
|
621
868
|
|
|
622
869
|
function checkCriteriaSatisfied(criteria: ResolvedAcceptanceGate[], report: AcceptanceReport): AcceptanceRuntimeCheck[] {
|
|
623
|
-
const reports = new Map((report.criteriaSatisfied ?? []).filter((item) => item.id).map((item) => [item.id
|
|
870
|
+
const reports = new Map((report.criteriaSatisfied ?? []).filter((item) => item.id).map((item) => [normalizedToken(item.id!), item]));
|
|
624
871
|
return criteria.filter((criterion) => criterion.severity !== "recommended").map((criterion) => {
|
|
625
|
-
const item = reports.get(criterion.id);
|
|
872
|
+
const item = reports.get(normalizedToken(criterion.id));
|
|
626
873
|
if (!item) return { id: `criterion:${criterion.id}`, status: "failed", message: `Required criterion '${criterion.id}' was not reported.` };
|
|
627
874
|
if (item.status !== "satisfied") return { id: `criterion:${criterion.id}`, status: "failed", message: `Required criterion '${criterion.id}' was reported as ${item.status}.` };
|
|
628
875
|
return { id: `criterion:${criterion.id}`, status: "passed", message: `Required criterion '${criterion.id}' satisfied.` };
|
|
629
876
|
});
|
|
630
877
|
}
|
|
631
878
|
|
|
632
|
-
function
|
|
879
|
+
function reportEvidenceStatus(report: AcceptanceReport, kind: AcceptanceEvidenceKind): AcceptanceRuntimeCheckStatus {
|
|
633
880
|
switch (kind) {
|
|
634
|
-
case "changed-files":
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
case "
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
case "
|
|
641
|
-
case "
|
|
642
|
-
case "
|
|
881
|
+
case "changed-files":
|
|
882
|
+
if (!isStringArray(report.changedFiles)) return "failed";
|
|
883
|
+
return report.changedFiles.length === 0 ? "not-applicable" : "passed";
|
|
884
|
+
case "tests-added":
|
|
885
|
+
if (!isStringArray(report.testsAddedOrUpdated)) return "failed";
|
|
886
|
+
return report.testsAddedOrUpdated.length === 0 ? "not-applicable" : "passed";
|
|
887
|
+
case "commands-run": return Array.isArray(report.commandsRun) && report.commandsRun.length > 0 ? "passed" : "failed";
|
|
888
|
+
case "validation-output": return isStringArray(report.validationOutput) && report.validationOutput.length > 0 ? "passed" : "failed";
|
|
889
|
+
case "residual-risks": return isStringArray(report.residualRisks) ? "passed" : "failed";
|
|
890
|
+
case "no-staged-files": return report.noStagedFiles === true ? "passed" : "failed";
|
|
891
|
+
case "diff-summary": return typeof report.diffSummary === "string" && report.diffSummary.trim().length > 0 ? "passed" : "failed";
|
|
892
|
+
case "review-findings": return isStringArray(report.reviewFindings) ? "passed" : "failed";
|
|
893
|
+
case "manual-notes": return Boolean((report.manualNotes ?? report.notes)?.trim()) ? "passed" : "failed";
|
|
643
894
|
}
|
|
644
895
|
}
|
|
645
896
|
|
|
@@ -657,11 +908,15 @@ function checkNoStagedFiles(cwd: string): AcceptanceRuntimeCheck {
|
|
|
657
908
|
function runStructuralChecks(acceptance: ResolvedAcceptanceConfig, report: AcceptanceReport, cwd: string): AcceptanceRuntimeCheck[] {
|
|
658
909
|
const checks: AcceptanceRuntimeCheck[] = [];
|
|
659
910
|
for (const kind of acceptance.evidence) {
|
|
660
|
-
const
|
|
911
|
+
const status = reportEvidenceStatus(report, kind);
|
|
661
912
|
checks.push({
|
|
662
913
|
id: `evidence:${kind}`,
|
|
663
|
-
status
|
|
664
|
-
message:
|
|
914
|
+
status,
|
|
915
|
+
message: status === "passed"
|
|
916
|
+
? `${kind} evidence present.`
|
|
917
|
+
: status === "not-applicable"
|
|
918
|
+
? `${kind} evidence explicitly reported as not applicable.`
|
|
919
|
+
: `${kind} evidence missing from child report.`,
|
|
665
920
|
});
|
|
666
921
|
}
|
|
667
922
|
if (acceptance.evidence.includes("no-staged-files")) checks.push(checkNoStagedFiles(cwd));
|
|
@@ -788,14 +1043,24 @@ export async function evaluateAcceptance(input: {
|
|
|
788
1043
|
acceptance: ResolvedAcceptanceConfig;
|
|
789
1044
|
output: string;
|
|
790
1045
|
cwd: string;
|
|
1046
|
+
/**
|
|
1047
|
+
* Content the child sent to its configured output file (from its own write
|
|
1048
|
+
* tool calls, not from disk, so a concurrent writer to the same path cannot
|
|
1049
|
+
* be misattributed). Searched for the acceptance report; searched before
|
|
1050
|
+
* the assistant output when `authoritative` (outputMode "file-only").
|
|
1051
|
+
*/
|
|
1052
|
+
fileOutput?: { content: string; path: string; authoritative?: boolean };
|
|
791
1053
|
report?: AcceptanceReport;
|
|
792
1054
|
reviewResult?: AcceptanceReviewResult;
|
|
793
1055
|
signal?: AbortSignal;
|
|
794
1056
|
abortMessage?: string;
|
|
1057
|
+
reportOptional?: boolean;
|
|
795
1058
|
}): Promise<AcceptanceLedger> {
|
|
796
1059
|
const acceptance = input.acceptance;
|
|
1060
|
+
const initialStatus = acceptance.level === "none" ? "not-required" : "claimed";
|
|
797
1061
|
const ledger: AcceptanceLedger = {
|
|
798
|
-
status:
|
|
1062
|
+
status: initialStatus,
|
|
1063
|
+
evidenceStatus: initialStatus,
|
|
799
1064
|
explicit: acceptance.explicit,
|
|
800
1065
|
effectiveAcceptance: acceptance,
|
|
801
1066
|
inferredReason: acceptance.inferredReason,
|
|
@@ -805,33 +1070,48 @@ export async function evaluateAcceptance(input: {
|
|
|
805
1070
|
};
|
|
806
1071
|
if (acceptance.level === "none") return ledger;
|
|
807
1072
|
|
|
808
|
-
const parsed = input.report
|
|
1073
|
+
const parsed = input.report
|
|
1074
|
+
? (() => {
|
|
1075
|
+
const validation = validateAcceptanceReport(input.report);
|
|
1076
|
+
return validation.report
|
|
1077
|
+
? { report: validation.report }
|
|
1078
|
+
: { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}` };
|
|
1079
|
+
})()
|
|
1080
|
+
: parseAcceptanceReportSources(input.output, input.fileOutput);
|
|
1081
|
+
const needsReport = acceptanceRequiresChildReport(acceptance);
|
|
809
1082
|
if (parsed.report) {
|
|
810
1083
|
ledger.childReport = parsed.report;
|
|
811
1084
|
ledger.status = "attested";
|
|
812
|
-
|
|
1085
|
+
ledger.evidenceStatus = "attested";
|
|
1086
|
+
} else if (!input.reportOptional || needsReport || parsed.error !== ACCEPTANCE_REPORT_NOT_FOUND) {
|
|
813
1087
|
ledger.childReportParseError = parsed.error;
|
|
814
1088
|
ledger.runtimeChecks.push({ id: "attestation", status: "failed", message: parsed.error ?? "Structured acceptance report missing." });
|
|
815
|
-
|
|
816
|
-
|
|
1089
|
+
if (!input.reportOptional) {
|
|
1090
|
+
ledger.status = "rejected";
|
|
1091
|
+
ledger.evidenceStatus = "rejected";
|
|
1092
|
+
return ledger;
|
|
1093
|
+
}
|
|
1094
|
+
} else {
|
|
1095
|
+
ledger.childReportParseError = parsed.error;
|
|
817
1096
|
}
|
|
818
1097
|
|
|
819
|
-
if (LEVEL_RANK[acceptance.level] >= LEVEL_RANK.checked) {
|
|
1098
|
+
if (parsed.report && LEVEL_RANK[acceptance.level] >= LEVEL_RANK.checked) {
|
|
820
1099
|
ledger.runtimeChecks = [
|
|
1100
|
+
...ledger.runtimeChecks,
|
|
821
1101
|
...checkCriteriaSatisfied(acceptance.criteria, parsed.report),
|
|
822
1102
|
...runStructuralChecks(acceptance, parsed.report, input.cwd),
|
|
823
1103
|
];
|
|
824
|
-
if (ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
825
|
-
ledger.status = "
|
|
826
|
-
|
|
1104
|
+
if (!ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
1105
|
+
ledger.status = "checked";
|
|
1106
|
+
ledger.evidenceStatus = "checked";
|
|
827
1107
|
}
|
|
828
|
-
ledger.status = "checked";
|
|
829
1108
|
}
|
|
830
1109
|
|
|
831
1110
|
if (LEVEL_RANK[acceptance.level] >= LEVEL_RANK.verified && (acceptance.level === "verified" || acceptance.verify.length > 0)) {
|
|
832
1111
|
if (acceptance.level === "verified" && acceptance.verify.length === 0) {
|
|
833
1112
|
ledger.runtimeChecks.push({ id: "verification-config", status: "failed", message: "verified acceptance requires runtime verify commands." });
|
|
834
1113
|
ledger.status = "rejected";
|
|
1114
|
+
ledger.evidenceStatus = "rejected";
|
|
835
1115
|
return ledger;
|
|
836
1116
|
}
|
|
837
1117
|
ledger.verifyRuns = [];
|
|
@@ -841,39 +1121,70 @@ export async function evaluateAcceptance(input: {
|
|
|
841
1121
|
}
|
|
842
1122
|
if (ledger.verifyRuns.some((run) => run.status === "failed" || run.status === "timed-out")) {
|
|
843
1123
|
ledger.status = "rejected";
|
|
1124
|
+
ledger.evidenceStatus = "rejected";
|
|
844
1125
|
return ledger;
|
|
845
1126
|
}
|
|
846
|
-
ledger.status
|
|
1127
|
+
if (!ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
1128
|
+
ledger.status = "verified";
|
|
1129
|
+
ledger.evidenceStatus = "verified";
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
1134
|
+
ledger.status = "rejected";
|
|
1135
|
+
ledger.evidenceStatus = "rejected";
|
|
1136
|
+
return ledger;
|
|
1137
|
+
}
|
|
1138
|
+
if (ledger.status === "claimed") {
|
|
1139
|
+
ledger.status = acceptance.level === "verified" ? "verified" : acceptance.level;
|
|
1140
|
+
ledger.evidenceStatus = ledger.status;
|
|
847
1141
|
}
|
|
848
1142
|
|
|
849
|
-
if (acceptance.
|
|
850
|
-
if (input.reviewResult) {
|
|
1143
|
+
if (acceptance.review && acceptance.review !== false) {
|
|
1144
|
+
if (input.reviewResult?.status === "reviewed") {
|
|
851
1145
|
ledger.reviewResult = input.reviewResult;
|
|
852
|
-
ledger.status =
|
|
853
|
-
} else {
|
|
854
|
-
|
|
855
|
-
ledger.
|
|
856
|
-
|
|
1146
|
+
ledger.status = "reviewed";
|
|
1147
|
+
} else if (input.reviewResult?.status === "blockers") {
|
|
1148
|
+
ledger.reviewResult = input.reviewResult;
|
|
1149
|
+
ledger.status = "rejected";
|
|
1150
|
+
} else if (acceptance.review.required !== false) {
|
|
1151
|
+
ledger.reviewResult = input.reviewResult ?? {
|
|
1152
|
+
status: "review-required",
|
|
857
1153
|
findings: [{
|
|
858
|
-
severity:
|
|
859
|
-
issue: "
|
|
1154
|
+
severity: "non-blocking",
|
|
1155
|
+
issue: "Independent review has not been supplied.",
|
|
860
1156
|
rationale: "The run cannot be marked reviewed from child evidence alone.",
|
|
861
1157
|
}],
|
|
862
1158
|
};
|
|
863
|
-
|
|
1159
|
+
ledger.status = "review-required";
|
|
864
1160
|
}
|
|
865
1161
|
}
|
|
866
1162
|
|
|
867
1163
|
return ledger;
|
|
868
1164
|
}
|
|
869
1165
|
|
|
1166
|
+
export function buildSkippedAcceptanceLedger(acceptance: ResolvedAcceptanceConfig, input: { id: string; message: string }): AcceptanceLedger {
|
|
1167
|
+
const status = acceptance.level === "none" ? "not-required" : "rejected";
|
|
1168
|
+
return {
|
|
1169
|
+
status,
|
|
1170
|
+
evidenceStatus: status,
|
|
1171
|
+
explicit: acceptance.explicit,
|
|
1172
|
+
effectiveAcceptance: acceptance,
|
|
1173
|
+
inferredReason: acceptance.inferredReason,
|
|
1174
|
+
criteria: acceptance.criteria,
|
|
1175
|
+
runtimeChecks: acceptance.level === "none"
|
|
1176
|
+
? []
|
|
1177
|
+
: [{ id: input.id, status: "failed", message: input.message }],
|
|
1178
|
+
verifyRuns: [],
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
|
|
870
1182
|
export function acceptanceFailureMessage(ledger: AcceptanceLedger): string | undefined {
|
|
871
1183
|
if (ledger.status !== "rejected") return undefined;
|
|
872
1184
|
const failedCheck = ledger.runtimeChecks.find((check) => check.status === "failed");
|
|
873
1185
|
if (failedCheck) return `Acceptance rejected: ${failedCheck.message}`;
|
|
874
1186
|
const failedVerify = ledger.verifyRuns.find((run) => run.status === "failed" || run.status === "timed-out");
|
|
875
1187
|
if (failedVerify) return `Acceptance verification '${failedVerify.id}' ${failedVerify.status}.`;
|
|
876
|
-
if (ledger.reviewResult?.status === "needs-parent-decision") return "Acceptance review required but no automatic reviewer result is available.";
|
|
877
1188
|
if (ledger.reviewResult?.status === "blockers") return "Acceptance review found blockers.";
|
|
878
1189
|
return "Acceptance rejected.";
|
|
879
1190
|
}
|