@brainervirus/workit-core 6.0.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -34
- package/package.json +1 -1
- package/skills/workit-deslop/SKILL.md +1 -1
- package/skills/workit-implement/SKILL.md +9 -4
- package/skills/workit-review/SKILL.md +2 -0
- package/skills/workit-shape/SKILL.md +1 -1
- package/skills/workit-ship/SKILL.md +1 -1
- package/src/autonomy.ts +24 -0
- package/src/core/methods.ts +7 -5
- package/src/core/operation-input.ts +506 -0
- package/src/core/policy/derive.ts +338 -0
- package/src/core/policy/judgment.ts +192 -0
- package/src/core/policy-resolver.ts +32 -427
- package/src/core/task-contract.ts +40 -229
- package/src/core/task-engine.ts +51 -58
- package/src/core/task-evaluation.ts +35 -1
- package/src/core/workspaces.ts +2 -0
- package/src/core.ts +8 -6
- package/src/hooks/handle.ts +19 -4
- package/src/hooks/hosts/claude-code.ts +20 -1
- package/src/hooks/hosts/codex.ts +3 -0
- package/src/hooks/hosts/cursor.ts +11 -2
- package/src/hooks/hosts/fields.ts +35 -0
- package/src/hooks/hosts/opencode.ts +1 -0
- package/src/hooks/hosts/pi.ts +1 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/protocol.ts +3 -0
- package/src/hooks/write-gate.ts +303 -0
- package/src/ledger.ts +20 -0
|
@@ -267,96 +267,27 @@ export const entrySchema = <T extends z.ZodType>(data: T) =>
|
|
|
267
267
|
z.object({ id, recordedAt: utc, provenance: provenanceSchema, data }).strict();
|
|
268
268
|
export type Entry<T> = { id: Id; recordedAt: Utc; provenance: Provenance; data: T };
|
|
269
269
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
ctx.issues.push({
|
|
280
|
-
code: "custom",
|
|
281
|
-
input: ctx.value,
|
|
282
|
-
message: "observed facts require supporting references",
|
|
283
|
-
path: ["refs"],
|
|
284
|
-
});
|
|
285
|
-
});
|
|
286
|
-
export type Fact = z.infer<typeof factSchema>;
|
|
287
|
-
export const signalSchema = z
|
|
270
|
+
/** Risk tiers an agent judges; aliases (low, medium, …) are mapped on input. */
|
|
271
|
+
export const RISK_TIERS = ["trivial", "normal", "high"] as const;
|
|
272
|
+
export type RiskTier = (typeof RISK_TIERS)[number];
|
|
273
|
+
/**
|
|
274
|
+
* The agent's policy judgment (S17): four flat calls plus an optional note
|
|
275
|
+
* and supporting refs. Everything else is derived deterministically
|
|
276
|
+
* (core/policy/derive.ts).
|
|
277
|
+
*/
|
|
278
|
+
export const judgmentSchema = z
|
|
288
279
|
.object({
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
280
|
+
riskTier: z.enum(RISK_TIERS),
|
|
281
|
+
behaviorChange: z.boolean(),
|
|
282
|
+
productChoiceOpen: z.boolean(),
|
|
283
|
+
needsPlan: z.boolean(),
|
|
284
|
+
note: text.nullable(),
|
|
292
285
|
refs: z.array(refSchema),
|
|
293
286
|
})
|
|
294
|
-
.strict()
|
|
295
|
-
.check((ctx) => {
|
|
296
|
-
const signal = ctx.value;
|
|
297
|
-
const valid =
|
|
298
|
-
signal.value === "unknown"
|
|
299
|
-
? signal.basis === "unknown"
|
|
300
|
-
: signal.basis === "observed" || signal.basis === "inferred";
|
|
301
|
-
if (!valid)
|
|
302
|
-
ctx.issues.push({
|
|
303
|
-
code: "custom",
|
|
304
|
-
input: signal,
|
|
305
|
-
message: "signal value and basis disagree",
|
|
306
|
-
path: [],
|
|
307
|
-
});
|
|
308
|
-
if (signal.basis === "observed" && signal.refs.length === 0)
|
|
309
|
-
ctx.issues.push({
|
|
310
|
-
code: "custom",
|
|
311
|
-
input: signal,
|
|
312
|
-
message: "observed signals require supporting references",
|
|
313
|
-
path: ["refs"],
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
export type Signal = z.infer<typeof signalSchema>;
|
|
317
|
-
const signalSet = z
|
|
318
|
-
.object({
|
|
319
|
-
approachUnknown: signalSchema,
|
|
320
|
-
productChoiceOpen: signalSchema,
|
|
321
|
-
behaviorChange: signalSchema,
|
|
322
|
-
mechanicalLowRisk: signalSchema,
|
|
323
|
-
durableAgreementNeeded: signalSchema,
|
|
324
|
-
coordinationPlanNeeded: signalSchema,
|
|
325
|
-
helperUseful: signalSchema,
|
|
326
|
-
testFirstPractical: signalSchema,
|
|
327
|
-
})
|
|
328
|
-
.strict();
|
|
329
|
-
const consequenceSchema = z
|
|
330
|
-
.object({
|
|
331
|
-
area: z.enum([
|
|
332
|
-
"behavior",
|
|
333
|
-
"public_contract",
|
|
334
|
-
"data",
|
|
335
|
-
"security",
|
|
336
|
-
"operations",
|
|
337
|
-
"reversibility",
|
|
338
|
-
"consumers",
|
|
339
|
-
]),
|
|
340
|
-
fact: factSchema,
|
|
341
|
-
})
|
|
342
|
-
.strict();
|
|
343
|
-
const verificationSchema = z
|
|
344
|
-
.object({
|
|
345
|
-
claim: text,
|
|
346
|
-
scope: scopeSchema,
|
|
347
|
-
availableChecks: z.array(refSchema),
|
|
348
|
-
gaps: z.array(text),
|
|
349
|
-
})
|
|
350
287
|
.strict();
|
|
351
|
-
export
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
signals: signalSet,
|
|
355
|
-
consequences: z.array(consequenceSchema),
|
|
356
|
-
verification: z.array(verificationSchema),
|
|
357
|
-
})
|
|
358
|
-
.strict();
|
|
359
|
-
export type Assessment = z.infer<typeof assessmentSchema>;
|
|
288
|
+
export type Judgment = z.infer<typeof judgmentSchema>;
|
|
289
|
+
/** A ≤6.x assessment as stored (facts/signals/…): kept verbatim, never written. */
|
|
290
|
+
export const legacyAssessmentSchema = z.looseObject({});
|
|
360
291
|
|
|
361
292
|
export const constraintSchema = z
|
|
362
293
|
.object({
|
|
@@ -714,7 +645,10 @@ export const taskRecordSchema = z
|
|
|
714
645
|
progress: progressSchema,
|
|
715
646
|
pauseReason: text.nullable().optional(),
|
|
716
647
|
runtime: runtimeSchema.optional(),
|
|
717
|
-
assessments
|
|
648
|
+
/** ≤6.x assessments, read-only history; judgments replace them. */
|
|
649
|
+
assessments: z.array(entrySchema(legacyAssessmentSchema)),
|
|
650
|
+
/** Listed in `critical` once written, so an older reader fails closed. */
|
|
651
|
+
judgments: z.array(entrySchema(judgmentSchema)).optional(),
|
|
718
652
|
policy: policySchema.nullable(),
|
|
719
653
|
policyChanges: z.array(policyChangeSchema),
|
|
720
654
|
candidates: z.array(candidateSchema),
|
|
@@ -969,14 +903,16 @@ const taskOperations = {
|
|
|
969
903
|
decisionIds: z.array(id).optional(),
|
|
970
904
|
}),
|
|
971
905
|
};
|
|
906
|
+
/** Judgment values as sent; core/policy/judgment.ts maps aliases and validates. */
|
|
907
|
+
const rawJudgment = z.record(z.string(), z.unknown());
|
|
972
908
|
const policyOperations = {
|
|
973
909
|
assess: operation({
|
|
974
910
|
action: z.literal("assess"),
|
|
975
911
|
...taskId,
|
|
976
912
|
expectedRevision: revision.optional(),
|
|
977
|
-
|
|
913
|
+
judgment: rawJudgment,
|
|
978
914
|
}),
|
|
979
|
-
preview: operation({ action: z.literal("preview"), ...taskId,
|
|
915
|
+
preview: operation({ action: z.literal("preview"), ...taskId, judgment: rawJudgment }),
|
|
980
916
|
explain: operation({ action: z.literal("explain"), ...taskId }),
|
|
981
917
|
};
|
|
982
918
|
const evidenceOperations = {
|
|
@@ -1015,7 +951,11 @@ const decisionOperations = {
|
|
|
1015
951
|
...taskId,
|
|
1016
952
|
expectedRevision: revision.optional(),
|
|
1017
953
|
purpose: decisionSchema.shape.purpose,
|
|
1018
|
-
|
|
954
|
+
// The engine binds the decision to the task and workspace it records on.
|
|
955
|
+
binding: decisionSchema.shape.binding.extend({
|
|
956
|
+
taskId: id.optional(),
|
|
957
|
+
workspaceId: id.optional(),
|
|
958
|
+
}),
|
|
1019
959
|
response: decisionSchema.shape.response,
|
|
1020
960
|
requirementIds: z.array(digest),
|
|
1021
961
|
}),
|
|
@@ -1071,6 +1011,16 @@ export const operationSchemas = {
|
|
|
1071
1011
|
state: z.discriminatedUnion("action", Object.values(stateOperations) as any),
|
|
1072
1012
|
} as const;
|
|
1073
1013
|
export type OperationRequest = z.infer<(typeof operationSchemas)[OperationFamily]>;
|
|
1014
|
+
/** Every action of each family, in contract order: the one action table. */
|
|
1015
|
+
export const OPERATION_ACTIONS: Record<OperationFamily, readonly string[]> = {
|
|
1016
|
+
task: Object.keys(taskOperations),
|
|
1017
|
+
policy: Object.keys(policyOperations),
|
|
1018
|
+
evidence: Object.keys(evidenceOperations),
|
|
1019
|
+
finding: Object.keys(findingOperations),
|
|
1020
|
+
decision: Object.keys(decisionOperations),
|
|
1021
|
+
worker: Object.keys(workerOperations),
|
|
1022
|
+
state: Object.keys(stateOperations),
|
|
1023
|
+
};
|
|
1074
1024
|
|
|
1075
1025
|
/**
|
|
1076
1026
|
* Schemas advertised to hosts. Every `taskId` is optional there: an operation
|
|
@@ -1186,145 +1136,6 @@ export function parseOperation(family: OperationFamily, input: unknown): Result<
|
|
|
1186
1136
|
return failure("invalid_input", "operation input is invalid", { fields });
|
|
1187
1137
|
}
|
|
1188
1138
|
|
|
1189
|
-
export function operationJsonSchema(family: OperationFamily): z.core.JSONSchema.BaseSchema {
|
|
1190
|
-
return z.toJSONSchema(advertisedOperationSchemas[family], { target: "draft-2020-12" });
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
/**
|
|
1194
|
-
* Advertised object depth shared by every host projection. Runtime validation
|
|
1195
|
-
* stays full-depth in parseOperation; this bound only keeps provider tool
|
|
1196
|
-
* schemas (e.g. Pi, MCP) within model nesting limits.
|
|
1197
|
-
*/
|
|
1198
|
-
export const OPERATION_SCHEMA_DEPTH = 1;
|
|
1199
|
-
|
|
1200
|
-
/** Provider nesting limit every advertised Workit schema must stay within. */
|
|
1201
|
-
export const OPERATION_SCHEMA_MAX_DEPTH = 10;
|
|
1202
|
-
|
|
1203
|
-
const schemaShapeDescription = (value: unknown, depth = 0): string => {
|
|
1204
|
-
if (!value || typeof value !== "object") return "value";
|
|
1205
|
-
const schema = value as Record<string, unknown>;
|
|
1206
|
-
if (Array.isArray(schema.oneOf) || Array.isArray(schema.anyOf)) {
|
|
1207
|
-
const options = (Array.isArray(schema.oneOf) ? schema.oneOf : schema.anyOf) as unknown[];
|
|
1208
|
-
return `(${options.map((option) => schemaShapeDescription(option, depth)).join("|")})`;
|
|
1209
|
-
}
|
|
1210
|
-
if (schema.const !== undefined) return `=${JSON.stringify(schema.const)}`;
|
|
1211
|
-
if (Array.isArray(schema.enum)) return `[${schema.enum.map(String).join("|")}]`;
|
|
1212
|
-
const type = Array.isArray(schema.type) ? schema.type.join("/") : schema.type;
|
|
1213
|
-
if (type === "array") return `arr<${schemaShapeDescription(schema.items, depth)}>`;
|
|
1214
|
-
if (type === "object" && schema.properties && typeof schema.properties === "object") {
|
|
1215
|
-
if (depth >= 2) return "obj";
|
|
1216
|
-
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
1217
|
-
const properties = schema.properties as Record<string, unknown>;
|
|
1218
|
-
const fields = Object.entries(properties).map(
|
|
1219
|
-
([name, child]) =>
|
|
1220
|
-
`${name}${required.has(name) ? "!" : ""}:${schemaShapeDescription(child, depth + 1)}`,
|
|
1221
|
-
);
|
|
1222
|
-
return `obj{${fields.join(",")}}`;
|
|
1223
|
-
}
|
|
1224
|
-
if (typeof type !== "string") return "?";
|
|
1225
|
-
return type
|
|
1226
|
-
.split("/")
|
|
1227
|
-
.map((part) => ({ string: "str", boolean: "bool", number: "num", null: "null" })[part] ?? part)
|
|
1228
|
-
.join("|");
|
|
1229
|
-
};
|
|
1230
|
-
|
|
1231
|
-
export const canonicalFieldsDescription = (
|
|
1232
|
-
value: string[] | Record<string, unknown> | z.ZodType,
|
|
1233
|
-
): string => {
|
|
1234
|
-
if (Array.isArray(value))
|
|
1235
|
-
return `Canonical object fields: ${value.join(", ")}. Workit validates the complete nested value.`;
|
|
1236
|
-
const isZodType = (schema: Record<string, unknown> | z.ZodType): schema is z.ZodType =>
|
|
1237
|
-
"_zod" in schema && typeof schema._zod === "object";
|
|
1238
|
-
const schema = isZodType(value)
|
|
1239
|
-
? (z.toJSONSchema(value, { target: "draft-2020-12" }) as Record<string, unknown>)
|
|
1240
|
-
: value;
|
|
1241
|
-
if (!schema.properties || typeof schema.properties !== "object") {
|
|
1242
|
-
const variants = Array.isArray(schema.oneOf)
|
|
1243
|
-
? schema.oneOf
|
|
1244
|
-
: Array.isArray(schema.anyOf)
|
|
1245
|
-
? schema.anyOf
|
|
1246
|
-
: null;
|
|
1247
|
-
return variants
|
|
1248
|
-
? `Canonical shapes: ${variants.map((variant) => schemaShapeDescription(variant)).join(" | ")}. Full nested value is validated.`
|
|
1249
|
-
: "Canonical nested value; Workit validates the complete value.";
|
|
1250
|
-
}
|
|
1251
|
-
const properties = schema.properties as Record<string, unknown>;
|
|
1252
|
-
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
1253
|
-
const fields = Object.entries(properties).map(
|
|
1254
|
-
([name, child]) => `${name}${required.has(name) ? "!" : ""}:${schemaShapeDescription(child)}`,
|
|
1255
|
-
);
|
|
1256
|
-
return `Fields (! required): ${fields.join(", ")}. Full nested value is validated.`;
|
|
1257
|
-
};
|
|
1258
|
-
|
|
1259
|
-
/**
|
|
1260
|
-
* Provider-safe projection of an operation JSON schema. Objects deeper than
|
|
1261
|
-
* maxDepth collapse to a described generic object that still names the
|
|
1262
|
-
* canonical fields, so models know what to send while providers accept the
|
|
1263
|
-
* shape. The full contract in operationJsonSchema is unchanged.
|
|
1264
|
-
*
|
|
1265
|
-
* With stringifiedObjects, every object, array, and primitive-const node also
|
|
1266
|
-
* accepts its JSON-encoded string form. Pi's transport stringifies nested
|
|
1267
|
-
* params before its own validation runs, so Pi publishes the tolerant shape
|
|
1268
|
-
* and decodes after validation; transports that carry real JSON stay strict.
|
|
1269
|
-
*/
|
|
1270
|
-
export function boundedOperationJsonSchema(
|
|
1271
|
-
family: OperationFamily,
|
|
1272
|
-
maxDepth: number = OPERATION_SCHEMA_DEPTH,
|
|
1273
|
-
stringifiedObjects = false,
|
|
1274
|
-
): Record<string, unknown> {
|
|
1275
|
-
const tolerate = (projected: Record<string, unknown>): Record<string, unknown> => {
|
|
1276
|
-
if (!stringifiedObjects) return projected;
|
|
1277
|
-
if (projected.properties && typeof projected.properties === "object")
|
|
1278
|
-
return { ...projected, type: ["object", "string"] };
|
|
1279
|
-
if (projected.items) return { ...projected, type: ["array", "string"] };
|
|
1280
|
-
if (
|
|
1281
|
-
"const" in projected &&
|
|
1282
|
-
(typeof projected.const === "number" || typeof projected.const === "boolean")
|
|
1283
|
-
)
|
|
1284
|
-
return { anyOf: [projected, { const: String(projected.const) }] };
|
|
1285
|
-
if (projected.type === "object") return { ...projected, type: ["object", "string"] };
|
|
1286
|
-
if (projected.type === "array") return { ...projected, type: ["array", "string"] };
|
|
1287
|
-
return projected;
|
|
1288
|
-
};
|
|
1289
|
-
const collapse = (node: unknown, currentDepth: number): unknown => {
|
|
1290
|
-
if (Array.isArray(node)) return node.map((item) => collapse(item, currentDepth));
|
|
1291
|
-
if (typeof node !== "object" || node === null) return node;
|
|
1292
|
-
const record = node as Record<string, unknown>;
|
|
1293
|
-
if (record.properties && typeof record.properties === "object") {
|
|
1294
|
-
const properties = record.properties as Record<string, unknown>;
|
|
1295
|
-
const fields = Object.keys(properties);
|
|
1296
|
-
if (currentDepth >= maxDepth || fields.length === 0) {
|
|
1297
|
-
const description = stringifiedObjects
|
|
1298
|
-
? `${canonicalFieldsDescription(record)} A JSON-encoded string is also accepted.`
|
|
1299
|
-
: canonicalFieldsDescription(record);
|
|
1300
|
-
return tolerate({ type: "object", description });
|
|
1301
|
-
}
|
|
1302
|
-
return tolerate({
|
|
1303
|
-
...record,
|
|
1304
|
-
properties: Object.fromEntries(
|
|
1305
|
-
Object.entries(record.properties as Record<string, unknown>).map(([key, child]) => [
|
|
1306
|
-
key,
|
|
1307
|
-
collapse(child, currentDepth + 1),
|
|
1308
|
-
]),
|
|
1309
|
-
),
|
|
1310
|
-
});
|
|
1311
|
-
}
|
|
1312
|
-
if (record.items) return tolerate({ ...record, items: collapse(record.items, currentDepth) });
|
|
1313
|
-
const composed = (["anyOf", "oneOf", "allOf"] as const).filter((key) =>
|
|
1314
|
-
Array.isArray(record[key]),
|
|
1315
|
-
);
|
|
1316
|
-
if (composed.length > 0) {
|
|
1317
|
-
const projected: Record<string, unknown> = { ...record };
|
|
1318
|
-
for (const key of composed)
|
|
1319
|
-
projected[key] = (record[key] as unknown[]).map((item) => collapse(item, currentDepth));
|
|
1320
|
-
return projected;
|
|
1321
|
-
}
|
|
1322
|
-
if ("const" in record) return tolerate({ ...record });
|
|
1323
|
-
return node;
|
|
1324
|
-
};
|
|
1325
|
-
return collapse(operationJsonSchema(family), 0) as Record<string, unknown>;
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
1139
|
type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
|
1329
1140
|
function canonical(value: unknown, seen: Set<object>): JsonValue {
|
|
1330
1141
|
if (value === null || typeof value === "boolean" || typeof value === "string") {
|
package/src/core/task-engine.ts
CHANGED
|
@@ -7,16 +7,16 @@ import {
|
|
|
7
7
|
failure,
|
|
8
8
|
decisionDigest,
|
|
9
9
|
exportBundleSchema,
|
|
10
|
-
assessmentSchema,
|
|
11
10
|
decisionSchema,
|
|
12
11
|
evidenceSchema,
|
|
13
12
|
findingSchema,
|
|
14
13
|
intentSchema,
|
|
14
|
+
judgmentSchema,
|
|
15
|
+
legacyAssessmentSchema,
|
|
15
16
|
newId,
|
|
16
17
|
parseOperation,
|
|
17
18
|
rewriteRecordRefs,
|
|
18
19
|
success,
|
|
19
|
-
type Assessment,
|
|
20
20
|
type Candidate,
|
|
21
21
|
type Capability,
|
|
22
22
|
type Caller,
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type Evidence,
|
|
28
28
|
type ExportBundle,
|
|
29
29
|
type Finding,
|
|
30
|
+
type Judgment,
|
|
30
31
|
type Policy,
|
|
31
32
|
provenanceSchema,
|
|
32
33
|
type Ref,
|
|
@@ -45,6 +46,11 @@ import {
|
|
|
45
46
|
scopeCovers as bindingCovers,
|
|
46
47
|
} from "./task-contract";
|
|
47
48
|
import { diffPolicy, resolvePolicy } from "./policy-resolver";
|
|
49
|
+
import { normalizeOperationInput } from "./operation-input";
|
|
50
|
+
import { latestJudgment } from "./policy/derive";
|
|
51
|
+
import { normalizeJudgment } from "./policy/judgment";
|
|
52
|
+
import { resolveAutonomy, type VerificationMode } from "../autonomy";
|
|
53
|
+
|
|
48
54
|
import { sameDirectoryIdentity, TaskStore } from "./task-store";
|
|
49
55
|
import { checkoutRootOf } from "../store/paths";
|
|
50
56
|
import { defaultLockTimeout } from "./store-lock";
|
|
@@ -76,6 +82,15 @@ import {
|
|
|
76
82
|
type CandidateEnvironment,
|
|
77
83
|
} from "./task-evaluation";
|
|
78
84
|
|
|
85
|
+
/** The user-config verification mode for `root`; `self` when it cannot be read. */
|
|
86
|
+
const workspaceVerification = (root: string): VerificationMode => {
|
|
87
|
+
try {
|
|
88
|
+
return resolveAutonomy(root).verification;
|
|
89
|
+
} catch {
|
|
90
|
+
return "self";
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
79
94
|
export type OperationContext = {
|
|
80
95
|
root: string;
|
|
81
96
|
caller: Caller;
|
|
@@ -429,29 +444,8 @@ const mapRef = (ref: Ref, ids: Map<string, string>): Ref => {
|
|
|
429
444
|
return { ...ref, id: ids.get(ref.id) ?? ref.id };
|
|
430
445
|
};
|
|
431
446
|
|
|
432
|
-
const portableRefs = (refs: Ref[]): Ref[] =>
|
|
433
|
-
refs.filter((ref) => ref.kind === "file" || ref.kind === "record");
|
|
434
447
|
const portableRef = (ref: Ref | null): Ref | null =>
|
|
435
448
|
ref && (ref.kind === "file" || ref.kind === "record") ? ref : null;
|
|
436
|
-
const portableFact = (fact: Assessment["facts"][number]) => {
|
|
437
|
-
const refs = portableRefs(fact.refs);
|
|
438
|
-
return fact.basis === "observed" && refs.length === 0
|
|
439
|
-
? { ...fact, basis: "unknown" as const, refs }
|
|
440
|
-
: { ...fact, refs };
|
|
441
|
-
};
|
|
442
|
-
const portableSignal = (signal: Assessment["signals"][keyof Assessment["signals"]]) => {
|
|
443
|
-
const refs = portableRefs(signal.refs);
|
|
444
|
-
return signal.basis === "observed" && refs.length === 0
|
|
445
|
-
? {
|
|
446
|
-
...signal,
|
|
447
|
-
value: "unknown" as const,
|
|
448
|
-
basis: "unknown" as const,
|
|
449
|
-
reason: "portable supporting reference was removed",
|
|
450
|
-
refs,
|
|
451
|
-
}
|
|
452
|
-
: { ...signal, refs };
|
|
453
|
-
};
|
|
454
|
-
|
|
455
449
|
const importedProvenance = (context: OperationContext): Provenance => ({
|
|
456
450
|
kind: "imported",
|
|
457
451
|
host: context.caller.host,
|
|
@@ -467,21 +461,6 @@ const portableTask = (task: TaskRecord): TaskRecord => {
|
|
|
467
461
|
if (typeof stripped !== "object" || stripped === null || Array.isArray(stripped))
|
|
468
462
|
throw new TypeError("portable task rewrite produced a non-record");
|
|
469
463
|
const clone = structuredClone(stripped) as TaskRecord;
|
|
470
|
-
const demote = (
|
|
471
|
-
facts: Assessment["facts"][number][],
|
|
472
|
-
signals: Assessment["signals"],
|
|
473
|
-
consequences: Assessment["consequences"],
|
|
474
|
-
): void => {
|
|
475
|
-
for (const fact of facts) Object.assign(fact, portableFact(fact));
|
|
476
|
-
for (const name of Object.keys(signals) as (keyof Assessment["signals"])[]) {
|
|
477
|
-
const signal = signals[name];
|
|
478
|
-
Object.assign(signal, portableSignal(signal));
|
|
479
|
-
}
|
|
480
|
-
for (const consequence of consequences)
|
|
481
|
-
Object.assign(consequence.fact, portableFact(consequence.fact));
|
|
482
|
-
};
|
|
483
|
-
for (const entry of clone.assessments)
|
|
484
|
-
demote(entry.data.facts, entry.data.signals, entry.data.consequences);
|
|
485
464
|
// Candidate metadata can contain environment-derived paths and digests. The destination
|
|
486
465
|
// must recapture its own candidate instead of receiving source checkout material.
|
|
487
466
|
clone.candidates = [];
|
|
@@ -506,6 +485,7 @@ const importedTask = (
|
|
|
506
485
|
for (const entry of [
|
|
507
486
|
source.intent,
|
|
508
487
|
...source.assessments,
|
|
488
|
+
...(source.judgments ?? []),
|
|
509
489
|
...source.evidence,
|
|
510
490
|
...source.decisions,
|
|
511
491
|
...source.findings,
|
|
@@ -529,9 +509,16 @@ const importedTask = (
|
|
|
529
509
|
id: fresh(entry.id),
|
|
530
510
|
recordedAt: timestamp,
|
|
531
511
|
provenance: imported,
|
|
532
|
-
data: rewriteRecordRefs(data,
|
|
512
|
+
data: rewriteRecordRefs(data, legacyAssessmentSchema, remap) as typeof data,
|
|
533
513
|
};
|
|
534
514
|
});
|
|
515
|
+
const judgments = source.judgments?.map((entry) => ({
|
|
516
|
+
...entry,
|
|
517
|
+
id: fresh(entry.id),
|
|
518
|
+
recordedAt: timestamp,
|
|
519
|
+
provenance: imported,
|
|
520
|
+
data: rewriteRecordRefs(entry.data, judgmentSchema, remap) as typeof entry.data,
|
|
521
|
+
}));
|
|
535
522
|
const evidence = source.evidence.map((entry) => ({
|
|
536
523
|
...entry,
|
|
537
524
|
id: fresh(entry.id),
|
|
@@ -603,6 +590,7 @@ const importedTask = (
|
|
|
603
590
|
status: "paused",
|
|
604
591
|
closure: null,
|
|
605
592
|
assessments,
|
|
593
|
+
...(judgments ? { judgments } : {}),
|
|
606
594
|
evidence,
|
|
607
595
|
decisions: decisions.map((entry) => {
|
|
608
596
|
const data = rewriteRecordRefs(entry.data, decisionSchema, remap) as typeof entry.data;
|
|
@@ -700,7 +688,9 @@ export class WorkitCore {
|
|
|
700
688
|
* this checkout's branch (D3). Recording actions create that task on first
|
|
701
689
|
* use; reads and lifecycle actions only look it up. Helpers never create.
|
|
702
690
|
*/
|
|
703
|
-
private implicitTask(family: OperationFamily,
|
|
691
|
+
private implicitTask(family: OperationFamily, raw: unknown): Result<unknown> {
|
|
692
|
+
// Flat tool inputs become the canonical request first (S17).
|
|
693
|
+
const request = normalizeOperationInput(family, raw, this.context.caller.host);
|
|
704
694
|
if (typeof request !== "object" || request === null || Array.isArray(request))
|
|
705
695
|
return success(null, null, request);
|
|
706
696
|
const value = request as { action?: unknown; taskId?: unknown };
|
|
@@ -969,7 +959,10 @@ export class WorkitCore {
|
|
|
969
959
|
if ((this.context.workerId ?? null) !== null)
|
|
970
960
|
return failure("permission_denied", "helpers cannot change task requirements");
|
|
971
961
|
this.fillRevisions(input, task.data);
|
|
972
|
-
const
|
|
962
|
+
const judged = normalizeJudgment(input.judgment, latestJudgment(task.data));
|
|
963
|
+
if (!judged.ok) return judged;
|
|
964
|
+
const judgment = judged.data.judgment;
|
|
965
|
+
const resolved = this.resolve(task.data, judgment);
|
|
973
966
|
if (!resolved.ok) return resolved;
|
|
974
967
|
if (input.action === "preview") return success(null, null, resolved.data);
|
|
975
968
|
if (input.action !== "assess")
|
|
@@ -986,16 +979,18 @@ export class WorkitCore {
|
|
|
986
979
|
"policy reassessed",
|
|
987
980
|
mutation.now,
|
|
988
981
|
);
|
|
989
|
-
const
|
|
982
|
+
const entry = {
|
|
990
983
|
id: newId(),
|
|
991
984
|
recordedAt: mutation.now,
|
|
992
985
|
provenance: provenance(this.context),
|
|
993
|
-
data:
|
|
986
|
+
data: judgment,
|
|
994
987
|
};
|
|
995
988
|
const next = {
|
|
996
989
|
...current,
|
|
997
990
|
constraints: this.context.constraints,
|
|
998
|
-
|
|
991
|
+
judgments: [...(current.judgments ?? []), entry],
|
|
992
|
+
// Older readers would drop judgments on rewrite: they fail closed instead.
|
|
993
|
+
critical: [...new Set([...(current.critical ?? []), "judgments"])],
|
|
999
994
|
policy: resolved.data,
|
|
1000
995
|
policyChanges: nextChange
|
|
1001
996
|
? [...current.policyChanges, nextChange]
|
|
@@ -1270,9 +1265,15 @@ export class WorkitCore {
|
|
|
1270
1265
|
const workspace = this.store.readWorkspace();
|
|
1271
1266
|
if (!workspace.ok) return workspace;
|
|
1272
1267
|
if (!workspace.data) return failure("not_found", "workspace not found");
|
|
1273
|
-
|
|
1268
|
+
// Flat inputs omit the binding ids: a decision binds to where it is recorded.
|
|
1269
|
+
const binding = {
|
|
1270
|
+
...input.binding,
|
|
1271
|
+
taskId: input.binding.taskId ?? task.data.id,
|
|
1272
|
+
workspaceId: input.binding.workspaceId ?? workspace.data.id,
|
|
1273
|
+
} as Decision["binding"];
|
|
1274
|
+
if (binding.taskId !== task.data.id || binding.workspaceId !== workspace.data.id)
|
|
1274
1275
|
return failure("invalid_input", "decision task or workspace binding is invalid");
|
|
1275
|
-
const content = verifyDecisionContentAtRoot(this.store.root,
|
|
1276
|
+
const content = verifyDecisionContentAtRoot(this.store.root, binding);
|
|
1276
1277
|
if (!content.ok) return content;
|
|
1277
1278
|
const knownRequirements = new Set(
|
|
1278
1279
|
task.data.policy?.requirements.map((item) => item.id) ?? [],
|
|
@@ -1281,7 +1282,7 @@ export class WorkitCore {
|
|
|
1281
1282
|
return failure("invalid_input", "decision references an unknown requirement");
|
|
1282
1283
|
const base = {
|
|
1283
1284
|
purpose: input.purpose,
|
|
1284
|
-
binding
|
|
1285
|
+
binding,
|
|
1285
1286
|
response: input.response,
|
|
1286
1287
|
requirementIds: input.requirementIds,
|
|
1287
1288
|
revoked: null,
|
|
@@ -1765,20 +1766,12 @@ export class WorkitCore {
|
|
|
1765
1766
|
});
|
|
1766
1767
|
}
|
|
1767
1768
|
|
|
1768
|
-
private resolve(task: TaskRecord,
|
|
1769
|
+
private resolve(task: TaskRecord, judgment: Judgment): Result<Policy> {
|
|
1769
1770
|
return resolvePolicy({
|
|
1770
1771
|
intent: task.intent.data,
|
|
1771
|
-
|
|
1772
|
+
judgment,
|
|
1772
1773
|
constraints: this.context.constraints,
|
|
1773
|
-
|
|
1774
|
-
decisions: task.decisions
|
|
1775
|
-
.filter((entry) => entry.provenance.kind !== "imported")
|
|
1776
|
-
.map((entry) => entry.data),
|
|
1777
|
-
findings: task.findings
|
|
1778
|
-
.filter((entry) => entry.provenance.kind !== "imported")
|
|
1779
|
-
.map((entry) => entry.data),
|
|
1780
|
-
requirements: task.origin || task.policy === null ? [] : task.policy.requirements,
|
|
1781
|
-
},
|
|
1774
|
+
verification: workspaceVerification(this.context.root),
|
|
1782
1775
|
});
|
|
1783
1776
|
}
|
|
1784
1777
|
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from "../check-config";
|
|
35
35
|
import { worktreeSignal, worktreeTree } from "../git/rev";
|
|
36
36
|
import { runtimeVersion } from "./task-store";
|
|
37
|
+
import { derivedRuleStatus, ledgerView } from "./policy/derive";
|
|
37
38
|
|
|
38
39
|
export type CandidateEnvironment =
|
|
39
40
|
| readonly (string | { name: string; value?: string | null })[]
|
|
@@ -556,7 +557,7 @@ export const verifyDecisionContentAtRoot = (
|
|
|
556
557
|
return success(null, null, null);
|
|
557
558
|
};
|
|
558
559
|
|
|
559
|
-
const applicableDecision = (
|
|
560
|
+
export const applicableDecision = (
|
|
560
561
|
task: TaskRecord,
|
|
561
562
|
workspace: WorkspaceRecord,
|
|
562
563
|
requirement: Requirement,
|
|
@@ -729,7 +730,40 @@ export function evaluateRequirements(
|
|
|
729
730
|
decisionIds: [],
|
|
730
731
|
reason: "stored policy version is unsupported; reassessment is required",
|
|
731
732
|
}));
|
|
733
|
+
const ledger = ledgerView(checkoutRoot);
|
|
732
734
|
return (task.policy?.requirements ?? []).map((requirement) => {
|
|
735
|
+
// Verdict, product-decision and plan rules read the ledger or the plan (S17).
|
|
736
|
+
const derived = derivedRuleStatus(task, requirement, checkoutRoot, ledger);
|
|
737
|
+
if (derived) {
|
|
738
|
+
if (derived.met)
|
|
739
|
+
return {
|
|
740
|
+
requirementId: requirement.id,
|
|
741
|
+
status: "satisfied" as const,
|
|
742
|
+
evidenceIds: [],
|
|
743
|
+
decisionIds: derived.decisionIds,
|
|
744
|
+
reason: derived.reason,
|
|
745
|
+
};
|
|
746
|
+
const waived = requirement.acceptanceAllowed
|
|
747
|
+
? applicableDecision(task, workspace, requirement, checkoutRoot)
|
|
748
|
+
: [];
|
|
749
|
+
if (waived.length)
|
|
750
|
+
return {
|
|
751
|
+
requirementId: requirement.id,
|
|
752
|
+
status: "accepted_limitation" as const,
|
|
753
|
+
evidenceIds: [],
|
|
754
|
+
decisionIds: waived.map(
|
|
755
|
+
(decision) => task.decisions.find((entry) => entry.data === decision)!.id,
|
|
756
|
+
),
|
|
757
|
+
reason: "an applicable approved limitation permits the missing evidence",
|
|
758
|
+
};
|
|
759
|
+
return {
|
|
760
|
+
requirementId: requirement.id,
|
|
761
|
+
status: "unsatisfied" as const,
|
|
762
|
+
evidenceIds: [],
|
|
763
|
+
decisionIds: [],
|
|
764
|
+
reason: derived.reason,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
733
767
|
const gate = observedGate(requirement)
|
|
734
768
|
? evaluateObservedGate(task, requirement, statuses, configFor())
|
|
735
769
|
: null;
|
package/src/core/workspaces.ts
CHANGED
|
@@ -121,6 +121,8 @@ const workspaceConfigSchema = z
|
|
|
121
121
|
autoApprove: z.union([z.boolean(), z.array(nonBlank)]).optional(),
|
|
122
122
|
/** Where an unnamed request stops (skills read it); same rules as grants. */
|
|
123
123
|
defaultEndpoint: z.enum(["commit", "pr"]).optional(),
|
|
124
|
+
/** How a normal-risk behavior change is verified (S17); same rules as grants. */
|
|
125
|
+
verification: z.enum(["self", "independent"]).optional(),
|
|
124
126
|
/** Autonomy grants (D15): only ever set here, by the user. */
|
|
125
127
|
autonomy: z
|
|
126
128
|
.object(
|
package/src/core.ts
CHANGED
|
@@ -8,11 +8,6 @@ export {
|
|
|
8
8
|
OPERATION_FAMILIES,
|
|
9
9
|
operationSchemas,
|
|
10
10
|
advertisedOperationSchemas,
|
|
11
|
-
operationJsonSchema,
|
|
12
|
-
boundedOperationJsonSchema,
|
|
13
|
-
OPERATION_SCHEMA_DEPTH,
|
|
14
|
-
OPERATION_SCHEMA_MAX_DEPTH,
|
|
15
|
-
canonicalFieldsDescription,
|
|
16
11
|
parseOperation,
|
|
17
12
|
success,
|
|
18
13
|
failure,
|
|
@@ -25,6 +20,13 @@ export {
|
|
|
25
20
|
requirementId,
|
|
26
21
|
rewriteRecordRefs,
|
|
27
22
|
} from "./core/task-contract";
|
|
23
|
+
export {
|
|
24
|
+
flatOperationJsonSchema,
|
|
25
|
+
normalizeOperationInput,
|
|
26
|
+
operationDescription,
|
|
27
|
+
operationJsonSchema,
|
|
28
|
+
parseAdvertisedOperation,
|
|
29
|
+
} from "./core/operation-input";
|
|
28
30
|
export type {
|
|
29
31
|
OperationFamily,
|
|
30
32
|
OperationRequest,
|
|
@@ -32,7 +34,7 @@ export type {
|
|
|
32
34
|
Caller,
|
|
33
35
|
Scope,
|
|
34
36
|
Ref,
|
|
35
|
-
|
|
37
|
+
Judgment,
|
|
36
38
|
Dimension,
|
|
37
39
|
Requirement,
|
|
38
40
|
Constraint,
|