@hone-ai/cli 1.16.0 → 1.17.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/hone-cli.js +23 -6
- package/lib/splice-repo-specific.js +105 -24
- package/package.json +1 -1
package/hone-cli.js
CHANGED
|
@@ -1112,24 +1112,41 @@ program
|
|
|
1112
1112
|
console.log('Writing generated skills...');
|
|
1113
1113
|
if (result.skills) {
|
|
1114
1114
|
let preservedCount = 0;
|
|
1115
|
+
let sidecarCount = 0;
|
|
1115
1116
|
for (const [skillName, content] of Object.entries(result.skills)) {
|
|
1116
1117
|
if (!content || content.length < 50) continue;
|
|
1117
1118
|
const skillDir = path.join(repoRoot, '.github', 'skills', skillName);
|
|
1118
1119
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
1119
1120
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1121
|
+
const spliceResult = splicePreserveRepoSpecific(skillFile, content);
|
|
1122
|
+
if (spliceResult.sidecar) {
|
|
1123
|
+
// HC-019y-followup-2: legacy unmarked file — write to sidecar,
|
|
1124
|
+
// don't overwrite, prompt adopter to review.
|
|
1125
|
+
fs.writeFileSync(spliceResult.sidecarPath, spliceResult.merged);
|
|
1126
|
+
sidecarCount++;
|
|
1127
|
+
console.log(` ⚠ .github/skills/${skillName}/SKILL.md — NOT overwritten`);
|
|
1128
|
+
console.log(` Wrote derive output to: ${path.relative(repoRoot, spliceResult.sidecarPath)}`);
|
|
1129
|
+
console.log(` Reason: ${spliceResult.reason}`);
|
|
1125
1130
|
} else {
|
|
1126
|
-
|
|
1131
|
+
fs.writeFileSync(skillFile, spliceResult.merged);
|
|
1132
|
+
if (spliceResult.preserved) {
|
|
1133
|
+
preservedCount++;
|
|
1134
|
+
console.log(` ✓ .github/skills/${skillName}/SKILL.md (preserved adopter REPO-SPECIFIC content)`);
|
|
1135
|
+
} else {
|
|
1136
|
+
console.log(` ✓ .github/skills/${skillName}/SKILL.md`);
|
|
1137
|
+
}
|
|
1127
1138
|
}
|
|
1128
1139
|
}
|
|
1129
1140
|
if (preservedCount > 0) {
|
|
1130
1141
|
console.log(`\n Preserved adopter REPO-SPECIFIC content in ${preservedCount} skill(s).`);
|
|
1131
1142
|
console.log(` (HC-019y-followup-1: derive no longer overwrites curated REPO-SPECIFIC sections)`);
|
|
1132
1143
|
}
|
|
1144
|
+
if (sidecarCount > 0) {
|
|
1145
|
+
console.log(`\n ⚠ HC-019y-followup-2: ${sidecarCount} legacy unmarked SKILL(s) routed to .derive-new sidecars.`);
|
|
1146
|
+
console.log(` Review each sidecar; if the derive output is acceptable, replace the original.`);
|
|
1147
|
+
console.log(` To opt into automatic splice protection on the next derive, add a`);
|
|
1148
|
+
console.log(` '<!-- REPO-SPECIFIC -->' marker to the original file.`);
|
|
1149
|
+
}
|
|
1133
1150
|
}
|
|
1134
1151
|
|
|
1135
1152
|
// H-022: surface parser warnings so silent drops become VISIBLE failures.
|
|
@@ -6,6 +6,24 @@
|
|
|
6
6
|
* (everything below the `<!-- REPO-SPECIFIC -->` marker) is NEVER lost
|
|
7
7
|
* across derive runs.
|
|
8
8
|
*
|
|
9
|
+
* HC-019y-followup-2: legacy adopter-curated SKILLs WITHOUT the
|
|
10
|
+
* REPO-SPECIFIC marker were still vulnerable. OptionsFlow's
|
|
11
|
+
* `python-developer/SKILL.md` (834 lines authored by stories E22-E /
|
|
12
|
+
* E22-A / E16-A predating the marker convention) was REPLACED with
|
|
13
|
+
* 69 lines of fresh LLM output on 2026-05-30 because the file had no
|
|
14
|
+
* marker — the helper fell through to "fresh write" and 765 lines of
|
|
15
|
+
* legitimately-curated content were lost. Restored manually in
|
|
16
|
+
* OptionsFlow PR #106.
|
|
17
|
+
*
|
|
18
|
+
* Fix: non-destructive sidecar mode. When the existing file is
|
|
19
|
+
* substantial (>500 bytes) AND has neither the REPO-SPECIFIC marker
|
|
20
|
+
* nor the ENTERPRISE-MANAGED marker, the helper now returns
|
|
21
|
+
* sidecar=true. The caller writes to `<path>.derive-new` instead of
|
|
22
|
+
* overwriting, emits a warning, and prompts the adopter to add a
|
|
23
|
+
* `<!-- REPO-SPECIFIC -->` marker to opt into splice on the next derive.
|
|
24
|
+
* Same pattern as `setup-ai-pipeline.sh copy_skills()` uses a `.bak`
|
|
25
|
+
* fallback.
|
|
26
|
+
*
|
|
9
27
|
* Pre-fix: derive wrote the file blind (fs.writeFileSync(skillFile,
|
|
10
28
|
* content)). 5 OptionsFlow regression tests broke after a single derive
|
|
11
29
|
* because 378 lines of E33-A/E34-B-added content disappeared.
|
|
@@ -21,41 +39,104 @@
|
|
|
21
39
|
*/
|
|
22
40
|
const fs = require('node:fs');
|
|
23
41
|
|
|
42
|
+
const REPO_SPECIFIC_MARKER = '<!-- REPO-SPECIFIC';
|
|
43
|
+
const ENTERPRISE_MARKER = '<!-- ENTERPRISE-MANAGED';
|
|
44
|
+
|
|
45
|
+
// HC-019y-followup-2: the sidecar threshold. Files smaller than this
|
|
46
|
+
// are treated as stubs / placeholders and overwritten normally — only
|
|
47
|
+
// genuinely substantial unmarked content triggers sidecar mode. 500
|
|
48
|
+
// bytes is roughly: 5-10 lines of meaningful markdown. Tonight's
|
|
49
|
+
// evidence (python-developer/SKILL.md at 834 lines / ~32KB) is well
|
|
50
|
+
// above the threshold; trivial scaffolding (a 1-line stub) is well
|
|
51
|
+
// below. Tunable if real adopter data shows this is wrong.
|
|
52
|
+
const SIDECAR_MIN_BYTES = 500;
|
|
53
|
+
|
|
24
54
|
/**
|
|
25
|
-
* Splice adopter REPO-SPECIFIC tail into derive output
|
|
55
|
+
* Splice adopter REPO-SPECIFIC tail into derive output, OR fall back
|
|
56
|
+
* to sidecar mode for legacy unmarked files (HC-019y-followup-2).
|
|
26
57
|
*
|
|
27
58
|
* @param {string} existingPath absolute path of the file being overwritten
|
|
28
59
|
* @param {string} newContent the content from `hone derive` server response
|
|
29
|
-
* @returns {{
|
|
30
|
-
* merged:
|
|
31
|
-
* preserved:
|
|
32
|
-
*
|
|
60
|
+
* @returns {{
|
|
61
|
+
* merged: string,
|
|
62
|
+
* preserved: boolean,
|
|
63
|
+
* sidecar: boolean,
|
|
64
|
+
* sidecarPath: string|null,
|
|
65
|
+
* reason: string|null
|
|
66
|
+
* }}
|
|
67
|
+
* merged: bytes to write (to existingPath OR sidecarPath)
|
|
68
|
+
* preserved: true if adopter REPO-SPECIFIC content was preserved
|
|
69
|
+
* sidecar: true if caller should write to sidecarPath instead of
|
|
70
|
+
* overwriting existingPath; false otherwise
|
|
71
|
+
* sidecarPath: when sidecar=true, the path the caller should write to
|
|
72
|
+
* (always `${existingPath}.derive-new`); null otherwise
|
|
73
|
+
* reason: when sidecar=true, human-readable explanation for the
|
|
74
|
+
* warning the caller should emit; null otherwise
|
|
33
75
|
*/
|
|
34
76
|
function splicePreserveRepoSpecific(existingPath, newContent) {
|
|
35
|
-
const MARKER = '<!-- REPO-SPECIFIC';
|
|
36
77
|
if (!fs.existsSync(existingPath)) {
|
|
37
78
|
// Fresh adopter — no existing file. Write new content as-is.
|
|
38
|
-
return { merged: newContent, preserved: false };
|
|
79
|
+
return { merged: newContent, preserved: false, sidecar: false, sidecarPath: null, reason: null };
|
|
39
80
|
}
|
|
40
81
|
const existing = fs.readFileSync(existingPath, 'utf8');
|
|
41
|
-
const oldIdx = existing.indexOf(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
|
|
82
|
+
const oldIdx = existing.indexOf(REPO_SPECIFIC_MARKER);
|
|
83
|
+
|
|
84
|
+
if (oldIdx >= 0) {
|
|
85
|
+
// Existing has REPO-SPECIFIC marker — splice as before.
|
|
86
|
+
const newIdx = newContent.indexOf(REPO_SPECIFIC_MARKER);
|
|
87
|
+
if (newIdx < 0) {
|
|
88
|
+
// New content has no REPO-SPECIFIC section. Append the existing one.
|
|
89
|
+
return {
|
|
90
|
+
merged: newContent.trimEnd() + '\n\n' + existing.slice(oldIdx),
|
|
91
|
+
preserved: true,
|
|
92
|
+
sidecar: false,
|
|
93
|
+
sidecarPath: null,
|
|
94
|
+
reason: null,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// Both have REPO-SPECIFIC. Take new content up to the marker, then
|
|
98
|
+
// append everything from existing's marker to EOF.
|
|
99
|
+
return {
|
|
100
|
+
merged: newContent.slice(0, newIdx) + existing.slice(oldIdx),
|
|
101
|
+
preserved: true,
|
|
102
|
+
sidecar: false,
|
|
103
|
+
sidecarPath: null,
|
|
104
|
+
reason: null,
|
|
105
|
+
};
|
|
46
106
|
}
|
|
47
|
-
|
|
48
|
-
if
|
|
49
|
-
|
|
50
|
-
|
|
107
|
+
|
|
108
|
+
// No REPO-SPECIFIC marker. HC-019y-followup-2: check if this is a
|
|
109
|
+
// legacy substantial-unmarked file that we'd risk destroying.
|
|
110
|
+
const hasEnterpriseMarker = existing.indexOf(ENTERPRISE_MARKER) >= 0;
|
|
111
|
+
const isSubstantial = Buffer.byteLength(existing, 'utf8') >= SIDECAR_MIN_BYTES;
|
|
112
|
+
|
|
113
|
+
if (isSubstantial && !hasEnterpriseMarker) {
|
|
114
|
+
// Legacy unmarked file with real content. Write new content to a
|
|
115
|
+
// sidecar path instead of overwriting. Adopter reviews + decides
|
|
116
|
+
// whether to adopt the derive output, manually merge, or opt into
|
|
117
|
+
// splice protection by adding a REPO-SPECIFIC marker.
|
|
118
|
+
return {
|
|
119
|
+
merged: newContent,
|
|
120
|
+
preserved: false,
|
|
121
|
+
sidecar: true,
|
|
122
|
+
sidecarPath: `${existingPath}.derive-new`,
|
|
123
|
+
reason:
|
|
124
|
+
`legacy unmarked file (${Buffer.byteLength(existing, 'utf8')} bytes) — ` +
|
|
125
|
+
`wrote derive output to <path>.derive-new instead of overwriting. ` +
|
|
126
|
+
`Review the sidecar, then either: (a) replace the original, ` +
|
|
127
|
+
`(b) manually merge curated sections, or (c) add ` +
|
|
128
|
+
`'<!-- REPO-SPECIFIC -->' to the original to opt into splice on next derive.`,
|
|
129
|
+
};
|
|
51
130
|
}
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
return {
|
|
56
|
-
merged: newContent.slice(0, newIdx) + existing.slice(oldIdx),
|
|
57
|
-
preserved: true,
|
|
58
|
-
};
|
|
131
|
+
|
|
132
|
+
// Either small (stub/placeholder) or has ENTERPRISE-MANAGED marker
|
|
133
|
+
// (deliberately enterprise-controlled). Safe to overwrite.
|
|
134
|
+
return { merged: newContent, preserved: false, sidecar: false, sidecarPath: null, reason: null };
|
|
59
135
|
}
|
|
60
136
|
|
|
61
|
-
module.exports = {
|
|
137
|
+
module.exports = {
|
|
138
|
+
splicePreserveRepoSpecific,
|
|
139
|
+
REPO_SPECIFIC_MARKER,
|
|
140
|
+
ENTERPRISE_MARKER,
|
|
141
|
+
SIDECAR_MIN_BYTES,
|
|
142
|
+
};
|