@tech-leads-club/harness-toolkit 0.15.3 → 0.15.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/docs/log.md CHANGED
@@ -16,6 +16,7 @@ newest first. For what landed in which npm release, see `CHANGELOG.md` at the re
16
16
  ## 2026-09-12
17
17
 
18
18
  - **AD-139** — lessonsView joins ProviderPort, closing the one dispatcher that bypassed it ([/decisions/ad-139.md](/decisions/ad-139.md))
19
+ - **AD-140** — an unrecognized shell wrapper can no longer hide a forced push ([/decisions/ad-140.md](/decisions/ad-140.md))
19
20
 
20
21
  ## 2026-09-11
21
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tech-leads-club/harness-toolkit",
3
- "version": "0.15.3",
3
+ "version": "0.15.4",
4
4
  "type": "module",
5
5
  "description": "Multi-provider agent steering: gates, follow-up, handoff, policy",
6
6
  "keywords": [
@@ -145,6 +145,26 @@ function pathArgs(args: ShellWord[]): ShellWord[] {
145
145
  return args.filter((word) => !word.text.startsWith("-") && word.text !== "");
146
146
  }
147
147
 
148
+ // hazard: `verbOf` strips a path down to its basename before comparing (`/usr/bin/git` → `git`) — a
149
+ // literal-text scan that skips the same normalization would deny `git push --force` but allow the identical
150
+ // command run as `/usr/bin/git push --force`, which is exactly the shape a path-qualifying wrapper produces.
151
+ function namesGit(text: string): boolean {
152
+ return (text.split("/").pop() ?? text) === "git";
153
+ }
154
+
155
+ // why: `verbOf` only sees through a wrapper this gate already knows by name, and `WRAPPERS` cannot list
156
+ // every proxy or shim an operator's shell might inject ahead of the real command — discovered live when one
157
+ // such wrapper carried a force push straight past this rule. Matching the whole segment, not the resolved
158
+ // head verb, means an unrecognized wrapper can delay `git` but not hide it.
159
+ function forcedGitPush(segment: ShellSegment): boolean {
160
+ const texts = segment.words.map((word) => word.text);
161
+ return (
162
+ texts.some(namesGit) &&
163
+ texts.includes("push") &&
164
+ texts.some((text) => text === "--force" || text === "-f")
165
+ );
166
+ }
167
+
148
168
  function checkShell(input: FloorInput): Decision {
149
169
  const command = input.command;
150
170
  if (!command) {
@@ -165,6 +185,17 @@ function checkShell(input: FloorInput): Decision {
165
185
  }
166
186
 
167
187
  for (const segment of segments) {
188
+ // why: checked before `verbOf` resolves anything — a wrapper this gate does not recognize can delay
189
+ // which word `verbOf` treats as the head, or exhaust the segment before a head is ever found, but it
190
+ // cannot remove `git`/`push`/`--force` from the segment's own words.
191
+ if (forcedGitPush(segment)) {
192
+ return denial(
193
+ "history-rewrite",
194
+ "`git push --force` discards remote commits that are not in your history. Use --force-with-lease, which refuses when the remote moved.",
195
+ "force push",
196
+ );
197
+ }
198
+
168
199
  const head = verbOf(segment.words);
169
200
  if (!head) {
170
201
  continue;
@@ -187,17 +218,6 @@ function checkShell(input: FloorInput): Decision {
187
218
  return denial("machine-control", `\`${verb}\` controls the machine, not the project.`, verb);
188
219
  }
189
220
 
190
- if (verb === "git" && args.some((word) => word.text === "push")) {
191
- const forced = args.some((word) => word.text === "--force" || word.text === "-f");
192
- if (forced) {
193
- return denial(
194
- "history-rewrite",
195
- "`git push --force` discards remote commits that are not in your history. Use --force-with-lease, which refuses when the remote moved.",
196
- "force push",
197
- );
198
- }
199
- }
200
-
201
221
  const destructive = DESTRUCTIVE_VERBS.has(verb) || isMkfs(verb);
202
222
  if (!destructive) {
203
223
  continue;