create-harness-vibe-coding 0.8.13 → 0.8.16
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/CHANGELOG.md +165 -0
- package/README-CN.md +41 -9
- package/README.md +39 -9
- package/package.json +2 -1
- package/src/generator.js +601 -472
- package/src/index.js +4 -4
- package/templates/common/.claude/agents/architect-manager.md +1 -0
- package/templates/common/.claude/agents/architect.md +2 -1
- package/templates/common/.claude/agents/codebase-explorer.md +1 -0
- package/templates/common/.claude/agents/context-master.md +2 -1
- package/templates/common/.claude/agents/debugger.md +1 -0
- package/templates/common/.claude/agents/docs-researcher.md +2 -1
- package/templates/common/.claude/agents/explore-manager.md +1 -0
- package/templates/common/.claude/agents/implement-manager.md +1 -0
- package/templates/common/.claude/agents/implementer.md +1 -0
- package/templates/common/.claude/agents/memory-master.md +2 -1
- package/templates/common/.claude/agents/planner.md +3 -2
- package/templates/common/.claude/agents/reflector.md +1 -0
- package/templates/common/.claude/agents/researcher.md +1 -0
- package/templates/common/.claude/agents/review-manager.md +1 -0
- package/templates/common/.claude/agents/reviewer.md +2 -1
- package/templates/common/.claude/agents/task-scribe.md +1 -0
- package/templates/common/.claude/agents/tdd-guide.md +5 -4
- package/templates/common/.claude/agents/test-writer.md +6 -5
- package/templates/common/.claude/agents/verifier.md +1 -0
- package/templates/common/.claude/commands/wf-help.md +1 -0
- package/templates/common/.claude/commands/wf-update.md +68 -10
- package/templates/common/.claude/rules/ecc/common.md +6 -5
- package/templates/common/.claude/skills/subagent-orchestrator/SKILL.md +12 -6
- package/templates/common/.claude/skills/tdd/SKILL.md +5 -5
- package/templates/common/.claude/skills/wf/SKILL.md +13 -5
- package/templates/common/.claude/skills/wf-agents-docs/SKILL.md +119 -0
- package/templates/common/.claude/skills/wf-auto/SKILL.md +27 -7
- package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +12 -5
- package/templates/common/.claude/skills/wf-learn/SKILL.md +6 -0
- package/templates/common/.claude/skills/wf-max/SKILL.md +22 -7
- package/templates/common/.claude/skills/wf-readme/SKILL.md +8 -2
- package/templates/common/.claude/skills/wf-remove/SKILL.md +6 -0
- package/templates/common/.claude/skills/wf-review/SKILL.md +10 -3
- package/templates/common/.claude/skills/wf-update/SKILL.md +41 -4
- package/templates/common/.harness-version +290 -128
- package/templates/common/.opencode/agents/architect-manager.md +1 -0
- package/templates/common/.opencode/agents/architect.md +2 -1
- package/templates/common/.opencode/agents/codebase-explorer.md +1 -0
- package/templates/common/.opencode/agents/context-master.md +2 -1
- package/templates/common/.opencode/agents/debugger.md +1 -0
- package/templates/common/.opencode/agents/docs-researcher.md +2 -1
- package/templates/common/.opencode/agents/explore-manager.md +1 -0
- package/templates/common/.opencode/agents/implement-manager.md +1 -0
- package/templates/common/.opencode/agents/implementer.md +1 -0
- package/templates/common/.opencode/agents/memory-master.md +2 -1
- package/templates/common/.opencode/agents/planner.md +3 -2
- package/templates/common/.opencode/agents/reflector.md +1 -0
- package/templates/common/.opencode/agents/researcher.md +1 -0
- package/templates/common/.opencode/agents/review-manager.md +1 -0
- package/templates/common/.opencode/agents/reviewer.md +2 -1
- package/templates/common/.opencode/agents/task-scribe.md +1 -0
- package/templates/common/.opencode/agents/tdd-guide.md +5 -4
- package/templates/common/.opencode/agents/test-writer.md +6 -5
- package/templates/common/.opencode/agents/verifier.md +1 -0
- package/templates/common/.opencode/commands/wf-auto-spark.md +3 -2
- package/templates/common/.opencode/commands/wf-auto.md +3 -2
- package/templates/common/.opencode/commands/wf-help.md +1 -0
- package/templates/common/.opencode/commands/wf-learn.md +3 -2
- package/templates/common/.opencode/commands/wf-max.md +3 -2
- package/templates/common/.opencode/commands/wf-readme.md +3 -2
- package/templates/common/.opencode/commands/wf-remove.md +3 -2
- package/templates/common/.opencode/commands/wf-review.md +3 -2
- package/templates/common/.opencode/commands/wf-update.md +68 -10
- package/templates/common/.opencode/commands/wf.md +3 -2
- package/templates/common/CLAUDE.md +14 -12
- package/templates/common/Harness/MEMORY.md +21 -18
- package/templates/common/Harness/README.md +41 -39
- package/templates/common/Harness/ownership.manifest.json +815 -0
- package/templates/common/Harness/{architecture.md → project/architecture.md} +1 -1
- package/templates/common/Harness/research/README.md +3 -3
- package/templates/common/Harness/scripts/context-budget.mjs +95 -0
- package/templates/common/Harness/scripts/l2-cache-telemetry.mjs +703 -0
- package/templates/common/Harness/scripts/scan-clean.mjs +13 -3
- package/templates/common/Harness/scripts/validate-harness.mjs +332 -175
- package/templates/common/Harness/scripts/wf-remove.mjs +60 -34
- package/templates/common/Harness/scripts/wf-update-check.mjs +355 -45
- package/templates/common/Harness/settings.json +43 -0
- package/templates/common/Harness/{ECC-GUIDE.md → specs/guides/ECC-GUIDE.md} +4 -4
- package/templates/common/Harness/{SETUP.md → specs/guides/SETUP.md} +32 -35
- package/templates/common/Harness/{extension.md → specs/guides/extension.md} +3 -3
- package/templates/common/Harness/{lifecycle.md → specs/guides/lifecycle.md} +2 -2
- package/templates/common/Harness/{agent-workflow.md → specs/runtime/agent-workflow.md} +6 -6
- package/templates/common/Harness/{context-loading.md → specs/runtime/context-loading.md} +85 -21
- package/templates/common/Harness/{dispatch.md → specs/runtime/dispatch.md} +3 -2
- package/templates/common/Harness/{subagents.md → specs/runtime/subagents.md} +10 -5
- package/templates/common/Harness/{WF-AUTO-SPARK.md → specs/workflows/WF-AUTO-SPARK.md} +2 -2
- package/templates/common/Harness/{WF-AUTO.md → specs/workflows/WF-AUTO.md} +6 -1
- package/templates/common/Harness/{WF-KERNEL.md → specs/workflows/WF-KERNEL.md} +10 -0
- package/templates/common/Harness/{WF-MAX.md → specs/workflows/WF-MAX.md} +10 -5
- package/templates/common/Harness/{WF-STATE.md → specs/workflows/WF-STATE.md} +5 -0
- package/templates/common/Harness/{WF.md → specs/workflows/WF.md} +12 -1
- package/templates/common/README.md +8 -6
- package/templates/common/memory/startup-hints.md +19 -17
- package/templates/optional/skills/browser-e2e/.claude/skills/browser-e2e/SKILL.md +1 -1
- package/templates/optional/skills/browser-e2e/.claude/skills/wf-browser/SKILL.md +7 -0
- package/templates/optional/skills/browser-e2e/.opencode/commands/wf-browser.md +3 -2
- package/templates/optional/skills/browser-e2e/Harness/workflows/browser-e2e.md +4 -4
- package/templates/optional/skills/github-pr-review/.claude/skills/github-pr-review/SKILL.md +1 -1
- package/templates/optional/skills/python-backend/.claude/skills/python-backend/SKILL.md +1 -1
- package/templates/optional/skills/ts-react-frontend/.claude/skills/ts-react-frontend/SKILL.md +1 -1
- package/templates/optional/skills/ui-ux-review/.claude/skills/ui-ux-review/SKILL.md +1 -1
- /package/templates/common/Harness/{ACCEPTANCE_PROTOCOL.md → specs/protocols/ACCEPTANCE_PROTOCOL.md} +0 -0
- /package/templates/common/Harness/{AGENT_ISOLATION.md → specs/protocols/AGENT_ISOLATION.md} +0 -0
- /package/templates/common/Harness/{DEBUG_PROTOCOL.md → specs/protocols/DEBUG_PROTOCOL.md} +0 -0
- /package/templates/common/Harness/{HARNESS_BRIDGE.md → specs/protocols/HARNESS_BRIDGE.md} +0 -0
- /package/templates/common/Harness/{MEMORY_PROTOCOL.md → specs/protocols/MEMORY_PROTOCOL.md} +0 -0
- /package/templates/common/Harness/{TASK_ARCHIVE.md → specs/protocols/TASK_ARCHIVE.md} +0 -0
- /package/templates/common/Harness/{TDD-GUIDE.md → specs/protocols/TDD-GUIDE.md} +0 -0
- /package/templates/common/Harness/{WF-AUTO-ANGLES.md → specs/workflows/WF-AUTO-ANGLES.md} +0 -0
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* --full-plan and --verbose are aliases.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, lstatSync } from 'fs';
|
|
18
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, lstatSync, unlinkSync } from 'fs';
|
|
19
19
|
import { createHash } from 'crypto';
|
|
20
20
|
import { resolve, dirname, sep } from 'path';
|
|
21
21
|
import { fileURLToPath } from 'url';
|
|
@@ -46,6 +46,7 @@ const PRESERVE_PATTERNS = [
|
|
|
46
46
|
/^Harness\/research\/PRD\.md$/,
|
|
47
47
|
/^Harness\/research\/research-results\.md$/,
|
|
48
48
|
/^Harness\/architecture\.md$/,
|
|
49
|
+
/^Harness\/project\/architecture\.md$/,
|
|
49
50
|
/^README\.md$/,
|
|
50
51
|
/^\.gitignore$/,
|
|
51
52
|
/^package\.json$/,
|
|
@@ -59,6 +60,7 @@ const MERGE_PATTERNS = [
|
|
|
59
60
|
/^MEMORY\.md$/,
|
|
60
61
|
/^Harness\/MEMORY\.md$/,
|
|
61
62
|
/^Harness\/README\.md$/,
|
|
63
|
+
/^Harness\/settings\.json$/,
|
|
62
64
|
];
|
|
63
65
|
|
|
64
66
|
const OPTIONAL_REGISTRATION_FILES = new Set([
|
|
@@ -67,9 +69,25 @@ const OPTIONAL_REGISTRATION_FILES = new Set([
|
|
|
67
69
|
'Harness/README.md',
|
|
68
70
|
]);
|
|
69
71
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
]
|
|
72
|
+
const HARNESS_OWNED_CANDIDATE_PATTERNS = [
|
|
73
|
+
/^\.claude\/agents\/[^/]+\.md$/,
|
|
74
|
+
/^\.opencode\/agents\/[^/]+\.md$/,
|
|
75
|
+
/^\.claude\/commands\/wf(?:-[^/]+)?\.md$/,
|
|
76
|
+
/^\.opencode\/commands\/wf(?:-[^/]+)?\.md$/,
|
|
77
|
+
/^\.claude\/skills\/(?:wf|wf-[^/]+|subagent-orchestrator|tdd)\/SKILL\.md$/,
|
|
78
|
+
/^\.agents\/skills\/(?:wf|wf-[^/]+|subagent-orchestrator|tdd)\/SKILL\.md$/,
|
|
79
|
+
/^\.codex\//,
|
|
80
|
+
/^\.opencode\/plugins\/harness-/,
|
|
81
|
+
/^Harness\/scripts\//,
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const HARNESS_OWNED_CONTENT_MARKERS = [
|
|
85
|
+
/^harness:\s*(?:wf-agent|wf-framework|create-harness-vibe-coding)\b/im,
|
|
86
|
+
/\bcreate-harness-vibe-coding\b/i,
|
|
87
|
+
/\bproject harness\b/i,
|
|
88
|
+
/\bHarness\/(?:specs|WF|MEMORY|tasks|scripts|subagents|dispatch|context-loading|lifecycle|SETUP)\b/,
|
|
89
|
+
/\bWF-(?:MAX|AUTO|KERNEL|STATE)\b/,
|
|
90
|
+
];
|
|
73
91
|
|
|
74
92
|
// Helpers
|
|
75
93
|
|
|
@@ -157,18 +175,146 @@ function sha256File(path) {
|
|
|
157
175
|
return sha256(content);
|
|
158
176
|
}
|
|
159
177
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
178
|
+
// ── Ownership manifest (manifest-first classification) ─────────────────────
|
|
179
|
+
//
|
|
180
|
+
// Harness/ownership.manifest.json is the machine-readable source of truth for
|
|
181
|
+
// file classification. When present it drives PRESERVE/MERGE/frameworkOwned/
|
|
182
|
+
// optionalOwned decisions; the path-regex + content-marker logic below remains
|
|
183
|
+
// as a FALLBACK for old installs that ship no manifest. The updater cannot
|
|
184
|
+
// import from scripts/lib (different package layer), so a small local glob
|
|
185
|
+
// matcher mirrors the one in scripts/lib/ownership-manifest.mjs.
|
|
186
|
+
|
|
187
|
+
function globToRegex(pattern) {
|
|
188
|
+
let re = '';
|
|
189
|
+
let i = 0;
|
|
190
|
+
while (i < pattern.length) {
|
|
191
|
+
const c = pattern[i];
|
|
192
|
+
if (c === '*' && pattern[i + 1] === '*') {
|
|
193
|
+
re += '.*';
|
|
194
|
+
i += 2;
|
|
195
|
+
if (pattern[i] === '/') i += 1; // swallow a trailing slash after **
|
|
196
|
+
} else if (c === '*') {
|
|
197
|
+
re += '[^/]*';
|
|
198
|
+
i += 1;
|
|
199
|
+
} else if ('.+?^${}()|[]\\'.includes(c)) {
|
|
200
|
+
re += '\\' + c;
|
|
201
|
+
i += 1;
|
|
202
|
+
} else {
|
|
203
|
+
re += c;
|
|
204
|
+
i += 1;
|
|
205
|
+
}
|
|
164
206
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
207
|
+
return new RegExp('^' + re + '$');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function matchesGlob(dest, pattern) {
|
|
211
|
+
if (!pattern.includes('*')) return dest === pattern;
|
|
212
|
+
return globToRegex(pattern).test(dest);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function matchesAnyGlob(dest, patterns) {
|
|
216
|
+
return (patterns || []).some(p => matchesGlob(dest, p));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function buildOptionalOwnedMap(optionalOwned) {
|
|
220
|
+
const map = new Map();
|
|
221
|
+
for (const entry of optionalOwned || []) {
|
|
222
|
+
if (!entry || typeof entry.option !== 'string') continue;
|
|
223
|
+
map.set(entry.option, new Set(entry.paths || []));
|
|
224
|
+
}
|
|
225
|
+
return map;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Load + validate the ownership manifest. Missing/unreadable/invalid -> null (fallback mode). */
|
|
229
|
+
function loadOwnershipManifest(root) {
|
|
230
|
+
const manifestPath = resolve(root, 'Harness', 'ownership.manifest.json');
|
|
231
|
+
try {
|
|
232
|
+
if (!existsSync(manifestPath)) return null;
|
|
233
|
+
const parsed = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
234
|
+
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.preserve)) return null;
|
|
235
|
+
return parsed;
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function buildManifestCtx(manifest) {
|
|
242
|
+
if (!manifest) {
|
|
243
|
+
return { present: false, preserveGlobs: [], mergeSet: new Set(), frameworkOwnedSet: new Set(), optionalOwned: new Map() };
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
present: true,
|
|
247
|
+
preserveGlobs: manifest.preserve || [],
|
|
248
|
+
mergeSet: new Set(manifest.merge || []),
|
|
249
|
+
frameworkOwnedSet: new Set((manifest.frameworkOwned || []).map(e => e && e.path).filter(Boolean)),
|
|
250
|
+
optionalOwned: buildOptionalOwnedMap(manifest.optionalOwned),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function isPreserveFile(file, ctx) {
|
|
255
|
+
if (ctx.present) return matchesAnyGlob(file, ctx.preserveGlobs);
|
|
256
|
+
return PRESERVE_PATTERNS.some(p => p.test(file));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function isMergeFile(file, ctx) {
|
|
260
|
+
if (ctx.present) return ctx.mergeSet.has(file);
|
|
261
|
+
return MERGE_PATTERNS.some(p => p.test(file));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Manifest-first declared ownership: frameworkOwned, or optionalOwned whose option is selected. */
|
|
265
|
+
function isDeclaredHarnessOwned(file, ctx, selectedOptions) {
|
|
266
|
+
if (!ctx.present) return false;
|
|
267
|
+
if (ctx.frameworkOwnedSet.has(file)) return true;
|
|
268
|
+
for (const [option, paths] of ctx.optionalOwned) {
|
|
269
|
+
if (paths.has(file) && selectedOptions.has(option)) return true;
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** An optional-owned file whose owning option is NOT selected for this install. */
|
|
275
|
+
function isOptionalOwnedUnselected(file, ctx, selectedOptions) {
|
|
276
|
+
if (!ctx.present) return false;
|
|
277
|
+
let owned = false;
|
|
278
|
+
for (const [option, paths] of ctx.optionalOwned) {
|
|
279
|
+
if (paths.has(file)) {
|
|
280
|
+
owned = true;
|
|
281
|
+
if (selectedOptions.has(option)) return false;
|
|
170
282
|
}
|
|
171
283
|
}
|
|
284
|
+
return owned;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Manifest-first untracked-existing-file ownership. When a remote file is NEW
|
|
289
|
+
* (not in local checksums) but exists on disk, decide Harness-owned vs
|
|
290
|
+
* user-owned. The MARKER decides instance ownership: a prior Harness install
|
|
291
|
+
* leaves a marker, a user-authored file at a Harness-looking path does not.
|
|
292
|
+
* Manifest declaration (frameworkOwned / installed-optional) and the candidate
|
|
293
|
+
* regex only build the Harness-interest candidate set; they never by themselves
|
|
294
|
+
* authorize overwriting an untracked file. A user file at a Harness-interest
|
|
295
|
+
* path with NO marker stays a conflict (protected) in BOTH manifest and
|
|
296
|
+
* fallback modes. This matches the installer and AC #5: a user's same-name
|
|
297
|
+
* agent/command/skill must never be overwritten.
|
|
298
|
+
*/
|
|
299
|
+
function isHarnessOwnedExistingFileManifestFirst(file, diskPath, ctx, selectedOptions) {
|
|
300
|
+
// A manifest-declared path joins the candidate set, but — like a regex
|
|
301
|
+
// candidate — still requires a Harness content marker to be adopted. The
|
|
302
|
+
// marker (not the declaration) decides adopt-vs-conflict for an untracked
|
|
303
|
+
// existing file, in both manifest and fallback modes.
|
|
304
|
+
if (isDeclaredHarnessOwned(file, ctx, selectedOptions)) {
|
|
305
|
+
return fileHasHarnessOwnedMarker(diskPath);
|
|
306
|
+
}
|
|
307
|
+
return isHarnessOwnedExistingFile(file, diskPath);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function classify(file, localHash, storedHash, ctx) {
|
|
311
|
+
// PRESERVE
|
|
312
|
+
if (isPreserveFile(file, ctx)) return 'PRESERVE';
|
|
313
|
+
// MERGE: dual-purpose, check if user modified.
|
|
314
|
+
if (isMergeFile(file, ctx)) {
|
|
315
|
+
if (localHash === storedHash) return 'SAFE'; // unmodified, safe
|
|
316
|
+
return 'CONFLICT'; // user modified, needs decision
|
|
317
|
+
}
|
|
172
318
|
// Everything else is SAFE runtime file: always overwrite.
|
|
173
319
|
// Harness system files (scripts, skills, agents, commands, WF docs) are
|
|
174
320
|
// not user data; the template is authoritative. Only PRESERVE and MERGE
|
|
@@ -176,6 +322,25 @@ function classify(file, localHash, storedHash) {
|
|
|
176
322
|
return 'SAFE';
|
|
177
323
|
}
|
|
178
324
|
|
|
325
|
+
function isHarnessOwnedCandidate(file) {
|
|
326
|
+
return HARNESS_OWNED_CANDIDATE_PATTERNS.some(pattern => pattern.test(file));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Read on-disk content and test for a Harness ownership marker. */
|
|
330
|
+
function fileHasHarnessOwnedMarker(diskPath) {
|
|
331
|
+
try {
|
|
332
|
+
const content = readFileSync(diskPath, 'utf-8');
|
|
333
|
+
return HARNESS_OWNED_CONTENT_MARKERS.some(pattern => pattern.test(content));
|
|
334
|
+
} catch {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function isHarnessOwnedExistingFile(file, diskPath) {
|
|
340
|
+
if (!isHarnessOwnedCandidate(file)) return false;
|
|
341
|
+
return fileHasHarnessOwnedMarker(diskPath);
|
|
342
|
+
}
|
|
343
|
+
|
|
179
344
|
async function fetchRemote(url, timeoutMs = 30000) {
|
|
180
345
|
if (url.startsWith('file://')) {
|
|
181
346
|
return readFileSync(fileURLToPath(url), 'utf-8');
|
|
@@ -438,6 +603,12 @@ async function main() {
|
|
|
438
603
|
|
|
439
604
|
const localChecksums = localVersion.checksums || {};
|
|
440
605
|
|
|
606
|
+
// Ownership manifest: manifest-first classification with regex/marker fallback
|
|
607
|
+
// for old installs that ship no manifest. manifest=null -> fallback mode.
|
|
608
|
+
const manifest = loadOwnershipManifest(ROOT);
|
|
609
|
+
const manifestCtx = buildManifestCtx(manifest);
|
|
610
|
+
const selectedOptions = selectedOptionIds(localVersion);
|
|
611
|
+
|
|
441
612
|
// 2. Read remote version metadata from the selected update source.
|
|
442
613
|
let remoteVersion = source?.manifest;
|
|
443
614
|
if (!remoteVersion) {
|
|
@@ -493,6 +664,18 @@ async function main() {
|
|
|
493
664
|
|
|
494
665
|
const remoteChecksums = remoteVersion.checksums || {};
|
|
495
666
|
const remoteSources = remoteVersion.sources || {};
|
|
667
|
+
const remoteMoves = Array.isArray(remoteVersion.moves)
|
|
668
|
+
? remoteVersion.moves
|
|
669
|
+
.map(move => ({
|
|
670
|
+
from: canonicalPath(move?.from || ''),
|
|
671
|
+
to: canonicalPath(move?.to || ''),
|
|
672
|
+
deleteOldIfChecksumMatches: move?.deleteOldIfChecksumMatches !== false,
|
|
673
|
+
preserveOldIfModified: move?.preserveOldIfModified !== false,
|
|
674
|
+
}))
|
|
675
|
+
.filter(move => move.from && move.to && remoteChecksums[move.to])
|
|
676
|
+
: [];
|
|
677
|
+
const moveByFrom = new Map(remoteMoves.map(move => [move.from, move]));
|
|
678
|
+
const movedToFiles = new Set();
|
|
496
679
|
localVersion.acceptedConflicts = localVersion.acceptedConflicts || {};
|
|
497
680
|
|
|
498
681
|
/** Resolve the remote template-relative path for a dest-keyed file. Falls back to the key itself for back-compat. */
|
|
@@ -510,6 +693,20 @@ async function main() {
|
|
|
510
693
|
return source.base + templateRelPath;
|
|
511
694
|
}
|
|
512
695
|
|
|
696
|
+
function releaseHighlights(versionObj) {
|
|
697
|
+
const highlights = versionObj?.releaseNotes?.highlights;
|
|
698
|
+
return Array.isArray(highlights)
|
|
699
|
+
? highlights.map(String).map(s => s.trim()).filter(Boolean)
|
|
700
|
+
: [];
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function printReleaseHighlights(versionObj) {
|
|
704
|
+
const highlights = releaseHighlights(versionObj).slice(0, 6);
|
|
705
|
+
if (highlights.length === 0) return;
|
|
706
|
+
console.log('\nRelease highlights:');
|
|
707
|
+
for (const item of highlights) console.log(` - ${item}`);
|
|
708
|
+
}
|
|
709
|
+
|
|
513
710
|
function withRemoteMeta(entry) {
|
|
514
711
|
if (!entry || !entry.file || !remoteChecksums[entry.file]) return entry;
|
|
515
712
|
const templateHint = remotePath(entry.file);
|
|
@@ -544,6 +741,7 @@ async function main() {
|
|
|
544
741
|
adopted: plan.adopted.map(withRemoteMeta),
|
|
545
742
|
conflict: verbose ? plan.conflict.map(withConflictActions) : plan.conflict.map(withConflictActions).slice(0, 5),
|
|
546
743
|
conflictTruncated: !verbose && plan.conflict.length > 5 ? plan.conflict.length - 5 : undefined,
|
|
744
|
+
moved: plan.moved.map(withRemoteMeta),
|
|
547
745
|
skipped: plan.skipped.map(withRemoteMeta),
|
|
548
746
|
};
|
|
549
747
|
}
|
|
@@ -563,8 +761,12 @@ async function main() {
|
|
|
563
761
|
safeApplyCommand: 'node Harness/scripts/wf-update-check.mjs --apply-safe',
|
|
564
762
|
strictApplyCommand: 'node Harness/scripts/wf-update-check.mjs --apply',
|
|
565
763
|
finalizeCommand: 'node Harness/scripts/wf-update-check.mjs --finalize',
|
|
764
|
+
pathMoves: remoteMoves,
|
|
566
765
|
partialUpdate: localVersion.partialUpdate || null,
|
|
567
766
|
acceptedConflicts: localVersion.acceptedConflicts || {},
|
|
767
|
+
releaseNotes: remoteVersion.releaseNotes || null,
|
|
768
|
+
releaseHighlights: releaseHighlights(remoteVersion),
|
|
769
|
+
updateReportRequired: 'After apply/finalize, tell the user the version, safe/new/conflict counts, validation results, and the core releaseHighlights from this update.',
|
|
568
770
|
// aiMergeRequired array is attached only in verbose mode to keep default output token-safe.
|
|
569
771
|
...(verbose ? { aiMergeRequired: conflicts } : {}),
|
|
570
772
|
aiMergeRequiredCount: totalConflicts,
|
|
@@ -615,12 +817,18 @@ async function main() {
|
|
|
615
817
|
const decision = localVersion.acceptedConflicts?.[file];
|
|
616
818
|
return Boolean(
|
|
617
819
|
decision
|
|
618
|
-
&& decision.targetGenerator === remoteGen
|
|
619
820
|
&& decision.localHash === localHash
|
|
620
821
|
&& decision.remoteHash === remoteHash
|
|
621
822
|
);
|
|
622
823
|
}
|
|
623
824
|
|
|
825
|
+
function acceptedDecisionReason(file) {
|
|
826
|
+
const decision = localVersion.acceptedConflicts?.[file];
|
|
827
|
+
if (!decision) return 'accepted conflict decision';
|
|
828
|
+
if (decision.targetGenerator === remoteGen) return `accepted conflict decision: ${decision.decision}`;
|
|
829
|
+
return `accepted conflict decision carried forward from ${decision.targetGenerator}: ${decision.decision}`;
|
|
830
|
+
}
|
|
831
|
+
|
|
624
832
|
let decisionMetadataDirty = false;
|
|
625
833
|
if (acceptLocal.length || acceptMerged.length || acceptTemplate.length) {
|
|
626
834
|
const appliedAt = new Date().toISOString();
|
|
@@ -642,10 +850,11 @@ async function main() {
|
|
|
642
850
|
|
|
643
851
|
const allFiles = new Set([...Object.keys(localChecksums), ...Object.keys(remoteChecksums)]);
|
|
644
852
|
|
|
645
|
-
const plan = { updated: [], created: [], adopted: [], conflict: [], skipped: [] };
|
|
853
|
+
const plan = { updated: [], created: [], adopted: [], moved: [], conflict: [], skipped: [] };
|
|
646
854
|
|
|
647
855
|
for (const file of [...allFiles].sort()) {
|
|
648
856
|
const canonical = canonicalPath(file);
|
|
857
|
+
if (movedToFiles.has(canonical)) continue;
|
|
649
858
|
|
|
650
859
|
// Reject paths that escape ROOT before any file access
|
|
651
860
|
const diskPath = safePath(file);
|
|
@@ -658,6 +867,47 @@ async function main() {
|
|
|
658
867
|
const storedHash = localChecksums[file];
|
|
659
868
|
const remoteHash = remoteChecksums[file];
|
|
660
869
|
|
|
870
|
+
const move = moveByFrom.get(canonical);
|
|
871
|
+
if (move && storedHash && !remoteHash) {
|
|
872
|
+
const toDiskPath = safePath(move.to);
|
|
873
|
+
if (!toDiskPath) {
|
|
874
|
+
plan.skipped.push({ file, movedTo: move.to, reason: 'path move target traversal rejected' });
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
const toLocalHash = sha256File(toDiskPath);
|
|
878
|
+
const toRemoteHash = remoteChecksums[move.to];
|
|
879
|
+
if (localHash && localHash !== storedHash) {
|
|
880
|
+
plan.skipped.push({
|
|
881
|
+
file,
|
|
882
|
+
movedTo: move.to,
|
|
883
|
+
reason: 'legacy moved file modified; preserved for manual review',
|
|
884
|
+
});
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
if (toLocalHash && toLocalHash !== toRemoteHash) {
|
|
888
|
+
plan.skipped.push({
|
|
889
|
+
file,
|
|
890
|
+
movedTo: move.to,
|
|
891
|
+
reason: 'legacy moved file preserved until canonical path conflict is resolved',
|
|
892
|
+
});
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
plan.moved.push({
|
|
896
|
+
file: move.to,
|
|
897
|
+
from: file,
|
|
898
|
+
to: move.to,
|
|
899
|
+
localHash,
|
|
900
|
+
storedHash,
|
|
901
|
+
remoteHash: toRemoteHash,
|
|
902
|
+
write: toLocalHash !== toRemoteHash,
|
|
903
|
+
reason: toLocalHash === toRemoteHash
|
|
904
|
+
? 'legacy framework path cleanup; canonical file already current'
|
|
905
|
+
: 'safe framework path move',
|
|
906
|
+
});
|
|
907
|
+
movedToFiles.add(move.to);
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
|
|
661
911
|
if (!remoteHash) {
|
|
662
912
|
if (storedHash && isInstalledOptionalFile(file, localVersion)) {
|
|
663
913
|
plan.skipped.push({ file, reason: 'installed optional workflow file' });
|
|
@@ -667,8 +917,11 @@ async function main() {
|
|
|
667
917
|
continue;
|
|
668
918
|
}
|
|
669
919
|
|
|
670
|
-
|
|
671
|
-
|
|
920
|
+
// Manifest-first: an optional-owned file whose option is NOT selected for
|
|
921
|
+
// this install is not in this install's plan; skip it rather than
|
|
922
|
+
// force-applying an optional workflow the user never opted into.
|
|
923
|
+
if (isOptionalOwnedUnselected(canonical, manifestCtx, selectedOptions)) {
|
|
924
|
+
plan.skipped.push({ file, reason: 'optional workflow not selected for this install' });
|
|
672
925
|
continue;
|
|
673
926
|
}
|
|
674
927
|
|
|
@@ -680,16 +933,22 @@ async function main() {
|
|
|
680
933
|
continue;
|
|
681
934
|
}
|
|
682
935
|
if (acceptedDecisionMatches(file, localHash, remoteHash)) {
|
|
683
|
-
plan.skipped.push({ file, reason:
|
|
936
|
+
plan.skipped.push({ file, reason: acceptedDecisionReason(file) });
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
if (isHarnessOwnedExistingFileManifestFirst(canonical, diskPath, manifestCtx, selectedOptions)) {
|
|
940
|
+
plan.updated.push({ file, localHash, remoteHash, reason: 'existing untracked Harness-owned file' });
|
|
684
941
|
continue;
|
|
685
942
|
}
|
|
686
943
|
plan.conflict.push({ file, localHash, storedHash: 'none', remoteHash, reason: 'new remote file conflicts with existing local file' });
|
|
687
944
|
continue;
|
|
688
945
|
}
|
|
689
|
-
// New file: still respect PRESERVE classification
|
|
690
|
-
|
|
946
|
+
// New file: still respect PRESERVE classification for existing user data,
|
|
947
|
+
// but create missing scaffold starter files. PRESERVE means "do not
|
|
948
|
+
// overwrite local user data", not "leave required new docs absent".
|
|
949
|
+
const tier = classify(canonical, null, null, manifestCtx);
|
|
691
950
|
if (tier === 'PRESERVE') {
|
|
692
|
-
plan.
|
|
951
|
+
plan.created.push({ file, remoteHash, reason: 'PRESERVE: missing scaffold starter; no local user data to overwrite' });
|
|
693
952
|
} else {
|
|
694
953
|
plan.created.push({ file, remoteHash });
|
|
695
954
|
}
|
|
@@ -704,7 +963,7 @@ async function main() {
|
|
|
704
963
|
&& localHash !== remoteHash
|
|
705
964
|
) {
|
|
706
965
|
if (acceptedDecisionMatches(file, localHash, remoteHash)) {
|
|
707
|
-
plan.skipped.push({ file, reason:
|
|
966
|
+
plan.skipped.push({ file, reason: acceptedDecisionReason(file) });
|
|
708
967
|
continue;
|
|
709
968
|
}
|
|
710
969
|
plan.conflict.push({
|
|
@@ -717,7 +976,7 @@ async function main() {
|
|
|
717
976
|
continue;
|
|
718
977
|
}
|
|
719
978
|
|
|
720
|
-
const tier = classify(canonical, localHash, storedHash);
|
|
979
|
+
const tier = classify(canonical, localHash, storedHash, manifestCtx);
|
|
721
980
|
|
|
722
981
|
if (tier === 'PRESERVE') {
|
|
723
982
|
plan.skipped.push({ file, reason: 'PRESERVE: user data' });
|
|
@@ -729,7 +988,7 @@ async function main() {
|
|
|
729
988
|
}
|
|
730
989
|
} else if (tier === 'CONFLICT') {
|
|
731
990
|
if (acceptedDecisionMatches(file, localHash, remoteHash)) {
|
|
732
|
-
plan.skipped.push({ file, reason:
|
|
991
|
+
plan.skipped.push({ file, reason: acceptedDecisionReason(file) });
|
|
733
992
|
continue;
|
|
734
993
|
}
|
|
735
994
|
plan.conflict.push({
|
|
@@ -737,7 +996,7 @@ async function main() {
|
|
|
737
996
|
localHash,
|
|
738
997
|
storedHash,
|
|
739
998
|
remoteHash,
|
|
740
|
-
reason:
|
|
999
|
+
reason: isMergeFile(canonical, manifestCtx)
|
|
741
1000
|
? 'user modified MERGE file'
|
|
742
1001
|
: 'user modified runtime file',
|
|
743
1002
|
});
|
|
@@ -753,11 +1012,13 @@ async function main() {
|
|
|
753
1012
|
from: localGen,
|
|
754
1013
|
to: remoteGen,
|
|
755
1014
|
sourceBase,
|
|
1015
|
+
releaseNotes: remoteVersion.releaseNotes || null,
|
|
756
1016
|
partialUpdate: localVersion.partialUpdate || null,
|
|
757
1017
|
acceptedConflicts: localVersion.acceptedConflicts || {},
|
|
758
1018
|
updated: plan.updated.length,
|
|
759
1019
|
created: plan.created.length,
|
|
760
1020
|
adopted: plan.adopted.length,
|
|
1021
|
+
moved: plan.moved.length,
|
|
761
1022
|
conflict: plan.conflict.length,
|
|
762
1023
|
skipped: plan.skipped.length,
|
|
763
1024
|
// Token-safe by default: attach the full plan only when --verbose / --full-plan is passed.
|
|
@@ -768,7 +1029,8 @@ async function main() {
|
|
|
768
1029
|
}
|
|
769
1030
|
|
|
770
1031
|
console.log(`\nUpdate: v${localGen} -> v${remoteGen}`);
|
|
771
|
-
console.log(` ${plan.updated.length} safe update, ${plan.created.length} new, ${plan.conflict.length} conflict, ${plan.skipped.length} skipped\n`);
|
|
1032
|
+
console.log(` ${plan.updated.length} safe update, ${plan.created.length} new, ${plan.moved.length} moved, ${plan.conflict.length} conflict, ${plan.skipped.length} skipped\n`);
|
|
1033
|
+
printReleaseHighlights(remoteVersion);
|
|
772
1034
|
|
|
773
1035
|
// Show conflicts (these need AI/user decision)
|
|
774
1036
|
if (plan.conflict.length > 0) {
|
|
@@ -783,10 +1045,11 @@ async function main() {
|
|
|
783
1045
|
}
|
|
784
1046
|
|
|
785
1047
|
// Show what will be auto-updated
|
|
786
|
-
if (plan.updated.length + plan.created.length > 0) {
|
|
1048
|
+
if (plan.updated.length + plan.created.length + plan.moved.length > 0) {
|
|
787
1049
|
console.log('AUTO (safe to apply):');
|
|
788
1050
|
for (const u of plan.updated) console.log(` ^ ${u.file}`);
|
|
789
1051
|
for (const c of plan.created) console.log(` + ${c.file}`);
|
|
1052
|
+
for (const m of plan.moved) console.log(` > ${m.from} -> ${m.to}`);
|
|
790
1053
|
console.log('');
|
|
791
1054
|
}
|
|
792
1055
|
|
|
@@ -809,35 +1072,43 @@ async function main() {
|
|
|
809
1072
|
async function prepareWrite(entry, { mustNotExist = false } = {}) {
|
|
810
1073
|
try {
|
|
811
1074
|
const dest = safePath(entry.file);
|
|
812
|
-
if (!dest) {
|
|
1075
|
+
if (!dest) return { ok: false, message: ` x Traversal rejected: ${entry.file}` };
|
|
813
1076
|
// Symlink rejection: do not follow symlinks.
|
|
814
1077
|
if (existsSync(dest)) {
|
|
815
|
-
try { if (lstatSync(dest).isSymbolicLink()) {
|
|
1078
|
+
try { if (lstatSync(dest).isSymbolicLink()) return { ok: false, message: ` x Symlink rejected: ${entry.file}` }; } catch (_) {}
|
|
816
1079
|
if (mustNotExist) {
|
|
817
|
-
|
|
818
|
-
failed++; return;
|
|
1080
|
+
return { ok: false, message: ` x File created since plan: ${entry.file} - treating as CONFLICT` };
|
|
819
1081
|
}
|
|
820
1082
|
}
|
|
821
1083
|
const content = await fetchSourceFile(remotePath(entry.file));
|
|
822
1084
|
const normalized = content.replace(/\r\n/g, '\n');
|
|
823
1085
|
const fetchedHash = sha256(normalized);
|
|
824
1086
|
if (fetchedHash !== entry.remoteHash) {
|
|
825
|
-
|
|
826
|
-
failed++; return;
|
|
1087
|
+
return { ok: false, message: ` x Hash mismatch: ${entry.file}` };
|
|
827
1088
|
}
|
|
828
|
-
|
|
1089
|
+
return { ok: true, prepared: { file: entry.file, dest, content: normalized, mustNotExist } };
|
|
829
1090
|
} catch (e) {
|
|
830
|
-
|
|
831
|
-
failed++;
|
|
1091
|
+
return { ok: false, message: ` x Failed: ${entry.file} - ${e.message}` };
|
|
832
1092
|
}
|
|
833
1093
|
}
|
|
834
1094
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1095
|
+
const prepareJobs = [
|
|
1096
|
+
...plan.updated.map(entry => ({ entry, options: {} })),
|
|
1097
|
+
...plan.created.map(entry => ({ entry, options: { mustNotExist: true } })),
|
|
1098
|
+
...plan.moved
|
|
1099
|
+
.filter(entry => entry.write !== false)
|
|
1100
|
+
.map(entry => ({ entry, options: { mustNotExist: !existsSync(safePath(entry.file)) } })),
|
|
1101
|
+
];
|
|
1102
|
+
const prepareResults = await Promise.all(
|
|
1103
|
+
prepareJobs.map(job => prepareWrite(job.entry, job.options)),
|
|
1104
|
+
);
|
|
1105
|
+
for (const result of prepareResults) {
|
|
1106
|
+
if (result.ok) {
|
|
1107
|
+
preparedWrites.push(result.prepared);
|
|
1108
|
+
} else {
|
|
1109
|
+
console.error(result.message);
|
|
1110
|
+
failed++;
|
|
1111
|
+
}
|
|
841
1112
|
}
|
|
842
1113
|
|
|
843
1114
|
if (failed === 0) {
|
|
@@ -849,6 +1120,12 @@ async function main() {
|
|
|
849
1120
|
}
|
|
850
1121
|
}
|
|
851
1122
|
|
|
1123
|
+
// All-or-nothing apply invariant: every prepared file above was hash-validated
|
|
1124
|
+
// against its manifest checksum before ANY writeFileSync below. The version
|
|
1125
|
+
// file is advanced only when failed === 0, so a failed apply leaves version
|
|
1126
|
+
// tracking unchanged and is safe to re-run with --apply-safe. A mid-loop
|
|
1127
|
+
// writeFileSync throw (disk failure) is the only partial-disk edge case;
|
|
1128
|
+
// re-running --apply-safe reconciles it.
|
|
852
1129
|
let applied = 0;
|
|
853
1130
|
if (failed === 0) {
|
|
854
1131
|
try {
|
|
@@ -857,6 +1134,17 @@ async function main() {
|
|
|
857
1134
|
writeFileSync(prepared.dest, prepared.content, 'utf-8');
|
|
858
1135
|
applied++;
|
|
859
1136
|
}
|
|
1137
|
+
for (const moved of plan.moved) {
|
|
1138
|
+
const from = safePath(moved.from);
|
|
1139
|
+
if (!from) throw new Error(`Traversal rejected: ${moved.from}`);
|
|
1140
|
+
if (!existsSync(from)) continue;
|
|
1141
|
+
if (lstatSync(from).isSymbolicLink()) throw new Error(`Symlink rejected: ${moved.from}`);
|
|
1142
|
+
const currentHash = sha256File(from);
|
|
1143
|
+
if (currentHash && currentHash !== moved.storedHash) {
|
|
1144
|
+
throw new Error(`Legacy moved file changed before cleanup: ${moved.from}`);
|
|
1145
|
+
}
|
|
1146
|
+
unlinkSync(from);
|
|
1147
|
+
}
|
|
860
1148
|
} catch (e) {
|
|
861
1149
|
console.error(` x Failed while writing prepared files - ${e.message}`);
|
|
862
1150
|
failed++;
|
|
@@ -870,24 +1158,43 @@ async function main() {
|
|
|
870
1158
|
for (const u of plan.updated) localVersion.checksums[u.file] = u.remoteHash;
|
|
871
1159
|
for (const c of plan.created) localVersion.checksums[c.file] = c.remoteHash;
|
|
872
1160
|
for (const a of plan.adopted) localVersion.checksums[a.file] = a.remoteHash;
|
|
1161
|
+
localVersion.sources = localVersion.sources || {};
|
|
1162
|
+
for (const u of plan.updated) localVersion.sources[u.file] = remoteSources[u.file] || u.file;
|
|
1163
|
+
for (const c of plan.created) localVersion.sources[c.file] = remoteSources[c.file] || c.file;
|
|
1164
|
+
for (const a of plan.adopted) localVersion.sources[a.file] = remoteSources[a.file] || a.file;
|
|
1165
|
+
for (const m of plan.moved) {
|
|
1166
|
+
localVersion.checksums[m.file] = m.remoteHash;
|
|
1167
|
+
localVersion.sources[m.file] = remoteSources[m.file] || m.file;
|
|
1168
|
+
delete localVersion.checksums[m.from];
|
|
1169
|
+
delete localVersion.sources[m.from];
|
|
1170
|
+
delete localVersion.acceptedConflicts[m.from];
|
|
1171
|
+
}
|
|
1172
|
+
if (Array.isArray(remoteVersion.moves)) localVersion.moves = remoteVersion.moves;
|
|
1173
|
+
else delete localVersion.moves;
|
|
873
1174
|
if (plan.conflict.length === 0) {
|
|
874
1175
|
localVersion.generator = remoteGen;
|
|
875
1176
|
localVersion.generated = appliedAt;
|
|
1177
|
+
if (remoteVersion.releaseNotes) localVersion.releaseNotes = remoteVersion.releaseNotes;
|
|
1178
|
+
else delete localVersion.releaseNotes;
|
|
876
1179
|
delete localVersion.partialUpdate;
|
|
877
1180
|
} else {
|
|
878
1181
|
localVersion.partialUpdate = {
|
|
879
1182
|
targetGenerator: remoteGen,
|
|
880
1183
|
updatedAt: appliedAt,
|
|
881
|
-
appliedFiles: [...plan.updated, ...plan.created].map(x => x.file),
|
|
1184
|
+
appliedFiles: [...plan.updated, ...plan.created, ...plan.moved].map(x => x.file),
|
|
882
1185
|
adoptedFiles: plan.adopted.map(x => x.file),
|
|
1186
|
+
movedFiles: plan.moved.map(x => ({ from: x.from, to: x.to })),
|
|
883
1187
|
conflicts: plan.conflict.map(x => x.file),
|
|
1188
|
+
releaseNotes: remoteVersion.releaseNotes || null,
|
|
884
1189
|
};
|
|
885
1190
|
}
|
|
886
1191
|
writeFileSync(VERSION_FILE, JSON.stringify(localVersion, null, 2) + '\n', 'utf-8');
|
|
887
1192
|
if (plan.conflict.length === 0) {
|
|
888
1193
|
console.log(`Applied ${applied} files. Version updated to ${remoteVersion.generator}.`);
|
|
1194
|
+
printReleaseHighlights(remoteVersion);
|
|
889
1195
|
} else {
|
|
890
1196
|
console.log(`Applied ${applied} SAFE/NEW files. Version remains ${localGen}; ${plan.conflict.length} conflicts still need merge.`);
|
|
1197
|
+
printReleaseHighlights(remoteVersion);
|
|
891
1198
|
}
|
|
892
1199
|
} else {
|
|
893
1200
|
console.log(`${failed} failures. NO files were version-tracked. Fix and re-run.`);
|
|
@@ -896,8 +1203,8 @@ async function main() {
|
|
|
896
1203
|
}
|
|
897
1204
|
|
|
898
1205
|
if (finalize) {
|
|
899
|
-
if (plan.updated.length > 0 || plan.created.length > 0 || plan.adopted.length > 0) {
|
|
900
|
-
console.log(`Cannot finalize: ${plan.updated.length} safe updates, ${plan.created.length} new files, and ${plan.adopted.length} adopted metadata entries still need --apply-safe.`);
|
|
1206
|
+
if (plan.updated.length > 0 || plan.created.length > 0 || plan.adopted.length > 0 || plan.moved.length > 0) {
|
|
1207
|
+
console.log(`Cannot finalize: ${plan.updated.length} safe updates, ${plan.created.length} new files, ${plan.moved.length} path moves, and ${plan.adopted.length} adopted metadata entries still need --apply-safe.`);
|
|
901
1208
|
if (decisionMetadataDirty) {
|
|
902
1209
|
writeFileSync(VERSION_FILE, JSON.stringify(localVersion, null, 2) + '\n', 'utf-8');
|
|
903
1210
|
}
|
|
@@ -915,9 +1222,12 @@ async function main() {
|
|
|
915
1222
|
const finalizedAt = new Date().toISOString();
|
|
916
1223
|
localVersion.generator = remoteGen;
|
|
917
1224
|
localVersion.generated = finalizedAt;
|
|
1225
|
+
if (remoteVersion.releaseNotes) localVersion.releaseNotes = remoteVersion.releaseNotes;
|
|
1226
|
+
else delete localVersion.releaseNotes;
|
|
918
1227
|
delete localVersion.partialUpdate;
|
|
919
1228
|
writeFileSync(VERSION_FILE, JSON.stringify(localVersion, null, 2) + '\n', 'utf-8');
|
|
920
1229
|
console.log(`Finalized Harness update to ${remoteGen}.`);
|
|
1230
|
+
printReleaseHighlights(remoteVersion);
|
|
921
1231
|
} else if (decisionMetadataDirty) {
|
|
922
1232
|
writeFileSync(VERSION_FILE, JSON.stringify(localVersion, null, 2) + '\n', 'utf-8');
|
|
923
1233
|
console.log('Recorded conflict decision metadata.');
|