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