@hone-ai/cli 1.16.0 → 1.18.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
@@ -55,8 +55,28 @@ function getConfig() {
55
55
  return { token, apiUrl };
56
56
  }
57
57
 
58
+ // HC-019y-followup-3: module-level upgrade-warning bucket. Set by the
59
+ // axios response interceptor when the server's X-CLI-Latest-Version
60
+ // header reports a newer version than ours. Printed once at process
61
+ // exit so the warning lands AFTER the command's normal output and
62
+ // doesn't fight for attention with whatever the user is doing.
63
+ let _outdatedWarning = null;
64
+
65
+ function _compareSemverMinor(a, b) {
66
+ // Returns true if `a` is strictly older than `b` for major.minor.patch.
67
+ // Liberal-parse: anything we can't make sense of → false (don't warn).
68
+ const re = /^(\d+)\.(\d+)\.(\d+)/;
69
+ const ma = re.exec(String(a || '')), mb = re.exec(String(b || ''));
70
+ if (!ma || !mb) return false;
71
+ const [, aMaj, aMin, aPat] = ma.map(Number);
72
+ const [, bMaj, bMin, bPat] = mb.map(Number);
73
+ if (aMaj !== bMaj) return aMaj < bMaj;
74
+ if (aMin !== bMin) return aMin < bMin;
75
+ return aPat < bPat;
76
+ }
77
+
58
78
  function api(config) {
59
- return axios.create({
79
+ const client = axios.create({
60
80
  baseURL: config.apiUrl,
61
81
  headers: {
62
82
  Authorization: `Bearer ${config.token}`,
@@ -64,8 +84,35 @@ function api(config) {
64
84
  },
65
85
  timeout: 30_000,
66
86
  });
87
+ // HC-019y-followup-3: response interceptor reads server's
88
+ // X-CLI-Latest-Version, sets the upgrade-warning bucket if outdated.
89
+ // Fire-and-forget — never throws. Captures errors silently so a
90
+ // missing/malformed header can't break the command.
91
+ client.interceptors.response.use(
92
+ (response) => {
93
+ try {
94
+ const latest = response?.headers?.['x-cli-latest-version'];
95
+ if (latest && _compareSemverMinor(pkg.version, latest)) {
96
+ _outdatedWarning =
97
+ ` ⚠ @hone-ai/cli ${latest} is available — you have ${pkg.version}\n` +
98
+ ` Run: npm install -g @hone-ai/cli@latest`;
99
+ }
100
+ } catch { /* never break the response path */ }
101
+ return response;
102
+ },
103
+ (error) => Promise.reject(error)
104
+ );
105
+ return client;
67
106
  }
68
107
 
108
+ // HC-019y-followup-3: emit the upgrade warning at process exit if set.
109
+ // Wrapped in try/catch — a failed warning is fundamentally non-critical.
110
+ process.on('exit', () => {
111
+ if (_outdatedWarning) {
112
+ try { console.error('\n' + _outdatedWarning); } catch { /* swallow */ }
113
+ }
114
+ });
115
+
69
116
  // ── SETUP command ─────────────────────────────────────────────────────────────
70
117
  program
71
118
  .command('setup')
@@ -1112,24 +1159,41 @@ program
1112
1159
  console.log('Writing generated skills...');
1113
1160
  if (result.skills) {
1114
1161
  let preservedCount = 0;
1162
+ let sidecarCount = 0;
1115
1163
  for (const [skillName, content] of Object.entries(result.skills)) {
1116
1164
  if (!content || content.length < 50) continue;
1117
1165
  const skillDir = path.join(repoRoot, '.github', 'skills', skillName);
1118
1166
  const skillFile = path.join(skillDir, 'SKILL.md');
1119
1167
  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)`);
1168
+ const spliceResult = splicePreserveRepoSpecific(skillFile, content);
1169
+ if (spliceResult.sidecar) {
1170
+ // HC-019y-followup-2: legacy unmarked file — write to sidecar,
1171
+ // don't overwrite, prompt adopter to review.
1172
+ fs.writeFileSync(spliceResult.sidecarPath, spliceResult.merged);
1173
+ sidecarCount++;
1174
+ console.log(` ⚠ .github/skills/${skillName}/SKILL.md — NOT overwritten`);
1175
+ console.log(` Wrote derive output to: ${path.relative(repoRoot, spliceResult.sidecarPath)}`);
1176
+ console.log(` Reason: ${spliceResult.reason}`);
1125
1177
  } else {
1126
- console.log(` ✓ .github/skills/${skillName}/SKILL.md`);
1178
+ fs.writeFileSync(skillFile, spliceResult.merged);
1179
+ if (spliceResult.preserved) {
1180
+ preservedCount++;
1181
+ console.log(` ✓ .github/skills/${skillName}/SKILL.md (preserved adopter REPO-SPECIFIC content)`);
1182
+ } else {
1183
+ console.log(` ✓ .github/skills/${skillName}/SKILL.md`);
1184
+ }
1127
1185
  }
1128
1186
  }
1129
1187
  if (preservedCount > 0) {
1130
1188
  console.log(`\n Preserved adopter REPO-SPECIFIC content in ${preservedCount} skill(s).`);
1131
1189
  console.log(` (HC-019y-followup-1: derive no longer overwrites curated REPO-SPECIFIC sections)`);
1132
1190
  }
1191
+ if (sidecarCount > 0) {
1192
+ console.log(`\n ⚠ HC-019y-followup-2: ${sidecarCount} legacy unmarked SKILL(s) routed to .derive-new sidecars.`);
1193
+ console.log(` Review each sidecar; if the derive output is acceptable, replace the original.`);
1194
+ console.log(` To opt into automatic splice protection on the next derive, add a`);
1195
+ console.log(` '<!-- REPO-SPECIFIC -->' marker to the original file.`);
1196
+ }
1133
1197
  }
1134
1198
 
1135
1199
  // 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.18.0",
4
4
  "description": "Hone AI — Enterprise SDLC Pipeline CLI",
5
5
  "main": "hone-cli.js",
6
6
  "bin": {
@@ -16,6 +16,8 @@
16
16
  "scripts": {
17
17
  "test": "echo \"No tests yet\" && exit 0",
18
18
  "link": "npm link",
19
+ "sync-server-cli-version": "node scripts/sync-server-cli-version.js",
20
+ "prepublishOnly": "node scripts/sync-server-cli-version.js",
19
21
  "postinstall": "echo '\\n Hone AI CLI installed successfully.\\n Next: run `hone init --token <YOUR_TOKEN>` to configure.\\n Docs: https://github.com/subbareddyvani/hone-server\\n'"
20
22
  },
21
23
  "dependencies": {