@nanhara/hara 0.122.6 → 0.123.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 +39 -0
- package/README.md +22 -6
- package/dist/agent/loop.js +3 -2
- package/dist/config.js +70 -11
- package/dist/context/workspace-scope.js +4 -4
- package/dist/index.js +206 -44
- package/dist/org-fleet/enroll.js +30 -4
- package/dist/security/guardian.js +13 -5
- package/dist/security/private-state.js +3 -1
- package/dist/serve/protocol.js +2 -1
- package/dist/serve/server.js +125 -50
- package/dist/serve/sessions.js +12 -7
- package/dist/session/store.js +32 -3
- package/dist/session/task.js +161 -0
- package/dist/tui/App.js +21 -9
- package/dist/tui/InputBox.js +61 -18
- package/dist/tui/bracketed-paste.js +265 -0
- package/dist/tui/run.js +8 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,45 @@ All notable changes to `@nanhara/hara`.
|
|
|
5
5
|
> Versioning (pre-1.0, SemVer-style): the **minor** (middle) number bumps for a **new feature**; the
|
|
6
6
|
> **patch** (last) number bumps for **optimizations/fixes of existing features**.
|
|
7
7
|
|
|
8
|
+
## 0.123.0 — 2026-07-15 — task-aware interaction and input/security hardening
|
|
9
|
+
|
|
10
|
+
- **Conversation and execution are separate state.** Sessions now persist a bounded, redacted task record
|
|
11
|
+
beside the transcript: stable task/turn IDs, the original objective, running/paused/completed/blocked
|
|
12
|
+
status, outcomes, and steering audit. A crashed running task recovers as paused/interrupted; the first
|
|
13
|
+
input after resume continues an unfinished objective instead of silently replacing it. Legacy sessions
|
|
14
|
+
without task state still load normally, and `/task` shows or clears only execution state while preserving
|
|
15
|
+
the conversation.
|
|
16
|
+
- **Mid-turn input targets an exact run.** TUI type-ahead carries an `expectedTurnId`, late input remains a
|
|
17
|
+
steer rather than becoming an accidental new task, and Desktop/serve exposes additive `session.steer`,
|
|
18
|
+
task/turn events, resumed task metadata, plus explicit `newTask: true`. Task context is injected separately
|
|
19
|
+
from transcript/project context so a conversational reply cannot redefine the active coding objective.
|
|
20
|
+
- **Terminal text paste follows bracketed-paste framing.** Hara enables terminal mode 2004, buffers split
|
|
21
|
+
`ESC[200~`/`ESC[201~` frames into one logical insert, handles immediate paste+Enter correctly, restores the
|
|
22
|
+
terminal on exit, and fails visibly on incomplete or over-2 MiB input. Multiline Claude output no longer
|
|
23
|
+
freezes, disappears, or requires Ctrl+C before it can be pasted.
|
|
24
|
+
- **Three reported path/alias escapes are closed.** Windows Guardian containment uses platform-aware relative
|
|
25
|
+
paths; global `~/.hara/config.json` reads reject symlink/hard-link aliases and writes use a private atomic
|
|
26
|
+
replacement without modifying an external inode; organization role sync accepts only portable role IDs
|
|
27
|
+
and verifies every final path remains directly below `~/.hara/org-roles`.
|
|
28
|
+
- **Home remains private without becoming inconvenient.** `hara --cwd /path/to/project` explicitly selects a
|
|
29
|
+
project as an alternative to `cd`, and every Home-boundary refusal now shows both choices. Hara deliberately
|
|
30
|
+
does not scan Home and guess a repository, avoiding ambiguous selection and renewed private-directory
|
|
31
|
+
inventory.
|
|
32
|
+
- **Cancellation gates no longer probe PID 0.** Computer/cron process-tree fixtures wait for a fully published
|
|
33
|
+
positive child PID before cancellation assertions, retaining the original strict settlement and process
|
|
34
|
+
disappearance deadlines under full-suite contention.
|
|
35
|
+
|
|
36
|
+
## 0.122.7 — 2026-07-15 — isolated build-context parity
|
|
37
|
+
|
|
38
|
+
- **Docker builds carry every package-build dependency.** The image build stage now copies the local-link
|
|
39
|
+
diagnostic script before `npm run build`, fixing the `MODULE_NOT_FOUND` failure that blocked the 0.122.6
|
|
40
|
+
Docker CI and GHCR multi-architecture image while npm and native standalone lanes succeeded.
|
|
41
|
+
- **The gate follows the build graph instead of one remembered filename.** Runtime packaging tests parse every
|
|
42
|
+
`node scripts/...` dependency from the package build command and require a matching Dockerfile `COPY`, so a
|
|
43
|
+
future build helper cannot pass host-only gates while disappearing from an isolated container context.
|
|
44
|
+
- `v0.122.6` remains immutable and is treated as a partial release; 0.122.7 supersedes it after npm, native
|
|
45
|
+
binaries, GitHub Release, and both GHCR architectures are independently verified.
|
|
46
|
+
|
|
8
47
|
## 0.122.6 — 2026-07-15 — resumed continuity and workspace/input hardening
|
|
9
48
|
|
|
10
49
|
- **Resume continues the persisted task instead of rediscovering the workspace.** CLI `--resume`/`-c`,
|
package/README.md
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
- **Multi-provider, all streamed** — Anthropic (Claude) or any OpenAI-compatible endpoint (Qwen/DashScope, GLM, Kimi, OpenAI) with live Markdown + visible reasoning.
|
|
19
19
|
- **Delegate to other agents** — the **`external_agent`** tool hands a self-contained task to **Claude Code** or **Codex** running headless, and returns the result — so you pick the best engine per task. It is a trusted extension outside Hara's protected-file boundary: every interactive call requires confirmation, and non-interactive use is disabled by default.
|
|
20
20
|
- **Honest under a slow network** — a live "waiting for the model… Ns" status, a stall watchdog that
|
|
21
|
-
auto-fails-over instead of hanging, big pastes folding to a token, and a
|
|
22
|
-
terminal never feels dead.
|
|
21
|
+
auto-fails-over instead of hanging, terminal-native bracketed paste, big pastes folding to a token, and a
|
|
22
|
+
startup update notice — the terminal never feels dead.
|
|
23
23
|
- **Solid coding core** — `edit_file` / `apply_patch` (atomic multi-file) with colored diffs · `grep`/`glob`/`ls`/`codebase_search` (lexical + optional semantic search over the repo) /`web_fetch` · fuzzy `@file` · `/undo` · `/compact` · **Esc-to-interrupt** · parallel sub-agents · MCP client · macOS sandbox.
|
|
24
24
|
|
|
25
25
|
Track it: https://github.com/hara-cli/hara · https://hara.run
|
|
@@ -176,6 +176,7 @@ hara -p "extract package metadata" --schema ./schema.json # stdout is ex
|
|
|
176
176
|
hara -p "review the current diff" --role reviewer # persona + model + tool policy from the role
|
|
177
177
|
hara --approval auto-edit # suggest (default) | auto-edit | full-auto (-y = full-auto)
|
|
178
178
|
hara --sandbox workspace-write # confine shell writes to the project (macOS Seatbelt)
|
|
179
|
+
hara --cwd /path/to/project # explicitly select a workspace without changing your shell directory
|
|
179
180
|
hara -c # resume the most recent session in this directory
|
|
180
181
|
hara --profile work # use a named profile from ~/.hara/config.json
|
|
181
182
|
hara -m glm-5 # pick a model
|
|
@@ -183,7 +184,8 @@ hara -m glm-5 # pick a model
|
|
|
183
184
|
|
|
184
185
|
Run Hara from a project directory. When the current directory resolves to your Home root, Hara does not treat
|
|
185
186
|
the whole Home tree as a repository: project init/index and default recursive grep/glob/codebase inventory are
|
|
186
|
-
disabled with a `cd /path/to/project` hint. Directory listing and
|
|
187
|
+
disabled with a `cd /path/to/project` / `hara --cwd /path/to/project` hint. Directory listing and
|
|
188
|
+
child-directory recursion are also refused so
|
|
187
189
|
the model cannot promote a discovered Home folder into an implicit project; explicitly named single files still
|
|
188
190
|
work. Resuming a session reuses its persisted conversation and must not trigger workspace rediscovery.
|
|
189
191
|
|
|
@@ -193,7 +195,10 @@ stderr and missing/invalid output exits non-zero. `--role reviewer` resolves loc
|
|
|
193
195
|
uses the portable global persona in the current project, and `--role shop:reviewer` runs at that registered
|
|
194
196
|
project home. Each form enforces the role's persona, model, `allowTools`/`denyTools`, and `readOnly` policy.
|
|
195
197
|
|
|
196
|
-
Inside the REPL: `/help` `/init` `/tools` `/model` `/approval` `/org` `/plan` `/roles` `/
|
|
198
|
+
Inside the REPL: `/help` `/init` `/tools` `/model` `/approval` `/org` `/plan` `/roles` `/task` `/usage`
|
|
199
|
+
`/doctor` `/sessions` `/undo` `/compact` `/recall` `/reset` `/exit` (type `/`+Tab to complete). `/task`
|
|
200
|
+
shows the active execution record; `/task clear` drops it without deleting the conversation. Type `@` + Tab
|
|
201
|
+
to attach a file (fuzzy, walks subdirectories).
|
|
197
202
|
|
|
198
203
|
The interactive REPL is an **ink TUI**: a bordered **input box pinned at the bottom** — session name in
|
|
199
204
|
the top-right corner, approval modes + token usage + concurrency in the bottom border — with the
|
|
@@ -205,6 +210,10 @@ token inline where your cursor is (backspace over it to remove it). hara auto-de
|
|
|
205
210
|
a vision model sees the image directly; a text-only model routes it through a `visionModel` describer (see
|
|
206
211
|
Setup), shown in the header at startup. Set `HARA_TUI=0` for the classic readline REPL.
|
|
207
212
|
|
|
213
|
+
Text pasted by a modern terminal is handled as one bracketed-paste event, including multiline Claude/Codex
|
|
214
|
+
output and a paste immediately followed by Enter. It is inserted for review and never auto-submitted merely
|
|
215
|
+
because it contains newlines; malformed/incomplete frames are bounded and surfaced instead of hanging raw mode.
|
|
216
|
+
|
|
208
217
|
Each session gets a **UUID** and an **auto-summarized name** from your first message (kept verbatim, CJK
|
|
209
218
|
included); `hara sessions` lists them by short id, and `--resume <prefix>` accepts the short id.
|
|
210
219
|
|
|
@@ -246,7 +255,14 @@ only changed files re-embed (a full repo rebuild that takes ~a minute re-runs in
|
|
|
246
255
|
`hara config set computerUse read|click|full` and allowlist apps with `hara config set computerApps "App, …"`. Guarded
|
|
247
256
|
by the tier, the frontmost-app allowlist, a dangerous-key blocklist, and a once-per-session grant. Screenshots are read via your
|
|
248
257
|
vision model into **actionable** output — interactive elements + positions (pass `focus` to target what you're after) — so even a text-only main model can click.
|
|
249
|
-
**Sessions**: conversations are saved automatically — `-c` / `--resume <id>` or
|
|
258
|
+
**Sessions and task execution**: conversations are saved automatically — `-c` / `--resume <id>` or
|
|
259
|
+
`hara resume <id>` to continue, `hara sessions` to list, `hara export [id] [--out file]` to render one as a
|
|
260
|
+
Markdown transcript. The current task is persisted separately with stable task/turn identity and resumes as
|
|
261
|
+
paused rather than being inferred from chat text; `/task` inspects it and `/task clear` leaves the transcript
|
|
262
|
+
intact. Type-ahead input steers the exact live turn. Desktop/serve clients can use `session.steer` with
|
|
263
|
+
`expectedTurnId`, and may pass `newTask: true` to `session.send` when an unfinished resumed task should be
|
|
264
|
+
replaced explicitly. The `hara resume` launcher preserves terminal input in both npm/Node and standalone-binary
|
|
265
|
+
installs.
|
|
250
266
|
**MCP**: add an `mcpServers` map to global config (a reviewed project config additionally needs `HARA_TRUST_PROJECT_CONFIG=1` at launch); their tools appear to the agent as `mcp__<server>__<tool>`. Configured MCP servers, like `external_agent`, are trusted host extensions outside Hara's protected-file boundary. Every interactive tool call requires confirmation (even in `full-auto`), and non-interactive runs disable them by default; reviewed automation can explicitly opt in before launch with `HARA_ALLOW_TRUSTED_EXTENSIONS=1`. hara can also **be** an MCP server — `hara mcp` exposes its read/search tools (esp. **`codebase_search`**) over stdio so other clients (Claude Desktop, Cursor, another hara) can use them; read-only by default (`HARA_MCP_TOOLS` to override).
|
|
251
267
|
**Vim mode**: `hara config set vimMode true` makes the prompt modal — Esc → normal, `i/a/A/I` insert, `h l 0 $ w b e` motions, `x D C dd cw p` edits. Off by default.
|
|
252
268
|
**Scheduled tasks**: `hara cron add "0 9 * * 1-5" "<task>"` (or `"every 30m"`, `"in 2h"`) runs a task on a schedule — each run is a fresh hara session. `hara cron install` wires a per-minute tick into launchd/crontab (no daemon); `--org` routes through the role org. Manage with `hara cron list/run/enable/disable/remove/logs`. Every job has a 30-minute deadline and the whole sequential tick has a non-renewable 60-minute watchdog: a job timeout kills its process tree, records `timed out` + duration/error, then continues with the next due job; a tick timeout stops the remainder and releases the global lock. Add `--deliver feishu:<chatId>` (or Telegram/webhook) for outcomes and `--alert-after N` for the consecutive-failure 🚨 threshold (default 3). Delivery intent is durable before transport, uses a stable idempotency key, and retries with bounded backoff on later ticks until confirmed. A failed channel cannot grow `jobs.json` forever: each job keeps at most 64 pending effects, reserves outcome/alert room before launch, and disables itself with a visible backlog error when full; restore delivery, let the queue drain, then re-enable it. Tune milliseconds with `HARA_CRON_JOB_TIMEOUT_MS` (hard max 24h) and `HARA_CRON_TICK_TIMEOUT_MS` (hard max 5h); scheduled jobs are also capped by the tick. After upgrading from a version whose tick is already stuck, terminate that specific legacy `hara cron tick` process tree once (or reboot); the next scheduler minute marks over-age state interrupted/disabled and recovers the lock without replaying a possibly orphaned task.
|
|
@@ -321,7 +337,7 @@ turn, or **`/undo`** to revert the last edit. In-session **`/diff`**, **`/review
|
|
|
321
337
|
- **Type-ahead steering**: keep typing while hara works — your message is held, then **folded into the next model call** (not deferred to a new turn), so a clarification or "also do X" course-corrects the task already in flight (codex-style). Messages typed after the final step start a fresh turn; **Esc** drops the queue and stops.
|
|
322
338
|
- **Project context**: auto-loads `AGENTS.md` (the cross-tool standard) walking up to the repo root; `hara init` writes one by analyzing the repo.
|
|
323
339
|
- **`@file` mentions**: attach file contents to a message (`@path`); Tab-completes with a **fuzzy** matcher over the project (subdirs, git-tracked + untracked) — `@idx` → `src/index.ts`. `@<dir>` loads a directory listing, `@src/`+Tab drills into a folder, and mistyped tool/file paths get a "did you mean" suggestion.
|
|
324
|
-
- **Explicit workspace boundary**: launching at Home does not inventory its directories or permit coding mutations. Start Hara from a concrete project to enable search, `@` completion, shell/external agents, and file edits; explicitly named single-file reads remain available at Home.
|
|
340
|
+
- **Explicit workspace boundary**: launching at Home does not inventory its directories or permit coding mutations. Start Hara from a concrete project, or pass `hara --cwd /path/to/project`, to enable search, `@` completion, shell/external agents, and file edits; explicitly named single-file reads remain available at Home.
|
|
325
341
|
- **Multi-provider**: Anthropic (Claude) or any OpenAI-compatible endpoint (Qwen/DashScope, GLM, Kimi, OpenAI) — **all streamed live**.
|
|
326
342
|
- **Chat gateway**: drive your local hara from **Telegram · WeChat · Discord · Feishu/Lark · Slack · Mattermost · Matrix · DingTalk · WeCom · Signal**. The daemon connects out (no public webhook), with per-chat sessions, project roaming (`/cd`), agent switching (`/agent`), and **two-way images on byte-upload-capable platforms**. Setup, platform capability details, and the group-flow security model: **[docs/gateway.md](docs/gateway.md)**.
|
|
327
343
|
|
package/dist/agent/loop.js
CHANGED
|
@@ -132,12 +132,13 @@ const CONTINUATION_SYSTEM = "# Existing-session continuity\n" +
|
|
|
132
132
|
"re-inventory the workspace, or summarize files merely to understand what happened before. Follow the latest user request " +
|
|
133
133
|
"and reuse prior conclusions and tool results. Inspect files only when the latest request requires it. If the working " +
|
|
134
134
|
"directory is Home, ask the user to start Hara from a concrete project instead of enumerating Home or its children.";
|
|
135
|
-
function composeSystem(cwd, projectContext, override, memory, continuationSession = false) {
|
|
135
|
+
function composeSystem(cwd, projectContext, override, memory, continuationSession = false, executionContext) {
|
|
136
136
|
const head = override ? `${override}\n\nWorking directory: ${cwd}` : HARA_SYSTEM(cwd);
|
|
137
137
|
const skills = skillsDigest(cwd);
|
|
138
138
|
return (head +
|
|
139
139
|
gatewayNote() +
|
|
140
140
|
(continuationSession ? `\n\n${CONTINUATION_SYSTEM}` : "") +
|
|
141
|
+
(executionContext ? `\n\n${executionContext}` : "") +
|
|
141
142
|
(projectContext ? `\n\n# Project context (AGENTS.md)\n${projectContext}` : "") +
|
|
142
143
|
(memory ? `\n\n# Memory (durable — facts/decisions/prefs you've saved; use memory_search/get for more)\n${memory}` : "") +
|
|
143
144
|
(skills ? `\n\n# Skills (capabilities you can load — call the \`skill\` tool with the id for full instructions before using one)\n${skills}` : ""));
|
|
@@ -427,7 +428,7 @@ async function runAgentInner(history, opts, life) {
|
|
|
427
428
|
return { text: "", toolUses: [], stop: "error", errorMsg: "interrupted" };
|
|
428
429
|
}
|
|
429
430
|
return activeProvider.turn({
|
|
430
|
-
system: composeSystem(ctx.cwd, opts.projectContext, opts.systemOverride, opts.memory, opts.continuationSession),
|
|
431
|
+
system: composeSystem(ctx.cwd, opts.projectContext, opts.systemOverride, opts.memory, opts.continuationSession, opts.executionContext),
|
|
431
432
|
history,
|
|
432
433
|
tools: specs,
|
|
433
434
|
// Any stream chunk keeps the connection considered alive — even suppressed reasoning_content, so a
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { homedir } from "node:os";
|
|
2
3
|
import { join, dirname, resolve } from "node:path";
|
|
3
|
-
import {
|
|
4
|
+
import { closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
4
5
|
import { agentMaxRounds, agentRunTimeoutMs } from "./agent/limits.js";
|
|
5
6
|
import { ensurePrivateHaraState } from "./security/private-state.js";
|
|
6
7
|
import { readVerifiedRegularFileSnapshotSync } from "./fs-read.js";
|
|
@@ -41,6 +42,7 @@ export const APPROVAL_MODES = ["suggest", "auto-edit", "full-auto"];
|
|
|
41
42
|
export const SANDBOX_MODES = ["off", "workspace-write", "read-only"];
|
|
42
43
|
const PROJECT_ROOT_MARKERS = [".git", "package.json", "Cargo.toml", "go.mod", "pyproject.toml", ".hg"];
|
|
43
44
|
const MAX_PROJECT_CONFIG_BYTES = 256 * 1024;
|
|
45
|
+
const MAX_GLOBAL_CONFIG_BYTES = 1024 * 1024;
|
|
44
46
|
const KNOWN_CONFIG_KEYS = new Set([
|
|
45
47
|
...CONFIG_KEYS,
|
|
46
48
|
"hooks", "mcpServers", "modelVision", "overlays", "profiles",
|
|
@@ -111,7 +113,12 @@ export function readRawConfig() {
|
|
|
111
113
|
if (!existsSync(p))
|
|
112
114
|
return {};
|
|
113
115
|
try {
|
|
114
|
-
|
|
116
|
+
const snapshot = readVerifiedRegularFileSnapshotSync(p, MAX_GLOBAL_CONFIG_BYTES, {
|
|
117
|
+
action: "read global config",
|
|
118
|
+
protectSensitive: false,
|
|
119
|
+
rejectHardLinks: true,
|
|
120
|
+
});
|
|
121
|
+
return configRecord(JSON.parse(snapshot.text));
|
|
115
122
|
}
|
|
116
123
|
catch {
|
|
117
124
|
return {};
|
|
@@ -232,20 +239,72 @@ function readProjectConfig(cwd) {
|
|
|
232
239
|
}
|
|
233
240
|
return {};
|
|
234
241
|
}
|
|
235
|
-
|
|
242
|
+
function existingConfigIdentity(path) {
|
|
243
|
+
try {
|
|
244
|
+
const info = lstatSync(path);
|
|
245
|
+
if (info.isSymbolicLink())
|
|
246
|
+
throw new Error(`refusing global config write: '${path}' is a symbolic link`);
|
|
247
|
+
if (!info.isFile())
|
|
248
|
+
throw new Error(`refusing global config write: '${path}' is not a regular file`);
|
|
249
|
+
if (info.nlink !== 1)
|
|
250
|
+
throw new Error(`refusing global config write: '${path}' is hard-linked`);
|
|
251
|
+
return { dev: info.dev, ino: info.ino };
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
if (error?.code === "ENOENT")
|
|
255
|
+
return null;
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** Atomically replace the global 0600 config without opening its current directory entry for write. */
|
|
236
260
|
function persistConfig(p, cfg) {
|
|
237
261
|
ensurePrivateHaraState();
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
262
|
+
const parent = dirname(p);
|
|
263
|
+
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
264
|
+
const parentInfo = lstatSync(parent);
|
|
265
|
+
const canonicalParent = realpathSync.native(parent);
|
|
266
|
+
if (!parentInfo.isDirectory() || parentInfo.isSymbolicLink()) {
|
|
267
|
+
throw new Error(`refusing global config write: '${parent}' is not a canonical directory`);
|
|
241
268
|
}
|
|
242
|
-
|
|
243
|
-
|
|
269
|
+
const existing = existingConfigIdentity(p);
|
|
270
|
+
const temp = join(parent, `.config-${process.pid}-${randomUUID()}.tmp`);
|
|
271
|
+
const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
|
|
272
|
+
let fd;
|
|
244
273
|
try {
|
|
245
|
-
|
|
274
|
+
fd = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollow, 0o600);
|
|
275
|
+
writeFileSync(fd, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
276
|
+
fsyncSync(fd);
|
|
277
|
+
const staged = fstatSync(fd);
|
|
278
|
+
if (!staged.isFile() || staged.nlink !== 1)
|
|
279
|
+
throw new Error("refusing global config write: unsafe staging inode");
|
|
280
|
+
closeSync(fd);
|
|
281
|
+
fd = undefined;
|
|
282
|
+
const parentAfter = lstatSync(parent);
|
|
283
|
+
if (!parentAfter.isDirectory() || parentAfter.isSymbolicLink() ||
|
|
284
|
+
parentAfter.dev !== parentInfo.dev || parentAfter.ino !== parentInfo.ino ||
|
|
285
|
+
realpathSync.native(parent) !== canonicalParent)
|
|
286
|
+
throw new Error(`refusing global config write: '${parent}' changed before commit`);
|
|
287
|
+
const current = existingConfigIdentity(p);
|
|
288
|
+
if ((existing === null) !== (current === null) || (existing && current && (existing.dev !== current.dev || existing.ino !== current.ino))) {
|
|
289
|
+
throw new Error(`refusing global config write: '${p}' changed before commit`);
|
|
290
|
+
}
|
|
291
|
+
// rename replaces the path entry; it never follows an alias planted after the last identity check.
|
|
292
|
+
renameSync(temp, p);
|
|
293
|
+
const committed = lstatSync(p);
|
|
294
|
+
if (!committed.isFile() || committed.isSymbolicLink() || committed.dev !== staged.dev || committed.ino !== staged.ino || committed.nlink !== 1) {
|
|
295
|
+
throw new Error(`global config changed during commit: '${p}'`);
|
|
296
|
+
}
|
|
246
297
|
}
|
|
247
|
-
|
|
248
|
-
|
|
298
|
+
finally {
|
|
299
|
+
if (fd !== undefined)
|
|
300
|
+
try {
|
|
301
|
+
closeSync(fd);
|
|
302
|
+
}
|
|
303
|
+
catch { /* preserve original error */ }
|
|
304
|
+
try {
|
|
305
|
+
rmSync(temp, { force: true });
|
|
306
|
+
}
|
|
307
|
+
catch { /* preserve original error */ }
|
|
249
308
|
}
|
|
250
309
|
}
|
|
251
310
|
export function writeConfigValue(key, value) {
|
|
@@ -28,17 +28,17 @@ export function recursiveRootContainsHome(root, home = homedir()) {
|
|
|
28
28
|
}
|
|
29
29
|
export function homeWorkspaceActionError(action) {
|
|
30
30
|
return (`Refusing to ${action} from the home directory: it is not an implicit project workspace. ` +
|
|
31
|
-
"Run `cd /path/to/project` first
|
|
31
|
+
"Run `cd /path/to/project` first, or launch with `hara --cwd /path/to/project`.");
|
|
32
32
|
}
|
|
33
33
|
export function recursiveHomeSearchError(tool) {
|
|
34
34
|
return (`Error: ${tool} will not recursively scan the home directory. ` +
|
|
35
|
-
"Run Hara from a project (`cd /path/to/project`). Explicit single-file reads remain available.");
|
|
35
|
+
"Run Hara from a project (`cd /path/to/project` or `hara --cwd /path/to/project`). Explicit single-file reads remain available.");
|
|
36
36
|
}
|
|
37
37
|
/** A model rooted at Home must not discover a child and silently promote it into the project scope. The
|
|
38
38
|
* user establishes that scope by launching Hara from the concrete project directory. */
|
|
39
39
|
export function homeWorkspaceDirectoryScanError(tool) {
|
|
40
40
|
return (`Error: ${tool} will not enumerate or recursively scan directories while Hara is rooted at the home directory. ` +
|
|
41
|
-
"Run `cd /path/to/project`
|
|
41
|
+
"Run `cd /path/to/project` or relaunch with `hara --cwd /path/to/project`. Explicit single-file reads remain available.");
|
|
42
42
|
}
|
|
43
43
|
/** Injected into model context when Hara was intentionally launched at ~/. Runtime checks enforce the same
|
|
44
44
|
* policy, but guidance avoids wasting turns on calls that are guaranteed to be rejected. */
|
|
@@ -49,5 +49,5 @@ export function homeWorkspaceGuidance(cwd) {
|
|
|
49
49
|
"The working directory resolves to the user's home directory, which is not an implicit project. " +
|
|
50
50
|
"Do not initialize a project, create or modify files, build a repository index, run shell/external " +
|
|
51
51
|
"executable tools, enumerate directories, or grep/glob/search Home or one of its child directories. " +
|
|
52
|
-
"Ask the user to run `cd /path/to/project` for project work. Only explicitly named single-file reads remain available.");
|
|
52
|
+
"Ask the user to run `cd /path/to/project` or `hara --cwd /path/to/project` for project work. Only explicitly named single-file reads remain available.");
|
|
53
53
|
}
|