@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 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 { merged, preserved } = splicePreserveRepoSpecific(skillFile, content);
1121
- fs.writeFileSync(skillFile, merged);
1122
- if (preserved) {
1123
- preservedCount++;
1124
- console.log(` ✓ .github/skills/${skillName}/SKILL.md (preserved adopter REPO-SPECIFIC content)`);
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
- console.log(` ✓ .github/skills/${skillName}/SKILL.md`);
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 {{ merged: string, preserved: boolean }}
30
- * merged: bytes to write
31
- * preserved: true if adopter REPO-SPECIFIC content was preserved
32
- * across the splice; false for fresh writes
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(MARKER);
42
- if (oldIdx < 0) {
43
- // Existing file has no REPO-SPECIFIC marker (likely a non-skill or
44
- // legacy file). Nothing to preserve.
45
- return { merged: newContent, preserved: false };
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
- const newIdx = newContent.indexOf(MARKER);
48
- if (newIdx < 0) {
49
- // New content has no REPO-SPECIFIC section. Append the existing one.
50
- return { merged: newContent.trimEnd() + '\n\n' + existing.slice(oldIdx), preserved: true };
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
- // Both have REPO-SPECIFIC. Take new content up to the marker, then
53
- // append everything from existing's marker to EOF. This preserves
54
- // adopter-curated content while updating the body before the marker.
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 = { splicePreserveRepoSpecific };
137
+ module.exports = {
138
+ splicePreserveRepoSpecific,
139
+ REPO_SPECIFIC_MARKER,
140
+ ENTERPRISE_MARKER,
141
+ SIDECAR_MIN_BYTES,
142
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hone-ai/cli",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "Hone AI — Enterprise SDLC Pipeline CLI",
5
5
  "main": "hone-cli.js",
6
6
  "bin": {