From 1446ad5bb2a2f720d58bb63a649211e343e64c1b Mon Sep 17 00:00:00 2001
From: jaymar921
Date: Fri, 14 Aug 2026 09:54:29 +0800
Subject: [PATCH 1/3] fix(0.6.1): stop the agent guessing at the machine, and
at what a step did
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A macOS run of the React TODO spec on 0.6.0, ornith:9b, step sessions on. Eight
steps, four refused, stopped at item one of six reporting "nothing was written" —
for a step whose two commands had both exited 0 with the project on disk. Four
faults, none of them the model being small.
The agent is told what machine it is on. Three of the four refusals were the model
reaching for shell syntax it had no way to know was unavailable: `mkdir -p`, then
`cd x && npm install`. It was guessing at the platform and at the execution model,
and the extension knew the first before the first token was generated.
core/environmentProfile detects the OS, release, arch, and Node version and states
them in the system prompt, along with the sentence that matters more — there is no
shell on any platform, cwd is how you run elsewhere, and create_folder/list_files/
read_file/write_file replace the shell utilities. Tool-using routes only; a Plan
prompt gets the facts without the tool names it structurally does not have.
.hirayacoder/ is added to the workspace .gitignore at startup. It holds transcripts,
memory, and the audit log, and has never been ignored anywhere. An existing file is
appended to, never rewritten, and left alone if any line already covers it —
including a !.hirayacoder negation.
npm create/init/exec is no longer refused for the absence of the project it creates.
NO_PACKAGE_JSON is right about `npm install`, which climbs out of the workspace, and
exactly wrong about the scaffolders, where the missing manifest is the point. This
was the first failure of the run and everything after it was the model trying to
satisfy a precondition that was never real.
`cd folder && command` is rewritten into the two arguments run_script already takes.
0.6.0 added cwd, documented it, and made the refusal name it; the model sent the
chained form anyway, was told, and sent it again. Exactly one shape is accepted and
nothing is relaxed — the rewrite goes through pre-flight, the gate, the allow-list,
and pathGuard, and the model is told what actually ran.
A step whose work was a command is no longer scored as having done nothing.
ChangeSet.recordCommand appends to commands, ChangeSet.since reads files, and the
two never met, so a scaffold step could not pass. Commands are now revision-stamped,
commandsSince is the counterpart to since, and stepGuard accepts a command that
succeeded as evidence — still judged from what the extension recorded, never from
the model's account.
---
CHANGELOG.md | 108 +++++++++++++
app/agent/agentSession.js | 38 ++++-
app/agent/scriptPreflight.js | 46 ++++++
app/agent/stepGuard.js | 46 ++++++
app/agent/tools/runScript.js | 110 ++++++++++++-
app/core/environmentProfile.js | 212 +++++++++++++++++++++++++
app/core/promptRouter.js | 42 ++++-
app/core/workspaceBootstrap.js | 139 ++++++++++++++++
app/extension.js | 16 ++
app/features/chatTab.js | 3 +
doc/FEATURES.md | 10 ++
package-lock.json | 4 +-
package.json | 2 +-
setup/prompts/agentic-system-prompt.md | 3 +
setup/prompts/lite-1b-system-prompt.md | 3 +
test/unit/environmentProfile.test.js | 103 ++++++++++++
test/unit/promptRouter.test.js | 74 +++++++++
test/unit/runScriptTool.test.js | 85 ++++++++++
test/unit/scriptPreflight.test.js | 25 +++
test/unit/stepGuard.test.js | 36 +++++
test/unit/stepSessions.test.js | 47 ++++++
test/unit/workspaceBootstrap.test.js | 119 ++++++++++++++
22 files changed, 1257 insertions(+), 14 deletions(-)
create mode 100644 app/core/environmentProfile.js
create mode 100644 app/core/workspaceBootstrap.js
create mode 100644 test/unit/environmentProfile.test.js
create mode 100644 test/unit/workspaceBootstrap.test.js
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 02319ed..5e9ba66 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,114 @@ All notable changes to HirayaCoder are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.6.1] — unreleased
+
+A macOS run of the same React TODO spec on the 0.6.0 build, `ornith:9b`, high thinking
+capacity, step sessions on. Eight steps, four of them refused, and the run stopped at
+item one of six reporting *"nothing was written"* — for a step whose two commands had
+both exited 0 with the scaffolded project sitting on disk. Four separate faults stacked
+up to produce that, and none of them were the model being small.
+
+### Added — the agent is told what machine it is on
+
+Three of the four refusals were the model reaching for shell syntax it had no way to know
+was unavailable: `mkdir -p todo-glass-app && cd todo-glass-app && npm create vite@latest .`,
+then `mkdir -p todo-glass-app`, then `cd todo-glass-app && npm install`. `mkdir -p` is
+POSIX; the same model on Windows proposes `md`. Both are wrong here for the same reason —
+there is no shell, and `mkdir` is not on the allow-list — but nothing in the prompt said
+which OS this was, and nothing said the shell was absent either. The model was guessing at
+the platform *and* at the execution model, and a guess wrong on both is refused three
+times before the step dies.
+
+`core/environmentProfile` detects the machine — OS, release, architecture, Node version —
+and states it in the system prompt as fact, alongside the sentence that matters more:
+there is no shell on any platform, `cwd` is how you run somewhere else, and `create_folder`
+/ `list_files` / `read_file` / `write_file` are what replace the shell utilities. Detected
+per session from `os.platform()`, never read back from disk, so a workspace synced between
+two machines never reports the other one's OS.
+
+- Carried on the tool-using routes only. Every line of it is about performing an action,
+ and Ask mode has no tools; a Plan prompt gets the platform facts without the lines
+ naming tools Plan mode structurally does not have.
+- Positioned by a `{environment}` placeholder in both prompt files, and appended when the
+ placeholder is absent — a customised prompt file cannot leave the model guessing at the
+ platform by not knowing about it.
+- Written to `.hirayacoder/environment.json` for the user to read. Nothing in the prompt
+ path reads it back; it is there so a session's records say which machine produced them.
+ Every benchmark folder in this repo had its OS reconstructed from the shape of the
+ commands the model tried.
+
+### Added — `.hirayacoder/` is ignored by git from the first session
+
+`.hirayacoder/` holds the audit log, the outcome ledger, full session transcripts, and
+per-session memory. It has never been added to a workspace's `.gitignore`, and in every
+benchmark workspace it sits untracked beside a scaffolded project whose `.gitignore`
+lists `node_modules` and nothing else. A user who stages everything commits their whole
+conversation history.
+
+`core/workspaceBootstrap` now runs at activation: an existing `.gitignore` is **appended
+to**, never rewritten; nothing is appended if any line already covers `.hirayacoder` in
+any form, including a `!.hirayacoder` negation, which is a decision to respect rather than
+overrule; a missing one is created holding that single entry and nothing else, since
+guessing at `node_modules` and `dist` for a project whose language nobody has looked at is
+how a helpful default becomes a wrong one. Idempotent, and every failure is logged and
+swallowed — a session must still start on a read-only checkout.
+
+### Fixed — a scaffolder was refused for the absence of the project it creates
+
+`npm create vite@latest todo-glass-app -- --template react` was refused with
+`NO_PACKAGE_JSON`: *"there is no package.json in the workspace root or any folder above
+it."* That guard is right about `npm install`, which climbs out of the workspace and
+installs into whatever manifest it finds above — that is how a dependency once landed in
+this extension's own `package.json`. It is exactly wrong about `npm create`, `npm init`,
+and `npm exec`, where the missing manifest is the point.
+
+This was the first failure of the run and it cascaded: step one was "scaffold the
+project", the only command that does it was refused, and the model spent its remaining
+attempts on `mkdir -p` and `cd … &&` trying to satisfy a precondition that was never
+real. `npm create` / `init` / `exec` / `x`, `yarn create` / `init` / `dlx`, and `pnpm
+create` / `init` / `dlx` are now exempt. Everything that acts on an existing project is
+guarded exactly as before.
+
+### Fixed — `cd folder && command` is now taken, not refused for a fourth time
+
+0.6.0 added `cwd`, documented it in both system prompts, and made the refusal name it
+outright: *"If you were chaining `cd folder && …`, drop the `cd` and pass the folder as
+'cwd' instead."* The model sent `cd todo-glass-app && npm install`, was told exactly
+that, and sent the identical line again. Two of the eight steps in the run went on it.
+
+That is the third wording of the same instruction, and a small model reaching for `cd x
+&& y` is not failing to understand the rule — it is producing the only phrasing it has
+for "run this over there". The folder and the command are both in the string,
+unambiguously, and the extension was throwing them away to ask for them back. `run_script`
+now rewrites that one shape into the two arguments it already takes.
+
+Nothing is relaxed. Exactly one shape is accepted — a single leading `cd` into one
+relative folder, one `&&`, and a remainder with no operator left in it — and a second
+`&&`, a pipe, a redirect, `cd ..`, an absolute path, or a quoted path is left untouched
+and refused as before. The rewritten command goes through pre-flight, the permission
+gate, the allow-list, and the tokenizer; the folder goes through `pathGuard` like every
+other `cwd`; the confirmation prompt and the audit log show what will actually run. The
+model is told about the rewrite in the observation every time, against its own input,
+which teaches better than the sentence in the system prompt did.
+
+### Fixed — a step that ran a command was scored as having done nothing
+
+The step that ended the run had already succeeded. `npx create-vite` exit 0, `npm
+install` exit 0, twenty seconds of it, eleven files and a `node_modules` on disk. The
+guard failed it anyway, stopped the run, and skipped the five remaining items, because a
+scaffolder's output never passes through `write_file`: `ChangeSet.recordCommand` appends
+to `commands`, `ChangeSet.since` reads `files`, and the two never met. A step whose entire
+job is "scaffold the project" could not pass.
+
+`ChangeSet` now stamps commands with the same revision counter it stamps file changes
+with, `commandsSince` is its counterpart to `since`, and `stepGuard.verify` accepts a
+command that succeeded as evidence the step did work — reported as such, so a later step
+knows the agent wrote nothing itself before it assumes a file is there. The principle is
+unchanged: judged from evidence the extension holds, never from the model's account. A
+process it spawned and whose exit code it read is exactly that; it was being read from
+the wrong half of the change set.
+
## [0.6.0] — unreleased
Machine B, a full model round on the 0.5.3 build: every model was given the same
diff --git a/app/agent/agentSession.js b/app/agent/agentSession.js
index 41dafd7..52e7a49 100644
--- a/app/agent/agentSession.js
+++ b/app/agent/agentSession.js
@@ -31,6 +31,7 @@ const promptRouter = require('../core/promptRouter');
const intentRouter = require('../core/intentRouter');
const toolRegistry = require('./toolRegistry');
const contextBuilder = require('../core/contextBuilder');
+const environmentProfile = require('../core/environmentProfile');
const projectOverview = require('../core/projectOverview');
const reactLoop = require('./reactLoop');
const nativeToolLoop = require('./nativeToolLoop');
@@ -70,7 +71,7 @@ class ChangeSet {
constructor() {
/** @type {Map} */
this.files = new Map();
- /** @type {Array<{command: string, exitCode: number | null, ok: boolean}>} */
+ /** @type {Array<{command: string, exitCode: number | null, ok: boolean, revision?: number}>} */
this.commands = [];
/**
* How many records this set has taken, ever.
@@ -118,7 +119,23 @@ class ChangeSet {
*/
recordCommand(entry) {
this.revision += 1;
- this.commands.push(entry);
+ // Stamped like a file change, and for the same question: `stepGuard` asks what *this
+ // step* did, and until 0.6.1 the commands half of the set could not answer it.
+ this.commands.push({ ...entry, revision: this.revision });
+ }
+
+ /**
+ * The commands run since a given revision.
+ *
+ * The counterpart to `since`, which reads the files half only. A step that scaffolds a
+ * project writes no files through the agent at all, so this is the only record that it
+ * did anything — see `stepGuard.ranSomething`.
+ *
+ * @param {number} revision
+ * @returns {Array<{command: string, ok: boolean}>}
+ */
+ commandsSince(revision) {
+ return this.commands.filter((entry) => (entry.revision || 0) > revision);
}
/** @returns {FileChange[]} */
@@ -459,6 +476,18 @@ class AgentSession {
*/
this.stepSessions = options.stepSessions === true;
+ /**
+ * The machine this session is running on, detected once.
+ *
+ * Held on the session rather than detected inside `promptRouter` per call: a
+ * six-item TODO run in step mode routes once per step, and the answer cannot change
+ * between them. Injected in tests to assert the Windows and POSIX prompts without
+ * needing the matching machine.
+ *
+ * @type {import('../core/environmentProfile').EnvironmentProfile}
+ */
+ this.environment = options.environment || environmentProfile.detect();
+
/**
* Earlier turns of this chat, set per `run` call.
*
@@ -519,6 +548,7 @@ class AgentSession {
thinkingCapacity: this.thinkingCapacity,
memory: await this._renderMemory(),
earnedHints: await this._earnedHints(mode),
+ environment: this.environment,
intent: intent.intent,
// "Read the README" should not be able to reach `run_script`, whatever the
// planner decides in between. See `intentRouter.isReadOnlyRequest`.
@@ -857,6 +887,9 @@ class AgentSession {
item: item.text,
stopReason: outcome.stopReason,
changed: changeSet.since(itemRevisionBefore),
+ // A scaffold step's whole output is a command's side effects, which never pass
+ // through `write_file` and so never appear above.
+ commands: changeSet.commandsSince(itemRevisionBefore),
steps: outcome.steps,
});
@@ -969,6 +1002,7 @@ class AgentSession {
thinkingCapacity: this.thinkingCapacity,
memory: await this._renderMemory({ about }),
earnedHints: await this._earnedHints(activeRoute.mode),
+ environment: this.environment,
intent: 'task',
});
} catch (err) {
diff --git a/app/agent/scriptPreflight.js b/app/agent/scriptPreflight.js
index 4de2df5..a33e1de 100644
--- a/app/agent/scriptPreflight.js
+++ b/app/agent/scriptPreflight.js
@@ -35,6 +35,46 @@ const NODE_RUNNERS = new Set(['npm', 'yarn', 'pnpm']);
*/
const IMPLICIT_SCRIPTS = new Set(['test', 'start', 'install', 'stop', 'restart']);
+/**
+ * Subcommands that exist to *create* a project rather than to act on one.
+ *
+ * The manifest check below exists because `npm install` with no `package.json` climbs
+ * out of the workspace and installs into whatever it finds above. These do not: `npm
+ * create vite@latest todo-glass-app` downloads `create-vite` and runs it against a
+ * folder it makes itself, `npm init` writes a manifest into the current directory, and
+ * `npm exec` / `npm x` are `npx` under another name — which was never guarded at all,
+ * since `npx` is not a package manager by `packageManagerOf`.
+ *
+ * Refusing them was the first failure of the macOS 0.6.0 benchmark and it cascaded
+ * through the whole run: step one was "scaffold the project", the only command that
+ * does it was refused for the absence of the very thing it was about to produce, and
+ * the model spent its remaining attempts on `mkdir -p` and `cd … &&` trying to satisfy
+ * a precondition that was never real.
+ *
+ * The rule this keeps: refuse only when being wrong is impossible. "npm cannot install
+ * into a project that does not exist" is certain. "npm cannot create a project where
+ * there is no project" is the opposite of certain — it is the one case where the
+ * missing manifest is the point.
+ */
+const PROJECT_CREATING_SUBCOMMANDS = new Map([
+ ['npm', new Set(['create', 'init', 'exec', 'x'])],
+ ['yarn', new Set(['create', 'init', 'dlx'])],
+ ['pnpm', new Set(['create', 'init', 'dlx'])],
+]);
+
+/**
+ * Is this command about to create a project rather than act on an existing one?
+ *
+ * @param {string} runner
+ * @param {string} command
+ * @returns {boolean}
+ */
+function createsProject(runner, command) {
+ const subcommand = (String(command || '').trim().split(/\s+/)[1] || '').toLowerCase();
+ const known = PROJECT_CREATING_SUBCOMMANDS.get(runner);
+ return Boolean(known && known.has(subcommand));
+}
+
/**
* @typedef {object} PreflightRefusal
* @property {string} code Machine-readable, matching the runner's refusal codes.
@@ -146,6 +186,10 @@ function preflight({ command, cwd, workspaceRoot }) {
const runner = packageManagerOf(command);
if (!runner || !workspaceRoot) return null;
+ // A scaffolder makes the project this module would otherwise insist already exists.
+ // Nothing below has anything useful to say about one, so it is left to run.
+ if (createsProject(runner, command)) return null;
+
/** @type {string} */
let directory;
try {
@@ -219,6 +263,8 @@ module.exports = {
parseRun,
packageManagerOf,
manifestDirectoryWithin,
+ createsProject,
NODE_RUNNERS,
IMPLICIT_SCRIPTS,
+ PROJECT_CREATING_SUBCOMMANDS,
};
diff --git a/app/agent/stepGuard.js b/app/agent/stepGuard.js
index 5101cdf..761fe82 100644
--- a/app/agent/stepGuard.js
+++ b/app/agent/stepGuard.js
@@ -46,8 +46,41 @@ const { namedFiles } = require('./stepBrief');
* @property {Array<{kind: string, path: string}>} changed
* Files this step created, edited, or deleted — this step's, not the session's.
* @property {import('./agentSession').AgentStep[]} steps
+ * @property {Array<{command: string, ok: boolean}>} [commands]
+ * Commands this step ran. See `ranSomething` for why a step that only ran commands is
+ * not a step that did nothing.
*/
+/**
+ * Did this step do its work through a command rather than through `write_file`?
+ *
+ * ## The step this was missing
+ *
+ * A scaffold step writes no files *through the agent*. `npx create-vite todo-glass-app`
+ * writes eleven of them and `npm install` writes several thousand, and the extension
+ * sees none of it: `ChangeSet.recordCommand` appends to `commands`, `ChangeSet.since`
+ * reads `files`, and the two never meet. So a step whose entire job is "scaffold the
+ * project" arrived here with an empty `changed` array and was judged to have produced
+ * "no work at all".
+ *
+ * That is what ended the macOS 0.6.0 benchmark run. Both commands the step needed had
+ * already succeeded — `npx create-vite` exit 0, `npm install` exit 0, twenty seconds of
+ * it, the project sitting on disk — and the run stopped with *"nothing was written"*
+ * and skipped the five remaining items. The user's report of the session was a wall of
+ * text about a scaffold that had in fact worked.
+ *
+ * The guard's principle is unchanged: judge from evidence the extension holds itself,
+ * never from the model's account. A process the extension spawned, whose exit code it
+ * read, is exactly that kind of evidence — it was simply being read from the wrong half
+ * of the change set.
+ *
+ * @param {StepEvidence} evidence
+ * @returns {boolean}
+ */
+function ranSomething(evidence) {
+ return (evidence.commands || []).some((entry) => entry && entry.ok);
+}
+
/**
* @typedef {object} Verdict
* @property {boolean} ok
@@ -115,6 +148,19 @@ function verify(evidence) {
const failedSteps = (evidence.steps || []).filter((step) => step.result && step.result.ok === false);
if (changed.length === 0) {
+ // A command that succeeded is work, and a scaffolder's output never passes through
+ // `write_file`. Reported rather than passed silently: the step is not failed, but
+ // "the agent wrote nothing itself" is the one thing a later step needs to know
+ // before it assumes a file is there.
+ if (ranSomething(evidence)) {
+ const ran = evidence.commands.filter((entry) => entry.ok).map((entry) => `\`${entry.command}\``);
+ return {
+ ok: true,
+ reason: 'ran',
+ detail: `no files were written directly, but ${ran.join(' and ')} ran and succeeded`,
+ };
+ }
+
// The distinction the user sees: a step that tried and was refused reads differently
// from one that never attempted anything, and only the first is worth naming a cause
// for.
diff --git a/app/agent/tools/runScript.js b/app/agent/tools/runScript.js
index c97e31f..ac2bbb0 100644
--- a/app/agent/tools/runScript.js
+++ b/app/agent/tools/runScript.js
@@ -207,6 +207,71 @@ function nextStepAfterRefusal(code, command) {
}
}
+/**
+ * Rewrite `cd folder && command` into the `cwd` this tool already takes.
+ *
+ * ## Why this is a rewrite and not another refusal message
+ *
+ * 0.6.0 added `cwd`, documented it in both system prompts, and made the refusal name it
+ * explicitly: *"If you were chaining `cd folder && …`, drop the `cd` and pass the folder
+ * as 'cwd' instead."* On the macOS benchmark run the model sent `cd todo-glass-app &&
+ * npm install` anyway, was told that, and sent `cd todo-glass-app && npm install` again.
+ * Two of the eight steps in the run went on it, and the step that was scaffolding the
+ * project died there.
+ *
+ * That is the third iteration of the same instruction, and the conclusion is that a
+ * small model reaching for `cd x && y` is not failing to understand the rule — it is
+ * producing the only phrasing it has for "run this over there", under a decoding
+ * distribution that a sentence in the system prompt does not move. The information the
+ * refusal wanted is *already in the command*: the folder and the command are both right
+ * there, unambiguously, and the extension is throwing them away to ask for them again.
+ *
+ * ## What this deliberately does not do
+ *
+ * It is not a shell, and it is not the beginning of one. Exactly one shape is accepted —
+ * a single leading `cd` into one relative folder, joined by one `&&`, followed by a
+ * remainder with no operator of any kind left in it. Anything else (a second `&&`, a
+ * pipe, a redirect, `cd ..`, an absolute path, a quoted path with a space) is left
+ * untouched and refused downstream exactly as before.
+ *
+ * Nothing here relaxes a guard. The rewritten command goes through `preflight`, the
+ * permission gate, the binary allow-list, and `tokenize` — which would still refuse it
+ * if any operator survived — and the folder goes through `pathGuard` like every other
+ * `cwd`. The user's confirmation prompt shows the rewritten form, which is what will
+ * actually run.
+ *
+ * @param {string} command
+ * @param {string} cwd The `cwd` already supplied, if any.
+ * @returns {{command: string, cwd: string} | null} null when the shape does not match.
+ */
+function unchainCd(command, cwd) {
+ // One `cd`, one relative folder, one `&&`, and a remainder. The folder is restricted
+ // to the characters a project directory actually uses, which is also what keeps a
+ // quoted path, a variable, or a second command out of it. A leading `./` is the one
+ // decoration allowed, because models write it constantly.
+ const match = /^\s*cd\s+(?:\.\/)?([A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*)\/?\s*&&\s*(.+)$/.exec(String(command || ''));
+ if (!match) return null;
+
+ const folder = match[1];
+ const rest = match[2].trim();
+
+ // `cd ..`, `cd .`, and anything climbing through a parent is refused rather than
+ // rewritten: `pathGuard` would reject the resulting `cwd` anyway, and a refusal that
+ // names the real problem beats a rewrite that produces one.
+ if (!rest || folder.split('/').some((segment) => segment === '.' || segment === '..')) return null;
+ // A remainder that still contains an operator was never the simple case. Chaining two
+ // commands after one `cd` is a genuine ambiguity — which of them the user is approving
+ // is not answerable — so it keeps the refusal it has always had.
+ if (/[;&|<>`$(){}\n\r]/.test(rest)) return null;
+
+ return {
+ command: rest,
+ // A `cwd` the model also supplied is the folder the `cd` is relative to, which is
+ // how `cd` behaves and how the two would compose in a real shell.
+ cwd: cwd ? `${cwd.replace(/\/+$/, '')}/${folder}` : folder,
+ };
+}
+
/**
* Turn a completed run into an observation.
*
@@ -287,12 +352,35 @@ function describeServerProbe(command, result, budget, cwd) {
* @returns {Promise}
*/
module.exports = async function runScript(args, context) {
- const command = String(args.command || '').trim();
+ let command = String(args.command || '').trim();
if (!command) {
return { ok: false, observation: 'run_script needs a "command" to run.' };
}
- const cwd = String(args.cwd || '').trim();
+ let cwd = String(args.cwd || '').trim();
+
+ // `cd folder && command` says the same thing this tool's two arguments say. Taken
+ // rather than refused — see `unchainCd` for why a fourth wording of the refusal was
+ // not the answer.
+ const unchained = unchainCd(command, cwd);
+ /**
+ * Told to the model on whatever the outcome turns out to be.
+ *
+ * Stated every time rather than silently absorbed: the command that ran is not the
+ * command the model sent, and a tool that quietly substitutes one for the other makes
+ * a failure impossible to read. It doubles as the correction — the shape the model
+ * should have sent, shown against its own input, which teaches better than the same
+ * sentence sitting in the system prompt did.
+ */
+ let rewriteNote = '';
+ if (unchained) {
+ const original = command;
+ ({ command, cwd } = unchained);
+ logger.info(`run_script rewrote "${original}" as "${command}" in "${cwd}".`);
+ rewriteNote =
+ `\n(You sent \`${original}\`. There is no shell, so \`cd\` and \`&&\` cannot work — ` +
+ `it was run as {"command": "${command}", "cwd": "${cwd}"}. Send that shape yourself next time.)`;
+ }
// Before the user is asked anything: a command that provably cannot work is answered
// from the filesystem instead of costing a click, a subprocess, and a page of npm
@@ -302,7 +390,7 @@ module.exports = async function runScript(args, context) {
logger.info(`run_script pre-flight refused "${command}": ${blocked.code}`);
return {
ok: false,
- observation: `\`${command}\` was not run: ${blocked.reason}`,
+ observation: `\`${command}\` was not run: ${blocked.reason}${rewriteNote}`,
error: blocked.code,
detail: { command, cwd: cwd || undefined, reason: blocked.code, attempts: 0 },
};
@@ -322,7 +410,9 @@ module.exports = async function runScript(args, context) {
if (!decision.allowed) {
return {
ok: false,
- observation: `\`${command}\` was not run: ${decision.reason}${nextStepAfterRefusal(decision.code, command)}`,
+ observation:
+ `\`${command}\` was not run: ${decision.reason}` +
+ `${nextStepAfterRefusal(decision.code, command)}${rewriteNote}`,
error: decision.code,
};
}
@@ -352,7 +442,9 @@ module.exports = async function runScript(args, context) {
logger.warn(`run_script could not start "${command}": ${code || 'unknown'}`);
return {
ok: false,
- observation: `\`${command}\` could not be started: ${/** @type {Error} */ (err).message}${nextStepAfterRefusal(code, command)}`,
+ observation:
+ `\`${command}\` could not be started: ${/** @type {Error} */ (err).message}` +
+ `${nextStepAfterRefusal(code, command)}${rewriteNote}`,
error: code,
};
}
@@ -392,9 +484,10 @@ module.exports = async function runScript(args, context) {
// just how often — "this model hits MISSING_DEPENDENCY eleven times a week" is a
// fixable observation, "37 failed steps" is not.
error: startedCleanly || result.ok || !diagnosis ? undefined : diagnosis.reason,
- observation: startedCleanly
- ? describeServerProbe(command, result, budget, cwd)
- : describeRun(command, result, budget, cwd, { diagnosis, attempts }),
+ observation:
+ (startedCleanly
+ ? describeServerProbe(command, result, budget, cwd)
+ : describeRun(command, result, budget, cwd, { diagnosis, attempts })) + rewriteNote,
detail: {
command,
cwd: cwd || undefined,
@@ -417,4 +510,5 @@ module.exports.looksLikeStartupFailure = looksLikeStartupFailure;
module.exports.PROBE_MS = PROBE_MS;
module.exports.nextStepAfterRefusal = nextStepAfterRefusal;
module.exports.toolForRefusedCommand = toolForRefusedCommand;
+module.exports.unchainCd = unchainCd;
module.exports.TOOL_INSTEAD_OF = TOOL_INSTEAD_OF;
diff --git a/app/core/environmentProfile.js b/app/core/environmentProfile.js
new file mode 100644
index 0000000..7010c51
--- /dev/null
+++ b/app/core/environmentProfile.js
@@ -0,0 +1,212 @@
+'use strict';
+
+/* eslint-disable security/detect-non-literal-fs-filename --
+ * Every fs call here targets a path built from the VS Code workspace root and a fixed
+ * filename. No component derives from model output. */
+
+/**
+ * What machine this is, told to the model instead of left for it to guess.
+ *
+ * ## The failure this exists for
+ *
+ * A live macOS run of the React TODO benchmark on 0.6.0, `ornith:9b`, eight steps, four
+ * of them refused. Three of the four were the model reaching for shell syntax it had no
+ * way to know was unavailable:
+ *
+ * mkdir -p todo-glass-app && cd todo-glass-app && npm create vite@latest .
+ * mkdir -p todo-glass-app
+ * cd todo-glass-app && npm install
+ *
+ * `mkdir -p` is POSIX; the same model on Windows proposes `mkdir` with no flag, or `md`.
+ * Both are wrong here for the same reason — there is no shell and `mkdir` is not on the
+ * allow-list — but the model had nothing in its prompt saying so, and nothing saying
+ * which OS it was on either. It was guessing at the platform *and* at the execution
+ * model, and a guess that is wrong on both is refused three times before the step dies.
+ *
+ * The fix is not a better refusal message. It is that the model should never have been
+ * guessing: the extension knows the platform exactly, at no cost, before the first token
+ * is generated. So it says so, once, in the system prompt.
+ *
+ * ## Why this is also written to disk
+ *
+ * `.hirayacoder/environment.json` is not read back by the prompt path — the profile is
+ * recomputed each session, because a workspace opened on a different machine must not
+ * inherit the last machine's answer. The file is there so the *user* can see what the
+ * agent was told, the same way `audit.log` shows what it did, and so a benchmark run
+ * carries its platform with it. Every one of the eleven benchmark folders in this repo
+ * had to have its OS reconstructed from the shape of the commands the model tried.
+ *
+ * @module core/environmentProfile
+ */
+
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const logger = require('../utils/logger');
+const { platformName } = require('../utils/platform');
+
+/** Where the profile is persisted, relative to the workspace root. */
+const PROFILE_PATH = '.hirayacoder/environment.json';
+
+/**
+ * @typedef {object} EnvironmentProfile
+ * @property {'win32' | 'darwin' | 'linux' | 'other'} platform
+ * @property {string} osName Human-readable, e.g. 'macOS'.
+ * @property {string} osVersion `os.release()`, as reported.
+ * @property {string} arch
+ * @property {'cmd' | 'sh'} commandStyle
+ * Which family a *user* would type these commands in. Not what HirayaCoder spawns —
+ * it spawns no shell at all — but the right answer to "what would this command look
+ * like here", which is what a model needs when it tells the user to run something.
+ * @property {string} pathSeparator
+ * @property {string} nodeVersion
+ * @property {string} generatedAt ISO timestamp of this detection.
+ */
+
+/**
+ * The display name for a platform.
+ *
+ * @param {ReturnType} name
+ * @returns {string}
+ */
+function osNameFor(name) {
+ if (name === 'win32') return 'Windows';
+ if (name === 'darwin') return 'macOS';
+ if (name === 'linux') return 'Linux';
+ return 'an unrecognised Unix-like system';
+}
+
+/**
+ * Detect the machine this session is running on.
+ *
+ * Pure inspection of `os` and `process` — no spawning, no filesystem probing, nothing
+ * that could fail or take a measurable amount of time. This runs on every session start
+ * and must stay free.
+ *
+ * @param {object} [overrides] For tests.
+ * @param {string} [overrides.platform]
+ * @param {string} [overrides.release]
+ * @param {string} [overrides.arch]
+ * @returns {EnvironmentProfile}
+ */
+function detect(overrides = {}) {
+ const name = platformName(overrides.platform || os.platform());
+
+ return {
+ platform: name,
+ osName: osNameFor(name),
+ osVersion: overrides.release || os.release(),
+ arch: overrides.arch || os.arch(),
+ commandStyle: name === 'win32' ? 'cmd' : 'sh',
+ pathSeparator: name === 'win32' ? '\\' : '/',
+ nodeVersion: process.versions.node,
+ generatedAt: new Date().toISOString(),
+ };
+}
+
+/**
+ * The platform-specific half of the prompt block.
+ *
+ * Kept to the differences that actually changed a model's behaviour in the benchmark
+ * runs. A longer list of trivia costs Tier B budget it does not have — at Medium
+ * capacity a 1B model has roughly 1800 tokens for everything.
+ *
+ * @param {EnvironmentProfile} profile
+ * @returns {string[]}
+ */
+function platformNotes(profile) {
+ if (profile.platform === 'win32') {
+ return [
+ 'This is Windows: paths a user types use backslashes, and there is no `mkdir -p`, ' +
+ '`rm -rf`, `ls`, `cat`, `touch`, or `&&` available to you.',
+ 'When you tell the user a command to run themselves, write it for cmd/PowerShell, not bash.',
+ ];
+ }
+
+ return [
+ `This is ${profile.osName}: a user's own shell is POSIX, but you are not running in one — ` +
+ '`mkdir -p`, `rm -rf`, `ls`, `cat`, `touch`, and `&&` are all unavailable to you here.',
+ 'When you tell the user a command to run themselves, write it for bash/zsh.',
+ ];
+}
+
+/**
+ * The block that goes into the system prompt.
+ *
+ * Two things at once, and both halves are load-bearing. The platform facts stop the
+ * model guessing at the OS. The execution-model sentence stops it assuming that knowing
+ * the OS means it has that OS's shell — which is the more expensive mistake, because a
+ * model told "this is macOS" and nothing else proposes `mkdir -p` with *more*
+ * confidence, not less.
+ *
+ * Written as statements of fact rather than rules. This is not a policy the model is
+ * being asked to follow; it is the machine it is standing on.
+ *
+ * ## Why the mutating half is optional
+ *
+ * Plan mode does not have `write_file`, `create_folder`, or `run_script` — they are
+ * absent from the schema, not merely refused, and that structural guarantee is what
+ * Plan mode *is*. A block naming them would be the one place in a Plan prompt that
+ * tells the model it can run commands, which is exactly the confusion the mode exists
+ * to prevent. So the platform facts are carried everywhere the block is used, and the
+ * lines about acting are carried only where acting is possible.
+ *
+ * @param {EnvironmentProfile} profile
+ * @param {object} [opts]
+ * @param {boolean} [opts.mutating] Does this route actually offer the write tools?
+ * Defaults to true; `promptRouter` passes the route's own answer.
+ * @returns {string}
+ */
+function render(profile, opts = {}) {
+ const mutating = opts.mutating !== false;
+
+ const lines = [
+ 'Environment — this is the machine you are running on right now. It is detected, not ' +
+ 'guessed, so never speculate about the operating system or ask the user what it is:',
+ `- Operating system: ${profile.osName} (${profile.platform} ${profile.osVersion}, ${profile.arch})`,
+ `- Node.js: v${profile.nodeVersion}`,
+ ...platformNotes(profile).map((note) => `- ${note}`),
+ ];
+
+ if (mutating) {
+ lines.push(
+ '- run_script does not use a shell on any platform. One plain command per call, no ' +
+ 'operators, no chaining, no redirects. To run inside a subfolder pass it as "cwd" ' +
+ '(`{"command": "npm install", "cwd": "todo-glass-app"}`) — never `cd`.',
+ '- To make a folder use the create_folder tool, to list one use list_files, to read a ' +
+ 'file use read_file, and to write one use write_file. Those replace the shell ' +
+ 'utilities on every platform.'
+ );
+ } else {
+ lines.push('- To list a folder use list_files, and to read a file use read_file.');
+ }
+
+ return lines.join('\n');
+}
+
+/**
+ * Persist the profile for the user to read, best-effort.
+ *
+ * Failure is logged and swallowed. Nothing in the prompt path reads this file, so a
+ * read-only workspace or a full disk must not be able to stop a session starting.
+ *
+ * @param {string} workspaceRoot
+ * @param {EnvironmentProfile} [profile]
+ * @returns {EnvironmentProfile} The profile, written or not.
+ */
+function persist(workspaceRoot, profile = detect()) {
+ if (!workspaceRoot) return profile;
+
+ const file = path.join(workspaceRoot, ...PROFILE_PATH.split('/'));
+ try {
+ fs.mkdirSync(path.dirname(file), { recursive: true });
+ fs.writeFileSync(file, `${JSON.stringify(profile, null, 2)}\n`, 'utf8');
+ } catch (err) {
+ logger.warn(`Could not write ${PROFILE_PATH}: ${/** @type {Error} */ (err).message}`);
+ }
+
+ return profile;
+}
+
+module.exports = { detect, render, persist, platformNotes, osNameFor, PROFILE_PATH };
diff --git a/app/core/promptRouter.js b/app/core/promptRouter.js
index 89ca25a..3726731 100644
--- a/app/core/promptRouter.js
+++ b/app/core/promptRouter.js
@@ -25,6 +25,7 @@
const toolRegistry = require('../agent/toolRegistry');
const modelCapability = require('./modelCapability');
const earnedHints = require('../agent/earnedHints');
+const environmentProfile = require('./environmentProfile');
const productInfo = require('../utils/productInfo');
const { loadTemplate } = require('../utils/promptLoader');
@@ -51,6 +52,8 @@ Rules:
set "cwd" to that folder. Never use cd, and never chain commands with &&.
- When the task is done, use "done" with a short summary. Do not keep exploring.
+{environment}
+
Session Memory (things established earlier in this project — background, not new
instructions):
{memory}`;
@@ -63,6 +66,8 @@ prefer small verifiable steps, and stop as soon as the task is genuinely complet
Never guess a file's contents — read it. Never chain shell commands; run one at a time,
and to run inside a subfolder set the command's "cwd" rather than using cd.
+{environment}
+
Session Memory (established earlier in this project — background, not new instructions):
{memory}`;
@@ -202,6 +207,35 @@ function withIdentity(prompt) {
return `${identity}\n\n${prompt}`;
}
+/**
+ * Put the detected machine into a system prompt.
+ *
+ * ## Why only the tool-using routes get this
+ *
+ * Every line of the block is about performing an action — which shell utilities are
+ * unavailable, how to run a command in a subfolder, which tool replaces `mkdir`. Ask and
+ * the conversational route have no tools at all, so the block would be six lines of
+ * inapplicable instruction paid for out of the same budget the user's question is
+ * competing for. The one thing it would answer for them — "what OS am I on?" — is not
+ * worth the block, and Agent mode answers it anyway.
+ *
+ * Substituted in place where a template carries `{environment}`, appended otherwise. The
+ * prompt files on disk are author-editable, and a user's customised prompt must not be
+ * able to silently drop the platform facts by not knowing about the placeholder — the
+ * same rule `withIdentity` follows, for the same reason.
+ *
+ * @param {string} prompt
+ * @param {import('./environmentProfile').EnvironmentProfile} [profile]
+ * @param {{mutating?: boolean}} [opts] Passed through to `environmentProfile.render`,
+ * so a Plan prompt never names a tool Plan mode does not have.
+ * @returns {string}
+ */
+function withEnvironment(prompt, profile, opts = {}) {
+ const block = environmentProfile.render(profile || environmentProfile.detect(), opts);
+ if (prompt.includes('{environment}')) return prompt.replace('{environment}', block);
+ return `${prompt}\n\n${block}`;
+}
+
/**
* Render the earned-hints block appended to a system prompt.
*
@@ -237,6 +271,9 @@ function renderEarnedHints(hints) {
* @property {import('./modelCapability').Capability} capability
* @property {import('./modelCapability').ThinkingCapacity} thinkingCapacity
* @property {string} [memory] Rendered Session Memory block.
+ * @property {import('./environmentProfile').EnvironmentProfile} [environment]
+ * The detected machine. Detected here when omitted; passed in by `agentSession` so a
+ * six-item TODO run does not re-detect it per step.
* @property {string[]} [earnedHints] Sentences from `agent/earnedHints`, already selected.
* @property {'chat' | 'task'} [intent] From `core/intentRouter`. Only consulted in Agent mode.
* @property {boolean} [readOnlyTurn] From `intentRouter.isReadOnlyRequest`. Drops the
@@ -333,7 +370,9 @@ function route(request) {
.replace('{session_memory}', memory);
}
- systemPrompt = withIdentity(systemPrompt);
+ systemPrompt = withIdentity(
+ withEnvironment(systemPrompt, request.environment, { mutating: toolMode !== 'plan' })
+ );
if (mode === 'plan') systemPrompt += PLAN_SUFFIX;
if (readOnlyTurn) systemPrompt += READ_ONLY_SUFFIX;
@@ -378,6 +417,7 @@ module.exports = {
canMutate,
renderEarnedHints,
withIdentity,
+ withEnvironment,
READ_ONLY_SUFFIX,
ASK_SYSTEM,
CHAT_SYSTEM,
diff --git a/app/core/workspaceBootstrap.js b/app/core/workspaceBootstrap.js
new file mode 100644
index 0000000..6b5fc47
--- /dev/null
+++ b/app/core/workspaceBootstrap.js
@@ -0,0 +1,139 @@
+'use strict';
+
+/* eslint-disable security/detect-non-literal-fs-filename --
+ * Both files written here are fixed names under the VS Code workspace root. No
+ * component of either path derives from model output. */
+
+/**
+ * The two things a workspace needs before the first turn runs.
+ *
+ * Both are small, both are idempotent, and both were previously left to chance:
+ *
+ * 1. `.hirayacoder/environment.json` — what machine this is, so the model is told
+ * rather than guessing. See `core/environmentProfile` for the run that motivated it.
+ * 2. `.gitignore` carries `.hirayacoder/` — so a session's audit log, transcripts, and
+ * memory do not turn up in the user's next commit.
+ *
+ * ## Why the .gitignore entry is not optional
+ *
+ * `.hirayacoder/` holds `audit.log`, `outcomes.jsonl`, `transcripts/*.json`, and
+ * `memory/session*.txt`. Transcripts contain the full text of every message in a
+ * session, and memory contains file paths and command lines from the project. None of
+ * that is secret exactly, and all of it is noise in a diff — but the failure mode is a
+ * user committing their whole conversation history to a public repository without
+ * noticing, which is a privacy claim this extension makes and should therefore keep.
+ *
+ * Every benchmark workspace in `.ignore/` shows the same thing: `.hirayacoder/` sitting
+ * beside the scaffolded project, untracked and unmentioned, next to a `.gitignore` the
+ * scaffolder wrote that lists `node_modules` and nothing else.
+ *
+ * ## The rules this follows
+ *
+ * - An existing `.gitignore` is **appended to**, never rewritten. It is the user's file.
+ * - Nothing is appended if any existing line already covers `.hirayacoder`, whatever
+ * form it is written in — a second entry would be noise, and a `!.hirayacoder`
+ * negation is a decision to respect rather than to overrule.
+ * - A missing `.gitignore` is created with the one entry and nothing else. Guessing at
+ * `node_modules`, `dist`, and `.env` for a project whose language is unknown is how a
+ * helpful default becomes a wrong one.
+ * - Every failure is logged and swallowed. A session must start on a read-only checkout.
+ *
+ * @module core/workspaceBootstrap
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+const logger = require('../utils/logger');
+const environmentProfile = require('./environmentProfile');
+
+/** The entry written into `.gitignore`. Trailing slash: it is always a directory. */
+const IGNORE_ENTRY = '.hirayacoder/';
+
+/** The comment written above the entry, so a reader knows what put it there. */
+const IGNORE_COMMENT = '# HirayaCoder session data (audit log, transcripts, memory)';
+
+/**
+ * Does this `.gitignore` already have an opinion about `.hirayacoder`?
+ *
+ * Deliberately broad. `.hirayacoder`, `.hirayacoder/`, `/.hirayacoder`, `**\/.hirayacoder`,
+ * and `!.hirayacoder` all count — the question is whether the user has already dealt
+ * with this path, not whether they spelled it the way this module would.
+ *
+ * @param {string} contents
+ * @returns {boolean}
+ */
+function alreadyIgnored(contents) {
+ return String(contents || '')
+ .split(/\r?\n/)
+ .some((line) => {
+ const trimmed = line.trim();
+ if (!trimmed || trimmed.startsWith('#')) return false;
+ // Strip the negation marker and any leading/trailing path decoration, then compare
+ // on the bare name.
+ const bare = trimmed
+ .replace(/^!/, '')
+ .replace(/^\*\*\//, '')
+ .replace(/^\//, '')
+ .replace(/\/$/, '');
+ return bare === '.hirayacoder';
+ });
+}
+
+/**
+ * Add `.hirayacoder/` to the workspace's `.gitignore`, creating it if there is none.
+ *
+ * @param {string} workspaceRoot
+ * @returns {'created' | 'appended' | 'present' | 'failed'} What happened, for the log
+ * and for tests. Never throws.
+ */
+function ensureGitignore(workspaceRoot) {
+ if (!workspaceRoot) return 'failed';
+ const file = path.join(workspaceRoot, '.gitignore');
+
+ try {
+ if (!fs.existsSync(file)) {
+ fs.writeFileSync(file, `${IGNORE_COMMENT}\n${IGNORE_ENTRY}\n`, 'utf8');
+ logger.info('Created .gitignore with .hirayacoder/ ignored.');
+ return 'created';
+ }
+
+ const contents = fs.readFileSync(file, 'utf8');
+ if (alreadyIgnored(contents)) return 'present';
+
+ // Respect the file's own line ending, and never join the entry onto a last line
+ // that has no newline of its own.
+ const eol = contents.includes('\r\n') ? '\r\n' : '\n';
+ const separator = contents.length === 0 || contents.endsWith('\n') ? '' : eol;
+ fs.appendFileSync(file, `${separator}${eol}${IGNORE_COMMENT}${eol}${IGNORE_ENTRY}${eol}`, 'utf8');
+ logger.info('Added .hirayacoder/ to the existing .gitignore.');
+ return 'appended';
+ } catch (err) {
+ logger.warn(`Could not update .gitignore: ${/** @type {Error} */ (err).message}`);
+ return 'failed';
+ }
+}
+
+/**
+ * Prepare a workspace for agent sessions.
+ *
+ * Safe to call repeatedly — on activation, and again whenever a chat tab opens. Both
+ * halves are idempotent, and the environment profile is deliberately rewritten each
+ * time rather than reused: a workspace synced between two machines must not report the
+ * other one's operating system.
+ *
+ * @param {string | null} workspaceRoot
+ * @returns {{profile: import('./environmentProfile').EnvironmentProfile, gitignore: string} | null}
+ * null when no workspace is open.
+ */
+function bootstrap(workspaceRoot) {
+ if (!workspaceRoot) return null;
+
+ const profile = environmentProfile.persist(workspaceRoot);
+ const gitignore = ensureGitignore(workspaceRoot);
+
+ logger.debug(`Workspace ready: ${profile.osName} (${profile.platform}), .gitignore ${gitignore}.`);
+ return { profile, gitignore };
+}
+
+module.exports = { bootstrap, ensureGitignore, alreadyIgnored, IGNORE_ENTRY, IGNORE_COMMENT };
diff --git a/app/extension.js b/app/extension.js
index 4f783de..97f3f63 100644
--- a/app/extension.js
+++ b/app/extension.js
@@ -43,6 +43,8 @@ const diffApply = require('./features/diffApply');
const modelManager = require('./features/modelManager');
const { SessionsProvider } = require('./features/sessionsView');
const { TranscriptStore } = require('./core/transcriptStore');
+const workspaceBootstrap = require('./core/workspaceBootstrap');
+const environmentProfile = require('./core/environmentProfile');
/** Workspace-state key holding models the user has waved off the ">7B" nudge for. */
const DISMISSED_RECOMMENDATIONS_KEY = 'hirayacoder.dismissedRecommendations';
@@ -183,10 +185,24 @@ class HirayaCoder {
/** @type {FileHistory | null} */
this.fileHistory = null;
+ /**
+ * What machine this is, and a workspace prepared to hold a session's records.
+ *
+ * Done here rather than at the first turn so the profile exists before anything can
+ * read it, and so `.gitignore` covers `.hirayacoder/` before the first audit line is
+ * written rather than after the user has already staged it.
+ *
+ * @type {import('./core/environmentProfile').EnvironmentProfile}
+ */
+ this.environment = environmentProfile.detect();
+
this.buildClient();
this.buildSecurityLayer();
this.buildLedger();
this.buildSession();
+
+ const ready = workspaceBootstrap.bootstrap(workspaceRoot());
+ if (ready) this.environment = ready.profile;
}
/**
diff --git a/app/features/chatTab.js b/app/features/chatTab.js
index e86f965..b3615bc 100644
--- a/app/features/chatTab.js
+++ b/app/features/chatTab.js
@@ -600,6 +600,9 @@ class ChatTab {
// about this conversation.
ledger: this.app.ledger,
adaptation: this.app.settings.adaptation,
+ // Detected once at activation and shared, like the ledger and for the same reason:
+ // the operating system is a fact about the machine, not about this conversation.
+ environment: this.app.environment,
stepSessions: this.stepSessions,
images: images.map((image) => image.base64),
});
diff --git a/doc/FEATURES.md b/doc/FEATURES.md
index ca89fff..2b743f1 100644
--- a/doc/FEATURES.md
+++ b/doc/FEATURES.md
@@ -303,6 +303,16 @@ The parts that decide what the agent is *allowed* to do.
explains itself so the model can correct and retry.
- **An append-only audit log** of every action, decision, and permission mode, at
`.hirayacoder/audit.log` (**Show Audit Log**).
+- **A stated environment.** The operating system, its release, the architecture, and the
+ Node version are detected at session start and put in the system prompt as fact, along
+ with the note that no shell exists on any platform. A model that has to guess whether
+ `mkdir -p` or `md` is the right spelling has already lost the step; both are wrong here
+ for the same reason. Written to `.hirayacoder/environment.json` so a session's records
+ say which machine produced them.
+- **A workspace prepared before the first turn.** `.hirayacoder/` holds transcripts,
+ memory, and the audit log, so it is added to the workspace's `.gitignore` at startup —
+ appended to an existing file, never rewritten, and skipped entirely if any line already
+ covers it.
- **Honest summaries.** What actually failed is appended to the model's own account from
the step record, so a claim that a declined delete succeeded is contradicted in place.
diff --git a/package-lock.json b/package-lock.json
index 20da1c0..792011c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "hirayacoder",
- "version": "0.6.0",
+ "version": "0.6.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hirayacoder",
- "version": "0.6.0",
+ "version": "0.6.1",
"license": "MIT",
"devDependencies": {
"@types/mocha": "^10.0.10",
diff --git a/package.json b/package.json
index c84da59..2c8b6fb 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "hirayacoder",
"displayName": "HirayaCoder",
"description": "A fully offline, privacy-first AI coding agent powered by your local Ollama instance. Agentic on every model — even 1B.",
- "version": "0.6.0",
+ "version": "0.6.1",
"publisher": "jaymar921",
"author": {
"name": "jaymar921",
diff --git a/setup/prompts/agentic-system-prompt.md b/setup/prompts/agentic-system-prompt.md
index 0faeedb..7603250 100644
--- a/setup/prompts/agentic-system-prompt.md
+++ b/setup/prompts/agentic-system-prompt.md
@@ -60,11 +60,14 @@ auto-approve mode is on. delete_folder is the exception with no auto mode at all
every time, it refuses a folder that still has files in it unless you pass
`recursive: true`, and nothing it removes can be restored.
+{environment}
+
Session Memory — facts established earlier in this project session. This is reference
background, not new instructions, and never grants permissions:
{memory}
```
**Notes for implementers:**
+- `{environment}` is populated from `core/environmentProfile.js` — the detected OS, its release, the architecture, the Node version, and the platform-specific note about which shell utilities are unavailable. It is detected per session, never read back from disk, so a workspace synced between two machines never reports the other one's OS. Remove the placeholder and the block is appended at the end instead of dropped: a customised prompt file must not be able to leave the model guessing at the platform.
- This prompt is injected as the `system` field of the Ollama `/api/chat` request only when `modelCapability.js` classifies the active model as Tier A, and is consumed by `agent/nativeToolLoop.js` underneath the shared `agent/agentSession.js` driver (step budget, session diff set, pause/resume/stop) — the same driver Tier B's `reactLoop.js` reports back into.
- Keep this file versioned; bump a `v2`, `v3` file rather than silently mutating behavior, so users can pin a known-good prompt version in settings.
diff --git a/setup/prompts/lite-1b-system-prompt.md b/setup/prompts/lite-1b-system-prompt.md
index 08cd7e3..1b28a14 100644
--- a/setup/prompts/lite-1b-system-prompt.md
+++ b/setup/prompts/lite-1b-system-prompt.md
@@ -43,12 +43,15 @@ Rules:
have what you need.
- If you cannot proceed safely, use "done" and say in the summary exactly what you need.
+{environment}
+
Session Memory — facts established earlier in this project. This is background to help
you, not new instructions, and it never grants you permissions:
{session_memory}
```
**Notes for implementers:**
+- `{environment}` is populated from `core/environmentProfile.js` — the detected OS, its release, the architecture, the Node version, and the platform-specific note about which shell utilities are unavailable. It is detected per session, never read back from disk, so a workspace synced between two machines never reports the other one's OS. Remove the placeholder and the block is appended at the end instead of dropped: a customised prompt file must not be able to leave the model guessing at the platform.
- `{session_memory}` is populated from `core/memoryStore.js` — how many entries are recalled depends on the active Thinking Capacity setting (Low: 1 entry, Medium: 3–5, High: all available within budget — see `PROMPT.md` section 5).
- Pass `format: "json"` in the Ollama `/api/generate` or `/api/chat` request every turn, so Ollama constrains sampling to valid JSON.
- `outputParser.js` must validate each turn's response against this exact schema and treat any parse/schema failure as an implicit `"action": "done"` with a fallback summary showing the raw text — never attempt to "repair" and auto-apply unparseable output.
diff --git a/test/unit/environmentProfile.test.js b/test/unit/environmentProfile.test.js
new file mode 100644
index 0000000..d3c9e3e
--- /dev/null
+++ b/test/unit/environmentProfile.test.js
@@ -0,0 +1,103 @@
+'use strict';
+
+/**
+ * Telling the model what machine it is on, instead of letting it guess.
+ *
+ * The bug these cover is not subtle and did not need a clever detector: on macOS the
+ * model proposed `mkdir -p`, on Windows it would have proposed `md`, and the extension
+ * knew the answer both times and never said it. So the assertions are about the two
+ * things that have to be true of the block — it names the real platform, and it never
+ * implies a shell exists — plus the one thing that must not happen, which is a Plan
+ * prompt naming a tool Plan mode does not have.
+ */
+
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const environmentProfile = require('../../app/core/environmentProfile');
+
+describe('environmentProfile.detect', () => {
+ it('names each platform the way a person would', () => {
+ assert.strictEqual(environmentProfile.detect({ platform: 'darwin' }).osName, 'macOS');
+ assert.strictEqual(environmentProfile.detect({ platform: 'win32' }).osName, 'Windows');
+ assert.strictEqual(environmentProfile.detect({ platform: 'linux' }).osName, 'Linux');
+ });
+
+ it('reports the command style a user of this machine would type in', () => {
+ assert.strictEqual(environmentProfile.detect({ platform: 'win32' }).commandStyle, 'cmd');
+ assert.strictEqual(environmentProfile.detect({ platform: 'darwin' }).commandStyle, 'sh');
+ assert.strictEqual(environmentProfile.detect({ platform: 'linux' }).commandStyle, 'sh');
+ });
+
+ it('describes this machine without being told what it is', () => {
+ const profile = environmentProfile.detect();
+ assert.strictEqual(profile.platform === 'other' ? 'other' : profile.platform, profile.platform);
+ assert.ok(profile.osVersion.length > 0);
+ assert.strictEqual(profile.nodeVersion, process.versions.node);
+ assert.ok(!Number.isNaN(Date.parse(profile.generatedAt)));
+ });
+});
+
+describe('environmentProfile.render', () => {
+ it('states the operating system as a fact, not a question to ask the user', () => {
+ const block = environmentProfile.render(environmentProfile.detect({ platform: 'darwin', release: '24.1.0' }));
+ assert.ok(block.includes('macOS'));
+ assert.ok(block.includes('24.1.0'));
+ assert.ok(/never speculate/i.test(block));
+ });
+
+ it('says the POSIX utilities are unavailable even on a POSIX machine', () => {
+ // The mistake worth designing against: a model told "this is macOS" and nothing else
+ // proposes `mkdir -p` with more confidence, not less.
+ const block = environmentProfile.render(environmentProfile.detect({ platform: 'darwin' }));
+ assert.ok(block.includes('mkdir -p'));
+ assert.ok(/unavailable/i.test(block));
+ assert.ok(block.includes('"cwd"'));
+ });
+
+ it('writes the Windows note for a Windows machine', () => {
+ const block = environmentProfile.render(environmentProfile.detect({ platform: 'win32' }));
+ assert.ok(block.includes('Windows'));
+ assert.ok(/cmd\/PowerShell/.test(block));
+ assert.ok(!/bash\/zsh/.test(block));
+ });
+
+ it('names no mutating tool when the route has none', () => {
+ const block = environmentProfile.render(environmentProfile.detect({ platform: 'linux' }), { mutating: false });
+ for (const name of ['write_file', 'create_folder', 'run_script', 'delete_file']) {
+ assert.ok(!block.includes(name), `${name} was named in a read-only environment block`);
+ }
+ // The platform facts still travel — they are true whatever the mode is.
+ assert.ok(block.includes('Linux'));
+ });
+});
+
+describe('environmentProfile.persist', () => {
+ /** @type {string} */
+ let root;
+
+ beforeEach(() => {
+ root = fs.mkdtempSync(path.join(os.tmpdir(), 'hiraya-env-'));
+ });
+
+ afterEach(() => {
+ fs.rmSync(root, { recursive: true, force: true });
+ });
+
+ it('leaves the profile where a user can read what the agent was told', () => {
+ const written = environmentProfile.persist(root, environmentProfile.detect({ platform: 'darwin' }));
+ const onDisk = JSON.parse(fs.readFileSync(path.join(root, '.hirayacoder', 'environment.json'), 'utf8'));
+
+ assert.strictEqual(onDisk.osName, 'macOS');
+ assert.strictEqual(onDisk.platform, 'darwin');
+ assert.strictEqual(onDisk.nodeVersion, written.nodeVersion);
+ });
+
+ it('returns the profile rather than throwing when the file cannot be written', () => {
+ // No workspace is the ordinary case, not an error: the extension runs without one.
+ const profile = environmentProfile.persist('', environmentProfile.detect({ platform: 'linux' }));
+ assert.strictEqual(profile.osName, 'Linux');
+ });
+});
diff --git a/test/unit/promptRouter.test.js b/test/unit/promptRouter.test.js
index 768b09f..51b68fa 100644
--- a/test/unit/promptRouter.test.js
+++ b/test/unit/promptRouter.test.js
@@ -276,3 +276,77 @@ describe('toolRegistry mode filtering', () => {
assert.strictEqual(toolRegistry.get('rm_rf', 'agent'), null);
});
});
+
+/**
+ * The machine, stated rather than left for the model to infer.
+ *
+ * On the macOS 0.6.0 run the model was guessing at the platform *and* at the execution
+ * model, and a guess that is wrong on both is refused three times before the step dies.
+ * The extension knew the answer to the first before the first token was generated.
+ */
+describe('promptRouter — environment', () => {
+ const environmentProfile = require('../../app/core/environmentProfile');
+
+ /**
+ * @param {'agent'|'plan'|'ask'} mode
+ * @param {string} platform
+ * @param {object} capability
+ */
+ const routeOn = (mode, platform, capability = TIER_B) =>
+ promptRouter.route({
+ mode,
+ capability,
+ thinkingCapacity: 'medium',
+ memory: '- earlier fact',
+ environment: environmentProfile.detect({ platform }),
+ });
+
+ it('tells both tiers which operating system this is', () => {
+ for (const capability of [TIER_A, TIER_B]) {
+ assert.match(routeOn('agent', 'darwin', capability).systemPrompt, /macOS/);
+ assert.match(routeOn('agent', 'win32', capability).systemPrompt, /Windows/);
+ }
+ });
+
+ it('says the shell utilities are unavailable, whichever platform it is', () => {
+ for (const platform of ['darwin', 'linux', 'win32']) {
+ const prompt = routeOn('agent', platform).systemPrompt;
+ assert.match(prompt, /mkdir -p/, platform);
+ assert.match(prompt, /"cwd"/, platform);
+ }
+ });
+
+ it('lands where the template asks for it rather than at the end', () => {
+ // Both prompt files carry `{environment}` above the memory block. Substituted in
+ // place, so a customised file controls the position; appended when the placeholder
+ // is gone, so a customised file cannot drop the platform facts by omission.
+ const prompt = routeOn('agent', 'darwin').systemPrompt;
+ assert.ok(!prompt.includes('{environment}'), 'the placeholder was left unsubstituted');
+ assert.ok(prompt.indexOf('Operating system:') < prompt.indexOf('earlier fact'));
+
+ const appended = promptRouter.withEnvironment('a prompt with no placeholder', environmentProfile.detect());
+ assert.match(appended, /^a prompt with no placeholder\n\nEnvironment/);
+ });
+
+ it('names no mutating tool in a Plan prompt', () => {
+ // Plan mode's guarantee is that the write tools are absent, not refused. A block
+ // telling the model how to run a command would be the one place in the prompt
+ // saying otherwise.
+ const prompt = routeOn('plan').systemPrompt;
+ for (const name of MUTATING) {
+ assert.ok(!prompt.includes(name), `Plan prompt mentions ${name}`);
+ }
+ assert.match(prompt, /macOS|Windows|Linux/);
+ });
+
+ it('leaves the loopless routes alone', () => {
+ // Every line of the block is about performing an action, and Ask has no tools to
+ // perform one with.
+ assert.ok(!routeOn('ask').systemPrompt.includes('Operating system:'));
+ });
+
+ it('detects the machine itself when no profile is passed', () => {
+ const prompt = routeFor('agent').systemPrompt;
+ assert.match(prompt, /Operating system:/);
+ });
+});
diff --git a/test/unit/runScriptTool.test.js b/test/unit/runScriptTool.test.js
index 81e7d7d..c1a90fb 100644
--- a/test/unit/runScriptTool.test.js
+++ b/test/unit/runScriptTool.test.js
@@ -453,3 +453,88 @@ describe('the allow-list', () => {
}
});
});
+
+/**
+ * `cd folder && command` taken as the two arguments it already is.
+ *
+ * 0.6.0 added `cwd`, documented it in both prompts, and made the refusal name it. The
+ * macOS benchmark model sent `cd todo-glass-app && npm install` anyway, was told to use
+ * `cwd`, and sent the identical line again. The folder and the command are both in the
+ * string; the extension was throwing them away to ask for them back.
+ */
+describe('runScript.unchainCd', () => {
+ const { unchainCd } = runScript;
+
+ it('splits the folder off the command', () => {
+ assert.deepStrictEqual(unchainCd('cd todo-glass-app && npm install', ''), {
+ command: 'npm install',
+ cwd: 'todo-glass-app',
+ });
+ });
+
+ it('handles the shapes a model actually writes', () => {
+ assert.deepStrictEqual(unchainCd('cd ./app && npm run build', ''), { command: 'npm run build', cwd: 'app' });
+ assert.deepStrictEqual(unchainCd('cd packages/web/ && npm test', ''), {
+ command: 'npm test',
+ cwd: 'packages/web',
+ });
+ });
+
+ it('composes with a cwd the model also supplied, the way cd would', () => {
+ assert.deepStrictEqual(unchainCd('cd src && npm test', 'app'), { command: 'npm test', cwd: 'app/src' });
+ });
+
+ it('refuses rather than rewrites anything that is not the one simple shape', () => {
+ // Two commands after one `cd`: which of them the user is approving is not
+ // answerable, so it keeps the refusal it has always had.
+ assert.strictEqual(unchainCd('cd app && npm install && npm run build', ''), null);
+ assert.strictEqual(unchainCd('cd app && npm test | tee out.txt', ''), null);
+ assert.strictEqual(unchainCd('cd app && npm test > out.txt', ''), null);
+ assert.strictEqual(unchainCd('cd .. && npm install', ''), null);
+ assert.strictEqual(unchainCd('cd ../sibling && npm install', ''), null);
+ assert.strictEqual(unchainCd('cd /etc && npm install', ''), null);
+ assert.strictEqual(unchainCd('cd app &&', ''), null);
+ assert.strictEqual(unchainCd('cd app', ''), null);
+ // Not a rewrite target at all — no `cd` to unchain.
+ assert.strictEqual(unchainCd('npm install', ''), null);
+ assert.strictEqual(unchainCd('npm install && npm test', ''), null);
+ });
+
+ it('does not let a quoted path smuggle a second command through', () => {
+ assert.strictEqual(unchainCd('cd "my app" && npm install', ''), null);
+ assert.strictEqual(unchainCd('cd $(whoami) && npm install', ''), null);
+ assert.strictEqual(unchainCd('cd `pwd` && npm install', ''), null);
+ });
+});
+
+describe('runScript with a chained cd', () => {
+ it('runs the rewritten command and says so', async () => {
+ /** @type {any} */
+ let requested = null;
+ const context = {
+ sessionId: '1',
+ mode: 'agent',
+ gate: {
+ async requestScript(request) {
+ requested = request;
+ return { allowed: true, decision: 'approved' };
+ },
+ async runScript() {
+ return { ok: true, code: 0, stdout: 'added 42 packages', stderr: '', timedOut: false, durationMs: 12 };
+ },
+ },
+ };
+
+ const result = await runScript({ command: 'cd todo-glass-app && npm install' }, context);
+
+ // The gate — and therefore the confirmation prompt and the audit log — sees the
+ // command that will actually run, not the one the model typed.
+ assert.strictEqual(requested.command, 'npm install');
+ assert.strictEqual(requested.cwd, 'todo-glass-app');
+ assert.strictEqual(result.ok, true);
+ // And the model is told, every time. A tool that quietly substitutes one command
+ // for another makes a failure impossible to read.
+ assert.match(result.observation, /cd todo-glass-app && npm install/);
+ assert.match(result.observation, /"cwd": "todo-glass-app"/);
+ });
+});
diff --git a/test/unit/scriptPreflight.test.js b/test/unit/scriptPreflight.test.js
index d856393..c144f7b 100644
--- a/test/unit/scriptPreflight.test.js
+++ b/test/unit/scriptPreflight.test.js
@@ -175,4 +175,29 @@ describe('scriptPreflight — npm climbing out of the workspace', () => {
assert.strictEqual(preflight({ command: 'node src/main.js', workspaceRoot: root }), null);
assert.strictEqual(preflight({ command: 'git status', workspaceRoot: root }), null);
});
+
+ // The macOS 0.6.0 run died on the first step of the first item: the only command that
+ // creates a project was refused for the absence of the project it was about to create.
+ it('lets a scaffolder run in the empty workspace it is there to fill', () => {
+ for (const command of [
+ 'npm create vite@latest todo-glass-app -- --template react',
+ 'npm init -y',
+ 'npm exec create-vite todo-glass-app',
+ 'yarn create vite todo-glass-app',
+ 'pnpm dlx create-vite todo-glass-app',
+ ]) {
+ assert.strictEqual(preflight({ command, workspaceRoot: root }), null, command);
+ }
+ });
+
+ it('still refuses the commands that would climb out of the workspace', () => {
+ // The guard this preserves: `npm install` with no manifest walks upwards and
+ // installs into whatever it finds, which is how a dependency once landed in the
+ // extension's own package.json. Creating a project is the exception; acting on one
+ // that is not there is not.
+ for (const command of ['npm install lucide-react', 'npm run build', 'yarn add react']) {
+ const refusal = preflight({ command, workspaceRoot: root });
+ assert.strictEqual(refusal && refusal.code, 'NO_PACKAGE_JSON', command);
+ }
+ });
});
diff --git a/test/unit/stepGuard.test.js b/test/unit/stepGuard.test.js
index ba44542..23da92f 100644
--- a/test/unit/stepGuard.test.js
+++ b/test/unit/stepGuard.test.js
@@ -52,6 +52,42 @@ describe('stepGuard', () => {
assert.match(verdict.detail, /no work at all/);
});
+ // The macOS 0.6.0 run: `npx create-vite` exit 0, `npm install` exit 0, twenty
+ // seconds of it, the project on disk — and the step was written off as "nothing was
+ // written", which stopped the run and skipped the five remaining items. A
+ // scaffolder's output never passes through write_file, so the files half of the
+ // change set is empty and the commands half is the only record that anything
+ // happened.
+ it('passes a step whose work was done by a command that succeeded', () => {
+ const verdict = stepGuard.verify(
+ evidence({
+ item: 'Scaffold the Vite React project and install dependencies',
+ commands: [
+ { command: 'npx create-vite@latest todo-glass-app -- --template react', ok: true },
+ { command: 'npm install', ok: true },
+ ],
+ })
+ );
+
+ assert.strictEqual(verdict.ok, true);
+ assert.strictEqual(verdict.reason, 'ran');
+ assert.match(verdict.detail, /npm install/);
+ // Said out loud rather than passed silently: a later step needs to know the agent
+ // wrote nothing itself before it assumes a file is there.
+ assert.match(verdict.detail, /no files were written directly/);
+ });
+
+ it('still fails a step whose commands all failed', () => {
+ const verdict = stepGuard.verify(
+ evidence({
+ item: 'Scaffold the Vite React project',
+ commands: [{ command: 'npm install', ok: false }],
+ })
+ );
+ assert.strictEqual(verdict.ok, false);
+ assert.strictEqual(verdict.reason, 'no-change');
+ });
+
it('names the blocking error when the step tried and was refused', () => {
const verdict = stepGuard.verify(
evidence({
diff --git a/test/unit/stepSessions.test.js b/test/unit/stepSessions.test.js
index c1fa60e..fa2f617 100644
--- a/test/unit/stepSessions.test.js
+++ b/test/unit/stepSessions.test.js
@@ -299,4 +299,51 @@ describe('step sessions', function () {
const firstStep = client.prompts.slice(1, 3).join('\n');
assert.ok(firstStep.includes('useTodos.js'), 'the step was not given the note about the file it had to import');
});
+
+ // The macOS 0.6.0 run, reduced. Item one was "scaffold the project": `npx create-vite`
+ // exited 0, `npm install` exited 0, the project was on disk — and because a
+ // scaffolder's output never passes through `write_file`, the guard saw an empty change
+ // set, failed the step, and skipped the five items after it. The user's report was a
+ // page of text about a scaffold that had worked.
+ it('does not write off a step whose work was done by a command', async () => {
+ const modes = new PermissionModes({ initial: { autoEdit: true, autoApproveScripts: true } });
+ const client = scriptedClient([
+ '1. Set up the project toolchain\n2. Write src/App.jsx',
+ // Allow-listed, present wherever these tests run, and fast — this stands in for
+ // the scaffolder. What matters is that it succeeds and writes nothing itself.
+ json({ action: 'run_script', command: 'node --version' }),
+ json({ action: 'done', summary: 'toolchain ready' }),
+ json({
+ action: 'write_file',
+ path: 'src/App.jsx',
+ code: 'export default function App() {\n return Todo
;\n}\n',
+ }),
+ json({ action: 'done', summary: 'app written' }),
+ ]);
+
+ const agent = new AgentSession({
+ client,
+ model: 'qwen3.5:4b',
+ capability: TIER_TODO,
+ gate: new PermissionGate({
+ workspaceRoot: root,
+ modes,
+ auditLog: new AuditLog(root),
+ confirm: async () => true,
+ }),
+ workspaceRoot: root,
+ thinkingCapacity: 'medium',
+ sessionId: '1',
+ stepSessions: true,
+ });
+
+ const result = await agent.run('Set up the toolchain, then write App.jsx', { mode: 'agent' });
+
+ assert.deepStrictEqual(
+ result.todos.map((item) => item.status),
+ ['done', 'done'],
+ 'the command-only step was written off, which stopped the run'
+ );
+ assert.ok(!/Stopped at step 1/.test(result.summary), result.summary);
+ });
});
diff --git a/test/unit/workspaceBootstrap.test.js b/test/unit/workspaceBootstrap.test.js
new file mode 100644
index 0000000..4667068
--- /dev/null
+++ b/test/unit/workspaceBootstrap.test.js
@@ -0,0 +1,119 @@
+'use strict';
+
+/**
+ * Getting a workspace ready before the first turn runs.
+ *
+ * The `.gitignore` half is the one with rules worth pinning down: it is the user's file,
+ * it may already say something about `.hirayacoder`, and it may not exist at all. The
+ * failure to avoid is not a missing entry — it is a rewritten file.
+ */
+
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const workspaceBootstrap = require('../../app/core/workspaceBootstrap');
+
+describe('workspaceBootstrap.alreadyIgnored', () => {
+ it('recognises every form the entry is written in', () => {
+ for (const line of ['.hirayacoder', '.hirayacoder/', '/.hirayacoder', '**/.hirayacoder', ' .hirayacoder/ ']) {
+ assert.ok(workspaceBootstrap.alreadyIgnored(`node_modules\n${line}\ndist\n`), `missed "${line}"`);
+ }
+ });
+
+ it('treats a negation as a decision already made', () => {
+ // `!.hirayacoder` is someone deliberately tracking it. Appending the positive entry
+ // underneath would silently overrule them.
+ assert.ok(workspaceBootstrap.alreadyIgnored('!.hirayacoder\n'));
+ });
+
+ it('is not fooled by a comment or by a longer name', () => {
+ assert.ok(!workspaceBootstrap.alreadyIgnored('# .hirayacoder\n'));
+ assert.ok(!workspaceBootstrap.alreadyIgnored('.hirayacoder-backup\n'));
+ assert.ok(!workspaceBootstrap.alreadyIgnored(''));
+ });
+});
+
+describe('workspaceBootstrap.ensureGitignore', () => {
+ /** @type {string} */
+ let root;
+
+ beforeEach(() => {
+ root = fs.mkdtempSync(path.join(os.tmpdir(), 'hiraya-bootstrap-'));
+ });
+
+ afterEach(() => {
+ fs.rmSync(root, { recursive: true, force: true });
+ });
+
+ /** @returns {string} */
+ const read = () => fs.readFileSync(path.join(root, '.gitignore'), 'utf8');
+
+ it('creates a .gitignore with the one entry when there is none', () => {
+ assert.strictEqual(workspaceBootstrap.ensureGitignore(root), 'created');
+ assert.ok(read().includes('.hirayacoder/'));
+ // Deliberately not guessing at node_modules or dist for a project whose language
+ // nobody has looked at yet.
+ assert.ok(!read().includes('node_modules'));
+ });
+
+ it('appends to an existing file without touching what is already in it', () => {
+ fs.writeFileSync(path.join(root, '.gitignore'), 'node_modules\ndist\n', 'utf8');
+ assert.strictEqual(workspaceBootstrap.ensureGitignore(root), 'appended');
+
+ const contents = read();
+ assert.ok(contents.startsWith('node_modules\ndist\n'));
+ assert.ok(contents.includes('.hirayacoder/'));
+ });
+
+ it('does not join the entry onto a last line with no newline of its own', () => {
+ fs.writeFileSync(path.join(root, '.gitignore'), 'node_modules', 'utf8');
+ workspaceBootstrap.ensureGitignore(root);
+
+ const lines = read().split('\n').map((line) => line.trim()).filter(Boolean);
+ assert.ok(lines.includes('node_modules'), 'the existing entry was corrupted');
+ assert.ok(lines.includes('.hirayacoder/'));
+ });
+
+ it('keeps a CRLF file on CRLF', () => {
+ fs.writeFileSync(path.join(root, '.gitignore'), 'node_modules\r\n', 'utf8');
+ workspaceBootstrap.ensureGitignore(root);
+ assert.ok(!/[^\r]\n/.test(read()), 'a bare LF was introduced into a CRLF file');
+ });
+
+ it('adds nothing when the entry is already there', () => {
+ fs.writeFileSync(path.join(root, '.gitignore'), 'node_modules\n.hirayacoder\n', 'utf8');
+ assert.strictEqual(workspaceBootstrap.ensureGitignore(root), 'present');
+ assert.strictEqual(read(), 'node_modules\n.hirayacoder\n');
+ });
+
+ it('is idempotent across repeated sessions', () => {
+ workspaceBootstrap.ensureGitignore(root);
+ workspaceBootstrap.ensureGitignore(root);
+ workspaceBootstrap.ensureGitignore(root);
+
+ const occurrences = read().split('\n').filter((line) => line.trim() === '.hirayacoder/').length;
+ assert.strictEqual(occurrences, 1);
+ });
+});
+
+describe('workspaceBootstrap.bootstrap', () => {
+ it('does nothing at all without a workspace', () => {
+ assert.strictEqual(workspaceBootstrap.bootstrap(null), null);
+ });
+
+ it('leaves both the profile and the ignore entry behind', () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hiraya-bootstrap-'));
+ try {
+ const ready = workspaceBootstrap.bootstrap(root);
+
+ assert.ok(ready.profile.osName.length > 0);
+ assert.strictEqual(ready.gitignore, 'created');
+ assert.ok(fs.existsSync(path.join(root, '.hirayacoder', 'environment.json')));
+ assert.ok(fs.readFileSync(path.join(root, '.gitignore'), 'utf8').includes('.hirayacoder/'));
+ } finally {
+ fs.rmSync(root, { recursive: true, force: true });
+ }
+ });
+});
From d0ca3688c2d5a98f947d5a419d22d69900b68bbc Mon Sep 17 00:00:00 2001
From: jaymar921
Date: Fri, 14 Aug 2026 10:01:11 +0800
Subject: [PATCH 2/3] docs: two marketing images for the Marketplace listing
and the README
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Rendered from self-contained HTML in docs/images/src — no fonts, scripts, or images
are fetched and the app icon is inlined, so any Chromium produces the same output.
The HTML is the source of truth; regenerating on a version bump is a one-line edit
rather than a redesign, and the how and the what-to-update are in the sources' own
README.
Excluded from the .vsix. The listing pulls them by absolute raw.githubusercontent
URL exactly as it already does the icon, so there is no reason for a megabyte of PNG
to ride along in every install.
---
.vscodeignore | 5 +
README.md | 8 +
docs/images/capabilities.png | Bin 0 -> 474552 bytes
docs/images/hero-offline-agent.png | Bin 0 -> 378977 bytes
docs/images/src/README.md | 40 +++++
docs/images/src/capabilities.html | 152 ++++++++++++++++
docs/images/src/hero-offline-agent.html | 228 ++++++++++++++++++++++++
7 files changed, 433 insertions(+)
create mode 100644 docs/images/capabilities.png
create mode 100644 docs/images/hero-offline-agent.png
create mode 100644 docs/images/src/README.md
create mode 100644 docs/images/src/capabilities.html
create mode 100644 docs/images/src/hero-offline-agent.html
diff --git a/.vscodeignore b/.vscodeignore
index 0a76297..7bc3fc3 100644
--- a/.vscodeignore
+++ b/.vscodeignore
@@ -67,3 +67,8 @@ docs/assets/**
# The activity bar glyph. Referenced by `contributes.viewsContainers`, so leaving it out
# ships an extension whose sidebar icon is a blank square.
!docs/assets/activity-bar.svg
+
+# The marketing images and the HTML they are rendered from. README references them by
+# absolute raw.githubusercontent URL, exactly as it does the icon, so the Marketplace
+# listing renders them without a megabyte of PNG riding along in every install.
+docs/images/**
diff --git a/README.md b/README.md
index 137cafe..6f9cc61 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,10 @@
+
+
+
+
*A local Filipino-inspired AI coder that brings imagination and speed to your VS Code workflow.*
**HirayaCoder is a free AI coding assistant that runs entirely on your own computer.**
@@ -129,6 +133,10 @@ There is a longer, friendlier walkthrough in
## Using it day to day
+
+
+
+
### The three modes
There is a row of buttons at the top of the chat. You can ignore them at first —
diff --git a/docs/images/capabilities.png b/docs/images/capabilities.png
new file mode 100644
index 0000000000000000000000000000000000000000..697cc193b8aefe55d0b9be2db92e254a70f3a860
GIT binary patch
literal 474552
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