@abelxiaoxing/cadence 1.2.0 → 1.2.2
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/agents/implementation-worker.md +9 -2
- package/package.json +1 -1
- package/src/control-contracts.ts +58 -0
- package/src/index.ts +71 -12
- package/src/run-store.ts +17 -0
- package/src/submit-tool.ts +250 -30
- package/src/workflow-engine.ts +566 -26
|
@@ -20,8 +20,15 @@ Your output is one complete structured patch delivered through `abel_submit_resu
|
|
|
20
20
|
- every operation path is within the declared phase boundary; paths that do
|
|
21
21
|
not need a change may remain untouched;
|
|
22
22
|
- preserve the expected parent-owned verification contract without claiming a result;
|
|
23
|
-
- use a typed `context-request` for bounded context
|
|
24
|
-
|
|
23
|
+
- use a typed `context-request` for bounded context or task splitting instead
|
|
24
|
+
of submitting a partial patch; classify every ref as a requested path with
|
|
25
|
+
`read | write` access, a source citation with path and line, or a
|
|
26
|
+
contract-diagnostic ref;
|
|
27
|
+
- report only the missing context and structural facts: never select
|
|
28
|
+
`approval-needed`, `boundary-review-needed`, a Gate, or a workflow stage;
|
|
29
|
+
- an `AGENTS.md` path is never a Worker write path;
|
|
30
|
+
- report an AGENTS read need separately, and leave every sealed AGENTS contract
|
|
31
|
+
and managed-block update to the parent control plane.
|
|
25
32
|
|
|
26
33
|
If the submit tool rejects malformed operations, use its concrete error to correct the submission once in this same disposable session; a second rejected structural submission ends the session.
|
|
27
34
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abelxiaoxing/cadence",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Four explicit Abel workflow prompts with stage-isolated design and diagnosis, durable resumable implementation, and three package-owned professional Agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/control-contracts.ts
CHANGED
|
@@ -74,6 +74,64 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
74
74
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
const CONTROL_TOOL_FIELDS = new Set([
|
|
78
|
+
"command",
|
|
79
|
+
"stage",
|
|
80
|
+
"change",
|
|
81
|
+
"operationId",
|
|
82
|
+
"deliveryRevision",
|
|
83
|
+
"receiptHash",
|
|
84
|
+
"routeId",
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
const COMMAND_FIELDS: Readonly<
|
|
88
|
+
Record<ControlCommandName, ReadonlySet<string>>
|
|
89
|
+
> = Object.freeze({
|
|
90
|
+
start: new Set(["command", "stage", "change", "operationId"]),
|
|
91
|
+
status: new Set(["command", "stage", "change"]),
|
|
92
|
+
resume: new Set([
|
|
93
|
+
"command",
|
|
94
|
+
"stage",
|
|
95
|
+
"change",
|
|
96
|
+
"operationId",
|
|
97
|
+
"deliveryRevision",
|
|
98
|
+
"receiptHash",
|
|
99
|
+
]),
|
|
100
|
+
rebind: new Set(["command", "stage", "change", "operationId", "routeId"]),
|
|
101
|
+
cancel: new Set(["command", "stage", "change", "operationId"]),
|
|
102
|
+
discard: new Set(["command", "stage", "change", "operationId"]),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Adapts Pi/provider strict-schema padding to the closed command union.
|
|
107
|
+
* Known fields belonging to a different command are projected away, while
|
|
108
|
+
* unknown fields and fields owned by the selected command remain available to
|
|
109
|
+
* the strict validator and therefore cannot bypass its exact-key/type checks.
|
|
110
|
+
*/
|
|
111
|
+
export function canonicalizeControlCommandToolInput(value: unknown): unknown {
|
|
112
|
+
if (
|
|
113
|
+
!isRecord(value) ||
|
|
114
|
+
!(CONTROL_COMMANDS as readonly unknown[]).includes(value.command)
|
|
115
|
+
) {
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
const command = value.command as ControlCommandName;
|
|
119
|
+
const allowed = COMMAND_FIELDS[command];
|
|
120
|
+
const canonical = Object.create(null) as Record<string, unknown>;
|
|
121
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
122
|
+
if (CONTROL_TOOL_FIELDS.has(key) && !allowed.has(key)) continue;
|
|
123
|
+
if (
|
|
124
|
+
command === "resume" &&
|
|
125
|
+
(key === "deliveryRevision" || key === "receiptHash") &&
|
|
126
|
+
(entry === null || entry === undefined)
|
|
127
|
+
) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
canonical[key] = entry;
|
|
131
|
+
}
|
|
132
|
+
return canonical;
|
|
133
|
+
}
|
|
134
|
+
|
|
77
135
|
function hasExactKeys(
|
|
78
136
|
value: Record<string, unknown>,
|
|
79
137
|
required: readonly string[],
|
package/src/index.ts
CHANGED
|
@@ -37,7 +37,10 @@ import {
|
|
|
37
37
|
type StructuredVerificationContract,
|
|
38
38
|
verificationSteps,
|
|
39
39
|
} from "./contracts.ts";
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
canonicalizeControlCommandToolInput,
|
|
42
|
+
validateControlCommand,
|
|
43
|
+
} from "./control-contracts.ts";
|
|
41
44
|
import {
|
|
42
45
|
assessDeliveryTraceability,
|
|
43
46
|
compileImplementPlan,
|
|
@@ -1522,19 +1525,46 @@ export function openPackageWorkflowControlEngine(
|
|
|
1522
1525
|
if (!phase) {
|
|
1523
1526
|
return { kind: "paused", code: "task-phase-unavailable" };
|
|
1524
1527
|
}
|
|
1528
|
+
const taskPhases = Object.values(input.task.phases);
|
|
1529
|
+
const executionBoundary = input.artifactCorrection
|
|
1530
|
+
? {
|
|
1531
|
+
read: [
|
|
1532
|
+
...new Set(
|
|
1533
|
+
taskPhases.flatMap((boundary) => [
|
|
1534
|
+
...boundary.read,
|
|
1535
|
+
...boundary.write,
|
|
1536
|
+
...boundary.delete,
|
|
1537
|
+
]),
|
|
1538
|
+
),
|
|
1539
|
+
].sort(),
|
|
1540
|
+
write: [
|
|
1541
|
+
...new Set(taskPhases.flatMap((boundary) => boundary.write)),
|
|
1542
|
+
].sort(),
|
|
1543
|
+
delete: [
|
|
1544
|
+
...new Set(taskPhases.flatMap((boundary) => boundary.delete)),
|
|
1545
|
+
].sort(),
|
|
1546
|
+
}
|
|
1547
|
+
: {
|
|
1548
|
+
read: [...phase.read],
|
|
1549
|
+
write: [...phase.write],
|
|
1550
|
+
delete: [...phase.delete],
|
|
1551
|
+
};
|
|
1525
1552
|
const phaseContract = {
|
|
1526
1553
|
candidateId: input.candidateArtifact.identity.candidateId,
|
|
1527
1554
|
taskId: input.taskId,
|
|
1528
1555
|
phase: input.phase,
|
|
1529
|
-
readSet:
|
|
1530
|
-
writeSet:
|
|
1531
|
-
deleteSet:
|
|
1556
|
+
readSet: executionBoundary.read,
|
|
1557
|
+
writeSet: executionBoundary.write,
|
|
1558
|
+
deleteSet: executionBoundary.delete,
|
|
1532
1559
|
verification: structuredClone(phase.verification),
|
|
1533
1560
|
agentsImpact: input.task.agents.impact,
|
|
1534
1561
|
agentsTarget: input.task.agents.target ?? null,
|
|
1535
1562
|
agentsManagedOnly: true,
|
|
1536
1563
|
agentsWriteAllowed: false,
|
|
1537
1564
|
impactClosure: structuredClone(input.task.impactClosure),
|
|
1565
|
+
...(input.artifactCorrection
|
|
1566
|
+
? { artifactCorrection: structuredClone(input.artifactCorrection) }
|
|
1567
|
+
: {}),
|
|
1538
1568
|
...(input.repair
|
|
1539
1569
|
? {
|
|
1540
1570
|
repair: {
|
|
@@ -1567,7 +1597,11 @@ export function openPackageWorkflowControlEngine(
|
|
|
1567
1597
|
path.resolve(input.workspaceRoot, root),
|
|
1568
1598
|
),
|
|
1569
1599
|
allowedPaths: [
|
|
1570
|
-
...new Set([
|
|
1600
|
+
...new Set([
|
|
1601
|
+
...executionBoundary.read,
|
|
1602
|
+
...executionBoundary.write,
|
|
1603
|
+
...executionBoundary.delete,
|
|
1604
|
+
]),
|
|
1571
1605
|
],
|
|
1572
1606
|
timeoutMs: LIMITS.phaseTimeoutMs,
|
|
1573
1607
|
signal: input.signal,
|
|
@@ -1597,11 +1631,28 @@ export function openPackageWorkflowControlEngine(
|
|
|
1597
1631
|
}
|
|
1598
1632
|
const result = child.result;
|
|
1599
1633
|
if (result.kind === "context-request") {
|
|
1600
|
-
return classifyCandidateContextRequest(result,
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1634
|
+
return classifyCandidateContextRequest(result, {
|
|
1635
|
+
phase: input.phase,
|
|
1636
|
+
readPaths: phase.read,
|
|
1637
|
+
writePaths: phase.write,
|
|
1638
|
+
deletePaths: phase.delete,
|
|
1639
|
+
taskPaths: [
|
|
1640
|
+
...new Set(
|
|
1641
|
+
Object.values(input.task.phases).flatMap((boundary) => [
|
|
1642
|
+
...boundary.read,
|
|
1643
|
+
...boundary.write,
|
|
1644
|
+
...boundary.delete,
|
|
1645
|
+
]),
|
|
1646
|
+
),
|
|
1647
|
+
],
|
|
1648
|
+
redWritePaths: input.task.phases.red.write,
|
|
1649
|
+
agents: {
|
|
1650
|
+
impact: input.task.agents.impact,
|
|
1651
|
+
...(input.task.agents.target
|
|
1652
|
+
? { target: input.task.agents.target }
|
|
1653
|
+
: {}),
|
|
1654
|
+
},
|
|
1655
|
+
});
|
|
1605
1656
|
}
|
|
1606
1657
|
if (result.kind !== "sealed-candidate") {
|
|
1607
1658
|
return { kind: "retryable", code: "candidate-diff-invalid" };
|
|
@@ -1841,7 +1892,7 @@ export function registerWorkflowControl(
|
|
|
1841
1892
|
receiptHash: { type: "string" },
|
|
1842
1893
|
routeId: { type: "string" },
|
|
1843
1894
|
},
|
|
1844
|
-
required: ["command", "stage"],
|
|
1895
|
+
required: ["command", "stage", "change"],
|
|
1845
1896
|
additionalProperties: false,
|
|
1846
1897
|
} as const;
|
|
1847
1898
|
const PACKET_PARAMETERS = {
|
|
@@ -1872,6 +1923,12 @@ export function registerWorkflowControl(
|
|
|
1872
1923
|
? "Private stage-bound Abel workflow control. Accepts durable Implement change commands."
|
|
1873
1924
|
: "Private stage-bound Abel packet control. Accepts bounded Design and Diagnose packet operations.",
|
|
1874
1925
|
executionMode: "parallel",
|
|
1926
|
+
...(kind === "command"
|
|
1927
|
+
? {
|
|
1928
|
+
prepareArguments: (args: unknown) =>
|
|
1929
|
+
canonicalizeControlCommandToolInput(args) as never,
|
|
1930
|
+
}
|
|
1931
|
+
: {}),
|
|
1875
1932
|
parameters:
|
|
1876
1933
|
kind === "command" ? CONTROL_COMMAND_PARAMETERS : PACKET_PARAMETERS,
|
|
1877
1934
|
async execute(
|
|
@@ -2004,7 +2061,9 @@ export function registerWorkflowControl(
|
|
|
2004
2061
|
...(usage === undefined ? {} : { usage }),
|
|
2005
2062
|
};
|
|
2006
2063
|
}
|
|
2007
|
-
const validation = validateControlCommand(
|
|
2064
|
+
const validation = validateControlCommand(
|
|
2065
|
+
canonicalizeControlCommandToolInput(params),
|
|
2066
|
+
);
|
|
2008
2067
|
if (!validation.ok) {
|
|
2009
2068
|
const error = new Error(validation.code);
|
|
2010
2069
|
error.name = "ControlCommandError";
|
package/src/run-store.ts
CHANGED
|
@@ -917,7 +917,23 @@ export class RunStore {
|
|
|
917
917
|
code?: string;
|
|
918
918
|
lease?: OperationLease;
|
|
919
919
|
}): RunProjection {
|
|
920
|
+
return this.transitionAtomically(input, () => {});
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
transitionAtomically(
|
|
924
|
+
input: {
|
|
925
|
+
runId: string;
|
|
926
|
+
to: RunState;
|
|
927
|
+
operationId: string;
|
|
928
|
+
code?: string;
|
|
929
|
+
lease?: OperationLease;
|
|
930
|
+
},
|
|
931
|
+
write: (database: DatabaseSync) => void,
|
|
932
|
+
): RunProjection {
|
|
920
933
|
requireIdentifier(input.operationId, "operation-id");
|
|
934
|
+
if (typeof write !== "function") {
|
|
935
|
+
throw new Error("state-transition-write-invalid");
|
|
936
|
+
}
|
|
921
937
|
return this.#transaction(() => {
|
|
922
938
|
if (input.lease) {
|
|
923
939
|
if (input.lease.runId !== input.runId) throw new Error("lease-fenced");
|
|
@@ -935,6 +951,7 @@ export class RunStore {
|
|
|
935
951
|
to: input.to,
|
|
936
952
|
...(input.code ? { code: input.code } : {}),
|
|
937
953
|
});
|
|
954
|
+
write(this.#database);
|
|
938
955
|
this.#recordOperation(
|
|
939
956
|
input.runId,
|
|
940
957
|
input.operationId,
|
package/src/submit-tool.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type CandidateFailure,
|
|
10
10
|
type DiffResult,
|
|
11
11
|
type EvidenceResult,
|
|
12
|
+
isAgentsPath,
|
|
12
13
|
isValidRelativePath,
|
|
13
14
|
type SubmitFinalCategory,
|
|
14
15
|
type SubmitSchemaState,
|
|
@@ -144,15 +145,57 @@ function candidateArtifactSchema(candidateId: string) {
|
|
|
144
145
|
code: Type.Union([
|
|
145
146
|
Type.Literal("approved-context-needed"),
|
|
146
147
|
Type.Literal("task-split-needed"),
|
|
147
|
-
Type.Literal("boundary-review-needed"),
|
|
148
148
|
]),
|
|
149
|
-
refs: Type.Array(
|
|
149
|
+
refs: Type.Array(
|
|
150
|
+
Type.Union([
|
|
151
|
+
Type.String(),
|
|
152
|
+
Type.Object(
|
|
153
|
+
{
|
|
154
|
+
kind: Type.Literal("requested-path"),
|
|
155
|
+
path: Type.String(),
|
|
156
|
+
access: Type.Union([
|
|
157
|
+
Type.Literal("read"),
|
|
158
|
+
Type.Literal("write"),
|
|
159
|
+
]),
|
|
160
|
+
},
|
|
161
|
+
{ additionalProperties: false },
|
|
162
|
+
),
|
|
163
|
+
Type.Object(
|
|
164
|
+
{
|
|
165
|
+
kind: Type.Literal("source-citation"),
|
|
166
|
+
path: Type.String(),
|
|
167
|
+
line: Type.Integer({ minimum: 1 }),
|
|
168
|
+
},
|
|
169
|
+
{ additionalProperties: false },
|
|
170
|
+
),
|
|
171
|
+
Type.Object(
|
|
172
|
+
{
|
|
173
|
+
kind: Type.Literal("contract-diagnostic"),
|
|
174
|
+
ref: Type.String(),
|
|
175
|
+
},
|
|
176
|
+
{ additionalProperties: false },
|
|
177
|
+
),
|
|
178
|
+
]),
|
|
179
|
+
{ minItems: 1, maxItems: 32 },
|
|
180
|
+
),
|
|
150
181
|
},
|
|
151
182
|
{ additionalProperties: false },
|
|
152
183
|
),
|
|
153
184
|
]);
|
|
154
185
|
}
|
|
155
186
|
|
|
187
|
+
export type CandidateContextRequestCode =
|
|
188
|
+
| "approved-context-needed"
|
|
189
|
+
| "task-split-needed"
|
|
190
|
+
| "boundary-review-needed";
|
|
191
|
+
|
|
192
|
+
export type CandidateContextRef =
|
|
193
|
+
| { kind: "requested-path"; path: string; access: "read" | "write" }
|
|
194
|
+
| { kind: "source-citation"; path: string; line: number }
|
|
195
|
+
| { kind: "contract-diagnostic"; ref: string };
|
|
196
|
+
|
|
197
|
+
export type CandidateContextRefInput = string | CandidateContextRef;
|
|
198
|
+
|
|
156
199
|
export type CandidateArtifactSubmissionResult =
|
|
157
200
|
| {
|
|
158
201
|
kind: "sealed-candidate";
|
|
@@ -166,11 +209,8 @@ export type CandidateArtifactSubmissionResult =
|
|
|
166
209
|
| {
|
|
167
210
|
kind: "context-request";
|
|
168
211
|
candidateId: string;
|
|
169
|
-
code:
|
|
170
|
-
|
|
171
|
-
| "task-split-needed"
|
|
172
|
-
| "boundary-review-needed";
|
|
173
|
-
refs: string[];
|
|
212
|
+
code: CandidateContextRequestCode;
|
|
213
|
+
refs: CandidateContextRef[];
|
|
174
214
|
};
|
|
175
215
|
|
|
176
216
|
export type CandidateContextRequest = Extract<
|
|
@@ -178,18 +218,114 @@ export type CandidateContextRequest = Extract<
|
|
|
178
218
|
{ kind: "context-request" }
|
|
179
219
|
>;
|
|
180
220
|
|
|
221
|
+
export interface CandidateContextBoundary {
|
|
222
|
+
phase: "red" | "green" | "refactor";
|
|
223
|
+
readPaths: readonly string[];
|
|
224
|
+
writePaths: readonly string[];
|
|
225
|
+
deletePaths?: readonly string[];
|
|
226
|
+
taskPaths: readonly string[];
|
|
227
|
+
redWritePaths: readonly string[];
|
|
228
|
+
agents: {
|
|
229
|
+
impact: "none" | "update-existing" | "create-index" | "remove-index";
|
|
230
|
+
target?: string;
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
181
234
|
export interface ClassifiedCandidateContextRequest {
|
|
182
|
-
kind: "paused" | "approval-needed";
|
|
235
|
+
kind: "paused" | "retryable" | "approval-needed";
|
|
183
236
|
code:
|
|
184
237
|
| "approved-context-needed"
|
|
185
238
|
| "task-split-needed"
|
|
186
|
-
| "boundary-review-needed"
|
|
239
|
+
| "boundary-review-needed"
|
|
240
|
+
| "red-artifact-constraint"
|
|
241
|
+
| "agents-context-needed"
|
|
242
|
+
| "agents-write-parent-owned"
|
|
243
|
+
| "agents-contract-insufficient";
|
|
187
244
|
contextRequest: {
|
|
188
|
-
code:
|
|
189
|
-
refs:
|
|
245
|
+
code: CandidateContextRequestCode;
|
|
246
|
+
refs: CandidateContextRef[];
|
|
190
247
|
};
|
|
191
248
|
}
|
|
192
249
|
|
|
250
|
+
const LEGACY_CITATION = /^(.*):(\d+)$/u;
|
|
251
|
+
const LEGACY_DIAGNOSTIC =
|
|
252
|
+
/^(?:phase-contract|task-contract|sealed-contract|workflow-contract)\.[a-z0-9._-]+$/iu;
|
|
253
|
+
|
|
254
|
+
function validDiagnosticRef(value: unknown): value is string {
|
|
255
|
+
return (
|
|
256
|
+
typeof value === "string" &&
|
|
257
|
+
value.length > 0 &&
|
|
258
|
+
value.length <= 512 &&
|
|
259
|
+
value.trim() === value &&
|
|
260
|
+
!/[\r\n\0]/u.test(value)
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function normalizeCandidateContextRef(
|
|
265
|
+
input: CandidateContextRefInput,
|
|
266
|
+
): CandidateContextRef {
|
|
267
|
+
if (typeof input === "string") {
|
|
268
|
+
const citation = LEGACY_CITATION.exec(input);
|
|
269
|
+
if (citation && isValidRelativePath(citation[1])) {
|
|
270
|
+
const line = Number(citation[2]);
|
|
271
|
+
if (Number.isSafeInteger(line) && line >= 1) {
|
|
272
|
+
return { kind: "source-citation", path: citation[1], line };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (LEGACY_DIAGNOSTIC.test(input)) {
|
|
276
|
+
return { kind: "contract-diagnostic", ref: input };
|
|
277
|
+
}
|
|
278
|
+
if (!isValidRelativePath(input)) {
|
|
279
|
+
throw new Error("context request refs are invalid");
|
|
280
|
+
}
|
|
281
|
+
return { kind: "requested-path", path: input, access: "read" };
|
|
282
|
+
}
|
|
283
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
284
|
+
throw new Error("context request refs are invalid");
|
|
285
|
+
}
|
|
286
|
+
if (
|
|
287
|
+
input.kind === "requested-path" &&
|
|
288
|
+
Object.keys(input).length === 3 &&
|
|
289
|
+
isValidRelativePath(input.path) &&
|
|
290
|
+
(input.access === "read" || input.access === "write")
|
|
291
|
+
) {
|
|
292
|
+
return { ...input };
|
|
293
|
+
}
|
|
294
|
+
if (
|
|
295
|
+
input.kind === "source-citation" &&
|
|
296
|
+
Object.keys(input).length === 3 &&
|
|
297
|
+
isValidRelativePath(input.path) &&
|
|
298
|
+
Number.isSafeInteger(input.line) &&
|
|
299
|
+
input.line >= 1
|
|
300
|
+
) {
|
|
301
|
+
return { ...input };
|
|
302
|
+
}
|
|
303
|
+
if (
|
|
304
|
+
input.kind === "contract-diagnostic" &&
|
|
305
|
+
Object.keys(input).length === 2 &&
|
|
306
|
+
validDiagnosticRef(input.ref)
|
|
307
|
+
) {
|
|
308
|
+
return { ...input };
|
|
309
|
+
}
|
|
310
|
+
throw new Error("context request refs are invalid");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function normalizeCandidateContextRefs(
|
|
314
|
+
value: unknown,
|
|
315
|
+
): CandidateContextRef[] {
|
|
316
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 32) {
|
|
317
|
+
throw new Error("context request refs are invalid");
|
|
318
|
+
}
|
|
319
|
+
const refs = value.map((entry) =>
|
|
320
|
+
normalizeCandidateContextRef(entry as CandidateContextRefInput),
|
|
321
|
+
);
|
|
322
|
+
const identities = refs.map((ref) => JSON.stringify(ref));
|
|
323
|
+
if (new Set(identities).size !== refs.length) {
|
|
324
|
+
throw new Error("context request refs are invalid");
|
|
325
|
+
}
|
|
326
|
+
return refs;
|
|
327
|
+
}
|
|
328
|
+
|
|
193
329
|
function contextRefWithinBoundary(relative: string, approved: string): boolean {
|
|
194
330
|
return (
|
|
195
331
|
approved === "." ||
|
|
@@ -198,23 +334,108 @@ function contextRefWithinBoundary(relative: string, approved: string): boolean {
|
|
|
198
334
|
);
|
|
199
335
|
}
|
|
200
336
|
|
|
337
|
+
function normalizedBoundary(
|
|
338
|
+
boundary: readonly string[] | CandidateContextBoundary,
|
|
339
|
+
): CandidateContextBoundary {
|
|
340
|
+
if (!Array.isArray(boundary)) return boundary as CandidateContextBoundary;
|
|
341
|
+
return {
|
|
342
|
+
phase: "green",
|
|
343
|
+
readPaths: boundary,
|
|
344
|
+
writePaths: [],
|
|
345
|
+
deletePaths: [],
|
|
346
|
+
taskPaths: boundary,
|
|
347
|
+
redWritePaths: [],
|
|
348
|
+
agents: { impact: "none" },
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function pathWithinAny(relative: string, approved: readonly string[]): boolean {
|
|
353
|
+
return approved.some((path) => contextRefWithinBoundary(relative, path));
|
|
354
|
+
}
|
|
355
|
+
|
|
201
356
|
export function classifyCandidateContextRequest(
|
|
202
|
-
request:
|
|
203
|
-
|
|
357
|
+
request: {
|
|
358
|
+
kind: "context-request";
|
|
359
|
+
candidateId: string;
|
|
360
|
+
code: CandidateContextRequestCode;
|
|
361
|
+
refs: readonly CandidateContextRefInput[];
|
|
362
|
+
},
|
|
363
|
+
inputBoundary: readonly string[] | CandidateContextBoundary,
|
|
204
364
|
): ClassifiedCandidateContextRequest {
|
|
205
|
-
const refs =
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
365
|
+
const refs = normalizeCandidateContextRefs(request.refs);
|
|
366
|
+
const boundary = normalizedBoundary(inputBoundary);
|
|
367
|
+
const phasePaths = [
|
|
368
|
+
...boundary.readPaths,
|
|
369
|
+
...boundary.writePaths,
|
|
370
|
+
...(boundary.deletePaths ?? []),
|
|
371
|
+
];
|
|
372
|
+
const requested = refs.filter(
|
|
373
|
+
(ref): ref is Extract<CandidateContextRef, { kind: "requested-path" }> =>
|
|
374
|
+
ref.kind === "requested-path",
|
|
375
|
+
);
|
|
376
|
+
const ordinaryRequested = requested.filter((ref) => !isAgentsPath(ref.path));
|
|
377
|
+
const outsideOrdinary = ordinaryRequested.filter(
|
|
378
|
+
(ref) =>
|
|
379
|
+
!pathWithinAny(
|
|
380
|
+
ref.path,
|
|
381
|
+
ref.access === "write"
|
|
382
|
+
? [...boundary.writePaths, ...(boundary.deletePaths ?? [])]
|
|
383
|
+
: phasePaths,
|
|
211
384
|
),
|
|
385
|
+
);
|
|
386
|
+
const agentsRequested = requested.filter((ref) => isAgentsPath(ref.path));
|
|
387
|
+
const agentsWrites = agentsRequested.filter((ref) => ref.access === "write");
|
|
388
|
+
const citedRedArtifacts = refs.filter(
|
|
389
|
+
(ref): ref is Extract<CandidateContextRef, { kind: "source-citation" }> =>
|
|
390
|
+
ref.kind === "source-citation" &&
|
|
391
|
+
pathWithinAny(ref.path, boundary.redWritePaths) &&
|
|
392
|
+
!pathWithinAny(ref.path, [
|
|
393
|
+
...boundary.writePaths,
|
|
394
|
+
...(boundary.deletePaths ?? []),
|
|
395
|
+
]),
|
|
396
|
+
);
|
|
397
|
+
const requestedRedArtifacts = outsideOrdinary.filter(
|
|
398
|
+
(ref) =>
|
|
399
|
+
pathWithinAny(ref.path, boundary.redWritePaths) &&
|
|
400
|
+
pathWithinAny(ref.path, boundary.taskPaths),
|
|
401
|
+
);
|
|
402
|
+
const redArtifactConstraint =
|
|
403
|
+
boundary.phase === "green" &&
|
|
404
|
+
(citedRedArtifacts.length > 0 || requestedRedArtifacts.length > 0) &&
|
|
405
|
+
outsideOrdinary.every((ref) => requestedRedArtifacts.includes(ref)) &&
|
|
406
|
+
agentsRequested.every((ref) => ref.access === "read");
|
|
407
|
+
|
|
408
|
+
let classification: Pick<ClassifiedCandidateContextRequest, "kind" | "code">;
|
|
409
|
+
if (outsideOrdinary.length > 0 && !redArtifactConstraint) {
|
|
410
|
+
classification = {
|
|
411
|
+
kind: "approval-needed",
|
|
412
|
+
code: "boundary-review-needed",
|
|
413
|
+
};
|
|
414
|
+
} else if (agentsWrites.length > 0) {
|
|
415
|
+
const targetMismatch = agentsWrites.some(
|
|
416
|
+
(ref) =>
|
|
417
|
+
boundary.agents.impact === "none" ||
|
|
418
|
+
boundary.agents.target === undefined ||
|
|
419
|
+
ref.path !== boundary.agents.target,
|
|
212
420
|
);
|
|
213
|
-
|
|
214
|
-
|
|
421
|
+
classification = targetMismatch
|
|
422
|
+
? { kind: "approval-needed", code: "agents-contract-insufficient" }
|
|
423
|
+
: { kind: "paused", code: "agents-write-parent-owned" };
|
|
424
|
+
} else if (redArtifactConstraint) {
|
|
425
|
+
classification = { kind: "retryable", code: "red-artifact-constraint" };
|
|
426
|
+
} else if (agentsRequested.length > 0) {
|
|
427
|
+
classification = { kind: "paused", code: "agents-context-needed" };
|
|
428
|
+
} else {
|
|
429
|
+
classification = {
|
|
430
|
+
kind: "paused",
|
|
431
|
+
code:
|
|
432
|
+
request.code === "task-split-needed"
|
|
433
|
+
? "task-split-needed"
|
|
434
|
+
: "approved-context-needed",
|
|
435
|
+
};
|
|
436
|
+
}
|
|
215
437
|
return {
|
|
216
|
-
|
|
217
|
-
code: inBoundary ? request.code : "boundary-review-needed",
|
|
438
|
+
...classification,
|
|
218
439
|
contextRequest: {
|
|
219
440
|
code: request.code,
|
|
220
441
|
refs,
|
|
@@ -391,18 +612,17 @@ export function createCandidateArtifactTool(
|
|
|
391
612
|
terminate: true,
|
|
392
613
|
};
|
|
393
614
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
refs
|
|
397
|
-
|
|
398
|
-
) {
|
|
615
|
+
let refs: CandidateContextRef[];
|
|
616
|
+
try {
|
|
617
|
+
refs = normalizeCandidateContextRefs(value.refs);
|
|
618
|
+
} catch (error) {
|
|
399
619
|
schema = "invalid";
|
|
400
620
|
failure = {
|
|
401
621
|
kind: "artifact",
|
|
402
622
|
code: "invalid-structural-result",
|
|
403
623
|
stage: "structural-submit",
|
|
404
624
|
};
|
|
405
|
-
throw
|
|
625
|
+
throw error;
|
|
406
626
|
}
|
|
407
627
|
result = {
|
|
408
628
|
kind: "context-request",
|
|
@@ -411,7 +631,7 @@ export function createCandidateArtifactTool(
|
|
|
411
631
|
CandidateArtifactSubmissionResult,
|
|
412
632
|
{ kind: "context-request" }
|
|
413
633
|
>["code"],
|
|
414
|
-
refs
|
|
634
|
+
refs,
|
|
415
635
|
};
|
|
416
636
|
return {
|
|
417
637
|
content: [
|