infernoflow 0.10.9 → 0.10.12

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 ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog — infernoflow
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.10.12 — 2026-04-12
6
+
7
+ ### Added
8
+ - `infernoflow install-cursor-hooks` — Cursor Agent hooks append assistant replies to `inferno/CONTEXT.draft.md`; `infernoflow init --cursor-hooks`.
9
+ - `infernoflow install-vscode-copilot-hooks` — VS Code + GitHub Copilot agent hooks (Preview) via `.github/hooks/`; `infernoflow init --vscode-copilot-hooks`.
10
+ - Shared draft tooling: `scripts/inferno-promote-draft.mjs`, `.gitignore` entry for `inferno/CONTEXT.draft.md`.
11
+ - `lib/draftToolingInstall.mjs` — shared installer logic for promote script and gitignore.
12
+
13
+ ### Changed
14
+ - CLI help widens command column for long names (e.g. `install-vscode-copilot-hooks`).
15
+
16
+ ## 0.1.0 — 2026-02-26
17
+
18
+ ### Added
19
+ - `infernoflow init` — interactive scaffold with prompts
20
+ - `infernoflow check` — full validation with clear error messages
21
+ - `infernoflow status` — at-a-glance dashboard
22
+ - `infernoflow doc-gate` — CI hook for keeping docs in sync
23
+ - Zero npm dependencies — works with Node.js 18+ out of the box
24
+ - `--json` flag on check for CI pipelines
25
+ - Auto-detect project name from package.json
26
+ - Auto-add npm scripts to package.json on init
package/README.md CHANGED
@@ -28,6 +28,17 @@ npx infernoflow init
28
28
  # 1. Scaffold in your project root:
29
29
  npx infernoflow init
30
30
 
31
+ # Optional: same as init, plus Cursor hooks that append each Agent reply
32
+ # to inferno/CONTEXT.draft.md (gitignored). Promote with npm run inferno:promote-draft.
33
+ npx infernoflow init --yes --cursor-hooks
34
+
35
+ # Optional: VS Code + GitHub Copilot agent hooks (Preview) — same draft file + promote flow
36
+ npx infernoflow init --yes --vscode-copilot-hooks
37
+
38
+ # Or add hooks to an existing infernoflow project:
39
+ npx infernoflow install-cursor-hooks
40
+ npx infernoflow install-vscode-copilot-hooks
41
+
31
42
  # 2. See your contract health:
32
43
  infernoflow status
33
44
 
@@ -172,11 +183,38 @@ infernoflow doc-gate --json
172
183
  | `infernoflow check` | Full validation: contract, capabilities, scenarios, changelog |
173
184
  | `infernoflow doc-gate` | Fails if code changed but docs weren't updated |
174
185
  | `infernoflow context` | Build/persist AI session context for this project |
186
+ | `infernoflow install-cursor-hooks` | Install `.cursor/hooks` + `scripts/inferno-promote-draft.mjs` (Agent → draft → promote) |
187
+ | `infernoflow install-vscode-copilot-hooks` | Install `.github/hooks` for VS Code + Copilot (Preview) + same promote script |
188
+
189
+ ### Cursor hooks (draft → promote)
190
+
191
+ [Cursor hooks](https://cursor.com/docs/agent/hooks) can run a small Node script after each assistant message. infernoflow can install:
192
+
193
+ - **`.cursor/hooks.json`** — `afterAgentResponse` and `stop` events
194
+ - **`.cursor/hooks/inferno-session-draft.mjs`** — appends assistant `text` to **`inferno/CONTEXT.draft.md`** (never overwrites `CONTEXT.md` automatically)
195
+ - **`scripts/inferno-promote-draft.mjs`** + **`npm run inferno:promote-draft`** — preview, `--append-notes` (merge under `## Decisions & notes` in `inferno/CONTEXT.md`), or `--clear`
196
+ - **`.gitignore`** entry for `inferno/CONTEXT.draft.md` when possible
197
+
198
+ Install with **`infernoflow install-cursor-hooks`** or **`infernoflow init --cursor-hooks`**. Restart Cursor after install. Review the draft before promoting; treat chat as **input**, not product truth.
199
+
200
+ ### VS Code + GitHub Copilot hooks (draft → promote, Preview)
201
+
202
+ VS Code can run [Agent hooks](https://code.visualstudio.com/docs/copilot/customization/hooks) from **`.github/hooks/*.json`**. infernoflow installs:
203
+
204
+ - **`.github/hooks/infernoflow-drafts.json`** — wires **`UserPromptSubmit`** (your prompt) and **`Stop`** (end of agent turn)
205
+ - **`scripts/inferno-vscode-copilot-hook.mjs`** — appends the user prompt on submit; on **Stop**, reads **`transcript_path`** from stdin (when present), parses **JSONL** or session **JSON**, and appends the **last assistant text** it can infer (format varies by VS Code / Copilot version — if parsing fails, a short marker is still appended)
206
+ - The same **`inferno:promote-draft`** script and **`.gitignore`** entry for **`inferno/CONTEXT.draft.md`** as the Cursor flow
207
+
208
+ Install with **`infernoflow install-vscode-copilot-hooks`** or **`infernoflow init --vscode-copilot-hooks`**. Restart VS Code, confirm your org allows hooks, and use the **GitHub Copilot Chat Hooks** output channel for diagnostics.
209
+
210
+ **Limitations:** Hooks are **Preview**; `transcript_path` / JSONL shape may differ by build; some hook events omit `transcript_path` ([vscode#300583](https://github.com/microsoft/vscode/issues/300583)). You still have the full **`infernoflow`** CLI in the terminal when hooks are not enough.
175
211
 
176
212
  ### Options
177
213
 
178
214
  ```bash
179
215
  infernoflow init --force # overwrite existing files
216
+ infernoflow init --cursor-hooks # with init: install Cursor draft hooks (see above)
217
+ infernoflow init --vscode-copilot-hooks # with init: install VS Code + Copilot draft hooks (Preview)
180
218
  infernoflow init --yes # skip prompts, use defaults
181
219
  infernoflow init --adopt # infer baseline from existing project
182
220
  infernoflow init --adopt --lang ts --framework react --project-type frontend
@@ -194,6 +232,8 @@ infernoflow pr-impact --json
194
232
  infernoflow sync --auto
195
233
  infernoflow sync --auto --json
196
234
  npm run inferno:hooks # install local git hooks (after init)
235
+ infernoflow install-cursor-hooks --force # overwrite hook files if present
236
+ infernoflow install-vscode-copilot-hooks --force
197
237
  infernoflow check --json # machine-readable output for CI
198
238
  infernoflow check --skip-doc-gate
199
239
  infernoflow status --json # machine-readable status summary
@@ -9,6 +9,9 @@ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8
9
9
  const VERSION = pkg.version || "0.0.0";
10
10
  const COMMAND_DESCRIPTIONS = {
11
11
  init: "Scaffold inferno/ in your project (or adopt existing project)",
12
+ "install-cursor-hooks": "Install Cursor hooks: draft agent replies to inferno/CONTEXT.draft.md",
13
+ "install-vscode-copilot-hooks":
14
+ "Install VS Code + Copilot agent hooks (Preview): draft to inferno/CONTEXT.draft.md",
12
15
  check: "Validate contract, capabilities, scenarios, changelog",
13
16
  status: "Show contract health at a glance",
14
17
  "pr-impact": "Summarize PR impact on capabilities and docs",
@@ -22,6 +25,10 @@ const COMMAND_DESCRIPTIONS = {
22
25
 
23
26
  const COMMAND_HANDLERS = {
24
27
  init: async (args) => (await import("../lib/commands/init.mjs")).initCommand(args),
28
+ "install-cursor-hooks": async (args) =>
29
+ (await import("../lib/commands/installCursorHooks.mjs")).installCursorHooksCommand(args),
30
+ "install-vscode-copilot-hooks": async (args) =>
31
+ (await import("../lib/commands/installVsCodeCopilotHooks.mjs")).installVsCodeCopilotHooksCommand(args),
25
32
  check: async (args) => (await import("../lib/commands/check.mjs")).checkCommand(args),
26
33
  status: async (args) => (await import("../lib/commands/status.mjs")).statusCommand(args),
27
34
  "pr-impact": async (args) => (await import("../lib/commands/prImpact.mjs")).prImpactCommand(args),
@@ -34,8 +41,10 @@ const COMMAND_HANDLERS = {
34
41
  };
35
42
 
36
43
  function formatCommandsHelp() {
44
+ const names = Object.keys(COMMAND_DESCRIPTIONS);
45
+ const w = Math.max(...names.map((n) => n.length), 8) + 1;
37
46
  return Object.entries(COMMAND_DESCRIPTIONS)
38
- .map(([name, desc]) => ` ${name.padEnd(13, " ")}${desc}`)
47
+ .map(([name, desc]) => ` ${name.padEnd(w, " ")}${desc}`)
39
48
  .join("\n");
40
49
  }
41
50
 
@@ -50,6 +59,8 @@ const HELP = `
50
59
  ${formatCommandsHelp()}
51
60
 
52
61
  ${bold("init options:")}
62
+ --cursor-hooks Also install Cursor hooks (draft → inferno/CONTEXT.draft.md)
63
+ --vscode-copilot-hooks Also install VS Code + Copilot hooks (.github/hooks — Preview)
53
64
  --adopt Infer capabilities from an existing codebase
54
65
  --lang <name> Override detected language (e.g. ts, js, py)
55
66
  --framework <name> Override detected framework (e.g. react, angular, express)
@@ -60,6 +71,12 @@ ${formatCommandsHelp()}
60
71
  --yes, -y Skip prompts and accept inferred/default values
61
72
  --force, -f Overwrite existing inferno/ files
62
73
 
74
+ ${bold("install-cursor-hooks options:")}
75
+ --force, -f Overwrite .cursor/hooks.json and hook scripts if they exist
76
+
77
+ ${bold("install-vscode-copilot-hooks options:")}
78
+ --force, -f Overwrite .github/hooks/infernoflow-drafts.json and scripts if they exist
79
+
63
80
  ${bold("context options:")}
64
81
  --intent "..." What you plan to build next
65
82
  --working "..." What you are building right now
@@ -11,6 +11,8 @@ import {
11
11
  summarizeCapabilities,
12
12
  buildSignalsReport,
13
13
  } from "./adopt.mjs";
14
+ import { installCursorHooksArtifacts } from "../cursorHooksInstall.mjs";
15
+ import { installVsCodeCopilotHooksArtifacts } from "../vsCodeCopilotHooksInstall.mjs";
14
16
 
15
17
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
18
 
@@ -153,6 +155,8 @@ export async function initCommand(args) {
153
155
  const force = args.includes("--force") || args.includes("-f");
154
156
  const yes = args.includes("--yes") || args.includes("-y");
155
157
  const adopt = args.includes("--adopt");
158
+ const cursorHooks = args.includes("--cursor-hooks");
159
+ const vscodeCopilotHooks = args.includes("--vscode-copilot-hooks");
156
160
  const reportJson = args.includes("--report-json");
157
161
  const reportJsonOnly = args.includes("--report-json-only");
158
162
  const reportHumanOnly = args.includes("--report-human-only");
@@ -317,6 +321,35 @@ export async function initCommand(args) {
317
321
 
318
322
  upsertScripts(cwd, silent);
319
323
 
324
+ if (cursorHooks) {
325
+ installCursorHooksArtifacts({
326
+ cwd,
327
+ templatesRoot: templates,
328
+ force,
329
+ silent,
330
+ logOk: (msg) => {
331
+ if (!silent) ok(msg);
332
+ },
333
+ logWarn: (msg) => {
334
+ if (!silent) warn(msg);
335
+ },
336
+ });
337
+ }
338
+ if (vscodeCopilotHooks) {
339
+ installVsCodeCopilotHooksArtifacts({
340
+ cwd,
341
+ templatesRoot: templates,
342
+ force,
343
+ silent,
344
+ logOk: (msg) => {
345
+ if (!silent) ok(msg);
346
+ },
347
+ logWarn: (msg) => {
348
+ if (!silent) warn(msg);
349
+ },
350
+ });
351
+ }
352
+
320
353
  if (adopt) {
321
354
  const statePath = path.join(infernoDir, "context-state.json");
322
355
  let state = {};
@@ -341,7 +374,28 @@ export async function initCommand(args) {
341
374
  cyan("infernoflow check") + " — validate everything",
342
375
  (adopt ? "Review inferred baseline in " : "Edit ") + yellow("inferno/capabilities.json") + (adopt ? " and refine IDs/titles" : " to describe each capability in detail"),
343
376
  "Add more " + yellow("inferno/scenarios/*.json") + " files for edge cases",
344
- "Add " + cyan("inferno:check") + " to your CI pipeline"
377
+ "Add " + cyan("inferno:check") + " to your CI pipeline",
378
+ ...(cursorHooks
379
+ ? [
380
+ "Restart Cursor — hooks write assistant text to " + yellow("inferno/CONTEXT.draft.md"),
381
+ "Promote when ready: " + cyan("npm run inferno:promote-draft -- --append-notes"),
382
+ ]
383
+ : []),
384
+ ...(vscodeCopilotHooks
385
+ ? [
386
+ "Restart VS Code — Copilot hooks append prompts + assistant (from transcript) to " +
387
+ yellow("inferno/CONTEXT.draft.md"),
388
+ "Promote when ready: " + cyan("npm run inferno:promote-draft -- --append-notes"),
389
+ ]
390
+ : []),
391
+ ...(!cursorHooks && !vscodeCopilotHooks
392
+ ? [
393
+ "Optional: " +
394
+ cyan("infernoflow install-cursor-hooks") +
395
+ " or " +
396
+ cyan("infernoflow install-vscode-copilot-hooks"),
397
+ ]
398
+ : []),
345
399
  ]);
346
400
  }
347
401
  }
@@ -0,0 +1,36 @@
1
+ import * as path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { header, ok, warn, done, nextSteps, cyan, yellow } from "../ui/output.mjs";
4
+ import { installCursorHooksArtifacts } from "../cursorHooksInstall.mjs";
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+
8
+ function getTemplatesRoot() {
9
+ return path.resolve(__dirname, "../../templates");
10
+ }
11
+
12
+ export async function installCursorHooksCommand(args) {
13
+ const cwd = process.cwd();
14
+ const force = args.includes("--force") || args.includes("-f");
15
+
16
+ header("install-cursor-hooks");
17
+
18
+ installCursorHooksArtifacts({
19
+ cwd,
20
+ templatesRoot: getTemplatesRoot(),
21
+ force,
22
+ silent: false,
23
+ logOk: (msg) => ok(msg),
24
+ logWarn: (msg) => warn(msg),
25
+ });
26
+
27
+ done("Cursor draft hooks installed");
28
+
29
+ nextSteps([
30
+ "Restart Cursor (or reload window) so " + yellow(".cursor/hooks.json") + " is picked up",
31
+ "Use Agent chat — each assistant reply appends to " + yellow("inferno/CONTEXT.draft.md") + " (gitignored)",
32
+ cyan("npm run inferno:promote-draft") + " — preview draft",
33
+ cyan("npm run inferno:promote-draft -- --append-notes") + " — merge into inferno/CONTEXT.md under Decisions",
34
+ cyan("npm run inferno:promote-draft -- --clear") + " — discard draft",
35
+ ]);
36
+ }
@@ -0,0 +1,37 @@
1
+ import * as path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { header, ok, warn, done, nextSteps, cyan, yellow } from "../ui/output.mjs";
4
+ import { installVsCodeCopilotHooksArtifacts } from "../vsCodeCopilotHooksInstall.mjs";
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+
8
+ function getTemplatesRoot() {
9
+ return path.resolve(__dirname, "../../templates");
10
+ }
11
+
12
+ export async function installVsCodeCopilotHooksCommand(args) {
13
+ const cwd = process.cwd();
14
+ const force = args.includes("--force") || args.includes("-f");
15
+
16
+ header("install-vscode-copilot-hooks");
17
+
18
+ installVsCodeCopilotHooksArtifacts({
19
+ cwd,
20
+ templatesRoot: getTemplatesRoot(),
21
+ force,
22
+ silent: false,
23
+ logOk: (msg) => ok(msg),
24
+ logWarn: (msg) => warn(msg),
25
+ });
26
+
27
+ done("VS Code / Copilot draft hooks installed");
28
+
29
+ nextSteps([
30
+ "Requires VS Code + GitHub Copilot and **Agent hooks (Preview)** — see " +
31
+ yellow("https://code.visualstudio.com/docs/copilot/customization/hooks"),
32
+ "Hooks load from " + yellow(".github/hooks/*.json") + " — restart VS Code or reload window after first install",
33
+ "Check the **GitHub Copilot Chat Hooks** output channel if nothing runs",
34
+ cyan("npm run inferno:promote-draft") + " — preview draft",
35
+ cyan("npm run inferno:promote-draft -- --append-notes") + " — merge into inferno/CONTEXT.md",
36
+ ]);
37
+ }
@@ -0,0 +1,39 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { installInfernoDraftTooling } from "./draftToolingInstall.mjs";
4
+
5
+ /**
6
+ * @param {object} opts
7
+ * @param {string} opts.cwd
8
+ * @param {string} opts.templatesRoot
9
+ * @param {boolean} opts.force
10
+ * @param {boolean} opts.silent
11
+ * @param {(msg: string) => void} [opts.logOk]
12
+ * @param {(msg: string) => void} [opts.logWarn]
13
+ */
14
+ export function installCursorHooksArtifacts(opts) {
15
+ const { cwd, templatesRoot, force, silent } = opts;
16
+ const logOk = opts.logOk || (() => {});
17
+ const logWarn = opts.logWarn || (() => {});
18
+
19
+ function copyFile(src, dst) {
20
+ if (fs.existsSync(dst) && !force) {
21
+ if (!silent) logWarn("Skipped (exists): " + path.relative(cwd, dst));
22
+ return false;
23
+ }
24
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
25
+ fs.copyFileSync(src, dst);
26
+ if (!silent) logOk("Created: " + path.relative(cwd, dst));
27
+ return true;
28
+ }
29
+
30
+ installInfernoDraftTooling({ cwd, templatesRoot, force, silent, logOk, logWarn });
31
+
32
+ const srcHooksJson = path.join(templatesRoot, "cursor", "hooks.json");
33
+ const dstHooksJson = path.join(cwd, ".cursor", "hooks.json");
34
+ const srcHook = path.join(templatesRoot, "cursor", "hooks", "inferno-session-draft.mjs");
35
+ const dstHook = path.join(cwd, ".cursor", "hooks", "inferno-session-draft.mjs");
36
+
37
+ copyFile(srcHooksJson, dstHooksJson);
38
+ copyFile(srcHook, dstHook);
39
+ }
@@ -0,0 +1,69 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ const GITIGNORE_SNIPPET = `
5
+ # infernoflow: agent draft (IDE hooks — review before commit)
6
+ inferno/CONTEXT.draft.md
7
+ `.trimStart();
8
+
9
+ function upsertPromoteScript(cwd, silent, logOk) {
10
+ const pkgPath = path.join(cwd, "package.json");
11
+ if (!fs.existsSync(pkgPath)) {
12
+ if (!silent) logOk("No package.json — add script manually: inferno:promote-draft");
13
+ return;
14
+ }
15
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
16
+ pkg.scripts = pkg.scripts || {};
17
+ if (!pkg.scripts["inferno:promote-draft"]) {
18
+ pkg.scripts["inferno:promote-draft"] = "node scripts/inferno-promote-draft.mjs";
19
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
20
+ if (!silent) logOk("Updated package.json script: inferno:promote-draft");
21
+ }
22
+ }
23
+
24
+ /**
25
+ * inferno/CONTEXT.draft.md gitignore + promote script (shared by Cursor and VS Code installers).
26
+ * @param {object} opts
27
+ * @param {string} opts.cwd
28
+ * @param {string} opts.templatesRoot
29
+ * @param {boolean} opts.force
30
+ * @param {boolean} opts.silent
31
+ * @param {(msg: string) => void} [opts.logOk]
32
+ * @param {(msg: string) => void} [opts.logWarn]
33
+ */
34
+ export function installInfernoDraftTooling(opts) {
35
+ const { cwd, templatesRoot, force, silent } = opts;
36
+ const logOk = opts.logOk || (() => {});
37
+ const logWarn = opts.logWarn || (() => {});
38
+
39
+ function copyFile(src, dst) {
40
+ if (fs.existsSync(dst) && !force) {
41
+ if (!silent) logWarn("Skipped (exists): " + path.relative(cwd, dst));
42
+ return false;
43
+ }
44
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
45
+ fs.copyFileSync(src, dst);
46
+ if (!silent) logOk("Created: " + path.relative(cwd, dst));
47
+ return true;
48
+ }
49
+
50
+ const srcPromote = path.join(templatesRoot, "scripts", "inferno-promote-draft.mjs");
51
+ const dstPromote = path.join(cwd, "scripts", "inferno-promote-draft.mjs");
52
+ copyFile(srcPromote, dstPromote);
53
+
54
+ upsertPromoteScript(cwd, silent, logOk);
55
+
56
+ const gi = path.join(cwd, ".gitignore");
57
+ if (fs.existsSync(gi)) {
58
+ const cur = fs.readFileSync(gi, "utf8");
59
+ if (cur.includes("CONTEXT.draft.md")) {
60
+ if (!silent) logOk(".gitignore already mentions CONTEXT.draft.md");
61
+ } else {
62
+ fs.appendFileSync(gi, `\n${GITIGNORE_SNIPPET}\n`, "utf8");
63
+ if (!silent) logOk("Updated: " + path.relative(cwd, gi));
64
+ }
65
+ } else {
66
+ fs.writeFileSync(gi, `${GITIGNORE_SNIPPET}\n`, "utf8");
67
+ if (!silent) logOk("Created: " + path.relative(cwd, gi));
68
+ }
69
+ }
@@ -0,0 +1,42 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { installInfernoDraftTooling } from "./draftToolingInstall.mjs";
4
+
5
+ /**
6
+ * VS Code + GitHub Copilot agent hooks (Preview). See:
7
+ * https://code.visualstudio.com/docs/copilot/customization/hooks
8
+ *
9
+ * @param {object} opts
10
+ * @param {string} opts.cwd
11
+ * @param {string} opts.templatesRoot
12
+ * @param {boolean} opts.force
13
+ * @param {boolean} opts.silent
14
+ * @param {(msg: string) => void} [opts.logOk]
15
+ * @param {(msg: string) => void} [opts.logWarn]
16
+ */
17
+ export function installVsCodeCopilotHooksArtifacts(opts) {
18
+ const { cwd, templatesRoot, force, silent } = opts;
19
+ const logOk = opts.logOk || (() => {});
20
+ const logWarn = opts.logWarn || (() => {});
21
+
22
+ function copyFile(src, dst) {
23
+ if (fs.existsSync(dst) && !force) {
24
+ if (!silent) logWarn("Skipped (exists): " + path.relative(cwd, dst));
25
+ return false;
26
+ }
27
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
28
+ fs.copyFileSync(src, dst);
29
+ if (!silent) logOk("Created: " + path.relative(cwd, dst));
30
+ return true;
31
+ }
32
+
33
+ installInfernoDraftTooling({ cwd, templatesRoot, force, silent, logOk, logWarn });
34
+
35
+ const srcHooks = path.join(templatesRoot, "github-hooks", "infernoflow-drafts.json");
36
+ const dstHooks = path.join(cwd, ".github", "hooks", "infernoflow-drafts.json");
37
+ const srcHookScript = path.join(templatesRoot, "scripts", "inferno-vscode-copilot-hook.mjs");
38
+ const dstHookScript = path.join(cwd, "scripts", "inferno-vscode-copilot-hook.mjs");
39
+
40
+ copyFile(srcHooks, dstHooks);
41
+ copyFile(srcHookScript, dstHookScript);
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.10.9",
3
+ "version": "0.10.12",
4
4
  "description": "The forge for liquid code — keep capabilities, contracts, and docs in sync.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,8 @@
13
13
  "bin",
14
14
  "lib",
15
15
  "templates",
16
- "README.md"
16
+ "README.md",
17
+ "CHANGELOG.md"
17
18
  ],
18
19
  "scripts": {
19
20
  "test": "node scripts/smoke.mjs && node scripts/json-smoke.mjs && node scripts/json-negative-smoke.mjs && node scripts/implement-smoke.mjs && node scripts/adopt-smoke.mjs && node scripts/pr-impact-smoke.mjs && node scripts/sync-smoke.mjs && node scripts/run-smoke.mjs",
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Cursor hook: append agent output to inferno/CONTEXT.draft.md (gitignored).
4
+ * - Default stdin: afterAgentResponse → { text }
5
+ * - --agent-stop stdin: stop → { status, loop_count, ... }
6
+ * Never fail closed: errors go to stderr; stdout is {} for Cursor.
7
+ */
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+
11
+ /** Keep in sync with templates/scripts/inferno-promote-draft.mjs (split at first \\n---\\n). */
12
+ const DRAFT_HEADER = `# CONTEXT draft (gitignored)
13
+
14
+ Auto-captured by Cursor hooks (\`.cursor/hooks/inferno-session-draft.mjs\`). **Not product truth** — review, then run \`npm run inferno:promote-draft\` or \`infernoflow context\`.
15
+
16
+ ---
17
+ `;
18
+
19
+ const MAX_MESSAGE_CHARS = 120_000;
20
+ const MAX_FILE_BYTES = 280_000;
21
+
22
+ function projectRoot() {
23
+ return process.cwd();
24
+ }
25
+
26
+ function draftPath() {
27
+ return path.join(projectRoot(), "inferno", "CONTEXT.draft.md");
28
+ }
29
+
30
+ function ensureDraftFile(file) {
31
+ if (!fs.existsSync(file)) {
32
+ fs.mkdirSync(path.dirname(file), { recursive: true });
33
+ fs.writeFileSync(file, DRAFT_HEADER, "utf8");
34
+ }
35
+ }
36
+
37
+ function trimFile(file) {
38
+ const raw = fs.readFileSync(file, "utf8");
39
+ if (Buffer.byteLength(raw, "utf8") <= MAX_FILE_BYTES) return;
40
+ const keep = raw.slice(-Math.floor(MAX_FILE_BYTES * 0.85));
41
+ const idx = keep.indexOf("\n### ");
42
+ const body = idx === -1 ? keep : keep.slice(idx);
43
+ fs.writeFileSync(file, `${DRAFT_HEADER}\n_(older capture trimmed for size)_\n\n${body}`, "utf8");
44
+ }
45
+
46
+ function appendBlock(file, block) {
47
+ ensureDraftFile(file);
48
+ fs.appendFileSync(file, block, "utf8");
49
+ trimFile(file);
50
+ }
51
+
52
+ async function readStdin() {
53
+ const chunks = [];
54
+ for await (const c of process.stdin) chunks.push(c);
55
+ return Buffer.concat(chunks).toString("utf8");
56
+ }
57
+
58
+ function main() {
59
+ const agentStop = process.argv.includes("--agent-stop");
60
+
61
+ readStdin()
62
+ .then((raw) => {
63
+ let data = {};
64
+ try {
65
+ data = raw.trim() ? JSON.parse(raw) : {};
66
+ } catch (e) {
67
+ console.error("[inferno-session-draft] stdin JSON parse:", e.message);
68
+ console.log("{}");
69
+ process.exit(0);
70
+ return;
71
+ }
72
+
73
+ const file = draftPath();
74
+ if (agentStop) {
75
+ const status = data.status ?? "unknown";
76
+ const loop = data.loop_count ?? 0;
77
+ appendBlock(
78
+ file,
79
+ `\n### _agent stop_ (${new Date().toISOString()})\n\nstatus: \`${status}\` · loop_count: ${loop}\n\n---\n`
80
+ );
81
+ console.log("{}");
82
+ process.exit(0);
83
+ return;
84
+ }
85
+
86
+ const text = typeof data.text === "string" ? data.text : "";
87
+ if (!text.trim()) {
88
+ console.log("{}");
89
+ process.exit(0);
90
+ return;
91
+ }
92
+
93
+ const clipped =
94
+ text.length > MAX_MESSAGE_CHARS
95
+ ? `${text.slice(0, MAX_MESSAGE_CHARS)}\n\n_…trimmed (${text.length - MAX_MESSAGE_CHARS} chars omitted)_\n`
96
+ : text;
97
+
98
+ appendBlock(
99
+ file,
100
+ `\n### Assistant message (${new Date().toISOString()})\n\n${clipped}\n\n---\n`
101
+ );
102
+ console.log("{}");
103
+ process.exit(0);
104
+ })
105
+ .catch((e) => {
106
+ console.error("[inferno-session-draft]", e);
107
+ console.log("{}");
108
+ process.exit(0);
109
+ });
110
+ }
111
+
112
+ main();
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "afterAgentResponse": [
5
+ {
6
+ "command": "node .cursor/hooks/inferno-session-draft.mjs"
7
+ }
8
+ ],
9
+ "stop": [
10
+ {
11
+ "command": "node .cursor/hooks/inferno-session-draft.mjs --agent-stop"
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "UserPromptSubmit": [
5
+ {
6
+ "type": "command",
7
+ "command": "node scripts/inferno-vscode-copilot-hook.mjs"
8
+ }
9
+ ],
10
+ "Stop": [
11
+ {
12
+ "type": "command",
13
+ "command": "node scripts/inferno-vscode-copilot-hook.mjs"
14
+ }
15
+ ]
16
+ }
17
+ }
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Merge inferno/CONTEXT.draft.md into inferno/CONTEXT.md under ## Decisions & notes,
4
+ * or clear the draft. Draft is gitignored; CONTEXT.md is the promoted source of truth.
5
+ */
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+
9
+ const root = process.cwd();
10
+ const draftFile = path.join(root, "inferno", "CONTEXT.draft.md");
11
+ const contextFile = path.join(root, "inferno", "CONTEXT.md");
12
+
13
+ /** Keep in sync with .cursor/hooks/inferno-session-draft.mjs (getDraftBody splits at first \\n---\\n). */
14
+ const DRAFT_HEADER = `# CONTEXT draft (gitignored)
15
+
16
+ Auto-captured by IDE hooks (Cursor and/or VS Code + Copilot). **Not product truth** — review, then run \`npm run inferno:promote-draft\` or \`infernoflow context\`.
17
+
18
+ ---
19
+ `;
20
+
21
+ const DECISIONS_ANCHOR = "## Decisions & notes";
22
+ const PASTE_FOOTER_ANCHOR = "\n---\n_Paste this block at the start of any new AI session._";
23
+
24
+ function read(p) {
25
+ return fs.readFileSync(p, "utf8");
26
+ }
27
+
28
+ function write(p, s) {
29
+ fs.writeFileSync(p, s, "utf8");
30
+ }
31
+
32
+ function getDraftBody() {
33
+ if (!fs.existsSync(draftFile)) return "";
34
+ const full = read(draftFile);
35
+ const sep = full.indexOf("\n---\n");
36
+ if (sep === -1) return full.trim();
37
+ const after = full.slice(sep + "\n---\n".length).trim();
38
+ return after;
39
+ }
40
+
41
+ function clearDraft() {
42
+ fs.mkdirSync(path.dirname(draftFile), { recursive: true });
43
+ write(draftFile, DRAFT_HEADER);
44
+ console.log("Cleared inferno/CONTEXT.draft.md (header only).");
45
+ }
46
+
47
+ function appendNotes() {
48
+ if (!fs.existsSync(contextFile)) {
49
+ console.error("Missing inferno/CONTEXT.md");
50
+ process.exit(1);
51
+ }
52
+ const body = getDraftBody();
53
+ if (!body) {
54
+ console.error("Nothing to promote: inferno/CONTEXT.draft.md is empty after the header.");
55
+ process.exit(1);
56
+ }
57
+
58
+ const ctx = read(contextFile);
59
+ const i = ctx.indexOf(DECISIONS_ANCHOR);
60
+ if (i === -1) {
61
+ console.error(`Could not find "${DECISIONS_ANCHOR}" in inferno/CONTEXT.md`);
62
+ process.exit(1);
63
+ }
64
+ const j = ctx.indexOf(PASTE_FOOTER_ANCHOR, i);
65
+ if (j === -1) {
66
+ console.error("Could not find paste footer block in inferno/CONTEXT.md");
67
+ process.exit(1);
68
+ }
69
+
70
+ const before = ctx.slice(0, i + DECISIONS_ANCHOR.length);
71
+ const decisionsAndFooter = ctx.slice(i + DECISIONS_ANCHOR.length, j);
72
+ const after = ctx.slice(j);
73
+
74
+ let middle = decisionsAndFooter;
75
+ if (middle.includes("_No decisions recorded_")) {
76
+ middle = middle.replace("_No decisions recorded_", "").replace(/\n\n\n+/g, "\n\n");
77
+ }
78
+
79
+ const stamp = new Date().toISOString().slice(0, 19);
80
+ const indented = body.split("\n").map((line) => ` ${line}`).join("\n");
81
+ const block = `\n\n### Captured from agent draft (${stamp})\n\n${indented}\n`;
82
+
83
+ write(contextFile, `${before}${middle}${block}${after}`);
84
+ clearDraft();
85
+ console.log("Appended draft under ## Decisions & notes in inferno/CONTEXT.md and cleared the draft.");
86
+ console.log("Next: edit wording if needed, then run infernoflow check when contract/changelog should match.");
87
+ }
88
+
89
+ const args = process.argv.slice(2);
90
+ if (args.includes("--clear")) {
91
+ clearDraft();
92
+ process.exit(0);
93
+ }
94
+ if (args.includes("--append-notes")) {
95
+ appendNotes();
96
+ process.exit(0);
97
+ }
98
+
99
+ const body = getDraftBody();
100
+ if (!body) {
101
+ console.log("inferno/CONTEXT.draft.md has no captured content yet (after the header).");
102
+ console.log("Use the Agent chat; each assistant reply appends via the afterAgentResponse hook.");
103
+ process.exit(0);
104
+ }
105
+
106
+ console.log("--- inferno/CONTEXT.draft.md (excerpt, first 2000 chars) ---\n");
107
+ console.log(body.slice(0, 2000) + (body.length > 2000 ? "\n…" : ""));
108
+ console.log("\n---");
109
+ console.log("Promote into CONTEXT.md under Decisions:");
110
+ console.log(" npm run inferno:promote-draft -- --append-notes");
111
+ console.log("Or set working/intent via CLI:");
112
+ console.log(' npm exec -- infernoflow context --working "..." --intent "..."');
113
+ console.log("Clear draft without merging:");
114
+ console.log(" npm run inferno:promote-draft -- --clear");
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * GitHub Copilot / VS Code agent hook (Preview): stdin JSON per
4
+ * https://code.visualstudio.com/docs/copilot/customization/hooks
5
+ *
6
+ * - UserPromptSubmit: appends the user's prompt to inferno/CONTEXT.draft.md
7
+ * - Stop: reads transcript_path (JSONL or session JSON), appends last assistant text if found
8
+ *
9
+ * Always prints {"continue":true} so the agent is never blocked. Errors → stderr only.
10
+ */
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+
14
+ /** Keep in sync with inferno-promote-draft.mjs / inferno-session-draft.mjs */
15
+ const DRAFT_HEADER = `# CONTEXT draft (gitignored)
16
+
17
+ Auto-captured by VS Code / Copilot hooks. **Not product truth** — review, then run \`npm run inferno:promote-draft\` or \`infernoflow context\`.
18
+
19
+ ---
20
+ `;
21
+
22
+ const MAX_APPEND = 120_000;
23
+ const MAX_FILE_BYTES = 280_000;
24
+
25
+ function draftPath(root) {
26
+ return path.join(root, "inferno", "CONTEXT.draft.md");
27
+ }
28
+
29
+ function ensureDraft(file) {
30
+ if (!fs.existsSync(file)) {
31
+ fs.mkdirSync(path.dirname(file), { recursive: true });
32
+ fs.writeFileSync(file, DRAFT_HEADER, "utf8");
33
+ }
34
+ }
35
+
36
+ function trimFile(file) {
37
+ const raw = fs.readFileSync(file, "utf8");
38
+ if (Buffer.byteLength(raw, "utf8") <= MAX_FILE_BYTES) return;
39
+ const keep = raw.slice(-Math.floor(MAX_FILE_BYTES * 0.85));
40
+ const idx = keep.indexOf("\n### ");
41
+ const body = idx === -1 ? keep : keep.slice(idx);
42
+ fs.writeFileSync(file, `${DRAFT_HEADER}\n_(older capture trimmed for size)_\n\n${body}`, "utf8");
43
+ }
44
+
45
+ function append(root, block) {
46
+ const file = draftPath(root);
47
+ ensureDraft(file);
48
+ fs.appendFileSync(file, block, "utf8");
49
+ trimFile(file);
50
+ }
51
+
52
+ function readStdinSync() {
53
+ /** Prefer readSync: on some Windows shells, readFileSync(0) returns empty for piped hook stdin. */
54
+ const buf = Buffer.alloc(16 * 1024 * 1024);
55
+ let n = 0;
56
+ try {
57
+ n = fs.readSync(0, buf, 0, buf.length, null);
58
+ } catch {
59
+ return "";
60
+ }
61
+ return buf.slice(0, n).toString("utf8");
62
+ }
63
+
64
+ function flattenResponse(resp) {
65
+ if (!resp) return "";
66
+ if (typeof resp === "string") return resp.slice(0, MAX_APPEND);
67
+ if (typeof resp.markdown === "string") return resp.markdown;
68
+ if (typeof resp.text === "string") return resp.text;
69
+ if (Array.isArray(resp.parts)) {
70
+ const bits = resp.parts
71
+ .map((p) => (typeof p === "string" ? p : p?.text || p?.content || p?.value || ""))
72
+ .filter(Boolean);
73
+ if (bits.length) return bits.join("\n").slice(0, MAX_APPEND);
74
+ }
75
+ if (resp.message && typeof resp.message.text === "string") return resp.message.text;
76
+ return "";
77
+ }
78
+
79
+ function lastAssistantFromSessionJson(data) {
80
+ const reqs = data.requests;
81
+ if (!Array.isArray(reqs)) return "";
82
+ for (let i = reqs.length - 1; i >= 0; i--) {
83
+ const t = flattenResponse(reqs[i]?.response);
84
+ if (t && t.trim()) return t.slice(0, MAX_APPEND);
85
+ }
86
+ return "";
87
+ }
88
+
89
+ function extractFromJsonlLine(obj) {
90
+ if (!obj || typeof obj !== "object") return "";
91
+ const a = obj.assistant;
92
+ if (a && typeof a.message === "string" && a.message.trim()) return a.message.slice(0, MAX_APPEND);
93
+ if (a && typeof a.text === "string" && a.text.trim()) return a.text.slice(0, MAX_APPEND);
94
+ if ((obj.role === "assistant" || obj.type === "assistant") && typeof obj.message === "string")
95
+ return obj.message.slice(0, MAX_APPEND);
96
+ if ((obj.role === "assistant" || obj.type === "assistant") && typeof obj.content === "string")
97
+ return obj.content.slice(0, MAX_APPEND);
98
+ if (obj.assistantMessage && typeof obj.assistantMessage === "string")
99
+ return obj.assistantMessage.slice(0, MAX_APPEND);
100
+ return "";
101
+ }
102
+
103
+ function lastAssistantFromTranscriptFile(transcriptPath) {
104
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) return "";
105
+ const raw = fs.readFileSync(transcriptPath, "utf8").trim();
106
+ if (!raw) return "";
107
+
108
+ if (raw.startsWith("{")) {
109
+ try {
110
+ const data = JSON.parse(raw);
111
+ const fromReq = lastAssistantFromSessionJson(data);
112
+ if (fromReq) return fromReq;
113
+ } catch {
114
+ /* fall through */
115
+ }
116
+ }
117
+
118
+ let last = "";
119
+ for (const line of raw.split("\n")) {
120
+ const t = line.trim();
121
+ if (!t || t[0] !== "{") continue;
122
+ try {
123
+ const v = extractFromJsonlLine(JSON.parse(t));
124
+ if (v) last = v;
125
+ } catch {
126
+ /* skip line */
127
+ }
128
+ }
129
+ return last;
130
+ }
131
+
132
+ function main() {
133
+ let data = {};
134
+ try {
135
+ const s = readStdinSync().trim();
136
+ if (s) data = JSON.parse(s);
137
+ } catch (e) {
138
+ console.error("[inferno-vscode-copilot-hook] stdin JSON:", e.message);
139
+ console.log(JSON.stringify({ continue: true }));
140
+ process.exit(0);
141
+ return;
142
+ }
143
+
144
+ const root = data.cwd || process.cwd();
145
+ const hook = String(data.hookEventName || data.hook_event_name || "")
146
+ .replace(/\s+/g, "")
147
+ .toLowerCase();
148
+
149
+ try {
150
+ if (hook === "userpromptsubmit") {
151
+ const prompt = data.prompt || data.Prompt || "";
152
+ if (typeof prompt === "string" && prompt.trim()) {
153
+ append(
154
+ root,
155
+ `\n### User prompt (${new Date().toISOString()})\n\n${prompt.slice(0, MAX_APPEND)}\n\n---\n`
156
+ );
157
+ }
158
+ } else if (hook === "stop") {
159
+ const tp = data.transcript_path || data.transcriptPath;
160
+ const assistant = lastAssistantFromTranscriptFile(tp);
161
+ const stopActive = data.stop_hook_active ?? data.stopHookActive;
162
+ if (assistant) {
163
+ append(
164
+ root,
165
+ `\n### Assistant (from transcript) (${new Date().toISOString()})\n\n${assistant}\n\n---\n`
166
+ );
167
+ } else {
168
+ append(
169
+ root,
170
+ `\n### _Copilot Stop_ (${new Date().toISOString()})\n\nstop_hook_active: ${Boolean(stopActive)}${
171
+ tp ? ` · transcript: ${tp}` : " · (no transcript_path or empty parse)"
172
+ }\n\n---\n`
173
+ );
174
+ }
175
+ }
176
+ } catch (e) {
177
+ console.error("[inferno-vscode-copilot-hook]", e);
178
+ }
179
+
180
+ console.log(JSON.stringify({ continue: true }));
181
+ process.exit(0);
182
+ }
183
+
184
+ main();