geneseed 4.9.3 → 4.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,16 @@ label. For the capability ↔ spec map, see [SHIPPED.md](SHIPPED.md).
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [4.9.4] — 2026-10-18
12
+
13
+ Patch: `git reset --hard` asks only when it would abandon existing commits, not over uncommitted work.
14
+
15
+ ### Upgrade notes
16
+ - Run `geneseed rebuild-all` once after updating so installed hooks, the OpenCode guard and Law IV text pick up the change.
17
+
18
+ ### Changed
19
+ - **`git reset --hard` asks only when it would abandon commits, not over uncommitted work** (user decision: what is in progress may break, what exists must be protected). A dirty tree reset with `git reset --hard` or `git reset --hard HEAD` now passes on every host. `git reset --hard <target>` asks (exit 2 on Bob, a block on OpenCode) when HEAD reaches a commit that neither `<target>` nor any remote branch holds — `HEAD~1` over an unpushed commit, `origin/main` behind local work — on any branch, and passes when those commits are pushed. A target the gate cannot resolve (`nosuchref`, `"$REF"`, `@{u}`) asks, saying it could not verify. Every other discard (`checkout -f`, `checkout -- <paths>`, `switch --discard-changes`, `restore`, `clean -f`, `stash drop`/`clear`) is unchanged, as are `reset --soft`/`--mixed`/`--keep`/`--merge`. Law IV (*Deletion Is Deliberate*) names the exception in both halves so the agent does not ask out of respect for the law. Run `geneseed rebuild-all` to pick it up.
20
+
11
21
  ## [4.9.3] — 2026-10-18
12
22
 
13
23
  Patch: rm -rf / and other catastrophic commands are caught on every host even with the deletion gate off; a skill you replaced with a link reads as yours, not as an edit.
package/README.md CHANGED
@@ -37,7 +37,7 @@ Want to read what it leaves behind before installing anything? [**geneseed-demo*
37
37
 
38
38
  A hand-written instructions file is prose the model may or may not honour, copied into every repo and drifting in each one. Geneseed is a build, and that changes four things:
39
39
 
40
- - **Laws are enforced, not suggested.** A force-push to `main`, a `reset --hard` over uncommitted work, or a credential written into a tracked file is caught by a hook *before* the tool runs, in the host's own dialect — a prompt on Claude Code and OpenClaude, a hard block on OpenCode, exit 2 on Bob. The gates fail closed, and every catch is one line in a ledger that `geneseed status` counts.
40
+ - **Laws are enforced, not suggested.** A force-push to `main`, a `reset --hard` that abandons unpushed commits, or a credential written into a tracked file is caught by a hook *before* the tool runs, in the host's own dialect — a prompt on Claude Code and OpenClaude, a hard block on OpenCode, exit 2 on Bob. The gates fail closed, and every catch is one line in a ledger that `geneseed status` counts.
41
41
  - **One source, five targets.** Skills are byte-identical on every host; only the wiring differs. Every commit renders all 263 emit configurations, and a second emit into the same tree must change nothing.
42
42
  - **Costs are measured, not guessed.** Zero runtime dependencies. The hook path loads in about 14 ms per tool call. The default *lean* footprint keeps the always-on context small and puts the full rationale one read away — the numbers are in [docs/token-footprint.md](docs/token-footprint.md).
43
43
  - **It follows you.** Install once, globally; every repo inherits it. One `git pull` or `npm install -g geneseed@latest` rebuilds every active install.
package/SHIPPED.md CHANGED
@@ -20,7 +20,7 @@ historical references, not files distributed with the repo.
20
20
  > **On version numbers.** [`harness.config.json`](harness.config.json)'s `version` is a
21
21
  > human-readable label only. The canonical identity of an installed harness is the
22
22
  > **source fingerprint** stamped in `.geneseed-version` (see `geneseed version`), not the
23
- > string. Current label: **4.9.3**.
23
+ > string. Current label: **4.9.4**.
24
24
 
25
25
  ## Capabilities
26
26
 
@@ -19,7 +19,7 @@ absolute-path hooks instead — `geneseed setup` does that for you.) It:
19
19
  destructive-git Rule. Drop the `process` pack (or exclude process 5) and the hook stays,
20
20
  wired with `--no-consent`: commits and pushes pass without asking, while the
21
21
  Law IV refusals (asked only on real data loss: a force-push to `main`, a `reset --hard`
22
- over uncommitted work, an `rm` git cannot undo) stay in every build unless the install is
22
+ that abandons unpushed commits, an `rm` git cannot undo) stay in every build unless the install is
23
23
  built `--no-deletion-gate`, which appends that flag and stands the real-loss check down
24
24
  (`--no-secrets-gate` does the same for `rule-gate`'s credential scan, `--no-done-gate` for
25
25
  `learn`'s done check). The hook inspects
@@ -577,6 +577,25 @@ function branchAct(rest, dir) {
577
577
  return null;
578
578
  }
579
579
 
580
+ /**
581
+ * `git reset --hard [<target>]` — asks only when it would abandon COMMITS: one HEAD reaches that
582
+ * neither `<target>` nor any remote ref does. The uncommitted work it discards passes: what is in
583
+ * progress may break, what exists must be protected (user decision, 2026-10-18). No target is
584
+ * HEAD itself, which abandons nothing.
585
+ */
586
+ function resetHardAct(rest, dir) {
587
+ const dd = rest.indexOf('--');
588
+ const target = (dd < 0 ? rest : rest.slice(0, dd)).find((a) => !a.startsWith('-'));
589
+ if (target === undefined) return null;
590
+ if (target.includes(OPAQUE)) unverified(`the target \`${target.replace(OPAQUE, '')}\` expands in the shell`);
591
+ repoRoot(dir);
592
+ const t = git(dir, ['rev-parse', '-q', '--verify', `${target}^{commit}`]);
593
+ if (t.status !== 0) unverified(`cannot resolve ${target} to a commit`);
594
+ const r = git(dir, ['rev-list', '-n1', 'HEAD', '--not', t.stdout.trim(), '--remotes']);
595
+ if (r.status !== 0) unverified(`cannot compare HEAD with ${target}: ${firstLine(r.stderr)}`);
596
+ return r.stdout.trim() ? `\`git reset --hard ${target}\` would abandon commits no remote and not ${target} holds` : null;
597
+ }
598
+
580
599
  /** `git clean` — forced, it asks when `git clean -n` with the same selection lists anything. */
581
600
  function cleanAct(rest, dir) {
582
601
  const forced = rest.some((a) => a === '--force' || hasShort(a, 'f'));
@@ -618,7 +637,7 @@ function gitAct(args, dir, always) {
618
637
  const act = () => {
619
638
  switch (verb) {
620
639
  case 'reset':
621
- return rest.includes('--hard') ? dirty(d, [], '`git reset --hard`') : null;
640
+ return rest.includes('--hard') ? resetHardAct(rest, d) : null;
622
641
  case 'checkout': {
623
642
  const forced = rest.some((a) => a === '--force' || hasShort(a, 'f')) && dirty(d, [], '`git checkout --force`');
624
643
  if (forced) return forced;
@@ -22,7 +22,7 @@ silently guarding the wrong rule after a renumber. See [ADR 0005](../adr/0005-st
22
22
  | id | Pack | Claude Code | OpenClaude | OpenCode | Bob |
23
23
  |---|---|---|---|---|---|
24
24
  | `sealed-secrets` | law | ask: rule-gate (credential-shaped content outside `.env*`). Switchable per install with `--no-secrets-gate` (see [Exclusions](exclusions.md)): this scan stands down on every host, the guard plugin's path block included | same as Claude Code | block: guard plugin (key/credential file path, or vendor-prefixed secret content); warns only on `.env*` | block (exit 2) |
25
- | `deletion-is-deliberate` | law | ask: git-gate, only on real data loss judged against the repository's state (the list in [Hooks](hooks.md): a protected branch deleted or force-pushed, an unpushed unmerged branch deleted, uncommitted work discarded, a stash dropped, the reflog pruned, files git cannot restore removed); asks when it cannot verify. Switchable per install with `--no-deletion-gate` (see [Exclusions](exclusions.md)): the real-loss check and the Law IV `permission.bash` keys stand down, everything else in this table stays **+** ask: git-gate, catastrophic shell (`rm -rf` of `/`, a drive root or `~`, `mkfs`, `dd` over a raw disk, `format`, fork bomb), whatever the state and even with the switch off | same as Claude Code | ask: permission prompt (`git reflog expire*`, `git gc --prune*`, a force push or delete of `main`/`master` by name) **+** block: guard plugin (catastrophic shell, the hook's check plus its own stricter text match: `rm -rf` of `/`, a drive root or `~`, fork bomb, `format`, `dd` over a raw disk) **+** block: guard plugin (every other real-loss case, the same state-aware decisions as the hook) | block (exit 2) |
25
+ | `deletion-is-deliberate` | law | ask: git-gate, only on real data loss judged against the repository's state (the list in [Hooks](hooks.md): a protected branch deleted or force-pushed, an unpushed unmerged branch deleted, unpushed commits abandoned by `reset --hard`, uncommitted work discarded other than by `reset --hard`, a stash dropped, the reflog pruned, files git cannot restore removed); asks when it cannot verify. Switchable per install with `--no-deletion-gate` (see [Exclusions](exclusions.md)): the real-loss check and the Law IV `permission.bash` keys stand down, everything else in this table stays **+** ask: git-gate, catastrophic shell (`rm -rf` of `/`, a drive root or `~`, `mkfs`, `dd` over a raw disk, `format`, fork bomb), whatever the state and even with the switch off | same as Claude Code | ask: permission prompt (`git reflog expire*`, `git gc --prune*`, a force push or delete of `main`/`master` by name) **+** block: guard plugin (catastrophic shell, the hook's check plus its own stricter text match: `rm -rf` of `/`, a drive root or `~`, fork bomb, `format`, `dd` over a raw disk) **+** block: guard plugin (every other real-loss case, the same state-aware decisions as the hook) | block (exit 2) |
26
26
  | `persist-insight` | process | ask: rule-gate (a write to `user-rules.md` or the memory store) | same as Claude Code | ask once: guard plugin (refuses the *first* write to that path; a re-issued write goes through) | log only (warning, call goes through) |
27
27
  | `consent-before-push` | process | ask: git-gate (every `git commit`/`git push`). Togglable: build without the process pack, or drop this rule alone with `--exclude-rules` (see [Exclusions](exclusions.md)). Only this question drops; the `deletion-is-deliberate` gate stays. | same as Claude Code | ask: permission prompt (`git commit*`/`git push*`), wired only when the process pack is on; a `loop/*` push is statically allowed | log only |
28
28
  | `external-gate` | rigor | ask: rule-gate (a write under a project's `.geneseed/protected-checks.txt` list) | same as Claude Code | block: guard plugin (same list) | block (exit 2) |
@@ -32,12 +32,13 @@ Geneseed writes its hooks into the host's settings file. Every hook calls one pr
32
32
  - **Real data loss** gets a confirmation prompt citing *Deletion Is Deliberate* — and only real loss, judged against the repository's actual state:
33
33
  - deleting or force-pushing a protected branch (`main`, `master`, or a remote's default branch), locally or on the remote;
34
34
  - `branch -D` of a branch whose commits are on no remote branch and not merged into the current one;
35
- - discarding uncommitted work (`reset --hard`, `checkout -f`, `checkout -- <paths>`, `switch --discard-changes`, `restore`, `clean -f`) when the tree actually holds some;
35
+ - `reset --hard <target>` abandoning commits that neither the target nor any remote branch holds (an unresolvable target asks too);
36
+ - discarding uncommitted work (`checkout -f`, `checkout -- <paths>`, `switch --discard-changes`, `restore`, `clean -f`) when the tree actually holds some;
36
37
  - on a protected branch only: `checkout <paths>` without `--` and `git rm -f` over changed files while `main`, `master` or the default branch is checked out, and `branch -f`/`checkout -B`/`switch -C` moving a protected branch off commits no remote and not the new target holds;
37
38
  - `stash drop`/`clear` while a stash exists; `reflog expire` and `gc --prune`, always;
38
39
  - removing files (`rm`, `rmdir`, `del`, `erase`, `Remove-Item`) that git cannot restore: untracked, ignored or modified files, a repository's `.git`, or anything outside a repository.
39
40
 
40
- A force push to a feature branch, the protected-branch-only forms above on a feature branch, a new branch (`checkout -b`, `switch -c`), `reset --hard` on a clean tree, `branch -D` of a pushed branch, or `rm` of a tracked, unmodified file passes. When the gate cannot tell — a glob or `$variable` it would have to expand, a `cd` it cannot follow, `sh -c`/`xargs`, git failing or missing — it asks, and says it could not verify. To read the state it runs `git` itself, only once a command already looked destructive; every other command pays nothing. That is a Rule, so it holds in every build — unless you switch its gate off for one install with `--no-deletion-gate` (or the switch on Rule IV in the console's Constitution page). The hook is then wired with `--no-deletion-gate` and this check stands down; the Rule's text still binds the agent, and the catastrophic-command check above and the root-scan and consent checks below still run ([Exclusions](exclusions.md)).
41
+ A force push to a feature branch, the protected-branch-only forms above on a feature branch, a new branch (`checkout -b`, `switch -c`), `reset --hard` over uncommitted work or to a commit that keeps every unpushed one, `branch -D` of a pushed branch, or `rm` of a tracked, unmodified file passes. When the gate cannot tell — a glob or `$variable` it would have to expand, a `cd` it cannot follow, `sh -c`/`xargs`, git failing or missing — it asks, and says it could not verify. To read the state it runs `git` itself, only once a command already looked destructive; every other command pays nothing. That is a Rule, so it holds in every build — unless you switch its gate off for one install with `--no-deletion-gate` (or the switch on Rule IV in the console's Constitution page). The hook is then wired with `--no-deletion-gate` and this check stands down; the Rule's text still binds the agent, and the catastrophic-command check above and the root-scan and consent checks below still run ([Exclusions](exclusions.md)).
41
42
  - **Every `git commit` and `git push`** gets a prompt citing *Consent Before Push*. This part belongs to the **process** doctrine pack: build without it, or exclude that rule, and the hook stays wired with `--no-consent` — commits and pushes go through without asking, real data loss is still stopped ([Rules](rules.md)).
42
43
  - **A recursive scan of a whole filesystem** — `find /`, `du -sh /`, `grep -r foo /`, `rg x C:\`, `Get-ChildItem C:\ -Recurse` — gets a prompt citing *Commands Must Return* (on Bob, it is blocked). In Git Bash `/` mounts every drive and mounted share, so such a scan runs for hours, and `| head` does not stop it. The prompt tells the agent to search a specific directory instead. A deeper path (`find /c/Users/me/project`), `ls /` without `-R` (`ls -r` is reverse order, not recursion), a quoted argument (a search pattern such as `grep -rn "/" src`), a `#` comment, and the remote half of an `ssh` command pass. This check is wired in every build, whatever packs or rules you excluded: it rides inside `git-gate` with no flag of its own, because a new flag or verb would reach machines still running an older hook shim, which would refuse it — and Claude Code blocks a tool call whose gate fails.
43
44
  - Any other command passes untouched.
@@ -10,7 +10,7 @@ One of the [OpenCode plugins](opencode-plugins.md). [Environment variables](envi
10
10
 
11
11
  **What it does.** Enforces the safety rules at the tool boundary — before a tool call runs, not after. High-confidence patterns only, so legitimate work is not caught.
12
12
 
13
- - **Blocks** writes to private-key and credential files and writes whose content carries a credential (*Sealed Secrets*; an install built `--no-secrets-gate` carries `Secrets gate: off` in its `AGENT.md`, read once at load, and both stop blocking), catastrophic shell commands such as deleting the filesystem root and every command that would really lose data — a protected branch deleted or force-pushed, an unpushed unmerged branch deleted, a protected branch force-moved off commits nothing else holds, uncommitted work discarded, a stash dropped, files git cannot restore removed — judged against the repository's state, the same decisions the Claude/Bob hook makes (*Deletion Is Deliberate*; `reflog expire` and `gc --prune` are left to the `permission.bash` ask; an install built `--no-deletion-gate` carries `Deletion gate: off` in its `AGENT.md`, which the plugin reads once at load and then stops blocking real loss — catastrophic commands still block), a recursive scan rooted at a whole filesystem such as `find /` (*Commands Must Return* — search a specific directory instead), and any change — write, move, rename, delete — under a wiki folder declared `protected` in `geneseed-wiki.jsonc`, and writes to a check listed in the project's `.geneseed/protected-checks.txt` (*External Gate* — see [Hooks](../concepts/hooks.md)).
13
+ - **Blocks** writes to private-key and credential files and writes whose content carries a credential (*Sealed Secrets*; an install built `--no-secrets-gate` carries `Secrets gate: off` in its `AGENT.md`, read once at load, and both stop blocking), catastrophic shell commands such as deleting the filesystem root and every command that would really lose data — a protected branch deleted or force-pushed, an unpushed unmerged branch deleted, a protected branch force-moved off commits nothing else holds, unpushed commits abandoned by `reset --hard`, uncommitted work discarded other than by `reset --hard`, a stash dropped, files git cannot restore removed — judged against the repository's state, the same decisions the Claude/Bob hook makes (*Deletion Is Deliberate*; `reflog expire` and `gc --prune` are left to the `permission.bash` ask; an install built `--no-deletion-gate` carries `Deletion gate: off` in its `AGENT.md`, which the plugin reads once at load and then stops blocking real loss — catastrophic commands still block), a recursive scan rooted at a whole filesystem such as `find /` (*Commands Must Return* — search a specific directory instead), and any change — write, move, rename, delete — under a wiki folder declared `protected` in `geneseed-wiki.jsonc`, and writes to a check listed in the project's `.geneseed/protected-checks.txt` (*External Gate* — see [Hooks](../concepts/hooks.md)).
14
14
  - **Warns** (logged, allowed) on `.env` writes.
15
15
  - **Speed-bumps** the *first* write to `user-rules.md` or a memory file: it is refused once, naming *Persist Insight* — whether something is a standing rule or a fact to remember is your call, settled through the `rule` skill. The re-issued write goes through.
16
16
 
@@ -22,7 +22,7 @@ Most of the harness is *asked*. A few rules, where one mistake is expensive and
22
22
 
23
23
  On Claude Code and OpenClaude the hooks never block silently. They make the host **ask you**, with the reason shown:
24
24
 
25
- - **Real data loss**: a force-push or delete of `main`/`master`/the remote's default branch, force-moving one off commits nothing else holds, `branch -D` of an unpushed unmerged branch, `reset --hard`/`checkout --`/`clean -f` and the like over uncommitted work, a stash dropped, the reflog pruned, an `rm` of files git cannot restore. Judged against the repository's real state, so the same command on a clean tree passes; when the gate cannot tell, it asks. Wired in every build.
25
+ - **Real data loss**: a force-push or delete of `main`/`master`/the remote's default branch, force-moving one off commits nothing else holds, `branch -D` of an unpushed unmerged branch, a `reset --hard` abandoning unpushed commits, `checkout --`/`clean -f` and the like over uncommitted work (`reset --hard` over it passes), a stash dropped, the reflog pruned, an `rm` of files git cannot restore. Judged against the repository's real state, so the same command on a clean tree passes; when the gate cannot tell, it asks. Wired in every build.
26
26
  - **Commit and push**: every `git commit` and `git push`, including chained one-liners and `git -C <path>`. This question belongs to the **process** pack. Build without it and the git gate stays wired. Only this question goes away, and destructive git is still caught.
27
27
  - **A scan of a whole filesystem**: a recursive `find`, `du`, `grep -r`, `rg` or `Get-ChildItem -Recurse` rooted at `/`, `/c` or `C:\`, which in Git Bash walks every drive for hours. Wired in every build.
28
28
  - **Credentials**: a write or edit whose content looks like a real key (AWS, GitHub, Anthropic, Slack, or a private-key block), anywhere except a `.env` file.
@@ -15,7 +15,7 @@ What a Claude Code or OpenClaude install writes, piece by piece. The **Kind** wo
15
15
  | `user-rules.md` | Your own standing rules, read every session. Seeded empty | asked | 0 until you write in it | Leave it empty |
16
16
  | `PROFILE.md` | Who you are and how you like answers pitched. Fill it with the `profile` skill | asked | 0 until you fill it | Leave the seeded text untouched |
17
17
  | context hook | At session start, and again after a resume or compaction, puts your repo's README, CONTRIBUTING and a listing of `docs/` in front of the agent, with your rules, profile, memory index and wiki | automatic | Your docs, capped at 16 KB a file and 48 KB a session | `geneseed exclude add <folder>` silences every hook in that folder |
18
- | git-gate hook | Before each shell command: asks you before any `git commit` or `git push`, and before a command that would really lose data — a force-push to `main`, a `reset --hard` over uncommitted work, `branch -D` of an unpushed branch, an `rm` git cannot undo | enforced | ~14 ms per shell call | The commit/push question goes away if you build without the **process** pack; the destructive-git question has no switch except `geneseed exclude` |
18
+ | git-gate hook | Before each shell command: asks you before any `git commit` or `git push`, and before a command that would really lose data — a force-push to `main`, a `reset --hard` that abandons unpushed commits, `branch -D` of an unpushed branch, an `rm` git cannot undo | enforced | ~14 ms per shell call | The commit/push question goes away if you build without the **process** pack; the destructive-git question has no switch except `geneseed exclude` |
19
19
  | rule-gate hook | Before each write or edit: asks you when the file would carry a credential-shaped string (outside `.env`), or when the agent writes to `user-rules.md` or memory | enforced | ~14 ms per write | `geneseed exclude add <folder>` |
20
20
  | learn hook | When the agent finishes a reply, and before compaction, distils durable facts from the recent conversation into `memory/`. Off until you set `GENESEED_LLM` to a model command such as `claude -p` | automatic | One model call each time it runs, once enabled | Leave `GENESEED_LLM` unset (the default) |
21
21
  | `skills/` | Written procedures you call by name, such as `brainstorm` or `plan` | on demand | ~4.1k tokens for names and descriptions; a skill's body loads only when called | No per-skill switch; `geneseed uninstall` removes them all |
@@ -2,5 +2,5 @@
2
2
  "_comment": "version is a human-readable release label only; the canonical identity of an installed harness is the source fingerprint stamped in .geneseed-version (see `harness version`), not this string.",
3
3
  "theme": "neutral",
4
4
  "product": "Geneseed",
5
- "version": "4.9.3"
5
+ "version": "4.9.4"
6
6
  }
@@ -74,7 +74,8 @@ const GIT_GATE_RE = /\bgit\b[^\n]*\b(?:commit|push)\b/;
74
74
 
75
75
  // Law IV's boundary half lives in `./loss.mjs`: it asks only when a command would really lose
76
76
  // data — a protected branch deleted or force-pushed, an unpushed unmerged branch deleted,
77
- // uncommitted work discarded on a tree that holds some, a stash dropped while one exists, the
77
+ // unpushed commits abandoned by `reset --hard`, uncommitted work discarded (other than by
78
+ // `reset --hard`, user decision 2026-10-18) on a tree that holds some, a stash dropped while one exists, the
78
79
  // reflog or unreachable commits pruned, a removal of what git cannot restore — and fails closed
79
80
  // when it cannot tell (user decision, 2026-10-17, replacing the form-only `DESTRUCTIVE_GIT_RE`).
80
81
  // `LOSS_CANDIDATE_RE` is the cheap prefilter: an ordinary command fails it and never reaches the
package/js/hosts/loss.mjs CHANGED
@@ -10,7 +10,9 @@
10
10
  * - a protected branch (`main`, `master`, a remote's default) deleted or force-pushed, local
11
11
  * or remote;
12
12
  * - `branch -D x` while x carries a commit no remote ref and no HEAD reaches;
13
- * - uncommitted work discarded (`reset --hard`, `checkout -f`/`-- <paths>`, `switch -f`/
13
+ * - `reset --hard <target>` abandoning commits HEAD reaches and neither <target> nor a remote
14
+ * does — the uncommitted work it discards passes (user decision, 2026-10-18);
15
+ * - uncommitted work discarded (`checkout -f`/`-- <paths>`, `switch -f`/
14
16
  * `--discard-changes`, `restore` beyond `--staged` alone, `clean -f`) — and only when the
15
17
  * tree actually holds such work (for `clean`, what `git clean -n` says it would remove);
16
18
  * - only where a protected branch is concerned (user ruling, 2026-10-17): `checkout <paths>`
@@ -332,6 +334,25 @@ function branchAct(rest, dir) {
332
334
  return null;
333
335
  }
334
336
 
337
+ /**
338
+ * `git reset --hard [<target>]` — asks only when it would abandon COMMITS: one HEAD reaches that
339
+ * neither `<target>` nor any remote ref does. The uncommitted work it discards passes: what is in
340
+ * progress may break, what exists must be protected (user decision, 2026-10-18). No target is
341
+ * HEAD itself, which abandons nothing.
342
+ */
343
+ function resetHardAct(rest, dir) {
344
+ const dd = rest.indexOf('--');
345
+ const target = (dd < 0 ? rest : rest.slice(0, dd)).find((a) => !a.startsWith('-'));
346
+ if (target === undefined) return null;
347
+ if (target.includes(OPAQUE)) unverified(`the target \`${target.replace(OPAQUE, '')}\` expands in the shell`);
348
+ repoRoot(dir);
349
+ const t = git(dir, ['rev-parse', '-q', '--verify', `${target}^{commit}`]);
350
+ if (t.status !== 0) unverified(`cannot resolve ${target} to a commit`);
351
+ const r = git(dir, ['rev-list', '-n1', 'HEAD', '--not', t.stdout.trim(), '--remotes']);
352
+ if (r.status !== 0) unverified(`cannot compare HEAD with ${target}: ${firstLine(r.stderr)}`);
353
+ return r.stdout.trim() ? `\`git reset --hard ${target}\` would abandon commits no remote and not ${target} holds` : null;
354
+ }
355
+
335
356
  /** `git clean` — forced, it asks when `git clean -n` with the same selection lists anything. */
336
357
  function cleanAct(rest, dir) {
337
358
  const forced = rest.some((a) => a === '--force' || hasShort(a, 'f'));
@@ -373,7 +394,7 @@ function gitAct(args, dir, always) {
373
394
  const act = () => {
374
395
  switch (verb) {
375
396
  case 'reset':
376
- return rest.includes('--hard') ? dirty(d, [], '`git reset --hard`') : null;
397
+ return rest.includes('--hard') ? resetHardAct(rest, d) : null;
377
398
  case 'checkout': {
378
399
  const forced = rest.some((a) => a === '--force' || hasShort(a, 'f')) && dirty(d, [], '`git checkout --force`');
379
400
  if (forced) return forced;
@@ -35,7 +35,7 @@ import { isFile, readText } from '../lib/fs.mjs';
35
35
  * console draws it on the cost chart, so it lives here, once, where both read it. Raise it by
36
36
  * exactly the measured overrun and say why in that test's docblock, which keeps its history.
37
37
  */
38
- export const CARRIER_CEILING = { full: 62_219, lean: 39_088 };
38
+ export const CARRIER_CEILING = { full: 62_219, lean: 39_111 };
39
39
 
40
40
  /**
41
41
  * A frontmatter value as the host shows it. The emit writes descriptions JSON-escaped
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "Every field here is argued and gated in tests/unit/package_manifest.test.mjs. `files`, `bin` and `engines` each have a test that fails when reality drifts from this file; `version` mirrors harness.config.json and is gated too. Read that docblock before editing this one.",
3
3
  "name": "geneseed",
4
- "version": "4.9.3",
4
+ "version": "4.9.4",
5
5
  "description": "A portable, theme-able harness you implant once and use everywhere to grow a disciplined AI coding agent.",
6
6
  "keywords": [
7
7
  "ai",
@@ -72,21 +72,21 @@ governs the goal).
72
72
  Real data loss and any irreversible, financial, privilege-changing or
73
73
  outward-facing act (publishing, sending data to a third party) need explicit
74
74
  confirmation bound to that act, or a durable authorization covering it; one
75
- yes never stretches to the next. Real loss: deleting what version control
76
- cannot restore (untracked, modified, unpushed or unmerged work), discarding
77
- uncommitted changes, rewriting, deleting or force-moving main, master or the
78
- default branch, or erasing reflog/gc data. Classify every action as Create,
79
- Read, Update, or Delete and tier by reversibility: what git restores — an
80
- edit, removing tracked clean files, force-pushing your own branch, removing a
81
- worktree — runs freely.
75
+ yes never stretches to the next. Real loss: deleting what git cannot restore
76
+ (untracked, modified, unpushed or unmerged work), discarding uncommitted work,
77
+ rewriting, deleting or force-moving main, master or the default branch, or
78
+ pruning reflog/gc. Classify every action as Create, Read, Update, or Delete
79
+ and tier by reversibility: what git restores — an edit, tracked clean files,
80
+ force-pushing your own branch, a worktree — runs freely, as does
81
+ `reset --hard` over a dirty tree.
82
82
  <!-- LEAN:else -->
83
83
  Real data loss — deleting untracked, modified, unpushed, unmerged work,
84
- discarding uncommitted work, deleting or force-moving main, master or the
85
- default branch, pruning reflog/gc — and any irreversible, financial,
86
- privilege-changing or outward-facing act need explicit confirmation bound to
87
- that act, or a durable authorization covering it; one yes never stretches to
88
- the next. What git restores runs freely: tracked clean files, a worktree,
89
- force-pushing your own branch.
84
+ discarding uncommitted work (except `reset --hard`), deleting or
85
+ force-moving main, master or the default branch, pruning reflog/gc — and any
86
+ irreversible, financial, privilege-changing or outward-facing act need
87
+ explicit confirmation bound to that act, or a durable authorization covering
88
+ it; one yes never stretches to the next. What git restores runs freely:
89
+ tracked clean files, a worktree, force-pushing your own branch.
90
90
  <!-- LEAN:end -->
91
91
 
92
92
  ### {{LAW:surface-failures}} Surface Failures