@piagent/platform 1.6.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.md +17 -14
- package/README.vi.md +26 -1
- package/benchmarks/production-v1/spend-control.v1.json +27 -3
- package/benchmarks/production-v1/suite.json +5 -1
- package/catalog/capabilities.json +16 -16
- package/docs/authority-manifest.md +1 -1
- package/docs/auto-delegation-policy.md +38 -73
- package/docs/command-reference-vietnamese.md +23 -31
- package/docs/context-engine.md +5 -3
- package/docs/context-window-policy.md +37 -1
- package/docs/distribution-standard.md +7 -7
- package/docs/en/architecture.md +26 -13
- package/docs/languages.json +4 -4
- package/docs/operator-manual-vietnamese.md +21 -41
- package/docs/publishing-for-teams.md +5 -0
- package/docs/quality-benchmark.md +179 -23
- package/docs/release-install-policy.md +6 -6
- package/docs/runtime-harness-standard.md +3 -3
- package/docs/security-threat-model.md +1 -1
- package/docs/subagent-orchestration-capabilities.md +19 -51
- package/docs/subagents-and-multiagent.md +21 -47
- package/docs/task-implementation-contract.md +3 -3
- package/docs/task-lifecycle-vietnamese.md +1 -1
- package/docs/team-onboarding.md +2 -2
- package/docs/vi/architecture.md +26 -13
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +89 -95
- package/docs-site/commands.html +1 -1
- package/docs-site/content/benchmark.html +88 -94
- package/docs-site/content/en/benchmark.html +134 -90
- package/docs-site/content/en/index.html +19 -19
- package/docs-site/content/en/runtime.html +4 -4
- package/docs-site/content/en/whats-new.html +12 -1
- package/docs-site/content/en/workflows.html +2 -2
- package/docs-site/content/index.html +19 -19
- package/docs-site/content/runtime.html +4 -4
- package/docs-site/content/whats-new.html +12 -1
- package/docs-site/content/workflows.html +2 -2
- package/docs-site/context-engine.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +135 -91
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/index.html +20 -20
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +5 -5
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/whats-new.html +13 -2
- package/docs-site/en/workflows.html +3 -3
- package/docs-site/index.html +20 -20
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +5 -5
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/whats-new.html +13 -2
- package/docs-site/workflows.html +3 -3
- package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
- package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
- package/evals/fixtures/codex-relative-efficiency-v1.invalid.json +4 -0
- package/evals/fixtures/codex-relative-efficiency-v1.valid.json +94 -0
- package/evals/fixtures/helper-request.invalid.json +1 -1
- package/evals/fixtures/helper-request.valid.json +1 -1
- package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +7 -0
- package/evals/fixtures/task-authority-snapshot.valid.json +4 -4
- package/evals/fs4-readiness-matrix.v1.json +2 -2
- package/evals/fs5-causal-arm.v1.json +2 -2
- package/evals/fs5-pilot-protocol.v1.json +3 -3
- package/evals/fs5-pilot-protocol.v2.json +8 -8
- package/evals/fs5-pilot-protocol.v3.json +8 -8
- package/evals/fs5-pilot-protocol.v4.json +6 -6
- package/evals/fs5-pilot-protocol.v5.json +7 -6
- package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
- package/evals/runtime-conformance-v1/README.md +43 -0
- package/evals/runtime-conformance-v1/evaluate-paid-canary.mjs +198 -0
- package/evals/runtime-conformance-v1/lane.json +57 -0
- package/evals/runtime-conformance-v1/paid-canary.json +31 -0
- package/evals/runtime-conformance-v1/runner.mjs +456 -0
- package/package.json +1 -1
- package/packages/piagent-core/README.md +13 -4
- package/packages/piagent-core/benchmark/benchmark-codex-relative-efficiency.js +698 -0
- package/packages/piagent-core/benchmark/benchmark-core.js +4 -3
- package/packages/piagent-core/benchmark/benchmark-forensics.js +4 -2
- package/packages/piagent-core/benchmark/benchmark-preflight.js +3 -1
- package/packages/piagent-core/benchmark/benchmark-provider-free-evidence.js +345 -0
- package/packages/piagent-core/benchmark/benchmark-record-validation.js +74 -5
- package/packages/piagent-core/benchmark/benchmark-report.js +15 -1
- package/packages/piagent-core/benchmark/benchmark-runtime-causal.js +244 -0
- package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +201 -4
- package/packages/piagent-core/benchmark/benchmark-suite.js +22 -1
- package/packages/piagent-core/benchmark/benchmark-summary-support.js +4 -0
- package/packages/piagent-core/benchmark/benchmark-usage.js +15 -4
- package/packages/piagent-core/capabilities/runtime-integrity.js +3 -0
- package/packages/piagent-core/extensions/acceptance-behavior-proof.js +806 -0
- package/packages/piagent-core/extensions/acceptance-receipt.js +89 -48
- package/packages/piagent-core/extensions/context-engine.js +1 -11
- package/packages/piagent-core/extensions/context-index-policy.js +4 -0
- package/packages/piagent-core/extensions/context-query-paths.js +57 -0
- package/packages/piagent-core/extensions/criterion-context-pack.js +16 -3
- package/packages/piagent-core/extensions/criterion-graph.js +4 -1
- package/packages/piagent-core/extensions/fallback-base-policy.ts +101 -0
- package/packages/piagent-core/extensions/guard-types.ts +4 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +194 -226
- package/packages/piagent-core/extensions/task-lifecycle.js +32 -0
- package/packages/piagent-core/extensions/task-state.js +69 -8
- package/packages/piagent-core/extensions/verification-intelligence.js +49 -12
- package/packages/piagent-core/extensions/workspace-evidence-roots.js +52 -16
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/policies/base-policy.json +42 -2
- package/packages/piagent-core/policy/authority-manifest.v1.json +3 -3
- package/packages/piagent-core/prompts/be-to-fe.md +2 -8
- package/packages/piagent-core/prompts/plan.md +1 -1
- package/packages/piagent-core/prompts/platform-improve.md +2 -6
- package/packages/piagent-core/prompts/review.md +2 -7
- package/packages/piagent-core/prompts/scout.md +1 -1
- package/packages/piagent-core/prompts/task.md +4 -3
- package/packages/piagent-core/runtime/hooks/completion-hook.ts +7 -6
- package/packages/piagent-core/runtime/hooks/input-hook.ts +6 -0
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +100 -1
- package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +58 -72
- package/packages/piagent-core/runtime/hooks/tool-result-value-helpers.ts +66 -0
- package/packages/piagent-core/runtime/inspection/bounded-emission-guard.ts +286 -0
- package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +37 -7
- package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +27 -4
- package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +20 -3
- package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +47 -11
- package/packages/piagent-core/runtime/orchestration/helper-lifecycle.ts +16 -12
- package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +71 -0
- package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +28 -20
- package/packages/piagent-core/runtime/orchestration/role-policy.ts +27 -9
- package/packages/piagent-core/runtime/orchestration/subagent-tool-policy.ts +329 -0
- package/packages/piagent-core/runtime/policy/block-remedy.ts +0 -4
- package/packages/piagent-core/runtime/product/efficiency-metrics.ts +3 -0
- package/packages/piagent-core/runtime/quality/edit-freshness-guard.ts +102 -0
- package/packages/piagent-core/runtime/quality/exact-output-contract.ts +20 -10
- package/packages/piagent-core/runtime/quality/performance-assurance.ts +3 -3
- package/packages/piagent-core/runtime/recovery/continuation-budget.ts +1 -1
- package/packages/piagent-core/runtime/recovery/handoff-projection.ts +4 -1
- package/packages/piagent-core/runtime/recovery/pristine-task-policy-refresh.ts +91 -0
- package/packages/piagent-core/runtime/recovery/recovery-policy.ts +7 -4
- package/packages/piagent-core/runtime/recovery/resume-state.ts +112 -14
- package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +7 -13
- package/packages/piagent-core/runtime/recovery/task-evidence-coverage-refresh.ts +217 -0
- package/packages/piagent-core/runtime/registration/context-commands.ts +16 -2
- package/packages/piagent-core/runtime/registration/knowledge-tools.ts +9 -5
- package/packages/piagent-core/runtime/registration/operator-request-intake.ts +49 -0
- package/packages/piagent-core/runtime/registration/policy-tools.ts +4 -4
- package/packages/piagent-core/runtime/registration/session-commands.ts +1 -1
- package/packages/piagent-core/runtime/registration/task-completion-tools.ts +12 -9
- package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +21 -12
- package/packages/piagent-core/runtime/registration/task-start-guidance.ts +19 -13
- package/packages/piagent-core/runtime/registration/task-start-manifest.ts +54 -0
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +57 -57
- package/packages/piagent-core/runtime/runtime-limits.ts +16 -2
- package/packages/piagent-core/runtime/session/adaptive-context-analysis.ts +461 -0
- package/packages/piagent-core/runtime/session/adaptive-context-governor.ts +464 -0
- package/packages/piagent-core/runtime/session/adaptive-context-ledger.ts +265 -0
- package/packages/piagent-core/runtime/session/message-signals.ts +7 -3
- package/packages/piagent-core/runtime/session/operator-request-carry.ts +44 -0
- package/packages/piagent-core/runtime/session/runtime-state.ts +11 -7
- package/packages/piagent-core/runtime/session/source-checkout-read-grants.ts +35 -0
- package/packages/piagent-core/runtime/session/system-prompt.ts +76 -14
- package/packages/piagent-core/runtime/session/task-contract-presentation.ts +39 -0
- package/packages/piagent-core/runtime/tools/phase-tools.ts +8 -8
- package/packages/piagent-core/runtime/tools/tool-groups.ts +12 -2
- package/packages/piagent-core/runtime/verification/exact-verifier-reuse.ts +101 -0
- package/packages/piagent-core/runtime/workflows/runtime-intake-compaction.ts +164 -0
- package/packages/piagent-core/runtime/workflows/task-intake.ts +167 -39
- package/packages/piagent-core/subagents/piagent-oracle.md +5 -3
- package/packages/piagent-core/subagents/piagent-planner.md +6 -4
- package/packages/piagent-core/subagents/piagent-reviewer.md +5 -3
- package/packages/piagent-core/subagents/piagent-scout.md +4 -2
- package/packages/piagent-core/subagents/piagent-worker.md +6 -4
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-CxeZqTte.js → MarkdownMessage-B4XehTCU.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{index-DynIMe8I.js → index-CgOEyRPG.js} +4 -4
- package/packages/piagent-webui/dist/client/index.html +1 -1
- package/packages/piagent-webui/extension/piagent-webui.ts +3 -1
- package/packages/piagent-webui/extension/same-session-bridge.ts +1 -1
- package/packages/piagent-webui/extension/session-stream-adapter.ts +33 -10
- package/packages/piagent-webui/gateway/gateway-session-stream.ts +37 -21
- package/packages/piagent-webui/gateway/session-operation-lifecycle.ts +237 -0
- package/packages/piagent-webui/gateway/session-operation-retry-control.ts +38 -0
- package/packages/piagent-webui/gateway/session-operation-watchdog.ts +9 -2
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +8 -7
- package/packages/piagent-webui/ownership/session-lease-store.ts +5 -2
- package/packages/piagent-webui/server/core-inspection-provider.ts +2 -1
- package/packages/piagent-webui/server/transcript-projection.ts +91 -5
- package/schemas/codex-relative-efficiency-v1.schema.json +155 -0
- package/schemas/piagent-webui/runtime-event-v2.schema.json +15 -5
- package/schemas/piagent-webui/subagent-tree-v1.schema.json +11 -1
- package/schemas/project-profile.schema.json +1 -1
- package/schemas/task-contract.schema.json +15 -0
- package/scripts/benchmark-runner-core.mjs +14 -6
- package/scripts/benchmark-runner-finalization.mjs +4 -0
- package/scripts/configure-subagents.sh +42 -34
- package/scripts/helper-evaluation.mjs +1 -1
- package/scripts/team-doctor.sh +1 -1
- package/scripts/verify-local.sh +5 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { TaskContract } from "../../extensions/guard-types.js";
|
|
5
|
+
import { taskAuthorityMode } from "../policy/task-authority-runtime.ts";
|
|
6
|
+
import { MINIMAL_DELEGATION_LIMITS } from "./minimal-delegation-policy.ts";
|
|
7
|
+
|
|
8
|
+
const SUBAGENT_TOOL = /(?:^|[_-])subagents?(?:[_-]|$)/i;
|
|
9
|
+
const NON_DISPATCH_ACTIONS = new Set(["cancel", "kill", "list", "status", "stop", "wait"]);
|
|
10
|
+
const READ_ONLY_AGENTS = new Set([
|
|
11
|
+
"piagent-oracle",
|
|
12
|
+
"piagent-planner",
|
|
13
|
+
"piagent-reviewer",
|
|
14
|
+
"piagent-scout"
|
|
15
|
+
]);
|
|
16
|
+
const URL = /\bhttps?:\/\/[^\s<>)\]}]+/i;
|
|
17
|
+
const PROVIDED_SOURCE = /\b(?:provided|supplied|attached|pasted)\s+(?:evidence|source|text)|\busing only (?:the )?(?:evidence|source|text) below|\bdo not (?:fetch|open|retrieve)|\bn[oộ]i dung (?:đ[aã] )?(?:cung c[aấ]p|d[aá]n|đ[ií]nh k[eè]m)/i;
|
|
18
|
+
|
|
19
|
+
type PlainRecord = Record<string, unknown>;
|
|
20
|
+
|
|
21
|
+
export type DirectSubagentDispatchPreflight = {
|
|
22
|
+
applicable: boolean;
|
|
23
|
+
dispatch: boolean;
|
|
24
|
+
allowed: boolean;
|
|
25
|
+
reasonCode: string | null;
|
|
26
|
+
reason: string | null;
|
|
27
|
+
role: string | null;
|
|
28
|
+
requestCount: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type DirectSubagentResult = {
|
|
32
|
+
applicable: boolean;
|
|
33
|
+
spawned: boolean;
|
|
34
|
+
failed: boolean;
|
|
35
|
+
reasonCode: string | null;
|
|
36
|
+
disposition: string;
|
|
37
|
+
role: "retriever" | "scout" | "planner" | "worker" | "reviewer" | "oracle" | "researcher";
|
|
38
|
+
requestRef: string;
|
|
39
|
+
outputDigest: string | null;
|
|
40
|
+
calls: number;
|
|
41
|
+
tokens: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type DirectSubagentTaskPersistence = {
|
|
45
|
+
now: () => string;
|
|
46
|
+
persist: (task: TaskContract) => TaskContract;
|
|
47
|
+
trace: (payload: Record<string, unknown>) => void;
|
|
48
|
+
sessionTrace?: (payload: Record<string, unknown>) => void;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function taskHelperUsageMode(task: TaskContract): "off" | "recommend" | "on" {
|
|
52
|
+
const boundMode = taskAuthorityMode(task, "CAP-14");
|
|
53
|
+
return boundMode === "on" || boundMode === "recommend" ? boundMode : "off";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function reserveDirectSubagentDispatch(
|
|
57
|
+
task: TaskContract | undefined,
|
|
58
|
+
toolCallId: string,
|
|
59
|
+
persistence: DirectSubagentTaskPersistence
|
|
60
|
+
): TaskContract | undefined {
|
|
61
|
+
if (!task || task.trace.outcome !== "pending" || task.orchestration?.subagents !== "not-used") return task;
|
|
62
|
+
task.orchestration = {
|
|
63
|
+
...(task.orchestration ?? {
|
|
64
|
+
mode: "solo-first" as const,
|
|
65
|
+
subagents: "not-used" as const,
|
|
66
|
+
reason: "Parent works directly by default."
|
|
67
|
+
}),
|
|
68
|
+
subagents: "optional",
|
|
69
|
+
reason: "One bounded read-only helper dispatch is authorized and awaiting a durable result."
|
|
70
|
+
};
|
|
71
|
+
task.updatedAt = persistence.now();
|
|
72
|
+
const written = persistence.persist(task);
|
|
73
|
+
persistence.trace({
|
|
74
|
+
event: "helper_dispatch_reserved",
|
|
75
|
+
taskId: written.taskId,
|
|
76
|
+
taskRunId: written.taskRunId,
|
|
77
|
+
sessionId: written.sessionId,
|
|
78
|
+
toolCallId
|
|
79
|
+
});
|
|
80
|
+
return written;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function recordDirectSubagentResult(
|
|
84
|
+
task: TaskContract | undefined,
|
|
85
|
+
event: { toolCallId?: string; toolName: string },
|
|
86
|
+
result: DirectSubagentResult,
|
|
87
|
+
persistence: DirectSubagentTaskPersistence
|
|
88
|
+
): TaskContract | undefined {
|
|
89
|
+
if (!task || task.trace.outcome !== "pending") return task;
|
|
90
|
+
const existingUsage = task.acceptanceReceipt?.helperUsage;
|
|
91
|
+
const helper = {
|
|
92
|
+
role: result.role,
|
|
93
|
+
disposition: result.disposition,
|
|
94
|
+
requestRef: result.requestRef,
|
|
95
|
+
outputDigest: result.outputDigest,
|
|
96
|
+
calls: result.calls,
|
|
97
|
+
tokens: result.tokens
|
|
98
|
+
};
|
|
99
|
+
const helpers = result.spawned
|
|
100
|
+
? [...(existingUsage?.helpers ?? []).filter((item) => item.requestRef !== result.requestRef), helper].slice(-3)
|
|
101
|
+
: existingUsage?.helpers ?? [];
|
|
102
|
+
const used = helpers.length > 0 || existingUsage?.used === true;
|
|
103
|
+
const reasonCodes = [...new Set([
|
|
104
|
+
...(existingUsage?.reasonCodes ?? []),
|
|
105
|
+
result.reasonCode ?? (result.spawned ? "helper-result-accepted" : "helper-dispatch-rejected")
|
|
106
|
+
])].slice(-16);
|
|
107
|
+
if (task.acceptanceReceipt) {
|
|
108
|
+
task.acceptanceReceipt.helperUsage = {
|
|
109
|
+
mode: taskHelperUsageMode(task),
|
|
110
|
+
used,
|
|
111
|
+
decision: result.spawned ? "dispatch" : "skip",
|
|
112
|
+
projectedSavingsRatio: existingUsage?.projectedSavingsRatio ?? null,
|
|
113
|
+
reasonCodes,
|
|
114
|
+
helpers,
|
|
115
|
+
recordedAt: persistence.now()
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (task.orchestration) {
|
|
119
|
+
task.orchestration.subagents = used ? "used" : "not-used";
|
|
120
|
+
task.orchestration.reason = result.spawned
|
|
121
|
+
? `A bounded read-only helper ran with disposition ${result.disposition}; its digest-only usage is recorded in the acceptance receipt.`
|
|
122
|
+
: `Helper dispatch did not start (${result.reasonCode ?? "helper-dispatch-rejected"}); the parent remains the sole execution owner.`;
|
|
123
|
+
}
|
|
124
|
+
task.updatedAt = persistence.now();
|
|
125
|
+
const written = persistence.persist(task);
|
|
126
|
+
const trace = {
|
|
127
|
+
event: "helper_result_recorded",
|
|
128
|
+
taskId: written.taskId,
|
|
129
|
+
taskRunId: written.taskRunId,
|
|
130
|
+
sessionId: written.sessionId,
|
|
131
|
+
toolCallId: event.toolCallId,
|
|
132
|
+
toolName: event.toolName,
|
|
133
|
+
spawned: result.spawned,
|
|
134
|
+
disposition: result.disposition,
|
|
135
|
+
reasonCode: result.reasonCode,
|
|
136
|
+
calls: result.calls,
|
|
137
|
+
tokens: result.tokens
|
|
138
|
+
};
|
|
139
|
+
persistence.trace(trace);
|
|
140
|
+
persistence.sessionTrace?.(trace);
|
|
141
|
+
return written;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function record(value: unknown): PlainRecord {
|
|
145
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as PlainRecord : {};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function finiteInteger(value: unknown, fallback = 0): number {
|
|
149
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
150
|
+
? Math.max(0, Math.trunc(value))
|
|
151
|
+
: fallback;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function helperRole(value: unknown): DirectSubagentResult["role"] {
|
|
155
|
+
const normalized = String(value ?? "").trim().toLowerCase().replace(/^piagent-/, "");
|
|
156
|
+
return ["retriever", "scout", "planner", "worker", "reviewer", "oracle", "researcher"].includes(normalized)
|
|
157
|
+
? normalized as DirectSubagentResult["role"]
|
|
158
|
+
: "scout";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function dispatchRequests(input: PlainRecord): PlainRecord[] {
|
|
162
|
+
if (Array.isArray(input.tasks)) {
|
|
163
|
+
return input.tasks.filter((item): item is PlainRecord => Boolean(item && typeof item === "object" && !Array.isArray(item)))
|
|
164
|
+
.map((item) => ({ ...input, ...item, tasks: undefined }));
|
|
165
|
+
}
|
|
166
|
+
return typeof input.agent === "string" && input.agent.trim() ? [input] : [];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function boundedNumber(input: PlainRecord, nestedField: string, field: string): number | undefined {
|
|
170
|
+
const nested = record(input[nestedField]);
|
|
171
|
+
const value = nested[field];
|
|
172
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function sameProjectRoot(currentCwd: string, requested: unknown): boolean {
|
|
176
|
+
if (typeof requested !== "string" || !requested.trim()) return true;
|
|
177
|
+
return path.resolve(currentCwd) === path.resolve(currentCwd, requested);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function blocked(reasonCode: string, reason: string, role: string | null, requestCount: number): DirectSubagentDispatchPreflight {
|
|
181
|
+
return { applicable: true, dispatch: true, allowed: false, reasonCode, reason, role, requestCount };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The optional pi-subagents tool is a second execution backend. It must not be
|
|
186
|
+
* able to bypass the task-bound CAP-14 snapshot merely because the model knows
|
|
187
|
+
* the tool name. This preflight is deliberately conservative: one fresh,
|
|
188
|
+
* read-only, locally capable helper, with the same hard ceilings as Piagent's
|
|
189
|
+
* owned helper runtime.
|
|
190
|
+
*/
|
|
191
|
+
export function evaluateDirectSubagentDispatch(input: {
|
|
192
|
+
toolName: string;
|
|
193
|
+
toolInput: unknown;
|
|
194
|
+
cwd: string;
|
|
195
|
+
helpersMode: "off" | "recommend" | "on";
|
|
196
|
+
taskPending: boolean;
|
|
197
|
+
dispatchAuthority: boolean;
|
|
198
|
+
durableSubagentState?: "not-used" | "optional" | "used";
|
|
199
|
+
}): DirectSubagentDispatchPreflight {
|
|
200
|
+
if (!SUBAGENT_TOOL.test(input.toolName)) {
|
|
201
|
+
return { applicable: false, dispatch: false, allowed: true, reasonCode: null, reason: null, role: null, requestCount: 0 };
|
|
202
|
+
}
|
|
203
|
+
const toolInput = record(input.toolInput);
|
|
204
|
+
const requests = dispatchRequests(toolInput);
|
|
205
|
+
const action = String(toolInput.action ?? "").trim().toLowerCase();
|
|
206
|
+
if (requests.length === 0 && NON_DISPATCH_ACTIONS.has(action)) {
|
|
207
|
+
return { applicable: true, dispatch: false, allowed: true, reasonCode: null, reason: null, role: null, requestCount: 0 };
|
|
208
|
+
}
|
|
209
|
+
if (requests.length === 0) return blocked("helper-dispatch-shape-invalid", "Subagent dispatch requires exactly one named read-only helper.", null, 0);
|
|
210
|
+
const role = String(requests[0].agent ?? "").trim();
|
|
211
|
+
if (requests.length !== 1) return blocked("helper-count-exceeds-one", `Piagent permits at most one helper for the entire task; ${requests.length} were requested.`, role || null, requests.length);
|
|
212
|
+
if (!input.taskPending) return blocked("helper-task-authority-missing", "A pending session-bound task is required before helper dispatch.", role || null, 1);
|
|
213
|
+
if (input.helpersMode !== "on" || !input.dispatchAuthority) {
|
|
214
|
+
return blocked("helper-dispatch-authority-disabled", "Task-bound helper authority is not on; continue in the parent model.", role || null, 1);
|
|
215
|
+
}
|
|
216
|
+
if (input.durableSubagentState === "optional" || input.durableSubagentState === "used") {
|
|
217
|
+
return blocked("helper-retry-forbidden", "This task already reserved or used its one helper; helper retries are disabled.", role || null, 1);
|
|
218
|
+
}
|
|
219
|
+
if (role === "piagent-worker" || !READ_ONLY_AGENTS.has(role)) {
|
|
220
|
+
return blocked("automatic-helper-role-disabled", `Automatic helper ${role || "(unknown)"} is not an enabled read-only Piagent role.`, role || null, 1);
|
|
221
|
+
}
|
|
222
|
+
const request = requests[0];
|
|
223
|
+
if (request.context !== undefined && request.context !== "fresh") {
|
|
224
|
+
return blocked("helper-parent-history-forbidden", "Helpers must use fresh isolated context and cannot fork the parent transcript.", role, 1);
|
|
225
|
+
}
|
|
226
|
+
if (!sameProjectRoot(input.cwd, request.cwd)) {
|
|
227
|
+
return blocked("helper-cwd-outside-project", "Helper cwd must remain the current project root.", role, 1);
|
|
228
|
+
}
|
|
229
|
+
const timeoutMs = typeof request.timeoutMs === "number" ? request.timeoutMs : undefined;
|
|
230
|
+
if (timeoutMs === undefined) {
|
|
231
|
+
return blocked("helper-time-budget-missing", "Helper dispatch requires an explicit timeoutMs no greater than 300 seconds.", role, 1);
|
|
232
|
+
}
|
|
233
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs > 300_000 || timeoutMs < 1) {
|
|
234
|
+
return blocked("helper-time-budget-exceeded", "Helper timeout must be between 1 ms and the 300 second Piagent ceiling.", role, 1);
|
|
235
|
+
}
|
|
236
|
+
const maxTurns = boundedNumber(request, "turnBudget", "maxTurns");
|
|
237
|
+
if (maxTurns === undefined) {
|
|
238
|
+
return blocked("helper-turn-budget-missing", "Helper dispatch requires an explicit turnBudget.maxTurns no greater than 8.", role, 1);
|
|
239
|
+
}
|
|
240
|
+
if (maxTurns < 1 || maxTurns > 8) {
|
|
241
|
+
return blocked("helper-turn-budget-exceeded", "Helper maxTurns exceeds the bounded 8-turn ceiling.", role, 1);
|
|
242
|
+
}
|
|
243
|
+
const hardCalls = boundedNumber(request, "toolBudget", "hard");
|
|
244
|
+
if (hardCalls === undefined) {
|
|
245
|
+
return blocked("helper-call-budget-missing", `Helper dispatch requires an explicit toolBudget.hard no greater than ${MINIMAL_DELEGATION_LIMITS.maxHelperCalls}.`, role, 1);
|
|
246
|
+
}
|
|
247
|
+
if (hardCalls < 1 || hardCalls > MINIMAL_DELEGATION_LIMITS.maxHelperCalls) {
|
|
248
|
+
return blocked("helper-call-budget-exceeded", `Helper hard tool budget exceeds the ${MINIMAL_DELEGATION_LIMITS.maxHelperCalls}-call ceiling.`, role, 1);
|
|
249
|
+
}
|
|
250
|
+
const objective = String(request.task ?? "").trim();
|
|
251
|
+
if (!objective) return blocked("helper-objective-missing", "Helper dispatch requires a bounded objective.", role, 1);
|
|
252
|
+
if (URL.test(objective) && !PROVIDED_SOURCE.test(objective)) {
|
|
253
|
+
return blocked("helper-source-access-unavailable", "The selected helper has local read tools only and cannot retrieve the remote URL; inspect it in the parent or provide a local checkout/evidence bundle.", role, 1);
|
|
254
|
+
}
|
|
255
|
+
return { applicable: true, dispatch: true, allowed: true, reasonCode: null, reason: null, role, requestCount: 1 };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function textContent(content: unknown): string {
|
|
259
|
+
if (typeof content === "string") return content;
|
|
260
|
+
if (!Array.isArray(content)) return "";
|
|
261
|
+
return content.filter((item) => item && typeof item === "object" && (item as { type?: unknown }).type === "text")
|
|
262
|
+
.map((item) => String((item as { text?: unknown }).text ?? "")).join("\n");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function childAcceptanceFailed(result: PlainRecord): boolean {
|
|
266
|
+
const acceptance = record(result.acceptance);
|
|
267
|
+
const report = record(acceptance.childReport);
|
|
268
|
+
const criteria = Array.isArray(report.criteriaSatisfied) ? report.criteriaSatisfied : [];
|
|
269
|
+
return criteria.some((item) => record(item).status === "not-satisfied")
|
|
270
|
+
|| ["failed", "rejected", "not-satisfied"].includes(String(acceptance.status ?? "").toLowerCase());
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function totalUsage(details: PlainRecord, results: PlainRecord[]): number {
|
|
274
|
+
const aggregate = record(details.totalChildUsage);
|
|
275
|
+
const sources = Object.keys(aggregate).length > 0 ? [aggregate] : results.map((result) => record(result.usage));
|
|
276
|
+
return Math.min(100_000_000, sources.reduce((total, usage) => total
|
|
277
|
+
+ finiteInteger(usage.input)
|
|
278
|
+
+ finiteInteger(usage.output)
|
|
279
|
+
+ finiteInteger(usage.cacheRead)
|
|
280
|
+
+ finiteInteger(usage.cacheWrite), 0));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Convert a pi-subagents provider result into fail-closed, digest-only truth. */
|
|
284
|
+
export function classifyDirectSubagentResult(input: {
|
|
285
|
+
toolName: string;
|
|
286
|
+
toolInput: unknown;
|
|
287
|
+
content: unknown;
|
|
288
|
+
details: unknown;
|
|
289
|
+
isError?: boolean;
|
|
290
|
+
}): DirectSubagentResult | undefined {
|
|
291
|
+
if (!SUBAGENT_TOOL.test(input.toolName)) return undefined;
|
|
292
|
+
const toolInput = record(input.toolInput);
|
|
293
|
+
if (dispatchRequests(toolInput).length === 0) return undefined;
|
|
294
|
+
const details = record(input.details);
|
|
295
|
+
const results = Array.isArray(details.results)
|
|
296
|
+
? details.results.filter((item): item is PlainRecord => Boolean(item && typeof item === "object" && !Array.isArray(item)))
|
|
297
|
+
: [];
|
|
298
|
+
const spawned = results.length > 0 || typeof details.runId === "string" && details.runId.length > 0;
|
|
299
|
+
const resultFailures = results.some((result) => finiteInteger(result.exitCode) !== 0 || childAcceptanceFailed(result));
|
|
300
|
+
const content = textContent(input.content);
|
|
301
|
+
const insufficientEvidence = results.some(childAcceptanceFailed)
|
|
302
|
+
|| /\bscope inspected:\s*none\b|\bno source (?:content|evidence) (?:was|could be) (?:accessible|collected)|\bcriteria?[^\n]{0,80}not-satisfied\b/i.test(content);
|
|
303
|
+
const failed = input.isError === true || !spawned || resultFailures || insufficientEvidence;
|
|
304
|
+
const reasonCode = input.isError === true
|
|
305
|
+
? "helper-tool-error"
|
|
306
|
+
: !spawned
|
|
307
|
+
? "helper-dispatch-rejected"
|
|
308
|
+
: insufficientEvidence
|
|
309
|
+
? "helper-insufficient-evidence"
|
|
310
|
+
: resultFailures
|
|
311
|
+
? "helper-run-failed"
|
|
312
|
+
: null;
|
|
313
|
+
const firstResult = results[0] ?? {};
|
|
314
|
+
const agent = firstResult.agent ?? dispatchRequests(toolInput)[0]?.agent;
|
|
315
|
+
const finalOutput = results.map((result) => String(result.finalOutput ?? "")).filter(Boolean).join("\n");
|
|
316
|
+
const calls = Math.min(100, results.reduce((total, result) => total + finiteInteger(result.toolCount), 0));
|
|
317
|
+
return {
|
|
318
|
+
applicable: true,
|
|
319
|
+
spawned,
|
|
320
|
+
failed,
|
|
321
|
+
reasonCode,
|
|
322
|
+
disposition: failed ? reasonCode ?? "failed" : "succeeded",
|
|
323
|
+
role: helperRole(agent),
|
|
324
|
+
requestRef: crypto.createHash("sha256").update(JSON.stringify(toolInput)).digest("hex"),
|
|
325
|
+
outputDigest: !failed && finalOutput ? crypto.createHash("sha256").update(finalOutput).digest("hex") : null,
|
|
326
|
+
calls,
|
|
327
|
+
tokens: totalUsage(details, results)
|
|
328
|
+
};
|
|
329
|
+
}
|
|
@@ -43,10 +43,6 @@ const REMEDIES: Array<{ match: RegExp; remedy: string }> = [
|
|
|
43
43
|
match: /outside resolved filesystem scope/i,
|
|
44
44
|
remedy: "Work inside the project, or widen the filesystem scope in the active profile and reapply it."
|
|
45
45
|
},
|
|
46
|
-
{
|
|
47
|
-
match: /cannot mutate paths outside its declared scope|semantic repair must stay inside its declared task scope/i,
|
|
48
|
-
remedy: "Start a new attempt with `piagent_task_start` listing this path in the scope; a running task cannot widen its own."
|
|
49
|
-
},
|
|
50
46
|
{
|
|
51
47
|
match: /Task Implementation Contract is required/i,
|
|
52
48
|
remedy: "Call `piagent_task_start` once with explicit project-relative scope, then retry."
|
|
@@ -112,6 +112,9 @@ export function buildTaskEfficiencyMetrics(
|
|
|
112
112
|
helpers: {
|
|
113
113
|
mode: helperUsage?.mode ?? "unknown",
|
|
114
114
|
used: helperUsage?.used ?? false,
|
|
115
|
+
decision: helperUsage?.decision ?? (helperUsage?.used ? "dispatch" : "skip"),
|
|
116
|
+
projectedSavingsRatio: helperUsage?.projectedSavingsRatio ?? null,
|
|
117
|
+
reasonCodes: helperUsage?.reasonCodes ?? ["parent-direct-default"],
|
|
115
118
|
entries: (helperUsage?.helpers ?? []).map((item) => ({
|
|
116
119
|
role: item.role,
|
|
117
120
|
model: null,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
import { currentFileContentDigests } from "./model-mutation-proof.ts";
|
|
5
|
+
|
|
6
|
+
export type EditFreshnessMode = "off" | "observe" | "enforce";
|
|
7
|
+
export type EditFreshnessSource = "read" | "mutation";
|
|
8
|
+
export type EditFreshnessEvaluation = {
|
|
9
|
+
decision: "off" | "unobserved" | "current" | "stale";
|
|
10
|
+
checkedPaths: string[];
|
|
11
|
+
stalePaths: string[];
|
|
12
|
+
enforce: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type SessionContext = Pick<ExtensionContext, "cwd" | "sessionManager">;
|
|
16
|
+
type Snapshot = { digest: string; taskRunId: string; source: EditFreshnessSource; recordedAt: string };
|
|
17
|
+
type SessionState = { identity: string; snapshots: Map<string, Snapshot> };
|
|
18
|
+
|
|
19
|
+
const MAX_SNAPSHOTS_PER_SESSION = 256;
|
|
20
|
+
|
|
21
|
+
function normalizeProjectPath(cwd: string, candidate: string): string | undefined {
|
|
22
|
+
if (!candidate || candidate.includes("\0")) return undefined;
|
|
23
|
+
const absolute = path.isAbsolute(candidate) ? path.resolve(candidate) : path.resolve(cwd, candidate);
|
|
24
|
+
const relative = path.relative(cwd, absolute).split(path.sep).join("/");
|
|
25
|
+
return relative && relative !== "." && relative !== ".." && !relative.startsWith("../") && !path.isAbsolute(relative)
|
|
26
|
+
? relative
|
|
27
|
+
: undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function editFreshnessModeFromEnvironment(value: unknown): EditFreshnessMode {
|
|
31
|
+
const normalized = String(value ?? "").trim().toLowerCase();
|
|
32
|
+
if (["0", "false", "no", "off", "disabled"].includes(normalized)) return "off";
|
|
33
|
+
if (["observe", "shadow", "advisory"].includes(normalized)) return "observe";
|
|
34
|
+
return "enforce";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Binds an edit to the complete file version last delivered by a successful
|
|
39
|
+
* read (or produced by a successful Piagent mutation). Files that have no
|
|
40
|
+
* observed snapshot retain the host tool's existing exact-anchor policy; the
|
|
41
|
+
* pilot only rejects proven drift, so it cannot invent a read requirement.
|
|
42
|
+
*/
|
|
43
|
+
export class EditFreshnessGuard {
|
|
44
|
+
readonly #mode: EditFreshnessMode;
|
|
45
|
+
readonly #sessions = new WeakMap<SessionContext["sessionManager"], SessionState>();
|
|
46
|
+
|
|
47
|
+
constructor(mode: EditFreshnessMode = "enforce") { this.#mode = mode; }
|
|
48
|
+
|
|
49
|
+
#identity(ctx: SessionContext): string {
|
|
50
|
+
return `${path.resolve(ctx.cwd)}\0${ctx.sessionManager.getSessionId()}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#session(ctx: SessionContext): SessionState {
|
|
54
|
+
const identity = this.#identity(ctx), existing = this.#sessions.get(ctx.sessionManager);
|
|
55
|
+
if (existing?.identity === identity) return existing;
|
|
56
|
+
const created = { identity, snapshots: new Map<string, Snapshot>() };
|
|
57
|
+
this.#sessions.set(ctx.sessionManager, created);
|
|
58
|
+
return created;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
observe(
|
|
62
|
+
ctx: SessionContext,
|
|
63
|
+
taskRunId: string | undefined,
|
|
64
|
+
targetPaths: string[],
|
|
65
|
+
source: EditFreshnessSource,
|
|
66
|
+
recordedAt = new Date().toISOString()
|
|
67
|
+
): string[] {
|
|
68
|
+
if (this.#mode === "off" || !taskRunId) return [];
|
|
69
|
+
const normalized = [...new Set(targetPaths.map((item) => normalizeProjectPath(ctx.cwd, item)).filter(Boolean) as string[])];
|
|
70
|
+
const digests = currentFileContentDigests(ctx.cwd, normalized), session = this.#session(ctx), observed: string[] = [];
|
|
71
|
+
for (const file of normalized) {
|
|
72
|
+
const digest = digests[file];
|
|
73
|
+
if (!digest) continue;
|
|
74
|
+
session.snapshots.delete(file);
|
|
75
|
+
session.snapshots.set(file, { digest, taskRunId, source, recordedAt });
|
|
76
|
+
observed.push(file);
|
|
77
|
+
}
|
|
78
|
+
while (session.snapshots.size > MAX_SNAPSHOTS_PER_SESSION) {
|
|
79
|
+
session.snapshots.delete(session.snapshots.keys().next().value as string);
|
|
80
|
+
}
|
|
81
|
+
return observed;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
evaluate(ctx: SessionContext, taskRunId: string | undefined, targetPaths: string[]): EditFreshnessEvaluation {
|
|
85
|
+
if (this.#mode === "off") return { decision: "off", checkedPaths: [], stalePaths: [], enforce: false };
|
|
86
|
+
if (!taskRunId) return { decision: "unobserved", checkedPaths: [], stalePaths: [], enforce: this.#mode === "enforce" };
|
|
87
|
+
const session = this.#session(ctx);
|
|
88
|
+
const tracked = [...new Set(targetPaths.map((item) => normalizeProjectPath(ctx.cwd, item)).filter(Boolean) as string[])]
|
|
89
|
+
.filter((file) => session.snapshots.get(file)?.taskRunId === taskRunId);
|
|
90
|
+
if (tracked.length === 0) return { decision: "unobserved", checkedPaths: [], stalePaths: [], enforce: this.#mode === "enforce" };
|
|
91
|
+
const current = currentFileContentDigests(ctx.cwd, tracked);
|
|
92
|
+
const stalePaths = tracked.filter((file) => current[file] !== session.snapshots.get(file)?.digest);
|
|
93
|
+
return {
|
|
94
|
+
decision: stalePaths.length > 0 ? "stale" : "current",
|
|
95
|
+
checkedPaths: tracked,
|
|
96
|
+
stalePaths,
|
|
97
|
+
enforce: this.#mode === "enforce"
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
clear(ctx: SessionContext): void { this.#sessions.delete(ctx.sessionManager); }
|
|
102
|
+
}
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { durableContextEvidenceEntries } from "../../extensions/context-evidence.js";
|
|
5
5
|
import type { TaskContract } from "../../extensions/guard-types.js";
|
|
6
|
+
import { operatorRequestDigest } from "../../extensions/task-state.js";
|
|
6
7
|
|
|
7
8
|
const MAX_EVIDENCE_BYTES = 256 * 1024;
|
|
8
9
|
const EXACT_OUTPUT_TEMPLATE = /\b([A-Z][A-Z0-9_]{2,63})\s*=\s*<([a-z][a-z0-9_-]{1,31})>/g;
|
|
@@ -11,6 +12,7 @@ export type ExactOutputContractEvaluation = {
|
|
|
11
12
|
applicable: boolean;
|
|
12
13
|
passed: boolean;
|
|
13
14
|
key?: string;
|
|
15
|
+
reason?: "exact-output-evidence-missing" | "exact-output-evidence-ambiguous";
|
|
14
16
|
expectedLines: string[];
|
|
15
17
|
evidencePaths: string[];
|
|
16
18
|
};
|
|
@@ -30,6 +32,15 @@ function exactOutputDirective(text: string): { key: string; placeholder: string
|
|
|
30
32
|
return undefined;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
type ExactOutputTaskText = Pick<TaskContract, "summary" | "expectedOutput" | "acceptanceCriteria" | "operatorRequest" | "operatorRequestDigest">;
|
|
36
|
+
|
|
37
|
+
function authoritativeTaskText(input: string | ExactOutputTaskText): string {
|
|
38
|
+
if (typeof input === "string") return input;
|
|
39
|
+
if (typeof input.operatorRequest === "string"
|
|
40
|
+
&& input.operatorRequestDigest === operatorRequestDigest(input.operatorRequest)) return input.operatorRequest;
|
|
41
|
+
return [input.summary, input.expectedOutput, ...(input.acceptanceCriteria ?? [])].filter(Boolean).join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
function safeObservedFile(cwd: string, candidate: string): { path: string; text: string } | undefined {
|
|
34
45
|
try {
|
|
35
46
|
const absolute = path.resolve(cwd, candidate);
|
|
@@ -50,24 +61,20 @@ function valuesForKey(key: string, text: string): string[] {
|
|
|
50
61
|
return [...text.matchAll(assignment)].map((match) => match[1]);
|
|
51
62
|
}
|
|
52
63
|
|
|
53
|
-
export function exactFinalOutputGuidance(taskText: string): string[] {
|
|
54
|
-
const directive = exactOutputDirective(taskText);
|
|
64
|
+
export function exactFinalOutputGuidance(taskText: string | ExactOutputTaskText): string[] {
|
|
65
|
+
const directive = exactOutputDirective(authoritativeTaskText(taskText));
|
|
55
66
|
if (!directive) return [];
|
|
56
67
|
return [
|
|
57
|
-
`Exact final-output contract: make ${directive.key}=<${directive.placeholder}> the last non-empty response line. Copy the complete value verbatim from observed
|
|
68
|
+
`Exact final-output contract: make ${directive.key}=<${directive.placeholder}> the last non-empty response line. Copy the complete value verbatim from observed project evidence and self-check every character before handoff.`
|
|
58
69
|
];
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
export function evaluateExactFinalOutputContract(
|
|
62
|
-
task: Pick<TaskContract, "summary" | "expectedOutput" | "acceptanceCriteria" | "contextManifest">,
|
|
73
|
+
task: Pick<TaskContract, "summary" | "expectedOutput" | "acceptanceCriteria" | "contextManifest" | "operatorRequest" | "operatorRequestDigest">,
|
|
63
74
|
responseText: string,
|
|
64
75
|
cwd: string
|
|
65
76
|
): ExactOutputContractEvaluation {
|
|
66
|
-
const directive = exactOutputDirective(
|
|
67
|
-
task.summary,
|
|
68
|
-
task.expectedOutput,
|
|
69
|
-
...(task.acceptanceCriteria ?? [])
|
|
70
|
-
].filter(Boolean).join("\n"));
|
|
77
|
+
const directive = exactOutputDirective(authoritativeTaskText(task));
|
|
71
78
|
if (!directive) return { applicable: false, passed: true, expectedLines: [], evidencePaths: [] };
|
|
72
79
|
|
|
73
80
|
const evidencePaths: string[] = [];
|
|
@@ -81,10 +88,13 @@ export function evaluateExactFinalOutputContract(
|
|
|
81
88
|
for (const value of values) observedValues.add(value);
|
|
82
89
|
}
|
|
83
90
|
if (observedValues.size === 0) {
|
|
84
|
-
return { applicable:
|
|
91
|
+
return { applicable: true, passed: false, key: directive.key, reason: "exact-output-evidence-missing", expectedLines: [], evidencePaths: [] };
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
const expectedLines = [...observedValues].sort().map((value) => `${directive.key}=${value}`);
|
|
95
|
+
if (expectedLines.length !== 1) {
|
|
96
|
+
return { applicable: true, passed: false, key: directive.key, reason: "exact-output-evidence-ambiguous", expectedLines, evidencePaths: [...new Set(evidencePaths)].sort() };
|
|
97
|
+
}
|
|
88
98
|
const finalLine = String(responseText ?? "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).at(-1) ?? "";
|
|
89
99
|
return {
|
|
90
100
|
applicable: true,
|
|
@@ -180,7 +180,7 @@ export function performanceReviewGuidance(task: TaskContract): string[] {
|
|
|
180
180
|
? [`Review the pre-change and current behavior for: ${changedFiles.slice(0, 12).join(", ")}.`]
|
|
181
181
|
: []),
|
|
182
182
|
...plan.reviewChecks,
|
|
183
|
-
"If no contradiction is found, stop reviewing and complete immediately. If one is found, name it, make the coordinated
|
|
183
|
+
"If no contradiction is found, stop reviewing and complete immediately. If one is found, name it, make the coordinated evidence-backed source/test edits required for one bounded revision, then run the exact configured verifier. A high-confidence verifier failure may open one final corrective revision."
|
|
184
184
|
];
|
|
185
185
|
}
|
|
186
186
|
|
|
@@ -489,11 +489,11 @@ export function performanceReviewToolDecision(input: PerformanceReviewToolInput)
|
|
|
489
489
|
}
|
|
490
490
|
return {
|
|
491
491
|
block: true,
|
|
492
|
-
reason: "The failed verifier opened a bounded corrective revision. Apply only the evidence-backed
|
|
492
|
+
reason: "The failed verifier opened a bounded corrective revision. Apply only the evidence-backed correction before re-running the exact verifier."
|
|
493
493
|
};
|
|
494
494
|
}
|
|
495
495
|
return {
|
|
496
496
|
block: true,
|
|
497
|
-
reason: "This semantic-review continuation is limited to bounded diff/read inspection, reviewed
|
|
497
|
+
reason: "This semantic-review continuation is limited to bounded diff/read inspection, reviewed evidence-backed edits, and one exact verifier per bounded revision."
|
|
498
498
|
};
|
|
499
499
|
}
|
|
@@ -52,7 +52,7 @@ function normalizedEvidenceDigest(value: string | null | undefined): string | nu
|
|
|
52
52
|
|
|
53
53
|
export function continuationClassForRecovery(decision: RecoveryDecision): ContinuationClass {
|
|
54
54
|
if (["permission-policy", "scope-protected-path", "environment"].includes(decision.failureCategory)
|
|
55
|
-
|| decision.reasonCodes.some((code) => ["permission-expansion-forbidden", "scope-replan-required", "operator-environment-action", "dependency-mutation-not-authorized"].includes(code))) return "policy-blocked";
|
|
55
|
+
|| decision.reasonCodes.some((code) => ["permission-expansion-forbidden", "protected-path-forbidden", "scope-replan-required", "operator-environment-action", "dependency-mutation-not-authorized"].includes(code))) return "policy-blocked";
|
|
56
56
|
if (decision.action === "repair") return "source-repair";
|
|
57
57
|
if (decision.failureCategory === "provider-network") return "model-retry";
|
|
58
58
|
if (decision.failureCategory === "flaky-infrastructure") return "infrastructure-retry";
|
|
@@ -211,6 +211,9 @@ function authority(recovery: RecoveryDecision | null): HandoffProjection["requir
|
|
|
211
211
|
const reasons = strings(recovery.reasonCodes, 20, 120);
|
|
212
212
|
if (recovery.action === "fresh-session") return { required: true, kind: "fresh-session", reasonCodes: reasons };
|
|
213
213
|
if (recovery.reasonCodes.includes("permission-expansion-forbidden")) return { required: true, kind: "permission", reasonCodes: reasons };
|
|
214
|
+
if (recovery.reasonCodes.includes("protected-path-forbidden")) return { required: true, kind: "permission", reasonCodes: reasons };
|
|
215
|
+
// Backward-compatible projection for recovery records written before task
|
|
216
|
+
// scope became advisory.
|
|
214
217
|
if (recovery.reasonCodes.includes("scope-replan-required")) return { required: true, kind: "scope", reasonCodes: reasons };
|
|
215
218
|
if (recovery.action === "ask-operator") return { required: true, kind: "operator", reasonCodes: reasons };
|
|
216
219
|
return { required: false, kind: "none", reasonCodes: reasons };
|
|
@@ -292,7 +295,7 @@ export function buildHandoffProjection(
|
|
|
292
295
|
decisionsAndInvariants: [
|
|
293
296
|
"Task Contract v2 is authoritative for task outcome.",
|
|
294
297
|
"Failure classification never authorizes source mutation by itself.",
|
|
295
|
-
"Source mutation still requires task scope
|
|
298
|
+
"Source mutation still requires hook authorization; task scope is an advisory retrieval/review focus.",
|
|
296
299
|
"Verifier evidence is current only when its namespaced working-tree digest matches the current tree."
|
|
297
300
|
],
|
|
298
301
|
contextReferences: {
|