@brainervirus/workit-core 2.9.0 → 4.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 +2 -1
- package/package.json +1 -1
- package/scripts/bundled-deps.json +1 -7
- package/scripts/install-codex-plugin.sh +1 -2
- package/scripts/install-cursor-plugin.sh +3 -4
- package/scripts/install-opencode-plugin.sh +2 -3
- package/scripts/install-pi-package.sh +2 -3
- package/scripts/sync-runtime.sh +0 -13
- package/src/core/init.ts +1 -1
- package/src/core/methods.ts +4 -1
- package/src/core/store-lock.ts +13 -2
- package/src/core/support-matrix.ts +4 -4
- package/src/core/task-contract.ts +17 -18
- package/src/core/task-engine.ts +108 -58
- package/src/core/task-store.ts +1857 -1259
- package/src/core/workspace-branch-policy.ts +123 -0
- package/src/core.ts +1 -6
- package/src/hooks/context.ts +57 -5
- package/src/hooks/hosts/opencode.ts +2 -2
- package/src/ledger.ts +12 -18
- package/src/store/event-log.ts +217 -0
- package/src/store/notes.ts +29 -0
- package/src/store/patch.ts +135 -0
- package/src/store/paths.ts +368 -0
- package/src/store/reduce.ts +203 -0
- package/scripts/doctor-check.ts +0 -37
- package/src/core/config-conversion.ts +0 -180
- package/src/core/cutover.ts +0 -1789
- package/src/core/detect-hosts.ts +0 -115
- package/src/core/doctor.ts +0 -1882
- package/src/core/host-install.ts +0 -617
- package/src/core/legacy-ownership.ts +0 -47
- package/src/core/registration.ts +0 -306
- package/src/core/setup-state.ts +0 -61
- package/src/core/setup.ts +0 -1582
- package/src/core/uninstall.ts +0 -544
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Pure reduction of a task's events into its state (design §4.1), plus the
|
|
2
|
+
// content-addressed blob encoding for stored candidates.
|
|
3
|
+
//
|
|
4
|
+
// Every event may carry, in `data`:
|
|
5
|
+
// record the whole task record (opening events and compaction checkpoints)
|
|
6
|
+
// ops a structural patch of the record (see patch.ts)
|
|
7
|
+
// key the implicit-task key the task is bound to (null unbinds)
|
|
8
|
+
// legacy where a migrated task came from
|
|
9
|
+
// The reducer applies whatever of these an event carries, so an event type
|
|
10
|
+
// this runtime does not know still replays correctly when it uses them. An
|
|
11
|
+
// unknown event that carries none of them is ignored, unless it is marked
|
|
12
|
+
// `critical`, in which case the reader fails closed.
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { LogDamage, type StoreEvent } from "./event-log";
|
|
15
|
+
import { apply, isPatch, type PatchOp } from "./patch";
|
|
16
|
+
import type { TaskKey } from "./paths";
|
|
17
|
+
|
|
18
|
+
export type LegacyOrigin = { path: string; digest: string };
|
|
19
|
+
|
|
20
|
+
export type ReducedTask = {
|
|
21
|
+
record: Record<string, unknown>;
|
|
22
|
+
key: TaskKey | null;
|
|
23
|
+
legacy: LegacyOrigin | null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Event types that open (or restart, after compaction) a task's log. */
|
|
27
|
+
export const OPENING_TYPES: ReadonlySet<string> = new Set([
|
|
28
|
+
"task.opened",
|
|
29
|
+
"task.imported",
|
|
30
|
+
"migrated.from_v2",
|
|
31
|
+
"task.checkpoint",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
/** Event types this runtime writes and reads. */
|
|
35
|
+
const KNOWN_TYPES: ReadonlySet<string> = new Set([
|
|
36
|
+
...OPENING_TYPES,
|
|
37
|
+
"task.bound",
|
|
38
|
+
"task.noted",
|
|
39
|
+
"task.revised",
|
|
40
|
+
"task.judged",
|
|
41
|
+
"task.paused",
|
|
42
|
+
"task.resumed",
|
|
43
|
+
"task.closed",
|
|
44
|
+
"task.updated",
|
|
45
|
+
"check.ran",
|
|
46
|
+
"evidence.recorded",
|
|
47
|
+
"finding.recorded",
|
|
48
|
+
"finding.updated",
|
|
49
|
+
"decision.recorded",
|
|
50
|
+
"decision.updated",
|
|
51
|
+
"worker.updated",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
55
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
56
|
+
|
|
57
|
+
const isKey = (value: unknown): value is TaskKey =>
|
|
58
|
+
isObject(value) &&
|
|
59
|
+
typeof value.key === "string" &&
|
|
60
|
+
(value.kind === "branch" || value.kind === "detached" || value.kind === "dir") &&
|
|
61
|
+
(value.branch === null || typeof value.branch === "string");
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// blobs: stored candidates are written once, by content digest
|
|
65
|
+
|
|
66
|
+
export type BlobStore = {
|
|
67
|
+
read(digest: string): unknown;
|
|
68
|
+
write(digest: string, value: unknown): void;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const BLOB_REF = "$blob";
|
|
72
|
+
const blobRef = (value: unknown): string | null =>
|
|
73
|
+
isObject(value) &&
|
|
74
|
+
Object.keys(value).length === 1 &&
|
|
75
|
+
typeof value[BLOB_REF] === "string" &&
|
|
76
|
+
/^[0-9a-f]{64}$/.test(value[BLOB_REF])
|
|
77
|
+
? value[BLOB_REF]
|
|
78
|
+
: null;
|
|
79
|
+
|
|
80
|
+
export const blobDigest = (value: unknown): string =>
|
|
81
|
+
createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
82
|
+
|
|
83
|
+
const toBlob = (value: unknown, blobs: BlobStore): unknown => {
|
|
84
|
+
if (!isObject(value)) return value;
|
|
85
|
+
const digest = blobDigest(value);
|
|
86
|
+
blobs.write(digest, value);
|
|
87
|
+
return { [BLOB_REF]: digest };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Replace candidate bodies in a record with blob references. */
|
|
91
|
+
export const encodeRecord = (
|
|
92
|
+
record: Record<string, unknown>,
|
|
93
|
+
blobs: BlobStore,
|
|
94
|
+
): Record<string, unknown> =>
|
|
95
|
+
Array.isArray(record.candidates)
|
|
96
|
+
? { ...record, candidates: record.candidates.map((item) => toBlob(item, blobs)) }
|
|
97
|
+
: record;
|
|
98
|
+
|
|
99
|
+
/** Replace candidate bodies inside patch ops with blob references. */
|
|
100
|
+
export const encodeOps = (ops: PatchOp[], blobs: BlobStore): PatchOp[] =>
|
|
101
|
+
ops.map((op) => {
|
|
102
|
+
if (op.path[0] !== "candidates") return op;
|
|
103
|
+
if (op.op === "push" && op.path.length === 1)
|
|
104
|
+
return { ...op, values: op.values.map((item) => toBlob(item, blobs)) };
|
|
105
|
+
if (op.op === "set" && op.path.length === 1 && Array.isArray(op.value))
|
|
106
|
+
return { ...op, value: op.value.map((item) => toBlob(item, blobs)) };
|
|
107
|
+
if (op.op === "set" && op.path.length === 2) return { ...op, value: toBlob(op.value, blobs) };
|
|
108
|
+
return op;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/** Resolve blob references anywhere in `value`. */
|
|
112
|
+
const decode = (value: unknown, blobs: BlobStore): unknown => {
|
|
113
|
+
const digest = blobRef(value);
|
|
114
|
+
if (digest) return blobs.read(digest);
|
|
115
|
+
if (Array.isArray(value)) return value.map((item) => decode(item, blobs));
|
|
116
|
+
if (isObject(value)) {
|
|
117
|
+
const out: Record<string, unknown> = {};
|
|
118
|
+
for (const [key, item] of Object.entries(value)) out[key] = decode(item, blobs);
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// reduction
|
|
126
|
+
|
|
127
|
+
/** Apply one event to `state` (null before the opening event). */
|
|
128
|
+
export function reduce(
|
|
129
|
+
state: ReducedTask | null,
|
|
130
|
+
event: StoreEvent,
|
|
131
|
+
blobs: BlobStore,
|
|
132
|
+
): ReducedTask | null {
|
|
133
|
+
const data = event.data;
|
|
134
|
+
const known = KNOWN_TYPES.has(event.type);
|
|
135
|
+
const carries = "record" in data || "ops" in data || "key" in data;
|
|
136
|
+
if (!known && !carries) {
|
|
137
|
+
if (event.critical === true)
|
|
138
|
+
throw new LogDamage(
|
|
139
|
+
`event ${event.type} was written by a newer Workit and must be understood; upgrade Workit to read this task`,
|
|
140
|
+
true,
|
|
141
|
+
);
|
|
142
|
+
return state;
|
|
143
|
+
}
|
|
144
|
+
let next = state;
|
|
145
|
+
if ("record" in data) {
|
|
146
|
+
if (!isObject(data.record))
|
|
147
|
+
throw new LogDamage(`event ${event.seq} carries a malformed record`);
|
|
148
|
+
next = {
|
|
149
|
+
record: decode(data.record, blobs) as Record<string, unknown>,
|
|
150
|
+
key: null,
|
|
151
|
+
legacy: null,
|
|
152
|
+
};
|
|
153
|
+
} else if (next === null) {
|
|
154
|
+
throw new LogDamage(`the log does not start with an opening event (found ${event.type})`);
|
|
155
|
+
} else next = { ...next };
|
|
156
|
+
if ("ops" in data) {
|
|
157
|
+
if (!isPatch(data.ops)) throw new LogDamage(`event ${event.seq} carries a malformed patch`);
|
|
158
|
+
try {
|
|
159
|
+
next.record = apply(
|
|
160
|
+
next.record,
|
|
161
|
+
decode(data.ops, blobs) as PatchOp[],
|
|
162
|
+
) as ReducedTask["record"];
|
|
163
|
+
} catch (error) {
|
|
164
|
+
throw new LogDamage(`event ${event.seq} does not apply: ${String(error)}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if ("key" in data) next.key = isKey(data.key) ? data.key : null;
|
|
168
|
+
if ("legacy" in data && isObject(data.legacy))
|
|
169
|
+
next.legacy = {
|
|
170
|
+
path: String(data.legacy.path ?? ""),
|
|
171
|
+
digest: String(data.legacy.digest ?? ""),
|
|
172
|
+
};
|
|
173
|
+
return next;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** A readable event type for a mutation (informational; replay does not use it). */
|
|
177
|
+
export function eventType(before: Record<string, unknown>, after: Record<string, unknown>): string {
|
|
178
|
+
const count = (value: unknown) => (Array.isArray(value) ? value.length : 0);
|
|
179
|
+
if (before.status !== after.status)
|
|
180
|
+
return after.status === "closed"
|
|
181
|
+
? "task.closed"
|
|
182
|
+
: after.status === "paused"
|
|
183
|
+
? "task.paused"
|
|
184
|
+
: "task.resumed";
|
|
185
|
+
if (count(after.evidence) > count(before.evidence)) {
|
|
186
|
+
const last = (after.evidence as Array<{ data?: { observation?: unknown } }>).at(-1);
|
|
187
|
+
return last?.data?.observation ? "check.ran" : "evidence.recorded";
|
|
188
|
+
}
|
|
189
|
+
if (count(after.findings) > count(before.findings)) return "finding.recorded";
|
|
190
|
+
if (count(after.decisions) > count(before.decisions)) return "decision.recorded";
|
|
191
|
+
if (JSON.stringify(before.findings) !== JSON.stringify(after.findings)) return "finding.updated";
|
|
192
|
+
if (JSON.stringify(before.decisions) !== JSON.stringify(after.decisions))
|
|
193
|
+
return "decision.updated";
|
|
194
|
+
if (JSON.stringify(before.workers) !== JSON.stringify(after.workers)) return "worker.updated";
|
|
195
|
+
if (JSON.stringify(before.intent) !== JSON.stringify(after.intent)) return "task.revised";
|
|
196
|
+
if (
|
|
197
|
+
JSON.stringify(before.assessments) !== JSON.stringify(after.assessments) ||
|
|
198
|
+
JSON.stringify(before.policy) !== JSON.stringify(after.policy)
|
|
199
|
+
)
|
|
200
|
+
return "task.judged";
|
|
201
|
+
if (JSON.stringify(before.progress) !== JSON.stringify(after.progress)) return "task.noted";
|
|
202
|
+
return "task.updated";
|
|
203
|
+
}
|
package/scripts/doctor-check.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
// DG-09: installers run the shared offline doctor after writing configuration and
|
|
3
|
-
// return nonzero when a required registration/config check fails. `installer:
|
|
4
|
-
// true` limits the enforced checks to the ones the installer just guaranteed, so
|
|
5
|
-
// a dev/stub checkout without built assets does not fail a post-install run.
|
|
6
|
-
// CA-02: `cursor --stale` is the installer's self-heal pre-check — it exits 2
|
|
7
|
-
// when the doctor's stale_install check fails and 0 otherwise. A
|
|
8
|
-
// registry-unreachable comparison warns as registry_unreachable and is NOT
|
|
9
|
-
// stale (CA-04): no false stale_install and no install failure. Other check
|
|
10
|
-
// failures are left to the post-install gate.
|
|
11
|
-
import { runDoctor } from "../src/core/doctor";
|
|
12
|
-
|
|
13
|
-
const hostArg = process.argv[2];
|
|
14
|
-
const host = hostArg === "cursor" ? "cursor" : hostArg === "cli" ? "cli" : "opencode";
|
|
15
|
-
const staleOnly = process.argv.includes("--stale");
|
|
16
|
-
const report = runDoctor({ host, installer: true });
|
|
17
|
-
|
|
18
|
-
if (staleOnly) {
|
|
19
|
-
const stale = report.checks.find((c) => c.id === "stale_install");
|
|
20
|
-
if (stale?.status === "fail") {
|
|
21
|
-
process.stderr.write(`STALE: ${stale.detail}\n`);
|
|
22
|
-
if (stale.fix) process.stderr.write(` fix: ${stale.fix}\n`);
|
|
23
|
-
process.exit(2);
|
|
24
|
-
}
|
|
25
|
-
process.exit(0);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (report.exitCode !== 0) {
|
|
29
|
-
for (const check of report.checks) {
|
|
30
|
-
if (check.status === "fail") {
|
|
31
|
-
process.stderr.write(`FATAL: workit doctor ${check.id}: ${check.detail}\n`);
|
|
32
|
-
if (check.fix) process.stderr.write(` fix: ${check.fix}\n`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
process.exit(0);
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { isConfigObject, mergePreset, type BranchPreset, type ToolkitConfig } from "./config";
|
|
5
|
-
import { writeFileAtomic } from "./safe-write";
|
|
6
|
-
|
|
7
|
-
export type ConversionInput = {
|
|
8
|
-
configDir: string;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export type ConversionMapping = {
|
|
12
|
-
key: string;
|
|
13
|
-
from: unknown;
|
|
14
|
-
to: unknown;
|
|
15
|
-
note?: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export type ConversionUnresolved = {
|
|
19
|
-
key: string;
|
|
20
|
-
reason: string;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export type ConversionPreview = {
|
|
24
|
-
mappings: ConversionMapping[];
|
|
25
|
-
preserved: string[];
|
|
26
|
-
unresolved: ConversionUnresolved[];
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const REDACT_KEYS = /token|secret|password|credential/i;
|
|
30
|
-
|
|
31
|
-
const readJsonObject = (file: string): Record<string, unknown> | null => {
|
|
32
|
-
if (!existsSync(file)) return null;
|
|
33
|
-
try {
|
|
34
|
-
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
35
|
-
return isConfigObject(parsed) ? parsed : null;
|
|
36
|
-
} catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const redactValue = (key: string, value: unknown): unknown => {
|
|
42
|
-
if (REDACT_KEYS.test(key)) return "[redacted]";
|
|
43
|
-
if (typeof value === "string" && value.length > 8 && !value.includes(" ")) return "[redacted]";
|
|
44
|
-
return value;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export const redactConversionPreview = (preview: ConversionPreview): ConversionPreview => ({
|
|
48
|
-
mappings: preview.mappings.map((m) => ({
|
|
49
|
-
...m,
|
|
50
|
-
from: redactValue(m.key, m.from),
|
|
51
|
-
to: redactValue(m.key, m.to),
|
|
52
|
-
})),
|
|
53
|
-
preserved: preview.preserved,
|
|
54
|
-
unresolved: preview.unresolved,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
export function previewConversion(input: ConversionInput): ConversionPreview {
|
|
58
|
-
const configFile = path.join(input.configDir, "config.json");
|
|
59
|
-
const config = readJsonObject(configFile);
|
|
60
|
-
const mappings: ConversionMapping[] = [];
|
|
61
|
-
const unresolved: ConversionUnresolved[] = [];
|
|
62
|
-
const preserved: string[] = [];
|
|
63
|
-
|
|
64
|
-
if (config) {
|
|
65
|
-
if (config.locale !== undefined) {
|
|
66
|
-
mappings.push({ key: "locale", from: config.locale, to: config.locale });
|
|
67
|
-
}
|
|
68
|
-
if (config.branchPolicy !== undefined && isConfigObject(config.branchPolicy)) {
|
|
69
|
-
const bp = config.branchPolicy as ToolkitConfig["branchPolicy"] & {
|
|
70
|
-
allowed?: string[];
|
|
71
|
-
protected?: string[];
|
|
72
|
-
};
|
|
73
|
-
const preset = bp.preset ?? "gitflow";
|
|
74
|
-
const merged = mergePreset(
|
|
75
|
-
preset,
|
|
76
|
-
{
|
|
77
|
-
allowed: Array.isArray(bp.allowed) ? bp.allowed : undefined,
|
|
78
|
-
protectedNames: Array.isArray(bp.protected) ? bp.protected : undefined,
|
|
79
|
-
},
|
|
80
|
-
{ branchPolicy: bp },
|
|
81
|
-
);
|
|
82
|
-
mappings.push({
|
|
83
|
-
key: "branchPolicy",
|
|
84
|
-
from: config.branchPolicy,
|
|
85
|
-
to: merged,
|
|
86
|
-
note: "preset-derived allowed/protected reset through mergePreset",
|
|
87
|
-
});
|
|
88
|
-
if (preset === "custom" && Array.isArray(bp.allowed)) {
|
|
89
|
-
if (bp.allowed.includes("*")) {
|
|
90
|
-
unresolved.push({
|
|
91
|
-
key: "branchPolicy.allowed",
|
|
92
|
-
reason: 'unsupported permissive value "*" requires an explicit v1 choice',
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if (config.workflowMode !== undefined) {
|
|
98
|
-
unresolved.push({
|
|
99
|
-
key: "legacyWorkflowMode",
|
|
100
|
-
reason: "0.x workflow mode has no automatic v1 equivalent; choose how to continue",
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
for (const name of ["youtrack.json", "vcs.json", "workspaces.json"]) {
|
|
106
|
-
const file = path.join(input.configDir, name);
|
|
107
|
-
if (!existsSync(file)) continue;
|
|
108
|
-
const parsed = readJsonObject(file);
|
|
109
|
-
if (parsed)
|
|
110
|
-
mappings.push({ key: name, from: parsed, to: parsed, note: "carry forward unchanged" });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
for (const token of ["youtrack.token", "gitlab.token", "github.token"]) {
|
|
114
|
-
const file = path.join(input.configDir, token);
|
|
115
|
-
if (existsSync(file)) preserved.push(file);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return { mappings, preserved, unresolved };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export const conversionDigest = (preview: ConversionPreview): string =>
|
|
122
|
-
createHash("sha256")
|
|
123
|
-
.update(JSON.stringify(redactConversionPreview(preview)))
|
|
124
|
-
.digest("hex");
|
|
125
|
-
|
|
126
|
-
export type ConversionApplyResult = {
|
|
127
|
-
configPath: string;
|
|
128
|
-
choicesPath: string;
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
/** Apply approved conversion mappings and record explicit resolution choices. */
|
|
132
|
-
export function applyConversionConfig(
|
|
133
|
-
configDir: string,
|
|
134
|
-
preview: ConversionPreview,
|
|
135
|
-
resolutions: Record<string, string>,
|
|
136
|
-
): ConversionApplyResult {
|
|
137
|
-
const configFile = path.join(configDir, "config.json");
|
|
138
|
-
const raw = readJsonObject(configFile) ?? {};
|
|
139
|
-
const next: Record<string, unknown> = { ...raw };
|
|
140
|
-
|
|
141
|
-
for (const mapping of preview.mappings) {
|
|
142
|
-
if (mapping.key === "locale") {
|
|
143
|
-
next[mapping.key] = mapping.to;
|
|
144
|
-
}
|
|
145
|
-
if (mapping.key === "branchPolicy" && !resolutions["branchPolicy.allowed"]) {
|
|
146
|
-
next.branchPolicy = mapping.to;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (resolutions["branchPolicy.allowed"]) {
|
|
151
|
-
const bp = isConfigObject(next.branchPolicy)
|
|
152
|
-
? (next.branchPolicy as ToolkitConfig["branchPolicy"])
|
|
153
|
-
: { preset: "custom" as BranchPreset, allowed: [], protected: [] };
|
|
154
|
-
const preset = bp.preset ?? "custom";
|
|
155
|
-
next.branchPolicy = mergePreset(
|
|
156
|
-
preset,
|
|
157
|
-
{
|
|
158
|
-
allowed: resolutions["branchPolicy.allowed"]
|
|
159
|
-
.split(",")
|
|
160
|
-
.map((s) => s.trim())
|
|
161
|
-
.filter(Boolean),
|
|
162
|
-
},
|
|
163
|
-
{ branchPolicy: bp },
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (resolutions["legacyWorkflowMode"]) {
|
|
168
|
-
delete next.workflowMode;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
writeFileAtomic(configFile, JSON.stringify(next, null, 2) + "\n");
|
|
172
|
-
|
|
173
|
-
const choicesPath = path.join(configDir, "cutover-choices.json");
|
|
174
|
-
writeFileAtomic(
|
|
175
|
-
choicesPath,
|
|
176
|
-
JSON.stringify({ resolutions, applied_at: new Date().toISOString() }, null, 2) + "\n",
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
return { configPath: configFile, choicesPath };
|
|
180
|
-
}
|