@skyf0xx/hedgehog 6.1.8 → 6.2.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/bin/cli.mjs CHANGED
@@ -64,7 +64,12 @@ import {
64
64
  shouldPromptForStar,
65
65
  recordStarAnswer,
66
66
  formatStarPrompt,
67
+ shouldPromptForShowcase,
68
+ recordShowcaseAnswer,
69
+ formatShowcasePrompt,
70
+ postShowcase,
67
71
  REPO_URL,
72
+ SHOWCASE_REPO_URL,
68
73
  } from '../src/db/community.mjs';
69
74
  import { rebuildDb } from '../src/db/rebuild.mjs';
70
75
  import {
@@ -628,6 +633,9 @@ ${bold('Usage')}
628
633
  npx @skyf0xx/hedgehog decision list [<task-id>] list declared decisions, oldest first
629
634
  npx @skyf0xx/hedgehog db migrate bring the graph's schema up to the latest version
630
635
  npx @skyf0xx/hedgehog community star --answer <a> record the star prompt's answer
636
+ npx @skyf0xx/hedgehog community showcase --repo <url> [--description <text>]
637
+ share what you built to the public showcase
638
+ npx @skyf0xx/hedgehog community showcase --answer later|dismissed defer or decline showcasing
631
639
  npx @skyf0xx/hedgehog --help
632
640
 
633
641
  Available cores: ${cores.join(', ')} (${bold('cores list')} for what each one is for)
@@ -2233,10 +2241,21 @@ async function verifyCommand(args) {
2233
2241
 
2234
2242
  // Fires once per project — see community.mjs. Deliberately last: after
2235
2243
  // the gate's own output, not before it.
2236
- if (await shouldPromptForStar(DEST_ROOT, { intentComplete: result.intentComplete })) {
2244
+ const starJustShown = await shouldPromptForStar(DEST_ROOT, { intentComplete: result.intentComplete });
2245
+ if (starJustShown) {
2237
2246
  console.log(formatStarPrompt());
2238
2247
  console.log('');
2239
2248
  }
2249
+
2250
+ // Independent second ask, gated on the star question already having a
2251
+ // pre-existing answer or deferral — never on the same verify call that
2252
+ // just showed the star prompt for the first time. See
2253
+ // shouldPromptForShowcase for why `starJustShown` has to come from
2254
+ // here rather than being re-derived from state.
2255
+ if (await shouldPromptForShowcase(DEST_ROOT, { intentComplete: result.intentComplete, starJustShown })) {
2256
+ console.log(formatShowcasePrompt());
2257
+ console.log('');
2258
+ }
2240
2259
  }
2241
2260
 
2242
2261
  // Prints the full task packet for each task in `tasks`, read back from
@@ -3836,20 +3855,34 @@ async function decisionCommand(args) {
3836
3855
  process.exitCode = 1;
3837
3856
  }
3838
3857
 
3839
- // `hedgehog community star --answer starred|later|dismissed` — records
3840
- // the star prompt's answer. No build graph or core needed: this is
3841
- // project state about a question asked, not about the build.
3858
+ const COMMUNITY_USAGE = [
3859
+ 'hedgehog community star --answer starred|later|dismissed',
3860
+ ' or: hedgehog community showcase --repo <url> [--description <text>]',
3861
+ ' or: hedgehog community showcase --answer later|dismissed',
3862
+ ].join('\n');
3863
+
3864
+ // `hedgehog community star --answer starred|later|dismissed` and
3865
+ // `hedgehog community showcase ...` — record each prompt's answer. No
3866
+ // build graph or core needed: this is project state about questions
3867
+ // asked, not about the build.
3842
3868
  async function communityCommand(args) {
3843
3869
  const sub = args[0];
3844
3870
 
3845
- if (sub !== 'star') {
3846
- console.error(
3847
- `${red('Unknown community subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog community star --answer starred|later|dismissed\n`,
3848
- );
3849
- process.exitCode = 1;
3871
+ if (sub === 'star') {
3872
+ await communityStarCommand(args.slice(1));
3873
+ return;
3874
+ }
3875
+
3876
+ if (sub === 'showcase') {
3877
+ await communityShowcaseCommand(args.slice(1));
3850
3878
  return;
3851
3879
  }
3852
3880
 
3881
+ console.error(`${red('Unknown community subcommand:')} ${sub ?? '(none)'}\n\nUsage: ${COMMUNITY_USAGE}\n`);
3882
+ process.exitCode = 1;
3883
+ }
3884
+
3885
+ async function communityStarCommand(args) {
3853
3886
  const answerIdx = args.indexOf('--answer');
3854
3887
  const answer = answerIdx !== -1 ? args[answerIdx + 1] : undefined;
3855
3888
  const ANSWERS = ['starred', 'later', 'dismissed'];
@@ -3872,6 +3905,61 @@ async function communityCommand(args) {
3872
3905
  }
3873
3906
  }
3874
3907
 
3908
+ // `hedgehog community showcase --repo <url> [--description <text>]`
3909
+ // records and submits a showcase entry; `hedgehog community showcase
3910
+ // --answer later|dismissed` records a deferral or decline with nothing
3911
+ // to submit. `--repo` and `--answer` are mutually exclusive ways of
3912
+ // answering the same prompt, mirroring the star command's single
3913
+ // `--answer` flag but split in two because only this branch has a
3914
+ // network call and a second, optional flag (`--description`).
3915
+ async function communityShowcaseCommand(args) {
3916
+ const repoIdx = args.indexOf('--repo');
3917
+ const repoUrl = repoIdx !== -1 ? args[repoIdx + 1] : undefined;
3918
+ const descIdx = args.indexOf('--description');
3919
+ const description = descIdx !== -1 ? args[descIdx + 1] : undefined;
3920
+ const answerIdx = args.indexOf('--answer');
3921
+ const answer = answerIdx !== -1 ? args[answerIdx + 1] : undefined;
3922
+
3923
+ if (repoUrl) {
3924
+ // Courtesy check only — well-formed and http(s). The relay (#365) is
3925
+ // the real validation authority; this just catches an obvious typo
3926
+ // before spending a network round trip on it.
3927
+ let parsed;
3928
+ try {
3929
+ parsed = new URL(repoUrl);
3930
+ } catch {
3931
+ parsed = null;
3932
+ }
3933
+ if (!parsed || !['http:', 'https:'].includes(parsed.protocol)) {
3934
+ console.error(`${red('Usage:')} hedgehog community showcase --repo <http(s) url> [--description <text>]\n`);
3935
+ process.exitCode = 1;
3936
+ return;
3937
+ }
3938
+
3939
+ const core = (await installedCore(DEST_ROOT))?.name;
3940
+ await postShowcase({ repoUrl, core, description });
3941
+ await recordShowcaseAnswer(DEST_ROOT, 'shared', { repoUrl, core, description });
3942
+
3943
+ console.log(` ${green('shared')} ${dim(SHOWCASE_REPO_URL)}`);
3944
+ return;
3945
+ }
3946
+
3947
+ const ANSWERS = ['later', 'dismissed'];
3948
+ if (!ANSWERS.includes(answer)) {
3949
+ console.error(`${red('Usage:')} ${COMMUNITY_USAGE}\n`);
3950
+ process.exitCode = 1;
3951
+ return;
3952
+ }
3953
+
3954
+ await recordShowcaseAnswer(DEST_ROOT, answer);
3955
+
3956
+ if (answer === 'later') {
3957
+ console.log(` ${dim('deferred')} ${dim('asked again after about a week of building')}`);
3958
+ } else {
3959
+ console.log(` ${dim('dismissed')} ${dim('not asked again in this project')}`);
3960
+ }
3961
+ }
3962
+
3875
3963
  // `hedgehog cores list` — every core this release can install, the
3876
3964
  // package that ships it, and which of its versions are already extracted
3877
3965
  // locally. The prose each entry carries is what planner reads in Phase 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyf0xx/hedgehog",
3
- "version": "6.1.8",
3
+ "version": "6.2.0",
4
4
  "description": "Install the Hedgehog build discipline (agents + skills) into a repo, for Claude Code, Cursor, or Gemini CLI.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1,21 +1,33 @@
1
- // The one thing Hedgehog asks of the person using it: a prompt to star
2
- // and watch the repo, raised by `hedgehog verify` at the first intent to
3
- // close every one of its layers — the first point the user has seen
4
- // planned work come out complete rather than merely generated.
1
+ // The two things Hedgehog asks of the person using it, both raised by
2
+ // `hedgehog verify` at the first intent to close every one of its layers
3
+ // — the first point the user has seen planned work come out complete
4
+ // rather than merely generated:
5
5
  //
6
- // 1. It asks once. `starred` and `dismissed` end it permanently;
7
- // `later` re-arms after a cooldown rather than repeating. Being
8
- // shown at all defers it on the same cooldown, so a prompt the user
9
- // talks past costs one interruption rather than one per intent.
10
- // 2. It stops the build. The instruction block tells the agent to hold
11
- // the Loop until the user answers, unlike every other notice this
12
- // CLI prints, which is advisory.
13
- // 3. It's framed as what the user gets: watching releases is how a user
14
- // finds out their installed payload is behind; starring helps the
15
- // project. Both stated plainly.
6
+ // 1. STAR a prompt to star and watch the repo.
7
+ // 2. SHOWCASE a prompt to share what got built: a repo URL, the
8
+ // installed core's name, and an optional description, POSTed to a
9
+ // public showcase relay. Independent of the star ask, and fires
10
+ // only after the star question has already been answered in the
11
+ // same verify pass — never on the same call as an unanswered star
12
+ // prompt, and never before it.
16
13
  //
17
- // State lives in `.hedgehog/community.json`, per project rather than in
18
- // `~/.hedgehog/`.
14
+ // Both share one shape:
15
+ //
16
+ // 1. Each asks once. A terminal answer ends it permanently; `later`
17
+ // re-arms after a cooldown rather than repeating. Being shown at
18
+ // all defers it on the same cooldown, so a prompt the user talks
19
+ // past costs one interruption rather than one per intent.
20
+ // 2. Each stops the build. The instruction block tells the agent to
21
+ // hold the Loop until the user answers, unlike every other notice
22
+ // this CLI prints, which is advisory.
23
+ // 3. Each is framed as what the user gets, or plainly as what leaves
24
+ // the project: the star ask states that watching releases is how a
25
+ // user finds out their installed payload is behind, and starring
26
+ // helps the project; the showcase ask states plainly that what's
27
+ // submitted is a public data point, not private telemetry.
28
+ //
29
+ // State for both lives in `.hedgehog/community.json`, per project rather
30
+ // than in `~/.hedgehog/`.
19
31
 
20
32
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
21
33
  import { dirname, join } from 'node:path';
@@ -27,16 +39,28 @@ export const COMMUNITY_PATH = '.hedgehog/community.json';
27
39
 
28
40
  export const REPO_URL = 'https://github.com/skyf0xx/hedgehog';
29
41
 
30
- // How long "later" (and an unanswered "shown") defers for.
42
+ // The public showcase repo submissions are committed into — named here
43
+ // so formatShowcasePrompt can point at it directly rather than making
44
+ // the user go find it.
45
+ export const SHOWCASE_REPO_URL = 'https://github.com/skyf0xx/hedgehog-showcase';
46
+
47
+ // Placeholder until #365 (the Cloudflare Worker relay) ships and hands
48
+ // over the real endpoint. Requests here fail closed (see
49
+ // postShowcase below) so a stale or unreachable placeholder is silently
50
+ // a no-op rather than a build-blocking error.
51
+ const SHOWCASE_RELAY_URL = 'https://showcase-relay.hedgehog.build/submit';
52
+
53
+ // How long "later" (and an unanswered "shown") defers for, shared by
54
+ // both prompts — one cooldown constant, not one per prompt.
31
55
  const LATER_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000;
32
56
 
33
- // `starPrompt` is one of:
57
+ // Both `starPrompt` and `showcasePrompt` are one of:
34
58
  // (unset) never shown
35
59
  // shown displayed but not answered — deferred
36
60
  // later user asked to be reminded — deferred
37
- // starred terminal
61
+ // starred* / shared* terminal (see each prompt's own answer set)
38
62
  // dismissed terminal
39
- const TERMINAL = new Set(['starred', 'dismissed']);
63
+ const TERMINAL = new Set(['starred', 'shared', 'dismissed']);
40
64
 
41
65
  async function readState(root) {
42
66
  try {
@@ -128,3 +152,128 @@ export function formatStarPrompt() {
128
152
  ' and carry on with the build.',
129
153
  ].join('\n');
130
154
  }
155
+
156
+ /**
157
+ * Whether the showcase prompt should fire now. Same terminal/cooldown
158
+ * state machine as shouldPromptForStar, keyed on its own `showcasePrompt`
159
+ * field — but gated first on the star question already having a
160
+ * pre-existing answer or deferral (from a *prior* verify pass), never on
161
+ * the same call that just showed the star prompt for the first time.
162
+ *
163
+ * `starJustShown` is the caller's own shouldPromptForStar return value
164
+ * from this same verify call: shouldPromptForStar's side effect writes
165
+ * `starPrompt: 'shown'` the instant it fires, so reading `starPrompt`
166
+ * back out of state right after can't otherwise tell "already shown
167
+ * before this call" apart from "shown just now, this call, unanswered"
168
+ * — both read as the string 'shown'. Passing the caller's own boolean is
169
+ * the one unambiguous signal for that distinction; the caller already
170
+ * has it for free as the value it just branched on to print the star
171
+ * prompt.
172
+ */
173
+ export async function shouldPromptForShowcase(root, { intentComplete, starJustShown = false }) {
174
+ if (!intentComplete) return false;
175
+ if (starJustShown) return false;
176
+
177
+ const { starPrompt, showcasePrompt, showcaseDeferredAt } = await readState(root);
178
+
179
+ // Unset means the star prompt has never been shown at all — never
180
+ // before it, whether or not it's been answered yet.
181
+ if (!starPrompt) return false;
182
+
183
+ if (TERMINAL.has(showcasePrompt)) return false;
184
+
185
+ if (showcasePrompt === 'later' || showcasePrompt === 'shown') {
186
+ const since = Date.now() - Date.parse(showcaseDeferredAt ?? '');
187
+ if (!Number.isFinite(since) || since < LATER_COOLDOWN_MS) return false;
188
+ }
189
+
190
+ // Recorded before returning, so an unanswered display defers itself
191
+ // rather than re-firing at every later intent completion.
192
+ await writeState(root, { showcasePrompt: 'shown', showcaseDeferredAt: new Date().toISOString() });
193
+
194
+ return true;
195
+ }
196
+
197
+ /**
198
+ * Record the user's answer. `answer` is 'shared' | 'later' | 'dismissed'.
199
+ * `submission` (repoUrl, core, description) is attached only when
200
+ * `answer` is 'shared', for anyone reading community.json back later.
201
+ */
202
+ export async function recordShowcaseAnswer(root, answer, submission) {
203
+ const patch = { showcasePrompt: answer, showcaseAnsweredAt: new Date().toISOString() };
204
+ if (answer === 'later') patch.showcaseDeferredAt = new Date().toISOString();
205
+ if (answer === 'shared' && submission) patch.showcaseSubmission = submission;
206
+ await writeState(root, patch);
207
+ }
208
+
209
+ /** The prompt itself, addressed to the agent — parallel to
210
+ * formatStarPrompt. Kept visually and textually distinct from the star
211
+ * ask: a separate heading, a separate command, and an explicit statement
212
+ * that this is public, optional, and unrelated to closing the intent. */
213
+ export function formatShowcasePrompt() {
214
+ return [
215
+ 'SHOWCASE PROMPT — a separate, optional question. Ask the user this now,',
216
+ ' then STOP.',
217
+ '',
218
+ ' This is unrelated to the star question and does not affect whether the',
219
+ ' intent is done — it is already done. Put this question to the user, wait',
220
+ ' for their answer, and do not start the next task, claim, or any other',
221
+ ' work until they have answered. Ask it once. It will not be asked again.',
222
+ '',
223
+ ' Say, in your own words:',
224
+ '',
225
+ ' Want to share what you built? Hedgehog keeps a public showcase of',
226
+ ` projects built with it: ${SHOWCASE_REPO_URL}`,
227
+ ' Sharing submits your repo URL, the core you built with, and an',
228
+ ' optional description. This is a public data point, committed into',
229
+ ' that public repo — not private telemetry, and entirely optional.',
230
+ '',
231
+ ' Then offer exactly three options and wait:',
232
+ '',
233
+ ' 1. Share it — ask for a repo URL and an optional description',
234
+ " 2. Later — ask again after about a week of building",
235
+ " 3. Don't show again — never ask in this project again",
236
+ '',
237
+ ' On option 1, ask for the repo URL (and, optionally, a short description),',
238
+ ' then record and submit it:',
239
+ '',
240
+ ' hedgehog community showcase --repo <url> [--description "<text>"]',
241
+ '',
242
+ ' On option 2 or 3, record the answer without submitting anything:',
243
+ '',
244
+ ' hedgehog community showcase --answer later|dismissed',
245
+ '',
246
+ ' Never re-ask after recording, and never pressure a decline — option 3',
247
+ ' is a legitimate answer and the correct response to it is to record it',
248
+ ' and carry on with the build.',
249
+ ].join('\n');
250
+ }
251
+
252
+ /**
253
+ * POST a showcase submission to the relay. Follows fetchLatest's
254
+ * (src/hosts/version.mjs) fail-silent convention exactly: a short abort
255
+ * timeout, catch-all, never throws, never blocks verify or the CLI. On
256
+ * any failure — unreachable host, timeout, non-2xx, relay not yet
257
+ * provisioned — this silently drops the submission. No retry, no stored
258
+ * pending state: the relay is the system of record once it exists (see
259
+ * #365), not this CLI.
260
+ */
261
+ export async function postShowcase({ repoUrl, core, description }) {
262
+ try {
263
+ const body = JSON.stringify({
264
+ repoUrl,
265
+ core,
266
+ ...(description ? { description } : {}),
267
+ timestamp: new Date().toISOString(),
268
+ });
269
+ await fetch(SHOWCASE_RELAY_URL, {
270
+ method: 'POST',
271
+ headers: { 'content-type': 'application/json' },
272
+ body,
273
+ signal: AbortSignal.timeout(1500),
274
+ });
275
+ } catch {
276
+ // Unreachable, timed out, or the relay doesn't exist yet (#365) —
277
+ // all the same outcome from here: the submission is dropped.
278
+ }
279
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgehog",
3
- "version": "6.1.8",
3
+ "version": "6.2.0",
4
4
  "description": "Hedgehog build discipline: ordered, tested, verified build steps.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }