@brainervirus/workit-core 1.3.0 → 1.3.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/package.json +1 -1
- package/src/core/branch.ts +1 -1
- package/src/core/external-action.ts +8 -1
- package/src/core/methods.ts +14 -0
- package/src/core/policy-resolver.ts +13 -5
- package/src/core/repo-context.ts +1 -1
- package/src/core/task-contract.ts +59 -5
- package/src/core/workspaces.ts +39 -33
- package/src/core/youtrack-tools.ts +4 -4
- package/src/core/youtrack.ts +1 -1
- package/src/core.ts +2 -0
package/package.json
CHANGED
package/src/core/branch.ts
CHANGED
|
@@ -711,7 +711,7 @@ export const branchSetup = ({
|
|
|
711
711
|
if (classifyBranchDirt(cwd) === "stash-required" && stash !== "yes") {
|
|
712
712
|
return {
|
|
713
713
|
error:
|
|
714
|
-
"dirty working tree —
|
|
714
|
+
"dirty working tree — obtain native host approval before stashing and setting up the branch",
|
|
715
715
|
phase: "preflight",
|
|
716
716
|
};
|
|
717
717
|
}
|
|
@@ -56,7 +56,7 @@ type WithExternalActionLock = <T>(
|
|
|
56
56
|
operation: () => Promise<ExternalActionResult<T>>,
|
|
57
57
|
) => Promise<ExternalActionResult<T>>;
|
|
58
58
|
|
|
59
|
-
const externalActionSchema = z.discriminatedUnion("operation", [
|
|
59
|
+
export const externalActionSchema = z.discriminatedUnion("operation", [
|
|
60
60
|
z
|
|
61
61
|
.object({
|
|
62
62
|
operation: z.literal("git.branch_setup"),
|
|
@@ -209,6 +209,13 @@ const externalActionSchema = z.discriminatedUnion("operation", [
|
|
|
209
209
|
]);
|
|
210
210
|
export type ExternalActionRequest = z.infer<typeof externalActionSchema>;
|
|
211
211
|
|
|
212
|
+
/** The complete external-action contract for hosts that register native tools. */
|
|
213
|
+
export const externalActionJsonSchema = (): z.core.JSONSchema.BaseSchema =>
|
|
214
|
+
({
|
|
215
|
+
type: "object",
|
|
216
|
+
...z.toJSONSchema(externalActionSchema, { target: "draft-2020-12" }),
|
|
217
|
+
}) as z.core.JSONSchema.BaseSchema;
|
|
218
|
+
|
|
212
219
|
export type ExternalActionOperation = ExternalActionRequest["operation"];
|
|
213
220
|
|
|
214
221
|
export const externalActionRequest = (value: unknown): Result<ExternalActionRequest> => {
|
package/src/core/methods.ts
CHANGED
|
@@ -95,6 +95,15 @@ need zero Workit task, assessment, or writer calls. Use one compact tracked
|
|
|
95
95
|
record only when handoff, dependent steps, concurrent actors, or meaningful
|
|
96
96
|
decisions make continuity useful; similar titles alone never merge tasks.
|
|
97
97
|
|
|
98
|
+
For a routine user-authorized branch or commit, prefer native host Git/shell
|
|
99
|
+
tools from the outset when managed coordination or outcome reconciliation is
|
|
100
|
+
not needed. Inspect the actual target checkout and its configured conventions;
|
|
101
|
+
use Workit's read-only context/policy tools when needed. Do not start a task,
|
|
102
|
+
acquire a writer, or mint a decision receipt solely for a native Git action.
|
|
103
|
+
Native permissions still apply. Never switch execution paths to evade a denial
|
|
104
|
+
or repeat an uncertain managed effect. A local-commit endpoint does not imply
|
|
105
|
+
PR readiness, a fresh review, or post-commit assessment/closure paperwork.
|
|
106
|
+
|
|
98
107
|
For tracked work, inspect its current state and use the shared operations for
|
|
99
108
|
task, policy, evidence, finding, decision, worker, writer, and state changes.
|
|
100
109
|
Start a record once for an explicit tracked objective; assess or reassess only
|
|
@@ -116,6 +125,11 @@ Preserve uncertain external outcomes and reconcile repository/provider state
|
|
|
116
125
|
before retrying. Internal reservations prevent duplicate or ambiguous effects;
|
|
117
126
|
they are not permission tickets for every edit.
|
|
118
127
|
|
|
128
|
+
On OpenCode V1 and V2, use native host tools for external mutations. Workit
|
|
129
|
+
provides read-only workit_context; it has no managed external-action executor.
|
|
130
|
+
Do not create proposals or request Workit approvals merely to invoke native
|
|
131
|
+
tools. Old uncertain managed effects still require evidence before any retry.
|
|
132
|
+
|
|
119
133
|
Use host-native authorization through the host's supported path. A native
|
|
120
134
|
question receipt records an actual question interaction; it is not automatic
|
|
121
135
|
host permission. Record a meaningful user choice once with provenance when
|
|
@@ -326,6 +326,11 @@ function resolveRequirements(input: NormalizedResolverInput): Requirement[] {
|
|
|
326
326
|
}),
|
|
327
327
|
);
|
|
328
328
|
|
|
329
|
+
const independentBehaviorReview =
|
|
330
|
+
input.preferences?.thorough === true ||
|
|
331
|
+
input.assessment.consequences.some((consequence) =>
|
|
332
|
+
["security", "data", "public_contract", "operations"].includes(consequence.area),
|
|
333
|
+
);
|
|
329
334
|
if (signals.behaviorChange.value === true)
|
|
330
335
|
requirements.push(
|
|
331
336
|
requirement({
|
|
@@ -340,15 +345,18 @@ function resolveRequirements(input: NormalizedResolverInput): Requirement[] {
|
|
|
340
345
|
acceptanceAllowed: false,
|
|
341
346
|
}),
|
|
342
347
|
requirement({
|
|
343
|
-
ruleId: "fresh-context-review",
|
|
348
|
+
ruleId: independentBehaviorReview ? "fresh-context-review" : "self-review",
|
|
344
349
|
dimension: "review",
|
|
345
350
|
scope,
|
|
346
|
-
reason:
|
|
347
|
-
|
|
348
|
-
"
|
|
351
|
+
reason: independentBehaviorReview
|
|
352
|
+
? "Consequential behavior or the thorough preference requires fresh-context review."
|
|
353
|
+
: "Bounded behavior changes use self-review unless consequential risks or trusted rules require more.",
|
|
354
|
+
satisfaction: independentBehaviorReview
|
|
355
|
+
? "A separate review context examines the current candidate and records its result."
|
|
356
|
+
: "The lead reviews the resulting diff and records the relevant scope and checks.",
|
|
349
357
|
before: "close",
|
|
350
358
|
dependentAction: null,
|
|
351
|
-
acceptanceAllowed:
|
|
359
|
+
acceptanceAllowed: !independentBehaviorReview,
|
|
352
360
|
}),
|
|
353
361
|
);
|
|
354
362
|
else if (signals.mechanicalLowRisk.value === true)
|
package/src/core/repo-context.ts
CHANGED
|
@@ -363,7 +363,7 @@ const CHANGELOG_RULES = `- Use an [Unreleased] section.
|
|
|
363
363
|
- Entries should be human-readable and user-facing.
|
|
364
364
|
- Do not use raw commit messages as changelog bullets.
|
|
365
365
|
- MERGE into existing ### Category under [Unreleased] — never append a second ### Added / ### Fixed block.
|
|
366
|
-
- Apply with the native
|
|
366
|
+
- Apply with the host-native editor, preserving the repository changelog conventions.
|
|
367
367
|
- If Unreleased already has duplicate category headings, normalize_only first.`;
|
|
368
368
|
|
|
369
369
|
/** Port of changelog-context.sh — changelog update context. */
|
|
@@ -1140,8 +1140,61 @@ export const OPERATION_SCHEMA_DEPTH = 1;
|
|
|
1140
1140
|
/** Provider nesting limit every advertised Workit schema must stay within. */
|
|
1141
1141
|
export const OPERATION_SCHEMA_MAX_DEPTH = 10;
|
|
1142
1142
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1143
|
+
const schemaShapeDescription = (value: unknown, depth = 0): string => {
|
|
1144
|
+
if (!value || typeof value !== "object") return "value";
|
|
1145
|
+
const schema = value as Record<string, unknown>;
|
|
1146
|
+
if (Array.isArray(schema.oneOf) || Array.isArray(schema.anyOf)) {
|
|
1147
|
+
const options = (Array.isArray(schema.oneOf) ? schema.oneOf : schema.anyOf) as unknown[];
|
|
1148
|
+
return `(${options.map((option) => schemaShapeDescription(option, depth)).join("|")})`;
|
|
1149
|
+
}
|
|
1150
|
+
if (schema.const !== undefined) return `=${JSON.stringify(schema.const)}`;
|
|
1151
|
+
if (Array.isArray(schema.enum)) return `[${schema.enum.map(String).join("|")}]`;
|
|
1152
|
+
const type = Array.isArray(schema.type) ? schema.type.join("/") : schema.type;
|
|
1153
|
+
if (type === "array") return `arr<${schemaShapeDescription(schema.items, depth)}>`;
|
|
1154
|
+
if (type === "object" && schema.properties && typeof schema.properties === "object") {
|
|
1155
|
+
if (depth >= 2) return "obj";
|
|
1156
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
1157
|
+
const properties = schema.properties as Record<string, unknown>;
|
|
1158
|
+
const fields = Object.entries(properties).map(
|
|
1159
|
+
([name, child]) =>
|
|
1160
|
+
`${name}${required.has(name) ? "!" : ""}:${schemaShapeDescription(child, depth + 1)}`,
|
|
1161
|
+
);
|
|
1162
|
+
return `obj{${fields.join(",")}}`;
|
|
1163
|
+
}
|
|
1164
|
+
if (typeof type !== "string") return "?";
|
|
1165
|
+
return type
|
|
1166
|
+
.split("/")
|
|
1167
|
+
.map((part) => ({ string: "str", boolean: "bool", number: "num", null: "null" })[part] ?? part)
|
|
1168
|
+
.join("|");
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
export const canonicalFieldsDescription = (
|
|
1172
|
+
value: string[] | Record<string, unknown> | z.ZodType,
|
|
1173
|
+
): string => {
|
|
1174
|
+
if (Array.isArray(value))
|
|
1175
|
+
return `Canonical object fields: ${value.join(", ")}. Workit validates the complete nested value.`;
|
|
1176
|
+
const isZodType = (schema: Record<string, unknown> | z.ZodType): schema is z.ZodType =>
|
|
1177
|
+
"_zod" in schema && typeof schema._zod === "object";
|
|
1178
|
+
const schema = isZodType(value)
|
|
1179
|
+
? (z.toJSONSchema(value, { target: "draft-2020-12" }) as Record<string, unknown>)
|
|
1180
|
+
: value;
|
|
1181
|
+
if (!schema.properties || typeof schema.properties !== "object") {
|
|
1182
|
+
const variants = Array.isArray(schema.oneOf)
|
|
1183
|
+
? schema.oneOf
|
|
1184
|
+
: Array.isArray(schema.anyOf)
|
|
1185
|
+
? schema.anyOf
|
|
1186
|
+
: null;
|
|
1187
|
+
return variants
|
|
1188
|
+
? `Canonical shapes: ${variants.map((variant) => schemaShapeDescription(variant)).join(" | ")}. Full nested value is validated.`
|
|
1189
|
+
: "Canonical nested value; Workit validates the complete value.";
|
|
1190
|
+
}
|
|
1191
|
+
const properties = schema.properties as Record<string, unknown>;
|
|
1192
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
1193
|
+
const fields = Object.entries(properties).map(
|
|
1194
|
+
([name, child]) => `${name}${required.has(name) ? "!" : ""}:${schemaShapeDescription(child)}`,
|
|
1195
|
+
);
|
|
1196
|
+
return `Fields (! required): ${fields.join(", ")}. Full nested value is validated.`;
|
|
1197
|
+
};
|
|
1145
1198
|
|
|
1146
1199
|
/**
|
|
1147
1200
|
* Provider-safe projection of an operation JSON schema. Objects deeper than
|
|
@@ -1178,11 +1231,12 @@ export function boundedOperationJsonSchema(
|
|
|
1178
1231
|
if (typeof node !== "object" || node === null) return node;
|
|
1179
1232
|
const record = node as Record<string, unknown>;
|
|
1180
1233
|
if (record.properties && typeof record.properties === "object") {
|
|
1181
|
-
const
|
|
1234
|
+
const properties = record.properties as Record<string, unknown>;
|
|
1235
|
+
const fields = Object.keys(properties);
|
|
1182
1236
|
if (currentDepth >= maxDepth || fields.length === 0) {
|
|
1183
1237
|
const description = stringifiedObjects
|
|
1184
|
-
? `${canonicalFieldsDescription(
|
|
1185
|
-
: canonicalFieldsDescription(
|
|
1238
|
+
? `${canonicalFieldsDescription(record)} A JSON-encoded string is also accepted.`
|
|
1239
|
+
: canonicalFieldsDescription(record);
|
|
1186
1240
|
return tolerate({ type: "object", description });
|
|
1187
1241
|
}
|
|
1188
1242
|
return tolerate({
|
package/src/core/workspaces.ts
CHANGED
|
@@ -409,6 +409,43 @@ const canonicalGlob = (glob: string): string => {
|
|
|
409
409
|
return real + (prefix.endsWith("/") ? "/" : "") + rest;
|
|
410
410
|
};
|
|
411
411
|
|
|
412
|
+
const globSpecificity = (glob: string): number =>
|
|
413
|
+
glob
|
|
414
|
+
.replaceAll("\\", "/")
|
|
415
|
+
.split("/")
|
|
416
|
+
.filter((segment) => segment.length > 0 && !/[*?[\]{]/.test(segment)).length;
|
|
417
|
+
|
|
418
|
+
const selectWorkspaceMatch = <T extends { name: string; glob: string }>(
|
|
419
|
+
matches: T[],
|
|
420
|
+
cwd: string,
|
|
421
|
+
workspaceName?: string,
|
|
422
|
+
): T | undefined => {
|
|
423
|
+
if (workspaceName !== undefined) {
|
|
424
|
+
const named = matches.filter((entry) => entry.name === workspaceName);
|
|
425
|
+
if (named.length === 1) return named[0];
|
|
426
|
+
if (named.length > 1)
|
|
427
|
+
throw new Error(`ambiguous workspace ${JSON.stringify(workspaceName)} for ${cwd}`);
|
|
428
|
+
if (matches.length > 0) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}; matching choices: ${matches.map((entry) => entry.name).join(", ")}`,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
throw new Error(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (matches.length < 2) return matches[0];
|
|
437
|
+
const highestSpecificity = Math.max(...matches.map((entry) => globSpecificity(entry.glob)));
|
|
438
|
+
const mostSpecific = matches.filter(
|
|
439
|
+
(entry) => globSpecificity(entry.glob) === highestSpecificity,
|
|
440
|
+
);
|
|
441
|
+
if (mostSpecific.length > 1) {
|
|
442
|
+
throw new Error(
|
|
443
|
+
`ambiguous workspace for ${cwd}; choose one of: ${mostSpecific.map((entry) => entry.name).join(", ")}`,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
return mostSpecific[0];
|
|
447
|
+
};
|
|
448
|
+
|
|
412
449
|
/** Match a cwd against the workspaces.json under an explicit config dir. */
|
|
413
450
|
export const resolveWorkspaceFrom = (
|
|
414
451
|
cwd: string,
|
|
@@ -440,23 +477,7 @@ export const resolveWorkspaceFrom = (
|
|
|
440
477
|
}
|
|
441
478
|
if (matched) matches.push(ws);
|
|
442
479
|
}
|
|
443
|
-
|
|
444
|
-
const selected = matches.find((ws) => ws.name === workspaceName);
|
|
445
|
-
if (!selected && matches.length > 0) {
|
|
446
|
-
const matchingNames = matches.map((ws) => ws.name);
|
|
447
|
-
throw new Error(
|
|
448
|
-
`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}; matching choices: ${matchingNames.join(", ")}`,
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
if (selected) return selected;
|
|
452
|
-
throw new Error(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}`);
|
|
453
|
-
}
|
|
454
|
-
if (matches.length > 1) {
|
|
455
|
-
throw new Error(
|
|
456
|
-
`ambiguous workspace for ${cwd}; choose one of: ${matches.map((ws) => ws.name).join(", ")}`,
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
return matches[0] ?? null;
|
|
480
|
+
return selectWorkspaceMatch(matches, cwd, workspaceName) ?? null;
|
|
460
481
|
};
|
|
461
482
|
|
|
462
483
|
export const resolveWorkspace = (
|
|
@@ -560,22 +581,7 @@ const resolveRuntimeWorkspaceCandidate = (
|
|
|
560
581
|
);
|
|
561
582
|
});
|
|
562
583
|
const workspaceName = process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined;
|
|
563
|
-
|
|
564
|
-
if (workspaceName) {
|
|
565
|
-
const named = matches.filter((entry) => entry.name === workspaceName);
|
|
566
|
-
if (named.length === 1) selected = named[0];
|
|
567
|
-
else if (named.length > 1)
|
|
568
|
-
throw new Error(`ambiguous workspace ${JSON.stringify(workspaceName)} for ${cwd}`);
|
|
569
|
-
else if (matches.length)
|
|
570
|
-
throw new Error(
|
|
571
|
-
`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}; matching choices: ${matches.map((entry) => entry.name).join(", ")}`,
|
|
572
|
-
);
|
|
573
|
-
else throw new Error(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}`);
|
|
574
|
-
} else if (matches.length > 1)
|
|
575
|
-
throw new Error(
|
|
576
|
-
`ambiguous workspace for ${cwd}; choose one of: ${matches.map((entry) => entry.name).join(", ")}`,
|
|
577
|
-
);
|
|
578
|
-
else selected = matches[0];
|
|
584
|
+
const selected = selectWorkspaceMatch(matches, cwd, workspaceName);
|
|
579
585
|
if (!selected) return null;
|
|
580
586
|
const policy = runtimeWorkspacePolicySchema(kind).safeParse(selected);
|
|
581
587
|
if (!policy.success) {
|
|
@@ -103,7 +103,7 @@ type PostData = {
|
|
|
103
103
|
loggedMinutes: number;
|
|
104
104
|
outcome?: "unknown" | "not_applied";
|
|
105
105
|
instructions?: string;
|
|
106
|
-
retry?: "
|
|
106
|
+
retry?: "youtrack.update" | "youtrack.time";
|
|
107
107
|
};
|
|
108
108
|
|
|
109
109
|
const notApplied = (value: LegacyValue): value is NotApplied =>
|
|
@@ -130,7 +130,7 @@ export async function postUpdate(
|
|
|
130
130
|
postedComment: false,
|
|
131
131
|
loggedMinutes: 0,
|
|
132
132
|
outcome: "not_applied",
|
|
133
|
-
retry: "
|
|
133
|
+
retry: "youtrack.update",
|
|
134
134
|
});
|
|
135
135
|
unwrap(comment);
|
|
136
136
|
} catch (error) {
|
|
@@ -158,7 +158,7 @@ export async function postUpdate(
|
|
|
158
158
|
postedComment: true,
|
|
159
159
|
loggedMinutes: 0,
|
|
160
160
|
outcome: "not_applied",
|
|
161
|
-
retry: "
|
|
161
|
+
retry: "youtrack.time",
|
|
162
162
|
});
|
|
163
163
|
unwrap(time);
|
|
164
164
|
} catch (error) {
|
|
@@ -191,7 +191,7 @@ export async function logTimeUpdate(
|
|
|
191
191
|
issueId: input.issueId,
|
|
192
192
|
loggedMinutes: 0,
|
|
193
193
|
outcome: "not_applied",
|
|
194
|
-
retry: "
|
|
194
|
+
retry: "youtrack.time",
|
|
195
195
|
});
|
|
196
196
|
return ok(unwrap(value));
|
|
197
197
|
} catch (error) {
|
package/src/core/youtrack.ts
CHANGED
|
@@ -846,7 +846,7 @@ export async function postUpdate(
|
|
|
846
846
|
postedComment: true,
|
|
847
847
|
loggedMinutes: 0,
|
|
848
848
|
error: time.error,
|
|
849
|
-
retry: "
|
|
849
|
+
retry: "youtrack.time",
|
|
850
850
|
};
|
|
851
851
|
}
|
|
852
852
|
return { ok: true, issueId, postedComment: true, loggedMinutes: minutes };
|
package/src/core.ts
CHANGED