@entelligentsia/forgecli 0.2.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -5,6 +5,89 @@ All notable changes to `@entelligentsia/forgecli` are documented here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ---
11
+
12
+ ## [0.3.0] — 2026-05-09
13
+
14
+ Headline: Pi-runtime parity adapters — interactive UX + hook safety net.
15
+ `forge:ask_user` delivers real TUI prompts for all `/forge:init` gate sites;
16
+ the hook safety net enforces store-cli write validity and legal status
17
+ transitions at runtime. Fixes BUG-026 (non-blocking Y/N prompts) and BUG-027
18
+ (unguarded store-cli writes under pi runtime).
19
+
20
+ ### Added
21
+
22
+ - **`forge:ask_user` custom tool** (FORGE-S18-T04).
23
+ Registers `forge_ask_user` via `pi.registerTool`. Accepts `{question, type, options?, default?}`
24
+ where `type` is `confirm` (Y/N), `choice` (select from list), or `text` (free-form input).
25
+ Uses `ctx.ui.confirm / select / input` from the pi `ExtensionContext` — no raw pi-tui
26
+ component wiring needed. Blocks the model loop until the user responds. Non-interactive
27
+ bypass: `FORGE_YES=1`, `--non-interactive`, or headless mode returns the default
28
+ immediately. Cancellation surfaces as `isError: true`. 14 Vitest tests.
29
+
30
+ - **`registerHookDispatcher` wired to `tool_call` / `tool_result`** (FORGE-S18-T02).
31
+ Replaces the 7-line empty shim with a real implementation. Subscribes both pi events
32
+ (wired to tool_call/tool_result; enforcement layer added by T03).
33
+ Exports `parseStoreCLIInvocation()` + `StoreCLICall` interface for T03 to layer
34
+ validation on top. Hook inventory document produced at
35
+ `engineering/sprints/FORGE-S18/FORGE-S18-T02/HOOK_INVENTORY.md`.
36
+
37
+ - **Store-cli pushback correction loop + audit-log mode** (FORGE-S18-T03).
38
+ Extends the T02 hook-dispatcher scaffold from audit-only to enforcement.
39
+ `store-validator.ts` spawns `store-cli validate` synchronously on every write call;
40
+ `transition-guard.ts` enforces the legal status-transition table for task/sprint/bug
41
+ records. Returns `{ block: true, reason }` on violation. `FORGE_HOOK_AUDIT=1` logs
42
+ every decision (would-block/would-allow) with timestamp, entity, reason — returns
43
+ `undefined` (allow-through) regardless, enabling observation without disruption.
44
+ Closes FORGE-BUG-027.
45
+
46
+ ### Changed
47
+
48
+ - **Gate sites in `/forge:init` now use `forge:ask_user`** (FORGE-S18-T05). G2
49
+ (pre-flight phase selector) and G3 (KB folder prompt) replaced from
50
+ `sendToAgent+waitForIdle` to `ctx.ui.confirm / ctx.ui.input`. Operator receives a
51
+ real TUI prompt instead of model-generated text. Gate audit captured in
52
+ `GATE_AUDIT.md`.
53
+
54
+ ### Fixed
55
+
56
+ - **BUG-026** — Pi runtime: TUI Y/N prompts in command bodies don't wait for user
57
+ input. Fixed by `forge:ask_user` tool (T04) + gate-site retrofit (T05). `/forge:init`
58
+ gate sites G2 and G3 now use `ctx.ui.confirm/input` and block until the operator
59
+ responds.
60
+ - **BUG-027** — Pi runtime: validation hooks not wired — store-cli writes unguarded,
61
+ no pushback correction loop. Fixed by hook adapter (T02) + enforcement layer (T03).
62
+ `registerHookDispatcher` intercepts `store-cli write` and `store-cli update-status`
63
+ calls, validates against schema and transition table, returns `{ block: true, reason }`
64
+ on violation.
65
+
66
+ ---
67
+
68
+ ## [0.2.1] — 2026-05-09
69
+
70
+ Headline: Non-interactive mode for CI and scripted use. `FORGE_YES=1` and
71
+ `forge --non-interactive` both bypass every Y/N gate in `/forge:init`, resolving
72
+ each to its documented default. Unblocks scripted adoption immediately (BUG-026
73
+ short-circuit; T04/T05 deliver the real interactive TUI path).
74
+
75
+ ### Added
76
+
77
+ - **`--non-interactive` CLI flag** (FORGE-S18-T01). Parsed by `argv.ts`,
78
+ sets `FORGE_NON_INTERACTIVE=1`. Documented in `--help` output.
79
+ - **`FORGE_YES=1` environment variable** (FORGE-S18-T01). Ergonomic shorthand
80
+ for scripts (`FORGE_YES=1 forge`). Checked alongside `FORGE_NON_INTERACTIVE`.
81
+ - **`isNonInteractive()` helper** in `forge-init.ts`. Bypasses G1 (resume
82
+ confirm), G2 (pre-flight phase selector), G3 (KB folder name), G4 (CLAUDE.md
83
+ create confirm) when active.
84
+ - **README non-interactive mode section** — flag, env var, and default-resolution
85
+ table for all four gate sites.
86
+ - **Vitest gate coverage** — 12 new test cases covering each gate under
87
+ interactive, `FORGE_NON_INTERACTIVE=1`, and `FORGE_YES=1` arms.
88
+ - **E2E smoke gates E2E-04/05/06** — auth-free checks that flag and env vars
89
+ are accepted without errors.
90
+
8
91
  ## [0.2.0] — 2026-05-09
9
92
 
10
93
  Headline: `/forge:init` is now a real implementation. The 0.1.0 stub at
@@ -109,4 +192,7 @@ ported onto `@earendil-works/pi-coding-agent`.
109
192
  - `claude-agent-sdk` plan-limit support (deferred to S17+).
110
193
  - Cost telemetry surfacing in `/forge:*` (waived for S16).
111
194
 
195
+ [0.3.0]: https://github.com/Entelligentsia/forge-cli/releases/tag/v0.3.0
196
+ [0.2.1]: https://github.com/Entelligentsia/forge-cli/releases/tag/v0.2.1
197
+ [0.2.0]: https://github.com/Entelligentsia/forge-cli/releases/tag/v0.2.0
112
198
  [0.1.0]: https://github.com/Entelligentsia/forge-cli/releases/tag/v0.1.0
package/README.md CHANGED
@@ -35,19 +35,146 @@ Outputs land in `.forge/{personas,skills,workflows,templates,config.json,project
35
35
  ## CLI flags
36
36
 
37
37
  ```
38
- forge --version Print version triplet (forgecli, forge, pi)
39
- forge --help Show forge + pi help
40
- forge --no-update-check Skip update check
41
- forge --registry <path> Override model registry
38
+ forge --version Print version triplet (forgecli, forge, pi)
39
+ forge --help Show forge + pi help
40
+ forge --no-update-check Skip update check
41
+ forge --non-interactive Bypass all Y/N gates with defaults (CI/scripted use)
42
+ forge --registry <path> Override model registry
42
43
  ```
43
44
 
44
45
  Pi flags (`-p`, `--cwd`, `--session`, `--model`, `--tools`, `--thinking`, …) are forwarded verbatim. Run `forge --help` for the full list.
45
46
 
47
+ ## Non-interactive mode
48
+
49
+ For CI, scripts, or any context where the model cannot answer Y/N prompts:
50
+
51
+ ```sh
52
+ # Using the flag
53
+ forge --non-interactive
54
+
55
+ # Using the environment variable
56
+ FORGE_YES=1 forge
57
+ ```
58
+
59
+ Both activate the same bypass. When active, every Y/N gate in `/forge:init` resolves to its documented default:
60
+
61
+ | Gate | Default resolution |
62
+ |------|--------------------|
63
+ | Resume previous init? | No — delete checkpoint, start fresh |
64
+ | Pre-flight phase selector (Phase 1–4 prompt) | Skip prompt — proceed from Phase 1 |
65
+ | Knowledge base folder name | Use default `engineering/` |
66
+ | Create CLAUDE.md? | Yes — create with KB links |
67
+
68
+ ## Hook safety net
69
+
70
+ forge-cli intercepts `store-cli write` and `store-cli update-status` bash calls and validates them before they reach the store. This prevents malformed payloads and illegal status transitions from corrupting the project's engineering knowledge base.
71
+
72
+ ### Default-on enforcement
73
+
74
+ When enforcement is active (default), the hook dispatcher:
75
+
76
+ 1. **Schema validation** — every `store-cli write <entity> '<json>'` invocation is validated against the entity schema. If the payload is invalid, the model receives a structured error via `{ block: true, reason: <error> }` and is expected to self-correct on the next attempt.
77
+
78
+ 2. **Transition guard** — every `store-cli update-status <entity> <id> status <value>` invocation is checked against the legal transition table for the entity. Illegal transitions (e.g. `draft → committed`, skipping required intermediate states) are blocked with an explanatory message naming both `from` and `to` states and the legal next states.
79
+
80
+ Both checks are enforced by default. The hooks fire synchronously before the tool executes, so the model sees the error as the tool result and retries with a corrected payload.
81
+
82
+ ### `--force` scope
83
+
84
+ When `--force` is present in the `store-cli` argv:
85
+
86
+ - **Transition guard** is bypassed — `--force` is an explicit operator override for status transitions.
87
+ - **Schema validation still runs** — a malformed payload is always invalid regardless of intent.
88
+
89
+ ### `FORGE_HOOK_AUDIT=1` — audit-only mode
90
+
91
+ Set `FORGE_HOOK_AUDIT=1` to observe hook decisions without taking action. In audit mode:
92
+
93
+ - Every decision (would-block, would-allow, lookup-failed) is logged to `.forge/logs/hooks.log`.
94
+ - Nothing is blocked — all calls proceed regardless of validation outcome.
95
+ - Useful for calibrating the false-positive rate before enabling enforcement in a new project.
96
+
97
+ Log format (one entry per line):
98
+
99
+ ```
100
+ [store-cli-intercept] subcmd=write entity=task payload={"taskId":"..."}
101
+ [store-cli-intercept] decision=would-block reason=missing required field: taskId
102
+ [store-cli-intercept] decision=would-allow
103
+ [store-cli-intercept] decision=lookup-failed entity=task entityId=FORGE-S18-T03
104
+ ```
105
+
106
+ ### Interpreting block messages
107
+
108
+ When the model emits a malformed `store-cli` call and the hook blocks it, the tool result will contain:
109
+
110
+ ```
111
+ block: true
112
+ reason: <error text>
113
+ ```
114
+
115
+ The model should read the `reason` field and self-correct the payload or transition before retrying. Common block reasons:
116
+
117
+ | Reason pattern | Cause | Fix |
118
+ |---|---|---|
119
+ | `missing required field: <field>` | Schema validation — required field absent | Add the missing field to the payload |
120
+ | `<from> → <to> is not a legal transition...` | Transition guard — illegal status jump | Use the listed legal next states |
121
+ | `store-cli validate exited with code 1` | Schema validation — malformed JSON or unknown entity | Fix the JSON payload |
122
+
123
+ ## Custom tools
124
+
125
+ ### `forge:ask_user` — interactive prompt
126
+
127
+ The `forge_ask_user` custom tool allows Forge workflows to request user input
128
+ during model execution. It presents the appropriate TUI prompt and blocks the
129
+ model loop until the user responds.
130
+
131
+ **Schema:**
132
+
133
+ ```typescript
134
+ {
135
+ question: string; // The prompt shown to the user
136
+ type: "confirm" // Y/N boolean confirmation
137
+ | "choice" // Select from a list
138
+ | "text"; // Free-form single-line input
139
+ options?: string[]; // Required when type === "choice"
140
+ default?: string; // Returned in non-interactive mode
141
+ }
142
+ ```
143
+
144
+ **Returns:** A string — `"Y"` or `"N"` for `confirm`, the selected option for
145
+ `choice`, or the entered text for `text`. On cancellation (user dismisses the
146
+ dialog), the tool returns `isError: true` with a structured message.
147
+
148
+ **Examples:**
149
+
150
+ ```
151
+ // Confirm
152
+ forge_ask_user({ question: "Overwrite existing files?", type: "confirm" })
153
+ // → "Y" or "N"
154
+
155
+ // Choice
156
+ forge_ask_user({
157
+ question: "Select environment:",
158
+ type: "choice",
159
+ options: ["development", "staging", "production"]
160
+ })
161
+ // → "development" | "staging" | "production"
162
+
163
+ // Text
164
+ forge_ask_user({ question: "Enter project name:", type: "text", default: "myproject" })
165
+ // → user-entered string (or "myproject" in non-interactive mode)
166
+ ```
167
+
168
+ **Non-interactive behaviour:** When `FORGE_YES=1`, `--non-interactive` is set,
169
+ or pi is running in headless/RPC mode, the tool returns the `default` immediately
170
+ without rendering any TUI. Fallback defaults when no explicit `default` is
171
+ provided: `confirm` → `"Y"`, `choice` → `options[0]`, `text` → `""`.
172
+
46
173
  ## Roadmap
47
174
 
48
175
  | Command | Status |
49
176
  |---------------------------|---------------------|
50
- | `/forge:init` | Shipped (0.2.0) |
177
+ | `/forge:init` | Shipped (0.3.0) |
51
178
  | Other `/forge:*` commands | Roadmap |
52
179
 
53
180
  Track via [issues](https://github.com/Entelligentsia/forge-cli/issues).
package/dist/bin/argv.js CHANGED
@@ -71,6 +71,11 @@ export function parseForgeArgv(argv) {
71
71
  i++;
72
72
  continue;
73
73
  }
74
+ if (token === "--non-interactive") {
75
+ env.FORGE_NON_INTERACTIVE = "1";
76
+ i++;
77
+ continue;
78
+ }
74
79
  if (token === "--registry") {
75
80
  if (i + 1 >= argv.length) {
76
81
  return { error: "forge: --registry requires a path argument. Run `forge --help` for usage." };
@@ -1 +1 @@
1
- {"version":3,"file":"argv.js","sourceRoot":"","sources":["../../src/bin/argv.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"argv.js","sourceRoot":"","sources":["../../src/bin/argv.ts"],"names":[],"mappings":"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"}
package/dist/bin/forge.js CHANGED
@@ -56,6 +56,7 @@ Forge-owned options:
56
56
  --version Print version triplet and exit
57
57
  --help, -h Print this help message
58
58
  --no-update-check Skip forge update check (sets FORGE_NO_UPDATE_CHECK=1)
59
+ --non-interactive Bypass all Y/N gates with defaults, e.g. for CI (sets FORGE_NON_INTERACTIVE=1)
59
60
  --registry <path> Override model registry path (sets FORGE_MODEL_REGISTRY=path)
60
61
 
61
62
  Pi options (forwarded verbatim):
@@ -1 +1 @@
1
- {"version":3,"file":"forge.js","sourceRoot":"","sources":["../../src/bin/forge.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"forge.js","sourceRoot":"","sources":["../../src/bin/forge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ export declare const AskUserParams: Type.TObject<{
4
+ question: Type.TString;
5
+ type: Type.TUnion<[Type.TLiteral<"confirm">, Type.TLiteral<"choice">, Type.TLiteral<"text">]>;
6
+ options: Type.TOptional<Type.TArray<Type.TString>>;
7
+ default: Type.TOptional<Type.TString>;
8
+ }>;
9
+ /**
10
+ * Register the forge_ask_user tool with the pi ExtensionAPI.
11
+ *
12
+ * The tool is named `forge_ask_user` (snake_case per pi convention); the
13
+ * human/LLM-facing name `forge:ask_user` appears in description and promptSnippet.
14
+ *
15
+ * @param pi The pi ExtensionAPI instance.
16
+ */
17
+ export declare function registerAskUserTool(pi: ExtensionAPI): void;
@@ -0,0 +1,139 @@
1
+ // forge:ask_user custom tool — FORGE-S18-T04
2
+ //
3
+ // Registers forge_ask_user via pi.registerTool. The tool accepts a question and
4
+ // an input type (confirm | choice | text), presents the appropriate TUI prompt
5
+ // via ctx.ui.confirm / ctx.ui.select / ctx.ui.input, and returns the user's
6
+ // answer as a string.
7
+ //
8
+ // Non-interactive bypass:
9
+ // When FORGE_YES=1 or FORGE_NON_INTERACTIVE=1 (set by `forge --non-interactive`),
10
+ // or when ctx.hasUI is false (headless / RPC mode), the tool returns the supplied
11
+ // default without rendering any TUI. Fallback defaults when no explicit default:
12
+ // confirm → "Y"
13
+ // choice → options[0] (or "" if empty)
14
+ // text → ""
15
+ //
16
+ // Cancellation:
17
+ // ctx.ui.* returns undefined when the user cancels. The tool surfaces this as
18
+ // isError: true with a structured message — never silently defaults.
19
+ //
20
+ // Iron Law 6 compliance: no shell-string interpolation. No subprocess spawning.
21
+ import { Type } from "typebox";
22
+ // ── Schema ────────────────────────────────────────────────────────────────────
23
+ export const AskUserParams = Type.Object({
24
+ question: Type.String({
25
+ description: "The question or prompt to display to the user.",
26
+ }),
27
+ type: Type.Union([Type.Literal("confirm"), Type.Literal("choice"), Type.Literal("text")], {
28
+ description: "Input modality: confirm (Y/N boolean), choice (select from list), or text (free-form single-line input).",
29
+ }),
30
+ options: Type.Optional(Type.Array(Type.String(), {
31
+ description: "Required when type === 'choice'. The list of options to present to the user.",
32
+ })),
33
+ default: Type.Optional(Type.String({
34
+ description: "Default value returned in non-interactive mode or when no default is needed. " +
35
+ "If absent, the fallback is: confirm → 'Y', choice → options[0], text → ''.",
36
+ })),
37
+ });
38
+ // ── Non-interactive helper ────────────────────────────────────────────────────
39
+ /**
40
+ * Returns true when running in non-interactive / CI mode.
41
+ *
42
+ * Inlined here (not imported from forge-init.ts) to keep the module boundary
43
+ * clean and avoid any risk of circular imports.
44
+ *
45
+ * Activated by:
46
+ * - `FORGE_YES=1` — ergonomic shell shorthand (FORGE-S18-T01)
47
+ * - `FORGE_NON_INTERACTIVE=1` — set by `forge --non-interactive` flag
48
+ */
49
+ function isNonInteractive() {
50
+ return process.env.FORGE_YES === "1" || process.env.FORGE_NON_INTERACTIVE === "1";
51
+ }
52
+ // ── Result helpers ────────────────────────────────────────────────────────────
53
+ function okResult(text) {
54
+ return {
55
+ content: [{ type: "text", text: text || "" }],
56
+ details: {},
57
+ };
58
+ }
59
+ function errResult(text) {
60
+ return {
61
+ content: [{ type: "text", text }],
62
+ details: {},
63
+ isError: true,
64
+ };
65
+ }
66
+ // ── Fallback computation ──────────────────────────────────────────────────────
67
+ /**
68
+ * Compute the non-interactive fallback value.
69
+ *
70
+ * Priority: explicit `default` field → type-specific hardcoded fallback.
71
+ */
72
+ function computeFallback(params) {
73
+ if (params.default !== undefined)
74
+ return params.default;
75
+ if (params.type === "confirm")
76
+ return "Y";
77
+ if (params.type === "choice")
78
+ return params.options?.[0] ?? "";
79
+ return ""; // text
80
+ }
81
+ // ── Public registration ───────────────────────────────────────────────────────
82
+ /**
83
+ * Register the forge_ask_user tool with the pi ExtensionAPI.
84
+ *
85
+ * The tool is named `forge_ask_user` (snake_case per pi convention); the
86
+ * human/LLM-facing name `forge:ask_user` appears in description and promptSnippet.
87
+ *
88
+ * @param pi The pi ExtensionAPI instance.
89
+ */
90
+ export function registerAskUserTool(pi) {
91
+ pi.registerTool({
92
+ name: "forge_ask_user",
93
+ label: "Forge Ask User",
94
+ description: "forge:ask_user — Present an interactive prompt to the user and return their answer. " +
95
+ "Accepts three input types: 'confirm' (Y/N), 'choice' (select from a list), or 'text' " +
96
+ "(free-form single-line input). Blocks the model loop until the user responds. " +
97
+ "In non-interactive mode (FORGE_YES=1 or --non-interactive), returns the default immediately.",
98
+ promptSnippet: "Use forge_ask_user when a Forge workflow needs synchronous user input — confirm (Y/N), choice from a list, or free-form text.",
99
+ parameters: AskUserParams,
100
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
101
+ // Non-interactive bypass: applies when env flag is set OR when running
102
+ // headless (ctx.hasUI=false, e.g. RPC mode / print mode).
103
+ if (isNonInteractive() || !ctx.hasUI) {
104
+ const fallback = computeFallback(params);
105
+ // Emit a one-line audit entry to stderr (not a file) so CI logs capture it.
106
+ process.stderr.write(`[forge:ask_user] non-interactive fallback — type=${params.type} question="${params.question}" default="${fallback}"\n`);
107
+ return okResult(fallback);
108
+ }
109
+ const opts = signal !== undefined ? { signal } : {};
110
+ if (params.type === "confirm") {
111
+ // ctx.ui.confirm returns true/false or undefined (cancel).
112
+ const answer = await ctx.ui.confirm("forge:ask_user", params.question, opts);
113
+ if (answer === undefined) {
114
+ return errResult(`forge:ask_user cancelled — user dismissed the prompt. question: "${params.question}"`);
115
+ }
116
+ return okResult(answer ? "Y" : "N");
117
+ }
118
+ if (params.type === "choice") {
119
+ if (!params.options || params.options.length === 0) {
120
+ return errResult("forge:ask_user error — type 'choice' requires a non-empty 'options' array.");
121
+ }
122
+ // ctx.ui.select returns the selected string or undefined (cancel).
123
+ const answer = await ctx.ui.select("forge:ask_user", params.options, opts);
124
+ if (answer === undefined) {
125
+ return errResult(`forge:ask_user cancelled — user dismissed the prompt. question: "${params.question}"`);
126
+ }
127
+ return okResult(answer);
128
+ }
129
+ // type === "text"
130
+ // ctx.ui.input returns the entered string or undefined (cancel/ESC).
131
+ const answer = await ctx.ui.input("forge:ask_user", params.question, opts);
132
+ if (answer === undefined) {
133
+ return errResult(`forge:ask_user cancelled — user dismissed the prompt. question: "${params.question}"`);
134
+ }
135
+ return okResult(answer);
136
+ },
137
+ });
138
+ }
139
+ //# sourceMappingURL=ask-user-tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ask-user-tool.js","sourceRoot":"","sources":["../../../src/extensions/forgecli/ask-user-tool.ts"],"names":[],"mappings":"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"}
@@ -90,6 +90,20 @@ function getBundledForgeVersion() {
90
90
  // ── Session-scoped banner state ────────────────────────────────────────────
91
91
  // Prevents re-rendering the hero banner on resume within the same session.
92
92
  let heroBannerShown = false;
93
+ // ── Non-interactive mode ───────────────────────────────────────────────────
94
+ /**
95
+ * Returns true when running in non-interactive / CI mode.
96
+ *
97
+ * Activated by either:
98
+ * - `FORGE_YES=1` — ergonomic shell shorthand (FORGE-S18-T01)
99
+ * - `FORGE_NON_INTERACTIVE=1` — set by `forge --non-interactive` flag
100
+ *
101
+ * When active, every Y/N gate resolves to its documented default without
102
+ * emitting a model-text prompt.
103
+ */
104
+ function isNonInteractive() {
105
+ return process.env.FORGE_YES === "1" || process.env.FORGE_NON_INTERACTIVE === "1";
106
+ }
93
107
  function parseInitFlags(args) {
94
108
  const parts = args.trim().split(/\s+/).filter(Boolean);
95
109
  const hasFast = parts.includes("--fast");
@@ -255,8 +269,10 @@ async function linkAgentInstructionFile(cwd, kbPath, projectName, ctx) {
255
269
  // Already exists — do NOT modify (per spec step 4-11: avoid KB-link bloat)
256
270
  return;
257
271
  }
258
- // None exist — prompt to create minimal CLAUDE.md
259
- const ok = await ctx.ui.confirm("Create CLAUDE.md?", `No agent instruction file found at project root.\nCreate a minimal CLAUDE.md with links to the Forge knowledge base? [Y/n]`);
272
+ // None exist — prompt to create minimal CLAUDE.md (G4: bypassed in non-interactive mode)
273
+ const ok = isNonInteractive()
274
+ ? true
275
+ : await ctx.ui.confirm("Create CLAUDE.md?", `No agent instruction file found at project root.\nCreate a minimal CLAUDE.md with links to the Forge knowledge base? [Y/n]`);
260
276
  if (!ok) {
261
277
  ctx.ui.notify("〇 KB not linked — run /forge:refresh-kb-links after creating CLAUDE.md.", "info");
262
278
  return;
@@ -343,7 +359,10 @@ export function registerForgeInit(pi) {
343
359
  const nextPhase = Math.min(lastPhase + 1, 4);
344
360
  const resumeBanner = `〇 Previous init detected — last completed phase: ${lastPhase} of 4\n` +
345
361
  `Resume from Phase ${nextPhase}?`;
346
- const shouldResume = await ctx.ui.confirm("Resume /forge:init?", resumeBanner);
362
+ // G1: in non-interactive mode, default to not resuming (start fresh)
363
+ const shouldResume = isNonInteractive()
364
+ ? false
365
+ : await ctx.ui.confirm("Resume /forge:init?", resumeBanner);
347
366
  if (shouldResume) {
348
367
  startPhase = nextPhase;
349
368
  // Skip hero banner on resume (session-scoped gate)
@@ -386,7 +405,7 @@ export function registerForgeInit(pi) {
386
405
  // ── 5. Pre-flight plan (unless jumping to a specific phase) ───────
387
406
  const projectName = discoverProjectName(cwd);
388
407
  if (flags.startPhase === null || flags.invalidPhase) {
389
- const preflightText = `## Forge Init — ${projectName}\n\n` +
408
+ const preflightSummary = `Forge Init — ${projectName}\n\n` +
390
409
  `4 phases will run in this session (~45 seconds non-interactive):\n\n` +
391
410
  ` 1 Collect — 5 parallel discovery scans → config.json\n` +
392
411
  ` KB folder prompt (interactive)\n` +
@@ -394,10 +413,15 @@ export function registerForgeInit(pi) {
394
413
  ` 3 Materialize — substitute-placeholders.cjs → fully functional workflows\n` +
395
414
  ` 4 Register — versioning, manifest, cache, store entries, Tomoshibi\n\n` +
396
415
  `Phase 1 is interactive (KB folder name prompt). Phases 2–4 are non-interactive\n` +
397
- `and complete in under 45 seconds.\n\n` +
398
- `Start from Phase 1? [Y] or specify phase (1–4): ___`;
399
- sendToAgent(preflightText);
400
- await ctx.waitForIdle();
416
+ `and complete in under 45 seconds.`;
417
+ // G2: skip pre-flight confirm in non-interactive mode (proceed directly to Phase 1)
418
+ if (!isNonInteractive()) {
419
+ const proceed = await ctx.ui.confirm("Start /forge:init?", preflightSummary);
420
+ if (!proceed) {
421
+ ctx.ui.notify("〇 /forge:init cancelled.", "info");
422
+ return;
423
+ }
424
+ }
401
425
  }
402
426
  // ── Phase 1 — Collect ─────────────────────────────────────────────
403
427
  if (startPhase <= 1) {
@@ -417,14 +441,22 @@ export function registerForgeInit(pi) {
417
441
  const phase1Prompt = buildPhase1PromptText(bundleRoot, projectName);
418
442
  sendToAgent(phase1Prompt);
419
443
  await ctx.waitForIdle();
420
- // KB folder prompt (spec §7, F2)
421
- const kbPromptText = `━━━ Knowledge Base Folder ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` +
422
- `Forge will create a folder for architecture docs, sprints, bugs, and features.\n` +
423
- `Default name: engineering/\n\n` +
424
- `Does "engineering" conflict with an existing folder in this project? [n/Y]\n` +
425
- `If yes, enter your preferred name (e.g. ai-docs, .forge-kb, docs/ai): ___`;
426
- sendToAgent(kbPromptText);
427
- await ctx.waitForIdle();
444
+ // KB folder prompt (spec §7, F2) — G3: skipped in non-interactive mode (default: "engineering")
445
+ if (!isNonInteractive()) {
446
+ const kbDescription = `Forge will create a folder for architecture docs, sprints, bugs, and features.\n` +
447
+ `Default name: engineering/\n\n` +
448
+ `Does "engineering" conflict with an existing folder in this project?`;
449
+ const hasConflict = await ctx.ui.confirm("Engineering folder name?", kbDescription);
450
+ if (hasConflict) {
451
+ const customName = await ctx.ui.input("Engineering folder name?", "Enter preferred folder name (e.g. ai-docs, .forge-kb, docs/ai): ");
452
+ if (customName && customName.trim()) {
453
+ const manageConfigToolEarly = path.join(toolsRoot, "manage-config.cjs");
454
+ if (fs.existsSync(manageConfigToolEarly)) {
455
+ await runToolAdvisory(manageConfigToolEarly, ["set", "paths.engineering", customName.trim()], cwd, ctx, "manage-config paths.engineering");
456
+ }
457
+ }
458
+ }
459
+ }
428
460
  // Marketplace skills advisory (sub-decision #1)
429
461
  ctx.ui.notify("〇 Marketplace skills auto-recommendation is Claude-Code-only. " +
430
462
  "Pi users install extensions manually. Writing installedSkills: []", "info");