@brainervirus/workit-core 2.1.4 → 2.1.5
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 +2 -2
- package/scripts/analyze-release-scope.ts +3 -3
- package/scripts/doctor-check.ts +1 -2
- package/scripts/sync-release-manifests.ts +4 -4
- package/src/core/authority.ts +8 -8
- package/src/core/auto-approval.ts +6 -6
- package/src/core/branch.ts +4 -4
- package/src/core/config-conversion.ts +1 -1
- package/src/core/config.ts +3 -3
- package/src/core/cutover.ts +6 -6
- package/src/core/doctor.ts +4 -3
- package/src/core/external-action-effects.ts +15 -15
- package/src/core/external-action.ts +16 -19
- package/src/core/logger.ts +2 -2
- package/src/core/policy-resolver.ts +9 -6
- package/src/core/registration.ts +3 -5
- package/src/core/repo-context.ts +1 -1
- package/src/core/session-context.ts +2 -2
- package/src/core/setup.ts +2 -2
- package/src/core/skill-manifests.ts +1 -1
- package/src/core/support-matrix.ts +1 -1
- package/src/core/task-context.ts +3 -3
- package/src/core/task-contract.ts +8 -8
- package/src/core/task-engine.ts +159 -159
- package/src/core/task-evaluation.ts +3 -3
- package/src/core/task-store.ts +12 -12
- package/src/core/tracker-issues.ts +1 -1
- package/src/core/uninstall.ts +3 -3
- package/src/core/workers.ts +3 -3
- package/src/core/workspaces.ts +4 -6
- package/src/core/youtrack-tools.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brainervirus/workit-core",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Workit shared core — task, policy, evidence, review, decision, worker, and writer state for agentic coding workflows",
|
|
6
6
|
"keywords": [
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@openclaw/fs-safe": "0.8.1",
|
|
51
|
-
"zod": "4.5
|
|
51
|
+
"zod": "4.6.5"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=24"
|
|
@@ -26,11 +26,11 @@ const g = (root: string, args: string[]): string =>
|
|
|
26
26
|
const SEMVER_TAG = /^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
27
27
|
|
|
28
28
|
export function latestTag(root = process.cwd()): string | null {
|
|
29
|
-
const
|
|
29
|
+
const tag = g(root, ["tag", "--list", "v*", "--sort=-v:refname"])
|
|
30
30
|
.split("\n")
|
|
31
31
|
.map((l) => l.trim())
|
|
32
|
-
.
|
|
33
|
-
return
|
|
32
|
+
.find((l) => SEMVER_TAG.test(l));
|
|
33
|
+
return tag ?? null;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
type Level = "major" | "minor" | "patch";
|
package/scripts/doctor-check.ts
CHANGED
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
import { runDoctor } from "../src/core/doctor";
|
|
12
12
|
|
|
13
13
|
const hostArg = process.argv[2];
|
|
14
|
-
const host =
|
|
15
|
-
hostArg === "cursor" ? "cursor" : hostArg === "cli" ? "cli" : "opencode";
|
|
14
|
+
const host = hostArg === "cursor" ? "cursor" : hostArg === "cli" ? "cli" : "opencode";
|
|
16
15
|
const staleOnly = process.argv.includes("--stale");
|
|
17
16
|
const report = runDoctor({ host, installer: true });
|
|
18
17
|
|
|
@@ -31,7 +31,8 @@ export type ManifestSyncResult = { version: string; changed: string[] };
|
|
|
31
31
|
* are left byte-untouched and omitted from `changed`. Throws on a value that
|
|
32
32
|
* is not a plain release version (`latest`, branches, ranges).
|
|
33
33
|
*/
|
|
34
|
-
export function syncManifests(root: string, tagOrVersion: string): ManifestSyncResult {
|
|
34
|
+
export function syncManifests(root: string, tagOrVersion: string): ManifestSyncResult {
|
|
35
|
+
const match = /^v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/.exec(tagOrVersion.trim());
|
|
35
36
|
if (!match) throw new Error(`invalid version tag: ${JSON.stringify(tagOrVersion)}`);
|
|
36
37
|
const version = match[1];
|
|
37
38
|
const changed: string[] = [];
|
|
@@ -48,15 +49,14 @@ export function syncManifests(root: string, tagOrVersion: string): ManifestSyncR
|
|
|
48
49
|
|
|
49
50
|
/** The newest `v*` tag by descending semver refname order (empty repo throws). */
|
|
50
51
|
function latestReleaseTag(cwd?: string): string {
|
|
51
|
-
const
|
|
52
|
+
const tag = execFileSync("git", ["tag", "--list", "v*", "--sort=-v:refname"], {
|
|
52
53
|
cwd,
|
|
53
54
|
encoding: "utf8",
|
|
54
55
|
stdio: ["pipe", "pipe", "pipe"],
|
|
55
56
|
})
|
|
56
57
|
.split("\n")
|
|
57
58
|
.map((line) => line.trim())
|
|
58
|
-
.
|
|
59
|
-
const tag = out[0];
|
|
59
|
+
.find(Boolean);
|
|
60
60
|
if (!tag) throw new Error("no v* release tag found — run after the first semantic-release");
|
|
61
61
|
return tag;
|
|
62
62
|
}
|
package/src/core/authority.ts
CHANGED
|
@@ -641,7 +641,7 @@ const validateAction = (
|
|
|
641
641
|
if (input.binding && canonicalJson(input.binding) !== canonicalJson(decision.binding))
|
|
642
642
|
return failure("permission_denied", "action binding does not match the approved decision");
|
|
643
643
|
const content = verifyContentRefs(store, decision.binding);
|
|
644
|
-
if (!content.ok) return content
|
|
644
|
+
if (!content.ok) return content;
|
|
645
645
|
const requestedSteps = parseSteps(decision.binding.approvedContent);
|
|
646
646
|
if (requestedSteps === null)
|
|
647
647
|
return failure("invalid_input", "bounded action steps must be a unique non-empty array");
|
|
@@ -679,7 +679,7 @@ export function reserveAction(input: ReserveActionInput): Result<ActionReservati
|
|
|
679
679
|
input.expectedRevision,
|
|
680
680
|
(current, mutation) => {
|
|
681
681
|
const workspace = input.store.readWorkspace();
|
|
682
|
-
if (!workspace.ok) return workspace
|
|
682
|
+
if (!workspace.ok) return workspace;
|
|
683
683
|
if (!workspace.data) return failure("not_found", "workspace not found");
|
|
684
684
|
if (workspace.data.revision !== input.expectedWorkspaceRevision)
|
|
685
685
|
return failure("revision_conflict", "workspace revision does not match", {
|
|
@@ -687,7 +687,7 @@ export function reserveAction(input: ReserveActionInput): Result<ActionReservati
|
|
|
687
687
|
actualWorkspaceRevision: workspace.data.revision,
|
|
688
688
|
});
|
|
689
689
|
const valid = validateAction(input.store, current, workspace.data.id, input, authority);
|
|
690
|
-
if (!valid.ok) return valid
|
|
690
|
+
if (!valid.ok) return valid;
|
|
691
691
|
const { entry, workflow } = valid.data;
|
|
692
692
|
const existing = entry.data.consumption;
|
|
693
693
|
if (existing?.state === "uncertain")
|
|
@@ -736,7 +736,7 @@ export function reserveAction(input: ReserveActionInput): Result<ActionReservati
|
|
|
736
736
|
},
|
|
737
737
|
input.native?.now,
|
|
738
738
|
);
|
|
739
|
-
if (!changed.ok) return changed
|
|
739
|
+
if (!changed.ok) return changed;
|
|
740
740
|
const workspace = input.store.readWorkspace();
|
|
741
741
|
if (!workspace.ok || !workspace.data)
|
|
742
742
|
return failure("recovery_required", "workspace disappeared after reservation");
|
|
@@ -793,7 +793,7 @@ export function settleAction(input: SettleActionInput): Result<Entry<Decision>>
|
|
|
793
793
|
input.taskRevision,
|
|
794
794
|
(current, mutation) => {
|
|
795
795
|
const workspace = input.store.readWorkspace();
|
|
796
|
-
if (!workspace.ok) return workspace
|
|
796
|
+
if (!workspace.ok) return workspace;
|
|
797
797
|
if (!workspace.data) return failure("not_found", "workspace not found");
|
|
798
798
|
if (
|
|
799
799
|
!authorityMatches(authority, {
|
|
@@ -909,7 +909,7 @@ export function settleAction(input: SettleActionInput): Result<Entry<Decision>>
|
|
|
909
909
|
},
|
|
910
910
|
input.native?.now,
|
|
911
911
|
);
|
|
912
|
-
if (!changed.ok) return changed
|
|
912
|
+
if (!changed.ok) return changed;
|
|
913
913
|
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
914
914
|
if (!entry) return failure("recovery_required", "settled decision disappeared");
|
|
915
915
|
if (outcomeResult) return outcomeResult;
|
|
@@ -943,7 +943,7 @@ export function reconcileAction(input: ReconcileActionInput): Result<Entry<Decis
|
|
|
943
943
|
input.taskRevision,
|
|
944
944
|
(current, mutation) => {
|
|
945
945
|
const workspace = input.store.readWorkspace();
|
|
946
|
-
if (!workspace.ok) return workspace
|
|
946
|
+
if (!workspace.ok) return workspace;
|
|
947
947
|
if (!workspace.data) return failure("not_found", "workspace not found");
|
|
948
948
|
if (authority.workspaceId !== workspace.data.id)
|
|
949
949
|
return failure("permission_denied", "native reconciliation workspace does not match");
|
|
@@ -1022,7 +1022,7 @@ export function reconcileAction(input: ReconcileActionInput): Result<Entry<Decis
|
|
|
1022
1022
|
},
|
|
1023
1023
|
input.native?.now,
|
|
1024
1024
|
);
|
|
1025
|
-
if (!changed.ok) return changed
|
|
1025
|
+
if (!changed.ok) return changed;
|
|
1026
1026
|
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
1027
1027
|
return entry
|
|
1028
1028
|
? success(changed.data.revision, input.workspaceRevision, entry)
|
|
@@ -172,7 +172,7 @@ export const verifyStandingApproval = (
|
|
|
172
172
|
if (task.status !== "active")
|
|
173
173
|
return failure("invalid_transition", "only active tasks can authorize actions");
|
|
174
174
|
const workspace = new TaskStore(root).readWorkspace();
|
|
175
|
-
if (!workspace.ok) return workspace
|
|
175
|
+
if (!workspace.ok) return workspace;
|
|
176
176
|
if (!workspace.data || !currentWriterOwnsTask(task, workspace.data, caller))
|
|
177
177
|
return failure("permission_denied", "standing approval requires the current writer session");
|
|
178
178
|
const standing = binding.standing;
|
|
@@ -200,8 +200,8 @@ export const verifyStandingApproval = (
|
|
|
200
200
|
)
|
|
201
201
|
return failure("permission_denied", "standing auto-approval is not live for this operation");
|
|
202
202
|
if (cls === "push") {
|
|
203
|
-
const
|
|
204
|
-
const provider =
|
|
203
|
+
const targetWorkspace = resolveWorkspaceFrom(target, configDir());
|
|
204
|
+
const provider = targetWorkspace?.vcs?.provider;
|
|
205
205
|
const account = resolved.account;
|
|
206
206
|
if (!provider || !account)
|
|
207
207
|
return failure(
|
|
@@ -217,7 +217,7 @@ export const verifyStandingApproval = (
|
|
|
217
217
|
session: { kind: "host", host: caller.host, handle: caller.actor },
|
|
218
218
|
workerId: null,
|
|
219
219
|
receipts: [standingReceiptFor(target, standing.workspace, cls)],
|
|
220
|
-
}
|
|
220
|
+
});
|
|
221
221
|
};
|
|
222
222
|
|
|
223
223
|
export type StandingAutoBinding = {
|
|
@@ -259,8 +259,8 @@ export const standingAutoApplies = (
|
|
|
259
259
|
const listed = store.listTasks();
|
|
260
260
|
const workspace = store.readWorkspace();
|
|
261
261
|
if (!listed.ok || !workspace.ok || !workspace.data) return null;
|
|
262
|
-
const task = listed.data.find((
|
|
263
|
-
currentWriterOwnsTask(
|
|
262
|
+
const task = listed.data.find((entry) =>
|
|
263
|
+
currentWriterOwnsTask(entry, workspace.data!, { host, actor }),
|
|
264
264
|
);
|
|
265
265
|
if (!task) return null;
|
|
266
266
|
const resolved = resolveAutoApproval(target);
|
package/src/core/branch.ts
CHANGED
|
@@ -132,8 +132,8 @@ export const branchPolicySnapshotFor = (workspaceRoot: string) => {
|
|
|
132
132
|
const source = resolved.provenance.branchPolicy;
|
|
133
133
|
const allowed = branchPolicy.allowed
|
|
134
134
|
.map((pattern) => `${pattern.source}/${pattern.flags}`)
|
|
135
|
-
.
|
|
136
|
-
const protectedRefs = [...branchPolicy.protected].map((name) => name.toLowerCase()).
|
|
135
|
+
.toSorted();
|
|
136
|
+
const protectedRefs = [...branchPolicy.protected].map((name) => name.toLowerCase()).toSorted();
|
|
137
137
|
const fingerprint = createHash("sha256")
|
|
138
138
|
.update(JSON.stringify({ allowed, protectedRefs, source }))
|
|
139
139
|
.digest("hex");
|
|
@@ -675,8 +675,8 @@ export const branchSetup = ({
|
|
|
675
675
|
targetExists = false;
|
|
676
676
|
}
|
|
677
677
|
if (!targetExists) {
|
|
678
|
-
const
|
|
679
|
-
if (!
|
|
678
|
+
const namePolicy = validateBranchNameFor(cwd, target);
|
|
679
|
+
if (!namePolicy.ok) return { error: namePolicy.error, phase: "preflight" };
|
|
680
680
|
}
|
|
681
681
|
if (!targetExists) {
|
|
682
682
|
const baseResolved = baseBranch(cwd);
|
|
@@ -70,7 +70,7 @@ export function previewConversion(input: ConversionInput): ConversionPreview {
|
|
|
70
70
|
allowed?: string[];
|
|
71
71
|
protected?: string[];
|
|
72
72
|
};
|
|
73
|
-
const preset =
|
|
73
|
+
const preset = bp.preset ?? "gitflow";
|
|
74
74
|
const merged = mergePreset(
|
|
75
75
|
preset,
|
|
76
76
|
{
|
package/src/core/config.ts
CHANGED
|
@@ -297,7 +297,7 @@ export const resolveBranchPolicy = (
|
|
|
297
297
|
integration: "pr" | "merge";
|
|
298
298
|
defaultTargetBranch: string;
|
|
299
299
|
} => {
|
|
300
|
-
const wp =
|
|
300
|
+
const wp = workspace?.branchPolicy ?? {};
|
|
301
301
|
// An invalid workspace preset (e.g. a typo) falls back to the global preset,
|
|
302
302
|
// preserving resolution order workspace > global > preset, instead of
|
|
303
303
|
// crashing on PRESETS[preset] (mirrors parseConfigResult's Object.hasOwn).
|
|
@@ -345,12 +345,12 @@ export const resolveCommitPolicy = (
|
|
|
345
345
|
config: ToolkitConfig,
|
|
346
346
|
workspace?: { commitPolicy?: Record<string, any> } | null,
|
|
347
347
|
): { preset: CommitFlavorPreset; pattern?: string } => {
|
|
348
|
-
const wp =
|
|
348
|
+
const wp = workspace?.commitPolicy ?? {};
|
|
349
349
|
if (COMMIT_PRESETS.includes(String(wp.preset ?? ""))) {
|
|
350
350
|
return {
|
|
351
351
|
preset: wp.preset as CommitFlavorPreset,
|
|
352
352
|
...(typeof wp.pattern === "string" ? { pattern: wp.pattern } : {}),
|
|
353
353
|
};
|
|
354
354
|
}
|
|
355
|
-
return config.commitPolicy ??
|
|
355
|
+
return config.commitPolicy ?? { preset: "conventional" };
|
|
356
356
|
};
|
package/src/core/cutover.ts
CHANGED
|
@@ -217,8 +217,8 @@ const readDirNamesBounded = (
|
|
|
217
217
|
try {
|
|
218
218
|
for (;;) {
|
|
219
219
|
const entry = handle.readSync();
|
|
220
|
-
if (!entry) return { names: names.
|
|
221
|
-
if (names.length === limit) return { names: names.
|
|
220
|
+
if (!entry) return { names: names.toSorted(), truncated: false };
|
|
221
|
+
if (names.length === limit) return { names: names.toSorted(), truncated: true };
|
|
222
222
|
names.push(entry.name);
|
|
223
223
|
}
|
|
224
224
|
} finally {
|
|
@@ -596,8 +596,8 @@ const cutoverInventoryScopes = (
|
|
|
596
596
|
category: "project-state",
|
|
597
597
|
disposition: "preserve",
|
|
598
598
|
},
|
|
599
|
-
...legacyFlowInventoryTargets(paths.workspace, issues).map((
|
|
600
|
-
path,
|
|
599
|
+
...legacyFlowInventoryTargets(paths.workspace, issues).map((target) => ({
|
|
600
|
+
path: target,
|
|
601
601
|
category: "project-history" as const,
|
|
602
602
|
disposition: "preserve" as const,
|
|
603
603
|
})),
|
|
@@ -743,7 +743,7 @@ const buildCutoverInventory = (paths: ReturnType<typeof resolvePaths>): CutoverI
|
|
|
743
743
|
}
|
|
744
744
|
hash.update(`${entry.path}\0${entry.kind}\0${entry.bytes}\0${entry.digest ?? ""}\n`);
|
|
745
745
|
}
|
|
746
|
-
const issueList = [...issues].
|
|
746
|
+
const issueList = [...issues].toSorted();
|
|
747
747
|
for (const issue of issueList) hash.update(`!${issue}\n`);
|
|
748
748
|
return {
|
|
749
749
|
complete: issueList.length === 0,
|
|
@@ -936,7 +936,7 @@ const readBackupFiles = (
|
|
|
936
936
|
issues.push(`backup content failed verification: ${entry.path}`);
|
|
937
937
|
continue;
|
|
938
938
|
}
|
|
939
|
-
files.set(entry.path, { digest: digest
|
|
939
|
+
files.set(entry.path, { digest: digest, storagePath });
|
|
940
940
|
}
|
|
941
941
|
return { files, issues };
|
|
942
942
|
};
|
package/src/core/doctor.ts
CHANGED
|
@@ -559,7 +559,8 @@ const registeredCursorLauncher = (res: Resolved): CursorLauncher | null | "inval
|
|
|
559
559
|
// no version abstraction.
|
|
560
560
|
const canonicalCursorHook = cursorHooksEntry("").command;
|
|
561
561
|
|
|
562
|
-
|
|
562
|
+
// Returns the registered hook command, null when absent, or "invalid".
|
|
563
|
+
const registeredCursorHook = (res: Resolved): string | null => {
|
|
563
564
|
const hooksFile = path.join(res.cursorPluginDir, "hooks", "hooks-cursor.json");
|
|
564
565
|
if (!existsSync(hooksFile)) return "invalid";
|
|
565
566
|
const config = readJson(hooksFile);
|
|
@@ -866,7 +867,7 @@ const checkOpenCodePackageCache = (res: Resolved): DoctorCheck & { registryProbe
|
|
|
866
867
|
detail: "no workit opencode pin — package cache not inspected",
|
|
867
868
|
};
|
|
868
869
|
}
|
|
869
|
-
const pin = entries[0]
|
|
870
|
+
const pin = entries[0];
|
|
870
871
|
if (pin.startsWith("file:") || pin.startsWith("git+file:")) {
|
|
871
872
|
return {
|
|
872
873
|
id: "stale_install",
|
|
@@ -1318,7 +1319,7 @@ const providerIdentityFinding = (
|
|
|
1318
1319
|
provider: "github" | "gitlab",
|
|
1319
1320
|
surfaces: IdentitySurface[],
|
|
1320
1321
|
): DoctorCheck => {
|
|
1321
|
-
const id
|
|
1322
|
+
const id: "github_identity" | "gitlab_identity" = `${provider}_identity`;
|
|
1322
1323
|
const label = provider === "github" ? "GitHub" : "GitLab";
|
|
1323
1324
|
const resolved = surfaces.filter(
|
|
1324
1325
|
(entry): entry is { surface: string; login: string } => typeof entry.login === "string",
|
|
@@ -86,9 +86,9 @@ const stagedPaths = (root: string): string[] | null => {
|
|
|
86
86
|
const rename = /^(?:R|C)\d{3}$/.test(status);
|
|
87
87
|
const count = rename ? 2 : 1;
|
|
88
88
|
for (let offset = 0; offset < count; offset += 1) {
|
|
89
|
-
const
|
|
90
|
-
if (!
|
|
91
|
-
paths.push(
|
|
89
|
+
const filePath = tokens[index++];
|
|
90
|
+
if (!filePath) return null;
|
|
91
|
+
paths.push(filePath);
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
return [...new Set(paths)];
|
|
@@ -258,8 +258,8 @@ export const assertLocalExternalActionWriter = (
|
|
|
258
258
|
const store = new TaskStore(root);
|
|
259
259
|
const listed = store.listTasks();
|
|
260
260
|
const workspace = store.readWorkspace();
|
|
261
|
-
if (!listed.ok) return listed
|
|
262
|
-
if (!workspace.ok) return workspace
|
|
261
|
+
if (!listed.ok) return listed;
|
|
262
|
+
if (!workspace.ok) return workspace;
|
|
263
263
|
if (!workspace.data) return failure("not_found", "workspace not found");
|
|
264
264
|
if (
|
|
265
265
|
expectedWorkspaceRevision !== undefined &&
|
|
@@ -317,7 +317,7 @@ const branchCreationBaseline = (
|
|
|
317
317
|
};
|
|
318
318
|
const remote = run(root, ["ls-remote", "origin", `refs/heads/${base}`]);
|
|
319
319
|
const sha = remote.exitCode === 0 ? remote.stdout.trim().split(/\s+/)[0] : "";
|
|
320
|
-
const remoteBase = /^[0-9a-f]{40}$/.test(sha ?? "") ?
|
|
320
|
+
const remoteBase = /^[0-9a-f]{40}$/.test(sha ?? "") ? sha : null;
|
|
321
321
|
return {
|
|
322
322
|
base_branch: base,
|
|
323
323
|
target_exists: false,
|
|
@@ -717,7 +717,7 @@ const mergedBranch = (root: string, branch: string): Result<Record<string, strin
|
|
|
717
717
|
},
|
|
718
718
|
);
|
|
719
719
|
const live = remoteBranchTip(root, provider, auth.host, repo, branch);
|
|
720
|
-
if (!live.ok) return live
|
|
720
|
+
if (!live.ok) return live;
|
|
721
721
|
const tip = live.data;
|
|
722
722
|
if (!tip)
|
|
723
723
|
return failure("invalid_input", "remote branch is absent or could not be read", {
|
|
@@ -978,8 +978,8 @@ export const readYouTrackAction = async (
|
|
|
978
978
|
const creds = youTrackToken();
|
|
979
979
|
if ("error" in creds || creds.base !== baseUrl) return unknownHostingEvidence(operation);
|
|
980
980
|
const bounded = "&$top=100&$skip=0";
|
|
981
|
-
const read = async (
|
|
982
|
-
const result = await youTrackRequest(`${baseUrl}${
|
|
981
|
+
const read = async (endpoint: string): Promise<unknown[] | null> => {
|
|
982
|
+
const result = await youTrackRequest(`${baseUrl}${endpoint}${bounded}`, {
|
|
983
983
|
method: "GET",
|
|
984
984
|
token: creds.token,
|
|
985
985
|
});
|
|
@@ -1670,7 +1670,7 @@ const resolveExternalActionAtRoot = (
|
|
|
1670
1670
|
request,
|
|
1671
1671
|
descriptorPayload: { ...request.payload, resolved: checked.data },
|
|
1672
1672
|
})
|
|
1673
|
-
:
|
|
1673
|
+
: checked;
|
|
1674
1674
|
}
|
|
1675
1675
|
case "changelog.apply": {
|
|
1676
1676
|
const preview = changelogApplyPreview({ ...request.payload, workspace_root: root });
|
|
@@ -1707,7 +1707,7 @@ const resolveExternalActionAtRoot = (
|
|
|
1707
1707
|
return failure("invalid_input", "youtrack.time requires an explicit dateMs");
|
|
1708
1708
|
{
|
|
1709
1709
|
const available = youTrackWritePreflight();
|
|
1710
|
-
if (!available.ok) return available
|
|
1710
|
+
if (!available.ok) return available;
|
|
1711
1711
|
const baseUrl = youTrackBase();
|
|
1712
1712
|
const date = youTrackWorkDateMs(String(request.payload.dateMs));
|
|
1713
1713
|
if (!baseUrl)
|
|
@@ -1741,7 +1741,7 @@ const resolveExternalActionAtRoot = (
|
|
|
1741
1741
|
case "youtrack.update":
|
|
1742
1742
|
case "youtrack.meeting": {
|
|
1743
1743
|
const available = youTrackWritePreflight();
|
|
1744
|
-
if (!available.ok) return available
|
|
1744
|
+
if (!available.ok) return available;
|
|
1745
1745
|
const baseUrl = youTrackBase();
|
|
1746
1746
|
const date = youTrackWorkDateMs(
|
|
1747
1747
|
preservedDateMs === undefined ? "auto" : String(preservedDateMs),
|
|
@@ -1787,7 +1787,7 @@ const resolveExternalActionAtRoot = (
|
|
|
1787
1787
|
}
|
|
1788
1788
|
default:
|
|
1789
1789
|
return success(null, null, {
|
|
1790
|
-
request: request
|
|
1790
|
+
request: request,
|
|
1791
1791
|
descriptorPayload: (request as unknown as { payload: unknown }).payload,
|
|
1792
1792
|
});
|
|
1793
1793
|
}
|
|
@@ -1802,7 +1802,7 @@ export const resolveExternalActionRequest = (
|
|
|
1802
1802
|
preservedDateMs?: number,
|
|
1803
1803
|
): Result<ResolvedExternalAction> => {
|
|
1804
1804
|
const target = actionRoot(taskRoot, request);
|
|
1805
|
-
if (!target.ok) return target
|
|
1805
|
+
if (!target.ok) return target;
|
|
1806
1806
|
const bindTarget = bindsActionTarget(request.operation);
|
|
1807
1807
|
const normalized = bindTarget
|
|
1808
1808
|
? ({ ...request, payload: { ...request.payload, cwd: target.data } } as ExternalActionRequest)
|
|
@@ -1909,7 +1909,7 @@ export const executeConcreteExternalAction = async (
|
|
|
1909
1909
|
);
|
|
1910
1910
|
if (!sameDirectoryIdentity(root, coordinationRoot)) {
|
|
1911
1911
|
const targetWorkspace = new TaskStore(root).readWorkspace();
|
|
1912
|
-
if (!targetWorkspace.ok) return targetWorkspace
|
|
1912
|
+
if (!targetWorkspace.ok) return targetWorkspace;
|
|
1913
1913
|
const owner = targetWorkspace.data?.writer?.owner;
|
|
1914
1914
|
if (
|
|
1915
1915
|
owner &&
|
|
@@ -210,11 +210,10 @@ export const externalActionSchema = z.discriminatedUnion("operation", [
|
|
|
210
210
|
export type ExternalActionRequest = z.infer<typeof externalActionSchema>;
|
|
211
211
|
|
|
212
212
|
/** The complete external-action contract for hosts that register native tools. */
|
|
213
|
-
export const externalActionJsonSchema = (): z.core.JSONSchema.BaseSchema =>
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}) as z.core.JSONSchema.BaseSchema;
|
|
213
|
+
export const externalActionJsonSchema = (): z.core.JSONSchema.BaseSchema => ({
|
|
214
|
+
type: "object",
|
|
215
|
+
...z.toJSONSchema(externalActionSchema, { target: "draft-2020-12" }),
|
|
216
|
+
});
|
|
218
217
|
|
|
219
218
|
export type ExternalActionOperation = ExternalActionRequest["operation"];
|
|
220
219
|
|
|
@@ -639,7 +638,7 @@ export const chainStepBinding = (
|
|
|
639
638
|
typeof descriptor.payload?.target_branch === "string"
|
|
640
639
|
? {
|
|
641
640
|
operation: "git.branch_setup",
|
|
642
|
-
target: descriptor.payload.target_branch
|
|
641
|
+
target: descriptor.payload.target_branch,
|
|
643
642
|
cwd: targetRoot,
|
|
644
643
|
}
|
|
645
644
|
: descriptor.operation === "hosting.pull_request"
|
|
@@ -947,7 +946,7 @@ export const createAuthorizedExternalActionRunner =
|
|
|
947
946
|
let latest: ExternalActionResult<T> | undefined;
|
|
948
947
|
for (const step of sequence) {
|
|
949
948
|
const binding = bind(operation);
|
|
950
|
-
if ("ok" in binding) return binding
|
|
949
|
+
if ("ok" in binding) return binding;
|
|
951
950
|
const targetRoot = externalActionTargetRoot(operation, coordinationRoot);
|
|
952
951
|
if (targetRoot === null)
|
|
953
952
|
return failure("capability_unavailable", "external action target could not be locked", {
|
|
@@ -960,7 +959,7 @@ export const createAuthorizedExternalActionRunner =
|
|
|
960
959
|
? (run) => new TaskStore(targetRoot).withExternalActionLock(run, true)
|
|
961
960
|
: undefined;
|
|
962
961
|
const result = await runAuthorizedExternalAction(
|
|
963
|
-
{ core, ...
|
|
962
|
+
{ core, ...binding, ...(step ? { step } : {}) },
|
|
964
963
|
(reservation) => effect(step, reservation),
|
|
965
964
|
withActionLock,
|
|
966
965
|
);
|
|
@@ -1039,12 +1038,10 @@ export const readNativeExternalActionObservation = (
|
|
|
1039
1038
|
actionRef: candidate.actionRef as Ref,
|
|
1040
1039
|
outcome: candidate.outcome as NativeExternalActionObservation["outcome"],
|
|
1041
1040
|
...(candidate.callId === undefined ? {} : { callId: candidate.callId }),
|
|
1042
|
-
...(candidate.taskRevision === undefined
|
|
1043
|
-
? {}
|
|
1044
|
-
: { taskRevision: candidate.taskRevision as Revision }),
|
|
1041
|
+
...(candidate.taskRevision === undefined ? {} : { taskRevision: candidate.taskRevision }),
|
|
1045
1042
|
...(candidate.workspaceRevision === undefined
|
|
1046
1043
|
? {}
|
|
1047
|
-
: { workspaceRevision: candidate.workspaceRevision
|
|
1044
|
+
: { workspaceRevision: candidate.workspaceRevision }),
|
|
1048
1045
|
};
|
|
1049
1046
|
};
|
|
1050
1047
|
|
|
@@ -1100,7 +1097,7 @@ export async function runAuthorizedExternalAction<T>(
|
|
|
1100
1097
|
...(input.step ? { step: input.step } : {}),
|
|
1101
1098
|
observation: input.reserveObservation,
|
|
1102
1099
|
});
|
|
1103
|
-
if (!reserved.ok) return reserved
|
|
1100
|
+
if (!reserved.ok) return reserved;
|
|
1104
1101
|
|
|
1105
1102
|
const settle = (
|
|
1106
1103
|
outcome: "succeeded" | "not_started" | "unknown",
|
|
@@ -1149,7 +1146,7 @@ export async function runAuthorizedExternalAction<T>(
|
|
|
1149
1146
|
effectValue = await effect(reserved.data);
|
|
1150
1147
|
} catch {
|
|
1151
1148
|
const settled = settleWithRefresh("unknown", settle("unknown"));
|
|
1152
|
-
if (!settled.ok) return settled
|
|
1149
|
+
if (!settled.ok) return settled;
|
|
1153
1150
|
return failure("external_outcome_unknown", "external action outcome is unknown", {
|
|
1154
1151
|
operation: "external_action",
|
|
1155
1152
|
outcome: "unknown",
|
|
@@ -1161,7 +1158,7 @@ export async function runAuthorizedExternalAction<T>(
|
|
|
1161
1158
|
"ok" in effectValue &&
|
|
1162
1159
|
typeof (effectValue as { ok?: unknown }).ok === "boolean"
|
|
1163
1160
|
) {
|
|
1164
|
-
const contract = effectValue
|
|
1161
|
+
const contract = effectValue;
|
|
1165
1162
|
if (!contract.ok) {
|
|
1166
1163
|
const preflight =
|
|
1167
1164
|
typeof contract.details === "object" &&
|
|
@@ -1169,13 +1166,13 @@ export async function runAuthorizedExternalAction<T>(
|
|
|
1169
1166
|
(contract.details as Record<string, unknown>).outcome === "not_started";
|
|
1170
1167
|
const outcome = input.failureOutcome ?? (preflight ? "not_started" : "unknown");
|
|
1171
1168
|
const settled = settleWithRefresh(outcome, settle(outcome));
|
|
1172
|
-
if (!settled.ok) return settled
|
|
1169
|
+
if (!settled.ok) return settled;
|
|
1173
1170
|
return contract;
|
|
1174
1171
|
}
|
|
1175
1172
|
effectValue = contract.data;
|
|
1176
1173
|
}
|
|
1177
1174
|
const settled = settleWithRefresh("succeeded", settle("succeeded"));
|
|
1178
|
-
if (!settled.ok) return settled
|
|
1175
|
+
if (!settled.ok) return settled;
|
|
1179
1176
|
return {
|
|
1180
1177
|
ok: true,
|
|
1181
1178
|
schemaVersion: 1,
|
|
@@ -1194,14 +1191,14 @@ export async function runAuthorizedExternalAction<T>(
|
|
|
1194
1191
|
outcome: "unknown",
|
|
1195
1192
|
});
|
|
1196
1193
|
const settled = settleWithRefresh("not_started", settle("not_started"));
|
|
1197
|
-
if (!settled.ok) return settled
|
|
1194
|
+
if (!settled.ok) return settled;
|
|
1198
1195
|
return failure("writer_conflict", "target checkout was unavailable before the effect", {
|
|
1199
1196
|
outcome: "not_started",
|
|
1200
1197
|
});
|
|
1201
1198
|
}
|
|
1202
1199
|
if (!effectStarted) {
|
|
1203
1200
|
const settled = settleWithRefresh("not_started", settle("not_started"));
|
|
1204
|
-
if (!settled.ok) return settled
|
|
1201
|
+
if (!settled.ok) return settled;
|
|
1205
1202
|
}
|
|
1206
1203
|
return result;
|
|
1207
1204
|
}
|
package/src/core/logger.ts
CHANGED
|
@@ -216,8 +216,8 @@ const pruneOldFiles = (dir: string): void => {
|
|
|
216
216
|
try {
|
|
217
217
|
const files = readdirSync(dir)
|
|
218
218
|
.filter((name) => DAY_FILE.test(name))
|
|
219
|
-
.
|
|
220
|
-
.
|
|
219
|
+
.toSorted()
|
|
220
|
+
.toReversed();
|
|
221
221
|
for (const file of files.slice(RETAINED_DAYS)) {
|
|
222
222
|
try {
|
|
223
223
|
unlinkSync(path.join(dir, file));
|
|
@@ -76,10 +76,13 @@ const compareCodeUnits = (left: string, right: string): number => {
|
|
|
76
76
|
return left.length - right.length;
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
const stableUnique = (values: string[]): string[] =>
|
|
79
|
+
const stableUnique = (values: string[]): string[] =>
|
|
80
|
+
[...new Set(values)].toSorted(compareCodeUnits);
|
|
80
81
|
|
|
81
82
|
const stableList = <T>(values: T[]): T[] =>
|
|
82
|
-
[...values].
|
|
83
|
+
[...values].toSorted((left, right) =>
|
|
84
|
+
compareCodeUnits(canonicalJson(left), canonicalJson(right)),
|
|
85
|
+
);
|
|
83
86
|
|
|
84
87
|
const normalizedScope = (scope: Scope): Scope => ({
|
|
85
88
|
description: scope.description,
|
|
@@ -463,10 +466,10 @@ export function diffPolicy(
|
|
|
463
466
|
if (!policySchema.safeParse(previous).success && previous !== null)
|
|
464
467
|
throw new TypeError("previous policy is invalid");
|
|
465
468
|
if (!policySchema.safeParse(next).success) throw new TypeError("next policy is invalid");
|
|
466
|
-
const oldIds = new Set(previous?.requirements.map((
|
|
467
|
-
const newIds = new Set(next.requirements.map((
|
|
468
|
-
const added = [...newIds].filter((id) => !oldIds.has(id)).
|
|
469
|
-
const retired = [...oldIds].filter((id) => !newIds.has(id)).
|
|
469
|
+
const oldIds = new Set(previous?.requirements.map((item) => item.id) ?? []);
|
|
470
|
+
const newIds = new Set(next.requirements.map((item) => item.id));
|
|
471
|
+
const added = [...newIds].filter((id) => !oldIds.has(id)).toSorted(compareCodeUnits);
|
|
472
|
+
const retired = [...oldIds].filter((id) => !newIds.has(id)).toSorted(compareCodeUnits);
|
|
470
473
|
if (added.length === 0 && retired.length === 0) return null;
|
|
471
474
|
const normalizedReason = reason.trim();
|
|
472
475
|
const changeReason = retired.length
|
package/src/core/registration.ts
CHANGED
|
@@ -138,9 +138,7 @@ export function mergeCursorMcp(
|
|
|
138
138
|
server: Record<string, unknown>,
|
|
139
139
|
): MergeResult<Record<string, unknown>> {
|
|
140
140
|
const base = isRecord(mcp) ? { ...mcp } : {};
|
|
141
|
-
const servers = isRecord(base.mcpServers)
|
|
142
|
-
? { ...(base.mcpServers as Record<string, unknown>) }
|
|
143
|
-
: {};
|
|
141
|
+
const servers = isRecord(base.mcpServers) ? { ...base.mcpServers } : {};
|
|
144
142
|
delete servers["workflow-toolkit"]; // legacy duplicate registration
|
|
145
143
|
servers[serverName] = server;
|
|
146
144
|
const changed = JSON.stringify(servers) !== JSON.stringify(base.mcpServers) ? ["mcpServers"] : [];
|
|
@@ -157,7 +155,7 @@ export function mergeCursorHooks(
|
|
|
157
155
|
sessionStartEntry: Record<string, unknown>,
|
|
158
156
|
): MergeResult<Record<string, unknown>> {
|
|
159
157
|
const base: Record<string, unknown> = isRecord(hooks) ? { ...hooks } : { version: 1 };
|
|
160
|
-
const hooksMap = isRecord(base.hooks) ? { ...
|
|
158
|
+
const hooksMap = isRecord(base.hooks) ? { ...base.hooks } : {};
|
|
161
159
|
const changed: string[] = [];
|
|
162
160
|
const list = Array.isArray(hooksMap.sessionStart) ? hooksMap.sessionStart : [];
|
|
163
161
|
const same =
|
|
@@ -223,7 +221,7 @@ const canonicalHookEntry = (
|
|
|
223
221
|
*/
|
|
224
222
|
export function cursorHookDrift(installed: unknown): string[] {
|
|
225
223
|
if (!isRecord(installed) || !isRecord(installed.hooks)) return ["hooks file is not a hook map"];
|
|
226
|
-
const hooks = installed.hooks
|
|
224
|
+
const hooks = installed.hooks;
|
|
227
225
|
const drift: string[] = [];
|
|
228
226
|
for (const event of ["preToolUse", "beforeShellExecution"] as const) {
|
|
229
227
|
const list = hooks[event];
|
package/src/core/repo-context.ts
CHANGED
|
@@ -258,7 +258,7 @@ export const documentationFiles = (cwd: string): string[] => {
|
|
|
258
258
|
}
|
|
259
259
|
};
|
|
260
260
|
walk(cwd, 1);
|
|
261
|
-
return matches.
|
|
261
|
+
return matches.toSorted().slice(0, 200);
|
|
262
262
|
};
|
|
263
263
|
|
|
264
264
|
const readTrimmed = (file: string, maxLines: number): string => {
|
|
@@ -18,7 +18,7 @@ const sessionTaskEntry = (
|
|
|
18
18
|
): TaskIndexEntry | null =>
|
|
19
19
|
entries
|
|
20
20
|
.filter((entry) => entry.status !== "closed" && boundTo(entry, session))
|
|
21
|
-
.
|
|
21
|
+
.toSorted(newestFirst)[0] ?? null;
|
|
22
22
|
|
|
23
23
|
/** Up to `limit` open tasks not bound to `session`, newest first. */
|
|
24
24
|
const unboundOpenTaskEntries = (
|
|
@@ -28,7 +28,7 @@ const unboundOpenTaskEntries = (
|
|
|
28
28
|
): TaskIndexEntry[] =>
|
|
29
29
|
entries
|
|
30
30
|
.filter((entry) => entry.status !== "closed" && !boundTo(entry, session))
|
|
31
|
-
.
|
|
31
|
+
.toSorted(newestFirst)
|
|
32
32
|
.slice(0, limit);
|
|
33
33
|
|
|
34
34
|
/**
|
package/src/core/setup.ts
CHANGED
|
@@ -506,13 +506,13 @@ type ResolvedApply = {
|
|
|
506
506
|
// the same options (defaulting both from env) — tests pass `home` explicitly.
|
|
507
507
|
const resolveSetupPaths = (
|
|
508
508
|
options: ApplySetupOptions,
|
|
509
|
-
|
|
509
|
+
resolvedConfigDir: string,
|
|
510
510
|
): Omit<ResolvedApply, "dev"> => {
|
|
511
511
|
const env = options.env ?? process.env;
|
|
512
512
|
const home = options.home ?? env.HOME ?? os.homedir();
|
|
513
513
|
return {
|
|
514
514
|
home,
|
|
515
|
-
configDir,
|
|
515
|
+
configDir: resolvedConfigDir,
|
|
516
516
|
cwd: options.cwd ?? process.cwd(),
|
|
517
517
|
env,
|
|
518
518
|
opencodeConfig:
|